Repository: Qiskit/qiskit

Issue:
Expected Output of VF2PostLayout 
### Environment

- **Qiskit Terra version**:  '0.45.0.dev0+a1a9cc3'
- **Python version**: 3.11.5
- **Operating system**: macOs Sonoma 14.0


### What is happening?

Running a VF2PostLayout on a quantum circuit with an initial layout that is optimal, i.e. already has the lowest cost for the given target quantum computer, returns `VF2PostLayoutStopReason.NO_SOLUTION_FOUND` and None as `post_layout` in the property set of the pass.

### How can we reproduce the issue?

```
def linear_target(n, trivial_layout_is_best):
    cm = CouplingMap.from_line(n)
    tqc = Target(num_qubits=n)
    if trivial_layout_is_best:
        # the first qubits of the device have the lowest errors 
        cx_props = {(v, u): InstructionProperties(error=10 ** (n-v)) for v, u in cm.get_edges()}
    else:
        # the last qubits of the device have the lowest errors
        cx_props = {(v, u): InstructionProperties(error=10 ** (-v)) for v, u in cm.get_edges()}
    tqc.add_instruction(CXGate(), cx_props)
    return tqc

def run_vf2post(target, qc):
    pm = PassManager()
    pm.append(VF2PostLayout(target=target))
    pm.run(qc)
    return pm

n = 4
qc = QuantumCircuit(n)
qc.cx(0, 1)
qc.cx(1,2)

pm0 = run_vf2post(linear_target(n, trivial_layout_is_best=False), qc)
print(pm0.property_set['VF2PostLayout_stop_reason'])
print(pm0.property_set['post_layout'])

pm1 = run_vf2post(linear_target(n, trivial_layout_is_best=True), qc)
print(pm1.property_set['VF2PostLayout_stop_reason'])
print(pm1.property_set['post_layout'])
```
* Output:

> VF2PostLayoutStopReason.SOLUTION_FOUND
Layout({
2: Qubit(QuantumRegister(4, 'q'), 1),
1: Qubit(QuantumRegister(4, 'q'), 2),
3: Qubit(QuantumRegister(4, 'q'), 0)
})
VF2PostLayoutStopReason.NO_SOLUTION_FOUND
None

### What should happen?

VF2PostLayout should always populate the `post_layout` field, if the pass can determine a layout that does not require the insertion of swap gates. Alternatively, VF2PostLayout should indicate that there is such a layout but that it already equals the initial layout of the input quantum circuit.

A typical use case where this issue becomes relevant is an user that determines a quantum circuit compliant with the coupling map of the target quantum computer outside of Qiskit and only wants to adapt their mapped quantum circuit to the current calibration data before submitting their quantum circuit to the queue. VF2PostLayout is presented as the prime solution for this task in arXiv:2209.15512 and it works really well but caused some confusion in the described case. 

### Any suggestions?

I will shortly submit a PR that extends the VF2PostLayout with a `NO_BETTER_SOLUTION_FOUND` indication in the described corner case.


Discussion:

Resolving patch:
diff --git a/qiskit/transpiler/passes/layout/vf2_post_layout.py b/qiskit/transpiler/passes/layout/vf2_post_layout.py
--- a/qiskit/transpiler/passes/layout/vf2_post_layout.py
+++ b/qiskit/transpiler/passes/layout/vf2_post_layout.py
@@ -35,6 +35,7 @@ class VF2PostLayoutStopReason(Enum):
     """Stop reasons for VF2PostLayout pass."""
 
     SOLUTION_FOUND = "solution found"
+    NO_BETTER_SOLUTION_FOUND = "no better solution found"
     NO_SOLUTION_FOUND = "nonexistent solution"
     MORE_THAN_2Q = ">2q gates in basis"
 
@@ -51,7 +52,7 @@ def _target_match(node_a, node_b):
 
 
 class VF2PostLayout(AnalysisPass):
-    """A pass for choosing a Layout after transpilation of a circuit onto a
+    """A pass for improving an existing Layout after transpilation of a circuit onto a
     Coupling graph, as a subgraph isomorphism problem, solved by VF2++.
 
     Unlike the :class:`~.VF2Layout` transpiler pass which is designed to find an
@@ -66,15 +67,16 @@ class VF2PostLayout(AnalysisPass):
 
     If a solution is found that means there is a lower error layout available for the
     circuit. If a solution is found the layout will be set in the property set as
-    ``property_set['post_layout']``. However, if no solution is found, no
+    ``property_set['post_layout']``. However, if no solution or no better solution is found, no
     ``property_set['post_layout']`` is set. The stopping reason is
     set in ``property_set['VF2PostLayout_stop_reason']`` in all the cases and will be
     one of the values enumerated in ``VF2PostLayoutStopReason`` which has the
     following values:
 
         * ``"solution found"``: If a solution was found.
+        * ``"no better solution found"``: If the initial layout of the circuit is the best solution.
         * ``"nonexistent solution"``: If no solution was found.
-        * ``">2q gates in basis"``: If VF2PostLayout can't work with basis
+        * ``">2q gates in basis"``: If VF2PostLayout can't work with the basis of the circuit.
 
     By default this pass will construct a heuristic scoring map based on the
     the error rates in the provided ``target`` (or ``properties`` if ``target``
@@ -278,6 +280,8 @@ def run(self, dag):
                     self.strict_direction,
                     run_in_parallel,
                 )
+            chosen_layout = initial_layout
+            stop_reason = VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND
         # Circuit not in basis so we have nothing to compare against return here
         except KeyError:
             self.property_set[
@@ -292,7 +296,6 @@ def run(self, dag):
         for mapping in mappings:
             trials += 1
             logger.debug("Running trial: %s", trials)
-            stop_reason = VF2PostLayoutStopReason.SOLUTION_FOUND
             layout_mapping = {im_i: cm_nodes[cm_i] for cm_i, im_i in mapping.items()}
             if self.strict_direction:
                 layout = Layout(
@@ -325,6 +328,7 @@ def run(self, dag):
                 )
                 chosen_layout = layout
                 chosen_layout_score = layout_score
+                stop_reason = VF2PostLayoutStopReason.SOLUTION_FOUND
 
             if self.max_trials and trials >= self.max_trials:
                 logger.debug("Trial %s is >= configured max trials %s", trials, self.max_trials)
@@ -338,9 +342,7 @@ def run(self, dag):
                     self.time_limit,
                 )
                 break
-        if chosen_layout is None:
-            stop_reason = VF2PostLayoutStopReason.NO_SOLUTION_FOUND
-        else:
+        if stop_reason == VF2PostLayoutStopReason.SOLUTION_FOUND:
             chosen_layout = vf2_utils.map_free_qubits(
                 free_nodes,
                 chosen_layout,
@@ -364,7 +366,10 @@ def run(self, dag):
                                 chosen_layout.add(bit, i)
                                 break
             self.property_set["post_layout"] = chosen_layout
-
+        else:
+            if chosen_layout is None:
+                stop_reason = VF2PostLayoutStopReason.NO_SOLUTION_FOUND
+            # else the initial layout is optimal -> don't set post_layout, return 'no better solution'
         self.property_set["VF2PostLayout_stop_reason"] = stop_reason
 
     def _score_layout(self, layout, bit_map, reverse_bit_map, im_graph):


Test patch:
diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py
--- a/test/python/transpiler/test_vf2_post_layout.py
+++ b/test/python/transpiler/test_vf2_post_layout.py
@@ -108,7 +108,7 @@ def test_empty_circuit(self):
         vf2_pass.run(circuit_to_dag(qc))
         self.assertEqual(
             vf2_pass.property_set["VF2PostLayout_stop_reason"],
-            VF2PostLayoutStopReason.NO_SOLUTION_FOUND,
+            VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND,
         )
 
     def test_empty_circuit_v2(self):
@@ -119,7 +119,7 @@ def test_empty_circuit_v2(self):
         vf2_pass.run(circuit_to_dag(qc))
         self.assertEqual(
             vf2_pass.property_set["VF2PostLayout_stop_reason"],
-            VF2PostLayoutStopReason.NO_SOLUTION_FOUND,
+            VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND,
         )
 
     def test_skip_3q_circuit(self):
@@ -389,6 +389,46 @@ def test_target_some_error(self):
         # No layout selected because nothing will beat initial layout
         self.assertNotIn("post_layout", vf2_pass.property_set)
 
+    def test_trivial_layout_is_best(self):
+        """Test that vf2postlayout reports no better solution if the trivial layout is the best layout"""
+        n_qubits = 4
+        trivial_target = Target()
+        trivial_target.add_instruction(
+            CXGate(), {(i, i + 1): InstructionProperties(error=0.001) for i in range(n_qubits - 1)}
+        )
+
+        circuit = QuantumCircuit(n_qubits)
+        circuit.cx(0, 1)
+        circuit.cx(1, 2)
+
+        vf2_pass = VF2PostLayout(target=trivial_target, seed=self.seed, strict_direction=False)
+        dag = circuit_to_dag(circuit)
+        vf2_pass.run(dag)
+        self.assertEqual(
+            vf2_pass.property_set["VF2PostLayout_stop_reason"],
+            VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND,
+        )
+
+    def test_last_qubits_best(self):
+        """Test that vf2postlayout determines the best layout when the last qubits have least error"""
+        n_qubits = 4
+        target_last_qubits_best = Target()
+        target_last_qubits_best.add_instruction(
+            CXGate(),
+            {(i, i + 1): InstructionProperties(error=10**-i) for i in range(n_qubits - 1)},
+        )
+
+        circuit = QuantumCircuit(n_qubits)
+        circuit.cx(0, 1)
+        circuit.cx(1, 2)
+
+        vf2_pass = VF2PostLayout(
+            target=target_last_qubits_best, seed=self.seed, strict_direction=False
+        )
+        dag = circuit_to_dag(circuit)
+        vf2_pass.run(dag)
+        self.assertLayout(dag, target_last_qubits_best.build_coupling_map(), vf2_pass.property_set)
+
 
 class TestVF2PostLayoutScoring(QiskitTestCase):
     """Test scoring heuristic function for VF2PostLayout."""
@@ -480,7 +520,7 @@ def test_empty_circuit(self):
         vf2_pass.run(circuit_to_dag(qc))
         self.assertEqual(
             vf2_pass.property_set["VF2PostLayout_stop_reason"],
-            VF2PostLayoutStopReason.NO_SOLUTION_FOUND,
+            VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND,
         )
 
     def test_empty_circuit_v2(self):
@@ -491,7 +531,7 @@ def test_empty_circuit_v2(self):
         vf2_pass.run(circuit_to_dag(qc))
         self.assertEqual(
             vf2_pass.property_set["VF2PostLayout_stop_reason"],
-            VF2PostLayoutStopReason.NO_SOLUTION_FOUND,
+            VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND,
         )
 
     def test_skip_3q_circuit(self):