Skip to content

Performance

Quantum simulation is computationally expensive. State space grows exponentially with the number of qudits. This page covers how to keep your game running at 60 FPS.

The One Rule: Pool Your Properties

Property pooling is the single most important optimization. Without pooling, every new property grows the tensor product. With pooling, measured properties are reset and reused within the existing shared state.

typescript
// After measurement, ALWAYS release to pool
const [value] = this.getModule().measure_properties([prop]);
this.deleteProperty(id);
this.releaseProperty(prop, value); // reset to |0⟩, return to pool

QuantumPropertyManager handles this automatically when you use removeProperty(id), which measures, deletes, and releases in one call.

Why Pooling Works

When you acquireProperty():

  1. Pool is not empty → reuses a property already in the shared state. No tensor product growth. Fast.
  2. Pool is empty → creates a fresh property. If this property later i_swaps with an existing one, the WASM module computes a tensor product to merge the state spaces. Expensive.

Pooled properties are already in the shared state, so entangling them (via i_swap) is cheap.

Shipped Limits

The published package ships two editions with different trade-offs:

EditionMax DimensionMax QuditsState Space per Qudit
Qutrit (default)3122–3 basis states
Qubit2202 basis states

Choose your edition during npx quantum-forge init (or with --edition qubit). The Qutrit edition gives you three-valued properties but fewer total qudits. The Qubit edition trades away qutrits for a higher qudit ceiling. See Quantum Setup: Editions.

Properties in separate (non-entangled) systems don't count against each other.

Performance Budget

60 FPS target guidelines (Qutrit edition: dimension ≤ 3, 12 max qudits; Qubit edition: dimension 2, 20 max qubits):

Active PropertiesPerformanceNotes
1–4No issuesComfortable headroom
4–8GoodMonitor frame times
8–10Watch carefullyOnly with good pooling
10–12At the limitAggressive pooling required

Expensive Operations

Ranked by cost:

  1. Tensor product: triggered by i_swap between properties in different shared states. Cost grows exponentially with the number of qudits in each state. This is the operation to minimize.

  2. Fresh property creation: when the pool is empty. The property itself is cheap, but the first i_swap with an existing property triggers a tensor product.

  3. Measurement: proportional to state vector size. Fast for small systems, noticeable for 15+ entangled qudits.

  4. Gate application: fast. Scales linearly with state vector size.

  5. Probability query: similar cost to measurement but no state change.

Optimization Strategies

1. Aggressive Pooling

Measure and release as soon as a property is no longer needed:

typescript
// Ball exits field → measure immediately, don't wait
const exists = registry.measureExistence(ballId);
// Property is now back in the pool

2. Limit Concurrent Quantum Objects

Design your game so that only a few objects are quantum at any time:

  • Quantum Pong: max 4-6 balls quantum at once (2-3 pairs)
  • Quantris: current piece + at most 2-3 recently placed pieces
  • Bloch Invaders: only the targeted invader is actively quantum

3. Choose the Right Dimension

Higher dimensions = exponentially larger state space:

DimensionStates per propertyMemory per entangled pair
2 (qubit)264 bytes
3 (qutrit)3144 bytes

The Qutrit edition supports dimensions 2 and 3. The Qubit edition is locked to dimension 2 but supports up to 20 qubits. If you only need binary states and want more quantum objects, consider the Qubit edition.

4. Separate Registries for Independent Systems

If your game has quantum objects that never interact, use separate registries:

typescript
const playerRegistry = new PlayerQuantumRegistry(logger);  // player abilities
const enemyRegistry = new EnemyQuantumRegistry(logger);    // enemy states

// These systems never entangle with each other,
// so they maintain independent (small) state spaces

5. Batch Operations

Batch gates: When applying many gates per frame (AI sweeps, circuit replay), use executeBatch() to send them all in one WASM call. This eliminates per-call JS-WASM boundary overhead (~1-2 us per crossing), which dominates when gate math itself is fast (~1-2 us per gate).

typescript
import type { BatchOp } from "quantum-forge/quantum";

const ops: BatchOp[] = tape.map(instr => ({
  op: instr.gate,
  target: instr.property,
  predicates: instr.controls,
}));
const result = m.executeBatch(ops); // one WASM call instead of ~100

See Gates: Batch Gate Execution for the full API.

For maximum throughput, use executeBatchTape() with a pre-encoded Float64Array — see Gates: Tape-based batch.

Batch measurement: Measure multiple properties at once instead of one at a time:

typescript
const values = this.getModule().measure_properties([prop1, prop2, prop3]);

6. Throttle Probability Queries

Reading probabilities for visualization doesn't need to happen every frame:

typescript
private probCache = new Map<string, number>();
private lastProbUpdate = 0;

getExistenceProbability(id: string): number {
  const now = performance.now();
  if (now - this.lastProbUpdate < 100) { // update at most 10x/sec
    return this.probCache.get(id) ?? 1.0;
  }
  this.lastProbUpdate = now;

  const prop = this.getProperty(id);
  if (!prop) return 1.0;
  const prob = /* query probabilities */;
  this.probCache.set(id, prob);
  return prob;
}

Destroying Unused Properties

When a property is no longer needed but you don't need its measurement value, call destroy() to remove it from the shared state vector entirely:

typescript
m.destroy(tempProp); // factorizes qudit out of state vector

This is more aggressive than pooling — it shrinks the state vector rather than keeping the qudit in |0⟩. Use it for temporary properties in replay/adapter scenarios. See Lifecycle & State Management for details.

Isolated Simulations

For search tree exploration or replay branches, use QuantumSimulation to create isolated quantum contexts. Each simulation has its own state vectors, preventing cross-contamination:

typescript
const sim = QuantumForge.createSimulation();
const prop = sim.createProperty(2);
// ... explore this branch ...
sim.destroy(); // releases everything at once

This prevents the common problem of replay branches growing a shared global state vector. See Lifecycle: QuantumSimulation.

Monitoring

WASM heap usage

Use getWasmMemoryBytes() to monitor WASM heap usage:

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

// In your debug overlay
const memBytes = getWasmMemoryBytes();
if (memBytes !== null) {
  debugText(`WASM: ${(memBytes / 1024).toFixed(0)} KB`);
}

State budget queries

Query the state vector size for any property to monitor quantum state complexity:

typescript
const sparseSize = m.state_vector_size(prop);
const numQudits = m.num_active_qudits(prop);

debugText(`State: ${sparseSize} amplitudes, ${numQudits} qudits`);

Use getMaxStateSize() to compare against the hard limit (100,000 basis amplitudes). See Lifecycle: State Budget for backpressure patterns.

Error Handling

WASM operations throw typed JavaScript errors when they fail. The most important for performance is [QuantumForgeStateSizeError], which fires when a tensor product would exceed the state size limit:

typescript
quantumSplit(originalId: string, newId: string): boolean {
  const prop1 = this.getProperty(originalId);
  if (!prop1) return false;

  const prop2 = this.acquireProperty();
  try {
    this.getModule().i_swap(prop1, prop2, 0.5);
  } catch (e) {
    if (e instanceof Error && e.message.startsWith("[QuantumForgeStateSizeError]")) {
      this.logger?.warn?.("State too large for split, falling back to classical");
    } else {
      this.logger?.warn?.("Quantum operation failed, falling back to classical");
    }
    this.releaseProperty(prop2, 0);
    return false;
  }

  this.setProperty(newId, prop2);
  return true;
}

The OOM guard estimates tensor product size before allocating, so the operation fails cleanly without corrupting state. Design your game so that hitting the limit is graceful, not fatal. See Error Handling for the full guide.

Powered by Quantum Forge