Repository navigation
Conversation
Co-authored-by: thierry-martinez <thierry.martinez@inria.fr>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #587 +/- ##
==========================================
+ Coverage 89.71% 89.91% +0.20%
==========================================
Files 49 49
Lines 8293 8430 +137
==========================================
+ Hits 7440 7580 +140
+ Misses 853 850 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…sable This commit fixes the export of circuit measurements to OpenQASM and makes the exporter more composable. Exporting circuit measurements is tested with a round-trip using TeamGraphix/graphix-qasm-parser#16.
The version of `graphix-qasm-parser` is set in `.github/qasm-parser-requirements.txt` for both nox and coverage. The gate `RZZ` does not belong to the OpenQASM standard set of gates and was incorrectly exported as `crz`. See TeamGraphix/graphix-qasm-parser#12 for more details.
`uv` ignores `pip`-installed packages.
thierry-martinez
left a comment
There was a problem hiding this comment.
Very nice work. Some minor comments.
| @override | ||
| def visit(self, visitor: InstructionVisitor, *, copy: bool = False) -> CNOT: | ||
| target = visitor.visit_qubit(self.target) | ||
| control = visitor.visit_qubit(self.control) | ||
| if target == control: | ||
| raise ValueError(f"Target and control qubits cannot be the same. Qubit index: {target}.") | ||
| if copy: | ||
| return CNOT(target, control) | ||
| self.target = target | ||
| self.control = control | ||
| return self | ||
|
|
There was a problem hiding this comment.
The check for target != control should be performed in the base method ControlledSingleTargetInstruction.visit, and we don't need a specific override for CNOT.
| class CONDINSTR(_KindChecker, BaseInstruction): | ||
| """Base class for conditional circuit instructions. | ||
|
|
||
| Condional 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`. |
There was a problem hiding this comment.
| Condional 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`. | |
| 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`. |
|
|
||
| # Needed to specify dataclass attributes in CONDINSTR, | ||
| # so it cannot be inside TYPE_CHECKING block. | ||
| InstructionTypeWithoutMandCONDINSTR = ( |
There was a problem hiding this comment.
We only need InstructionTypeWithoutM, not InstructionTypeWithoutMandCONDINSTR. To define InstructionTypeWithoutM directly, even if CONDINSTR is not yet defined and recursively depends on InstructionTypeWithoutM, we can quote the definition:
InstructionTypeWithoutM: TypeAlias = """(
I
| X
| Y
| Z
| H
| S
| SDG
| T
| TDG
| SX
| SXDG
| J
| P
| RX
| RY
| RZ
| U
| CJ
| CP
| CRX
| CRY
| CRZ
| CU
| CNOT
| CY
| CZ
| CCX
| RZZ
| SWAP
| CSWAP
| GPHASE
| CONDINSTR
)"""There was a problem hiding this comment.
I didn't know that, thank you. Done in 980319b
| case Clifford.SDG: | ||
| circuit.sdg(qubit) | ||
| case _: | ||
| for clifford in cmd.clifford.hsz: |
There was a problem hiding this comment.
Using clifford.qasm3 would lead to more compact decompositions and avoid the special cases for x, y, and sdg.
| # The following code adds SWAP gates so that qubits are in the same | ||
| # order that output nodes. |
There was a problem hiding this comment.
| # The following code adds SWAP gates so that qubits are in the same | |
| # order that output nodes. | |
| # The following code adds SWAP gates so that qubits are in the same | |
| # order as the output nodes. |
| ----- | ||
| .. The instruction sequence is applied when the XOR of the measurement outcomes of the qubits in ``domain`` evaluates to ``1``. | ||
|
|
||
| .. Condional measurements are not well defined since they would result in circuits with an indeterminate number of qubits. Therefore, the parameter ``instrs`` cannot contain instances of `Instruction.M`. This is statically ensured by the type checker. |
There was a problem hiding this comment.
| .. Condional measurements are not well defined since they would result in circuits with an indeterminate number of qubits. Therefore, the parameter ``instrs`` cannot contain instances of `Instruction.M`. This is statically ensured by the type checker. | |
| .. Conditional measurements are not well defined since they would result in circuits with an indeterminate number of qubits. Therefore, the parameter ``instrs`` cannot contain instances of `Instruction.M`. This is statically ensured by the type checker. |
|
Hi @thierry-martinez, thank you for the review. I addressed all your comments. Further, I changed the instruction methods of qc = Circuit(2)
qc.h(0)
qc.cnot(0, 1)Now we can also do: qc = Circuit(2).h(0).cnot(0, 1)I noticed that I never removed the |
| case "y": | ||
| circuit.y(qubit) | ||
| case "z": | ||
| circuit.y(qubit) |
There was a problem hiding this comment.
| circuit.y(qubit) | |
| circuit.z(qubit) |
| match bloch.plane: | ||
| case Plane.XY: | ||
| circuit.rz(qubit, -bloch.angle) | ||
| circuit.m(qubit, Axis.X) | ||
| case Plane.XZ: | ||
| circuit.ry(qubit, -bloch.angle) | ||
| circuit.m(qubit, Axis.Z) | ||
| case Plane.YZ: | ||
| circuit.rx(qubit, bloch.angle) | ||
| circuit.m(qubit, Axis.Z) | ||
| case _: | ||
| assert_never(bloch.plane) |
There was a problem hiding this comment.
The following may be more compact and better explain the intention:
| match bloch.plane: | |
| case Plane.XY: | |
| circuit.rz(qubit, -bloch.angle) | |
| circuit.m(qubit, Axis.X) | |
| case Plane.XZ: | |
| circuit.ry(qubit, -bloch.angle) | |
| circuit.m(qubit, Axis.Z) | |
| case Plane.YZ: | |
| circuit.rx(qubit, bloch.angle) | |
| circuit.m(qubit, Axis.Z) | |
| case _: | |
| assert_never(bloch.plane) | |
| circuit.r(qubit, bloch.plane.orth, Sign.plus_if(bloch.plane == Plane.YZ) * bloch.angle) | |
| circuit.m(qubit, bloch.plane.cos) |
pranav97nair
left a comment
There was a problem hiding this comment.
Thanks for this very nice work, Mateo! Looks good to me, I have just made some very minor comments.
| The initial state for each input node. Only |0⟩ or |+⟩ states are supported. | ||
| """ | ||
| return "".join(pattern_to_qasm3_lines(pattern, input_state=input_state)) | ||
| return "\n".join(pattern_to_qasm3_lines(pattern, input_state=input_state)) |
| Returns | ||
| ------- | ||
| Circuit | ||
| Quantum circuit represented as a set of instructions. |
There was a problem hiding this comment.
I would probably change this to "sequence of instructions" just to be as precise as possible.
| """ | ||
| assert control in self.active_qubits | ||
| assert target in self.active_qubits | ||
| assert control != target |
There was a problem hiding this comment.
So if I understand correctly, these checks for multi-qubit gates are now moved into graphix/insturction.py ?
There was a problem hiding this comment.
Yes, the visit method of instructions checks that qubit indices are consistent with each other. For instance, for CCX we have:
@override
def visit(self, visitor: InstructionVisitor, *, copy: bool = False) -> CCX:
u, v = self.controls
target = visitor.visit_qubit(self.target)
controls = (visitor.visit_qubit(u), visitor.visit_qubit(v))
if controls[0] == controls[1]:
raise ValueError(f"Control qubits cannot be the same. Qubit index: {controls[0]}.")
for i, c in enumerate(controls):
if target == c:
raise ValueError(f"Target and control-{i} qubits cannot be the same. Qubit index: {target}.")
...Crucially, in transpiler.py we defined a instruction visitor InstructionValidatorVisitor that is called every time we add a new instruction (in method Circuit.add we have self.instruction.append(instr.visit(self._visitor))).
The visitor also ensures that no gates are applied on measured qubits (note that this check cannot be encapsulated in the visit method, because it needs information about other gates).
| return f"Circuit(width={self.width}, instr={self.instruction}, ancillas={self.ancillas}, ancilla_state={self.ancilla_state!r})" | ||
|
|
||
| def cnot(self, control: int, target: int) -> None: | ||
| def cnot(self, control: int, target: int) -> Self: |
There was a problem hiding this comment.
Was there a specific motivation to have the circuit methods return self ?
There was a problem hiding this comment.
Yes. Now gate methods return the circuit itself and we can chain methods. Instead of
qc = Circuit(2)
qc.h(0)
qc.cnot(0, 1)Now we can also do:
qc = Circuit(2).h(0).cnot(0, 1)| case InstructionKind.CONDINSTR: | ||
| if instr.domain: | ||
| domain_list = list(instr.domain) | ||
| for cbit in domain_list: | ||
| meas_instr = measurements[cbit] | ||
| assert cbit == meas_instr.target | ||
| if meas_instr.axis == Axis.X: | ||
| circuit.h(cbit) | ||
| elif meas_instr.axis == Axis.Y: | ||
| circuit.rx(cbit, ANGLE_PI / 2) | ||
| measurements[cbit] = Instruction.M(cbit, Axis.Z) # Record updated measurement | ||
| cnot_instr = [ | ||
| Instruction.CNOT(control=control, target=target) | ||
| for control, target in itertools.pairwise(domain_list) | ||
| ] | ||
| circuit.extend(cnot_instr) | ||
| circuit.extend(insert_control(domain_list[-1], instr.instructions)) | ||
| circuit.extend(cnot_instr[::-1]) |
| ) # Tested in test_transpile.TestTranspilerUnitGates.test_instructions() | ||
| circuit_test = pattern.to_circuit() | ||
|
|
||
| if circuit_test.nqubit > 20: |
There was a problem hiding this comment.
Just to be sure - this is a matter of compute power, right? There is no explicit 20-qubit limit on simulation in the codebase?
There was a problem hiding this comment.
yes there is no limit. Something to keep in mind though is that Circuit.simulate is extremely inefficient to simulate circuits with feedforward because it instantiates all qubits (inputs and ancillas) at the very beginning. Roughly speaking, it is as bad as standardizing the pattern before simulating. Some gates (notably 3-qubit gates) need a rather large number of ancilla qubits, so doing a naive simulation of the Circuit with mid-circuit measurements fails, hence the limit.
I added a comment in 0a61233
| ) # Tested in test_transpile.TestTranspilerUnitGates.test_instructions() | ||
| circuit_test = pattern.to_circuit() | ||
|
|
||
| # Simulatin circuits with feedforward is extremely inefficient because we instantiate |
There was a problem hiding this comment.
| # Simulatin circuits with feedforward is extremely inefficient because we instantiate | |
| # Simulating circuits with feedforward is extremely inefficient because we instantiate |
This PR addresses #580. The PR comprises three blocks:
Block 1: Introduction of ancilla qubits
Concerns
graphix.transpiler.pyonly.Now it's possible to distinguish between input and ancilla qubits in circuits.
Two new attributes are added to the
Circuitclass:ancillas: intto indicate the number of ancilla qubits. Ancilla qubits are indexed fromrange(width, width + ancillas - 1), wherewidthis the number of input qubits (existing attribute).ancilla_state: Stateto indicate the state of the ancilla qubits.Supported operations on circuits with ancillas:
ancilla_stateis aBasicState:ancilla_stateisBasicState.PLUS, ancilla qubits are represented as non-input nodes in the open graph.BasicStateis transpiled intoBasicState.PLUSby prepending the appropriate gates to the circuit (viaCircuit.transpile_ancilla_state). This is unavoidable because transpilation is carried out by first constructing an open graph with causal flow, and non-input nodes inOpenGraphalways representBasicState.PLUSqubits, even if Graphix supportsNcommands with an arbitrary state.QASM 3 export: only if
ancilla_stateis aBasicState. 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.pyandgraphix.transpiler.py.Now it's possible to represent conditional instructions in circuits.
A new instruction
CONDINSTRis added. This instruction bundles a sequence of instructions (possibly incuding otherCONDINSTR) and a qubit domain (set[int]). The instruction sequence is applied when the XOR of the measurement outcomes of the qubits indomainevaluates to1.Two side modifications:
InstructionVisitorInstructionVisitornow has an additional methodvisit_domain. The methodCONDINSTR.visitvisits the instruction's domain and all the instructions in the instruction sequence.Introduced a new
_InstructionValidatorVisitor(InstructionVisitor)class. This visitor reimplements the methodvisit_qubitandvisit_domainso that callingvisitfor a given instruction checks the qubit validity. As a result:Valid-qubit sanity checks are factorized into a single function.
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
visitof the corresponding instructions instead of theCircuitmethods.graphix.utils.check_kindgraphix.utils.check_kindhas been slightly modified to fix an error due to the self-reference in the definition ofCONDINSTR. This modification is an LLM suggestion, and I don't understand it fully.Supported operations on circuits with conditional instructions:
Simulation: always.
simulate_instructionsthat is called recursively forCONDINSTR.simulatormodule.MeasureMethod.classical_outputsto achieve a minimal PR.Transpilation to pattern or causal flow: always.
Circuit.transpile_condinstrthat is called from exisiting transpilation methods when there is any conditional instruction in the circuit.QASM 3 export: always (implemented by @thierry-martinez).
Block 3: Introduction of a
Pattern.to_circuitmethod.Concerns
graphix.pattern.pyPattern.to_qasm3, with two differences:Pattern.to_qasm3?