Docs Playground
dmvjs / ket GitHub @kirkelliott / ket npm

ket

Quantum circuit simulation for TypeScript · four exact backends · 14 interchange formats · zero dependencies · 1,850 tests · v0.6.1

A general-purpose quantum circuit simulator for TypeScript and JavaScript. Four 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.
1,850 testsAnalytic correctness against known amplitudes, not "doesn't crash".
Zero dependencies154 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.

Four 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 — 1ms
MPS / tensorrunMps()O(n·χ²), χ grows on demandLow-entanglement circuits, 50+ qubitsGHZ-127 — 9ms
Density matrixdm()sparse → dense, automaticMixed states and exact noise channelsGHZ-8 + noise — 1ms
CliffordrunClifford()O(n²) tableauClifford circuits, QEC thresholdsGHZ-1024 — 3587ms

The statevector starts as a sparse map and promotes itself to a flat Float64Array past ⅛ 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

Four backends built on four unrelated representations — amplitudes, density matrices, tensor trains, stabilizer tableaux — have to agree. Where a circuit can be expressed in all four, 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: 433msGate-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": 0.5 }, { "gate": "gpi2", "target": 0, "phase": 0 }, ... ] }

Published device profiles IONQ_DEVICES

Every IonQ system ships with its native gate set and published error rates, so a noisy run is one option away — run({ noise: 'forte-1' }).

DeviceQubitsNative gates1Q error2Q errorReadout
aria-125gpi, gpi2, ms, vz0.030%0.50%0.40%
forte-136gpi, gpi2, ms, vz, zz0.010%0.20%0.20%
harmony11gpi, gpi2, ms, vz0.100%1.50%1.00%
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⟩) run across six backends with 4,096 shots each. Lower gate error rates preserve the quadratic amplitude advantage. All results use published device noise models — not real hardware runs.

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
BEST
IonQ Forte-1
Trapped-Ion · 36 qubits
IonQ Forte-1 (all 8 states)94%101
93.90%
P(|101⟩) — target state
IonQ Aria-1
Trapped-Ion · 25 qubits
IonQ Aria-1 (all 8 states)93%101
93.04%
P(|101⟩) — target state
IonQ Harmony
Trapped-Ion · 11 qubits
IonQ Harmony (all 8 states)90%101
90.06%
P(|101⟩) — target state
IBM Torino
Superconducting · 133 qubits
IBM Torino (all 8 states)92%101
91.72%
P(|101⟩) — target state
IBM Sherbrooke
Superconducting · 127 qubits
IBM Sherbrooke (all 8 states)91%101
90.72%
P(|101⟩) — target state
1,024-Qubit GHZ State CliffordSim

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

Scalability

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

QubitsTimeStatevector RAM
64<1ms >16 PB
128<1ms > universe
256<1ms > universe
5122ms > universe
1,0244ms > universe
2,04822ms > universe
4,096100ms > 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

P(|00…0⟩ + |11…1⟩) for an n-qubit GHZ state under depolarizing noise. Bond dimension stays at χ = 2 for all n — fidelity decays from gate errors only, not simulation approximation.

0%25%50%75%100%0255075100127IonQ Forte-1IBM SherbrookeGHZ qubits (n)
IBM Sherbrooke: p₂ = 7.4 × 10⁻³ IonQ Forte-1: p₂ ≈ 10⁻³ Model: depolarizing channel · gate errors only

127-Qubit IBM Sherbrooke χ = 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['ibm_sherbrooke'].noise, }) // → { '000…0': 2.3%, '111…1': 2.1%, … }
127
qubits · noisy · χ = 2
O(n)
memory — not O(2ⁿ)

Scalability — GHZ with Noise

256 shots, IBM Sherbrooke noise model, single-threaded. Time scales linearly with n because χ = 2 throughout the GHZ circuit.

QubitsTimeχStatevector RAM
105ms 216 KB
208ms 216 MB
5016ms 2>16 PB
10034ms 2> universe
12741ms 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.

circuit.runMps({ shots: 1024, noise: DEVICES['forte-1'].noise, workers: 4, // ~4× wall-time speedup seed: 42, })
speedup · 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: 819ms

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.8s
total · JavaScript
O(n·χ²)
scales to 100+ qubits
ket — quantum circuits in JavaScript
Built by @dmvjs
dmvjs/ket on GitHub → npm install @kirkelliott/ket