Quantum Forge setup
The WASM quantum simulator ships pre-built in the quantum-forge package. Load it once with ensureLoaded(), then declare quantum properties with quantum().
Quick start
npm install quantum-forgeThen add the Vite plugin and call ensureLoaded(). To see a working setup first, npx quantum-forge example my-pong copies Quantum Pong, which ships inside the package, into a new project with the plugin already configured. See the Quick start.
The optional quantum-forge-engine package also scaffolds projects with this setup included: npx quantum-forge-engine init my-game. See the CLI.
Loading the module
The WASM module loads asynchronously. Await ensureLoaded() once at startup, before the first quantum() call:
import { ensureLoaded, startBackgroundLoad, quantum } from "quantum-forge/quantum";
// Option 1: start loading in the background (recommended)
// Call after your first render for faster perceived startup
startBackgroundLoad(logger);
// Option 2, or later on: wait until the module is ready
await ensureLoaded();
const coin = quantum([false, true]);Calling quantum() before the module has loaded throws.
TIP
startBackgroundLoad() uses requestIdleCallback to begin loading without blocking the main thread. Call it early, then await ensureLoaded() where you first need a quantum property. ensureLoaded() hands back the same promise on every call, so calling it more than once is safe.
TypeScript and using
A using declaration disposes a handle at the end of its scope:
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:
{
"compilerOptions": {
"lib": ["ES2022", "DOM", "ESNext.Disposable"]
}
}Projects made by npx quantum-forge-engine init have both. Code that never writes using needs neither: the package's types carry their own Symbol.dispose declaration, so a project on a default lib still type-checks.
Vite plugin
The framework includes a Vite plugin that serves WASM artifacts during development:
// vite.config.ts
import { defineConfig } from "vite";
import { quantumForgeVitePlugin } from "quantum-forge/vite-plugin";
export default defineConfig({
plugins: [quantumForgeVitePlugin()],
build: {
rollupOptions: {
external: [/quantum-forge-web-api/],
},
},
});The plugin routes /quantum-forge/* requests to the built WASM files in dist/. No other configuration is needed.
Plugin options:
| Option | Default | Description |
|---|---|---|
wasmDir | "dist" | Directory containing WASM artifacts |
servePath | "/quantum-forge" | URL path prefix for serving |
If you use vite.config.js (not .ts or .mjs), add "type": "module" to your package.json.
Node and headless use
The same code runs in Node (tests, servers, agents) with no Vite plugin. In Node the loader finds the WASM inside the installed package: dist/ for the default build, or dist/quantum-forge-qubit/ after useQuantumForgeBuild("qubit"). You call the same functions in the same order as in a page:
// check-circuit.mts
import { ensureLoaded, quantum, measure } from "quantum-forge/quantum";
await ensureLoaded();
const a = quantum([false, true]).superpose();
const b = quantum([false, true]);
b.flip({ when: [a.is(true)] }); // CNOT: a and b now always measure equal
console.log(a.probabilities());
// [{ value: false, probability: 0.5 }, { value: true, probability: 0.5 }]
console.log(measure(a, b)); // [false, false] or [true, true]
a.dispose();
b.dispose();This is how CI, unit tests, and AI coding agents should exercise quantum code. Nothing about the simulator needs a browser, so a plain Node script is enough to check that a circuit does what you think it does.
In Vitest, put await ensureLoaded() in a beforeAll. Every test file in a suite can call it, because it caches its promise.
getWasmBasePath() returns the path the loader imports from. Log it when a load fails to see where the loader looked.
Node 22 or newer.
Core 2.7.0 and earlier
In those releases the Node loader defaulted to the URL path /quantum-forge, which Node cannot import, so headless code had to call setWasmBasePath() with a file URL to the package's dist/ first. From 3.0 that call is only needed when the WASM files live outside the package.
Custom WASM base path
If you serve the WASM files from somewhere other than the default location, set the base path before loading:
import { setWasmBasePath, ensureLoaded } from "quantum-forge/quantum";
setWasmBasePath("/assets/wasm"); // call BEFORE ensureLoaded()
await ensureLoaded();In Node, pass a file URL:
import { setWasmBasePath, ensureLoaded } from "quantum-forge/quantum";
import { pathToFileURL } from "node:url";
setWasmBasePath(pathToFileURL("/opt/wasm/quantum-forge-qubit").href);
await ensureLoaded();setWasmBasePath() wins over the default whenever you call it. useQuantumForgeBuild() replaces any earlier setWasmBasePath(), so if you use both, call setWasmBasePath() second.
WASM output redirection
The loader routes the WASM module's stderr through the logger you pass to startBackgroundLoad(), as logger.warn(text, "QuantumForge/WASM"):
startBackgroundLoad(logger);To take full control, initialize the module yourself with print and printErr callbacks:
import { getQuantumForge } from "quantum-forge/quantum";
const QF = getQuantumForge();
await QF.initialize({
printErr: (text) => myLogger.warn(text), // stderr
print: (text) => myLogger.debug(text), // stdout
});TIP
This keeps WASM-level warnings in your game's logging system instead of losing them in the browser console.
Offline support
The core package ships a service worker file. Projects created with npx quantum-forge-engine init get public/quantum-forge-sw.js written for them, so there is nothing to copy.
For a manual install, or a project scaffolded before the file existed, copy it out of the package into your public directory:
cp node_modules/quantum-forge/quantum-forge-sw.js public/Then register it once, after page load:
import { registerServiceWorker } from "quantum-forge/quantum";
await registerServiceWorker(); // defaults to "/quantum-forge-sw.js"registerServiceWorker() returns null instead of throwing when the browser has no service worker support or registration fails, so it is safe to call unconditionally. Pass a different path as the only argument if you serve the file from somewhere other than the root.
The service worker caches WASM artifacts cache-first. After the first load, the module works offline.
WARNING
The shipped quantum-forge-sw.js requires quantum-forge 2.6.5 or newer. Older packages did not include the file.
Editions
The published npm package ships two WASM builds, called editions. You choose one when you run npx quantum-forge-engine init:
| Edition | Values per property | Max qudits | WASM variant |
|---|---|---|---|
| Qutrit (default) | 2 or 3 | 12 | Default build, no extra config |
| Qubit | 2 | 20 | Loads from /quantum-forge-qubit/ |
The Qutrit Edition allows properties with two or three values, such as quantum([false, true]) or quantum(["rock", "paper", "scissors"]), at the cost of a lower qudit ceiling. The Qubit Edition allows only two values per property but raises the limit to 20 qubits, so more quantum objects can share one entangled state. quantum() throws when you declare more values than the loaded edition allows.
The qudit limit applies to one entangled state. Properties that never interacted don't count against each other, and a disposed property stops counting. See Performance.
Switching editions at runtime
If you chose the Qubit Edition during init, your generated main.ts already contains the call. To add it by hand:
import { useQuantumForgeBuild, ensureLoaded } from "quantum-forge/quantum";
useQuantumForgeBuild("qubit"); // call BEFORE ensureLoaded()
await ensureLoaded();The Qutrit Edition is the default build and does not need useQuantumForgeBuild(). The call works the same way in Node, where it selects the variant folder inside the package.
WARNING
Call useQuantumForgeBuild() before ensureLoaded(). Once the module has loaded, the call is ignored with a warning.
Querying the active edition
After loading, check which build is active:
import { getMaxDimension, getMaxQudits } from "quantum-forge/quantum";
console.log(getMaxDimension()); // 3 (Qutrit) or 2 (Qubit)
console.log(getMaxQudits()); // 12 (Qutrit) or 20 (Qubit)Future versions
Future versions will support more values per property and higher qudit counts. The current limits balance capability against WASM binary size and memory use.
Runtime queries
After loading, you can query the module's configuration:
import { getVersion, getMaxDimension, getMaxQudits, getMaxStateSize } from "quantum-forge/quantum";
console.log(getVersion()); // the package version, e.g. "3.0.0"
console.log(getMaxDimension()); // 3 (Qutrit) or 2 (Qubit)
console.log(getMaxQudits()); // 12 (Qutrit) or 20 (Qubit)
console.log(getMaxStateSize()); // 100,000: max basis amplitudes in one state vectorOlder releases returned ".." from getVersion(). From 3.0 it reports the package version.
getWasmMemoryBytes() is also exported, but in current builds it always returns null. See Performance: WASM heap usage.
Attribution
The license requires visible attribution. getAttribution() returns the string to display, so you don't have to keep it in sync by hand:
import { getAttribution } from "quantum-forge/quantum";
console.log(getAttribution());
// "Powered by Quantum Forge — © Quantum Native — quantumnative.io"Put it somewhere a player can find it: a credits screen, an about panel, a corner of the title screen. The loader also prints a badge to the browser console on startup, but that is a developer nicety, not the attribution.
Troubleshooting
"QuantumForge not loaded. Call ensureLoaded() first and await it."
Something called quantum() or another quantum function before the module finished loading. Await ensureLoaded() first.
WASM files not found in production
Make sure the WASM artifacts are copied to your public or static directory and that setWasmBasePath() points at the URL path they are served from.
Headless load fails
Log getWasmBasePath() to see where the loader looked. If the WASM files are not inside the installed quantum-forge package, point the loader at them with setWasmBasePath() and a file URL.