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

Method

Recorded as

Description

h()

Gate('H')

Hadamard

x()

Gate('X')

Pauli-X

y()

Gate('Y')

Pauli-Y

z()

Gate('Z')

Pauli-Z

s()

Gate('S')

Phase, √Z

sdg()

Gate('SDG')

S†

t()

Gate('T')

π/8 phase

tdg()

Gate('TDG')

T†

Parametric single-qubit gates

Method

Recorded as

Description

rx() (theta, qubit)

Gate('RX', params=[theta])

Rotation about X

ry() (theta, qubit)

Gate('RY', params=[theta])

Rotation about Y

rz() (theta, qubit)

Gate('RZ', params=[theta])

Rotation about Z

Angles are in radians. Note the argument order: the angle comes first.

Two- and three-qubit gates

Method

Recorded as

Description

cx() (control, target)

ControlledGate([c], [Gate('X')])

CNOT

cz() (control, target)

ControlledGate([c], [Gate('Z')])

Controlled-Z

cs() (control, target)

ControlledGate([c], [Gate('S')])

Controlled-S

ct() (control, target)

ControlledGate([c], [Gate('T')])

Controlled-T

cp() (control, target, theta)

ControlledGate([c], [Gate('P', params=[theta])])

Controlled phase

crz() (theta, control, target)

ControlledGate([c], [Gate('RZ', params=[theta])])

Controlled-RZ

swap() (qubit1, qubit2)

Gate('SWAP')

SWAP

ccx() (c1, c2, target)

ControlledGate([c1, c2], [Gate('X')])

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() (qubit, cbit)

Measure one qubit into one classical bit

measure_all() ()

Measure qubit i into classical bit i, for every data qubit

reset() (qubit)

Reset a qubit to `

barrier() (qubits=None)

Insert a barrier; None means the whole circuit

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

h x y z s t

✅

✅

✅

✅

sdg tdg

✅

❌

✅

✅

rx ry rz

✅

⚠️ [1]

✅

⚠️ [2]

swap

✅

∅ [3]

✅

❌

cx cz

✅

❌ [4]

✅

❌ [5]

cs ct cp

✅

❌ [4]

∅ [6]

❌ [5]

crz

✅

❌ [4]

✅

⚠️ [5]

ccx

✅

❌ [4]

✅

❌

measure measure_all

✅

✅

✅

✅

reset

✅

❌

✅

❌

barrier

❌

❌

✅

❌

Classically controlled gates

❌

❌

❌

❌

qsend / qrecv

✅

✅

⚠️ [7]

✅

qscatter / qgather

✅

❌

❌

❌

expose / unexpose

✅

❌

❌

❌

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.