Repository: Qiskit/qiskit

Issue:
Circuits optimised on transpile level >= 2 are not equivalent
### Environment

- **Qiskit Terra version**: 0.25.1
- **Python version**: 3.10.13
- **Operating system**: MacOS Ventura


### What is happening?

When optimising the circuit below using `optimization_level=2` or `optimization_level=3` with target gate set `basis_gates=['cx', 'u3']`, the result is not equivalent to the input.

Input circuit
```
q_0: ─■───────────────■─
      │ ┌───┐   ┌───┐ │ 
q_1: ─■─┤ H ├─■─┤ H ├─■─
        └───┘ │ └───┘   
q_2: ─────────■─────────
```

Output circuit:
```                  
q_0: ─────────────────────────────────────
     ┌──────────────┐     ┌──────────────┐
q_1: ┤ U3(π/2,0,-π) ├──■──┤ U3(π/2,0,-π) ├
     ├─────────────┬┘┌─┴─┐├─────────────┬┘
q_2: ┤ U3(π/2,0,π) ├─┤ X ├┤ U3(π/2,0,π) ├─
     └─────────────┘ └───┘└─────────────┘ 
```

### How can we reproduce the issue?

The following code outputs `"Circuits are not equivalent"`.

```python
import numpy as np
from qiskit import transpile, QuantumCircuit
import qiskit.quantum_info as qi


circ = QuantumCircuit(3)
circ.cz(0, 1)
circ.h(1)
circ.cz(1, 2)
circ.h(1)
circ.cz(0, 1)

# optimisation
opt_circ = transpile(circ, optimization_level=2, basis_gates=["cx", "u3"])

# check equivalence
def get_unitary(circ):
    op = qi.Operator(circ)
    return np.matrix(op.data)

u = get_unitary(circ)
u_opt = get_unitary(opt_circ)

u_uinv = u * u_opt.H
# fix global phase
u_uinv /= u_uinv[0, 0]

if np.isclose(u_uinv, np.eye(2**circ.num_qubits)).all():
    print("Circuits are equivalent")
else:
    print("Circuits are not equivalent")

```

### What should happen?

The code should output `Circuits are equivalent`.

### Any suggestions?

This only applies to gate sets including the `u3` gate. Compiling to e.g. `['cx', 'rz', 'sx', 'x']` does not have this problem.


Discussion:

Resolving patch:
diff --git a/qiskit/transpiler/passes/optimization/commutation_analysis.py b/qiskit/transpiler/passes/optimization/commutation_analysis.py
--- a/qiskit/transpiler/passes/optimization/commutation_analysis.py
+++ b/qiskit/transpiler/passes/optimization/commutation_analysis.py
@@ -1,6 +1,6 @@
 # This code is part of Qiskit.
 #
-# (C) Copyright IBM 2017, 2018.
+# (C) Copyright IBM 2017, 2023.
 #
 # This code is licensed under the Apache License, Version 2.0. You may
 # obtain a copy of this license in the LICENSE.txt file in the root directory
@@ -25,9 +25,6 @@ class CommutationAnalysis(AnalysisPass):
     Property_set['commutation_set'] is a dictionary that describes
     the commutation relations on a given wire, all the gates on a wire
     are grouped into a set of gates that commute.
-
-    TODO: the current pass determines commutativity through matrix multiplication.
-    A rule-based analysis would be potentially faster, but more limited.
     """
 
     def __init__(self):
@@ -68,18 +65,24 @@ def run(self, dag):
                     current_comm_set.append([current_gate])
 
                 if current_gate not in current_comm_set[-1]:
-                    prev_gate = current_comm_set[-1][-1]
-                    does_commute = False
-
-                    if isinstance(current_gate, DAGOpNode) and isinstance(prev_gate, DAGOpNode):
-                        does_commute = self.comm_checker.commute(
-                            current_gate.op,
-                            current_gate.qargs,
-                            current_gate.cargs,
-                            prev_gate.op,
-                            prev_gate.qargs,
-                            prev_gate.cargs,
+                    does_commute = True
+
+                    # Check if the current gate commutes with all the gates in the current block
+                    for prev_gate in current_comm_set[-1]:
+                        does_commute = (
+                            isinstance(current_gate, DAGOpNode)
+                            and isinstance(prev_gate, DAGOpNode)
+                            and self.comm_checker.commute(
+                                current_gate.op,
+                                current_gate.qargs,
+                                current_gate.cargs,
+                                prev_gate.op,
+                                prev_gate.qargs,
+                                prev_gate.cargs,
+                            )
                         )
+                        if not does_commute:
+                            break
 
                     if does_commute:
                         current_comm_set[-1].append(current_gate)


Test patch:
diff --git a/test/python/transpiler/test_commutative_cancellation.py b/test/python/transpiler/test_commutative_cancellation.py
--- a/test/python/transpiler/test_commutative_cancellation.py
+++ b/test/python/transpiler/test_commutative_cancellation.py
@@ -762,6 +762,22 @@ def test_cancellation_not_crossing_between_blocks(self):
         new_circuit = passmanager.run(test2)
         self.assertEqual(new_circuit, test2)
 
+    def test_no_intransitive_cancellation(self):
+        """Test that no unsound optimization occurs due to "intransitively-commuting" gates.
+        See: https://github.com/Qiskit/qiskit-terra/issues/8020.
+        """
+        circ = QuantumCircuit(1)
+
+        circ.x(0)
+        circ.id(0)
+        circ.h(0)
+        circ.id(0)
+        circ.x(0)
+
+        passmanager = PassManager([CommutationAnalysis(), CommutativeCancellation()])
+        new_circuit = passmanager.run(circ)
+        self.assertEqual(new_circuit, circ)
+
 
 if __name__ == "__main__":
     unittest.main()