Repository: Qiskit/qiskit

Issue:
VF2PostLayout corrupts final_layout output
### Environment

- **Qiskit Terra version**: 0.24.1 (but it also should affect 0.24.2 and main)
- **Python version**:  3.11
- **Operating system**:  Linux


### What is happening?

When running `transpile()` if the `VF2PostLayout` pass finds a better letter and re-applies the initial layout the final layout is not adjusted to correct for that change in initial position. This previously was not an issue because the final layout was an internal implementation detail (primarily used only for sabre layout) and we were done using it after layout in the transpiler. But now we're returning the final layout as part of the public interface in a QuantumCircuit. 

### How can we reproduce the issue?

```
from qiskit import QuantumCircuit, transpile
from qiskit.providers.fake_provider import FakeVigo
from qiskit_aer import AerSimulator

backend = AerSimulator.from_backend(FakeVigo())
qubits = 3
qc = QuantumCircuit(qubits)
for i in range(5):
    qc.cx(i % qubits, int(i + qubits / 2) % qubits)

tqc = transpile(qc, backend=backend, callback=callback)
print(tqc)
print(tqc.layout)
```

### What should happen?

In that example if `VF2PostLayout` runs the final layout will potentially point to incorrect qubits. One example incorrect output is:

```
ancilla_0 -> 0 ───────────────────────────────────────────────────────
                    ┌───┐     ┌───┐┌───┐     ┌───┐     ┌───┐┌───┐     
      q_0 -> 1 ──■──┤ X ├──■──┤ X ├┤ X ├──■──┤ X ├──■──┤ X ├┤ X ├──■──
               ┌─┴─┐└─┬─┘  │  └─┬─┘└─┬─┘  │  └─┬─┘┌─┴─┐└─┬─┘└─┬─┘┌─┴─┐
      q_1 -> 2 ┤ X ├──┼────┼────┼────■────┼────■──┤ X ├──■────┼──┤ X ├
               └───┘  │  ┌─┴─┐  │       ┌─┴─┐     └───┘       │  └───┘
      q_2 -> 3 ───────■──┤ X ├──■───────┤ X ├─────────────────■───────
                         └───┘          └───┘                         
ancilla_1 -> 4 ───────────────────────────────────────────────────────
```
```
TranspileLayout(initial_layout=Layout({
1: Qubit(QuantumRegister(3, 'q'), 0),
2: Qubit(QuantumRegister(3, 'q'), 1),
3: Qubit(QuantumRegister(3, 'q'), 2),
0: Qubit(QuantumRegister(2, 'ancilla'), 0),
4: Qubit(QuantumRegister(2, 'ancilla'), 1)
}), input_qubit_mapping={Qubit(QuantumRegister(3, 'q'), 0): 0, Qubit(QuantumRegister(3, 'q'), 1): 1, Qubit(QuantumRegister(3, 'q'), 2): 2, Qubit(QuantumRegister(2, 'ancilla'), 0): 3, Qubit(QuantumRegister(2, 'ancilla'), 1): 4}, final_layout=Layout({
0: Qubit(QuantumRegister(5, 'q'), 0),
3: Qubit(QuantumRegister(5, 'q'), 1),
2: Qubit(QuantumRegister(5, 'q'), 2),
4: Qubit(QuantumRegister(5, 'q'), 3),
1: Qubit(QuantumRegister(5, 'q'), 4)
}))
```

In this case the final layout is incorrect because it's not taking into account that vf2postlayout changed the initial layout. Instead it should be:

```
Layout({
0: Qubit(QuantumRegister(5, 'q'), 0),
3: Qubit(QuantumRegister(5, 'q'), 1),
1: Qubit(QuantumRegister(5, 'q'), 2),
2: Qubit(QuantumRegister(5, 'q'), 3),
4: Qubit(QuantumRegister(5, 'q'), 4)
}))
```

(I was running without seeds set so it was random failure based on sabre's output)

### Any suggestions?

We should update the `ApplyLayout` pass to also update the `final_layout` in the property set if it's present, just like it does for the `layout` field.


Discussion:

Resolving patch:
diff --git a/qiskit/transpiler/passes/layout/apply_layout.py b/qiskit/transpiler/passes/layout/apply_layout.py
--- a/qiskit/transpiler/passes/layout/apply_layout.py
+++ b/qiskit/transpiler/passes/layout/apply_layout.py
@@ -83,9 +83,11 @@ def run(self, dag):
             full_layout = Layout()
             old_phys_to_virtual = layout.get_physical_bits()
             new_virtual_to_physical = post_layout.get_virtual_bits()
+            phys_map = list(range(len(new_dag.qubits)))
             for new_virt, new_phys in new_virtual_to_physical.items():
                 old_phys = dag.find_bit(new_virt).index
                 old_virt = old_phys_to_virtual[old_phys]
+                phys_map[old_phys] = new_phys
                 full_layout.add(old_virt, new_phys)
             for reg in layout.get_registers():
                 full_layout.add_register(reg)
@@ -94,6 +96,13 @@ def run(self, dag):
                 qargs = [q[new_virtual_to_physical[qarg]] for qarg in node.qargs]
                 new_dag.apply_operation_back(node.op, qargs, node.cargs)
             self.property_set["layout"] = full_layout
+            if (final_layout := self.property_set["final_layout"]) is not None:
+                final_layout_mapping = {
+                    new_dag.qubits[phys_map[dag.find_bit(old_virt).index]]: phys_map[old_phys]
+                    for old_virt, old_phys in final_layout.get_virtual_bits().items()
+                }
+                out_layout = Layout(final_layout_mapping)
+                self.property_set["final_layout"] = out_layout
         new_dag._global_phase = dag._global_phase
 
         return new_dag


Test patch:
diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py
--- a/test/python/compiler/test_transpiler.py
+++ b/test/python/compiler/test_transpiler.py
@@ -67,6 +67,7 @@
     FakeNairobiV2,
     FakeRueschlikon,
     FakeSherbrooke,
+    FakeVigo,
 )
 from qiskit.providers.options import Options
 from qiskit.pulse import InstructionScheduleMap
@@ -75,7 +76,7 @@
 from qiskit.tools import parallel
 from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass
 from qiskit.transpiler.exceptions import TranspilerError
-from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection
+from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection, VF2PostLayout
 from qiskit.transpiler.passmanager_config import PassManagerConfig
 from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager, level_0_pass_manager
 from qiskit.transpiler.target import InstructionProperties, Target
@@ -1857,6 +1858,45 @@ def test_transpile_target_no_measurement_error(self, opt_level):
         res = transpile(qc, target=target, optimization_level=opt_level)
         self.assertEqual(qc, res)
 
+    def test_transpile_final_layout_updated_with_post_layout(self):
+        """Test that the final layout is correctly set when vf2postlayout runs.
+
+        Reproduce from #10457
+        """
+
+        def _get_index_layout(transpiled_circuit: QuantumCircuit, num_source_qubits: int):
+            """Return the index layout of a transpiled circuit"""
+            layout = transpiled_circuit.layout
+            if layout is None:
+                return list(range(num_source_qubits))
+
+            pos_to_virt = {v: k for k, v in layout.input_qubit_mapping.items()}
+            qubit_indices = []
+            for index in range(num_source_qubits):
+                qubit_idx = layout.initial_layout[pos_to_virt[index]]
+                if layout.final_layout is not None:
+                    qubit_idx = layout.final_layout[transpiled_circuit.qubits[qubit_idx]]
+                qubit_indices.append(qubit_idx)
+            return qubit_indices
+
+        vf2_post_layout_called = False
+
+        def callback(**kwargs):
+            nonlocal vf2_post_layout_called
+            if isinstance(kwargs["pass_"], VF2PostLayout):
+                vf2_post_layout_called = True
+                self.assertIsNotNone(kwargs["property_set"]["post_layout"])
+
+        backend = FakeVigo()
+        qubits = 3
+        qc = QuantumCircuit(qubits)
+        for i in range(5):
+            qc.cx(i % qubits, int(i + qubits / 2) % qubits)
+
+        tqc = transpile(qc, backend=backend, seed_transpiler=4242, callback=callback)
+        self.assertTrue(vf2_post_layout_called)
+        self.assertEqual([3, 2, 1], _get_index_layout(tqc, qubits))
+
 
 class StreamHandlerRaiseException(StreamHandler):
     """Handler class that will raise an exception on formatting errors."""
diff --git a/test/python/transpiler/test_apply_layout.py b/test/python/transpiler/test_apply_layout.py
--- a/test/python/transpiler/test_apply_layout.py
+++ b/test/python/transpiler/test_apply_layout.py
@@ -18,8 +18,11 @@
 from qiskit.converters import circuit_to_dag
 from qiskit.test import QiskitTestCase
 from qiskit.transpiler.layout import Layout
-from qiskit.transpiler.passes import ApplyLayout
+from qiskit.transpiler.passes import ApplyLayout, SetLayout
 from qiskit.transpiler.exceptions import TranspilerError
+from qiskit.transpiler.preset_passmanagers import common
+from qiskit.providers.fake_provider import FakeVigoV2
+from qiskit.transpiler import PassManager
 
 
 class TestApplyLayout(QiskitTestCase):
@@ -115,6 +118,54 @@ def test_circuit_with_swap_gate(self):
 
         self.assertEqual(circuit_to_dag(expected), after)
 
+    def test_final_layout_is_updated(self):
+        """Test that if vf2postlayout runs that we've updated the final layout."""
+        qubits = 3
+        qc = QuantumCircuit(qubits)
+        for i in range(5):
+            qc.cx(i % qubits, int(i + qubits / 2) % qubits)
+        initial_pm = PassManager([SetLayout([1, 3, 4])])
+        cmap = FakeVigoV2().coupling_map
+        initial_pm += common.generate_embed_passmanager(cmap)
+        first_layout_circ = initial_pm.run(qc)
+        out_pass = ApplyLayout()
+        out_pass.property_set["layout"] = first_layout_circ.layout.initial_layout
+        out_pass.property_set[
+            "original_qubit_indices"
+        ] = first_layout_circ.layout.input_qubit_mapping
+        out_pass.property_set["final_layout"] = Layout(
+            {
+                first_layout_circ.qubits[0]: 0,
+                first_layout_circ.qubits[1]: 3,
+                first_layout_circ.qubits[2]: 2,
+                first_layout_circ.qubits[3]: 4,
+                first_layout_circ.qubits[4]: 1,
+            }
+        )
+        # Set a post layout like vf2postlayout would:
+        out_pass.property_set["post_layout"] = Layout(
+            {
+                first_layout_circ.qubits[0]: 0,
+                first_layout_circ.qubits[2]: 4,
+                first_layout_circ.qubits[1]: 2,
+                first_layout_circ.qubits[3]: 1,
+                first_layout_circ.qubits[4]: 3,
+            }
+        )
+        out_pass(first_layout_circ)
+        self.assertEqual(
+            out_pass.property_set["final_layout"],
+            Layout(
+                {
+                    first_layout_circ.qubits[0]: 0,
+                    first_layout_circ.qubits[2]: 1,
+                    first_layout_circ.qubits[4]: 4,
+                    first_layout_circ.qubits[1]: 3,
+                    first_layout_circ.qubits[3]: 2,
+                }
+            ),
+        )
+
 
 if __name__ == "__main__":
     unittest.main()