Repository: Qiskit/qiskit

Issue:
Add handling for `operational`, `faulty_{qubits,gates}` to `convert_to_target`
### What should we add?

`convert_to_target` doesn't currently look at `operational` and `faulty_{qubits,gates}` fields. `Target` nominally has support for these by removing faulty gates from each qubit, but this needs to be done while constructing the `Target`. Handling these within `convert_to_target` would allow for compiling against backends with faulty qubits.

(N.B. Executing jobs against these backends would still require an additional step of re-indexing compiled circuits to the original qubit indicies, but that may be eased by #9851 ).


Discussion:
#9900 might obsolete this feature request. (*edit*: unless you're intending for this to be a mitigation method for #9900.)
> for this to be a mitigation method for https://github.com/Qiskit/qiskit-terra/pull/9900.)

Right, exactly this.

Resolving patch:
diff --git a/qiskit/providers/backend_compat.py b/qiskit/providers/backend_compat.py
--- a/qiskit/providers/backend_compat.py
+++ b/qiskit/providers/backend_compat.py
@@ -36,6 +36,7 @@ def convert_to_target(
     defaults: PulseDefaults = None,
     custom_name_mapping: Optional[Dict[str, Any]] = None,
     add_delay: bool = False,
+    filter_faulty: bool = False,
 ):
     """Uses configuration, properties and pulse defaults
     to construct and return Target class.
@@ -70,6 +71,12 @@ def convert_to_target(
                 )
 
             qubits = tuple(gate.qubits)
+            if filter_faulty:
+                if any(not properties.is_qubit_operational(qubit) for qubit in qubits):
+                    continue
+                if not properties.is_gate_operational(name, gate.qubits):
+                    continue
+
             gate_props = {}
             for param in gate.parameters:
                 if param.name == "gate_error":
@@ -83,6 +90,9 @@ def convert_to_target(
         # Create measurement instructions:
         measure_props = {}
         for qubit, _ in enumerate(properties.qubits):
+            if filter_faulty:
+                if not properties.is_qubit_operational(qubit):
+                    continue
             measure_props[(qubit,)] = InstructionProperties(
                 duration=properties.readout_length(qubit),
                 error=properties.readout_error(qubit),
@@ -116,6 +126,9 @@ def convert_to_target(
         target.acquire_alignment = configuration.timing_constraints.get("acquire_alignment")
     # If a pulse defaults exists use that as the source of truth
     if defaults is not None:
+        faulty_qubits = set()
+        if filter_faulty and properties is not None:
+            faulty_qubits = set(properties.faulty_qubits())
         inst_map = defaults.instruction_schedule_map
         for inst in inst_map.instructions:
             for qarg in inst_map.qubits_with_instruction(inst):
@@ -129,8 +142,12 @@ def convert_to_target(
                 if inst in target:
                     if inst == "measure":
                         for qubit in qargs:
+                            if filter_faulty and qubit in faulty_qubits:
+                                continue
                             target[inst][(qubit,)].calibration = calibration_entry
                     elif qargs in target[inst]:
+                        if filter_faulty and any(qubit in faulty_qubits for qubit in qargs):
+                            continue
                         target[inst][qargs].calibration = calibration_entry
     combined_global_ops = set()
     if configuration.basis_gates:
@@ -196,6 +213,7 @@ def __init__(
         backend: BackendV1,
         name_mapping: Optional[Dict[str, Any]] = None,
         add_delay: bool = False,
+        filter_faulty: bool = False,
     ):
         """Initialize a BackendV2 converter instance based on a BackendV1 instance.
 
@@ -211,6 +229,9 @@ def __init__(
             add_delay: If set to true a :class:`~qiskit.circuit.Delay` operation
                 will be added to the target as a supported operation for all
                 qubits
+            filter_faulty: If the :class:`~.BackendProperties` object (if present) for
+                ``backend`` has any qubits or gates flagged as non-operational filter
+                those from the output target.
         """
         self._backend = backend
         self._config = self._backend.configuration()
@@ -229,6 +250,7 @@ def __init__(
         self._target = None
         self._name_mapping = name_mapping
         self._add_delay = add_delay
+        self._filter_faulty = filter_faulty
 
     @property
     def target(self):
@@ -247,6 +269,7 @@ def target(self):
                 self._defaults,
                 custom_name_mapping=self._name_mapping,
                 add_delay=self._add_delay,
+                filter_faulty=self._filter_faulty,
             )
         return self._target
 


Test patch:
diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py
--- a/test/python/providers/test_fake_backends.py
+++ b/test/python/providers/test_fake_backends.py
@@ -13,6 +13,8 @@
 # pylint: disable=missing-class-docstring,missing-function-docstring
 # pylint: disable=missing-module-docstring
 
+import datetime
+import itertools
 import operator
 import unittest
 
@@ -31,8 +33,10 @@
     FakeMumbaiV2,
     FakeYorktown,
     FakeMumbai,
+    FakeWashington,
 )
 from qiskit.providers.backend_compat import BackendV2Converter
+from qiskit.providers.models.backendproperties import BackendProperties
 from qiskit.providers.backend import BackendV2
 from qiskit.utils import optionals
 from qiskit.circuit.library import (
@@ -460,3 +464,132 @@ def __init__(self, num_ctrl_qubits, ctrl_state=None):
         counts = result.get_counts()
         max_count = max(counts.items(), key=operator.itemgetter(1))[0]
         self.assertEqual(max_count, "11")
+
+    def test_filter_faulty_qubits_backend_v2_converter(self):
+        """Test faulty qubits in v2 conversion."""
+        backend = FakeWashington()
+        # Get properties dict to make it easier to work with the properties API
+        # is difficult to edit because of the multiple layers of nesting and
+        # different object types
+        props_dict = backend.properties().to_dict()
+        for i in range(62, 67):
+            non_operational = {
+                "date": datetime.datetime.utcnow(),
+                "name": "operational",
+                "unit": "",
+                "value": 0,
+            }
+            props_dict["qubits"][i].append(non_operational)
+        backend._properties = BackendProperties.from_dict(props_dict)
+        v2_backend = BackendV2Converter(backend, filter_faulty=True)
+        for i in range(62, 67):
+            for qarg in v2_backend.target.qargs:
+                self.assertNotIn(i, qarg)
+
+    def test_filter_faulty_qubits_and_gates_backend_v2_converter(self):
+        """Test faulty gates and qubits."""
+        backend = FakeWashington()
+        # Get properties dict to make it easier to work with the properties API
+        # is difficult to edit because of the multiple layers of nesting and
+        # different object types
+        props_dict = backend.properties().to_dict()
+        for i in range(62, 67):
+            non_operational = {
+                "date": datetime.datetime.utcnow(),
+                "name": "operational",
+                "unit": "",
+                "value": 0,
+            }
+            props_dict["qubits"][i].append(non_operational)
+        invalid_cx_edges = {
+            (113, 114),
+            (114, 113),
+            (96, 100),
+            (100, 96),
+            (114, 109),
+            (109, 114),
+            (24, 34),
+            (34, 24),
+        }
+        non_operational_gate = {
+            "date": datetime.datetime.utcnow(),
+            "name": "operational",
+            "unit": "",
+            "value": 0,
+        }
+        for gate in props_dict["gates"]:
+            if tuple(gate["qubits"]) in invalid_cx_edges:
+                gate["parameters"].append(non_operational_gate)
+
+        backend._properties = BackendProperties.from_dict(props_dict)
+        v2_backend = BackendV2Converter(backend, filter_faulty=True)
+        for i in range(62, 67):
+            for qarg in v2_backend.target.qargs:
+                self.assertNotIn(i, qarg)
+        for edge in invalid_cx_edges:
+            self.assertNotIn(edge, v2_backend.target["cx"])
+
+    def test_filter_faulty_gates_v2_converter(self):
+        """Test just faulty gates in conversion."""
+        backend = FakeWashington()
+        # Get properties dict to make it easier to work with the properties API
+        # is difficult to edit because of the multiple layers of nesting and
+        # different object types
+        props_dict = backend.properties().to_dict()
+        invalid_cx_edges = {
+            (113, 114),
+            (114, 113),
+            (96, 100),
+            (100, 96),
+            (114, 109),
+            (109, 114),
+            (24, 34),
+            (34, 24),
+        }
+        non_operational_gate = {
+            "date": datetime.datetime.utcnow(),
+            "name": "operational",
+            "unit": "",
+            "value": 0,
+        }
+        for gate in props_dict["gates"]:
+            if tuple(gate["qubits"]) in invalid_cx_edges:
+                gate["parameters"].append(non_operational_gate)
+
+        backend._properties = BackendProperties.from_dict(props_dict)
+        v2_backend = BackendV2Converter(backend, filter_faulty=True)
+        for i in range(62, 67):
+            self.assertIn((i,), v2_backend.target.qargs)
+        for edge in invalid_cx_edges:
+            self.assertNotIn(edge, v2_backend.target["cx"])
+
+    def test_filter_faulty_no_faults_v2_converter(self):
+        """Test that faulty qubit filtering does nothing with all operational qubits and gates."""
+        backend = FakeWashington()
+        v2_backend = BackendV2Converter(backend, filter_faulty=True)
+        for i in range(v2_backend.num_qubits):
+            self.assertIn((i,), v2_backend.target.qargs)
+
+    @data(0, 1, 2, 3)
+    def test_faulty_full_path_transpile_connected_cmap(self, opt_level):
+        backend = FakeYorktown()
+        non_operational_gate = {
+            "date": datetime.datetime.utcnow(),
+            "name": "operational",
+            "unit": "",
+            "value": 0,
+        }
+        props = backend.properties().to_dict()
+        for gate in props["gates"]:
+            if tuple(sorted(gate["qubits"])) == (0, 1):
+                gate["parameters"].append(non_operational_gate)
+        backend._properties = BackendProperties.from_dict(props)
+        v2_backend = BackendV2Converter(backend, filter_faulty=True)
+        qc = QuantumCircuit(5)
+        for x, y in itertools.product(range(5), range(5)):
+            if x == y:
+                continue
+            qc.cx(x, y)
+        tqc = transpile(qc, v2_backend, seed_transpiler=433, optimization_level=opt_level)
+        connections = [tuple(sorted(tqc.find_bit(q).index for q in x.qubits)) for x in tqc.data]
+        self.assertNotIn((0, 1), connections)