From 1731cd948cc47cdda192d4e75700e6abf7341a74 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Sat, 20 Apr 2024 22:22:41 +0100 Subject: [PATCH 1/2] Support standalone `Var` throughout transpiler This adds the missing pieces to fully support standalone `Var` nodes through every part of the transpiler (that I could detect). It's quite possible there's some problem in a more esoteric non-preset pass somewhere, but I couldn't spot them. For the most part there were very few changes needed to the actual passes, and only one place in `QuantumCircuit` that had previously been missed. Most of the commit is updating passes to correctly pass `inline_captures=True` when appropriate for dealing with `DAGCircuit.compose`, and making sure that any place that needed to build a raw `DAGCircuit` for a rebuild _without_ using `DAGCircuit.copy_empty_like` made sure to correctly add in the variables. This commit adds specific tests for every pass that I touched, plus the general integration tests that we have for the transpiler to make sure that OQ3 and QPY serialisation work afterwards. --- qiskit/circuit/quantumcircuit.py | 4 +- qiskit/circuit/store.py | 3 + .../passes/basis/basis_translator.py | 4 +- .../passes/basis/unroll_custom_definitions.py | 2 +- .../transpiler/passes/layout/apply_layout.py | 6 ++ .../transpiler/passes/layout/sabre_layout.py | 6 ++ .../passes/optimization/optimize_annotated.py | 2 +- .../passes/routing/stochastic_swap.py | 23 +++-- .../passes/synthesis/high_level_synthesis.py | 2 +- qiskit/transpiler/passes/utils/gates_basis.py | 6 +- .../python/circuit/test_circuit_operations.py | 25 +++++ test/python/compiler/test_transpiler.py | 89 ++++++++++++++++- test/python/transpiler/test_apply_layout.py | 26 +++++ .../transpiler/test_basis_translator.py | 97 ++++++++++++++++++- .../transpiler/test_gates_in_basis_pass.py | 42 ++++++++ .../transpiler/test_high_level_synthesis.py | 55 +++++++++++ .../transpiler/test_optimize_annotated.py | 24 ++++- test/python/transpiler/test_sabre_layout.py | 43 +++++++- .../python/transpiler/test_stochastic_swap.py | 44 ++++++++- .../test_unroll_custom_definitions.py | 56 ++++++++++- 20 files changed, 536 insertions(+), 23 deletions(-) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index ad966b685e71..abd48c686b15 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -621,9 +621,7 @@ def reverse_ops(self) -> "QuantumCircuit": q_1: ┤ RX(1.57) ├───── └──────────┘ """ - reverse_circ = QuantumCircuit( - self.qubits, self.clbits, *self.qregs, *self.cregs, name=self.name + "_reverse" - ) + reverse_circ = self.copy_empty_like(self.name + "_reverse") for instruction in reversed(self.data): reverse_circ._append(instruction.replace(operation=instruction.operation.reverse_ops())) diff --git a/qiskit/circuit/store.py b/qiskit/circuit/store.py index 857cb4f6c2d0..6bbc5439332d 100644 --- a/qiskit/circuit/store.py +++ b/qiskit/circuit/store.py @@ -59,6 +59,9 @@ class Store(Instruction): :class:`~.circuit.Measure` is a primitive for quantum measurement), and is not safe for subclassing.""" + # This is a compiler/backend intrinsic operation, separate to any quantum processing. + _directive = True + def __init__(self, lvalue: expr.Expr, rvalue: expr.Expr): """ Args: diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index c38d65817769..074c6d341baa 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -148,12 +148,12 @@ def run(self, dag): # Names of instructions assumed to supported by any backend. if self._target is None: - basic_instrs = ["measure", "reset", "barrier", "snapshot", "delay"] + basic_instrs = ["measure", "reset", "barrier", "snapshot", "delay", "store"] target_basis = set(self._target_basis) source_basis = set(self._extract_basis(dag)) qargs_local_source_basis = {} else: - basic_instrs = ["barrier", "snapshot"] + basic_instrs = ["barrier", "snapshot", "store"] target_basis = self._target.keys() - set(self._non_global_operations) source_basis, qargs_local_source_basis = self._extract_basis_target(dag, qarg_indices) diff --git a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py index 12e6811a2f03..2a95f540f886 100644 --- a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py +++ b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py @@ -61,7 +61,7 @@ def run(self, dag): return dag if self._target is None: - basic_insts = {"measure", "reset", "barrier", "snapshot", "delay"} + basic_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} device_insts = basic_insts | set(self._basis_gates) for node in dag.op_nodes(): diff --git a/qiskit/transpiler/passes/layout/apply_layout.py b/qiskit/transpiler/passes/layout/apply_layout.py index c36a7e111070..e87b6f7ce4e9 100644 --- a/qiskit/transpiler/passes/layout/apply_layout.py +++ b/qiskit/transpiler/passes/layout/apply_layout.py @@ -61,6 +61,12 @@ def run(self, dag): new_dag = DAGCircuit() new_dag.add_qreg(q) + for var in dag.iter_input_vars(): + new_dag.add_input_var(var) + for var in dag.iter_captured_vars(): + new_dag.add_captured_var(var) + for var in dag.iter_declared_vars(): + new_dag.add_declared_var(var) new_dag.metadata = dag.metadata new_dag.add_clbits(dag.clbits) for creg in dag.cregs.values(): diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 92227f3c37d6..31609b878683 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -308,6 +308,12 @@ def run(self, dag): mapped_dag.add_clbits(dag.clbits) for creg in dag.cregs.values(): mapped_dag.add_creg(creg) + for var in dag.iter_input_vars(): + mapped_dag.add_input_var(var) + for var in dag.iter_captured_vars(): + mapped_dag.add_captured_var(var) + for var in dag.iter_declared_vars(): + mapped_dag.add_declared_var(var) mapped_dag._global_phase = dag._global_phase self.property_set["original_qubit_indices"] = { bit: index for index, bit in enumerate(dag.qubits) diff --git a/qiskit/transpiler/passes/optimization/optimize_annotated.py b/qiskit/transpiler/passes/optimization/optimize_annotated.py index 65d06436cc5c..0b9b786a07f4 100644 --- a/qiskit/transpiler/passes/optimization/optimize_annotated.py +++ b/qiskit/transpiler/passes/optimization/optimize_annotated.py @@ -77,7 +77,7 @@ def __init__( self._top_level_only = not recurse or (self._basis_gates is None and self._target is None) if not self._top_level_only and self._target is None: - basic_insts = {"measure", "reset", "barrier", "snapshot", "delay"} + basic_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} self._device_insts = basic_insts | set(self._basis_gates) def run(self, dag: DAGCircuit): diff --git a/qiskit/transpiler/passes/routing/stochastic_swap.py b/qiskit/transpiler/passes/routing/stochastic_swap.py index 3b80bf7b31af..ec7ea8149137 100644 --- a/qiskit/transpiler/passes/routing/stochastic_swap.py +++ b/qiskit/transpiler/passes/routing/stochastic_swap.py @@ -33,7 +33,6 @@ ForLoopOp, SwitchCaseOp, ControlFlowOp, - Instruction, CASE_DEFAULT, ) from qiskit._accelerate import stochastic_swap as stochastic_swap_rs @@ -266,11 +265,15 @@ def _layer_update(self, dag, layer, best_layout, best_depth, best_circuit): # Output any swaps if best_depth > 0: logger.debug("layer_update: there are swaps in this layer, depth %d", best_depth) - dag.compose(best_circuit, qubits={bit: bit for bit in best_circuit.qubits}) + dag.compose( + best_circuit, qubits={bit: bit for bit in best_circuit.qubits}, inline_captures=True + ) else: logger.debug("layer_update: there are no swaps in this layer") # Output this layer - dag.compose(layer["graph"], qubits=best_layout.reorder_bits(dag.qubits)) + dag.compose( + layer["graph"], qubits=best_layout.reorder_bits(dag.qubits), inline_captures=True + ) def _mapper(self, circuit_graph, coupling_graph, trials=20): """Map a DAGCircuit onto a CouplingMap using swap gates. @@ -438,7 +441,7 @@ def _controlflow_layer_update(self, dagcircuit_output, layer_dag, current_layout root_dag, self.coupling_map, layout, final_layout, seed=self._new_seed() ) if swap_dag.size(recurse=False): - updated_dag_block.compose(swap_dag, qubits=swap_qubits) + updated_dag_block.compose(swap_dag, qubits=swap_qubits, inline_captures=True) idle_qubits &= set(updated_dag_block.idle_wires()) # Now for each block, expand it to be full width over all active wires (all blocks of a @@ -504,10 +507,18 @@ def _dag_from_block(block, node, root_dag): out.add_qreg(qreg) # For clbits, we need to take more care. Nested control-flow might need registers to exist for # conditions on inner blocks. `DAGCircuit.substitute_node_with_dag` handles this register - # mapping when required, so we use that with a dummy block. + # mapping when required, so we use that with a dummy block that pretends to act on all variables + # in the DAG. out.add_clbits(node.cargs) + for var in block.iter_input_vars(): + out.add_input_var(var) + for var in block.iter_captured_vars(): + out.add_captured_var(var) + for var in block.iter_declared_vars(): + out.add_declared_var(var) + dummy = out.apply_operation_back( - Instruction("dummy", len(node.qargs), len(node.cargs), []), + IfElseOp(expr.lift(True), block.copy_empty_like(vars_mode="captures")), node.qargs, node.cargs, check=False, diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index fd21ae6a75fc..150874a84c7d 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -363,7 +363,7 @@ def __init__( # include path for when target exists but target.num_qubits is None (BasicSimulator) if not self._top_level_only and (self._target is None or self._target.num_qubits is None): - basic_insts = {"measure", "reset", "barrier", "snapshot", "delay"} + basic_insts = {"measure", "reset", "barrier", "snapshot", "delay", "store"} self._device_insts = basic_insts | set(self._basis_gates) def run(self, dag: DAGCircuit) -> DAGCircuit: diff --git a/qiskit/transpiler/passes/utils/gates_basis.py b/qiskit/transpiler/passes/utils/gates_basis.py index 657b1d134852..b1f004cc0df3 100644 --- a/qiskit/transpiler/passes/utils/gates_basis.py +++ b/qiskit/transpiler/passes/utils/gates_basis.py @@ -32,7 +32,7 @@ def __init__(self, basis_gates=None, target=None): self._basis_gates = None if basis_gates is not None: self._basis_gates = set(basis_gates).union( - {"measure", "reset", "barrier", "snapshot", "delay"} + {"measure", "reset", "barrier", "snapshot", "delay", "store"} ) self._target = target @@ -46,8 +46,8 @@ def run(self, dag): def _visit_target(dag, wire_map): for gate in dag.op_nodes(): - # Barrier is universal and supported by all backends - if gate.name == "barrier": + # Barrier and store are assumed universal and supported by all backends + if gate.name in ("barrier", "store"): continue if not self._target.instruction_supported( gate.name, tuple(wire_map[bit] for bit in gate.qargs) diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 9a934d70c710..8ba71989219b 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -1002,6 +1002,31 @@ def test_reverse(self): self.assertEqual(qc.reverse_ops(), expected) + def test_reverse_with_standlone_vars(self): + """Test that instruction-reversing works in the presence of stand-alone variables.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Uint(8)) + + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + with qc.if_test(a): + # We don't really comment on what should happen to control-flow operations in this + # method, and Sabre doesn't care (nor use this method, in the fast paths), so this is + # deliberately using a body of length 1 (a single `Store`). + qc.add_var(c, 12) + + expected = qc.copy_empty_like() + with expected.if_test(a): + expected.add_var(c, 12) + expected.cx(0, 1) + expected.h(0) + expected.store(b, 12) + + self.assertEqual(qc.reverse_ops(), expected) + def test_repeat(self): """Test repeating the circuit works.""" qr = QuantumRegister(2) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 943de7b932e7..2a8c86b27ab0 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -42,13 +42,13 @@ SwitchCaseOp, WhileLoopOp, ) +from qiskit.circuit.classical import expr, types from qiskit.circuit.annotated_operation import ( AnnotatedOperation, InverseModifier, ControlModifier, PowerModifier, ) -from qiskit.circuit.classical import expr from qiskit.circuit.delay import Delay from qiskit.circuit.measure import Measure from qiskit.circuit.reset import Reset @@ -2175,6 +2175,38 @@ def _control_flow_expr_circuit(self): base.append(CustomCX(), [3, 4]) return base + def _standalone_var_circuit(self): + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Uint(8)) + + qc = QuantumCircuit(5, 5, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + qc.store(a, expr.bit_xor(qc.clbits[0], qc.clbits[1])) + with qc.if_test(a) as else_: + qc.cx(2, 3) + qc.cx(3, 4) + qc.cx(4, 2) + with else_: + qc.add_var(c, 12) + with qc.while_loop(a): + with qc.while_loop(a): + qc.add_var(c, 12) + qc.cz(1, 0) + qc.cz(4, 1) + qc.store(a, False) + with qc.switch(expr.bit_and(b, 7)) as case: + with case(0): + qc.cz(0, 1) + qc.cx(1, 2) + qc.cy(2, 0) + with case(case.DEFAULT): + qc.store(b, expr.bit_and(b, 7)) + return qc + @data(0, 1, 2, 3) def test_qpy_roundtrip(self, optimization_level): """Test that the output of a transpiled circuit can be round-tripped through QPY.""" @@ -2300,6 +2332,46 @@ def test_qpy_roundtrip_control_flow_expr_backendv2(self, optimization_level): round_tripped = qpy.load(buffer)[0] self.assertEqual(round_tripped, transpiled) + @data(0, 1, 2, 3) + def test_qpy_roundtrip_standalone_var(self, optimization_level): + """Test that the output of a transpiled circuit with control flow including standalone `Var` + nodes can be round-tripped through QPY.""" + backend = GenericBackendV2(num_qubits=7) + transpiled = transpile( + self._standalone_var_circuit(), + backend=backend, + basis_gates=backend.operation_names + + ["if_else", "for_loop", "while_loop", "switch_case"], + optimization_level=optimization_level, + seed_transpiler=2024_05_01, + ) + buffer = io.BytesIO() + qpy.dump(transpiled, buffer) + buffer.seek(0) + round_tripped = qpy.load(buffer)[0] + self.assertEqual(round_tripped, transpiled) + + @data(0, 1, 2, 3) + def test_qpy_roundtrip_standalone_var_target(self, optimization_level): + """Test that the output of a transpiled circuit with control flow including standalone `Var` + nodes can be round-tripped through QPY.""" + backend = GenericBackendV2(num_qubits=11) + backend.target.add_instruction(IfElseOp, name="if_else") + backend.target.add_instruction(ForLoopOp, name="for_loop") + backend.target.add_instruction(WhileLoopOp, name="while_loop") + backend.target.add_instruction(SwitchCaseOp, name="switch_case") + transpiled = transpile( + self._standalone_var_circuit(), + backend=backend, + optimization_level=optimization_level, + seed_transpiler=2024_05_01, + ) + buffer = io.BytesIO() + qpy.dump(transpiled, buffer) + buffer.seek(0) + round_tripped = qpy.load(buffer)[0] + self.assertEqual(round_tripped, transpiled) + @data(0, 1, 2, 3) def test_qasm3_output(self, optimization_level): """Test that the output of a transpiled circuit can be dumped into OpenQASM 3.""" @@ -2350,6 +2422,21 @@ def test_qasm3_output_control_flow_expr(self, optimization_level): str, ) + @data(0, 1, 2, 3) + def test_qasm3_output_standalone_var(self, optimization_level): + """Test that the output of a transpiled circuit with control flow and standalone `Var` nodes + can be dumped into OpenQASM 3.""" + transpiled = transpile( + self._standalone_var_circuit(), + backend=GenericBackendV2(num_qubits=13, control_flow=True), + optimization_level=optimization_level, + seed_transpiler=2024_05_01, + ) + # TODO: There's not a huge amount we can sensibly test for the output here until we can + # round-trip the OpenQASM 3 back into a Terra circuit. Mostly we're concerned that the dump + # itself doesn't throw an error, though. + self.assertIsInstance(qasm3.dumps(transpiled), str) + @data(0, 1, 2, 3) def test_transpile_target_no_measurement_error(self, opt_level): """Test that transpile with a target which contains ideal measurement works diff --git a/test/python/transpiler/test_apply_layout.py b/test/python/transpiler/test_apply_layout.py index bd119c010f04..b92cc710095d 100644 --- a/test/python/transpiler/test_apply_layout.py +++ b/test/python/transpiler/test_apply_layout.py @@ -15,6 +15,7 @@ import unittest from qiskit.circuit import QuantumRegister, QuantumCircuit, ClassicalRegister +from qiskit.circuit.classical import expr, types from qiskit.converters import circuit_to_dag from qiskit.transpiler.layout import Layout from qiskit.transpiler.passes import ApplyLayout, SetLayout @@ -167,6 +168,31 @@ def test_final_layout_is_updated(self): ), ) + def test_works_with_var_nodes(self): + """Test that standalone var nodes work.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(2, 2, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + qc.store(a, expr.bit_and(a, expr.bit_xor(qc.clbits[0], qc.clbits[1]))) + + expected = QuantumCircuit(QuantumRegister(2, "q"), *qc.cregs, inputs=[a]) + expected.add_var(b, 12) + expected.h(1) + expected.cx(1, 0) + expected.measure([1, 0], [0, 1]) + expected.store(a, expr.bit_and(a, expr.bit_xor(qc.clbits[0], qc.clbits[1]))) + + pass_ = ApplyLayout() + pass_.property_set["layout"] = Layout(dict(enumerate(reversed(qc.qubits)))) + after = pass_(qc) + + self.assertEqual(after, expected) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py index 218cd8162d50..24e5e68ba987 100644 --- a/test/python/transpiler/test_basis_translator.py +++ b/test/python/transpiler/test_basis_translator.py @@ -19,8 +19,10 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit import transpile -from qiskit.circuit import Gate, Parameter, EquivalenceLibrary, Qubit, Clbit +from qiskit.circuit import Gate, Parameter, EquivalenceLibrary, Qubit, Clbit, Measure +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import ( + HGate, U1Gate, U2Gate, U3Gate, @@ -889,6 +891,50 @@ def test_unrolling_parameterized_composite_gates(self): self.assertEqual(circuit_to_dag(expected), out_dag) + def test_treats_store_as_builtin(self): + """Test that the `store` instruction is allowed as a builtin in all cases with no target.""" + + class MyHGate(Gate): + """Hadamard, but it's _mine_.""" + + def __init__(self): + super().__init__("my_h", 1, []) + + class MyCXGate(Gate): + """CX, but it's _mine_.""" + + def __init__(self): + super().__init__("my_cx", 2, []) + + h_to_my = QuantumCircuit(1) + h_to_my.append(MyHGate(), [0], []) + cx_to_my = QuantumCircuit(2) + cx_to_my.append(MyCXGate(), [0, 1], []) + eq_lib = EquivalenceLibrary() + eq_lib.add_equivalence(HGate(), h_to_my) + eq_lib.add_equivalence(CXGate(), cx_to_my) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(2, 2, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + qc.store(a, expr.bit_xor(qc.clbits[0], qc.clbits[1])) + + expected = qc.copy_empty_like() + expected.store(b, 12) + expected.append(MyHGate(), [0], []) + expected.append(MyCXGate(), [0, 1], []) + expected.measure([0, 1], [0, 1]) + expected.store(a, expr.bit_xor(expected.clbits[0], expected.clbits[1])) + + # Note: store is present in the circuit but not in the basis set. + out = BasisTranslator(eq_lib, ["my_h", "my_cx"])(qc) + self.assertEqual(out, expected) + class TestBasisExamples(QiskitTestCase): """Test example circuits targeting example bases over the StandardEquivalenceLibrary.""" @@ -1127,3 +1173,52 @@ def test_2q_with_non_global_1q(self): expected.sx(1) expected.rz(3 * pi, 1) self.assertEqual(output, expected) + + def test_treats_store_as_builtin(self): + """Test that the `store` instruction is allowed as a builtin in all cases with a target.""" + + class MyHGate(Gate): + """Hadamard, but it's _mine_.""" + + def __init__(self): + super().__init__("my_h", 1, []) + + class MyCXGate(Gate): + """CX, but it's _mine_.""" + + def __init__(self): + super().__init__("my_cx", 2, []) + + h_to_my = QuantumCircuit(1) + h_to_my.append(MyHGate(), [0], []) + cx_to_my = QuantumCircuit(2) + cx_to_my.append(MyCXGate(), [0, 1], []) + eq_lib = EquivalenceLibrary() + eq_lib.add_equivalence(HGate(), h_to_my) + eq_lib.add_equivalence(CXGate(), cx_to_my) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(2, 2, inputs=[a]) + qc.add_var(b, 12) + qc.h(0) + qc.cx(0, 1) + qc.measure([0, 1], [0, 1]) + qc.store(a, expr.bit_xor(qc.clbits[0], qc.clbits[1])) + + expected = qc.copy_empty_like() + expected.store(b, 12) + expected.append(MyHGate(), [0], []) + expected.append(MyCXGate(), [0, 1], []) + expected.measure([0, 1], [0, 1]) + expected.store(a, expr.bit_xor(expected.clbits[0], expected.clbits[1])) + + # Note: store is present in the circuit but not in the target. + target = Target() + target.add_instruction(MyHGate(), {(i,): None for i in range(qc.num_qubits)}) + target.add_instruction(Measure(), {(i,): None for i in range(qc.num_qubits)}) + target.add_instruction(MyCXGate(), {(0, 1): None, (1, 0): None}) + + out = BasisTranslator(eq_lib, {"my_h", "my_cx"}, target)(qc) + self.assertEqual(out, expected) diff --git a/test/python/transpiler/test_gates_in_basis_pass.py b/test/python/transpiler/test_gates_in_basis_pass.py index 2138070ed9d9..06ce5e0f6702 100644 --- a/test/python/transpiler/test_gates_in_basis_pass.py +++ b/test/python/transpiler/test_gates_in_basis_pass.py @@ -13,6 +13,7 @@ """Test GatesInBasis pass.""" from qiskit.circuit import QuantumCircuit, ForLoopOp, IfElseOp, SwitchCaseOp, Clbit +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import HGate, CXGate, UGate, XGate, ZGate from qiskit.circuit.measure import Measure from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary @@ -269,3 +270,44 @@ def test_basis_gates_target(self): pass_ = GatesInBasis(target=complete) pass_(circuit) self.assertTrue(pass_.property_set["all_gates_in_basis"]) + + def test_store_is_treated_as_builtin_basis_gates(self): + """Test that `Store` is treated as an automatic built-in when given basis gates.""" + pass_ = GatesInBasis(basis_gates=["h", "cx"]) + + a = expr.Var.new("a", types.Bool()) + good = QuantumCircuit(2, inputs=[a]) + good.store(a, False) + good.h(0) + good.cx(0, 1) + _ = pass_(good) + self.assertTrue(pass_.property_set["all_gates_in_basis"]) + + bad = QuantumCircuit(2, inputs=[a]) + bad.store(a, False) + bad.x(0) + bad.cz(0, 1) + _ = pass_(bad) + self.assertFalse(pass_.property_set["all_gates_in_basis"]) + + def test_store_is_treated_as_builtin_target(self): + """Test that `Store` is treated as an automatic built-in when given a target.""" + target = Target() + target.add_instruction(HGate(), {(0,): None, (1,): None}) + target.add_instruction(CXGate(), {(0, 1): None, (1, 0): None}) + pass_ = GatesInBasis(target=target) + + a = expr.Var.new("a", types.Bool()) + good = QuantumCircuit(2, inputs=[a]) + good.store(a, False) + good.h(0) + good.cx(0, 1) + _ = pass_(good) + self.assertTrue(pass_.property_set["all_gates_in_basis"]) + + bad = QuantumCircuit(2, inputs=[a]) + bad.store(a, False) + bad.x(0) + bad.cz(0, 1) + _ = pass_(bad) + self.assertFalse(pass_.property_set["all_gates_in_basis"]) diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index 0f074865f419..9a2432b82f9c 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -28,6 +28,7 @@ Operation, EquivalenceLibrary, ) +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import ( SwapGate, CXGate, @@ -36,6 +37,7 @@ U3Gate, U2Gate, U1Gate, + UGate, CU3Gate, CU1Gate, ) @@ -2042,6 +2044,59 @@ def test_unroll_empty_definition_with_phase(self): expected = QuantumCircuit(2, global_phase=0.5) self.assertEqual(pass_(qc), expected) + def test_leave_store_alone_basis(self): + """Don't attempt to synthesise `Store` instructions with basis gates.""" + + pass_ = HighLevelSynthesis(equivalence_library=std_eqlib, basis_gates=["u", "cx"]) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, a) + qc.compose(bell, [0, 1], inplace=True) + qc.store(b, a) + + expected = qc.copy_empty_like() + expected.store(b, a) + expected.compose(pass_(bell), [0, 1], inplace=True) + expected.store(b, a) + + self.assertEqual(pass_(qc), expected) + + def test_leave_store_alone_with_target(self): + """Don't attempt to synthesise `Store` instructions with a `Target`.""" + + # Note no store. + target = Target() + target.add_instruction( + UGate(Parameter("a"), Parameter("b"), Parameter("c")), {(0,): None, (1,): None} + ) + target.add_instruction(CXGate(), {(0, 1): None, (1, 0): None}) + + pass_ = HighLevelSynthesis(equivalence_library=std_eqlib, target=target) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, a) + qc.compose(bell, [0, 1], inplace=True) + qc.store(b, a) + + expected = qc.copy_empty_like() + expected.store(b, a) + expected.compose(pass_(bell), [0, 1], inplace=True) + expected.store(b, a) + + self.assertEqual(pass_(qc), expected) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_optimize_annotated.py b/test/python/transpiler/test_optimize_annotated.py index 6a506516d199..5e573b551dd5 100644 --- a/test/python/transpiler/test_optimize_annotated.py +++ b/test/python/transpiler/test_optimize_annotated.py @@ -13,7 +13,8 @@ """Test OptimizeAnnotated pass""" from qiskit.circuit import QuantumCircuit, Gate -from qiskit.circuit.library import SwapGate, CXGate +from qiskit.circuit.classical import expr, types +from qiskit.circuit.library import SwapGate, CXGate, HGate from qiskit.circuit.annotated_operation import ( AnnotatedOperation, ControlModifier, @@ -193,3 +194,24 @@ def test_if_else(self): ) self.assertEqual(qc_optimized, expected_qc) + + def test_standalone_var(self): + """Test that standalone vars work.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(3, 3, inputs=[a]) + qc.add_var(b, 12) + qc.append(AnnotatedOperation(HGate(), [ControlModifier(1), ControlModifier(1)]), [0, 1, 2]) + qc.append(AnnotatedOperation(CXGate(), [InverseModifier(), InverseModifier()]), [0, 1]) + qc.measure([0, 1, 2], [0, 1, 2]) + qc.store(a, expr.logic_and(qc.clbits[0], qc.clbits[1])) + + expected = qc.copy_empty_like() + expected.store(b, 12) + expected.append(HGate().control(2, annotated=True), [0, 1, 2]) + expected.cx(0, 1) + expected.measure([0, 1, 2], [0, 1, 2]) + expected.store(a, expr.logic_and(expected.clbits[0], expected.clbits[1])) + + self.assertEqual(OptimizeAnnotated()(qc), expected) diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 7640149e039e..487fbf9daef1 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -17,9 +17,10 @@ import math from qiskit import QuantumRegister, QuantumCircuit +from qiskit.circuit.classical import expr, types from qiskit.circuit.library import EfficientSU2 from qiskit.transpiler import CouplingMap, AnalysisPass, PassManager -from qiskit.transpiler.passes import SabreLayout, DenseLayout +from qiskit.transpiler.passes import SabreLayout, DenseLayout, StochasticSwap from qiskit.transpiler.exceptions import TranspilerError from qiskit.converters import circuit_to_dag from qiskit.compiler.transpiler import transpile @@ -257,6 +258,46 @@ def test_layout_many_search_trials(self): [layout[q] for q in qc.qubits], [22, 7, 2, 12, 1, 5, 14, 4, 11, 0, 16, 15, 3, 10] ) + def test_support_var_with_rust_fastpath(self): + """Test that the joint layout/embed/routing logic for the Rust-space fast-path works in the + presence of standalone `Var` nodes.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(5, inputs=[a]) + qc.add_var(b, 12) + qc.cx(0, 1) + qc.cx(1, 2) + qc.cx(2, 3) + qc.cx(3, 4) + qc.cx(4, 0) + + out = SabreLayout(CouplingMap.from_line(8), seed=0, swap_trials=2, layout_trials=2)(qc) + + self.assertIsInstance(out, QuantumCircuit) + self.assertEqual(out.layout.initial_index_layout(), [4, 5, 6, 3, 2, 0, 1, 7]) + + def test_support_var_with_explicit_routing_pass(self): + """Test that the logic works if an explicit routing pass is given.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + + qc = QuantumCircuit(5, inputs=[a]) + qc.add_var(b, 12) + qc.cx(0, 1) + qc.cx(1, 2) + qc.cx(2, 3) + qc.cx(3, 4) + qc.cx(4, 0) + + cm = CouplingMap.from_line(8) + pass_ = SabreLayout( + cm, seed=0, routing_pass=StochasticSwap(cm, trials=1, seed=0, fake_run=True) + ) + _ = pass_(qc) + layout = pass_.property_set["layout"] + self.assertEqual([layout[q] for q in qc.qubits], [2, 3, 4, 1, 5]) + class DensePartialSabreTrial(AnalysisPass): """Pass to run dense layout as a sabre trial.""" diff --git a/test/python/transpiler/test_stochastic_swap.py b/test/python/transpiler/test_stochastic_swap.py index fb27076d03de..df5948ed715b 100644 --- a/test/python/transpiler/test_stochastic_swap.py +++ b/test/python/transpiler/test_stochastic_swap.py @@ -27,7 +27,7 @@ from qiskit.providers.fake_provider import Fake27QPulseV1, GenericBackendV2 from qiskit.compiler.transpiler import transpile from qiskit.circuit import ControlFlowOp, Clbit, CASE_DEFAULT -from qiskit.circuit.classical import expr +from qiskit.circuit.classical import expr, types from test import QiskitTestCase # pylint: disable=wrong-import-order from test.utils._canonical import canonicalize_control_flow # pylint: disable=wrong-import-order @@ -897,6 +897,48 @@ def test_if_else_expr(self): check_map_pass.run(cdag) self.assertTrue(check_map_pass.property_set["is_swap_mapped"]) + def test_standalone_vars(self): + """Test that the routing works in the presence of stand-alone variables.""" + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Uint(8)) + c = expr.Var.new("c", types.Uint(8)) + qc = QuantumCircuit(5, inputs=[a]) + qc.add_var(b, 12) + qc.cx(0, 2) + qc.cx(1, 3) + qc.cx(3, 2) + qc.cx(3, 0) + qc.cx(4, 2) + qc.cx(4, 0) + qc.cx(1, 4) + qc.cx(3, 4) + with qc.if_test(a): + qc.store(a, False) + qc.add_var(c, 12) + qc.cx(0, 1) + with qc.if_test(a) as else_: + qc.store(a, False) + qc.add_var(c, 12) + qc.cx(0, 1) + with else_: + qc.cx(1, 2) + with qc.while_loop(a): + with qc.while_loop(a): + qc.add_var(c, 12) + qc.cx(1, 3) + qc.store(a, False) + with qc.switch(b) as case: + with case(0): + qc.add_var(c, 12) + qc.cx(3, 1) + with case(case.DEFAULT): + qc.cx(3, 1) + + cm = CouplingMap.from_line(5) + pm = PassManager([StochasticSwap(cm, seed=0), CheckMap(cm)]) + _ = pm.run(qc) + self.assertTrue(pm.property_set["is_swap_mapped"]) + def test_no_layout_change(self): """test controlflow with no layout change needed""" num_qubits = 5 diff --git a/test/python/transpiler/test_unroll_custom_definitions.py b/test/python/transpiler/test_unroll_custom_definitions.py index cfed023795d7..5bd16f027e45 100644 --- a/test/python/transpiler/test_unroll_custom_definitions.py +++ b/test/python/transpiler/test_unroll_custom_definitions.py @@ -16,10 +16,11 @@ from qiskit.circuit import EquivalenceLibrary, Gate, Qubit, Clbit, Parameter from qiskit.circuit import QuantumCircuit, QuantumRegister +from qiskit.circuit.classical import expr, types from qiskit.converters import circuit_to_dag from qiskit.exceptions import QiskitError from qiskit.transpiler import Target -from qiskit.circuit.library import CXGate, U3Gate +from qiskit.circuit.library import CXGate, U3Gate, UGate from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -317,3 +318,56 @@ def test_unroll_empty_definition_with_phase(self): pass_ = UnrollCustomDefinitions(EquivalenceLibrary(), ["u"]) expected = QuantumCircuit(2, global_phase=0.5) self.assertEqual(pass_(qc), expected) + + def test_leave_store_alone(self): + """Don't attempt to unroll `Store` instructions.""" + + pass_ = UnrollCustomDefinitions(EquivalenceLibrary(), ["u", "cx"]) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, a) + qc.compose(bell, [0, 1], inplace=True) + qc.store(b, a) + + expected = qc.copy_empty_like() + expected.store(b, a) + expected.compose(pass_(bell), [0, 1], inplace=True) + expected.store(b, a) + + self.assertEqual(pass_(qc), expected) + + def test_leave_store_alone_with_target(self): + """Don't attempt to unroll `Store` instructions with a `Target`.""" + + # Note no store. + target = Target() + target.add_instruction( + UGate(Parameter("a"), Parameter("b"), Parameter("c")), {(0,): None, (1,): None} + ) + target.add_instruction(CXGate(), {(0, 1): None, (1, 0): None}) + + pass_ = UnrollCustomDefinitions(EquivalenceLibrary(), target=target) + + bell = QuantumCircuit(2) + bell.h(0) + bell.cx(0, 1) + + a = expr.Var.new("a", types.Bool()) + b = expr.Var.new("b", types.Bool()) + qc = QuantumCircuit(2, inputs=[a]) + qc.add_var(b, a) + qc.compose(bell, [0, 1], inplace=True) + qc.store(b, a) + + expected = qc.copy_empty_like() + expected.store(b, a) + expected.compose(pass_(bell), [0, 1], inplace=True) + expected.store(b, a) + + self.assertEqual(pass_(qc), expected) From 4dedd6cd389c094ad72f3bf5449bae4ea82d23dd Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 2 May 2024 12:44:43 +0100 Subject: [PATCH 2/2] Clarify comment --- test/python/circuit/test_circuit_operations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 8ba71989219b..6caf194d37d9 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -1013,9 +1013,9 @@ def test_reverse_with_standlone_vars(self): qc.h(0) qc.cx(0, 1) with qc.if_test(a): - # We don't really comment on what should happen to control-flow operations in this - # method, and Sabre doesn't care (nor use this method, in the fast paths), so this is - # deliberately using a body of length 1 (a single `Store`). + # We don't really comment on what should happen within control-flow operations in this + # method - it's not really defined in a non-linear CFG. This deliberately uses a body + # of length 1 (a single `Store`), so there's only one possibility. qc.add_var(c, 12) expected = qc.copy_empty_like()