Skip to content

Lifecycle and state management ​

A quantum property lives from quantum() to dispose(). This page covers what disposing does, how the engine's EntityManager disposes handles for you, how to watch the size of the quantum state, and how to run an isolated simulation for search.

Disposing a property ​

dispose() is the only lifecycle call:

typescript
import { ensureLoaded, quantum } from "quantum-forge/quantum";

await ensureLoaded();

const coin = quantum([false, true]).superpose();
const heads = coin.measure();
coin.dispose();

dispose() measures the property first, which also collapses every property entangled with it. What happens next depends on whether the qudit is alone in its state:

  • Alone: dispose() resets it and keeps it in an internal cache. The next quantum() with the same number of values reuses it instead of allocating a new one.
  • Still sharing a state with other properties: dispose() destroys it, which takes it out of that state. It is not cached.

Either way the next quantum() starts from a qudit of its own, never one inside an old group, and a disposed property stops counting against the qudit limit.

You do not pass dispose() a value. Calling it twice does nothing, and any other call on a disposed handle throws Quantum property #N was disposed and can no longer be used. dispose() also works on a handle frozen with Object.freeze(), which deep-freeze helpers on game state sometimes do.

A measured handle is not disposed. After measure() it holds a definite value and can go back into superposition with another gate. Only dispose() ends its life.

Disposing collapses partners ​

Because disposing measures, disposing one half of an entangled pair settles the other half:

typescript
const a = quantum([false, true]).flip(); // a starts true
const b = quantum([false, true]);        // b starts false
a.iSwap(b, 0.5);                         // exactly one of them is true

a.dispose();
// b is now definitely true or definitely false, and a qudit of its own again
b.numActiveQudits(); // 1

If you want the outcome, call measure() before dispose(). dispose() does not return it.

using declarations ​

A using declaration disposes the handle when the scope ends, including when it ends by a throw:

typescript
function rollDie(): number {
  using die = quantum(3).superpose();
  return die.measure();
} // die.dispose() runs here

using 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. See Setup: TypeScript and using.

Handles on entities ​

Store a handle on the entity it describes. The engine's EntityManager disposes quantum handles when it removes an entity, so most games never call dispose() themselves:

typescript
import { ensureLoaded, quantum, type Quantum } from "quantum-forge/quantum";
import { EntityManager } from "quantum-forge-engine/entities";

await ensureLoaded();

interface Enemy {
  id: string;
  x: number;
  y: number;
  alive: Quantum<boolean>;
}

const enemies = new EntityManager<Enemy>();
enemies.add({ id: "e1", x: 10, y: 20, alive: quantum([false, true]).flip() });

enemies.remove("e1"); // disposes enemy.alive

What the manager disposes:

  • remove() and clear() dispose the handles on each entity that leaves.
  • add() with an id that is already present replaces the entity and disposes the old entity's handles that the new one no longer carries.

What it finds: handles stored as own enumerable properties of the entity, under a string or a symbol key, and handles inside an array stored in one. Nothing else is walked: not nested objects, Map or Set contents, arrays of arrays, non-enumerable properties, or #private fields. Getters are never called, so a handle behind one is not found. Dispose those handles yourself.

Disposing measures, so removing an entity collapses whatever its handles were entangled with. If e1 and e2 were split from one enemy, enemies.remove("e1") settles whether e2 exists.

A handle belongs to one entity. If two entities hold the same handle, removing either one disposes it, and the other's next use of it throws.

Keeping a handle past its entity ​

To keep a handle after its entity goes, for a replay or a score screen, move it off the entity first, or pass { disposeQuantum: false } to that remove(), clear(), or add() call:

typescript
enemies.remove("e1", { disposeQuantum: false }); // e1's handles stay live

new EntityManager({ disposeQuantum: false }) turns the default off for the whole manager. Handles you keep this way are yours to dispose.

Engine 2.0

Automatic disposal is new in engine 2.0. In 1.x, EntityManager never touched quantum state. See Migrating to 3.0.

State budget queries ​

Every property you interact with joins a shared state, and that state can grow as large as dimension ^ qudits entries. Two methods on the handle report how big the state it belongs to is:

typescript
const numQudits = prop.numActiveQudits();  // qudits in this property's shared state
const sparseSize = prop.stateVectorSize(); // basis amplitudes stored for that state

Both are read-only and cheap enough to call every frame.

QueryReturnsUse case
prop.numActiveQudits()Number of qudits in the property's shared stateCheck entanglement group size
prop.stateVectorSize()Current number of basis amplitudes (sparse)Monitor memory pressure
getMaxQudits()Most qudits one shared state may hold: 12 (Qutrit) or 20 (Qubit)Compare against numActiveQudits()
getMaxStateSize()Most basis amplitudes one state vector may hold: 100,000Compare against stateVectorSize()

A property that has never interacted with another reports one qudit. After a CNOT with another qubit, both report two. When one of them is disposed, the other drops back to one.

Checking before an interaction ​

An interaction that would push a shared state past the qudit limit throws. Check capacity before the interaction instead of catching the throw: each caught throw leaks WASM memory, and after enough of them every later call fails.

typescript
import { getMaxQudits, quantum, type Quantum } from "quantum-forge/quantum";

interface Ball {
  exists?: Quantum<boolean>;
}

/** Split an already-quantum ball, or stay classical if its state is full. */
function quantumSplit(original: Ball, clone: Ball): boolean {
  if (!original.exists) return false;
  if (original.exists.numActiveQudits() >= getMaxQudits()) return false; // state is full
  const split = quantum([false, true]);
  original.exists.iSwap(split, 0.5);
  clone.exists = split;
  return true;
}

On the Qutrit Edition the amplitude limit can bite first. A group of fully superposed qutrits passes 100,000 amplitudes at 11 qudits, one short of the 12-qudit limit, and the interaction throws [QuantumForgeStateSizeError]. If your game entangles many qutrits, compare stateVectorSize() against getMaxStateSize() as well. See Error handling.

The internal cache ​

clearQuantumCache() destroys every cached qudit and empties the cache. Live handles are not affected. Tests use it to start from a clean slate, and a game can call it between scenes to free memory.

typescript
import { clearQuantumCache } from "quantum-forge/quantum";

clearQuantumCache();

The raw WASM property ​

handle.raw is the WASM property behind a handle. It exists for the batch APIs (executeBatch, executeBatchTape), which have no handle form yet. Three rules:

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

QuantumSimulation ​

QuantumSimulation creates an isolated simulation context for search, as quantum chess does. It works on raw WASM properties, not quantum() handles: quantum() always creates properties in the global context. Properties created within a simulation can interact with each other but not with properties from another simulation.

typescript
import { getQuantumForge, getModule, ensureLoaded } from "quantum-forge/quantum";

await ensureLoaded();

const sim = getQuantumForge().createSimulation();
const prop1 = sim.createProperty(2); // two values
const prop2 = sim.createProperty(2);

// Gates come from the module, not from the simulation
const m = getModule();
m.hadamard(prop1);
m.i_swap(prop1, prop2, 0.5); // works, same simulation

// Clean up. Releases all properties and state vectors at once
sim.destroy();

A simulation owns properties, not gates. Its whole surface is createProperty, destroyProperty, factorizeAllSeparable, destroy, and isDestroyed. Gates and queries come from getModule() and use the WASM names (i_swap, measure_properties, and so on). They work on properties from any simulation as long as a single call does not mix two simulations.

Why isolated simulations? ​

Search tree exploration needs a fresh quantum state per branch. Without isolation, every replayed branch shares, and grows, the same state vector:

typescript
// Each replay is isolated
for (const move of candidateMoves) {
  const sim = getQuantumForge().createSimulation();
  const props = replayUpTo(move, sim); // properties are sandboxed
  const score = evaluate(props);       // state vector holds just this replay
  sim.destroy();                       // everything released at once
}

Cross-simulation entanglement is an error ​

A gate across two simulations throws:

typescript
const simA = getQuantumForge().createSimulation();
const simB = getQuantumForge().createSimulation();

const propA = simA.createProperty(2);
const propB = simB.createProperty(2);

getModule().i_swap(propA, propB, 0.5);
// Error: [QuantumForgeError] Cannot entangle properties from different QuantumSimulation contexts

If you match on the message, match on that exact text. It is the string the C++ layer throws.

Simulation reference ​

MethodDescription
getQuantumForge().createSimulation()Create a new isolated simulation
sim.createProperty(dimension)Create a property bound to this simulation
sim.destroyProperty(prop)Take one property out and free its resources
sim.factorizeAllSeparable()Split out any separable qudits across all shared states
sim.destroy()Release all properties and state vectors at once
sim.isDestroyed()Whether destroy() has already been called

Incremental property destruction ​

For search algorithms that create many helper properties, sim.destroyProperty(prop) frees them one at a time instead of waiting for sim.destroy():

typescript
const sim = getQuantumForge().createSimulation();
const board = sim.createProperty(2);
const ancilla = sim.createProperty(2);

const m = getModule();
m.i_swap(board, ancilla, 0.5);
// ... use the entanglement ...
m.i_swap(board, ancilla, -0.5); // undo

// ancilla is separable again, so destroying it leaves board untouched
sim.destroyProperty(ancilla);

destroyProperty also splits out any qudits that became separable once the destroyed property is gone. That keeps qudits from piling up across many do/undo search cycles.

Destroying entangled properties

If the property is still entangled with others, destroying it measures it, which collapses its partners. A console warning is emitted in this case. Undo the entanglement before destroying to avoid the collapse.

After undo operations that may have restored separability, such as i_swap(a, b, f) followed by i_swap(a, b, -f), call sim.factorizeAllSeparable() to split separable qudits into independent states without destroying anything.

After sim.destroy(), every property created in the simulation is invalid. Check prop.is_valid() if you still hold references.

Lifecycle summary ​

ToolPurposeWhen to use
dispose() or usingMeasure, then cache or destroy the quditWhenever a property's object is gone
EntityManagerDispose handles on entities it removesGames built on the engine
State budget queriesWatch shared state sizeBefore interactions, debug overlays
clearQuantumCache()Free cached quditsTests, scene changes
QuantumSimulationIsolated raw WASM contextsSearch trees, replay branches

Powered by Quantum Forge