Repository: Qiskit/qiskit

Issue:
Add support for variable width operations to Target
### What should we add?

Right now the target class doesn't have proper support for modelling instructions that have variable width. If you have a gate that can use more than a single number of qubits and the backend supports all variants of that instruction (or at least more than one). We don't have a way to express that. The target currently requires a 1:1 mapping of operation name to a `Operation` instance and that instance is used for type checking and parameter checking at different points in the transpiler. However this requirement won't work for variable width instructions because a single instance of the instructions can only have a single number of qubits.

To be able to accurately model these instructions being supported on a target backend we need to come up with an interface to model this in the target, which might mean introducing a `TargetV2`.


Discussion:

Resolving patch:
diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py
--- a/qiskit/transpiler/target.py
+++ b/qiskit/transpiler/target.py
@@ -22,12 +22,14 @@
 import datetime
 import io
 import logging
+import inspect
 
 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 +297,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 +325,37 @@ 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 or ``properties`` is set.
         """
+        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"
+                )
+            if properties is not None:
+                raise TranspilerError(
+                    "An instruction added globally by class can't have properties set."
+                )
+            instruction_name = name
         if properties is None:
             properties = {None: None}
-        instruction_name = name or instruction.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 +518,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 +533,18 @@ 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]]
+        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):
         """Get the operation names for a specified qargs tuple
@@ -609,6 +639,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 +671,23 @@ 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):
+                    # 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
+                    # 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 +704,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,7 +734,12 @@ def operations(self):
     @property
     def instructions(self):
         """Get the list of tuples ``(:class:`~qiskit.circuit.Instruction`, (qargs))``
-        for the target"""
+        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.
+        """
         return [
             (self._gate_name_map[op], qarg) for op in self._gate_map for qarg in self._gate_map[op]
         ]
@@ -711,6 +789,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 +799,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 +843,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):


Test patch:
diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py
--- 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,79 @@ 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)
+
+        def _visit_block(circuit, qubit_mapping=None):
+            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
--- 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
--- 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,342 @@ 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)))
+
+    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):
         properties = InstructionProperties()