| name | quantumjs |
| description | Write, understand, and translate QuantumJS code โ the expressive DSL for quantum circuit construction targeting OpenQASM 3.0. Covers circuit creation, every gate with exact QASM output, scoped layout staircases, measurement patterns, pipeline abstraction, custom functions, full algorithm library, and detailed translation from Qiskit and QASM into idiomatic QuantumJS.
|
QuantumJS Skill
QuantumJS is a modern quantum circuit
DSL for JavaScript/TypeScript. It compiles to OpenQASM 3.0 (with 2.0
compatibility) and features fluent chaining, context-aware loop scopes, and
structural pipelines.
Table of Contents
Environment & Setup
Installing as a library
npm install @quantum-js/dsl
import { circuit, pipeline, pi, div, mult } from '@quantum-js/dsl';
The Bench (live IDE)
The Bench at quantumjs.netlify.app is a
sandboxed eval environment. Only the Quantum global is available:
const c = Quantum.circuit({ qubits: 2 }, Q => {
Q.bit(0).h();
Q.all().measure();
});
return c;
Not available in the Bench: import, require, circuit() bare function,
pipeline() bare function โ use Quantum.circuit() and Quantum.pipeline().
Circuit Constructor
circuit({ qubits: 3 }, Q => { ... });
circuit({ qubits: 3, bits: 3 }, Q => { ... });
circuit({ qubits: 3, version: '2.0' }, Q => { ... });
Compile:
c.compile();
c.compile({ version: '2.0' });
The callback naming convention is Q (for the Circuit instance):
const c = circuit({ qubits: 2 }, Q => {
});
Qubit Selection
Every gate method is called on a qubit proxy โ an object representing one
or more qubits:
Q.bit(0);
Q.bits([0, 2, 3]);
Q.all();
Q.first();
Q.last();
Complete Gate Reference
Each entry shows the QuantumJS call and its exact OpenQASM 3.0 output.
Single-Qubit Gates
| Method | QASM output | Notes |
|---|
Q.bit(i).h() | h q[i]; | Hadamard |
Q.bit(i).x() | x q[i]; | Pauli X (NOT) |
Q.bit(i).y() | y q[i]; | Pauli Y |
Q.bit(i).z() | z q[i]; | Pauli Z |
Q.bit(i).s() | s q[i]; | Phase (โZ) |
Q.bit(i).s_() | sdg q[i]; | Sโ (inverse phase) |
Q.bit(i).t() | t q[i]; | T (ฯ/8, โS) |
Q.bit(i).t_() | tdg q[i]; | Tโ (inverse T) |
Q.bit(i).id() | id q[i]; | Identity |
Q.bit(i).reset() | reset q[i]; | Reset to |0โฉ |
Rotation & Parametric Gates
| Method | QASM output | Notes |
|---|
Q.bit(i).u([t, p, l]) | U(t, p, l) q[i]; | Universal gate (3 Euler angles) |
Q.bit(i).rx(theta) | rx(theta) q[i]; | Rx rotation |
Q.bit(i).ry(theta) | ry(theta) q[i]; | Ry rotation |
Q.bit(i).rz(phi) | rz(phi) q[i]; | Rz rotation |
Parameters can be:
- Raw JS numbers:
Q.bit(0).rx(Math.PI / 2)
- ฯ expressions:
Q.bit(0).rx(Q.ฯ.div(2))
- Module-level:
Q.bit(0).rx(mult(pi, 0.5)) (requires import { pi, mult })
Controlled Gates
First argument is always the target qubit. Control comes from the proxy.
| Method | QASM output | Notes |
|---|
.cx(target) | cx q[ctrl], q[tgt]; | CNOT |
.cnot(target) | cx q[ctrl], q[tgt]; | Alias for cx |
.cy(target) | cy q[ctrl], q[tgt]; | Controlled-Y |
.cz(target) | cz q[ctrl], q[tgt]; | Controlled-Z |
.ch(target) | ch q[ctrl], q[tgt]; | Controlled-H |
.cp(target, theta) | cp(theta) q[ctrl], q[tgt]; | Controlled phase |
.cu1(target, theta) | cp(theta) q[ctrl], q[tgt]; | Alias for cp |
.swap(target) | swap q[a], q[b]; | Swap two qubits |
Chained controlled gates (control multiple targets):
Q.bit(0).cx(Q.bit(1)).cx(Q.bit(2));
Q.bits([0, 1]).cx(Q.bits([2, 3]));
Self-control is silently skipped: Q.bit(0).cx(Q.bit(0)) produces nothing.
Multi-Controlled Gates
| Method | QASM output | Notes |
|---|
.ccx(b, c) | ccx q[a], q[b], q[c]; | Toffoli gate |
.toffoli(b, c) | ccx q[a], q[b], q[c]; | Alias for ccx |
Argument order: The calling proxy is the first control, b is the second
control, c is the target:
Q.bit(0).ccx(Q.bit(1), Q.bit(2));
Overlapping qubits are silently skipped: Q.bit(0).ccx(Q.bit(1), Q.bit(0))
produces nothing (control and target cannot be the same qubit).
Special Operations
Q.barrier();
Q.barrier([0, 1]);
repeat helper โ applies a gate multiple times:
Q.bit(0).repeat(3, 'h');
Flexible Input
The input() method prepares initial state without manual X gates:
Q.input("101");
Q.input("101", { endian: 'little' });
Q.input("XXIZI");
Q.input(['X', 'H', 'S']);
Q.input((Q) => { Q.bit(0).h(); Q.bit(1).x(); });
Behavior: 0 and I are always skipped (ground state โ no gate needed).
1 maps to X, other letters map to their corresponding gate.
Measurement
Q.bit(0).measure();
Q.bit(1).measureTo(0);
Q.bit(1).measureTo(cbit);
Q.all().measure();
Q.bit(0).measureX();
Q.bit(0).measureY();
Q.bit(0).measureZ();
Q.bit(0).measureW();
Q.bit(0).measureV();
Math Helpers (ฯ expressions)
Two levels of ฯ arithmetic:
Inside the circuit callback (via Q.ฯ)
Q.bit(0).rz(Q.ฯ.div(4));
Q.bit(0).rx(Q.ฯ.mult(0.5));
Q.bit(0).ry(Q.ฯ.times(0.75));
At module level (standalone functions)
import { pi, div, mult } from '@quantum-js/dsl';
Q.bit(0).rz(div(pi, 2));
Q.bit(0).rx(mult(pi, 0.25));
Important: pi at module level is a bare AST expression object, not a
wrapper โ pi.mult() does NOT exist. Use mult(pi, 0.5) instead.
Scoped Layouts (Staircases)
Context-aware loops that dynamically manage sizes, offsets, and iteration
context. These are QuantumJS's most idiomatic feature.
Each scope provides these context properties:
q.iteration โ absolute qubit index of the current sub-circuit's active qubit
q.size โ number of qubits in the current sub-circuit
q.offset โ starting qubit index of this sub-circuit
q.parentSpan โ total qubits in the parent circuit
q.inverseSpan โ distance from the bottom of the parent circuit
growDown โ Top-aligned, growing
Q.growDown(q => {
q.first().cx(q.last());
});
growUp โ Bottom-aligned, growing
Q.growUp(q => {
q.first().cx(q.last());
});
shrinkUp โ Top-aligned, shrinking
Q.shrinkUp(q => {
q.last().h();
});
shrinkDown โ Bottom-aligned, shrinking
Q.shrinkDown(q => {
q.last().h();
});
When to Use Each Staircase
| Pattern | Use | Example |
|---|
| Controlled gates cascade growing from top | growDown | CNOT ladder: cx(0,1), cx(0,2), cx(0,3) |
| Controlled gates cascade growing from bottom | growUp | CNOT ladder bottom-anchored |
| Rotations shrinking from the top | shrinkUp | QFT โ most significant qubit first |
| Rotations shrinking from the bottom | shrinkDown | Inverse QFT |
Nested Scopes (QFT Pattern)
Nesting staircases creates the canonical QFT structure. The outer scope
iterates over qubits from most-to-least significant. The inner scope applies
controlled phase rotations to all previously-iterated qubits:
Q.shrinkUp(q => {
Q.shrinkDown(r => {
if (r.iteration < q.iteration) {
r.last().cp(
r.first(),
Q.ฯ.div(2 ** (1 + q.iteration - r.iteration))
);
}
});
q.last().h().brk();
});
What this produces (4 qubits):
h q[3]; โ shrinkUp iteration 0
cp(pi/2) q[3], q[2]; โ shrinkDown r.iteration=0, q.iteration=1
h q[2];
cp(pi/4) q[3], q[1]; โ shrinkDown r.iteration=0, q.iteration=2
cp(pi/2) q[2], q[1];
h q[1];
cp(pi/8) q[3], q[0]; โ shrinkDown r.iteration=0, q.iteration=3
cp(pi/4) q[2], q[0];
cp(pi/2) q[1], q[0];
h q[0];
Loop Helper (simple repeat, no staircase context)
Q.loop(4, q => {
q.bit(0).h().cx(q.bit(1));
});
Chaining Semantics
Each gate method returns the same qubit proxy, so operations compose linearly:
Q.bit(0).h().x().y().z();
Q.bits([0, 2]).x().y();
Q.bit(0).h().cx(Q.bit(1)).measure();
Custom Functions
Register reusable functions on the circuit instance:
const c = circuit({ qubits: 4 }, Q => {
Q.addFunction('swapGate', (q, a, b) => {
q.bit(a).cx(q.bit(b));
q.bit(b).cx(q.bit(a));
q.bit(a).cx(q.bit(b));
});
Q.fnc.swapGate(0, 1);
Q.fnc.swapGate(2, 3);
});
Helper Functions Pattern
For complex circuits, define external helpers that take Q as a parameter.
This is the pattern used by QASMBench-migrated circuits:
function rx(Q, qubit, theta) {
Q.bit(qubit).u([theta, -Math.PI / 2, Math.PI / 2]);
}
function ry(Q, qubit, theta) {
Q.bit(qubit).u([theta, 0, 0]);
}
function rz(Q, qubit, phi) {
Q.bit(qubit).u([0, 0, phi]);
}
function u3(Q, qubit, theta, phi, lambda) {
Q.bit(qubit).u([theta, phi, lambda]);
}
function phasedISWAP(Q, a, b, k) {
const p = Math.PI;
rz(Q, a, p * 0.25);
rz(Q, b, p * -0.25);
Q.bit(a).cx(Q.bit(b));
Q.bit(a).h();
Q.bit(b).cx(Q.bit(a));
rz(Q, a, p * k);
Q.bit(b).cx(Q.bit(a));
rz(Q, a, p * -k);
Q.bit(a).h();
Q.bit(a).cx(Q.bit(b));
rz(Q, a, p * -0.25);
rz(Q, b, p * 0.25);
}
const c = circuit({ qubits: 4 }, Q => {
phasedISWAP(Q, 0, 1, -0.5);
phasedISWAP(Q, 2, 3, 0.5);
Q.all().measure();
});
This pattern is essential for translating Cirq/Qiskit circuits that use
decompositions of gates like PhasedISWAP, ZZ**k, YY**k, etc.
Conditionals
Q.bit(0).measure();
Q.bit(1).x()._if(Q.cbit(0));
Q.bit(1)._if(Q.cbit(0), q => q.x());
Q.bit(1).x()._if(Q.cbit(0).isFalse());
Important: The _if() conditions the preceding gate when used without
a callback. With a callback, the condition wraps the callback body.
Pipeline Abstraction
import { pipeline } from '@quantum-js/dsl';
const job = pipeline(
{ qubits: 3 },
"101",
Q => Q.all().measure(),
Q => {
Q.bit(0).cx(Q.bit(1));
}
);
job.compile();
job.run(simulator);
Comments & Breaks
Q.comment("3-bit Quantum Fourier Transform");
Q.brk();
Uses: annotating algorithm stages, visual separation in the QASM output.
Expressiveness Spectrum
QuantumJS supports multiple levels of expressiveness. Choose the style that
matches the user's needs.
Level 1: Idiomatic (staircases, input(), high-level)
Best for clean, maintainable circuits. Uses scoped layouts, input(), and
fluent chaining. This is the preferred style.
const c = circuit({ qubits: 4 }, Q => {
Q.input("1011");
Q.barrier();
Q.shrinkUp(q => {
Q.shrinkDown(r => {
if (r.iteration < q.iteration) {
r.last().cp(
r.first(),
Q.ฯ.div(2 ** (1 + q.iteration - r.iteration))
);
}
});
q.last().h().brk();
});
Q.barrier();
Q.all().measure();
});
return c;
Level 2: Gate-by-gate (QASM-like, explicit)
Best for direct translations from QASM or Qiskit, or when the user wants to
see the exact gate sequence. Uses explicit index-based operations.
const c = circuit({ qubits: 4 }, Q => {
Q.bit(3).h();
Q.bit(2).cp(Q.bit(3), Q.ฯ.div(8));
Q.bit(2).h();
Q.bit(1).cp(Q.bit(3), Q.ฯ.div(16));
Q.bit(1).cp(Q.bit(2), Q.ฯ.div(8));
Q.bit(1).h();
Q.bit(0).cp(Q.bit(3), Q.ฯ.div(32));
Q.bit(0).cp(Q.bit(2), Q.ฯ.div(16));
Q.bit(0).cp(Q.bit(1), Q.ฯ.div(8));
Q.bit(0).h();
Q.all().measure();
});
return c;
When to use each level
| User request | Style | Rationale |
|---|
| "Translate this QASM" | Level 2 (gate-by-gate) | Mirrors the source structure, easy to verify |
| "Write a QFT circuit" | Level 1 (idiomatic) | Shorter, intent-revealing, maintainable |
| "Translate this Qiskit code" | Level 2 (gate-by-gate) | Direct mapping from Qiskit calls |
| "Optimize / refactor this circuit" | Level 1 (idiomatic) | Scoped layouts often produce cleaner structure |
| "Educational / tutorial" | Both | Show both styles so the user learns the range |
Rule of thumb: default to Level 1 (idiomatic) unless the user explicitly
asks for a literal QASM translation or provides QASM/Qiskit source.
Translation Guides
Qiskit โ QuantumJS
| Qiskit | QuantumJS |
|---|
QuantumCircuit(2, 2) | circuit({ qubits: 2, bits: 2 }, Q => { ... }) |
qc.h(0) | Q.bit(0).h() |
qc.x(0) | Q.bit(0).x() |
qc.y(0) | Q.bit(0).y() |
qc.z(0) | Q.bit(0).z() |
qc.s(0) | Q.bit(0).s() |
qc.sdg(0) | Q.bit(0).s_() |
qc.t(0) | Q.bit(0).t() |
qc.tdg(0) | Q.bit(0).t_() |
qc.cx(0, 1) | Q.bit(0).cx(Q.bit(1)) |
qc.cy(0, 1) | Q.bit(0).cy(Q.bit(1)) |
qc.cz(0, 1) | Q.bit(0).cz(Q.bit(1)) |
qc.ch(0, 1) | Q.bit(0).ch(Q.bit(1)) |
qc.swap(0, 1) | Q.bit(0).swap(Q.bit(1)) |
qc.ccx(0, 1, 2) | Q.bit(0).ccx(Q.bit(1), Q.bit(2)) |
qc.rz(pi/2, 0) | Q.bit(0).rz(Q.ฯ.div(2)) |
qc.rx(pi/2, 0) | Q.bit(0).rx(Q.ฯ.div(2)) |
qc.ry(pi/2, 0) | Q.bit(0).ry(Q.ฯ.div(2)) |
qc.cp(pi/2, 0, 1) | Q.bit(0).cp(Q.bit(1), Q.ฯ.div(2)) |
qc.u(pi, pi/2, 0, 0) | Q.bit(0).u([Math.PI, Math.PI/2, 0]) |
qc.measure(0, 0) | Q.bit(0).measureTo(0) or Q.bit(0).measure() |
qc.measure_all() | Q.all().measure() |
qc.barrier() | Q.barrier() |
qc.reset(0) | Q.bit(0).reset() |
qc.x(0).c_if(c, 1) | Q.bit(0).x()._if(Q.cbit(0)) |
initialize('101', [0,1,2]) | Q.input("101") |
for i in range(n): | Q.loop(n, q => { ... }) or JS for loop |
High-level pattern mapping:
| Qiskit concept | QuantumJS alternative |
|---|
with qc.if_test( ... ) | Q.bit(0)._if(Q.cbit(0), q => q.x()) |
qc.append(custom_gate, [q0, q1]) | Q.fnc.myGate(0, 1) after Q.addFunction(...) |
QuantumCircuit.compose() | Pipeline or sub() scopes |
qc.initialize() | Q.input() |
AerSimulator.run(qc) | Pipeline's .run(simulator) |
OpenQASM โ QuantumJS
| OpenQASM 3.0 | QuantumJS |
|---|
h q[0]; | Q.bit(0).h() |
x q[0]; | Q.bit(0).x() |
y q[0]; | Q.bit(0).y() |
z q[0]; | Q.bit(0).z() |
s q[0]; | Q.bit(0).s() |
sdg q[0]; | Q.bit(0).s_() |
t q[0]; | Q.bit(0).t() |
tdg q[0]; | Q.bit(0).t_() |
id q[0]; | Q.bit(0).id() |
reset q[0]; | Q.bit(0).reset() |
U(theta, phi, lambda) q[0]; | Q.bit(0).u([theta, phi, lambda]) |
rx(theta) q[0]; | Q.bit(0).rx(theta) |
ry(theta) q[0]; | Q.bit(0).ry(theta) |
rz(phi) q[0]; | Q.bit(0).rz(phi) |
cx q[0], q[1]; | Q.bit(0).cx(Q.bit(1)) |
cy q[0], q[1]; | Q.bit(0).cy(Q.bit(1)) |
cz q[0], q[1]; | Q.bit(0).cz(Q.bit(1)) |
ch q[0], q[1]; | Q.bit(0).ch(Q.bit(1)) |
cp(theta) q[0], q[1]; | Q.bit(0).cp(Q.bit(1), theta) |
swap q[0], q[1]; | Q.bit(0).swap(Q.bit(1)) |
ccx q[0], q[1], q[2]; | Q.bit(0).ccx(Q.bit(1), Q.bit(2)) |
measure q[0] -> c[0]; | Q.bit(0).measure() or Q.bit(0).measureTo(0) |
c[0] = measure q[0]; | Q.bit(0).measure() |
barrier q[0], q[1]; | Q.barrier([0, 1]) |
if (c == 1) x q[0]; | Q.bit(0).x()._if(Q.cbit(0)) |
// comment | Q.comment("comment") |
Full QFT Translation Walkthrough
QASM input:
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit[3] c;
h q[2];
cp(pi/2) q[2], q[1];
h q[1];
cp(pi/4) q[2], q[0];
cp(pi/2) q[1], q[0];
h q[0];
c = measure q;
Step-by-step translation:
| QASM line | Translation | Reasoning |
|---|
h q[2]; | Q.bit(2).h() | Direct: .h() on qubit 2 |
cp(pi/2) q[2], q[1]; | Q.bit(2).cp(Q.bit(1), Q.ฯ.div(2)) | .cp(target, theta), control is the proxy's qubit |
h q[1]; | Q.bit(1).h() | Direct |
cp(pi/4) q[2], q[0]; | Q.bit(2).cp(Q.bit(0), Q.ฯ.div(4)) | Control q[2], target q[0] |
cp(pi/2) q[1], q[0]; | Q.bit(1).cp(Q.bit(0), Q.ฯ.div(2)) | Control q[1], target q[0] |
h q[0]; | Q.bit(0).h() | Direct |
c = measure q; | Q.all().measure() | Measure all |
Result:
const c = circuit({ qubits: 3 }, Q => {
Q.bit(2).h();
Q.bit(2).cp(Q.bit(1), Q.ฯ.div(2));
Q.bit(1).h();
Q.bit(2).cp(Q.bit(0), Q.ฯ.div(4));
Q.bit(1).cp(Q.bit(0), Q.ฯ.div(2));
Q.bit(0).h();
Q.all().measure();
});
Idiomatic improvement (using staircases):
const c = circuit({ qubits: 3 }, Q => {
Q.shrinkUp(q => {
Q.shrinkDown(r => {
if (r.iteration < q.iteration) {
r.last().cp(r.first(),
Q.ฯ.div(2 ** (1 + q.iteration - r.iteration)));
}
});
q.last().h().brk();
});
Q.all().measure();
});
Bench Environment
When writing code for the Bench:
- Use
Quantum.circuit(...) not bare circuit(...). Only Quantum is global.
- Always
return c; โ the Bench expects the circuit object back.
- All samples use
.js extension โ they're regular JavaScript, no special dialect.
- Samples are registered in
src/sampleRegistry.ts. Each entry needs an import + path.
- Samples live in
src/samples/ โ the raw-loader is scoped to that directory.
Idiomatic Style Guide
- Prefer staircases over for-loops for QFT, IQFT, and triangular structures.
- Use
Q.input() instead of manual X gates for binary state prep.
- Chain calls where natural:
Q.bit(0).h().cx(Q.bit(1)).measure().
- Use
Q.all() for operations on every qubit.
- Name the circuit callback parameter
Q by convention.
- Always
return c; at the end.
- Use
Q.ฯ.div(n) and Q.ฯ.mult(n) over raw Math.PI / n for readable QASM.
- Wrap decompositions in helper functions for circuits with repeated gate patterns.
- Use
Q.comment() to label algorithm stages.
- Prefer explicit qubit indices over hard-to-read
.bits() on large selections.
Common Pitfalls
| Pitfall | Why | Fix |
|---|
Q.bit(0).cx(Q.bit(1)).cx(Q.bit(0)) | Second cx has ctrl=q[0], tgt=q[0] โ skipped | Ensure target != control |
Q.bits([0,1]).cx(Q.bits([0,1])) | Self-mapping: cx(0,0) and cx(1,1) โ both skipped | Use Q.all().cx(Q.bit(0)) for broadcast to single target |
Using pi.mult(0.5) at module level | pi is a bare AST object, not a wrapper | Use mult(pi, 0.5) or inside callback Q.ฯ.mult(0.5) |
Forgetting return c; in the Bench | Bench needs the circuit object | Always end with return c; |
| Importing in the Bench | import is not available | Use Quantum.circuit() instead |
Using .quantumjs extension | Misleading โ looks like a custom dialect | Use .js extension |
Expecting u(...) in QASM output | Lowercase u is QASM 2.0 style | The DSL emits U(...) (uppercase, QASM 3.0) |