Circuits and gates¶
A circuit is created through the environment, never constructed directly:
circuit = env.create_circuit(num_qubits=2, num_clbits=2)
What comes back is a backend-specific subclass of
Circuit behind a uniform interface. Every gate
method returns the circuit, so calls chain:
circuit.h(0).cx(0, 1).measure(0, 0).measure(1, 1)
Qubit indexing¶
Indices span two ranges:
0 .. num_qubits-1 data qubits (the ones you asked for)
num_qubits .. num_qubits+num_comm-1 communication qubits (runtime-owned)
Both are accepted by the gate API, so a control borrowed from a remote rank
through expose() is used exactly like a local
qubit. You never compute a communication index yourself — expose returns it,
and comm_qubit() converts a slot to an index
if you need it.
Indices are validated as you build:
IndexError: Qubit index 5 out of range [0, 2) (2 data + 0 comm qubits).
IndexError: Qubit index 1 is a communication qubit whose expose window is already closed.
IndexError: Qubit index 3 is not a data qubit (expected [0, 2)).
The last one comes from operations that only accept data qubits: the rooted collectives, and the qubit a root exposes. Communication qubits belong to the protocols and are gone by the time the block is over, so they cannot be moved.
Gate reference¶
Single-qubit gates¶
Parametric single-qubit gates¶
Method |
Recorded as |
Description |
|---|---|---|
|
|
Rotation about X |
|
|
Rotation about Y |
|
|
Rotation about Z |
Angles are in radians. Note the argument order: the angle comes first.
Two- and three-qubit gates¶
Method |
Recorded as |
Description |
|---|---|---|
|
|
CNOT |
|
|
Controlled-Z |
|
|
Controlled-S |
|
|
Controlled-T |
|
|
Controlled phase |
|
|
Controlled-RZ |
|
|
SWAP |
|
|
Toffoli |
cp generalises cs (θ = π/2) and ct (θ = π/4), which is what the crossing
rotations of a QFT are made of — see
5_qft_expose.py.
Note
cp takes its angle last (cp(control, target, theta)) while crz takes it
first (crz(theta, control, target)), matching rx/ry/rz. The
inconsistency is in the API as it stands.
Non-unitary operations¶
Method |
Description |
|---|---|
|
Measure one qubit into one classical bit |
Measure qubit i into classical bit i, for every data qubit |
|
|
Reset a qubit to ` |
|
Insert a barrier; |
measure_all refuses to run if the circuit is short of classical bits:
ValueError: Not enough classical bits to measure all qubits (1 clbits < 3 qubits).
Backend support matrix¶
Not every backend implements every operation. The SDK accepts all of them at trace time; a backend that cannot express one reports it at translation time.
Legend: ✅ supported · ❌ raises NotImplementedError ·
⚠️ see the note · ∅ silently ignored — no instruction is emitted and
no error is raised.
Operation |
CUNQA |
NetQASM |
Aer |
Qoala |
|---|---|---|---|---|
|
✅ |
✅ |
✅ |
✅ |
|
✅ |
❌ |
✅ |
✅ |
|
✅ |
⚠️ [1] |
✅ |
⚠️ [2] |
|
✅ |
∅ [3] |
✅ |
❌ |
|
✅ |
❌ [4] |
✅ |
❌ [5] |
|
✅ |
❌ [4] |
∅ [6] |
❌ [5] |
|
✅ |
❌ [4] |
✅ |
⚠️ [5] |
|
✅ |
❌ [4] |
✅ |
❌ |
|
✅ |
✅ |
✅ |
✅ |
|
✅ |
❌ |
✅ |
❌ |
|
❌ |
❌ |
✅ |
❌ |
Classically controlled gates |
❌ |
❌ |
❌ |
❌ |
|
✅ |
✅ |
⚠️ [7] |
✅ |
|
✅ |
❌ |
❌ |
❌ |
|
✅ |
❌ |
❌ |
❌ |
If in doubt, use CUNQA
CUNQA is the only backend that implements the whole primitive set, and the one the shipped examples are written against. Start there, then port to the simulator that matches the physics you want to study.
Operations under the hood¶
Every call appends an Operation — a
Command object — to the circuit’s
OperationContainer. The container is
a Composite, so a block that means more than its parts (a qscatter, which
expands into individual transfers) stays a sub-container rather than being
flattened away.
for op in circuit: # depth-first over leaf operations
print(op)
# Gate(H, qubits=[0])
# QSend(qubits=[0], dest_rank=1, ...)
len(circuit) # top-level entries, not flattened
circuit.ops.children # direct children, nesting preserved
This is the representation a backend adapter consumes; see Writing a backend.