GitHub - dmvjs/ket: TypeScript quantum circuit simulator. Immutable API, four backends, 14 import/export formats, zero dependencies.

4 min read Original article ↗

Test npm License: MIT

Quantum circuits in TypeScript. Immutable API, four backends, zero dependencies.

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()    // { '00': 0.5, '11': 0.5 }

Playground  ·  Live demos  ·  API docs  ·  Full reference

Install

npm install @kirkelliott/ket

No dependencies, no build step, no Python. Works in Node ≥ 22 and directly in the browser:

<script type="module">
  import { Circuit } from 'https://unpkg.com/@kirkelliott/ket/dist/ket.js'
</script>

Factor a number with Shor's algorithm

Not an oracle mock-up — the full Beauregard circuit, with modular exponentiation decomposed into primitive gates, simulated exactly:

import { shorBeauregard } from '@kirkelliott/ket'

const r = shorBeauregard(15n, { a: 7n })

r.factors  // [5n, 3n]
r.period   // 4n        — 7⁴ ≡ 1 (mod 15), recovered by phase estimation
r.method   // 'quantum' — the circuit really ran
r.qubits   // 19

Nineteen qubits, 0.4 seconds, six lines.

Shor's tries classical shortcuts first, and at these sizes they hit often — an even N, or a base sharing a factor with N, is resolved by gcd without building a circuit. method tells you which happened, so a demo can prove the quantum path ran:

shorBeauregard(15n, { a: 3n }).method  // 'classical-gcd' — no circuit
shorBeauregard(15n, { a: 7n }).method  // 'quantum'
N qubits time
15 19 0.4s
21 23 3.0s
33 27 22.4s
35 27 29.0s

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

Pick a backend, or let ket pick

circuit.simulate({ shots: 1024 })   // routes to the cheapest exact backend
Backend Memory Best for
Statevector sparse → dense, automatic Exact simulation to ~20 qubits
MPS / tensor network O(n·χ²), χ grows on demand Low-entanglement circuits, 50+ qubits
Density matrix sparse → dense, automatic Mixed states and noise
Clifford stabilizer O(n²) Clifford circuits, QEC thresholds

The statevector backend starts sparse and promotes itself to a contiguous Float64Array once a state is more than ⅛ full, so sparse circuits stay cheap and dense ones stop paying for a hash map. MPS bond dimension is exact by default — maxBond is an initial allocation, not a cap, and χ grows as the circuit demands.

GHZ-50 runs in milliseconds on MPS at χ=2. A 127-qubit GHZ samples 1024 shots in 186ms.

Why you can build on it

It is typed, and the types are the source. Written in strict TypeScript, not JavaScript with a .d.ts bolted on afterward. Every qubit index is bounds-checked at gate-construction time — an out-of-range index throws RangeError immediately instead of silently corrupting state 200 gates later.

Immutable. Every gate method returns a new Circuit. Compose, branch, and reuse without defensive copying.

BigInt state indices. No 32-bit overflow at qubit 31, the failure mode that silently corrupts integer-indexed simulators.

1,843 tests. Analytic correctness against known amplitudes — not "doesn't crash." Gate invertibility (U†U = I), backend cross-agreement, BigInt correctness at indices 30/31/40, and full round-trips for every supported import/export format. The sparse and dense statevector kernels are differentially tested against each other gate by gate, over every qubit ordering, so promotion can never change a result.

Zero dependencies. 154 KB minified, total. Nothing to audit but ket.

Performance

ket matches its representation to the circuit instead of committing to one, so the same API is efficient across shapes that usually need different tools.

A statevector starts as a sparse map and promotes itself to a flat Float64Array once it is more than ⅛ full. A GHZ state holds two non-zero amplitudes at any width, so it stays sparse and never touches the other million slots; a depth-4 random circuit fills every amplitude in its first layer, so it moves to the dense kernel once and runs the rest with no allocation at all. The density matrix does the same at 1/32 fill, and MPS bond dimension grows on demand rather than being capped up front.

Measured on Node 24 / Apple silicon, best of 5:

Circuit Representation Time
GHZ-20, statevector sparse 5µs
QFT-16, statevector dense 10.5ms
random-16 depth 4, statevector dense 15.7ms
GHZ-50, MPS χ=2 tensor network milliseconds
GHZ-127, MPS, 1024 shots tensor network 186ms
12-qubit noisy run, 1024 shots dense 0.69s

None of this needs a flag — the thresholds are defaults, adjustable per call via the dense option when you want to trade memory against speed.

Documentation

  • Live demos — QAOA, Grover across hardware, 1,024-qubit GHZ, VQE
  • Playground — run circuits in the browser, nothing to install
  • API docs — every export
  • Full reference — all gates, 14 import/export formats, noise models, device targeting, visualization, QEC

License

MIT