Repository: Qiskit/qiskit

Issue:
`commutation analysis` lead to `numpy.core._exceptions._ArrayMemoryError` with large `mct`
### Environment

- **Qiskit Terra version**: 0.43.1 meta package, terra 0.24.1
- **Python version**: 3.10
- **Operating system**: docker continuumio/miniconda3

### What is happening?

When transpiling (level 2) a circuit with a large multi-cx gate (see doc: [here](https://qiskit.org/documentation/stubs/qiskit.circuit.QuantumCircuit.mct.html)) with a large numebr of qubits (e.g. `28`) the `commutation analysis` pass crashes.

### How can we reproduce the issue?

Run this python script:

```python
from qiskit import QuantumCircuit, transpile
qc = QuantumCircuit(29)
qc.mct(list(range(28)), qc.num_qubits - 1)
transpile(qc, optimization_level=2)
```
Produces this output and error:
```bash
Traceback (most recent call last):
  File "myfile.py", line 7, in <module>                       transpile(qc, optimization_level=2)
  File "...qiskit/compiler/transpiler.py", line 380, in transpile                    _serial_transpile_circuit(
  File "...qiskit/compiler/transpiler.py", line 462, in _serial_transpile_circuit    result = pass_manager.run(circuit, callback=callback, output_name=outpu
t_name)                                                                      File "...qiskit/transpiler/passmanager.py", line 537, in run                       return super().run(circuits, output_name, callback)
  File "...qiskit/transpiler/passmanager.py", line 231, in run                       return self._run_single_circuit(circuits, output_name, callback)
  File "...qiskit/transpiler/passmanager.py", line 292, in _run_single_circuit       result = running_passmanager.run(circuit, output_name=output_name, call
back=callback)                                                               File "...qiskit/transpiler/runningpassmanager.py", line 125, in run                dag = self._do_pass(pass_, dag, passset.options)
  File "...qiskit/transpiler/runningpassmanager.py", line 169, in _do_pass           dag = self._do_pass(required_pass, dag, options)
  File "...qiskit/transpiler/runningpassmanager.py", line 173, in _do_pass           dag = self._run_this_pass(pass_, dag)
  File "...qiskit/transpiler/runningpassmanager.py", line 227, in _run_this_pass     pass_.run(FencedDAGCircuit(dag))
  File "...qiskit/transpiler/passes/optimization/commutation_analysis.py", line 75, in run                                                                      does_commute = self.comm_checker.commute(
  File "...qiskit/circuit/commutation_checker.py", line 136, in commute              operator_2 = Operator(op2, input_dims=(2,) * len(qarg2), output_dims=(2
,) * len(qarg2))                                                             File "...qiskit/quantum_info/operators/operator.py", line 85, in __init__          self._data = self._init_instruction(data).data
  File "...qiskit/quantum_info/operators/operator.py", line 610, in _init_instruction                                                                           op = Operator(np.eye(dimension))
  File "...numpy/lib/twodim_base.py", line 215, in eye                               m = zeros((N, M), dtype=dtype, order=order)
numpy.core._exceptions._ArrayMemoryError: Unable to allocate 2.00 EiB for a
n array with shape (536870912, 536870912) and data type float64
```

### What should happen?

I would expect the optimizer to skip the pass if too large to optimize and leave it unoptimized.


### Any suggestions?

I would skip the optimization pass when the number of qubits is too large (precise threshold to be determined based on ram memory of the current machine).



Discussion:

Resolving patch:
diff --git a/qiskit/circuit/commutation_checker.py b/qiskit/circuit/commutation_checker.py
--- a/qiskit/circuit/commutation_checker.py
+++ b/qiskit/circuit/commutation_checker.py
@@ -65,10 +65,20 @@ def _hashable_parameters(self, params):
         return ("fallback", str(params))
 
     def commute(
-        self, op1: Operation, qargs1: List, cargs1: List, op2: Operation, qargs2: List, cargs2: List
-    ):
+        self,
+        op1: Operation,
+        qargs1: List,
+        cargs1: List,
+        op2: Operation,
+        qargs2: List,
+        cargs2: List,
+        max_num_qubits: int = 3,
+    ) -> bool:
         """
-        Checks if two Operations commute.
+        Checks if two Operations commute. The return value of `True` means that the operations
+        truly commute, and the return value of `False` means that either the operations do not
+        commute or that the commutation check was skipped (for example, when the operations
+        have conditions or have too many qubits).
 
         Args:
             op1: first operation.
@@ -77,10 +87,14 @@ def commute(
             op2: second operation.
             qargs2: second operation's qubits.
             cargs2: second operation's clbits.
+            max_num_qubits: the maximum number of qubits to consider, the check may be skipped if
+                the number of qubits for either operation exceeds this amount.
 
         Returns:
             bool: whether two operations commute.
         """
+        # pylint: disable=too-many-return-statements
+
         # We don't support commutation of conditional gates for now due to bugs in
         # CommutativeCancellation.  See gh-8553.
         if (
@@ -105,6 +119,10 @@ def commute(
         if not (intersection_q or intersection_c):
             return True
 
+        # Skip the check if the number of qubits for either operation is too large
+        if len(qargs1) > max_num_qubits or len(qargs2) > max_num_qubits:
+            return False
+
         # These lines are adapted from commutation_analysis, which is more restrictive than the
         # check from dag_dependency when considering nodes with "_directive".  It would be nice to
         # think which optimizations from dag_dependency can indeed be used.


Test patch:
diff --git a/test/python/circuit/test_commutation_checker.py b/test/python/circuit/test_commutation_checker.py
--- a/test/python/circuit/test_commutation_checker.py
+++ b/test/python/circuit/test_commutation_checker.py
@@ -25,6 +25,7 @@
     XGate,
     CXGate,
     CCXGate,
+    MCXGate,
     RZGate,
     Measure,
     Barrier,
@@ -362,6 +363,20 @@ def test_c7x_gate(self):
         res = CommutationChecker().commute(XGate(), qargs[:1], [], XGate().control(7), qargs, [])
         self.assertFalse(res)
 
+    def test_wide_gates_over_nondisjoint_qubits(self):
+        """Test that checking wide gates does not lead to memory problems."""
+        res = CommutationChecker().commute(MCXGate(29), list(range(30)), [], XGate(), [0], [])
+        self.assertFalse(res)
+        res = CommutationChecker().commute(XGate(), [0], [], MCXGate(29), list(range(30)), [])
+        self.assertFalse(res)
+
+    def test_wide_gates_over_disjoint_qubits(self):
+        """Test that wide gates still commute when they are over disjoint sets of qubits."""
+        res = CommutationChecker().commute(MCXGate(29), list(range(30)), [], XGate(), [30], [])
+        self.assertTrue(res)
+        res = CommutationChecker().commute(XGate(), [30], [], MCXGate(29), list(range(30)), [])
+        self.assertTrue(res)
+
 
 if __name__ == "__main__":
     unittest.main()