Repository: Qiskit/qiskit

Issue:
Recursively controlling a circuit takes exponential time and memory
### Environment

- **Qiskit version**: 1.0
- **Python version**: 3.12
- **Operating system**: OSX and Linux
 

### What is happening?

Recursively adding controls to an initial sub-circuit will cause Qiskit to hang, using up ever increasing CPU and memory resources.  E.g. the below will never complete, but taking `N=5` or so will.

```python

N = 10
sub_qc = QuantumCircuit(1)
sub_qc.x(0)

out = QuantumCircuit(N)
out.compose(sub_qc,range(sub_qc.num_qubits), inplace=True)

for kk in range(N-1):
    sub_qc = sub_qc.control()
    out.compose(sub_qc,range(sub_qc.num_qubits), inplace=True)
```

### How can we reproduce the issue?

Run above

### What should happen?

It should not take apparently exponential time

### Any suggestions?

_No response_


Discussion:
This is because `control` by default, for historical reasons, performs an eager synthesis of the control operation, so the scaling is fairly naturally super-linear (for arbitrary controlled gates).  The solution for the simple representation of the objects is to use the modern form that @alexanderivrii has been getting in place, where controls, inverses and gate powers are represented as modifier tags on the gate, and only realised at synthesis time.  In your example, setting `annotated=True` in the `control` call will make this script work in linear time of `N` (*edit*: actually, it's quadratic in `N` because we create a gate with 1, then 2, ... `N` qubits, but the actual creation of the gate object doesn't scale exponentially)

That said, when it actually comes to synthesising the gates through `transpile`, you're still almost certainly going to see the super-linear blow up.  Our arbitrary-control syntheses probably aren't the best they can be yet, but I think the general case is still going to scale quite a lot with the number of controls.
Just as an immediate copy-pastable example of the above:
```python
from qiskit import QuantumCircuit

N = 100
sub_qc = QuantumCircuit(1)
sub_qc.x(0)

out = QuantumCircuit(N)
out.compose(sub_qc,range(sub_qc.num_qubits), inplace=True)

for kk in range(N-1):
    sub_qc = sub_qc.control(annotated=True)
    out.compose(sub_qc,range(sub_qc.num_qubits), inplace=True)
```
Fwiw, slightly better form for this would be to do:

```python
from qiskit import QuantumCircuit

N = 100
sub_qc = QuantumCircuit(1)
sub_qc.x(0)
sub = sub_qc.to_gate()

out = QuantumCircuit(N)
out.append(sub, range(sub.num_qubits))

for kk in range(N-1):
    sub = sub.control(annotated=True)
    out.append(sub, range(sub.num_qubits))
```

`compose` is a circuit-inlining method, where a circuit represents a program.  A single object in a circuit is a `Gate` (if unitary) or `Instruction` (if not), and that's the base blocking structure of the object and how it's pulled through the transpiler.  Using the native block structure like that (rather than inlining things with `compose`) will be / should be better for performance longer term, especially as our things like synthesis of these gate annotations improves, because the tranpsiler will be able to retain the abstract/logical structure of the circuit for more longer, and do operations on large-scale abstract components, rather than a flattened structure caused by over-use of `compose`.   `compose` is of course better when you _want_ the structures to be merged / inlined, but I don't think this particular example is one of those.
Ok thanks.  I am trying to benchmark various aspects of Qiskit, and this one broke things, where as other frameworks can handle the controlled circuit.  

Is there anyway for an user to find out what `annotated=True` does in the control?  I cannot seem to find it.  Also, if it prevents blow-up, why is it not on by default?
It's not on by default because that would have been a major breaking API change, and we're not confident that it's as well supported through the entire stack as the current form yet.  It will become the default once it and its synthesis have matured a little more - I'd imagine changing the default of `control` will be one of the API breaks of Qiskit 2.0.

For example, at the moment, the multiple `control` form of annotated gate isn't merging the controlled modifiers into one, nor do the transpiler passes that synthesise the annotated forms of the gates, so we'll still effectively be doing the same looped calls you were underneath.  That's not super hard to fix of course, just an indication of the newness of the feature in Qiskit.

What are you meaning by "any way for a user to find out what it does?"?  It's a modifier, so it doesn't really _do_ anything (in the sense of the eager synthesis of `ControlledGate`) does on the spot.  How it gets synthesised to actual hardware depends (in theory, not yet in practice) on the basis gates and coupling constraints of the hardware, which isn't defined at the point of creating an abstract circuit, but would happen during compilation.
Yeah, no I answered my own question when I tried to change the basis on the circuit and it just kept running.  

It also modifies the circuit drawing, which is not too important, but unexpected.
Continuing with Jake's example, here are a few thoughts on the following synthesis step:

An expression of the form `sub.control(annotated=True) ... .control(annotated=True)` would actually combine all of the control modifiers into a single list, that is create an "annotated operation" whose "base operation" is `sub` and whose modifiers are the list `[ControlModifier(1), ..., ControlModifier(1)]`. 

For `N=10`, synthesizing this directly is very slow, but we do have a new transpiler pass `OptimizeAnnotated` in Qiskit 1.0 that canonicalizes the modifiers, that is replaces the list of modifiers by a single control modifier with multiple controls. We are still investigating how to best integrate it into the transpiler flow, so it's not on by default, but one would use this along the following lines:
```
from qiskit.transpiler.passes import OptimizeAnnotated
basis_gates = ['cx', 'id', 'u']
out = OptimizeAnnotated(basis_gates=basis_gates)(out)
```
The pass is recursive, descending into the definitions of custom gates, and it needs basis gates and equivalence library to know which definitions to examine and which to skip. 

For `N=10`, the optimized circuit `out` is now susceptible to transpilation (i.e. to either `transpile` or to `HighLevelSynthesis`). However the approach still does not scale to much larger values of `N`. If I recall correctly (not too sure about this), our algorithm for synthesizing multiply-controlled gates without ancilla qubits produces exponentially many gates. And, needless to say, a synthesis algorithm that produces exponentially many gates requires exponential runtime.

One could do a bit better by replacing the code
```
sub_qc = QuantumCircuit(1)
sub_qc.x(0)
sub = sub_qc.to_gate()
```
by
```
sub = XGate()
```
in which case the synthesis algorithm would treat the annotated `sub` essentially as an `MCX`-gate, producing half as many gates and somewhat decreasing runtime, but unfortunately our synthesis algorithm ("mcx_gray") for MCX-gates without ancillas still produces exponentially many gates.
Indeed, it seems that the current synthesis algorithms for multi-controlled gates could be improved. See the discussion in #5872 

Resolving patch:
diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py
--- a/qiskit/circuit/library/standard_gates/p.py
+++ b/qiskit/circuit/library/standard_gates/p.py
@@ -354,20 +354,32 @@ def _define(self):
         # pylint: disable=cyclic-import
         from qiskit.circuit.quantumcircuit import QuantumCircuit
 
-        q = QuantumRegister(self.num_qubits, "q")
-        qc = QuantumCircuit(q, name=self.name)
+        qr = QuantumRegister(self.num_qubits, "q")
+        qc = QuantumCircuit(qr, name=self.name)
 
         if self.num_ctrl_qubits == 0:
             qc.p(self.params[0], 0)
         if self.num_ctrl_qubits == 1:
             qc.cp(self.params[0], 0, 1)
         else:
-            from .u3 import _gray_code_chain
-
-            scaled_lam = self.params[0] / (2 ** (self.num_ctrl_qubits - 1))
-            bottom_gate = CPhaseGate(scaled_lam)
-            for operation, qubits, clbits in _gray_code_chain(q, self.num_ctrl_qubits, bottom_gate):
-                qc._append(operation, qubits, clbits)
+            lam = self.params[0]
+            if type(lam) in [float, int]:
+                q_controls = list(range(self.num_ctrl_qubits))
+                q_target = self.num_ctrl_qubits
+                new_target = q_target
+                for k in range(self.num_ctrl_qubits):
+                    qc.mcrz(lam / (2**k), q_controls, new_target, use_basis_gates=True)
+                    new_target = q_controls.pop()
+                qc.p(lam / (2**self.num_ctrl_qubits), new_target)
+            else:  # in this case type(lam) is ParameterValueType
+                from .u3 import _gray_code_chain
+
+                scaled_lam = self.params[0] / (2 ** (self.num_ctrl_qubits - 1))
+                bottom_gate = CPhaseGate(scaled_lam)
+                for operation, qubits, clbits in _gray_code_chain(
+                    qr, self.num_ctrl_qubits, bottom_gate
+                ):
+                    qc._append(operation, qubits, clbits)
         self.definition = qc
 
     def control(
@@ -407,5 +419,5 @@ def control(
         return gate
 
     def inverse(self, annotated: bool = False):
-        r"""Return inverted MCU1 gate (:math:`MCU1(\lambda)^{\dagger} = MCU1(-\lambda)`)"""
+        r"""Return inverted MCPhase gate (:math:`MCPhase(\lambda)^{\dagger} = MCPhase(-\lambda)`)"""
         return MCPhaseGate(-self.params[0], self.num_ctrl_qubits)
diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py
--- a/qiskit/circuit/library/standard_gates/x.py
+++ b/qiskit/circuit/library/standard_gates/x.py
@@ -1095,7 +1095,7 @@ def __new__(
         explicit CX, CCX, C3X or C4X instance or a generic MCX gate.
         """
         # The CXGate and CCXGate will be implemented for all modes of the MCX, and
-        # the C3XGate and C4XGate will be implemented in the MCXGrayCode class.
+        # the C3XGate and C4XGate are handled in the gate definition.
         explicit: dict[int, Type[ControlledGate]] = {1: CXGate, 2: CCXGate}
         gate_class = explicit.get(num_ctrl_qubits, None)
         if gate_class is not None:
@@ -1166,14 +1166,25 @@ def get_num_ancilla_qubits(num_ctrl_qubits: int, mode: str = "noancilla") -> int
         raise AttributeError(f"Unsupported mode ({mode}) specified!")
 
     def _define(self):
-        """The standard definition used the Gray code implementation."""
+        """This definition is based on MCPhaseGate implementation."""
         # pylint: disable=cyclic-import
         from qiskit.circuit.quantumcircuit import QuantumCircuit
 
         q = QuantumRegister(self.num_qubits, name="q")
         qc = QuantumCircuit(q)
-        qc._append(MCXGrayCode(self.num_ctrl_qubits), q[:], [])
-        self.definition = qc
+        if self.num_qubits == 4:
+            qc._append(C3XGate(), q[:], [])
+            self.definition = qc
+        elif self.num_qubits == 5:
+            qc._append(C4XGate(), q[:], [])
+            self.definition = qc
+        else:
+            q_controls = list(range(self.num_ctrl_qubits))
+            q_target = self.num_ctrl_qubits
+            qc.h(q_target)
+            qc.mcp(numpy.pi, q_controls, q_target)
+            qc.h(q_target)
+            self.definition = qc
 
     @property
     def num_ancilla_qubits(self):


Test patch:
diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py
--- a/test/python/circuit/test_controlled_gate.py
+++ b/test/python/circuit/test_controlled_gate.py
@@ -749,6 +749,19 @@ def test_mcx_gates_yield_explicit_gates(self, num_ctrl_qubits):
         explicit = {1: CXGate, 2: CCXGate}
         self.assertEqual(cls, explicit[num_ctrl_qubits])
 
+    @data(1, 2, 3, 4)
+    def test_small_mcx_gates_yield_cx_count(self, num_ctrl_qubits):
+        """Test the creating a MCX gate with small number of controls (with no ancillas)
+        yields the expected number of cx gates."""
+        qc = QuantumCircuit(num_ctrl_qubits + 1)
+        qc.append(MCXGate(num_ctrl_qubits), range(num_ctrl_qubits + 1))
+        from qiskit import transpile
+
+        cqc = transpile(qc, basis_gates=["u", "cx"])
+        cx_count = cqc.count_ops()["cx"]
+        expected = {1: 1, 2: 6, 3: 14, 4: 36}
+        self.assertEqual(cx_count, expected[num_ctrl_qubits])
+
     @data(1, 2, 3, 4)
     def test_mcxgraycode_gates_yield_explicit_gates(self, num_ctrl_qubits):
         """Test creating an mcx gate calls MCXGrayCode and yeilds explicit definition."""