From c47f4eab4543504bddd77a0dd5e83a06a5a14400 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 7 Oct 2022 13:51:27 -0400 Subject: [PATCH 01/11] Fix transpile() for control flow operations with a Target/BackendV2 This commit fixes an issue when compiling with circuits that have control flow operations and are running transpile() with a BackendV2 instance or a custom Target. In these cases the transpile() operation would fail to run because the Target didn't have a provision to recognize a global variable width operation as part of the target. Previously all operations in the target needed to have an instance of an Operation so that the parameters and number of qubits could be verified. Each of these operation instances would be assigned a unique name. But, for control flow operations they're defined over a variable number of qubits and all have the same name (this is a similar problem for gates like mcx too). Not being able to fully represent the control flow operations in a target was preventing running transpile() on a circuit with control flow. This commit fixes this by adding support to the target to represent globally defined operations by passing the class into Target.add_instruction instead of an instance of that class. When the class is received the Target class will treat that operation name and class as always being valid for the target for all qubits and parameters. This can then be used to represent control flow operations in the target and run transpile() with control flow operations. Fixes #8824 --- qiskit/transpiler/target.py | 132 ++++++-- test/python/compiler/test_transpiler.py | 90 +++++- test/python/transpiler/test_dense_layout.py | 2 +- test/python/transpiler/test_target.py | 317 ++++++++++++++++++++ 4 files changed, 518 insertions(+), 23 deletions(-) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 34884c8392e5..e73adb1c57db 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -22,12 +22,15 @@ import datetime import io import logging +import inspect +import warnings import retworkx as rx from qiskit.circuit.parameter import Parameter from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap from qiskit.transpiler.coupling import CouplingMap +from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.timing_constraints import TimingConstraints @@ -295,7 +298,11 @@ def add_instruction(self, instruction, properties=None, name=None): Args: instruction (qiskit.circuit.Instruction): The operation object to add to the map. If it's - paramerterized any value of the parameter can be set + paramerterized any value of the parameter can be set. Optionally for variable width + instructions (such as control flow operations such as :class:`~.ForLoop` or + :class:`~MCXGate`) you can specify the class. If the class is specified than the + ``name`` argument must be specified. When a class is used the gate is treated as global + and not having any properties set. properties (dict): A dictionary of qarg entries to an :class:`~qiskit.transpiler.InstructionProperties` object for that instruction implementation on the backend. Properties are optional @@ -319,19 +326,33 @@ def add_instruction(self, instruction, properties=None, name=None): documentation for the :class:`~qiskit.transpiler.Target` class). Raises: AttributeError: If gate is already in map + TranspilerError: If an operation class is passed in for ``instruction`` and no name + is specified. """ if properties is None: properties = {None: None} - instruction_name = name or instruction.name + is_class = inspect.isclass(instruction) + if not is_class: + instruction_name = name or instruction.name + else: + # Invalid to have class input without a name with characters set "" is not a valid name + if not name: + raise TranspilerError( + "A name must be specified when defining a supported global operation by class" + ) + instruction_name = name if instruction_name in self._gate_map: raise AttributeError("Instruction %s is already in the target" % instruction_name) self._gate_name_map[instruction_name] = instruction - qargs_val = {} - for qarg in properties: - if qarg is not None: - self.num_qubits = max(self.num_qubits, max(qarg) + 1) - qargs_val[qarg] = properties[qarg] - self._qarg_gate_map[qarg].add(instruction_name) + if is_class: + qargs_val = {None: None} + else: + qargs_val = {} + for qarg in properties: + if qarg is not None: + self.num_qubits = max(self.num_qubits, max(qarg) + 1) + qargs_val[qarg] = properties[qarg] + self._qarg_gate_map[qarg].add(instruction_name) self._gate_map[instruction_name] = qargs_val self._coupling_graph = None self._instruction_durations = None @@ -494,7 +515,9 @@ def operation_from_name(self, instruction): instruction (str): The instruction name to get the :class:`~qiskit.circuit.Instruction` instance for Returns: - qiskit.circuit.Instruction: The Instruction instance corresponding to the name + qiskit.circuit.Instruction: The Instruction instance corresponding to the + name. This also can also be the class for globally defined variable with + operations. """ return self._gate_name_map[instruction] @@ -507,14 +530,17 @@ def operations_for_qargs(self, qargs): instructions that apply to qubit 0. Returns: list: The list of :class:`~qiskit.circuit.Instruction` instances - that apply to the specified qarg. + that apply to the specified qarg. This may also be a class if + a variable width operation is globally defined. Raises: KeyError: If qargs is not in target """ if qargs not in self._qarg_gate_map: raise KeyError(f"{qargs} not in target.") - return [self._gate_name_map[x] for x in self._qarg_gate_map[qargs]] + res = [self._gate_name_map[x] for x in self._qarg_gate_map[qargs]] + res += [self._gate_name_map[x] for x in self._qarg_gate_map[None]] + return res def operation_names_for_qargs(self, qargs): """Get the operation names for a specified qargs tuple @@ -609,6 +635,20 @@ def check_obj_params(parameters, obj): qargs = tuple(qargs) if operation_class is not None: for op_name, obj in self._gate_name_map.items(): + if inspect.isclass(obj): + if obj != operation_class: + continue + # If no qargs a operation class is supported + if qargs is None: + return True + # If qargs set then validate no duplicates and all indices are valid on device + elif all(qarg <= self.num_qubits for qarg in qargs) and len(set(qargs)) == len( + qargs + ): + return True + else: + return False + if isinstance(obj, operation_class): if parameters is not None: if len(parameters) != len(obj.params): @@ -627,6 +667,25 @@ def check_obj_params(parameters, obj): if operation_name in self._gate_map: if parameters is not None: obj = self._gate_name_map[operation_name] + if inspect.isclass(obj): + warnings.warn( + "A parameter was specified for an operation that is defined as a " + "globally supported class there is no available validation (including " + "whether the specified operation supports parameters), the returned " + "value will not factor in the argument `parameters`.", + UserWarning, + stacklevel=2, + ) + # If no qargs a operation class is supported + if qargs is None: + return True + # If qargs set then validate no duplicates and all indices are valid on device + elif all(qarg <= self.num_qubits for qarg in qargs) and len(set(qargs)) == len( + qargs + ): + return True + else: + return False if len(parameters) != len(obj.params): return False for index, param in enumerate(parameters): @@ -643,9 +702,21 @@ def check_obj_params(parameters, obj): if qargs in self._gate_map[operation_name]: return True if self._gate_map[operation_name] is None or None in self._gate_map[operation_name]: - return self._gate_name_map[operation_name].num_qubits == len(qargs) and all( - x < self.num_qubits for x in qargs - ) + obj = self._gate_name_map[operation_name] + if inspect.isclass(obj): + if qargs is None: + return True + # If qargs set then validate no duplicates and all indices are valid on device + elif all(qarg <= self.num_qubits for qarg in qargs) and len(set(qargs)) == len( + qargs + ): + return True + else: + return False + else: + return self._gate_name_map[operation_name].num_qubits == len(qargs) and all( + x < self.num_qubits for x in qargs + ) return False @property @@ -661,10 +732,20 @@ def operations(self): @property def instructions(self): """Get the list of tuples ``(:class:`~qiskit.circuit.Instruction`, (qargs))`` - for the target""" - return [ - (self._gate_name_map[op], qarg) for op in self._gate_map for qarg in self._gate_map[op] - ] + for the target + + For globally defined variable width operations the tuple will be of the form + ``(class, None)`` where class is the actual operation class that + is globally defined. + """ + res = [] + for op in self._gate_map: + for qarg in self._gate_map[op]: + if not inspect.isclass(self._gate_name_map[op]): + res.append((self._gate_name_map[op], qarg)) + else: + res.append((self._gate_name_map[op], None)) + return res def instruction_properties(self, index): """Get the instruction properties for a specific instruction tuple @@ -711,6 +792,8 @@ def _build_coupling_graph(self): self._coupling_graph.add_nodes_from([{} for _ in range(self.num_qubits)]) for gate, qarg_map in self._gate_map.items(): for qarg, properties in qarg_map.items(): + if qarg is None: + continue if len(qarg) == 1: self._coupling_graph[qarg[0]] = properties elif len(qarg) == 2: @@ -719,6 +802,8 @@ def _build_coupling_graph(self): edge_data[gate] = properties except rx.NoEdgeBetweenNodes: self._coupling_graph.add_edge(*qarg, {gate: properties}) + if self._coupling_graph.num_edges() == 0 and any(x is None for x in self._qarg_gate_map): + self._coupling_graph = None def build_coupling_map(self, two_q_gate=None): """Get a :class:`~qiskit.transpiler.CouplingMap` from this target. @@ -761,9 +846,14 @@ def build_coupling_map(self, two_q_gate=None): if self._coupling_graph is None: self._build_coupling_graph() - cmap = CouplingMap() - cmap.graph = self._coupling_graph - return cmap + # if there is no connectivity constraints in the coupling graph treat it as not + # existing and return + if self._coupling_graph is not None: + cmap = CouplingMap() + cmap.graph = self._coupling_graph + return cmap + else: + return None @property def physical_qubits(self): diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index f628e3702e2d..8c4ec0ecb90f 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -32,7 +32,18 @@ from qiskit.compiler import transpile from qiskit.dagcircuit import DAGOutNode from qiskit.converters import circuit_to_dag -from qiskit.circuit.library import CXGate, U3Gate, U2Gate, U1Gate, RXGate, RYGate, RZGate, UGate +from qiskit.circuit.library import ( + CXGate, + U3Gate, + U2Gate, + U1Gate, + RXGate, + RYGate, + RZGate, + UGate, + CZGate, +) +from qiskit.circuit import IfElseOp, WhileLoopOp, ForLoopOp, ControlFlowOp from qiskit.circuit.measure import Measure from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import ( @@ -1469,6 +1480,83 @@ def test_target_ideal_gates(self, opt_level): expected.measure(qubit_reg, clbit_reg) self.assertEqual(result, expected) + # TODO: Add optimization level 2 and 3 after they support control flow + # compilation + @data(0, 1) + def test_transpile_with_custom_control_flow_target(self, opt_level): + """Test transpile() with a target and constrol flow ops.""" + target = FakeMumbaiV2().target + target.add_instruction(ForLoopOp, name="for_loop") + target.add_instruction(WhileLoopOp, name="while_loop") + target.add_instruction(IfElseOp, name="if_else") + + class CustomCX(Gate): + """Custom CX""" + + def __init__(self): + super().__init__("custom_cx", 2, []) + + def _define(self): + self._definition = QuantumCircuit(2) + self._definition.cx(0, 1) + + circuit = QuantumCircuit(6, 1) + circuit.h(0) + circuit.measure(0, 0) + circuit.cx(0, 1) + circuit.cz(0, 2) + circuit.append(CustomCX(), [1, 2], []) + with circuit.for_loop((1,)): + circuit.cx(0, 1) + circuit.cz(0, 2) + circuit.append(CustomCX(), [1, 2], []) + with circuit.if_test((circuit.clbits[0], True)) as else_: + circuit.cx(0, 1) + circuit.cz(0, 2) + circuit.append(CustomCX(), [1, 2], []) + with else_: + circuit.cx(3, 4) + circuit.cz(3, 5) + circuit.append(CustomCX(), [4, 5], []) + with circuit.while_loop((circuit.clbits[0], True)): + circuit.cx(3, 4) + circuit.cz(3, 5) + circuit.append(CustomCX(), [4, 5], []) + + transpiled = transpile( + circuit, optimization_level=opt_level, target=target, seed_transpiler=12434 + ) + # Tests of the complete validity of a circuit are mostly done at the indiviual pass level; + # here we're just checking that various passes do appear to have run. + self.assertIsInstance(transpiled, QuantumCircuit) + # Assert layout ran. + self.assertIsNot(getattr(transpiled, "_layout", None), None) + print(target) + print(transpiled) + + def _visit_block(circuit, qubit_mapping=None): + """Assert that every block contains at least one swap to imply that routing has run.""" + for instruction in circuit: + qargs = tuple(qubit_mapping[x] for x in instruction.qubits) + self.assertTrue(target.instruction_supported(instruction.operation.name, qargs)) + if isinstance(instruction.operation, ControlFlowOp): + for block in instruction.operation.blocks: + new_mapping = { + inner: qubit_mapping[outer] + for outer, inner in zip(instruction.qubits, block.qubits) + } + _visit_block(block, new_mapping) + # Assert unrolling ran. + self.assertNotIsInstance(instruction.operation, CustomCX) + # Assert translation ran. + self.assertNotIsInstance(instruction.operation, CZGate) + + # Assert routing ran. + _visit_block( + transpiled, + qubit_mapping={qubit: index for index, qubit in enumerate(transpiled.qubits)}, + ) + class StreamHandlerRaiseException(StreamHandler): """Handler class that will raise an exception on formatting errors.""" diff --git a/test/python/transpiler/test_dense_layout.py b/test/python/transpiler/test_dense_layout.py index aa0814d5e2df..10e5bbed9252 100644 --- a/test/python/transpiler/test_dense_layout.py +++ b/test/python/transpiler/test_dense_layout.py @@ -112,7 +112,7 @@ def test_5q_circuit_19q_target_without_noise(self): dag = circuit_to_dag(circuit) instruction_props = {edge: None for edge in CouplingMap.from_heavy_hex(3).get_edges()} noiseless_target = Target() - noiseless_target.add_instruction(CXGate, instruction_props) + noiseless_target.add_instruction(CXGate(), instruction_props) pass_ = DenseLayout(target=noiseless_target) pass_.run(dag) layout = pass_.property_set["layout"] diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 939432c33e81..150e58c641a3 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -30,6 +30,7 @@ RZXGate, CZGate, ) +from qiskit.circuit import IfElseOp, ForLoopOp, WhileLoopOp from qiskit.circuit.measure import Measure from qiskit.circuit.parameter import Parameter from qiskit import pulse @@ -1165,6 +1166,322 @@ def test_timing_constraints(self): ) +class TestGlobalVariableWidthOperations(QiskitTestCase): + def setUp(self): + super().setUp() + self.theta = Parameter("theta") + self.phi = Parameter("phi") + self.lam = Parameter("lambda") + self.target_global_gates_only = Target(num_qubits=5) + self.target_global_gates_only.add_instruction(CXGate()) + self.target_global_gates_only.add_instruction(UGate(self.theta, self.phi, self.lam)) + self.target_global_gates_only.add_instruction(Measure()) + self.target_global_gates_only.add_instruction(IfElseOp, name="if_else") + self.target_global_gates_only.add_instruction(ForLoopOp, name="for_loop") + self.target_global_gates_only.add_instruction(WhileLoopOp, name="while_loop") + self.ibm_target = Target() + i_props = { + (0,): InstructionProperties(duration=35.5e-9, error=0.000413), + (1,): InstructionProperties(duration=35.5e-9, error=0.000502), + (2,): InstructionProperties(duration=35.5e-9, error=0.0004003), + (3,): InstructionProperties(duration=35.5e-9, error=0.000614), + (4,): InstructionProperties(duration=35.5e-9, error=0.006149), + } + self.ibm_target.add_instruction(IGate(), i_props) + rz_props = { + (0,): InstructionProperties(duration=0, error=0), + (1,): InstructionProperties(duration=0, error=0), + (2,): InstructionProperties(duration=0, error=0), + (3,): InstructionProperties(duration=0, error=0), + (4,): InstructionProperties(duration=0, error=0), + } + self.ibm_target.add_instruction(RZGate(self.theta), rz_props) + sx_props = { + (0,): InstructionProperties(duration=35.5e-9, error=0.000413), + (1,): InstructionProperties(duration=35.5e-9, error=0.000502), + (2,): InstructionProperties(duration=35.5e-9, error=0.0004003), + (3,): InstructionProperties(duration=35.5e-9, error=0.000614), + (4,): InstructionProperties(duration=35.5e-9, error=0.006149), + } + self.ibm_target.add_instruction(SXGate(), sx_props) + x_props = { + (0,): InstructionProperties(duration=35.5e-9, error=0.000413), + (1,): InstructionProperties(duration=35.5e-9, error=0.000502), + (2,): InstructionProperties(duration=35.5e-9, error=0.0004003), + (3,): InstructionProperties(duration=35.5e-9, error=0.000614), + (4,): InstructionProperties(duration=35.5e-9, error=0.006149), + } + self.ibm_target.add_instruction(XGate(), x_props) + cx_props = { + (3, 4): InstructionProperties(duration=270.22e-9, error=0.00713), + (4, 3): InstructionProperties(duration=305.77e-9, error=0.00713), + (3, 1): InstructionProperties(duration=462.22e-9, error=0.00929), + (1, 3): InstructionProperties(duration=497.77e-9, error=0.00929), + (1, 2): InstructionProperties(duration=227.55e-9, error=0.00659), + (2, 1): InstructionProperties(duration=263.11e-9, error=0.00659), + (0, 1): InstructionProperties(duration=519.11e-9, error=0.01201), + (1, 0): InstructionProperties(duration=554.66e-9, error=0.01201), + } + self.ibm_target.add_instruction(CXGate(), cx_props) + measure_props = { + (0,): InstructionProperties(duration=5.813e-6, error=0.0751), + (1,): InstructionProperties(duration=5.813e-6, error=0.0225), + (2,): InstructionProperties(duration=5.813e-6, error=0.0146), + (3,): InstructionProperties(duration=5.813e-6, error=0.0215), + (4,): InstructionProperties(duration=5.813e-6, error=0.0333), + } + self.ibm_target.add_instruction(Measure(), measure_props) + self.ibm_target.add_instruction(IfElseOp, name="if_else") + self.ibm_target.add_instruction(ForLoopOp, name="for_loop") + self.ibm_target.add_instruction(WhileLoopOp, name="while_loop") + self.aqt_target = Target(description="AQT Target") + rx_props = { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + } + self.aqt_target.add_instruction(RXGate(self.theta), rx_props) + ry_props = { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + } + self.aqt_target.add_instruction(RYGate(self.theta), ry_props) + rz_props = { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + } + self.aqt_target.add_instruction(RZGate(self.theta), rz_props) + r_props = { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + } + self.aqt_target.add_instruction(RGate(self.theta, self.phi), r_props) + rxx_props = { + (0, 1): None, + (0, 2): None, + (0, 3): None, + (0, 4): None, + (1, 0): None, + (2, 0): None, + (3, 0): None, + (4, 0): None, + (1, 2): None, + (1, 3): None, + (1, 4): None, + (2, 1): None, + (3, 1): None, + (4, 1): None, + (2, 3): None, + (2, 4): None, + (3, 2): None, + (4, 2): None, + (3, 4): None, + (4, 3): None, + } + self.aqt_target.add_instruction(RXXGate(self.theta), rxx_props) + measure_props = { + (0,): None, + (1,): None, + (2,): None, + (3,): None, + (4,): None, + } + self.aqt_target.add_instruction(Measure(), measure_props) + self.aqt_target.add_instruction(IfElseOp, name="if_else") + self.aqt_target.add_instruction(ForLoopOp, name="for_loop") + self.aqt_target.add_instruction(WhileLoopOp, name="while_loop") + + def test_qargs(self): + expected_ibm = { + (0,), + (1,), + (2,), + (3,), + (4,), + (3, 4), + (4, 3), + (3, 1), + (1, 3), + (1, 2), + (2, 1), + (0, 1), + (1, 0), + } + self.assertEqual(expected_ibm, self.ibm_target.qargs) + expected_aqt = { + (0,), + (1,), + (2,), + (3,), + (4,), + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (1, 0), + (2, 0), + (3, 0), + (4, 0), + (1, 2), + (1, 3), + (1, 4), + (2, 1), + (3, 1), + (4, 1), + (2, 3), + (2, 4), + (3, 2), + (4, 2), + (3, 4), + (4, 3), + } + self.assertEqual(expected_aqt, self.aqt_target.qargs) + self.assertEqual(None, self.target_global_gates_only.qargs) + + def test_qargs_for_operation_name(self): + self.assertEqual( + self.ibm_target.qargs_for_operation_name("rz"), {(0,), (1,), (2,), (3,), (4,)} + ) + self.assertEqual( + self.aqt_target.qargs_for_operation_name("rz"), {(0,), (1,), (2,), (3,), (4,)} + ) + self.assertIsNone(self.target_global_gates_only.qargs_for_operation_name("cx")) + self.assertIsNone(self.ibm_target.qargs_for_operation_name("if_else")) + self.assertIsNone(self.aqt_target.qargs_for_operation_name("while_loop")) + + def test_instruction_names(self): + self.assertEqual( + self.ibm_target.operation_names, + {"rz", "id", "sx", "x", "cx", "measure", "if_else", "while_loop", "for_loop"}, + ) + self.assertEqual( + self.aqt_target.operation_names, + {"rz", "ry", "rx", "rxx", "r", "measure", "if_else", "while_loop", "for_loop"}, + ) + self.assertEqual( + self.target_global_gates_only.operation_names, + {"u", "cx", "measure", "if_else", "while_loop", "for_loop"}, + ) + + def test_operations(self): + ibm_expected = [ + RZGate(self.theta), + IGate(), + SXGate(), + XGate(), + CXGate(), + Measure(), + WhileLoopOp, + IfElseOp, + ForLoopOp, + ] + for gate in ibm_expected: + self.assertIn(gate, self.ibm_target.operations) + aqt_expected = [ + RZGate(self.theta), + RXGate(self.theta), + RYGate(self.theta), + RGate(self.theta, self.phi), + RXXGate(self.theta), + ForLoopOp, + IfElseOp, + WhileLoopOp, + ] + for gate in aqt_expected: + self.assertIn(gate, self.aqt_target.operations) + fake_expected = [ + UGate(self.theta, self.phi, self.lam), + CXGate(), + Measure(), + ForLoopOp, + WhileLoopOp, + IfElseOp, + ] + for gate in fake_expected: + self.assertIn(gate, self.target_global_gates_only.operations) + + def test_instructions(self): + ibm_expected = [ + (IGate(), (0,)), + (IGate(), (1,)), + (IGate(), (2,)), + (IGate(), (3,)), + (IGate(), (4,)), + (RZGate(self.theta), (0,)), + (RZGate(self.theta), (1,)), + (RZGate(self.theta), (2,)), + (RZGate(self.theta), (3,)), + (RZGate(self.theta), (4,)), + (SXGate(), (0,)), + (SXGate(), (1,)), + (SXGate(), (2,)), + (SXGate(), (3,)), + (SXGate(), (4,)), + (XGate(), (0,)), + (XGate(), (1,)), + (XGate(), (2,)), + (XGate(), (3,)), + (XGate(), (4,)), + (CXGate(), (3, 4)), + (CXGate(), (4, 3)), + (CXGate(), (3, 1)), + (CXGate(), (1, 3)), + (CXGate(), (1, 2)), + (CXGate(), (2, 1)), + (CXGate(), (0, 1)), + (CXGate(), (1, 0)), + (Measure(), (0,)), + (Measure(), (1,)), + (Measure(), (2,)), + (Measure(), (3,)), + (Measure(), (4,)), + (IfElseOp, None), + (ForLoopOp, None), + (WhileLoopOp, None), + ] + self.assertEqual(ibm_expected, self.ibm_target.instructions) + ideal_sim_expected = [ + (CXGate(), None), + (UGate(self.theta, self.phi, self.lam), None), + (Measure(), None), + (IfElseOp, None), + (ForLoopOp, None), + (WhileLoopOp, None), + ] + self.assertEqual(ideal_sim_expected, self.target_global_gates_only.instructions) + + def test_instruction_supported(self): + self.assertTrue(self.aqt_target.instruction_supported("r", (0,))) + self.assertFalse(self.aqt_target.instruction_supported("cx", (0, 1))) + self.assertTrue(self.target_global_gates_only.instruction_supported("cx", (0, 1))) + self.assertFalse(self.target_global_gates_only.instruction_supported("cx", (0, 524))) + self.assertFalse(self.target_global_gates_only.instruction_supported("cx", (0, 1, 2))) + self.assertTrue(self.aqt_target.instruction_supported("while_loop", (0, 1, 2, 3))) + self.assertTrue( + self.aqt_target.instruction_supported(operation_class=WhileLoopOp, qargs=(0, 1, 2, 3)) + ) + self.assertFalse( + self.ibm_target.instruction_supported( + operation_class=IfElseOp, qargs=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + ) + ) + self.assertFalse( + self.ibm_target.instruction_supported(operation_class=IfElseOp, qargs=(0, 425)) + ) + self.assertFalse(self.ibm_target.instruction_supported("for_loop", qargs=(0, 425))) + + class TestInstructionProperties(QiskitTestCase): def test_empty_repr(self): properties = InstructionProperties() From 8f2605e72d1a6a4b721c6827c418406c5381ab89 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 10 Oct 2022 09:28:58 -0400 Subject: [PATCH 02/11] Simplify test slightly --- test/python/compiler/test_transpiler.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 8c4ec0ecb90f..9481aacb211e 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1518,11 +1518,6 @@ def _define(self): circuit.cx(3, 4) circuit.cz(3, 5) circuit.append(CustomCX(), [4, 5], []) - with circuit.while_loop((circuit.clbits[0], True)): - circuit.cx(3, 4) - circuit.cz(3, 5) - circuit.append(CustomCX(), [4, 5], []) - transpiled = transpile( circuit, optimization_level=opt_level, target=target, seed_transpiler=12434 ) @@ -1531,11 +1526,8 @@ def _define(self): self.assertIsInstance(transpiled, QuantumCircuit) # Assert layout ran. self.assertIsNot(getattr(transpiled, "_layout", None), None) - print(target) - print(transpiled) def _visit_block(circuit, qubit_mapping=None): - """Assert that every block contains at least one swap to imply that routing has run.""" for instruction in circuit: qargs = tuple(qubit_mapping[x] for x in instruction.qubits) self.assertTrue(target.instruction_supported(instruction.operation.name, qargs)) From 905adc358aa06e2b9b8feebbda8aea1a5353380e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 10 Oct 2022 18:47:50 -0400 Subject: [PATCH 03/11] Add release note --- ...-flow-representation-09520e2838f0657e.yaml | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 releasenotes/notes/fix-target-control-flow-representation-09520e2838f0657e.yaml diff --git a/releasenotes/notes/fix-target-control-flow-representation-09520e2838f0657e.yaml b/releasenotes/notes/fix-target-control-flow-representation-09520e2838f0657e.yaml new file mode 100644 index 000000000000..e6607a59e186 --- /dev/null +++ b/releasenotes/notes/fix-target-control-flow-representation-09520e2838f0657e.yaml @@ -0,0 +1,82 @@ +--- +features: + - | + Add support for representing an operation that has a variable width + to the :class:`~.Target` class. Previously, a :class:`~.Target` object + needed to have an instance of :class:`~Operation` defined for each + operation supported in the target. This was used for both validation + of arguments and parameters of the operation. However, for operations + that have a variable width this wasn't possible because each instance + of an :class:`~Operation` class can only have a fixed number of qubits. + For cases where a backend supports variable width operations the + instruction can be added with the class of the operation instead of an + instance. In such cases the operation will be treated as globally + supported on all qubits. For example, if building a target like:: + + from qiskit.transpiler import Target + + ibm_target = Target() + i_props = { + (0,): InstructionProperties(duration=35.5e-9, error=0.000413), + (1,): InstructionProperties(duration=35.5e-9, error=0.000502), + (2,): InstructionProperties(duration=35.5e-9, error=0.0004003), + (3,): InstructionProperties(duration=35.5e-9, error=0.000614), + (4,): InstructionProperties(duration=35.5e-9, error=0.006149), + } + ibm_target.add_instruction(IGate(), i_props) + rz_props = { + (0,): InstructionProperties(duration=0, error=0), + (1,): InstructionProperties(duration=0, error=0), + (2,): InstructionProperties(duration=0, error=0), + (3,): InstructionProperties(duration=0, error=0), + (4,): InstructionProperties(duration=0, error=0), + } + ibm_target.add_instruction(RZGate(theta), rz_props) + sx_props = { + (0,): InstructionProperties(duration=35.5e-9, error=0.000413), + (1,): InstructionProperties(duration=35.5e-9, error=0.000502), + (2,): InstructionProperties(duration=35.5e-9, error=0.0004003), + (3,): InstructionProperties(duration=35.5e-9, error=0.000614), + (4,): InstructionProperties(duration=35.5e-9, error=0.006149), + } + ibm_target.add_instruction(SXGate(), sx_props) + x_props = { + (0,): InstructionProperties(duration=35.5e-9, error=0.000413), + (1,): InstructionProperties(duration=35.5e-9, error=0.000502), + (2,): InstructionProperties(duration=35.5e-9, error=0.0004003), + (3,): InstructionProperties(duration=35.5e-9, error=0.000614), + (4,): InstructionProperties(duration=35.5e-9, error=0.006149), + } + ibm_target.add_instruction(XGate(), x_props) + cx_props = { + (3, 4): InstructionProperties(duration=270.22e-9, error=0.00713), + (4, 3): InstructionProperties(duration=305.77e-9, error=0.00713), + (3, 1): InstructionProperties(duration=462.22e-9, error=0.00929), + (1, 3): InstructionProperties(duration=497.77e-9, error=0.00929), + (1, 2): InstructionProperties(duration=227.55e-9, error=0.00659), + (2, 1): InstructionProperties(duration=263.11e-9, error=0.00659), + (0, 1): InstructionProperties(duration=519.11e-9, error=0.01201), + (1, 0): InstructionProperties(duration=554.66e-9, error=0.01201), + } + ibm_target.add_instruction(CXGate(), cx_props) + measure_props = { + (0,): InstructionProperties(duration=5.813e-6, error=0.0751), + (1,): InstructionProperties(duration=5.813e-6, error=0.0225), + (2,): InstructionProperties(duration=5.813e-6, error=0.0146), + (3,): InstructionProperties(duration=5.813e-6, error=0.0215), + (4,): InstructionProperties(duration=5.813e-6, error=0.0333), + } + ibm_target.add_instruction(Measure(), measure_props) + ibm_target.add_instruction(IfElseOp, name="if_else") + ibm_target.add_instruction(ForLoopOp, name="for_loop") + ibm_target.add_instruction(WhileLoopOp, name="while_loop") + + The :class:`~.IfElseOp`, :class:`~.ForLoopOp`, and :class:`~.WhileLoopOp` + operations are globally supported for any number of qubits. This is then + reflected by other calls in the :class:`~.Target` API such as + :meth:`~.Target.instruction_supported`:: + + ibm_target.instruction_supported(operation_class=WhileLoopOp, qargs=(0, 2, 3, 4)) + ibm_target.instruction_supported('if_else', qargs=(0, 1)) + + both return ``True``. From 730f04ec1f9ed321fdd25d0f39a3a4f40d6bd95e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 10 Oct 2022 18:52:45 -0400 Subject: [PATCH 04/11] Add coupling map target test --- test/python/transpiler/test_target.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 150e58c641a3..dd1ca7ba0a0c 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -1481,6 +1481,26 @@ def test_instruction_supported(self): ) self.assertFalse(self.ibm_target.instruction_supported("for_loop", qargs=(0, 425))) + def test_coupling_map(self): + self.assertIsNone(self.target_global_gates_only.build_coupling_map()) + self.assertEqual( + set(CouplingMap.from_full(5).get_edges()), + set(self.aqt_target.build_coupling_map().get_edges()), + ) + self.assertEqual( + { + (3, 4), + (4, 3), + (3, 1), + (1, 3), + (1, 2), + (2, 1), + (0, 1), + (1, 0), + }, + set(self.ibm_target.build_coupling_map().get_edges()), + ) + class TestInstructionProperties(QiskitTestCase): def test_empty_repr(self): From 6ce7fe6375d34bf879ed9aac7bcdae2d3102179d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 12 Oct 2022 13:10:18 -0400 Subject: [PATCH 05/11] Raise if instruction class and properties are both set --- qiskit/transpiler/target.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index e73adb1c57db..ce7756d32460 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -327,7 +327,7 @@ def add_instruction(self, instruction, properties=None, name=None): Raises: AttributeError: If gate is already in map TranspilerError: If an operation class is passed in for ``instruction`` and no name - is specified. + is specified or ``properties`` is set. """ if properties is None: properties = {None: None} @@ -340,6 +340,10 @@ def add_instruction(self, instruction, properties=None, name=None): raise TranspilerError( "A name must be specified when defining a supported global operation by class" ) + if properties is not None: + raise TranspilerError( + "An instruction added globally by class can't have properties set." + ) instruction_name = name if instruction_name in self._gate_map: raise AttributeError("Instruction %s is already in the target" % instruction_name) From e1b72bd35cd47cfdb55b562eca2e98e277240a1b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 12 Oct 2022 13:18:00 -0400 Subject: [PATCH 06/11] Only return global gates if they exist --- qiskit/transpiler/target.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index ce7756d32460..1f72893162c2 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -543,7 +543,8 @@ def operations_for_qargs(self, qargs): if qargs not in self._qarg_gate_map: raise KeyError(f"{qargs} not in target.") res = [self._gate_name_map[x] for x in self._qarg_gate_map[qargs]] - res += [self._gate_name_map[x] for x in self._qarg_gate_map[None]] + if None in self._qarg_gate_map: + res += [self._gate_name_map[x] for x in self._qarg_gate_map[None]] return res def operation_names_for_qargs(self, qargs): From 07ed34fb046ec7fb798406405c55aaffaf476535 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 12 Oct 2022 13:26:54 -0400 Subject: [PATCH 07/11] Change UserWarning to a comment --- qiskit/transpiler/target.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 1f72893162c2..9fbefc278154 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -673,14 +673,12 @@ def check_obj_params(parameters, obj): if parameters is not None: obj = self._gate_name_map[operation_name] if inspect.isclass(obj): - warnings.warn( - "A parameter was specified for an operation that is defined as a " - "globally supported class there is no available validation (including " - "whether the specified operation supports parameters), the returned " - "value will not factor in the argument `parameters`.", - UserWarning, - stacklevel=2, - ) + # The parameters argument was set and the operation_name specified is + # defined as a globally supported class in the target. This means + # there is no available validation (including whether the specified + # operation supports parameters), the returned value will not factor + # in the argument `parameters`, + # If no qargs a operation class is supported if qargs is None: return True From 17ee512e87676059faee21a21e62c6afbec35201 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 12 Oct 2022 13:26:58 -0400 Subject: [PATCH 08/11] Revert change to instructions() getter --- qiskit/transpiler/target.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 9fbefc278154..32d440056674 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -741,14 +741,9 @@ def instructions(self): ``(class, None)`` where class is the actual operation class that is globally defined. """ - res = [] - for op in self._gate_map: - for qarg in self._gate_map[op]: - if not inspect.isclass(self._gate_name_map[op]): - res.append((self._gate_name_map[op], qarg)) - else: - res.append((self._gate_name_map[op], None)) - return res + return [ + (self._gate_name_map[op], qarg) for op in self._gate_map for qarg in self._gate_map[op] + ] def instruction_properties(self, index): """Get the instruction properties for a specific instruction tuple From f06d1b89ec38c0762df6b9352d1e21c3f1a2fad4 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 12 Oct 2022 13:36:44 -0400 Subject: [PATCH 09/11] Add release note about coupling map api change --- ...-flow-representation-09520e2838f0657e.yaml | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/releasenotes/notes/fix-target-control-flow-representation-09520e2838f0657e.yaml b/releasenotes/notes/fix-target-control-flow-representation-09520e2838f0657e.yaml index e6607a59e186..51a8f906a6c2 100644 --- a/releasenotes/notes/fix-target-control-flow-representation-09520e2838f0657e.yaml +++ b/releasenotes/notes/fix-target-control-flow-representation-09520e2838f0657e.yaml @@ -80,3 +80,26 @@ features: ibm_target.instruction_supported('if_else', qargs=(0, 1)) both return ``True``. +upgrade: + - | + For :class:`~.Target` objects that only contain globally defined 2 qubit + operations without any connectivity constaints the return from the + :meth:`.Target.build_coupling_map` method will now return ``None`` instead + of a :class:`~.CouplingMap` object that contains ``num_qubits`` nodes + and no edges. This change was made to better reflect the actual + connectivity constraints of the :class:`~.Target` because in this case + there are no connectivity constraints on the backend being modeled by + the :class:`~.Target`, not a lack of connecitvity. If you desire the + previous behavior for any reason you can reproduce it by checking for a + ``None`` and manually building a coupling map, for example:: + + from qiskit.transpiler import Target + from qiskit.circuit.library import CXGate + + target = Target(num_qubits=3) + target.add_instruction(CXGate()) + cmap = target.build_coupling_map() + if cmap is None: + cmap = CouplingMap() + for i in range(target.num_qubits): + cmap.add_physical_qubit(i) From 746519bcf471d24cff2988ea7b202b8e153b156b Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 12 Oct 2022 14:08:39 -0400 Subject: [PATCH 10/11] Fix lint and logic bug --- qiskit/transpiler/target.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 32d440056674..c48731208d54 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -23,7 +23,6 @@ import io import logging import inspect -import warnings import retworkx as rx @@ -329,8 +328,6 @@ def add_instruction(self, instruction, properties=None, name=None): TranspilerError: If an operation class is passed in for ``instruction`` and no name is specified or ``properties`` is set. """ - if properties is None: - properties = {None: None} is_class = inspect.isclass(instruction) if not is_class: instruction_name = name or instruction.name @@ -345,6 +342,8 @@ def add_instruction(self, instruction, properties=None, name=None): "An instruction added globally by class can't have properties set." ) instruction_name = name + if properties is None: + properties = {None: None} if instruction_name in self._gate_map: raise AttributeError("Instruction %s is already in the target" % instruction_name) self._gate_name_map[instruction_name] = instruction From 600c092fe91ccdaf49066e78bc8afa0e1e5dca74 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 12 Oct 2022 14:11:25 -0400 Subject: [PATCH 11/11] Add back nested while_loop to transpile() test --- test/python/compiler/test_transpiler.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 9481aacb211e..0a9f2027edad 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1518,6 +1518,10 @@ def _define(self): circuit.cx(3, 4) circuit.cz(3, 5) circuit.append(CustomCX(), [4, 5], []) + with circuit.while_loop((circuit.clbits[0], True)): + circuit.cx(3, 4) + circuit.cz(3, 5) + circuit.append(CustomCX(), [4, 5], []) transpiled = transpile( circuit, optimization_level=opt_level, target=target, seed_transpiler=12434 )