Repository: Qiskit/qiskit

Issue:
no validity check on Target construction
### Environment

- **Qiskit Terra version**: 
- **Python version**: 
- **Operating system**: 


### What is happening?

It's possible to build a Target where the Instruction does not match the number of qubits it's being added to. 

### How can we reproduce the issue?

For example here I add a 1-qubit gate to a two-qubit tuple.

```py
from qiskit.transpiler import Target, InstructionProperties
from qiskit.circuit.library import RZGate
from qiskit.circuit import Parameter

θ = Parameter('θ')
rz_props = {
    (0, 1): InstructionProperties(duration=0, error=0)
}

target = Target()
target.add_instruction(RZGate(θ), rz_props, name='rz')
```

### What should happen?

raise Error 

### Any suggestions?

_No response_


Discussion:
I also feel like this shouldn't be valid, but currently is:
```py
target = Target(2)
target.add_instruction(CXGate())
```

i.e. adding an instruction to unknown qubits. It will later fail if we try to do things like `target.operation_names_for_qargs((0,1))`
For the main issue: seems fine to me to add the checks, unless Matthew has reasons not to.

Your newest comment looks like a bug in `operation_names_for_qargs` to me - the construction example you gave is explicitly allowed and means "this instruction is valid on all pairs of qubits".  There may need to be an extra tracker for operations that are defined over `None`, and the numbers of qubits they're valid for.
Yeah I think this 2nd one is a bug, and is currently blocking https://github.com/Qiskit/qiskit-terra/pull/8917 by causing 2 tests to fail.
So the first point is half a feature half a bug. The `add_instruction()` method allows you to add instructions with new qubits and extend the qubits in the target. Something like:

```
target = Target()
target.add_instruction(RXGate(), {(0,): None, (1,): None, (2,): None})
```
To make a target with 3 qubits. The idea behind that was for generating target's programmatically it was easier to just parse an instructions payload and get a valid target without having to know the number of qubits ahead of time. But I agree it's a bug that we're not validating the number of qubits matches the operation width.

The second thing is definitely a bug as we should check the number of qubits for globally defined operations and return them in `operation_names_for_qargs()`.

Resolving patch:
diff --git a/qiskit/transpiler/passes/layout/dense_layout.py b/qiskit/transpiler/passes/layout/dense_layout.py
--- a/qiskit/transpiler/passes/layout/dense_layout.py
+++ b/qiskit/transpiler/passes/layout/dense_layout.py
@@ -147,7 +147,7 @@ def _build_error_matrix(num_qubits, target=None, coupling_map=None, backend_prop
             error = 0.0
             ops = target.operation_names_for_qargs(qargs)
             for op in ops:
-                props = target[op][qargs]
+                props = target[op].get(qargs, None)
                 if props is not None and props.error is not None:
                     # Use max error rate to represent operation error
                     # on a qubit(s). If there is more than 1 operation available
diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py
--- a/qiskit/transpiler/target.py
+++ b/qiskit/transpiler/target.py
@@ -181,6 +181,7 @@ class Target(Mapping):
         "_non_global_basis",
         "_non_global_strict_basis",
         "qubit_properties",
+        "_global_operations",
     )
 
     def __init__(
@@ -238,6 +239,8 @@ def __init__(
         self._gate_name_map = {}
         # A nested mapping of gate name -> qargs -> properties
         self._gate_map = {}
+        # A mapping of number of qubits to set of op names which are global
+        self._global_operations = defaultdict(set)
         # A mapping of qarg -> set(gate name)
         self._qarg_gate_map = defaultdict(set)
         self.dt = dt
@@ -350,8 +353,15 @@ def add_instruction(self, instruction, properties=None, name=None):
         if is_class:
             qargs_val = {None: None}
         else:
+            if None in properties:
+                self._global_operations[instruction.num_qubits].add(instruction_name)
             qargs_val = {}
             for qarg in properties:
+                if qarg is not None and len(qarg) != instruction.num_qubits:
+                    raise TranspilerError(
+                        f"The number of qubits for {instruction} does not match the number "
+                        f"of qubits in the properties dictionary: {qarg}"
+                    )
                 if qarg is not None:
                     self.num_qubits = max(self.num_qubits, max(qarg) + 1)
                 qargs_val[qarg] = properties[qarg]
@@ -530,7 +540,8 @@ def operations_for_qargs(self, qargs):
         Args:
             qargs (tuple): A qargs tuple of the qubits to get the gates that apply
                 to it. For example, ``(0,)`` will return the set of all
-                instructions that apply to qubit 0.
+                instructions that apply to qubit 0. If set to ``None`` this will
+                return any globally defined operations in the target.
         Returns:
             list: The list of :class:`~qiskit.circuit.Instruction` instances
             that apply to the specified qarg. This may also be a class if
@@ -539,12 +550,17 @@ def operations_for_qargs(self, qargs):
         Raises:
             KeyError: If qargs is not in target
         """
-        if qargs not in self._qarg_gate_map:
+        if qargs is not None and any(x not in range(0, self.num_qubits) for x in qargs):
             raise KeyError(f"{qargs} not in target.")
         res = [self._gate_name_map[x] for x in self._qarg_gate_map[qargs]]
-        if None in self._qarg_gate_map:
-            res += [self._gate_name_map[x] for x in self._qarg_gate_map[None]]
-        return res
+        if qargs is not None:
+            res += self._global_operations.get(len(qargs), list())
+        for op in self._gate_name_map.values():
+            if inspect.isclass(op):
+                res.append(op)
+        if not res:
+            raise KeyError(f"{qargs} not in target.")
+        return list(res)
 
     def operation_names_for_qargs(self, qargs):
         """Get the operation names for a specified qargs tuple
@@ -552,7 +568,8 @@ def operation_names_for_qargs(self, qargs):
         Args:
             qargs (tuple): A qargs tuple of the qubits to get the gates that apply
                 to it. For example, ``(0,)`` will return the set of all
-                instructions that apply to qubit 0.
+                instructions that apply to qubit 0. If set to ``None`` this will
+                return the names for any globally defined operations in the target.
         Returns:
             set: The set of operation names that apply to the specified
             `qargs``.
@@ -560,9 +577,17 @@ def operation_names_for_qargs(self, qargs):
         Raises:
             KeyError: If qargs is not in target
         """
-        if qargs not in self._qarg_gate_map:
+        if qargs is not None and any(x not in range(0, self.num_qubits) for x in qargs):
+            raise KeyError(f"{qargs} not in target.")
+        res = self._qarg_gate_map.get(qargs, set())
+        if qargs is not None:
+            res.update(self._global_operations.get(len(qargs), set()))
+        for name, op in self._gate_name_map.items():
+            if inspect.isclass(op):
+                res.add(name)
+        if not res:
             raise KeyError(f"{qargs} not in target.")
-        return self._qarg_gate_map[qargs]
+        return res
 
     def instruction_supported(
         self, operation_name=None, qargs=None, operation_class=None, parameters=None


Test patch:
diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py
--- a/test/python/transpiler/test_target.py
+++ b/test/python/transpiler/test_target.py
@@ -38,6 +38,7 @@
 from qiskit.transpiler.coupling import CouplingMap
 from qiskit.transpiler.instruction_durations import InstructionDurations
 from qiskit.transpiler.timing_constraints import TimingConstraints
+from qiskit.transpiler.exceptions import TranspilerError
 from qiskit.transpiler import Target
 from qiskit.transpiler import InstructionProperties
 from qiskit.test import QiskitTestCase
@@ -398,6 +399,30 @@ def test_get_instructions_for_qargs(self):
         for gate in ideal_sim_expected:
             self.assertIn(gate, self.ideal_sim_target.operations_for_qargs(None))
 
+    def test_get_operation_for_qargs_global(self):
+        expected = [
+            RXGate(self.theta),
+            RYGate(self.theta),
+            RZGate(self.theta),
+            RGate(self.theta, self.phi),
+            Measure(),
+        ]
+        res = self.aqt_target.operations_for_qargs((0,))
+        self.assertEqual(len(expected), len(res))
+        for x in expected:
+            self.assertIn(x, res)
+        expected = [RXXGate(self.theta)]
+        res = self.aqt_target.operations_for_qargs((0, 1))
+        self.assertEqual(len(expected), len(res))
+        for x in expected:
+            self.assertIn(x, res)
+
+    def test_get_invalid_operations_for_qargs(self):
+        with self.assertRaises(KeyError):
+            self.ibm_target.operations_for_qargs((0, 102))
+        with self.assertRaises(KeyError):
+            self.ibm_target.operations_for_qargs(None)
+
     def test_get_operation_names_for_qargs(self):
         with self.assertRaises(KeyError):
             self.empty_target.operation_names_for_qargs((0,))
@@ -416,6 +441,20 @@ def test_get_operation_names_for_qargs(self):
         for gate in ideal_sim_expected:
             self.assertIn(gate, self.ideal_sim_target.operation_names_for_qargs(None))
 
+    def test_get_operation_names_for_qargs_invalid_qargs(self):
+        with self.assertRaises(KeyError):
+            self.ibm_target.operation_names_for_qargs((0, 102))
+        with self.assertRaises(KeyError):
+            self.ibm_target.operation_names_for_qargs(None)
+
+    def test_get_operation_names_for_qargs_global_insts(self):
+        expected = {"r", "rx", "rz", "ry", "measure"}
+        self.assertEqual(self.aqt_target.operation_names_for_qargs((0,)), expected)
+        expected = {
+            "rxx",
+        }
+        self.assertEqual(self.aqt_target.operation_names_for_qargs((0, 1)), expected)
+
     def test_coupling_map(self):
         self.assertEqual(
             CouplingMap().get_edges(), self.empty_target.build_coupling_map().get_edges()
@@ -1374,6 +1413,84 @@ def test_instruction_names(self):
             {"u", "cx", "measure", "if_else", "while_loop", "for_loop"},
         )
 
+    def test_operations_for_qargs(self):
+        expected = [
+            IGate(),
+            RZGate(self.theta),
+            SXGate(),
+            XGate(),
+            Measure(),
+            IfElseOp,
+            ForLoopOp,
+            WhileLoopOp,
+        ]
+        res = self.ibm_target.operations_for_qargs((0,))
+        self.assertEqual(len(expected), len(res))
+        for x in expected:
+            self.assertIn(x, res)
+        expected = [
+            CXGate(),
+            IfElseOp,
+            ForLoopOp,
+            WhileLoopOp,
+        ]
+        res = self.ibm_target.operations_for_qargs((0, 1))
+        self.assertEqual(len(expected), len(res))
+        for x in expected:
+            self.assertIn(x, res)
+        expected = [
+            RXGate(self.theta),
+            RYGate(self.theta),
+            RZGate(self.theta),
+            RGate(self.theta, self.phi),
+            Measure(),
+            IfElseOp,
+            ForLoopOp,
+            WhileLoopOp,
+        ]
+        res = self.aqt_target.operations_for_qargs((0,))
+        self.assertEqual(len(expected), len(res))
+        for x in expected:
+            self.assertIn(x, res)
+        expected = [RXXGate(self.theta), IfElseOp, ForLoopOp, WhileLoopOp]
+        res = self.aqt_target.operations_for_qargs((0, 1))
+        self.assertEqual(len(expected), len(res))
+        for x in expected:
+            self.assertIn(x, res)
+
+    def test_operation_names_for_qargs(self):
+        expected = {
+            "id",
+            "rz",
+            "sx",
+            "x",
+            "measure",
+            "if_else",
+            "for_loop",
+            "while_loop",
+        }
+        self.assertEqual(expected, self.ibm_target.operation_names_for_qargs((0,)))
+        expected = {
+            "cx",
+            "if_else",
+            "for_loop",
+            "while_loop",
+        }
+        self.assertEqual(expected, self.ibm_target.operation_names_for_qargs((0, 1)))
+        expected = {
+            "rx",
+            "ry",
+            "rz",
+            "r",
+            "measure",
+            "if_else",
+            "for_loop",
+            "while_loop",
+        }
+        self.assertEqual(self.aqt_target.operation_names_for_qargs((0,)), expected)
+        expected = {"rxx", "if_else", "for_loop", "while_loop"}
+        self.assertEqual(self.aqt_target.operation_names_for_qargs((0, 1)), expected)
+
     def test_operations(self):
         ibm_expected = [
             RZGate(self.theta),
@@ -1411,6 +1528,12 @@ def test_operations(self):
         for gate in fake_expected:
             self.assertIn(gate, self.target_global_gates_only.operations)
 
+    def test_add_invalid_instruction(self):
+        inst_props = {(0, 1, 2, 3): None}
+        target = Target()
+        with self.assertRaises(TranspilerError):
+            target.add_instruction(CXGate(), inst_props)
+
     def test_instructions(self):
         ibm_expected = [
             (IGate(), (0,)),