Circuits and gates

Building circuits

Circuit stores a list of operations. Gates are attributes; qubits are selected with [...]; parametric gates take their parameters as a call before the qubit indexing. Everything chains:

import math
from blueqat import Circuit

Circuit().h[0].cx[0, 1].rz(math.pi / 4)[1].m[:]

Qubit 0 is always the least-significant bit of the statevector index ('10' means qubit 1 is 1, qubit 0 is 0 – the same convention as Qiskit’s Statevector).

Gate set

Single-qubit gates

i, x, y, z, h, s, sdg, t, tdg, sx, sxdg, phase(theta) (aliases p, r), rx(theta), ry(theta), rz(theta), u(theta, phi, lam[, gamma]), mat1(matrix) (arbitrary 2x2 unitary).

Two-qubit gates

cx (alias cnot), cy, cz, ch, swap, iswap, iswapdg, cphase(theta) (aliases cp, cr), crx, cry, crz, cu(theta, phi, lam[, gamma]), rxx(theta), ryy(theta), rzz(theta), zz, zzdg, exch(theta) (Heisenberg exchange pulse, see Exchange-only spin qubits).

Three-qubit gates

ccx (alias toffoli), ccz, cswap.

Other operations

m / measure (optionally m(key="name") for keyed mid-circuit measurement), reset, barrier.

Gates that take no parameters raise ValueError if parameters are passed (e.g. x(0.5)[0] is rejected rather than silently ignored).

Introspection

c = Circuit(3).h[:].cx[0, 1].cx[1, 2].m[:]
c.n_qubits      # 3
c.depth()       # 4  (parallel gates count once; barriers don't count)
c.count_ops()   # Counter({'h': 3, 'cx': 2, 'measure': 3})

Measurement probabilities (differentiable, optionally marginalized onto selected qubits) and Hamiltonian expectation values:

from blueqat.utils import Z

Circuit(2).h[0].cx[0, 1].probs()          # tensor([0.5, 0., 0., 0.5])
Circuit(2).h[0].cx[0, 1].probs([1])       # marginal of qubit 1
Circuit(1).rx(0.4)[0].expect(1.0 * Z[0])  # <Z> = cos(0.4)

Inverse circuits

dagger() returns the Hermitian conjugate (gates reversed and conjugated). Measurement and reset have no inverse; dagger(ignore_measurement=True) drops them instead of raising:

c = Circuit(3)  # ... build ...
identity = c + c.dagger()   # uncomputes back to |0...0>

OpenQASM 2.0

qasm = Circuit(2).h[0].cx[0, 1].to_qasm()

from blueqat.circuit_funcs import from_qasm
c = from_qasm(qasm)

JSON serialization

Circuits round-trip through a versioned, JSON-compatible schema (this is also the cloud submission wire format):

from blueqat.circuit_funcs.json_serializer import serialize, deserialize

data = serialize(Circuit(2).h[0].cx[0, 1])
c = deserialize(data)

Drawing

run(backend='draw') renders the circuit with matplotlib. Every registered gate is drawable; unknown (user-registered) gates are omitted with a UserWarning.

Named gate blocks

Real algorithms are nests of subroutines – Shor’s order finding is initialization, controlled modular multiplications and an inverse QFT, each built from smaller pieces. Named blocks keep that structure in the circuit object without changing execution (every backend transparently sees the inner gates):

c = Circuit(7)
with c.block("order-finding"):
    with c.block("superposition"):
        c.h[4, 5, 6]
    with c.block("c-U^1"):
        c.cswap[4, 2, 3].cswap[4, 1, 2].cswap[4, 0, 1]
    # place a library circuit as a block, shifted to qubits 4..6
    c.append_block("IQFT", qft_circuit(3).dagger(), offset=4)

print(c.tree())
# Circuit(7)
# └─ order-finding
#    ├─ superposition
#    │  └─ h[4, 5, 6]
#    ├─ c-U^1
#    │  └─ ...
#    └─ IQFT
#       └─ ...

Blocks nest arbitrarily, show up in repr() and tree(), and survive dagger() as mirrored blocks ("order-finding†"). depth() / count_ops() count the contained gates; flatten() / JSON serialization expand blocks into plain gates (the flat wire format keeps no hierarchy). The circuit drawer renders a block as a single labeled box spanning its qubits; a circuit wrapped entirely in one block automatically descends so its child blocks appear as boxes. Pass expand_blocks=n to open n levels of blocks, or expand_blocks=True to draw every inner gate. See examples/shor_15.py for a complete Shor-at-15 program written this way.

Ancilla qubits

c = Circuit(4).h[:]
with c.ancilla() as a:        # allocates a fresh qubit
    c.cx[0, a[0]]
    c.cx[0, a[0]]
# the ancilla is reset to |0> on exit (reset=True by default)

Macros and custom gates

Register a function as a circuit method, or a gate class into the gate set:

from blueqat import BlueqatGlobalSetting
from blueqat.decorators import circuitmacro

@circuitmacro
def bell(c, a, b):
    return c.h[a].cx[a, b]

Circuit(2).bell(0, 1)

BlueqatGlobalSetting.register_gate('mygate', MyGateClass)