Writing Custom Passes¶
qb-compiler’s pass infrastructure is designed for extensibility. You can write custom analysis or transformation passes and compose them with built-in passes.
Pass Contract¶
Every pass follows a strict contract:
__init__takes configuration only — no circuit datarun(circuit, context)returns aPassResultAnalysis passes override
analyze(circuit, context)— read-onlyTransformation passes override
transform(circuit, context)— may modify
Base Classes¶
There are two base classes to choose from:
AnalysisPass— overrideanalyze(circuit, context) -> None. Writes results tocontextbut does not modify the circuit.TransformationPass— overridetransform(circuit, context) -> PassResult. May return a new circuit.
Example: Analysis Pass — Gate Frequency¶
from qb_compiler.passes.base import AnalysisPass
from qb_compiler.ir.circuit import QBCircuit
class GateFrequencyAnalysis(AnalysisPass):
"""Count how many times each gate type appears."""
@property
def name(self) -> str:
return "GateFrequencyAnalysis"
def analyze(self, circuit: QBCircuit, context: dict) -> None:
freq: dict[str, int] = {}
for gate in circuit.gates:
freq[gate.name] = freq.get(gate.name, 0) + 1
context["gate_frequencies"] = freq
Usage:
from qb_compiler.ir.circuit import QBCircuit
from qb_compiler.ir.operations import QBGate
circ = QBCircuit(n_qubits=2, n_clbits=0)
circ.add_gate(QBGate("h", (0,)))
circ.add_gate(QBGate("cx", (0, 1)))
analysis = GateFrequencyAnalysis()
ctx = {}
analysis.run(circ, ctx)
print(ctx["gate_frequencies"])
# {'h': 1, 'cx': 1}
Example: Transformation Pass — Identity Gate Removal¶
from qb_compiler.passes.base import TransformationPass, PassResult
from qb_compiler.ir.circuit import QBCircuit
class IdentityGateRemoval(TransformationPass):
"""Remove identity (id) gates from the circuit."""
@property
def name(self) -> str:
return "IdentityGateRemoval"
def transform(self, circuit: QBCircuit, context: dict) -> PassResult:
non_id_gates = [g for g in circuit.gates if g.name != "id"]
removed = len(list(circuit.gates)) - len(non_id_gates)
if removed == 0:
return PassResult(circuit=circuit, modified=False)
result = QBCircuit(
n_qubits=circuit.n_qubits,
n_clbits=circuit.n_clbits,
name=circuit.name,
)
for gate in non_id_gates:
result.add_gate(gate)
for m in circuit.measurements:
result.add_measurement(m.qubit, m.clbit)
context["identity_gates_removed"] = removed
return PassResult(circuit=result, modified=True)
Example: Redundant Gate Removal¶
This pass removes adjacent pairs of self-inverse gates (H-H, X-X, CX-CX on the same qubits):
from qb_compiler.passes.base import TransformationPass, PassResult
from qb_compiler.ir.circuit import QBCircuit
SELF_INVERSE = frozenset({"h", "x", "y", "z", "cx", "cz", "swap"})
class RedundantGateRemoval(TransformationPass):
"""Remove adjacent pairs of self-inverse gates."""
@property
def name(self) -> str:
return "RedundantGateRemoval"
def transform(self, circuit: QBCircuit, context: dict) -> PassResult:
gates = list(circuit.gates)
new_gates = []
modified = False
i = 0
while i < len(gates):
if (
i + 1 < len(gates)
and gates[i].name in SELF_INVERSE
and gates[i].name == gates[i + 1].name
and gates[i].qubits == gates[i + 1].qubits
):
i += 2 # Skip both gates
modified = True
else:
new_gates.append(gates[i])
i += 1
if not modified:
return PassResult(circuit=circuit, modified=False)
result = QBCircuit(
n_qubits=circuit.n_qubits,
n_clbits=circuit.n_clbits,
name=circuit.name,
)
for gate in new_gates:
result.add_gate(gate)
for m in circuit.measurements:
result.add_measurement(m.qubit, m.clbit)
context["redundant_gates_removed"] = len(gates) - len(new_gates)
return PassResult(circuit=result, modified=True)
Composing Passes with PassManager¶
Use PassManager to chain passes together:
from qb_compiler.passes.base import PassManager
pm = PassManager([
GateFrequencyAnalysis(),
IdentityGateRemoval(),
RedundantGateRemoval(),
GateFrequencyAnalysis(), # Re-analyze after cleanup
])
result = pm.run_all(circuit)
# result.circuit is the cleaned circuit
# result.metadata["passes"] has timing for each pass
Using Custom Passes with QBCompiler¶
You can run custom passes on compiled output:
from qb_compiler import QBCompiler, QBCircuit
compiler = QBCompiler.from_backend("ibm_fez")
circ = QBCircuit(2).h(0).cx(0, 1).measure_all()
result = compiler.compile(circ)
# Run a custom analysis on the compiled circuit
analysis = GateFrequencyAnalysis()
ctx = {}
analysis.run(result.compiled_circuit, ctx)
print(ctx["gate_frequencies"])
Testing Custom Passes¶
Test with known circuits and expected outcomes:
def test_identity_removal():
from qb_compiler.ir.circuit import QBCircuit
from qb_compiler.ir.operations import QBGate
circ = QBCircuit(n_qubits=2, n_clbits=0)
circ.add_gate(QBGate("h", (0,)))
circ.add_gate(QBGate("id", (1,)))
circ.add_gate(QBGate("cx", (0, 1)))
pass_ = IdentityGateRemoval()
result = pass_.run(circ, {})
assert result.modified is True
assert result.circuit.gate_count == 2 # h + cx
def test_no_ids_is_noop():
circ = QBCircuit(n_qubits=1, n_clbits=0)
circ.add_gate(QBGate("h", (0,)))
pass_ = IdentityGateRemoval()
result = pass_.run(circ, {})
assert result.modified is False
Tips¶
Keep passes focused — one pass, one transformation.
Use
AnalysisPassfor read-only passes,TransformationPassfor mutations.Test with edge cases: empty circuits, single-gate circuits, measurements.
Passes should be idempotent — running twice produces the same result.
The
contextdict is shared across all passes in aPassManager. Use descriptive keys to avoid collisions.