Repository: Qiskit/qiskit

Issue:
qiskit.provider.back_compat.convert_to_target doesn't add control flow instructions to the target
### Environment

- **Qiskit version**: 1.0
- **Python version**: 3.11.7
- **Operating system**: macOS 13.6


### What is happening?

`convert_to_target` doesn't populate the target with control flow instructions.

### How can we reproduce the issue?

Example using an IBM backend, but the problem is not due to the `qiskit_ibm_runtime`.

```python
from qiskit_ibm_runtime import QiskitRuntimeService

# pick any Eagle backend
backend = service.get_backend("ibm_brisbane")

set(instr.name for instr, qargs in backend.target.instructions)
```

will be missing `if_else`, `for_loop`, and `switch_case`. 

Following the chain, you see that `backend.target` is calling:
```python
convert_to_target(
                configuration=self._configuration,
                properties=self._properties,
                defaults=self._defaults,
            )
```

### What should happen?

The control flow operations should be in the target instructions.

### Any suggestions?

The issue seems to be that `convert_to_target` [populates a value](https://github.com/Qiskit/qiskit/blob/main/qiskit/providers/backend_compat.py#L94) `all_instructions` from `configuration.basis_gates`. And then iterates over `all_instructions` to [pick up control flow](https://github.com/Qiskit/qiskit/blob/main/qiskit/providers/backend_compat.py#L275). However, control flow instructions are not in basis gates. Instead, they are found in `supported_instructions`. Therefore, I think the change that is required is to populate `all_instructions` from `supported_instructions` rather than `basis_gates`.


Discussion:
We can switch this over to use `supported_instructions`, though that is a behavioural change of the function.

We'll need to have a fallback to `basis_gates`, though, because `supported_instructions` is an optional field on `BackendV1::BackendConfiguration`, so not all implementors will necessarily have it.  I think we might also need to define what we do if `supported_instructions` is not a superset of `basis_gates`; do we prefer `supported_instructions`, or use the union of the two?
I think the union of the two makes more sense.
I concur that union of the two makes the most sense.
The reason supported instructions isn't typically considered by default is historically the api response from IBM included a lot of instructions which had no context in the circuit model or for the transpiler in that field. In the `BackendV1`  path the transpiler ignores `supported_instructions` field as well and only looks at `basis_gates` (this is why things like measure and reset are in the basis gates list)

A straight union won't actually work in the general case for the converter as it's possible to end up with a backend that has an inconsistent set of gates which won't be interpreted well. For a concrete example you can look at the snapshots of older backends like:

https://github.com/Qiskit/qiskit-ibm-runtime/blob/main/qiskit_ibm_runtime/fake_provider/backends/hanoi/conf_hanoi.json

the supported instructions list there is:

```python
["rz", "play", "measure", "delay", "acquire", "sx", "cx", "reset", "x", "shiftf", "u1", "u3", "u2", "setf", "id"]
```

while the basis gates were:

```python
["id", "rz", "sx", "x", "cx", "reset"]
```

Doing a union would result in instructions that would trigger a warning but ultimately get ignored for the pulse things like `play` or `shiftf`. But the bigger concern are things like `u1`, `u2`, and `u3` (which are in the list because they were still defined pulse calibrations on the backend despite not being basis gates anymore). Doing a union of these lists would result in the output target having `u1`, `u2`, and `u3` being globally defined ideal gates (like what a simulator would put in the target). This potential inconsistency is a major issue with the backend v1 models and is larger than just this specific example. We need to take care with how we extend things here as we need the converter to support the general `BackendV1`  models not just the current IBM usage of the interface. 

That all being said I think the best path forward here is to look specifically for the presence of control flow instructions in the `supported_instructions` field and only add those to the target if they're present. There are only 4 of them: https://github.com/Qiskit/qiskit/blob/main/qiskit/circuit/controlflow/__init__.py#L27 and we can just do the intersection of `configuration.supported_instructions` and that frozen set. IIRC this is actually what the `qiskit-ibm-provider` used to do when it had it's own internal version of the `convert_to_target` function.
Special casing control flow is probably fine for now.

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
@@ -21,7 +21,7 @@
 from qiskit.providers.backend import QubitProperties
 from qiskit.providers.models.backendconfiguration import BackendConfiguration
 from qiskit.providers.models.backendproperties import BackendProperties
-
+from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES
 from qiskit.providers.models.pulsedefaults import PulseDefaults
 from qiskit.providers.options import Options
 from qiskit.providers.exceptions import BackendPropertyError
@@ -92,8 +92,12 @@ def convert_to_target(
 
     # Create instruction property placeholder from backend configuration
     basis_gates = set(getattr(configuration, "basis_gates", []))
+    supported_instructions = set(getattr(configuration, "supported_instructions", []))
     gate_configs = {gate.name: gate for gate in configuration.gates}
-    all_instructions = set.union(basis_gates, set(required))
+    all_instructions = set.union(
+        basis_gates, set(required), supported_instructions.intersection(CONTROL_FLOW_OP_NAMES)
+    )
+
     inst_name_map = {}  # type: Dict[str, Instruction]
 
     faulty_ops = set()


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
@@ -34,7 +34,7 @@
     FakeOpenPulse2Q,
     GenericBackendV2,
 )
-from qiskit.providers.backend_compat import BackendV2Converter
+from qiskit.providers.backend_compat import BackendV2Converter, convert_to_target
 from qiskit.providers.models.backendproperties import BackendProperties
 from qiskit.providers.backend import BackendV2
 from qiskit.providers.models import GateConfig
@@ -709,3 +709,59 @@ def test_faulty_full_path_transpile_connected_cmap(self, opt_level):
         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)
+
+    def test_convert_to_target_control_flow(self):
+        backend = Fake27QPulseV1()
+        properties = backend.properties()
+        configuration = backend.configuration()
+        configuration.supported_instructions = [
+            "cx",
+            "id",
+            "delay",
+            "measure",
+            "reset",
+            "rz",
+            "sx",
+            "x",
+            "if_else",
+            "for_loop",
+            "switch_case",
+        ]
+        defaults = backend.defaults()
+        target = convert_to_target(configuration, properties, defaults)
+        self.assertTrue(target.instruction_supported("if_else", ()))
+        self.assertFalse(target.instruction_supported("while_loop", ()))
+        self.assertTrue(target.instruction_supported("for_loop", ()))
+        self.assertTrue(target.instruction_supported("switch_case", ()))
+
+    def test_convert_unrelated_supported_instructions(self):
+        backend = Fake27QPulseV1()
+        properties = backend.properties()
+        configuration = backend.configuration()
+        configuration.supported_instructions = [
+            "cx",
+            "id",
+            "delay",
+            "measure",
+            "reset",
+            "rz",
+            "sx",
+            "x",
+            "play",
+            "u2",
+            "u3",
+            "u1",
+            "shiftf",
+            "acquire",
+            "setf",
+            "if_else",
+            "for_loop",
+            "switch_case",
+        ]
+        defaults = backend.defaults()
+        target = convert_to_target(configuration, properties, defaults)
+        self.assertTrue(target.instruction_supported("if_else", ()))
+        self.assertFalse(target.instruction_supported("while_loop", ()))
+        self.assertTrue(target.instruction_supported("for_loop", ()))
+        self.assertTrue(target.instruction_supported("switch_case", ()))
+        self.assertFalse(target.instruction_supported("u3", (0,)))