Gates
Quantum gates change the state of properties without measuring them. Gates are methods on the handle quantum() returns, and every gate returns the handle, so calls chain. Every gate accepts predicates in { when: [...] }, and a gate predicated on another property entangles the two (see Core concepts: predicated operations).
import { ensureLoaded, quantum } from "quantum-forge/quantum";
await ensureLoaded();
const color = quantum(["red", "green", "blue"]);
color.superpose(); // spread evenly over red, green and blue
color.next().next(); // move two values forward, wrappingComing from 2.x, where gates were getModule() calls such as m.cycle(prop)? Migrating to 3.0 has the full mapping.
Gate reference
Physics names are the primary names. The game-word alias next to each one runs exactly the same gate under another name; it never adds behavior.
| Physics call | Alias | Dim | Fraction | What it does |
|---|---|---|---|---|
hadamard(fraction?, opts?) | superpose() | Any | Optional | Spread the property evenly across every value |
inverseHadamard(opts?) | Any | None | Undo hadamard() | |
cycle(fraction?, opts?) | next(), and flip() on qubits | Any | Optional | Move to the next value, wrapping |
shift(fraction?, opts?) | previous() | Any | Optional | Move to the previous value, wrapping |
clock(fraction?, opts?) | phase() | Any | Optional | Rotate the phase of each value by its index |
x(fraction?, opts?) | Any | Optional | Pauli X, the same gate as shift() | |
y(fraction?, opts?) | 2 only | Optional | Pauli Y | |
z(fraction?, opts?) | Any | Optional | Pauli Z, the same gate as clock() | |
swap(other, opts?) | Any | None | Exchange the states of two properties | |
iSwap(other, fraction, opts?) | Any | Required | iSwap between two properties | |
phaseRotate(angle, { when }) | Any | None | Free function: phase on the part of the state where the predicates hold |
x() is the same gate as shift(), not cycle(). z() is the same gate as clock(). flip() and y() need a property with two values and throw on any other dimension; use next() or cycle() on a qutrit. swap() and iSwap() need two properties with the same number of values, and throw when passed the handle they are called on.
Fractions
Every gate except inverseHadamard, swap and iSwap takes an optional fraction as its first argument. Leave it out for the discrete gate. Any other number runs the continuous version, and 0.5 is the square root of the gate:
alive.flip(); // NOT
alive.flip(0.5); // square root of NOT
color.next(0.25); // a quarter step toward the next valueOn a handle, a fraction of exactly 1 also runs the discrete gate: the handle never sends 1.0 to the simulator's slower fractional path. Leave the fraction out anyway when you mean the whole gate; it reads better. See Fractional gates for more.
Conditional gates (predicates)
Every gate takes predicates in { when: [...] } that condition it on the value of other properties. This is the main way to create entanglement: when a gate depends on another property's value, the two become correlated.
target.flip({ when: [control.is(true)] }); // CNOT, entangles the pair
target.superpose({ when: [control.is(true)] }); // controlled Hadamard, conditional superposition
target.flip(0.5, { when: [control.is(true)] }); // controlled square root of NOT
a.iSwap(b, 0.5, { when: [gate.is(true)] }); // controlled split
target.next({ when: [controlA.is(true), controlB.isNot(0)] }); // two conditions, both must holdThe options object can follow the fraction or stand in its place. There is no fraction slot to fill with undefined.
is(value) holds where the property equals the value, isNot(value) where it does not. Both accept a declared value or its index, so on quantum([false, true]), a.is(true) and a.is(1) build the same predicate. A value that is not declared throws when you build the predicate. With several predicates, the gate acts only where all of them hold.
Predicates cannot target the gated property
A predicate may not read the property the gate is acting on. The handle throws before anything reaches the simulator:
// WRONG: the predicate reads the property being gated
prop.flip({ when: [prop.is(true)] });
// throws: "flip(): a gate on quantum property #1 cannot be conditioned on that same property. ..."
// RIGHT: the predicate reads a different property
target.flip({ when: [control.is(true)] });This applies to two-property gates too. Neither property of a swap() or iSwap() may appear in its predicates.
Phase rotate
phaseRotate rotates the phase of the part of the state where every predicate holds, by an angle in radians. Unlike the other gates it has no single target, so it is a free function:
import { phaseRotate } from "quantum-forge/quantum";
phaseRotate(Math.PI, { when: [a.is(true), b.is(true)] }); // π phase flip: a CZ
phaseRotate(Math.PI / 4, { when: [a.is(true)] }); // π/4 phase shiftAn empty when list does nothing and raises nothing, so a bug that empties your predicate array shows up as a missing phase, not as an error. Check the array length yourself if that matters.
Single-property gates
Cycle
Cyclic permutation of values, incrementing the index mod d: |0⟩→|1⟩→|2⟩→...→|d-1⟩→|0⟩. At dimension 2 this is the NOT gate. next() is the same call, and flip() is the same call restricted to qubits.
alive.cycle(); // false → true, true → false
alive.flip(); // the same gate
alive.flip(0.5); // square root of NOT
color.next(); // "red" → "green"Game use case: start a ball at "exists" with quantum([false, true]).flip(). Quantum Pong's entangle-split does this.
Hadamard
Creates an equal superposition from a definite value. The most common gate for putting objects "into quantum." superpose() is the same call.
coin.superpose(); // false → (|0⟩ + |1⟩)/√2, true → (|0⟩ − |1⟩)/√2
color.superpose(); // "red" → 1/3 each on red, green and blue
coin.superpose(0.5); // fractional Hadamard (H^0.5)
coin.superpose(0.1); // very gentle, barely moves probabilitiesGame use case: a small fractional Hadamard every frame for gradual probability diffusion. Small fractions spread slowly, larger ones fast.
Inverse Hadamard
The adjoint (reverse) of Hadamard. It undoes hadamard(), taking the superposition back to the value it started from.
color.next().hadamard().inverseHadamard(); // back to "green"It takes no fraction.
Clock / Z
Phase rotation. Changes the phase of each value by its index without changing measurement probabilities, until the property meets another gate that mixes values. phase() and z() are the same call.
coin.phase(); // the generalized Z gate
coin.phase(0.5); // fractional clock: true picks up a phase of π/2 at dimension 2Game use case: Quantum Pong's phase dial. Paddle hits bias later interactions between entangled balls.
TIP
Phase alone doesn't change what you'll measure. It changes what the next superpose() or interaction does with the property. On a qubit, superpose().superpose() ends at false, while superpose().phase().superpose() ends at true every time. See Phase and interference.
Shift / X
Generalized bit-flip, and the inverse of cycle. shift decrements the index mod d: |0⟩→|d-1⟩, |1⟩→|0⟩, |2⟩→|1⟩, and so on. previous() and x() are the same call.
// dimension 3
const q = quantum(3);
q.shift(); // 0 → 2 (cycle would give 1)
// dimension 2
alive.shift(); // false → true (same as cycle: +1 and -1 coincide mod 2)At dimension 2, increment and decrement are the same operation, which is why shift and cycle look interchangeable in qubit code. At dimension 3 and above they move the state in opposite directions.
If you reach for x() expecting |0⟩ → |1⟩ on a qutrit, you will get |2⟩.
Y gate
Pauli Y gate. Qubits only: it throws if the property does not have two values. Composite gate: S-X-S-dagger.
coin.y(); // full Y gate
coin.y(0.5); // fractional Y: rotate around the Y axis on the Bloch sphereDANGER
y() throws if the property's dimension is not 2. This is intentional. The Y gate has no standard generalization to higher dimensions.
Two-property gates
iSwap
The main entangling gate for splits. The fraction is required.
a.iSwap(b, 0.5); // half iSwap, maximal entanglement
a.iSwap(b, 1); // full iSwap: the values trade places, with a phase
a.iSwap(b, 0.25); // quarter iSwap, partial entanglementStarting from a true, b false (|10⟩), a.iSwap(b, 0.5) gives:
(|10⟩ + i|01⟩) / √2That is a 50% chance a is true and b false, and 50% the reverse, and the two are correlated. Measuring one settles the other. On two equal values, iSwap does nothing: a split needs one property set and the other not.
WARNING
iSwap between properties in different shared states merges those states into one, growing the combined state. Dispose handles you are done with so the shared states stay small. See Performance.
Game use case: Quantum Pong's entangle-split. A ball enters a quantum zone and splits into a correlated pair.
Swap
Direct state exchange between two properties of the same dimension.
a.swap(b);Batch gate execution
When you apply many gates in a tight loop (an AI sweep applying 50 to 100 gates per tick, say), each gate call crosses the JS-WASM boundary separately. The cost of those crossings can outweigh the gate math.
The simulator's batch API sends a whole array of gate operations in one WASM call. The handle API has no batch call of its own yet, so this is an advanced route: it runs on raw WASM properties, which you reach through handle.raw, and the batch functions live on the loaded module from getModule().
Three rules for handle.raw:
- 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.
import { getModule, type BatchOp, type BatchResult } from "quantum-forge/quantum";
const p1 = a.raw;
const p2 = b.raw;
const p3 = c.raw;
const ops: BatchOp[] = [
{ op: "hadamard", target: p1 },
{ op: "cycle", target: p2, predicates: [p1.is(1)] },
{ op: "shift", target: p3, fraction: 0.5 },
{ op: "swap", target: p1, target2: p2 },
{ op: "phase_rotate", angle: Math.PI / 4, predicates: [p1.is(1)] },
];
const result: BatchResult = getModule().executeBatch(ops);
// result.success true if all operations completed
// result.opsExecuted number of operations that ran before stopping
// result.errorMessage non-empty on failureBatch operations use the simulator's own spelling: op names are snake_case ("inverse_hadamard", "i_swap"), and predicates are built on the raw property by index, p1.is(1) or p1.is_not(0), not by declared value.
BatchOp fields
| Field | Type | Required | Description |
|---|---|---|---|
op | OpCode | Yes | Gate name: "cycle", "shift", "clock", "x", "z", "y", "hadamard", "inverse_hadamard", "swap", "i_swap", "phase_rotate" |
target | QuantumProperty | Most gates | Primary target, a handle.raw (omit for phase_rotate) |
target2 | QuantumProperty | swap, i_swap | Second target |
fraction | number | No | Gate fraction. Omit for the discrete gate. |
angle | number | phase_rotate | Rotation angle in radians |
predicates | Predicate[] | No | Raw WASM predicates, built with raw.is(index) or raw.is_not(index) |
Fractional vs. discrete in a batch
The handle turns a fraction of exactly 1 into the discrete gate. A batch does not. Omitting fraction runs the discrete gate (a permutation: cycle adds 1 to the index). Setting fraction: 1.0 runs the fractional gate at 1.0, a continuous rotation that reaches the same state by a slower path. Omit the field when you want the discrete gate.
Error handling
Operations run in order. On the first error, execution stops, and gates that already ran are not rolled back. Check result.opsExecuted to know how many completed.
const result = getModule().executeBatch(ops);
if (!result.success) {
console.warn(`Batch failed after ${result.opsExecuted}/${ops.length} ops: ${result.errorMessage}`);
}Tape-based batch (maximum throughput)
For 50 or more operations, executeBatchTape() skips per-op marshaling entirely by taking a pre-encoded Float64Array. The tape is bulk-copied to WASM in one memcpy.
OP is importable from the package. executeBatchTape lives on the loaded WASM module, so reach it through getModule().
import { getModule, OP } from "quantum-forge/quantum";
const properties = [a.raw, b.raw, c.raw];
const tape = new Float64Array([
OP.HADAMARD, 0, -1, NaN, 0, 0, // hadamard(a)
OP.CYCLE, 1, -1, NaN, 0, 1, 0, 1, 1, // cycle(b) where a is index 1
OP.SWAP, 0, 2, NaN, 0, 0, // swap(a, c)
]);
const result = getModule().executeBatchTape(properties, tape);The NaN in the fraction slot is the "no fraction" sentinel, the tape's way of asking for the discrete gate. Writing 1 there gives you the fractional gate at 1.0 instead.
See the API reference for the full tape format and the OP constants, including ROTATE_BASIS_PAIR for fused basis-state rotations.
When to use batch vs. individual calls
| Approach | Best for | Boundary cost |
|---|---|---|
| Handle methods | Interactive gates, measurement-dependent logic, anything you record | One JS-WASM crossing per gate |
getModule().executeBatch(ops) | Moderate batches, readable code | One crossing, but each op is still marshaled through embind |
getModule().executeBatchTape(props, tape) | Pre-built sequences, 50+ ops, real-time animation | One crossing, one memcpy, no per-op marshaling |
Measurement is not part of a batch. Measure through the handles (a.measure() or measure(a, b)) after the batch completes.
Fractional gates
Most gates take a fraction for partial application:
0.5: the square root of the gate. Twoflip(0.5)calls make oneflip()0.25: a quarter step. Fournext(0.25)calls make onenext()0.1: very gentle, barely moves the state
Fractional gates are how you build gradual quantum effects: superpose(0.05) every frame for slow probability diffusion, or y(0.3) for precise steps around the Bloch sphere.
Leaving the fraction out selects the discrete gate, a permutation of values. Passing any fraction other than 1 selects a continuous rotation. On a handle, exactly 1 is treated as the discrete gate, so flip(1) and flip() are the same call. That rule belongs to the handle: in the batch API, fraction: 1.0 is the fractional path.