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=0
  • amplitude('10'), exactProbs()['10'], initialState: '10' all follow q0-left
Every gate method is immutable — it returns a new Circuit and leaves the original unchanged. Safe to compose, branch, and reuse without copying.

Circuit constructor

new Circuit(n: number) Circuit.device(name: DeviceName) // new Circuit(device.qubits)
ParameterTypeDescription
nnumberNumber of qubits. All qubits start in |0⟩.
const c = new Circuit(3)
const aria = Circuit.device('aria-1')  // new Circuit(25)

State inspection

MethodReturnsDescription
statevector(opts?)Map<bigint, Complex>Full sparse amplitude map. All basis states with |a|² ≥ 1e-10.
amplitude(bitstring)ComplexSingle amplitude ⟨bitstring|ψ⟩.
probability(bitstring)number|amplitude|².
exactProbs()Record<string, number>All non-negligible probabilities. No sampling variance.
stateAsString()stringHuman-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()numberCircuit 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

circuit.compose(other: Circuit): Circuit

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.

circuit.bind(values: Record<string, number>): Circuit circuit.params // string[] — sorted unbound parameter 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

GateMethodDescription
Hh(q)Hadamard
Xx(q)Pauli-X (NOT)
Yy(q)Pauli-Y
Zz(q)Pauli-Z
Ss(q)Phase — Rz(π/2)
S†si(q) / sdg(q)S-inverse
Tt(q)T gate — Rz(π/4)
T†ti(q) / tdg(q)T-inverse
√Xv(q) / srn(q)Square-root NOT
√X†vi(q) / srndg(q)√X-inverse
Rxrx(θ, q)X-axis rotation
Ryry(θ, q)Y-axis rotation
Rzrz(θ, q)Z-axis rotation
U1 / Pu1(λ, q) / p(λ, q)Phase gate. p = Qiskit 1.0+ name
U2u2(φ, λ, q)Two-parameter single-qubit unitary
U3u3(θ, φ, λ, q)General single-qubit unitary
VZvz(θ, q)VirtualZ — Rz alias
Iid(q)Identity

Two-qubit gates

GateMethodDescription
CNOT / CXcnot(c, t) / cx(c, t)Controlled-X
SWAPswap(q0, q1)SWAP
CYcy(c, t)Controlled-Y
CZcz(c, t)Controlled-Z
CHch(c, t)Controlled-H
CRx / CRy / CRzcrx(θ, c, t)Controlled rotations
CU1cu1(λ, c, t)Controlled-U1
CU2 / CU3cu2(φ, λ, c, t)Controlled-U2 / U3
CS / CTcs(c, t) / ct(c, t)Controlled-S / T
XXxx(θ, q0, q1)Ising XX interaction
YYyy(θ, q0, q1)Ising YY interaction
ZZzz(θ, q0, q1)Ising ZZ interaction
XYxy(θ, q0, q1)XY interaction
iSWAPiswap(q0, q1)iSWAP
√iSWAPsrswap(q0, q1)Square-root iSWAP

Three-qubit gates

GateMethodDescription
CCXccx(c0, c1, t)Toffoli
CSWAPcswap(c, q0, q1)Fredkin
C√SWAPcsrswap(c, q0, q1)Controlled-√SWAP

Custom unitary gate

circuit.unitary(matrix: number[][] | Complex[][], ...qubits): Circuit

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

GateMethodDescription
GPIgpi(φ, q)Single-qubit rotation on Bloch equator
GPI2gpi2(φ, q)Half-angle GPI
MSms(φ₀, φ₁, 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

circuit.run() / circuit.statevector()
O(2ⁿ) sparse — BigInt keys

Exact amplitudes. Practical up to ~20 qubits for dense circuits. Sparse circuits (Bell, GHZ) remain fast at any width.

MPS / Tensor Network

circuit.runMps({ shots, maxBond? })
O(n·χ²)

Low-entanglement circuits. GHZ-50 runs in milliseconds at χ=2. Use larger χ for general circuits.

Density Matrix

circuit.dm({ noise? })
O(4ⁿ) sparse

Mixed-state and noisy simulation. Exact per-gate depolarizing channels, no Monte Carlo. Practical up to n=12.

Clifford Stabilizer

circuit.runClifford({ shots, noise? })
O(n²) tableau

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

MethodDescription
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 } })
ParameterTypeDescription
p1numberSingle-qubit depolarizing error probability per gate.
p2numberTwo-qubit depolarizing error probability.
pMeasnumberBit-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

ExportSignatureDescription
qftqft(n)Quantum Fourier Transform circuit on n qubits.
iqftiqft(n)Inverse QFT.
grovergrover(n, oracle)Grover's search. Oracle receives a Circuit and returns it with the phase-kick applied.
phaseEstimationphaseEstimation(precision, U, nTargets)QPE. Returns a circuit with precision+nTargets qubits.
trottertrotter(n, H, t, steps?, order?)Trotterized Hamiltonian simulation. order=1 (Lie-Trotter) or order=2 (Suzuki).
qaoaqaoa(n, edges, gamma, beta)QAOA Max-Cut circuit.
maxCutHamiltonianmaxCutHamiltonian(n, edges)Max-Cut Hamiltonian as PauliTerms[] for vqe.
realAmplitudesrealAmplitudes(n, reps)Ry + CNOT ansatz. Real amplitudes. Has .paramCount.
efficientSU2efficientSU2(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

ExportSignatureDescription
vqevqe(circuit, hamiltonian)⟨ψ(θ)|H|ψ(θ)⟩ — exact statevector, no sampling noise.
gradientgradient(ansatz, H, params)Analytic gradient via parameter-shift rule. 2N vqe() calls for N params.
minimizeminimize(ansatz, H, initParams, opts?)Gradient descent. Returns { params, energy, steps, converged }.
minimize optionTypeDescription
lrnumberLearning rate. Default 0.1.
stepsnumberMax gradient steps. Default 500.
tolnumberConvergence 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

PauliOp.from(terms: PauliTerm[]): PauliOp
MethodReturnsDescription
.add(other)PauliOpOperator addition.
.scale(c)PauliOpScalar multiplication.
.mul(other)PauliOpOperator 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

OpenQASM 2 / 3
fromQASM · toQASM
IonQ JSON
fromIonQ · toIonQ
Quil 2.0
fromQuil · toQuil
Qiskit (Python)
fromQiskit · toQiskit
Cirq (Python)
fromCirq · toCirq
ket JSON
fromJSON · toJSON
Q#
toQSharp
Amazon Braket
toBraket
CUDA-Q
toCudaQ
TensorFlow Quantum
toTFQ
Quirk JSON
toQuirk
LaTeX (quantikz)
toLatex
// 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
DeviceVendorQubitsp1 (1Q)p2 (2Q)pMeas
forte-1IonQ360.010%0.20%0.20%
aria-1IonQ250.030%0.50%0.40%
harmonyIonQ110.100%1.50%1.00%
h2-1Quantinuum560.0019%0.11%0.10%
h1-1Quantinuum200.0018%0.097%0.23%
ibm_torinoIBM1330.020%0.30%1.00%
ibm_sherbrookeIBM1270.024%0.74%1.35%
ibm_brisbaneIBM1270.024%0.76%1.35%

IONQ_DEVICES additionally exposes nativeGates for compilation and validation.

Visualization

MethodReturnsDescription
draw()stringASCII diagram. Gates on non-conflicting qubits share a column.
toSVG()stringSelf-contained SVG. No external fonts or stylesheets.
blochSphere(q)stringSVG Bloch sphere for qubit q via partial trace.
blochAngles(q){ theta, phi }Bloch sphere angles for qubit q.
toLatex()stringquantikz 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.

new CliffordSim(n: number)
MethodReturnsDescription
h(q), s(q), x(q), cnot(c, t) …voidApply Clifford gates. Mutates in place (unlike Circuit).
measure(q, rand)0 | 1Measure 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.

ket — quantum circuits in JavaScript
Built by @dmvjs · MIT License · 1,301 tests
Open playground → GitHub