Repository: Qiskit/qiskit-terra (python)

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`.

Pull request:
Fix transpile() for control flow operations with a Target/BackendV2
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary

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.

### Details and comments

Fixes #8824

TODO:

- [x] Fix transpile() test (may be a bug in test code or a bug in a transpiler pass with control flow), depends on #8880 and #8879
- [x] Add tests for coupling map generation in target
- [x] Add release note

Patch:
diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py
index 34884c839..c48731208 100644
--- a/qiskit/transpiler/target.py
+++ b/qiskit/transpiler/target.py
@@ -22,12 +22,14 @@ from collections import defaultdict
 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 @@ class Target(Mapping):
 
         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 @@ class Target(Mapping):
                 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 @@ class Target(Mapping):
             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 @@ class Target(Mapping):
                 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 @@ class Target(Mapping):
             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 @@ class Target(Mapping):
         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 @@ class Target(Mapping):
             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 @@ class Target(Mapping):
     @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 @@ class Target(Mapping):
         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 @@ class Target(Mapping):
                         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 @@ class Target(Mapping):
 
         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/releasenotes/notes/fix-target-control-flow-representation-09520e2838f0657e.yaml b/releasenotes/notes/fix-target-control-flow-representation-09520e2838f0657e.yaml
new file mode 100644
index 000000000..51a8f906a
--- /dev/null
+++ b/releasenotes/notes/fix-target-control-flow-representation-09520e2838f0657e.yaml
@@ -0,0 +1,105 @@
+---
+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``.
+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)


Test patch:
diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py
index f628e3702..0a9f2027e 100644
--- a/test/python/compiler/test_transpiler.py
+++ b/test/python/compiler/test_transpiler.py
@@ -32,7 +32,18 @@ from qiskit.circuit import Parameter, Gate, Qubit, Clbit
 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 @@ class TestTranspile(QiskitTestCase):
             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
index aa0814d5e..10e5bbed9 100644
--- a/test/python/transpiler/test_dense_layout.py
+++ b/test/python/transpiler/test_dense_layout.py
@@ -112,7 +112,7 @@ class TestDenseLayout(QiskitTestCase):
         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 939432c33..dd1ca7ba0 100644
--- a/test/python/transpiler/test_target.py
+++ b/test/python/transpiler/test_target.py
@@ -30,6 +30,7 @@ from qiskit.circuit.library import (
     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 @@ Instructions:
             )
 
 
+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()