Skip to content

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).

typescript
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, wrapping

Coming 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 callAliasDimFractionWhat it does
hadamard(fraction?, opts?)superpose()AnyOptionalSpread the property evenly across every value
inverseHadamard(opts?)AnyNoneUndo hadamard()
cycle(fraction?, opts?)next(), and flip() on qubitsAnyOptionalMove to the next value, wrapping
shift(fraction?, opts?)previous()AnyOptionalMove to the previous value, wrapping
clock(fraction?, opts?)phase()AnyOptionalRotate the phase of each value by its index
x(fraction?, opts?)AnyOptionalPauli X, the same gate as shift()
y(fraction?, opts?)2 onlyOptionalPauli Y
z(fraction?, opts?)AnyOptionalPauli Z, the same gate as clock()
swap(other, opts?)AnyNoneExchange the states of two properties
iSwap(other, fraction, opts?)AnyRequirediSwap between two properties
phaseRotate(angle, { when })AnyNoneFree 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:

typescript
alive.flip();       // NOT
alive.flip(0.5);    // square root of NOT
color.next(0.25);   // a quarter step toward the next value

On 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.

typescript
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 hold

The 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:

typescript
// 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:

typescript
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 shift

An 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.

typescript
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.

typescript
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 probabilities

Game 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.

typescript
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.

typescript
coin.phase();       // the generalized Z gate
coin.phase(0.5);    // fractional clock: true picks up a phase of π/2 at dimension 2

Game 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.

typescript
// 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.

typescript
coin.y();          // full Y gate
coin.y(0.5);       // fractional Y: rotate around the Y axis on the Bloch sphere

DANGER

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.

typescript
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 entanglement

Starting from a true, b false (|10⟩), a.iSwap(b, 0.5) gives:

(|10⟩ + i|01⟩) / √2

That 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.

typescript
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 .raw bypass the handle, so observers and QuantumRecorder never see them. A recorded session that mixes them in will not replay correctly.
  • Never call destroy() on it. The handle owns the property, and its dispose() fails after one.
  • It is valid only while the handle is live. Reading .raw on a disposed handle throws, and a reference kept from earlier may already back a different handle.
typescript
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 failure

Batch 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 ​

FieldTypeRequiredDescription
opOpCodeYesGate name: "cycle", "shift", "clock", "x", "z", "y", "hadamard", "inverse_hadamard", "swap", "i_swap", "phase_rotate"
targetQuantumPropertyMost gatesPrimary target, a handle.raw (omit for phase_rotate)
target2QuantumPropertyswap, i_swapSecond target
fractionnumberNoGate fraction. Omit for the discrete gate.
anglenumberphase_rotateRotation angle in radians
predicatesPredicate[]NoRaw 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.

typescript
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().

typescript
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 ​

ApproachBest forBoundary cost
Handle methodsInteractive gates, measurement-dependent logic, anything you recordOne JS-WASM crossing per gate
getModule().executeBatch(ops)Moderate batches, readable codeOne crossing, but each op is still marshaled through embind
getModule().executeBatchTape(props, tape)Pre-built sequences, 50+ ops, real-time animationOne 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. Two flip(0.5) calls make one flip()
  • 0.25: a quarter step. Four next(0.25) calls make one next()
  • 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.

Powered by Quantum Forge