Repository: Qiskit/qiskit

Issue:
VF2 layout fails with cryptic error message when instruction properties are not available
### Environment

- **Qiskit Terra version**: 0.22.0
- **Python version**: 3.9.12
- **Operating system**: Windows 10


### What is happening?

```
Traceback (most recent call last):
  File ".\src\playground.py", line 87, in <module>
    transpile(qc, target=target, optimization_level=2)
  File ".\lib\site-packages\qiskit\compiler\transpiler.py", line 382, in transpile
    _serial_transpile_circuit(
  File ".\lib\site-packages\qiskit\compiler\transpiler.py", line 475, in _serial_transpile_circuit
    result = pass_manager.run(circuit, callback=callback, output_name=output_name)
  File ".\lib\site-packages\qiskit\transpiler\passmanager.py", line 528, in run
    return super().run(circuits, output_name, callback)
  File ".\lib\site-packages\qiskit\transpiler\passmanager.py", line 228, in run
    return self._run_single_circuit(circuits, output_name, callback)
  File ".\lib\site-packages\qiskit\transpiler\passmanager.py", line 283, in _run_single_circuit
    result = running_passmanager.run(circuit, output_name=output_name, callback=callback)
  File ".\lib\site-packages\qiskit\transpiler\runningpassmanager.py", line 125, in run
    dag = self._do_pass(pass_, dag, passset.options)
  File ".\lib\site-packages\qiskit\transpiler\runningpassmanager.py", line 172, in _do_pass
    dag = self._run_this_pass(pass_, dag)
  File ".\lib\site-packages\qiskit\transpiler\runningpassmanager.py", line 226, in _run_this_pass
    pass_.run(FencedDAGCircuit(dag))
  File ".\lib\site-packages\qiskit\transpiler\passes\layout\vf2_layout.py", line 157, in run
    layout_score = vf2_utils.score_layout(
  File ".\lib\site-packages\qiskit\transpiler\passes\layout\vf2_utils.py", line 87, in score_layout
    fidelity *= (1 - avg_error_map[(bits[bit],)]) ** gate_count
KeyError: (5,)
```

### How can we reproduce the issue?

The below code will reproduce the error pasted above
```
from qiskit import QuantumCircuit
from qiskit.circuit.library import CXGate
from qiskit.compiler import transpile
from qiskit.transpiler import Target

n_qubits = 15
target = Target()
target.add_instruction(CXGate(), {(i, i+1): None for i in range(n_qubits - 1)})

qc = QuantumCircuit(2)
qc.cx(0, 1)

transpile(qc, target=target, optimization_level=2)
```
My understanding is, that the VF2 layout method tries to score the layout based on average error rates, but fails because instructions do not have properties hence no average error rate is available.

Note that the properties for `CXGate` instructions are specified as `None`, which is a valid value and indicates that any properties are not available. The documentation of `qiskit.transpiler.Target.add_instruction` says `Properties are optional for any instruction implementation, if there are no InstructionProperties available for the backend the value can be None.`.

Note, that `optimization_level=2` is important here. The error does not happen with lower optimization levels.

### What should happen?

I am not well aware of the full philosophy behind the design of VF2 layout method, but I think at least one of the following two options should be happening instead of the error pasted above:

- If layout scoring based on average error rates is a mandatory component of VF2 layout method, then it should raise a better explanatory error saying something like "Hey, you are trying to use a layout method that mandatorily requires average errors, but you do not have any instruction properties in your target, so please use other layout methods, e.g. 'dense', 'sabre', etc.".
- Else, the transpilation should not fail with any error and should produce properly transpiled result.

### Any suggestions?

_No response_


Discussion:
Yeah, this is a bug `VF2Layout`, should operate even if there are no error rates present in the target. In those cases it just shouldn't do the heuristic scoring and just pick the first isomorphic subgraph it finds.

Just for posterity the `VF2PostLayout` pass is different and does require scores, but in that case it still shouldn't raise it should just exit if no error rates are present.

Resolving patch:
diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py
--- a/qiskit/transpiler/passes/layout/vf2_layout.py
+++ b/qiskit/transpiler/passes/layout/vf2_layout.py
@@ -154,6 +154,11 @@ def run(self, dag):
             if len(cm_graph) == len(im_graph):
                 chosen_layout = layout
                 break
+            # If there is no error map avilable we can just skip the scoring stage as there
+            # is nothing to score with, so any match is the best we can find.
+            if not self.avg_error_map:
+                chosen_layout = layout
+                break
             layout_score = vf2_utils.score_layout(
                 self.avg_error_map,
                 layout,
@@ -162,6 +167,12 @@ def run(self, dag):
                 im_graph,
                 self.strict_direction,
             )
+            # If the layout score is 0 we can't do any better and we'll just
+            # waste time finding additional mappings that will at best match
+            # the performance, so exit early in this case
+            if layout_score == 0.0:
+                chosen_layout = layout
+                break
             logger.debug("Trial %s has score %s", trials, layout_score)
             if chosen_layout is None:
                 chosen_layout = layout
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
@@ -13,6 +13,7 @@
 """VF2PostLayout pass to find a layout after transpile using subgraph isomorphism"""
 from enum import Enum
 import logging
+import inspect
 import time
 
 from retworkx import PyDiGraph, vf2_mapping, PyGraph
@@ -131,6 +132,7 @@ def run(self, dag):
             self.avg_error_map = vf2_utils.build_average_error_map(
                 self.target, self.properties, self.coupling_map
             )
+
         result = vf2_utils.build_interaction_graph(dag, self.strict_direction)
         if result is None:
             self.property_set["VF2PostLayout_stop_reason"] = VF2PostLayoutStopReason.MORE_THAN_2Q
@@ -138,21 +140,53 @@ def run(self, dag):
         im_graph, im_graph_node_map, reverse_im_graph_node_map = result
 
         if self.target is not None:
+            # If qargs is None then target is global and ideal so no
+            # scoring is needed
+            if self.target.qargs is None:
+                return
             if self.strict_direction:
                 cm_graph = PyDiGraph(multigraph=False)
             else:
                 cm_graph = PyGraph(multigraph=False)
-            cm_graph.add_nodes_from(
-                [self.target.operation_names_for_qargs((i,)) for i in range(self.target.num_qubits)]
-            )
+            # If None is present in qargs there are globally defined ideal operations
+            # we should add these to all entries based on the number of qubits so we
+            # treat that as a valid operation even if there is no scoring for the
+            # strict direction case
+            global_ops = None
+            if None in self.target.qargs:
+                global_ops = {1: [], 2: []}
+                for op in self.target.operation_names_for_qargs(None):
+                    operation = self.target.operation_for_name(op)
+                    # If operation is a class this is a variable width ideal instruction
+                    # so we treat it as available on both 1 and 2 qubits
+                    if inspect.isclass(operation):
+                        global_ops[1].append(op)
+                        global_ops[2].append(op)
+                    else:
+                        num_qubits = operation.num_qubits
+                        if num_qubits in global_ops:
+                            global_ops[num_qubits].append(op)
+            op_names = []
+            for i in range(self.target.num_qubits):
+                entry = set()
+                try:
+                    entry = set(self.target.operation_names_for_qargs((i,)))
+                except KeyError:
+                    pass
+                if global_ops is not None:
+                    entry.update(global_ops[1])
+                op_names.append(entry)
+
+                cm_graph.add_nodes_from(op_names)
             for qargs in self.target.qargs:
                 len_args = len(qargs)
                 # If qargs == 1 we already populated it and if qargs > 2 there are no instructions
                 # using those in the circuit because we'd have already returned by this point
                 if len_args == 2:
-                    cm_graph.add_edge(
-                        qargs[0], qargs[1], self.target.operation_names_for_qargs(qargs)
-                    )
+                    ops = set(self.target.operation_names_for_qargs(qargs))
+                    if global_ops is not None:
+                        ops.update(global_ops[2])
+                    cm_graph.add_edge(qargs[0], qargs[1], ops)
             cm_nodes = list(cm_graph.node_indexes())
         else:
             cm_graph, cm_nodes = vf2_utils.shuffle_coupling_graph(
diff --git a/qiskit/transpiler/passes/layout/vf2_utils.py b/qiskit/transpiler/passes/layout/vf2_utils.py
--- a/qiskit/transpiler/passes/layout/vf2_utils.py
+++ b/qiskit/transpiler/passes/layout/vf2_utils.py
@@ -84,13 +84,16 @@ def score_layout(avg_error_map, layout, bit_map, reverse_bit_map, im_graph, stri
     fidelity = 1
     for bit, node_index in bit_map.items():
         gate_count = sum(im_graph[node_index].values())
-        fidelity *= (1 - avg_error_map[(bits[bit],)]) ** gate_count
+        error_rate = avg_error_map.get((bits[bit],))
+        if error_rate is not None:
+            fidelity *= (1 - avg_error_map[(bits[bit],)]) ** gate_count
     for edge in im_graph.edge_index_map().values():
         gate_count = sum(edge[2].values())
         qargs = (bits[reverse_bit_map[edge[0]]], bits[reverse_bit_map[edge[1]]])
         if not strict_direction and qargs not in avg_error_map:
             qargs = (qargs[1], qargs[0])
-        fidelity *= (1 - avg_error_map[qargs]) ** gate_count
+        if qargs in avg_error_map:
+            fidelity *= (1 - avg_error_map[qargs]) ** gate_count
     return 1 - fidelity
 
 


Test patch:
diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py
--- a/test/python/transpiler/test_vf2_layout.py
+++ b/test/python/transpiler/test_vf2_layout.py
@@ -32,8 +32,9 @@
     FakeYorktown,
     FakeGuadalupeV2,
 )
-from qiskit.circuit.library import GraphState, CXGate
+from qiskit.circuit.library import GraphState, CXGate, XGate
 from qiskit.transpiler import PassManager
+from qiskit.transpiler.target import InstructionProperties
 from qiskit.transpiler.preset_passmanagers.common import generate_embed_passmanager
 
 
@@ -217,6 +218,34 @@ def test_neither_coupling_map_or_target(self):
         with self.assertRaises(TranspilerError):
             vf2_pass.run(dag)
 
+    def test_target_no_error(self):
+        """Test that running vf2layout on a pass against a target with no error rates works."""
+        n_qubits = 15
+        target = Target()
+        target.add_instruction(CXGate(), {(i, i + 1): None for i in range(n_qubits - 1)})
+        vf2_pass = VF2Layout(target=target)
+        circuit = QuantumCircuit(2)
+        circuit.cx(0, 1)
+        dag = circuit_to_dag(circuit)
+        vf2_pass.run(dag)
+        self.assertLayout(dag, target.build_coupling_map(), vf2_pass.property_set)
+
+    def test_target_some_error(self):
+        """Test that running vf2layout on a pass against a target with some error rates works."""
+        n_qubits = 15
+        target = Target()
+        target.add_instruction(
+            XGate(), {(i,): InstructionProperties(error=0.00123) for i in range(n_qubits)}
+        )
+        target.add_instruction(CXGate(), {(i, i + 1): None for i in range(n_qubits - 1)})
+        vf2_pass = VF2Layout(target=target)
+        circuit = QuantumCircuit(2)
+        circuit.h(0)
+        circuit.cx(0, 1)
+        dag = circuit_to_dag(circuit)
+        vf2_pass.run(dag)
+        self.assertLayout(dag, target.build_coupling_map(), vf2_pass.property_set)
+
 
 class TestVF2LayoutLattice(LayoutTestCase):
     """Fit in 25x25 hexagonal lattice coupling map"""
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
@@ -16,6 +16,7 @@
 
 from qiskit import QuantumRegister, QuantumCircuit
 from qiskit.circuit import ControlFlowOp
+from qiskit.circuit.library import CXGate, XGate
 from qiskit.transpiler import CouplingMap, Layout, TranspilerError
 from qiskit.transpiler.passes.layout import vf2_utils
 from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayout, VF2PostLayoutStopReason
@@ -24,6 +25,7 @@
 from qiskit.providers.fake_provider import FakeLima, FakeYorktown, FakeLimaV2, FakeYorktownV2
 from qiskit.circuit import Qubit
 from qiskit.compiler.transpiler import transpile
+from qiskit.transpiler.target import Target, InstructionProperties
 
 
 class TestVF2PostLayout(QiskitTestCase):
@@ -326,6 +328,35 @@ def test_target_invalid_2q_gate_control_flow(self):
             VF2PostLayoutStopReason.NO_SOLUTION_FOUND,
         )
 
+    def test_target_no_error(self):
+        """Test that running vf2layout on a pass against a target with no error rates works."""
+        n_qubits = 15
+        target = Target()
+        target.add_instruction(CXGate(), {(i, i + 1): None for i in range(n_qubits - 1)})
+        vf2_pass = VF2PostLayout(target=target)
+        circuit = QuantumCircuit(2)
+        circuit.cx(0, 1)
+        dag = circuit_to_dag(circuit)
+        vf2_pass.run(dag)
+        self.assertNotIn("post_layout", vf2_pass.property_set)
+
+    def test_target_some_error(self):
+        """Test that running vf2layout on a pass against a target with some error rates works."""
+        n_qubits = 15
+        target = Target()
+        target.add_instruction(
+            XGate(), {(i,): InstructionProperties(error=0.00123) for i in range(n_qubits)}
+        )
+        target.add_instruction(CXGate(), {(i, i + 1): None for i in range(n_qubits - 1)})
+        vf2_pass = VF2PostLayout(target=target, seed=1234, strict_direction=False)
+        circuit = QuantumCircuit(2)
+        circuit.h(0)
+        circuit.cx(0, 1)
+        dag = circuit_to_dag(circuit)
+        vf2_pass.run(dag)
+        # No layout selected because nothing will beat initial layout
+        self.assertNotIn("post_layout", vf2_pass.property_set)
+
 
 class TestVF2PostLayoutScoring(QiskitTestCase):
     """Test scoring heuristic function for VF2PostLayout."""