ket
TypeScript quantum circuit simulator. Immutable API, four backends (statevector, MPS, density matrix, Clifford), 14 import/export formats, zero dependencies. Runs in Node.js ≥ 22 and any modern browser.
Install
npm install @kirkelliott/ket
Or load directly in a browser via CDN (no bundler needed):
<script type="module">
import { Circuit } from 'https://unpkg.com/@kirkelliott/ket/dist/ket.min.js'
const bell = new Circuit(2).h(0).cnot(0, 1)
console.log(bell.stateAsString()) // 0.7071|00⟩ + 0.7071|11⟩
</script>
Two bundle flavors ship in the package: ket.js (unminified, for bundlers that tree-shake) and ket.min.js (110 kB, for direct CDN use). The unpkg field points to the minified build.
Quick start
import { Circuit } from '@kirkelliott/ket'
// Build a Bell state
const bell = new Circuit(2).h(0).cnot(0, 1)
bell.draw() // ASCII diagram
bell.stateAsString() // '0.7071|00⟩ + 0.7071|11⟩'
bell.exactProbs() // { '00': 0.5, '11': 0.5 }
// Shot-based sampling
const result = bell
.creg('out', 2)
.measure(0, 'out', 0)
.measure(1, 'out', 1)
.run({ shots: 1000, seed: 42 })
// result.probs → { '00': ~0.5, '11': ~0.5 }
Conventions
All bitstrings use q0 leftmost throughout the entire API — matching Qiskit, Cirq, and textbooks.
'10'→ q0=1, q1=0amplitude('10'),exactProbs()['10'],initialState: '10'all follow q0-left
Circuit and leaves the original unchanged. Safe to compose, branch, and reuse without copying.Circuit constructor
| Parameter | Type | Description |
|---|---|---|
| n | number | Number of qubits. All qubits start in |0⟩. |
const c = new Circuit(3)
const aria = Circuit.device('aria-1') // new Circuit(25)
State inspection
| Method | Returns | Description |
|---|---|---|
| statevector(opts?) | Map<bigint, Complex> | Full sparse amplitude map. All basis states with |a|² ≥ 1e-10. |
| amplitude(bitstring) | Complex | Single amplitude ⟨bitstring|ψ⟩. |
| probability(bitstring) | number | |amplitude|². |
| exactProbs() | Record<string, number> | All non-negligible probabilities. No sampling variance. |
| stateAsString() | string | Human-readable superposition: '0.7071|00⟩ + 0.7071|11⟩'. |
| stateAsArray() | StateEntry[] | Sorted by prob desc. Each entry: { bitstring, re, im, prob, phase }. |
| marginals() | number[] | P(qᵢ=1) for each qubit. |
| blochAngles(q) | { theta, phi } | Single-qubit Bloch sphere angles via partial trace. |
| expectation(pauli) | number | ⟨ψ|P|ψ⟩ for a Pauli string (e.g. 'ZZ', 'XX'). |
| depth() | number | Circuit depth (longest gate-layer path). |
statevector(), exactProbs(), and friends throw on circuits with measurements or unbound parameters. Add a .creg and .run() for measurement-based sampling.Circuit composition
Concatenates two circuits of the same width. Classical registers are merged; custom gate definitions merged (this wins on name conflicts). Mismatched qubit counts throw TypeError.
const prep = new Circuit(2).h(0).cnot(0, 1)
const rot = new Circuit(2).rz(Math.PI / 4, 0).rz(Math.PI / 4, 1)
const full = prep.compose(rot)
Parametric circuits
Gate angles can be symbolic strings. Use .bind() to substitute values. Calling statevector() or any export on a circuit with unbound parameters throws a TypeError listing the missing names.
const ansatz = new Circuit(2)
.ry('theta', 0)
.rz('phi', 0)
.cnot(0, 1)
ansatz.params // ['phi', 'theta']
const bound = ansatz.bind({ theta: Math.PI / 4, phi: 0.1 })
bound.statevector() // runs normally
Gates that accept symbolic angles: rx, ry, rz, vz, u1, p, u2, u3, gpi, gpi2, xx, yy, zz, xy, ms, crx, cry, crz, cu1, cu2, cu3.
Single-qubit gates
| Gate | Method | Description |
|---|---|---|
| H | h(q) | Hadamard |
| X | x(q) | Pauli-X (NOT) |
| Y | y(q) | Pauli-Y |
| Z | z(q) | Pauli-Z |
| S | s(q) | Phase — Rz(π/2) |
| S† | si(q) / sdg(q) | S-inverse |
| T | t(q) | T gate — Rz(π/4) |
| T† | ti(q) / tdg(q) | T-inverse |
| √X | v(q) / srn(q) | Square-root NOT |
| √X† | vi(q) / srndg(q) | √X-inverse |
| Rx | rx(θ, q) | X-axis rotation |
| Ry | ry(θ, q) | Y-axis rotation |
| Rz | rz(θ, q) | Z-axis rotation |
| U1 / P | u1(λ, q) / p(λ, q) | Phase gate. p = Qiskit 1.0+ name |
| U2 | u2(φ, λ, q) | Two-parameter single-qubit unitary |
| U3 | u3(θ, φ, λ, q) | General single-qubit unitary |
| VZ | vz(θ, q) | VirtualZ — Rz alias |
| I | id(q) | Identity |
Two-qubit gates
| Gate | Method | Description |
|---|---|---|
| CNOT / CX | cnot(c, t) / cx(c, t) | Controlled-X |
| SWAP | swap(q0, q1) | SWAP |
| CY | cy(c, t) | Controlled-Y |
| CZ | cz(c, t) | Controlled-Z |
| CH | ch(c, t) | Controlled-H |
| CRx / CRy / CRz | crx(θ, c, t) | Controlled rotations |
| CU1 | cu1(λ, c, t) | Controlled-U1 |
| CU2 / CU3 | cu2(φ, λ, c, t) | Controlled-U2 / U3 |
| CS / CT | cs(c, t) / ct(c, t) | Controlled-S / T |
| XX | xx(θ, q0, q1) | Ising XX interaction |
| YY | yy(θ, q0, q1) | Ising YY interaction |
| ZZ | zz(θ, q0, q1) | Ising ZZ interaction |
| XY | xy(θ, q0, q1) | XY interaction |
| iSWAP | iswap(q0, q1) | iSWAP |
| √iSWAP | srswap(q0, q1) | Square-root iSWAP |
Three-qubit gates
| Gate | Method | Description |
|---|---|---|
| CCX | ccx(c0, c1, t) | Toffoli |
| CSWAP | cswap(c, q0, q1) | Fredkin |
| C√SWAP | csrswap(c, q0, q1) | Controlled-√SWAP |
Custom unitary gate
Matrix must be 2ᴺ × 2ᴺ where N is the number of qubits. Entries can be plain number (real) or { re, im } complex objects. Supported in statevector, density matrix, and MPS (1 and 2-qubit only). Throws in runClifford.
const SWAP = [[1,0,0,0],[0,0,1,0],[0,1,0,0],[0,0,0,1]]
circuit.unitary(SWAP, 0, 1)
const S = [
[{ re: 1, im: 0 }, { re: 0, im: 0 }],
[{ re: 0, im: 0 }, { re: 0, im: 1 }],
]
circuit.unitary(S, 0)
IonQ native gates
| Gate | Method | Description |
|---|---|---|
| GPI | gpi(φ, q) | Single-qubit rotation on Bloch equator |
| GPI2 | gpi2(φ, q) | Half-angle GPI |
| MS | ms(φ₀, φ₁, q0, q1) | Mølmer-Sørensen entangling gate |
Use circuit.checkDevice('aria-1') to validate a circuit against a device's native gate set before calling circuit.toIonQ().
Simulation backends
Statevector
Exact amplitudes. Practical up to ~20 qubits for dense circuits. Sparse circuits (Bell, GHZ) remain fast at any width.
MPS / Tensor Network
Low-entanglement circuits. GHZ-50 runs in milliseconds at χ=2. Use larger χ for general circuits.
Density Matrix
Mixed-state and noisy simulation. Exact per-gate depolarizing channels, no Monte Carlo. Practical up to n=12.
Clifford Stabilizer
Clifford-only circuits. H, S, X, Y, Z, CNOT, CZ, CY, SWAP. Non-Clifford gates throw at runtime. 1,024 qubits in milliseconds.
All backends accept an initialState option to start from an arbitrary computational basis state:
circuit.run({ initialState: '110' })
circuit.runMps({ shots: 1000, initialState: '110' })
circuit.statevector({ initialState: '110' })
Measurements & classical control
| Method | Description |
|---|---|
| creg(name, size) | Add a classical register. |
| measure(q, reg, bit) | Measure qubit q into classical register bit. |
| reset(q) | Reset qubit to |0⟩. |
| if(reg, val, fn) | Apply gates conditionally on classical register value. |
| barrier(...qubits) | Scheduling hint — no-op in simulation, emits barrier in QASM. |
| defineGate(name, circ) | Register a named sub-circuit gate. |
| gate(name, ...qubits) | Apply a registered named gate. |
| decompose() | Inline all named gates back to primitives. |
const c = new Circuit(2)
.creg('out', 2)
.h(0).cnot(0, 1)
.measure(0, 'out', 0)
.measure(1, 'out', 1)
const result = c.run({ shots: 1000, seed: 42 })
// result.probs → { '00': ~0.5, '11': ~0.5 }
// Conditional gate
const teleport = new Circuit(3)
.if('out', 1, q => q.x(2))
.if('out', 2, q => q.z(2))
Noise models
All three stochastic backends accept a noise option — either a named device or explicit parameters:
// Named device profile
circuit.run({ noise: 'forte-1' })
circuit.runClifford({ shots: 10000, noise: 'aria-1' })
circuit.dm({ noise: 'h2-1' })
// Custom depolarizing parameters
circuit.run({ noise: { p1: 0.001, p2: 0.005, pMeas: 0.004 } })
| Parameter | Type | Description |
|---|---|---|
| p1 | number | Single-qubit depolarizing error probability per gate. |
| p2 | number | Two-qubit depolarizing error probability. |
| pMeas | number | Bit-flip probability per measured bit (SPAM error). |
Noiseless circuits take the fast path — zero overhead. The density matrix backend applies exact per-gate depolarizing channels (no sampling).
Built-in algorithms
| Export | Signature | Description |
|---|---|---|
| qft | qft(n) | Quantum Fourier Transform circuit on n qubits. |
| iqft | iqft(n) | Inverse QFT. |
| grover | grover(n, oracle) | Grover's search. Oracle receives a Circuit and returns it with the phase-kick applied. |
| phaseEstimation | phaseEstimation(precision, U, nTargets) | QPE. Returns a circuit with precision+nTargets qubits. |
| trotter | trotter(n, H, t, steps?, order?) | Trotterized Hamiltonian simulation. order=1 (Lie-Trotter) or order=2 (Suzuki). |
| qaoa | qaoa(n, edges, gamma, beta) | QAOA Max-Cut circuit. |
| maxCutHamiltonian | maxCutHamiltonian(n, edges) | Max-Cut Hamiltonian as PauliTerms[] for vqe. |
| realAmplitudes | realAmplitudes(n, reps) | Ry + CNOT ansatz. Real amplitudes. Has .paramCount. |
| efficientSU2 | efficientSU2(n, reps) | Ry·Rz + CNOT ansatz. Full SU(2). Has .paramCount. |
// Grover's search — find |101⟩ in 3 qubits
const circ = grover(3, (c) => {
c = c.x(1)
c = c.h(2)
c = c.ccx(0, 1, 2)
c = c.h(2)
c = c.x(1)
return c
})
circ.exactProbs() // { '101': ~0.945, ... }
// QPE — estimate phase of T gate (φ = 1/8)
const qpe = phaseEstimation(3,
(c, ctrl, pow, tgts) => c.cu1(Math.PI * pow / 4, ctrl, tgts[0]), 1)
qpe.run({ shots: 1000, seed: 42, initialState: '0001' })
VQE & optimization
| Export | Signature | Description |
|---|---|---|
| vqe | vqe(circuit, hamiltonian) | ⟨ψ(θ)|H|ψ(θ)⟩ — exact statevector, no sampling noise. |
| gradient | gradient(ansatz, H, params) | Analytic gradient via parameter-shift rule. 2N vqe() calls for N params. |
| minimize | minimize(ansatz, H, initParams, opts?) | Gradient descent. Returns { params, energy, steps, converged }. |
| minimize option | Type | Description |
|---|---|---|
| lr | number | Learning rate. Default 0.1. |
| steps | number | Max gradient steps. Default 500. |
| tol | number | Convergence tolerance (gradient L2 norm). Default 1e-6. |
const H = [{ coeff: 0.5, ops: 'ZI' }, { coeff: 0.5, ops: 'IZ' }]
const ansatz = realAmplitudes(2, 2) // paramCount = 6
const { energy, params, converged } = minimize(
ansatz, H, Array(ansatz.paramCount).fill(0.1), { lr: 0.2, steps: 500 }
)
Pauli algebra
| Method | Returns | Description |
|---|---|---|
| .add(other) | PauliOp | Operator addition. |
| .scale(c) | PauliOp | Scalar multiplication. |
| .mul(other) | PauliOp | Operator product with phase tracking. |
| .commutator(other) | PauliOp | [A, B] = AB − BA. |
| .toTerms() | PauliTerm[] | Convert back to terms array. Throws if not Hermitian. |
const X = PauliOp.from([{ coeff: 1, ops: 'X' }])
const Y = PauliOp.from([{ coeff: 1, ops: 'Y' }])
X.mul(Y) // iZ
X.commutator(Y) // 2iZ
const H1 = PauliOp.from([{ coeff: 1, ops: 'ZI' }, { coeff: 1, ops: 'IZ' }])
const H2 = PauliOp.from([{ coeff: 0.5, ops: 'XX' }])
vqe(circuit, H1.add(H2).toTerms())
Import / Export
// Import
const c = Circuit.fromQASM(`
OPENQASM 2.0;
qreg q[2];
h q[0];
cx q[0],q[1];
`)
// Export
c.toQASM() // OpenQASM 2.0 string
c.toIonQ() // IonQ JSON for hardware submission
c.toCirq() // Cirq Python code
c.toLatex() // quantikz LaTeX environment
Devices
import { DEVICES, IONQ_DEVICES, Circuit } from '@kirkelliott/ket'
const forte = DEVICES['forte-1'] // { qubits: 36, noise: { p1, p2, pMeas } }
// Validate before submitting
circuit.checkDevice('aria-1') // throws if incompatible
circuit.toIonQ() // safe to call after check
| Device | Vendor | Qubits | p1 (1Q) | p2 (2Q) | pMeas |
|---|---|---|---|---|---|
| forte-1 | IonQ | 36 | 0.010% | 0.20% | 0.20% |
| aria-1 | IonQ | 25 | 0.030% | 0.50% | 0.40% |
| harmony | IonQ | 11 | 0.100% | 1.50% | 1.00% |
| h2-1 | Quantinuum | 56 | 0.0019% | 0.11% | 0.10% |
| h1-1 | Quantinuum | 20 | 0.0018% | 0.097% | 0.23% |
| ibm_torino | IBM | 133 | 0.020% | 0.30% | 1.00% |
| ibm_sherbrooke | IBM | 127 | 0.024% | 0.74% | 1.35% |
| ibm_brisbane | IBM | 127 | 0.024% | 0.76% | 1.35% |
IONQ_DEVICES additionally exposes nativeGates for compilation and validation.
Visualization
| Method | Returns | Description |
|---|---|---|
| draw() | string | ASCII diagram. Gates on non-conflicting qubits share a column. |
| toSVG() | string | Self-contained SVG. No external fonts or stylesheets. |
| blochSphere(q) | string | SVG Bloch sphere for qubit q via partial trace. |
| blochAngles(q) | { theta, phi } | Bloch sphere angles for qubit q. |
| toLatex() | string | quantikz LaTeX environment with proper \ctrl{}, \targ{}, etc. |
const bell = new Circuit(2).h(0).cnot(0, 1)
console.log(bell.draw())
// q0: ─H──●─
// │
// q1: ─────⊕─
// Write SVG files
import fs from 'fs'
fs.writeFileSync('bell.svg', bell.toSVG())
fs.writeFileSync('state.svg', bell.blochSphere(0))
Serialization
Lossless round-trip through JSON. All operation types preserved: gates, measure, reset, if, named sub-circuits.
const json = circuit.toJSON()
const restored = Circuit.fromJSON(json)
// Or pass a parsed object
const restored2 = Circuit.fromJSON(JSON.parse(json))
CliffordSim
CliffordSim is exported directly for researchers who need more control than runClifford — custom decoders, syndrome extraction, mid-circuit readout.
| Method | Returns | Description |
|---|---|---|
| h(q), s(q), x(q), cnot(c, t) … | void | Apply Clifford gates. Mutates in place (unlike Circuit). |
| measure(q, rand) | 0 | 1 | Measure qubit. rand is a uniform random in [0, 1). |
| stabilizerGenerators() | string[] | Current generators as signed Pauli strings, e.g. '+XX', '-ZZ'. |
import { CliffordSim } from '@kirkelliott/ket'
const sim = new CliffordSim(2)
sim.h(0); sim.cnot(0, 1)
sim.stabilizerGenerators() // ['+XX', '+ZZ']
// Inject error
sim.x(0)
sim.stabilizerGenerators() // ['+XX', '-ZZ'] ← syndrome bit flipped
const outcome = sim.measure(0, Math.random())
Quantum error correction
Sweep physical error rate to find the logical threshold:
// Surface code threshold curve
for (const p2 of [0.001, 0.005, 0.01, 0.02, 0.05]) {
const result = surfaceCode.runClifford({ shots: 10000, noise: { p2 } })
console.log(p2, result.probs['0']) // logical error rate vs physical
}
For detailed threshold analysis, use CliffordSim directly to extract stabilizer syndromes after each noise injection step without going through the full circuit runner.