Repository: Qiskit/qiskit

Issue:
Unitaries equal to identity are not removed from circuit at optimization_level=3
### Environment

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


### What is happening?

The following circuit is equal to the identity:
```python

qr1 = QuantumRegister(3, 'state')
qr2 = QuantumRegister(2, 'ancilla')
cr = ClassicalRegister(2, 'c')

qc2 = QuantumCircuit(qr1, qr2, cr)
qc2.h(qr1[0])
qc2.cx(qr1[0], qr1[1])
qc2.cx(qr1[1], qr1[2])

qc2.cx(qr1[1], qr1[2])
qc2.cx(qr1[0], qr1[1])
qc2.h(qr1[0])
```

At `optimization_level=2` one gets a empty circuit:

![image](https://user-images.githubusercontent.com/1249193/204908535-5fa18c9b-5660-4527-bc3d-58dc5ff29d6e.png)

At `optimization_level=3` one does not:

![image](https://user-images.githubusercontent.com/1249193/204908623-d0510302-2237-4e93-a773-0e821024dada.png)

What is left is a unitary that is equal to the identity (as it should be) but it is not removed from the circuit data.

### How can we reproduce the issue?

Transpile above example

### What should happen?

The circuit should be empty like at `optimization_level=2`

### Any suggestions?

_No response_


Discussion:
So I think the issue here is that you're not specifying a target or `basis_gates` in the transpile call. The 2q unitary synth optimization passes need a basis to determine how to synthesize the unitary and without that being specified it's skipping the synthesis stage after the circuit is collected into a single unitary block.

That being said it's probably not too much extra overhead to add a check either in the collection pass or in the synthesis pass that still checks for identity unitary removal even without a target specified. As this isn't the first time this exact behavior has been raised in an issue.
The difference in behavior between optimization levels is the surprising part, and leaves one having to explain why the higher optimization level is also not an empty circuit; A tricky conversation when it is an implementation issue, and one that until now I was not even aware of. 

Resolving patch:
diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py
--- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py
+++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py
@@ -12,6 +12,8 @@
 
 """Replace each block of consecutive gates by a single Unitary node."""
 
+import numpy as np
+
 from qiskit.circuit.classicalregister import ClassicalRegister
 from qiskit.circuit.quantumregister import QuantumRegister
 from qiskit.circuit.quantumcircuit import QuantumCircuit
@@ -113,9 +115,17 @@ def run(self, dag):
                     or ((self.basis_gates is not None) and outside_basis)
                     or ((self.target is not None) and outside_basis)
                 ):
-                    dag.replace_block_with_op(block, unitary, block_index_map, cycle_check=False)
+                    identity = np.eye(2**unitary.num_qubits)
+                    if np.allclose(identity, unitary.to_matrix()):
+                        for node in block:
+                            dag.remove_op_node(node)
+                    else:
+                        dag.replace_block_with_op(
+                            block, unitary, block_index_map, cycle_check=False
+                        )
         # If 1q runs are collected before consolidate those too
         runs = self.property_set["run_list"] or []
+        identity_1q = np.eye(2)
         for run in runs:
             if any(gate in all_block_gates for gate in run):
                 continue
@@ -134,7 +144,11 @@ def run(self, dag):
                 if already_in_block:
                     continue
                 unitary = UnitaryGate(operator)
-                dag.replace_block_with_op(run, unitary, {qubit: 0}, cycle_check=False)
+                if np.allclose(identity_1q, unitary.to_matrix()):
+                    for node in run:
+                        dag.remove_op_node(node)
+                else:
+                    dag.replace_block_with_op(run, unitary, {qubit: 0}, cycle_check=False)
         # Clear collected blocks and runs as they are no longer valid after consolidation
         if "run_list" in self.property_set:
             del self.property_set["run_list"]
diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py
--- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py
+++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py
@@ -55,6 +55,9 @@ def __init__(self, basis=None, target=None):
 
         if basis:
             self._global_decomposers = _possible_decomposers(set(basis))
+        elif target is None:
+            self._global_decomposers = _possible_decomposers(None)
+            self._basis_gates = None
 
     def _resynthesize_run(self, run, qubit=None):
         """
@@ -97,10 +100,14 @@ def _substitution_checks(self, dag, old_run, new_circ, basis, qubit):
         # does this run have uncalibrated gates?
         uncalibrated_p = not has_cals_p or any(not dag.has_calibration_for(g) for g in old_run)
         # does this run have gates not in the image of ._decomposers _and_ uncalibrated?
-        uncalibrated_and_not_basis_p = any(
-            g.name not in basis and (not has_cals_p or not dag.has_calibration_for(g))
-            for g in old_run
-        )
+        if basis is not None:
+            uncalibrated_and_not_basis_p = any(
+                g.name not in basis and (not has_cals_p or not dag.has_calibration_for(g))
+                for g in old_run
+            )
+        else:
+            # If no basis is specified then we're always in the basis
+            uncalibrated_and_not_basis_p = False
 
         # if we're outside of the basis set, we're obligated to logically decompose.
         # if we're outside of the set of gates for which we have physical definitions,
@@ -124,10 +131,6 @@ def run(self, dag):
         Returns:
             DAGCircuit: the optimized DAG.
         """
-        if self._basis_gates is None and self._target is None:
-            logger.info("Skipping pass because no basis or target is set")
-            return dag
-
         runs = dag.collect_1q_runs()
         qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)}
         for run in runs:
@@ -151,11 +154,17 @@ def run(self, dag):
 
 def _possible_decomposers(basis_set):
     decomposers = []
-    euler_basis_gates = one_qubit_decompose.ONE_QUBIT_EULER_BASIS_GATES
-    for euler_basis_name, gates in euler_basis_gates.items():
-        if set(gates).issubset(basis_set):
-            decomposer = one_qubit_decompose.OneQubitEulerDecomposer(euler_basis_name)
-            decomposers.append(decomposer)
+    if basis_set is None:
+        decomposers = [
+            one_qubit_decompose.OneQubitEulerDecomposer(basis)
+            for basis in one_qubit_decompose.ONE_QUBIT_EULER_BASIS_GATES
+        ]
+    else:
+        euler_basis_gates = one_qubit_decompose.ONE_QUBIT_EULER_BASIS_GATES
+        for euler_basis_name, gates in euler_basis_gates.items():
+            if set(gates).issubset(basis_set):
+                decomposer = one_qubit_decompose.OneQubitEulerDecomposer(euler_basis_name)
+                decomposers.append(decomposer)
     return decomposers
 
 
diff --git a/qiskit/visualization/timeline/core.py b/qiskit/visualization/timeline/core.py
--- a/qiskit/visualization/timeline/core.py
+++ b/qiskit/visualization/timeline/core.py
@@ -162,7 +162,10 @@ def load_program(self, program: circuit.QuantumCircuit):
 
             try:
                 program = transpile(
-                    program, scheduling_method="alap", instruction_durations=InstructionDurations()
+                    program,
+                    scheduling_method="alap",
+                    instruction_durations=InstructionDurations(),
+                    optimization_level=0,
                 )
             except TranspilerError as ex:
                 raise VisualizationError(


Test patch:
diff --git a/test/python/circuit/test_scheduled_circuit.py b/test/python/circuit/test_scheduled_circuit.py
--- a/test/python/circuit/test_scheduled_circuit.py
+++ b/test/python/circuit/test_scheduled_circuit.py
@@ -154,7 +154,10 @@ def test_transpile_delay_circuit_without_backend(self):
         qc.delay(500, 1)
         qc.cx(0, 1)
         scheduled = transpile(
-            qc, scheduling_method="alap", instruction_durations=[("h", 0, 200), ("cx", [0, 1], 700)]
+            qc,
+            scheduling_method="alap",
+            basis_gates=["h", "cx"],
+            instruction_durations=[("h", 0, 200), ("cx", [0, 1], 700)],
         )
         self.assertEqual(scheduled.duration, 1200)
 
@@ -259,6 +262,7 @@ def test_per_qubit_durations(self):
         sc = transpile(
             qc,
             scheduling_method="alap",
+            basis_gates=["h", "cx"],
             instruction_durations=[("h", None, 200), ("cx", [0, 1], 700)],
         )
         self.assertEqual(sc.qubit_start_time(0), 300)
@@ -274,6 +278,7 @@ def test_per_qubit_durations(self):
         sc = transpile(
             qc,
             scheduling_method="alap",
+            basis_gates=["h", "cx", "measure"],
             instruction_durations=[("h", None, 200), ("cx", [0, 1], 700), ("measure", None, 1000)],
         )
         q = sc.qubits
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
@@ -1542,6 +1542,27 @@ def _visit_block(circuit, qubit_mapping=None):
             qubit_mapping={qubit: index for index, qubit in enumerate(transpiled.qubits)},
         )
 
+    @data(1, 2, 3)
+    def test_transpile_identity_circuit_no_target(self, opt_level):
+        """Test circuit equivalent to identity is optimized away for all optimization levels >0.
+
+        Reproduce taken from https://github.com/Qiskit/qiskit-terra/issues/9217
+        """
+        qr1 = QuantumRegister(3, "state")
+        qr2 = QuantumRegister(2, "ancilla")
+        cr = ClassicalRegister(2, "c")
+        qc = QuantumCircuit(qr1, qr2, cr)
+        qc.h(qr1[0])
+        qc.cx(qr1[0], qr1[1])
+        qc.cx(qr1[1], qr1[2])
+        qc.cx(qr1[1], qr1[2])
+        qc.cx(qr1[0], qr1[1])
+        qc.h(qr1[0])
+
+        empty_qc = QuantumCircuit(qr1, qr2, cr)
+        result = transpile(qc, optimization_level=opt_level)
+        self.assertEqual(empty_qc, result)
+
 
 @ddt
 class TestPostTranspileIntegration(QiskitTestCase):
diff --git a/test/python/transpiler/test_consolidate_blocks.py b/test/python/transpiler/test_consolidate_blocks.py
--- a/test/python/transpiler/test_consolidate_blocks.py
+++ b/test/python/transpiler/test_consolidate_blocks.py
@@ -407,6 +407,27 @@ def test_single_gate_block_outside_target_with_matching_basis_gates(self):
         expected.swap(0, 1)
         self.assertEqual(expected, pass_manager.run(qc))
 
+    def test_identity_unitary_is_removed(self):
+        """Test that a 2q identity unitary is removed without a basis."""
+        qc = QuantumCircuit(5)
+        qc.h(0)
+        qc.cx(0, 1)
+        qc.cx(0, 1)
+        qc.h(0)
+
+        pm = PassManager([Collect2qBlocks(), ConsolidateBlocks()])
+        self.assertEqual(QuantumCircuit(5), pm.run(qc))
+
+    def test_identity_1q_unitary_is_removed(self):
+        """Test that a 1q identity unitary is removed without a basis."""
+        qc = QuantumCircuit(5)
+        qc.h(0)
+        qc.h(0)
+        qc.h(0)
+        qc.h(0)
+        pm = PassManager([Collect2qBlocks(), Collect1qRuns(), ConsolidateBlocks()])
+        self.assertEqual(QuantumCircuit(5), pm.run(qc))
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py
--- a/test/python/transpiler/test_optimize_1q_decomposition.py
+++ b/test/python/transpiler/test_optimize_1q_decomposition.py
@@ -135,6 +135,16 @@ def test_optimize_identity_target(self, target):
         result = passmanager.run(circuit)
         self.assertEqual(expected, result)
 
+    def test_optimize_away_idenity_no_target(self):
+        """Test identity run is removed for no target specified."""
+        circuit = QuantumCircuit(1)
+        circuit.h(0)
+        circuit.h(0)
+        passmanager = PassManager()
+        passmanager.append(Optimize1qGatesDecomposition())
+        result = passmanager.run(circuit)
+        self.assertEqual(QuantumCircuit(1), result)
+
     def test_optimize_error_over_target_1(self):
         """XZX is re-written as ZXZ, which is cheaper according to target."""
         qr = QuantumRegister(1, "qr")