Repository: PennyLaneAI/pennylane-qiskit

Issue:
Layout of transpiled circuit is not being applied to the operators
**Describe the bug**
The layout of the transpiled circuit is not being applied to the observables, leading to wrong results when it differs from the layout of the original circuit. This can be solved by adding

```python
compiled_observables = [op.apply_layout(compiled_circuits[0].layout) for op in pauli_observables]
```

 as _line 657_ of "_execute_estimator" in **qiskit_device.py**, and modifying _line 663_ to be:

```python
circ_and_obs = [(compiled_circuits[0], compiled_observables)]
```

**To Reproduce**
The result of the following minimal circuit should be close to _1_, but instead the circuit execution gives close to _0_. (I have used _optimization_level=2_ to allow the layout to change in transpilation. Using _optimization_level=0_ produce the right result for this circuit as the layout is not changed.)

```python
from qiskit_ibm_runtime.fake_provider import FakeKyoto
backend = FakeKyoto()

dev = qml.device("qiskit.remote", wires=127, backend=backend, optimization_level=2)

@qml.qnode(dev)
def circuit():
    qml.Hadamard(0)
    return qml.expval(qml.PauliX(0))

circuit()
```

**Expected behavior**
Layout of transpiled circuit should be applied to the operators.

**Screenshots**
I have added some prints as reference:
![imagen](https://github.com/user-attachments/assets/35d2e98c-7a5b-4649-b82b-d6d5086d2f5d)

It can be seen that the layout has changed in transpilation
![imagen](https://github.com/user-attachments/assets/0f256cf7-1fa9-421e-859d-7e16198b9328)

The Pauli observable has the layout of the original circuit, while the compiled observable has the correct Layout and gives an expectation value close to _1_ (the correct result).
![imagen](https://github.com/user-attachments/assets/7bfc62f6-3346-41c2-9df4-f34361b212bd)


**Version (qml.about()):**
  Name: PennyLane
  Version: 0.39.0
  Platform info:           Linux-6.11.0-13-generic-x86_64-with-glibc2.40
  Python version:          3.10.0
  Numpy version:           2.0.2
  Scipy version:           1.15.0
  Installed devices:
  - default.clifford (PennyLane-0.39.0)
  - default.gaussian (PennyLane-0.39.0)
  - default.mixed (PennyLane-0.39.0)
  - default.qubit (PennyLane-0.39.0)
  - default.qutrit (PennyLane-0.39.0)
  - default.qutrit.mixed (PennyLane-0.39.0)
  - default.tensor (PennyLane-0.39.0)
  - null.qubit (PennyLane-0.39.0)
  - reference.qubit (PennyLane-0.39.0)
  - qiskit.aer (PennyLane-qiskit-0.39.1)
  - qiskit.basicaer (PennyLane-qiskit-0.39.1)
  - qiskit.basicsim (PennyLane-qiskit-0.39.1)
  - qiskit.remote (PennyLane-qiskit-0.39.1)
  - lightning.qubit (PennyLane_Lightning-0.39.0)

Discussion:
@CatalinaAlbornoz: Thank you for the very clear description of this bug @felipelewyee !!

I don't know if we'll have the time to fix this before the next release (which will likely be next week) but we'll do our best to fix it very soon and keep you updated.

Thanks again.@CatalinaAlbornoz: Hi @felipelewyee,

Thanks again for reporting this bug. Our team has reviewed it and they liked your suggestion for the fix. Would you like to open a [Pull Request](https://github.com/PennyLaneAI/pennylane-qiskit/pulls) (PR) for the fix? Make sure the PR is targeting master. If this is your first PR let us know and we can guide you through the process. On the other hand, if you prefer that we make the PR (instead of opening it yourself) we can do it. Just let us know what you prefer.@felipelewyee: Hi, thanks for checking my report. I am opening the PR in a moment.@felipelewyee: I have done the [PR](https://github.com/PennyLaneAI/pennylane-qiskit/pull/613). Let me know if something is needed, and thanks in advance. 

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d811817..e8db7bb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,9 @@
 
 ### Bug fixes 🐛
 
+* The layout of the circuit is now applied to the observables when it is changed on transpilation.
+  [(#613)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/613)
+
 ### Contributors ✍️
 
 This release contains contributions from (in alphabetical order):
@@ -25,7 +28,7 @@ This release contains contributions from (in alphabetical order):
   [(#601)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/601)
 
 ### Bug fixes 🐛  
-
+  
 * Fixed premature session closure when using `qiskit_session`.  
   [(#608)](https://github.com/PennyLaneAI/pennylane-qiskit/pull/608)
 
diff --git a/pennylane_qiskit/qiskit_device.py b/pennylane_qiskit/qiskit_device.py
index f4104ed..75466ed 100644
--- a/pennylane_qiskit/qiskit_device.py
+++ b/pennylane_qiskit/qiskit_device.py
@@ -654,12 +654,15 @@ class QiskitDevice(Device):
 
         pauli_observables = [mp_to_pauli(mp, self.num_wires) for mp in circuit.measurements]
         compiled_circuits = self.compile_circuits(qcirc)
+        compiled_observables = [
+            op.apply_layout(compiled_circuits[0].layout) for op in pauli_observables
+        ]
         estimator.options.update(**self._kwargs)
         # split into one call per measurement
         # could technically be more efficient if there are some observables where we ask
         # for expectation value and variance on the same observable, but spending time on
         # that right now feels excessive
-        circ_and_obs = [(compiled_circuits[0], pauli_observables)]
+        circ_and_obs = [(compiled_circuits[0], compiled_observables)]
         result = estimator.run(
             circ_and_obs,
             precision=np.sqrt(1 / circuit.shots.total_shots) if circuit.shots else None,


Test patch:
diff --git a/tests/test_qiskit_device.py b/tests/test_qiskit_device.py
index 2329213..68060b5 100644
--- a/tests/test_qiskit_device.py
+++ b/tests/test_qiskit_device.py
@@ -217,6 +217,29 @@ class TestAerBackendOptions:
         assert dev2.backend.options.get("noise_model") is None
 
 
+class TestExecutionAtOptimizationLevels:
+    """Test that a known circuit returns the expected result at all
+    optimization levels."""
+
+    @pytest.mark.parametrize("optimization_level", [0, 1, 2, 3])
+    def test_layout_at_optimization_levels(self, optimization_level):
+        """Test circuit to detect layout problems in the circuit and observables."""
+
+        fakebackend = FakeManilaV2()
+        dev = qml.device(
+            "qiskit.remote", wires=5, backend=fakebackend, optimization_level=optimization_level
+        )
+
+        @qml.qnode(dev)
+        def circuit():
+            qml.Hadamard(2)
+            qml.CNOT([1, 4])
+            return qml.expval(qml.PauliX(2))
+
+        res = circuit()
+        assert np.allclose(res, 1.0, atol=0.2)
+
+
 @pytest.mark.parametrize("shots", [None])
 class TestBatchExecution:
     """Tests for the batch_execute method."""