Docs Playground
dmvjs / ket GitHub @kirkelliott / ket npm

ket

Quantum circuit simulation for TypeScript · seven exact backends · 14 interchange formats · zero dependencies · 2,456 tests · v0.9.1

A general-purpose quantum circuit simulator for TypeScript and JavaScript. Seven backends, 14 import/export formats, and no dependencies — install it and a Bell state is three lines away.

npm install @kirkelliott/ket

Node ≥ 22, or straight from a CDN in the browser. No Python, no build step, no toolchain.

import { Circuit } from '@kirkelliott/ket' const bell = new Circuit(2).h(0).cnot(0, 1)
bell.draw()
q0: ─H──●── │ q1: ────⊕──
bell.stateAsString()
0.7071|00⟩ + 0.7071|11⟩
bell.exactProbs()
{'11': 0.4999999999999999, '00': 0.4999999999999999}
ImmutableEvery gate returns a new Circuit. Compose and branch without defensive copying.
Types are the sourceStrict TypeScript, not JS with a .d.ts bolted on.
Bounds-checkedBad qubit index throws RangeError at construction, not 200 gates later.
BigInt indicesNo 32-bit overflow at qubit 31 — the failure mode that silently corrupts.
2,456 testsAnalytic correctness against known amplitudes, not "doesn't crash".
Zero dependencies215 KB minified, total. Nothing to audit but ket.
One call, right backend simulate()

You don't have to know which representation fits your circuit. simulate() inspects it and routes to the cheapest exact backend. Same call, three very different circuits:

circuit.simulate({ shots: 1024 }) // → Distribution, with .backend telling you what ran
CircuitQubitsRouted toTime
GHZ-8, Clifford-only gates8clifford3ms
QFT-10, non-Clifford phases10statevector2ms
40 qubits, non-Clifford phases40mps4ms

Exact in every case — routing changes the cost, never the answer.

Seven backends, one API

ket matches its representation to the circuit instead of committing to one, so the same API stays efficient across shapes that usually need different tools. Pick a backend explicitly when you want to.

BackendCallMemoryBest forMeasured
Statevectorrun()sparse → dense, automaticExact amplitudes to ~20 qubitsGHZ-20, 1024 shots — <1ms
MPS / tensorrunMps()O(n·χ²), χ grows on demandLow-entanglement circuits, 50+ qubitsGHZ-127, 1024 shots — 10ms
Density matrixdm()sparse → dense, automaticMixed states and exact noise channelsGHZ-8 + noise — <1ms
CliffordrunClifford()O(n²) tableauClifford circuits, QEC thresholdsGHZ-1024 — 4ms evolve + 57ms/shot
Stabilizer rankrunStabilizerRank()O(20.228t·n²/8)Clifford+T beyond statevector width100q, 50 T — 5.6s build + 43ms/shot
Tensor networkamplitudeByContraction()O(2width), width = treewidth not nWide, shallow circuits at any qubit count400q depth 4 — 362ms
Pauli pathpauliPathExpectation()O(terms), set by the light cone⟨O⟩ without ever building a state120q QAOA layer — 28ms

The statevector starts as a sparse map and promotes itself to a flat Float64Array past 1/64 fill. MPS bond dimension χ is exact by default and grows on demand — maxBond is an initial allocation, not a cap.

How you know it's right

Backends built on unrelated representations — amplitudes, density matrices, tensor trains, stabilizer tableaux — have to agree. Where a circuit can be expressed in several, ket checks that they do, and the suite is built on analytic expectations rather than snapshots. These numbers are computed while this page builds:

GHZ-6 — same circuit, four representations, 200,000 shots for the sampled ones
OutcomeStatevector (exact)Density matrixMPSClifford
|000000⟩0.5000000.5000000.4998700.499870
|111111⟩0.5000000.5000000.5001300.500130
Density matrix: bit-identical MPS: within 1.3e-4 (sampling) Clifford: within 1.3e-4 (sampling)

The density matrix backend is exact, so it matches to floating-point precision. MPS and Clifford sample, so they land within shot noise of the analytic answer — which is the correct result, not a tolerance that was widened until it passed.

Unitarity, from the circuit's own matrix

Every circuit can hand you its full 2ⁿ×2ⁿ unitary via circuitMatrix(). For a five-gate circuit mixing Clifford and non-Clifford rotations, U†U − I is:

max |U†U − I| = 4.44e-16

Machine epsilon. The suite asserts this for every gate in the library, not just this one circuit.

Honest about limits

A serious simulator tells you when its answer is approximate. MPS runs set truncated the moment a physically significant singular value is discarded, and report the peak bond dimension actually reached:

const d = circuit.runMps({ shots: 1024 }) d.truncated // false — nothing was thrown away d.peakChi // the χ this circuit really needed d.backend // which representation ran

Exact by default. Approximation is something you opt into with truncErr or maxChi, and it is always flagged.

Talks to the rest of the ecosystem 14 formats

A simulator you can't get circuits into or out of is a dead end. ket reads and writes the formats the field actually uses, with round-trips covered by the test suite for every one of them.

OpenQASM 2 / 3import · export
IonQ JSONimport · export
Quil 2.0import · export
Qiskitimport · export
Cirqimport · export
ket JSONimport · export
Qiskit Qobjimport
Q#export
pyQuilexport
Amazon Braketexport
CUDA-Qexport
TensorFlow Qexport
Quirkexport
LaTeX quantikzexport
QASM round-trip — export, re-import, compare distributions
OPENQASM 2.0; include "qelib1.inc"; qreg q[3]; h q[0]; cx q[0],q[1]; t q[2]; ...
Distributions identical: yesRound-tripped: H · CNOT · T · Rz · CCX
Shor's algorithm, actually decomposed shorBeauregard()

Not an oracle mock-up. The full Beauregard circuit, modular exponentiation compiled down to primitive gates, simulated exactly — then the period recovered from phase estimation by continued fractions.

import { shorBeauregard } from '@kirkelliott/ket' const r = shorBeauregard(15n, { a: 7n })
r.factors[5n, 3n]
r.period4n
r.method'quantum'
r.qubits19
Wall time: 478msGate-decomposed: no oracle

It tells you when it cheated

At these sizes classical shortcuts hit often — an even N, or a base sharing a factor with N, falls to gcd with no circuit at all. method reports which path ran, so a demo can prove the quantum one did.

shorBeauregard(15n, { a: 3n }).method // 'classical-gcd' — no circuit built shorBeauregard(15n, { a: 7n }).method // 'quantum'

This does not reach cryptographic sizes — no classical simulator does. What you get is the real circuit, exactly simulated, at sizes you can inspect.

IonQ hardware, first class compile()

IonQ's native gate set is built in, not bolted on. gpi, gpi2, ms, and vz are first-class gates you can write directly, compile(device) transpiles an abstract circuit into them, and toIonQ() emits submit-ready JSON.

Transpile to native gates

A textbook Bell pair, rewritten into what the trap actually executes — GPI2 and MS, with virtual-Z frame rotations that cost nothing on hardware.

new Circuit(2).h(0).cnot(0, 1).compile('forte-1')
q0: ─Rz(π/2)──GPI2(0)──Rz(π/2)──GPI2(π/2)──MS(0,0)──GPI2(-π/2)──Rz(-π/2)── │ q1: ─GPI2(π)───────────────────────────────MS(0,0)────────────────────────
Native ops: 8Validated: checkDevice()

Submit-ready JSON

toIonQ() produces the API payload directly — no adapter, no Python bridge.

{ "format": "ionq.circuit.v0", "qubits": 2, "circuit": [ { "gate": "rz", "target": 0, "rotation": 1.5707963267948966 }, { "gate": "gpi2", "target": 0, "phase": 0 } ... ] }

Submit it, and get results back runIonQ

The client uses the global fetch, so submission adds no dependency. Algorithm circuits pass through toIonQBasis() first — QFT and modular-exponentiation circuits are built almost entirely from cu1, which IonQ has no direct representation for.

const shor = shorCircuit(15n, 7n, 3) const { id } = await submitIonQ(shor.toIonQBasis().toIonQ(), { apiKey, target: 'simulator', noise: { model: 'ideal' }, }) const job = await getIonQJob(id, { apiKey }) // 'ready' means queued await getIonQResults(job, { apiKey }) // behind results_url await cancelIonQJob(id, { apiKey }) // release the slot

Run on IonQ's simulator, this factors 15: the counting register returns 0, 2, 4 and 6 at 25 % each — period 4, giving 15 = 3 × 5 — with the work register on the orbit of 7 mod 15. The discrete-logarithm circuit recovers its exponent the same way.

Device profiles IONQ_DEVICES

Devices currently accepting jobs, so a noisy run is one option away — run({ noise: 'forte-1' }). The Source column is load-bearing: vendor-published means IonQ’s own r_1q/r_2q parameters, estimated means it is not their figure. Every readout rate is estimated — no vendor here publishes one — and Forte’s post-2025-09 model is unpublished, so these are the earlier parameters. Regenerate with scripts/refresh-ionq-devices.ts.

DeviceQubitsNative gates1Q error2Q errorReadoutSource
forte-1
forte-enterprise-1 — same figures
36gpi, gpi2, zz0.027%0.49%0.20%vendor-published
enterprise: estimated
Everything below runs at build time

No screenshots and no mock data — every chart, circuit diagram, and number on the rest of this page is produced by executing ket when the page is generated.

QAOA Max-Cut — Quantum Optimization

A 4-cycle graph has two optimal bipartitions: {0,2} vs {1,3} and {1,3} vs {0,2}, each cutting all 4 edges. QAOA p=1 finds them — the two dominant peaks are the correct answers.

0123measurement outcomes (all 16 states)26%010126%1010{0, 2}{1, 3}
const edges = [[0,1],[1,2],[2,3],[3,0]] const circuit = qaoa(4, edges, [Math.PI / 4], [0.15 * Math.PI]) circuit.exactProbs() // → { '0101': 26.5%, '1010': 26.5%, … }
Expected cut: 2.95 edges Random baseline: 2.0 edges Optimal: 4.0 edges Top states: '1010' 26.5% · '0101' 26.5%
Circuit Drawing API — toSVG() · blochSphere(q)

Circuit SVG toSVG()

Self-contained SVG string — no external fonts or stylesheets. Same column layout as draw(). Embed directly in HTML, save to a file, or use in notebooks.

q0: q1: H
new Circuit(2).h(0).cnot(0, 1).toSVG({ theme: 'dark' }) // → self-contained SVG string
Format: SVG string Background: self-contained Also: toLatex()

Bloch Sphere blochSphere(q)

300×300 SVG for a single qubit's state using cavalier projection. Partial-traces over all other qubits — works on any circuit, including entangled states.

|0⟩ |1⟩ |+⟩ |i⟩
|0⟩
ground state
|0⟩ |1⟩ |+⟩ |i⟩
|+⟩
H gate
|0⟩ |1⟩ |+⟩ |i⟩
|1⟩
X gate
|0⟩ |1⟩ |+⟩ |i⟩
|−⟩
H · Z
|0⟩ |1⟩ |+⟩ |i⟩
|+i⟩
H · S
|0⟩ |1⟩ |+⟩ |i⟩
|T⟩
H · T
Grover's Search — Hardware Fidelity

The same 3-qubit Grover circuit (target: |101⟩), noiseless and under IonQ Forte-1’s published error rates, 4,096 shots each. Bars are all eight basis states; the labelled one is the search target. A depolarizing model from IonQ’s published rates — not a hardware run.

Circuit toSVG()

q0: q1: q2: H H H X H H X H H H X X X H H X X X H H H X H H X H H H X X X H H X X X H H H
Ideal
Statevector · no noise
Ideal (all 8 states)95%101
94.53%
P(|101⟩) — target state
HARDWARE
IonQ Forte-1
Trapped-Ion · 36 qubits
IonQ Forte-1 (all 8 states)93%101
93.48%
P(|101⟩) — target state

Stabilizer Tableau Heatmap Gottesman-Knill

A 1,024-qubit GHZ state built with one H gate + 1,023 CNOTs. Classically infeasible with statevector simulation (21024 amplitudes), but the Gottesman-Knill theorem lets us track it exactly in O(n²) space. Each row is a stabilizer generator; each column is a qubit.

Row 0 (bright stripe): X⊗¹⁰²⁴ — all 1,024 qubits correlated via X.  ·  Rows 1–1023 (diagonal): ZZ at adjacent qubit positions.

Qubits: 1,024 Gates: 1,024 (1H + 1,023 CNOT) Build time: 6ms Stabilizers: 1,024 generators published

Scalability

O(n²) per gate — statevector needs 2n complex numbers, impossible beyond ~50 qubits.

QubitsTimeStatevector RAM
64<1ms >16 PB
128<1ms > universe
2561ms > universe
5121ms > universe
1,0247ms > universe
2,04825ms > universe
4,096117ms > universe

Measurement Verification

Every qubit collapses to the same value — perfect quantum correlation across all 1,024 qubits.

|ψ⟩ = (1/√2)(|000…0⟩ + |111…1⟩) ← 1,024 qubits → Qubit 0 measured: 1 Qubit 511 measured: 1 (same) Qubit 1023 measured: 1 (same)
1,024
qubits — all correlated
6ms
to build & run
Matrix Product State Trajectories runMps()

The trajectory-MPS backend simulates noisy circuits at 127+ qubits by representing the quantum state as a product of rank-3 tensors. Bond dimension χ tracks entanglement, not qubit count — a 127-qubit GHZ state maintains χ = 2 throughout, making it as cheap as a 3-qubit simulation. Noisy trajectories are embarrassingly parallel: pass workers: N for N× speedup.

GHZ Fidelity vs Qubit Count noise model

How often an n-qubit GHZ run finishes with no gate error at all. The circuit is one H and n − 1 CNOTs, and each gate survives with probability 1 − p, so the curve is that product as n grows. At n = 127 roughly half of all runs come back clean.

Landing on one of the two ideal answers is rarer still — 46.9 % ± 1.5 % over 4,096 shots — because an error usually moves the result off |0…0⟩ and |1…1⟩ rather than back onto them.

All of this is the hardware, not the simulator: bond dimension stays at χ = 2 for every n, so ket adds no approximation of its own. Forte-1 holds 36 qubits, so past n = 36 the curve extrapolates its published error rates to a machine that does not exist.

0%25%50%75%100%0255075100127IonQ Forte-1GHZ qubits (n)
IonQ Forte-1: p₁ = 2.7e-04, p₂ = 4.9e-03 (vendor-published) GHZ-127: 52% of runs error-free · 46.9% on an ideal outcome Model: depolarizing channel · gate errors only

127-Qubit GHZ State χ = 2

Statevector simulation of a 127-qubit circuit would require 2127 complex amplitudes — roughly 1020× more RAM than all data ever created. MPS stores O(n · χ²) numbers — linear in qubit count.

import { Circuit, DEVICES } from '@kirkelliott/ket' let c = new Circuit(127).h(0) for (let i = 0; i < 126; i++) c = c.cnot(i, i+1) const result = c.runMps({ shots: 256, seed: 42, noise: DEVICES['forte-1'].noise, }) // → { '000…0': 24.2%, '111…1': 20.3%, … }
127
qubits · noisy · χ = 2
O(n)
memory — not O(2ⁿ)
That circuit runs in this page. Scroll here with JavaScript enabled to execute it.

Scalability — GHZ with Noisepublished

256 shots, IonQ Forte-1 error rates as the depolarizing model, single-threaded. Time scales linearly with n because χ = 2 throughout the GHZ circuit.

QubitsTimeχStatevector RAM
103ms 216 KB
202ms 216 MB
504ms 2>16 PB
1009ms 2> universe
12710ms 2> universe

Multi-Core Speedup

Each noisy trajectory is an independent simulation — distribute across OS threads with workers: N. Requires built bundle and Node.js ≥ 22. Measured at 3.8× with 4 workers and 7.1× with 8 on a 16-core machine (50 qubits, 32k shots).

circuit.runMps({ shots: 1024, noise: DEVICES['forte-1'].noise, workers: 4, // ~3.8× wall-time speedup seed: 42, })
3.8×
measured · 4 workers
O(n·χ²)
memory — linear in n
VQE at Scale — 12-Qubit Heisenberg XXX minimizeMps()

Variational quantum eigensolver on the 12-qubit antiferromagnetic Heisenberg chain (H = Σ XiXi+1 + YiYi+1 + ZiZi+1) using MPS-based parameter-shift gradients. A 12-qubit statevector needs 128 KB — manageable — but 50+ qubits needs petabytes. MPS VQE scales to hundreds of qubits at low entanglement.

Energy Convergence MPS gradient

Parameter-shift gradients computed via MPS — same accuracy as statevector but O(n·χ²) memory. Each step: 2 × 36 MPS evaluations.

6.35-2.58-11.51-20.44020406080gradient descent step
Qubits: 12 Params: 36 Steps: 80 Initial E: 6.354 Final E: -18.4590 Wall time: 915ms

Ground State area law

Bond entropies of the optimized ground state — the area-law profile confirms the Heisenberg chain has limited entanglement, making MPS the ideal representation.

Heisenberg ground state — area law0.00.51.0bond
import { minimizeMps, realAmplitudes } from '@kirkelliott/ket' const ansatz = realAmplitudes(12, 2) const result = minimizeMps(ansatz, heisenbergH, params, { maxBond: 8, // MPS bond dimension }) // → { energy: -18.4590, converged: … }
12
qubits · open chain
-18.459
VQE ground energy
0.9s
total · JavaScript
O(n·χ²)
scales to 100+ qubits
ket — quantum circuits in JavaScript
Built by @dmvjs
dmvjs/ket on GitHub → npm install @kirkelliott/ket