Skip to content

Commit 457a351

Browse files
Improving Circuit API (#587)
This PR addresses #580. The PR comprises three blocks: ## Block 1: Introduction of ancilla qubits Concerns `graphix.transpiler.py` only. **Now it's possible to distinguish between input and ancilla qubits in circuits.** - Two new attributes are added to the `Circuit` class: - `ancillas: int` to indicate the number of ancilla qubits. Ancilla qubits are indexed from `range(width, width + ancillas - 1)`, where `width` is the number of input qubits (existing attribute). - `ancilla_state: State` to indicate the state of the ancilla qubits. - Supported operations on circuits with ancillas: - Simulation: always - Transpilation to pattern or causal flow: only if `ancilla_state` is a `BasicState`: - If `ancilla_state` is `BasicState.PLUS`, ancilla qubits are represented as non-input nodes in the open graph. - Any other `BasicState` is transpiled into `BasicState.PLUS` by prepending the appropriate gates to the circuit (via `Circuit.transpile_ancilla_state`). This is unavoidable because transpilation is carried out by first constructing an open graph with causal flow, and non-input nodes in `OpenGraph` always represent `BasicState.PLUS` qubits, even if Graphix supports `N` commands with an arbitrary state. - QASM 3 export: only if `ancilla_state` is a `BasicState`. Same mechanism as for transpilation but transforming to state |0>. - Note: this addition is a first step towards implementing circuit compilation of PDDAGs representing isometries. ## Block 2: Introduction of conditional instructions Concerns `graphix.instruction.py` and `graphix.transpiler.py`. **Now it's possible to represent conditional instructions in circuits.** - A new instruction `CONDINSTR` is added. This instruction bundles a sequence of instructions (possibly incuding other `CONDINSTR`) and a qubit domain (`set[int]`). The instruction sequence is applied when the XOR of the measurement outcomes of the qubits in ``domain`` evaluates to ``1``. - Two side modifications: - #### `InstructionVisitor` - `InstructionVisitor` now has an additional method `visit_domain`. The method `CONDINSTR.visit` visits the instruction's domain **and** all the instructions in the instruction sequence. - Introduced a new `_InstructionValidatorVisitor(InstructionVisitor)` class. This visitor reimplements the method `visit_qubit` and `visit_domain` so that calling `visit` for a given instruction checks the qubit validity. As a result: 1) Valid-qubit sanity checks are factorized into a single function. 2) There is a generic way to check the validity of any `InstructionKind`, without knowing which specific instruction it is. This allows to verify the validity of conditional instructions containing a sequence of other instructions. - Checking that multi-qubit gates don't act on repeated qubits is delegated to the method `visit` of the corresponding instructions instead of the `Circuit` methods. - #### `graphix.utils.check_kind` - The function `graphix.utils.check_kind` has been slightly modified to fix an error due to the self-reference in the definition of `CONDINSTR`. This modification is an LLM suggestion, it's a bit obscure. - Supported operations on circuits with conditional instructions: - Simulation: always. - Simulation is **very inefficient**: all qubits are instantianted at the beginning. - Exisiting simulation logic is encapsulated in a new function `simulate_instructions` that is called recursively for `CONDINSTR`. - Changes in simulation are minimal to support testing conditional instructions and keeping the PR as small as possible. In a future PR we'll address the following: - Simulation logic should be moved to `simulator` module. - Currently we are rewriting some code exisiting in `MeasureMethod`. - How to handle classical output results ? - Now keeping `classical_outputs` to achieve a minimal PR. - Ideally we should find a common interface for pattern and circuit simulation. - Transpilation to pattern or causal flow: always. - Required logic is encapsulated in `Circuit.transpile_condinstr` that is called from exisiting transpilation methods when there is any conditional instruction in the circuit. - Conditional instructions are replaced by controlled instructions, using measurements of their domain qubits in the $Z$ basis as control conditions. Measurements are moved to the end of the circuit. When the domain of the conditional instruction comprises various qubits (whose measurement outputs are xored), they are combined by chaining CNOT gates). - QASM 3 export: always (implemented by @thierry-martinez). ## Block 3: Introduction of a `Pattern.to_circuit` method. Concerns `graphix.pattern.py` - This method requires the changes in block 1 and block 2. - Logic is similar to existing `Pattern.to_qasm3`, with two differences: - Swap gates are added at the end of circuit to ensure that active qubits are in the same order as output nodes in the pattern. This guarantees that a pattern and its circuit produce the same state up to a phase. - Qubits are labelled with consecutive integers starting from 0. In the qasm export, we label qubits with the same node labels. - `Pattern.to_qasm3` should be refactored to be a wrapper over `Pattern.to_circuit().to_qasm3` --------- Co-authored-by: thierry-martinez <thierry.martinez@inria.fr>
1 parent c2c20f5 commit 457a351

17 files changed

Lines changed: 1703 additions & 496 deletions
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
graphix-qasm-parser @ git+https://github.com/TeamGraphix/graphix-qasm-parser@refs/pull/15/head
1+
#graphix-qasm-parser @ git+https://github.com/TeamGraphix/graphix-qasm-parser@refs/pull/17/head
2+
graphix-qasm-parser @ git+https://github.com/matulni/graphix-qasm-parser@new-circuit-api

‎CHANGELOG.md‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- Support for Python 3.15
1818
- Drop support for Python 3.10 (end-of-life: 2026-10-01)
1919

20+
- #587:
21+
- Added ancilla qubits to `Circuit` API (new attributes `ancillas` and `ancilla_state`).
22+
- Added conditional instructions (`CONDINSTR`). Added simulation, transpilation and qasm-conversion capabilities.
23+
- Added `Pattern.to_circuit` method to convert a `Pattern` into a `Circuit` with ancillas and classical control.
24+
2025
### Fixed
2126

2227
- #591: `FixedBranchSelector` now passes its RNG parameter to its `default` branch selector.
2328

24-
- #595: `RZZ` gates are no longer incorrectly exported as `crz`; transpilation to `CNOT`-`RZ`-`CNOT` is provided.
25-
2629
- #596, #597: Pattern's `n_node` property updated after Pauli removal.
2730

28-
## Changed
31+
- #595: Fixed the export of circuit measurements and `RZZ` gates to OpenQASM. `RZZ` gates are no longer incorrectly exported as `crz`; transpilation to `CNOT`-`RZ`-`CNOT` is provided.
32+
33+
- #592:
34+
- Added the following gates specified in the OpenQASM 3.0 specification: SDG, T, TDG, SX, SXDG, CY, P, U, CP, CRX, CRY, CRZ, CU, CSWAP, and GPHASE.
35+
- Subsumed `Circuit.transpile_measurements_to_z_axis`, `Circuit.transpile_j_to_rzh` and `Circuit.transpile_rzz` by `Circuit.transpile_to_qasm_gates`
2936

30-
- #601: New documentation / tutorial
37+
### Changed
38+
39+
- #587;
40+
- Factorized instruction-validity checks in method `Circuit.add` via an instruction visitor.
41+
- Instruction methods of the `Circuit` class now return `self` allowing method chaining.
3142

43+
3244
## [0.4] - 2026-08-18
3345

3446
### Added

‎COMPATIBILITY.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
Graphix does not guarantee backwards compatibility between 0.x version releases. Here we list API changes which may break existing code using Graphix.
44

5+
## Unreleased
6+
7+
- #592:
8+
- `Circuit.transpile_measurements_to_z_axis`, `Circuit.transpile_j_to_rzh` and `Circuit.transpile_rzz` are subsumed by `Circuit.transpile_to_qasm_gates`
9+
510
## [0.4] - 2026-08-18
611

712
- #484: J & CZ transpilation.

‎graphix/instruction.py‎

Lines changed: 95 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class InstructionKind(Enum):
7070
CU = enum.auto()
7171
CSWAP = enum.auto()
7272
GPHASE = enum.auto()
73+
CONDINSTR = enum.auto()
7374

7475

7576
class _KindChecker:
@@ -106,6 +107,10 @@ def visit_axis(self, axis: Axis) -> Axis:
106107
"""Rewrite an axis."""
107108
return axis
108109

110+
def visit_domain(self, domain: set[int]) -> set[int]:
111+
"""Rewrite a domain of a conditional instruction."""
112+
return domain
113+
109114

110115
class BaseInstruction(ABC, DataclassReprMixin):
111116
"""Base class for circuit instructions."""
@@ -171,6 +176,12 @@ def visit(self, visitor: InstructionVisitor, *, copy: bool = False) -> CCX:
171176
u, v = self.controls
172177
target = visitor.visit_qubit(self.target)
173178
controls = (visitor.visit_qubit(u), visitor.visit_qubit(v))
179+
if controls[0] == controls[1]:
180+
raise ValueError(f"Control qubits cannot be the same. Qubit index: {controls[0]}.")
181+
for i, c in enumerate(controls):
182+
if target == c:
183+
raise ValueError(f"Target and control-{i} qubits cannot be the same. Qubit index: {target}.")
184+
174185
if copy:
175186
return CCX(target, controls)
176187
self.target = target
@@ -218,6 +229,8 @@ class RZZ(_KindChecker, BaseInstruction):
218229
def visit(self, visitor: InstructionVisitor, *, copy: bool = False) -> RZZ:
219230
target = visitor.visit_qubit(self.target)
220231
control = visitor.visit_qubit(self.control)
232+
if target == control:
233+
raise ValueError(f"Target and control qubits cannot be the same. Qubit index: {target}.")
221234
angle = visitor.visit_angle(self.angle)
222235
if copy:
223236
return RZZ(target, control, angle)
@@ -238,6 +251,8 @@ class ControlledSingleTargetInstruction(BaseInstruction):
238251
def visit(self, visitor: InstructionVisitor, *, copy: bool = False) -> Self:
239252
target = visitor.visit_qubit(self.target)
240253
control = visitor.visit_qubit(self.control)
254+
if target == control:
255+
raise ValueError(f"Target and control qubits cannot be the same. Qubit index: {target}.")
241256
if copy:
242257
return type(self)(target, control)
243258
self.target = target
@@ -339,6 +354,8 @@ class CZ(_KindChecker, BaseInstruction):
339354
def visit(self, visitor: InstructionVisitor, *, copy: bool = False) -> CZ:
340355
u, v = self.targets
341356
targets = (visitor.visit_qubit(u), visitor.visit_qubit(v))
357+
if targets[0] == targets[1]:
358+
raise ValueError(f"Target qubits cannot be the same. Qubit index: {targets[0]}.")
342359
if copy:
343360
return CZ(targets)
344361
self.targets = targets
@@ -377,6 +394,8 @@ class SWAP(_KindChecker, BaseInstruction):
377394
def visit(self, visitor: InstructionVisitor, *, copy: bool = False) -> SWAP:
378395
u, v = self.targets
379396
targets = (visitor.visit_qubit(u), visitor.visit_qubit(v))
397+
if targets[0] == targets[1]:
398+
raise ValueError(f"Target qubits cannot be the same. Qubit index: {targets[0]}.")
380399
if copy:
381400
return SWAP(targets)
382401
self.targets = targets
@@ -836,6 +855,8 @@ def visit(self, visitor: InstructionVisitor, *, copy: bool = False) -> Self:
836855
target = visitor.visit_qubit(self.target)
837856
control = visitor.visit_qubit(self.control)
838857
angle = visitor.visit_angle(self.angle)
858+
if target == control:
859+
raise ValueError(f"Target and control qubits cannot be the same. Qubit index: {target}.")
839860
if copy:
840861
return type(self)(target, control, angle)
841862
self.target = target
@@ -1047,12 +1068,70 @@ def visit(self, visitor: InstructionVisitor, *, copy: bool = False) -> GPHASE:
10471068
return self
10481069

10491070

1071+
InstructionTypeWithoutM: TypeAlias = """(
1072+
I
1073+
| X
1074+
| Y
1075+
| Z
1076+
| H
1077+
| S
1078+
| SDG
1079+
| T
1080+
| TDG
1081+
| SX
1082+
| SXDG
1083+
| J
1084+
| P
1085+
| RX
1086+
| RY
1087+
| RZ
1088+
| U
1089+
| CJ
1090+
| CP
1091+
| CRX
1092+
| CRY
1093+
| CRZ
1094+
| CU
1095+
| CNOT
1096+
| CY
1097+
| CZ
1098+
| CCX
1099+
| RZZ
1100+
| SWAP
1101+
| CSWAP
1102+
| GPHASE
1103+
| CONDINSTR
1104+
)"""
1105+
1106+
1107+
@dataclass(repr=False)
1108+
class CONDINSTR(_KindChecker, BaseInstruction):
1109+
"""Base class for conditional circuit instructions.
1110+
1111+
Conditional measurements are not well defined since they would result in circuits with an indeterminate number of qubits. Therefore, the attribute ``instructions`` cannot contain instances of `Instruction.M`.
1112+
"""
1113+
1114+
instructions: tuple[InstructionTypeWithoutM, ...]
1115+
domain: set[int] = field(default_factory=set)
1116+
kind: ClassVar[Literal[InstructionKind.CONDINSTR]] = field(default=InstructionKind.CONDINSTR, init=False)
1117+
1118+
@override
1119+
def visit(self, visitor: InstructionVisitor, *, copy: bool = False) -> Self:
1120+
instructions = tuple(instr.visit(visitor, copy=copy) for instr in self.instructions)
1121+
domain = visitor.visit_domain(self.domain)
1122+
if copy:
1123+
return type(self)(instructions, domain)
1124+
self.instructions = instructions
1125+
self.domain = domain
1126+
return self
1127+
1128+
10501129
class Instruction:
10511130
"""Grouping of all instructions for namespace exposure.
10521131
10531132
Notes
10541133
-----
1055-
This class is not meant to be instantiated, but rather serves as a namespace for all instructions except RZZ.
1134+
This class is not meant to be instantiated, but rather serves as a namespace for all instructions.
10561135
The type alias for "any command" is :data:`InstructionKind`.
10571136
"""
10581137

@@ -1088,43 +1167,25 @@ class Instruction:
10881167
CSWAP: TypeAlias = CSWAP
10891168
M: TypeAlias = M
10901169
GPHASE: TypeAlias = GPHASE
1170+
CONDINSTR: TypeAlias = CONDINSTR
10911171

10921172
def __init__(self) -> None:
10931173
raise TypeError("Instruction is a namespace, not a class.")
10941174

10951175

10961176
if TYPE_CHECKING:
1097-
InstructionType = (
1098-
I
1099-
| X
1100-
| Y
1101-
| Z
1102-
| H
1103-
| S
1104-
| SDG
1105-
| T
1106-
| TDG
1107-
| SX
1108-
| SXDG
1109-
| J
1110-
| P
1111-
| RX
1112-
| RY
1113-
| RZ
1114-
| U
1115-
| CJ
1116-
| CP
1117-
| CRX
1118-
| CRY
1119-
| CRZ
1120-
| CU
1121-
| CNOT
1122-
| CY
1123-
| CZ
1124-
| CCX
1125-
| RZZ
1126-
| SWAP
1127-
| CSWAP
1128-
| M
1129-
| GPHASE
1177+
InstructionTypeWithControl = (
1178+
Instruction.CNOT
1179+
| Instruction.CY
1180+
| Instruction.CZ
1181+
| Instruction.CJ
1182+
| Instruction.CCX
1183+
| Instruction.CRX
1184+
| Instruction.CRY
1185+
| Instruction.CRZ
1186+
| Instruction.CU
1187+
| Instruction.P
1188+
| Instruction.CP
1189+
| Instruction.CSWAP
11301190
)
1191+
InstructionType = InstructionTypeWithoutM | M

‎graphix/opengraph.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -651,11 +651,11 @@ def to_circuit(
651651
>>> with warnings.catch_warnings():
652652
... warnings.filterwarnings("ignore", message="Open graph with non-inferred Pauli measurements.")
653653
... og.to_circuit()
654-
Circuit(width=3, instr=[H(2), H(1), CNOT(2, 1), H(1), H(1), H(0), CNOT(2, 0), CNOT(1, 0), H(2), H(1), H(0)])
654+
Circuit(width=3, instr=[H(2), H(1), CNOT(2, 1), H(1), H(1), H(0), CNOT(2, 0), CNOT(1, 0), H(2), H(1), H(0)], ancillas=0, ancilla_state=graphix.states.PlanarState(Plane.XY, 0))
655655
>>> # The default compilation passes do not exploit the lower depth of the Pauli flow
656656
>>> # compared the gflow.
657657
>>> og.infer_pauli_measurements().to_circuit()
658-
Circuit(width=3, instr=[H(2), H(1), CNOT(2, 1), H(1), H(1), H(0), CNOT(2, 0), CNOT(1, 0), H(2), H(1), H(0)])
658+
Circuit(width=3, instr=[H(2), H(1), CNOT(2, 1), H(1), H(1), H(0), CNOT(2, 0), CNOT(1, 0), H(2), H(1), H(0)], ancillas=0, ancilla_state=graphix.states.PlanarState(Plane.XY, 0))
659659
"""
660660
return (
661661
self.to_pauliflow(stacklevel=stacklevel + 1)

0 commit comments

Comments
 (0)