Repository: Qiskit/qiskit (python)

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

Pull request:
Fix handling of control flow instructions in convert_to_target()
<!--
⚠️ 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 in the convert_to_target() function where it wasn't looking for control flow instructions in the proper location. Typically the control flow instructions are put in the supported_instructions field of the BackendConfiguration, but the convert_to_target() function was ignoring this field. This commit updates the function to check supported_instructions for the control flow instructions. It doesn't more broadly look at the supported_instructions field, because on other backends this field is used to list the supported pulse instructions which might have name conflicts with other instructions and cause issues building a target.

### Details and comments

Fixes #11872

Patch:
diff --git a/qiskit/primitives/containers/estimator_pub.py b/qiskit/primitives/containers/estimator_pub.py
index 1996e315f..fed278ae1 100644
--- a/qiskit/primitives/containers/estimator_pub.py
+++ b/qiskit/primitives/containers/estimator_pub.py
@@ -141,7 +141,7 @@ class EstimatorPub(ShapedMixin):
 
         if len(pub) > 2 and pub[2] is not None:
             values = pub[2]
-            if not isinstance(values, Mapping):
+            if not isinstance(values, (BindingsArray, Mapping)):
                 values = {tuple(circuit.parameters): values}
             parameter_values = BindingsArray.coerce(values)
         else:
diff --git a/qiskit/providers/backend_compat.py b/qiskit/providers/backend_compat.py
index 3d5465654..fcd82765e 100644
--- a/qiskit/providers/backend_compat.py
+++ b/qiskit/providers/backend_compat.py
@@ -21,7 +21,7 @@ from qiskit.providers.backend import BackendV1, BackendV2
 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()
diff --git a/releasenotes/notes/fix-control-flow-convert-to-target-ae838418a7ad2a20.yaml b/releasenotes/notes/fix-control-flow-convert-to-target-ae838418a7ad2a20.yaml
new file mode 100644
index 000000000..be29520fa
--- /dev/null
+++ b/releasenotes/notes/fix-control-flow-convert-to-target-ae838418a7ad2a20.yaml
@@ -0,0 +1,9 @@
+---
+fixes:
+  - |
+    Fixed an issue with the :func:`.convert_to_target` where the converter
+    would incorrectly ignore control flow instructions if they were specified
+    in the :attr:`.BackendConfiguration.supported_instructions` attribute which
+    is the typical location that control flow instructions are specified in a
+    :class:`.BackendConfiguration` object.
+    Fixed `#11872 <https://github.com/Qiskit/qiskit/issues/11872>`__.
diff --git a/releasenotes/notes/fix-estimator-pub-coerce-5d13700e15126421.yaml b/releasenotes/notes/fix-estimator-pub-coerce-5d13700e15126421.yaml
new file mode 100644
index 000000000..0c22a2a95
--- /dev/null
+++ b/releasenotes/notes/fix-estimator-pub-coerce-5d13700e15126421.yaml
@@ -0,0 +1,7 @@
+---
+
+fixes:
+  - |
+    Fixed a bug where `qiskit.primitives.containers.estimator_pub.EstimatorPub.coerce()`
+    improperly handles the case where the third value is a `BindingsArray` instance, giving
+    rise to a ``ValueError`` whenever it is attempted.
\ No newline at end of file


Test patch:
diff --git a/test/python/primitives/containers/test_estimator_pub.py b/test/python/primitives/containers/test_estimator_pub.py
index ecf82e334..4a1bbc5e2 100644
--- a/test/python/primitives/containers/test_estimator_pub.py
+++ b/test/python/primitives/containers/test_estimator_pub.py
@@ -182,6 +182,19 @@ class EstimatorPubTestCase(QiskitTestCase):
         pub2 = EstimatorPub.coerce(pub1, precision=precision)
         self.assertEqual(pub1, pub2)
 
+    def test_coerce_pub_with_exact_types(self):
+        """Test coercing an EstimatorPub"""
+        params = (Parameter("a"), Parameter("b"))
+        circuit = QuantumCircuit(2)
+        circuit.rx(params[0], 0)
+        circuit.ry(params[1], 1)
+        obs = ObservablesArray({"XY": 1})
+        params = BindingsArray(data={params: np.ones((10, 2))})
+        pub = EstimatorPub.coerce((circuit, obs, params))
+        self.assertIs(pub.circuit, circuit)
+        self.assertIs(pub.observables, obs)
+        self.assertIs(pub.parameter_values, params)
+
     @ddt.data(0.01, 0.02)
     def test_coerce_pub_without_shots(self, precision):
         """Test coercing an EstimatorPub"""
diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py
index f4f42e839..101e35acc 100644
--- a/test/python/providers/test_fake_backends.py
+++ b/test/python/providers/test_fake_backends.py
@@ -34,7 +34,7 @@ from qiskit.providers.fake_provider import (
     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 @@ class TestFakeBackends(QiskitTestCase):
         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,)))