Quantum API
Everything on this page comes from quantum-forge/quantum. It documents quantum-forge 3.0. Coming from 2.x? See Migrating to 3.0.
import {
// Handles
quantum,
Quantum,
isQuantum,
QUANTUM_HANDLE,
clearQuantumCache,
observeQuantum,
// Joint operations
measure,
forcedMeasure,
probabilities,
densityMatrix,
measureWhen,
forcedMeasureWhen,
probabilityWhen,
phaseRotate,
// Recording
QuantumRecorder,
LegacyQuantumRecorder,
// Loading and build info
ensureLoaded,
startBackgroundLoad,
isReady,
useQuantumForgeBuild,
setWasmBasePath,
getWasmBasePath,
getVersion,
getMaxDimension,
getMaxQudits,
getMaxStateSize,
getWasmMemoryBytes,
getAttribution,
registerServiceWorker,
// Raw WASM access
getModule,
getQuantumForge,
OP,
} from "quantum-forge/quantum";
import type {
QuantumValue,
GateOptions,
QuantumPredicate,
SerializedQuantumPredicate,
QuantumGateEvent,
QuantumMeasureEvent,
QuantumObserver,
QuantumLog,
QuantumLogEntry,
QuantumProperty,
Predicate,
OpCode,
BatchOp,
BatchResult,
} from "quantum-forge/quantum";Creating a property
quantum(values)
function quantum<V extends QuantumValue>(values: readonly V[]): Quantum<V>;
function quantum(dimension: number): Quantum<number>;Creates a quantum property declared by the values it can take. The dimension is the number of values, and the property starts at the first one.
import { ensureLoaded, quantum } from "quantum-forge/quantum";
await ensureLoaded(); // once, before the first quantum() call
const color = quantum(["red", "green", "blue"]); // Quantum<"red" | "green" | "blue">, starts "red"
const alive = quantum([false, true]); // Quantum<boolean>, starts false
const die = quantum(3); // Quantum<number>, values 0, 1, 2Values are strings, numbers or booleans, each declared once. TypeScript infers the literal union from the array, so no as const is needed.
quantum() throws:
- before
ensureLoaded()has finished, - on fewer than two values, including
quantum(1)and a one-element array (RangeError), - on a non-integer dimension or a non-finite number (
RangeError), - on a value that is not a string, number or boolean (
TypeError), - on a value declared twice,
- past the loaded build's maximum dimension: 3 in the Qutrit Edition, 2 in the Qubit Edition.
quantum() is the only way to make a handle. The Quantum constructor is private and throws a TypeError if called.
QuantumValue
type QuantumValue = string | number | boolean;The Quantum handle
class Quantum<V extends QuantumValue = QuantumValue>One quantum property. A bare Quantum means Quantum<QuantumValue>, so a field typed Quantum holds any handle, named or numeric. Write Quantum<boolean> to narrow it.
Wherever a method takes a value, pass a declared value or its index in the declaration. color.is("green") and color.is(1) build the same predicate. When the declared values are themselves numbers, a number is read as a declared value first: on quantum([1, 0]), is(0) means the value 0 at index 1. A value that is neither throws a RangeError.
Properties
| Member | Type | Description |
|---|---|---|
id | number | Unique per quantum() call, increasing, never reused |
values | readonly V[] | Declared values in order. Index 0 is the starting value |
dimension | number | Number of declared values |
disposed | boolean | true once dispose() has run |
raw | QuantumProperty | The WASM property behind the handle, for the batch API. See Raw WASM access |
Gates
Every gate returns the handle, so calls chain. A gate that reads only this property is evolution. A gate that touches a second property, or whose when predicate reads another property, is an interaction and leaves the two entangled.
| Method | Alias | Description |
|---|---|---|
hadamard(fraction?, opts?) | superpose() | Spread the property evenly across every value |
inverseHadamard(opts?) | Undo hadamard() | |
cycle(fraction?, opts?) | next(), and flip() on two values | Move to the next value, wrapping |
shift(fraction?, opts?) | previous() | Move to the previous value, wrapping |
clock(fraction?, opts?) | phase() | Rotate the phase of each value by its index |
x(fraction?, opts?) | Pauli X, the same gate as shift() | |
y(fraction?, opts?) | Pauli Y, two values only | |
z(fraction?, opts?) | Pauli Z, the same gate as clock() | |
swap(other, opts?) | Exchange the states of two properties | |
iSwap(other, fraction, opts?) | iSwap between two properties. The fraction is required |
An alias is the same call under another name. It never adds behavior, and observers and the recorder report it under the physics name.
Signatures:
hadamard(fraction?: number | GateOptions, opts?: GateOptions): this; // also cycle, shift, clock, x, y, z
superpose(fraction?: number | GateOptions, opts?: GateOptions): this; // also next, previous, phase, flip
inverseHadamard(opts?: GateOptions): this;
swap(other: Quantum<any>, opts?: GateOptions): this;
iSwap(other: Quantum<any>, fraction: number, opts?: GateOptions): this;Fractions. Every gate except inverseHadamard, swap and iSwap takes an optional first fraction. Leave it out for the discrete gate. Any other number runs the continuous version, and 0.5 is the square root of the gate. Exactly 1 also runs the discrete gate, but write the call without a fraction when that is what you mean. A fraction that is NaN or infinite throws a RangeError.
Options. The options object can follow the fraction or stand in its place: b.flip({ when: [a.is(true)] }) and b.flip(0.5, { when: [a.is(true)] }) both work.
Throws.
flip()andy()on a property that does not have exactly two values. Usenext()orcycle()on a qutrit.swap()andiSwap()passed the handle they are called on, or a property with a different number of values.- Any gate whose
whenpredicate reads the property the gate acts on (forswapandiSwap, either of the two). - Any gate on a disposed handle, or with a predicate on a disposed handle.
- An interaction that would put more qudits in one entangled state than the build allows. The message names the method and the limit.
GateOptions
interface GateOptions {
/** The gate acts only on the part of the state where every predicate holds. */
when?: QuantumPredicate[];
}Predicates
| Method | Returns | Description |
|---|---|---|
is(value) | QuantumPredicate<V> | Holds where this property equals value |
isNot(value) | QuantumPredicate<V> | Holds where this property does not equal value |
Pass predicates to a gate in { when: [...] }, or to the *When free functions. Several predicates must all hold.
b.flip({ when: [a.is(true)] }); // CNOT
b.phase({ when: [a.is(true)] }); // CZ
c.next({ when: [a.is(true), b.isNot(0)] }); // two conditionsinterface QuantumPredicate<V extends QuantumValue = QuantumValue> {
readonly property: Quantum<V>; // the property this predicate tests
readonly value: V; // the declared value being tested
readonly index: number; // the index of that value
readonly isEqual: boolean; // true for is(), false for isNot()
}Measurement and inspection
| Method | Returns | Description |
|---|---|---|
measure() | V | Measure, collapsing this property and every property entangled with it. Returns the declared value |
forcedMeasure(value) | V | Measure with the outcome forced to value. For replays and tests |
probability(value) | number | Probability that a measurement gives value. No collapse |
probabilities() | Array<{ value: V; probability: number }> | Probability of every declared value, in declaration order. No collapse |
The handle stays usable after measure(). It holds a definite value and can go back into superposition with another gate.
forcedMeasure() throws when value has zero probability in the current state, and leaves the state unchanged.
Lifecycle
| Method | Returns | Description |
|---|---|---|
dispose() | void | End the property's life |
[Symbol.dispose]() | void | Same as dispose(), so a using declaration disposes at the end of the scope |
dispose() measures the property first, which collapses every property entangled with it. If the qudit is then alone in its state, it is reset and cached for the next quantum() of the same dimension. If it still shares a state with other properties, it is destroyed, which takes it out of that state. Calling dispose() a second time does nothing. Every other call on a disposed handle throws Quantum property #N was disposed and can no longer be used. dispose() also works on a frozen handle.
function rollDie(): number {
using die = quantum(3).superpose();
return die.measure();
} // die.dispose() runs hereusing needs TypeScript 5.2 or newer and "ESNext.Disposable" in the lib of your tsconfig.json. Projects made by npx quantum-forge-engine init have both. Code that never writes using needs neither: the shipped types declare Symbol.dispose themselves.
See Lifecycle for patterns, and the Entities API for how EntityManager disposes handles on removal.
Diagnostics
| Method | Returns | Description |
|---|---|---|
numActiveQudits() | number | Qudits in the shared state this property belongs to |
stateVectorSize() | number | Basis amplitudes in that shared state vector |
Compare numActiveQudits() against getMaxQudits() before an interaction that could grow a shared state past the limit, rather than catching the throw.
Joint operations
Free functions for operations that span several properties, or that have no single target.
| Function | Returns | Description |
|---|---|---|
measure(...props) | QuantumValue[] | Measure several properties in one step. One declared value per property, in argument order |
forcedMeasure(props, values) | QuantumValue[] | Measure with each outcome forced. For replays and tests |
probabilities(...props) | Array<{ values: QuantumValue[]; probability: number }> | Joint probabilities. No collapse |
densityMatrix(...props) | Array<{ row: QuantumValue[]; col: QuantumValue[]; real: number; imag: number }> | Reduced density matrix, rows and columns labelled with declared values. No collapse |
measureWhen(preds) | boolean | Measure whether every predicate holds, collapsing the state to agree |
forcedMeasureWhen(preds, outcome) | boolean | The same with the outcome forced. For replays and tests |
probabilityWhen(preds) | number | Probability that every predicate holds at once. No collapse |
phaseRotate(angle, { when }) | void | Rotate the phase of the part of the state where every predicate holds by angle radians |
import { measure, densityMatrix, phaseRotate } from "quantum-forge/quantum";
const [va, vb] = measure(a, b);
phaseRotate(Math.PI, { when: [a.is(true), b.is(true)] });
const rho = densityMatrix(a, b);
const entry = rho.find(
(e) => e.row[0] === true && e.row[1] === false && e.col[0] === false && e.col[1] === true,
);
const relativePhase = entry ? Math.atan2(entry.imag, entry.real) : 0;measure, forcedMeasure, probabilities and densityMatrix throw when called with no properties. forcedMeasure() throws when values has a different length from props.
All three forced calls throw when the forced outcome has zero probability in the current state. forcedMeasure() and forcedMeasureWhen() check before they measure and leave the state unchanged when they throw.
phaseRotate() throws on an angle that is NaN or infinite.
Handle utilities
isQuantum(x): x is Quantum<any>
True when x is a handle made by quantum(). A spread copy of a handle is not a handle. The engine's EntityManager uses this to find handles on an entity.
QUANTUM_HANDLE
The registry symbol Symbol.for("quantum-forge.quantum-handle"). Every handle carries it as a non-enumerable property with the value true. isQuantum() reads it, and two copies of the core package recognize each other's handles through it.
clearQuantumCache(): void
Destroys every cached qudit. Live handles are not affected. Tests use it, and a game can call it between scenes to free memory.
observeQuantum(observer): () => void
Attaches an observer that sees every quantum operation after it succeeds, and returns a function that detaches it. QuantumRecorder is built on this. Most games never need it.
interface QuantumObserver {
onCreate?(prop: Quantum<any>): void;
onGate?(event: QuantumGateEvent): void;
onMeasure?(event: QuantumMeasureEvent): void;
onDispose?(prop: Quantum<any>, value: QuantumValue): void;
}Events name handles by id and values by index, and use the WASM operation names: superpose() reports "hadamard", flip() and next() report "cycle". A gate's fraction is undefined when the discrete gate ran.
interface SerializedQuantumPredicate { id: number; index: number; isEqual: boolean }
type QuantumGateEvent =
| { op: "hadamard" | "cycle" | "shift" | "clock" | "x" | "y" | "z";
target: number; fraction: number | undefined; predicates: SerializedQuantumPredicate[] }
| { op: "inverse_hadamard"; target: number; predicates: SerializedQuantumPredicate[] }
| { op: "swap"; targets: [number, number]; predicates: SerializedQuantumPredicate[] }
| { op: "i_swap"; targets: [number, number]; fraction: number; predicates: SerializedQuantumPredicate[] }
| { op: "phase_rotate"; angle: number; predicates: SerializedQuantumPredicate[] };
type QuantumMeasureEvent =
| { op: "measure"; targets: number[]; outcomes: number[] }
| { op: "forced_measure"; targets: number[]; forced: number[]; outcomes: number[] }
| { op: "measure_predicate"; predicates: SerializedQuantumPredicate[]; outcome: number }
| { op: "forced_measure_predicate"; predicates: SerializedQuantumPredicate[]; forced: number; outcome: number };Delivery rules:
- An observer that throws does not stop the others from seeing the event, and the error never reaches the game call. It is reported through
reportError(), orconsole.errorwhere the runtime lacks it. - Events arrive in the order the operations ran. An operation run from inside an observer is delivered after the current event has reached every observer.
- Gate and measurement events are frozen, and every observer gets the same object.
onCreateandonDisposereceive the live handle. - The measurement
dispose()makes is not reported throughonMeasure.onDisposecarries its value instead. - Operations run through
handle.raware never reported.
QuantumRecorder
Records every operation on quantum() handles into a JSON log and replays it into fresh handles. See Recording & Replay for the guide.
import { QuantumRecorder, quantum, measure } from "quantum-forge/quantum";
const recorder = new QuantumRecorder();
recorder.startRecording();
const a = quantum([false, true]);
const b = quantum([false, true]);
a.superpose();
b.flip({ when: [a.is(true)] });
measure(a, b);
const saved = QuantumRecorder.serialize(recorder.stopRecording());
const handles = QuantumRecorder.replay(QuantumRecorder.deserialize(saved));
const replayedA = handles.get(a.id);Constructor
new QuantumRecorder()Takes no arguments. Passing one, as 2.x code did with a manager, throws a TypeError that points to LegacyQuantumRecorder.
Methods
| Method | Returns | Description |
|---|---|---|
startRecording() | void | Begin recording with an empty log. Calling it while recording restarts |
stopRecording() | QuantumLog | Stop and return the log |
isRecording() | boolean | True between startRecording() and stopRecording() |
getLog() | QuantumLog | A copy of the log so far. Works while recording |
QuantumRecorder.replay(log) | Map<number, Quantum<any>> | Replay into fresh handles. Keys are the ids in the log |
QuantumRecorder.serialize(log) | string | JSON text |
QuantumRecorder.deserialize(text) | QuantumLog | Parse and validate JSON text |
replay() makes a fresh handle for every create entry and replays every measurement as a forced measurement with the recorded outcome. The returned map holds the handles still live at the end of the log; handles the log disposes are not in it. A recorder that is running during replay() records what the replay did.
deserialize() throws on text that is not JSON, a missing or unsupported version, and any malformed entry. replay() validates the same way, and also throws on a log that lists untrackedIds or references an id with no earlier create entry. No handle survives a throw from replay().
Start recording before creating the handles you want to replay. An operation on a handle created earlier logs a warning once per handle and adds its id to untrackedIds, and replay() refuses that log.
QuantumLog
interface QuantumLog {
version: 1;
entries: QuantumLogEntry[];
/** Present only when the log touched handles created before startRecording(). */
untrackedIds?: number[];
}
type QuantumLogEntry =
| { op: "create"; id: number; values: QuantumValue[] }
| { op: "dispose"; id: number; outcome: number }
| { op: "hadamard" | "cycle" | "shift" | "clock" | "x" | "y" | "z";
target: number; fraction?: number; predicates: SerializedQuantumPredicate[] }
| { op: "inverse_hadamard"; target: number; predicates: SerializedQuantumPredicate[] }
| { op: "swap"; targets: [number, number]; predicates: SerializedQuantumPredicate[] }
| { op: "i_swap"; targets: [number, number]; fraction: number; predicates: SerializedQuantumPredicate[] }
| { op: "phase_rotate"; angle: number; predicates: SerializedQuantumPredicate[] }
| { op: "measure"; targets: number[]; outcomes: number[] }
| { op: "forced_measure"; targets: number[]; forced: number[]; outcomes: number[] }
| { op: "measure_predicate"; predicates: SerializedQuantumPredicate[]; outcome: number }
| { op: "forced_measure_predicate"; predicates: SerializedQuantumPredicate[]; forced: number; outcome: number };Entries name handles by id, values by index, and gates by their physics name. A gate's fraction is left out when the discrete gate ran.
LegacyQuantumRecorder
The 2.x recorder under a new name. It records QuantumPropertyManager operations and replays logs made by the 2.x QuantumRecorder. It is deprecated along with the manager and goes in 4.0. See QuantumPropertyManager (deprecated).
Module loading
ensureLoaded(): Promise<void>
Loads the WASM module. Await it once before the first quantum() call. Later calls resolve at once.
startBackgroundLoad(logger?: LoggerInterface): void
Starts loading in the background via requestIdleCallback, or setTimeout where that is missing. Call it early for faster perceived startup, then await ensureLoaded() where you need the module.
isReady(): boolean
Non-blocking check. true once the module has loaded.
useQuantumForgeBuild(name: string): void
Selects a named WASM build, such as "qubit" for the Qubit Edition. Call it before ensureLoaded(). In a page it loads from /quantum-forge-<name>; in Node it loads from the variant's folder inside the installed package. A call after the module has loaded logs a warning and does nothing.
setWasmBasePath(path: string): void
Sets where the WASM files come from. In a page the default is "/quantum-forge", the path the Vite plugin serves. In Node the default is the WASM inside the installed package, so you only need this when the files live somewhere else; pass a file URL. Call it before ensureLoaded(). A later useQuantumForgeBuild() call replaces it.
getWasmBasePath(): string
The base path ensureLoaded() will load from, after the defaults above are applied. Useful when a load fails.
registerServiceWorker(swPath?: string): Promise<ServiceWorkerRegistration | null>
Registers the offline caching service worker. Default path: "/quantum-forge-sw.js". Resolves null where service workers are unavailable or registration fails.
getQuantumForge().initialize(options?: InitializeOptions): Promise<void>
Low-level WASM initialization. The QuantumForge class is not exported directly; reach it through getQuantumForge(). Most consumers use ensureLoaded() instead, but calling initialize() directly lets you pass InitializeOptions:
interface InitializeOptions {
/** Override Emscripten's default stderr handler (console.warn). */
printErr?: (text: string) => void;
/** Override Emscripten's default stdout handler (console.log). */
print?: (text: string) => void;
}See Setup: WASM Output Redirection for usage examples.
INFO
The framework's startBackgroundLoad(logger) / ensureLoaded() already passes printErr to route WASM stderr through the provided logger at warn level.
Build info
getVersion(): string
The package version, as reported by the loaded WASM module.
getMaxDimension(): number
Maximum number of values per property in the loaded build: 3 for the Qutrit Edition, 2 for the Qubit Edition. Read it after ensureLoaded() to detect which edition is running.
getMaxQudits(): number
Maximum number of qudits one shared state can hold in the loaded build: 12 in the Qutrit Edition, 20 in the Qubit Edition.
getMaxStateSize(): number
Maximum number of basis amplitudes any single state vector can hold (compile-time limit, currently 100,000). See Lifecycle: State Budget.
getWasmMemoryBytes(): number | null
Current WASM heap usage in bytes, or null before the module has loaded.
getAttribution(): string
The attribution string to display in your application. Showing it is a license requirement, so put it somewhere a player can see: a credits screen, an about panel, or a splash screen.
Raw WASM access
Game code does not need this section. The handle API has no batch call of its own yet, so the batch APIs work on raw WASM properties, reached through handle.raw and getModule().
handle.raw
The WASM property behind a handle, typed QuantumProperty. Three rules:
- Operations through
.rawbypass the handle, so observers andQuantumRecordernever see them. A recorded session that mixes them in will not replay correctly. - Never call
destroy()on it. The handle owns the property, and itsdispose()fails after one. - It is valid only while the handle is live. Reading
.rawon a disposed handle throws, and a reference kept from earlier may already back a different handle.
getModule()
Returns the loaded WASM module, and throws if ensureLoaded() has not finished. It carries executeBatch, executeBatchTape, and the QuantumSimulation class. WASM calls use snake_case names (i_swap, measure_properties) and basis indices, not declared values. Predicates for the batch API come from the raw property: a.raw.is(1), a.raw.is_not(0).
Batch execution
m.executeBatch(ops: BatchOp[]): BatchResult
Execute multiple gate operations in a single WASM call. See Gates: Batch Gate Execution for the full guide.
import { getModule, quantum, type BatchOp } from "quantum-forge/quantum";
const a = quantum([false, true]);
const b = quantum([false, true]);
const ops: BatchOp[] = [
{ op: "hadamard", target: a.raw },
{ op: "cycle", target: b.raw, predicates: [a.raw.is(1)] },
];
const result = getModule().executeBatch(ops);BatchOp
type OpCode =
| 'cycle' | 'shift' | 'clock'
| 'x' | 'z' | 'y'
| 'hadamard' | 'inverse_hadamard'
| 'swap' | 'i_swap'
| 'phase_rotate';
interface BatchOp {
op: OpCode;
target?: QuantumProperty;
target2?: QuantumProperty; // swap, i_swap
fraction?: number; // omit for the discrete gate
angle?: number; // phase_rotate only
predicates?: Predicate[];
}At the WASM level an omitted fraction selects the discrete gate, and fraction: 1.0 selects the fractional gate at 1.0, a slower path. Unlike the handle methods, the batch API does not turn 1 into the discrete gate.
BatchResult
interface BatchResult {
opsExecuted: number; // how many completed before stopping
success: boolean;
errorMessage: string; // non-empty on failure
}m.executeBatchTape(properties: QuantumProperty[], tape: Float64Array): BatchResult
High-throughput batch execution using a pre-encoded Float64Array. Bypasses per-op embind marshaling, so the tape crosses into WASM as one bulk memcpy instead of thousands of JS object conversions. Use for pre-built operation sequences with 50+ ops. Pass raw properties: [a.raw, b.raw].
Tape format per operation (variable length, minimum 6 doubles):
[opcode, target_idx, target2_idx, fraction, angle, pred_count,
(pred_prop_idx, pred_value, pred_is_equal) × pred_count]target_idx/target2_idx: index intopropertiesarray, or -1 for nonefraction: NaN for the discrete gatepred_value: a basis index, not a declared valuepred_is_equal: 1.0 for is(), 0.0 for is_not()
OP constants
import { OP } from "quantum-forge/quantum";
OP.CYCLE // 0
OP.SHIFT // 1
OP.CLOCK // 2
OP.X // 3
OP.Z // 4
OP.Y // 5
OP.HADAMARD // 6
OP.INVERSE_HADAMARD // 7
OP.SWAP // 8
OP.I_SWAP // 9
OP.PHASE_ROTATE // 10
OP.ROTATE_BASIS_PAIR // 11ROTATE_BASIS_PAIR has a different tape layout: [11, nq, -1, NaN, angle, 0, a[0]..a[nq-1], b[0]..b[nq-1]]
See Gates: Batch Gate Execution for usage guide.
QuantumSimulation
Isolated simulation context for search and look-ahead, the way quantum chess uses it. Properties created within a simulation can entangle with each other but not with properties from other simulations or with quantum() handles. It works on raw WASM properties, not handles. See Lifecycle: QuantumSimulation for usage guide.
QuantumSimulation is not exported from quantum-forge/quantum. Reach it through the loaded module:
const sim = getQuantumForge().createSimulation();
// or, if you want the class itself
const { QuantumSimulation } = getModule();
const sim2 = new QuantumSimulation();| Method | Returns | Description |
|---|---|---|
createProperty(dimension) | QuantumProperty | Create a property bound to this simulation |
destroyProperty(prop) | void | Factorize one property out, auto-split separable qudits |
factorizeAllSeparable() | void | Scan all shared states and split separable qudits |
destroy() | void | Release all properties and state vectors at once |
isDestroyed() | boolean | Whether destroy() has already run |
Gates and queries on simulation properties come from getModule(). The simulation itself has no getModule().
Error types
Errors raised inside the WASM module are standard Error objects with a message prefix that names the error type. See Error Handling for the full guide.
| Prefix | Meaning |
|---|---|
[QuantumForgeStateSizeError] | State vector would exceed the maximum size (100,000 basis amplitudes) |
[QuantumForgeOutOfMemoryError] | WASM memory allocation failed |
[QuantumForgeError] | General error (invalid operation, destroyed property, cross-simulation entanglement) |
The handle catches most mistakes before they reach the WASM and throws with a message in game terms instead: a disposed handle, an undeclared value, flip() on a qutrit, a predicate on the gate's own target, an impossible forced outcome, and an interaction past the qudit limit.
Deprecated: QuantumPropertyManager
QuantumPropertyManager, its PredicateSpec and QuantumRecorderHook types, and LegacyQuantumRecorder still ship in 3.x so a game can move over one system at a time. They go in 4.0. QuantumPropertyManager (deprecated) documents the old API, and Migrating to 3.0 maps each call to its handle form.