Repository: Qiskit/qiskit (python)

Issue:
GlobalPhaseGate causing `circuit.depth()` error
### Environment

- **Qiskit version**: 1.0.2
- **Python version**: 3.11.9
- **Operating system**: Windows


### What is happening?

Greetings,

Hope all are well. I have noticed that when you apply the GlobalPhaseGate to a circuit, and then try to retrieve the depth of the circuit, it causes an error due to the GlobalPhaseGate instruction not using any qubit indices. I think this should be fixed inside the `.depth()` method as perhaps a special case.

### How can we reproduce the issue?

```py
from qiskit import QuantumCircuit, transpile
from qiskit.circuit.library import GlobalPhaseGate


qc_qiskit = QuantumCircuit(2, 2)

qc_qiskit.cx(0, 1)

transpiled_qc = transpile(qc_qiskit,
                          basis_gates=['u3', 'cx'],
                          optimization_level=3,
                          seed_transpiler=0)

transpiled_qc.append(GlobalPhaseGate(2.5928519981876477), [], [])

transpiled_qc.global_phase

transpiled_qc.depth()
```

This will output the following error:
```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[15], [line 19](vscode-notebook-cell:?execution_count=15&line=19)
     [15](vscode-notebook-cell:?execution_count=15&line=15) transpiled_qc.append(GlobalPhaseGate(2.5928519981876477), [], [])
     [17](vscode-notebook-cell:?execution_count=15&line=17) transpiled_qc.global_phase
---> [19](vscode-notebook-cell:?execution_count=15&line=19) transpiled_qc.depth()

File c:\Users\A.C.EA\OneDrive\Documents\GitHub\QICKIT\.venv\Lib\site-packages\qiskit\circuit\quantumcircuit.py:1909, in QuantumCircuit.depth(self, filter_function)
   [1906](file:///C:/Users/A.C.EA/OneDrive/Documents/GitHub/QICKIT/.venv/Lib/site-packages/qiskit/circuit/quantumcircuit.py:1906)             reg_ints.append(idx)
   [1907](file:///C:/Users/A.C.EA/OneDrive/Documents/GitHub/QICKIT/.venv/Lib/site-packages/qiskit/circuit/quantumcircuit.py:1907)             levels.append(op_stack[idx] + 1)
-> [1909](file:///C:/Users/A.C.EA/OneDrive/Documents/GitHub/QICKIT/.venv/Lib/site-packages/qiskit/circuit/quantumcircuit.py:1909) max_level = max(levels)
   [1910](file:///C:/Users/A.C.EA/OneDrive/Documents/GitHub/QICKIT/.venv/Lib/site-packages/qiskit/circuit/quantumcircuit.py:1910) for ind in reg_ints:
   [1911](file:///C:/Users/A.C.EA/OneDrive/Documents/GitHub/QICKIT/.venv/Lib/site-packages/qiskit/circuit/quantumcircuit.py:1911)     op_stack[ind] = max_level

ValueError: max() arg is an empty sequence
```

### What should happen?

Inside the `.depth()` method, the code should simply pass when the instruction is a GlobalPhaseGate. Since it's not, because GlobalPhaseGate has no qubit index as parameter, it causes the `levels` variable to be `[]`, thus raising the error.

### Any suggestions?

I tried this and it worked:
```py
def depth(
        self,
        filter_function: Callable[..., int] = lambda x: not getattr(
            x.operation, "_directive", False
        ),
    ) -> int:
        """Return circuit depth (i.e., length of critical path).

        Args:
            filter_function (callable): A function to filter instructions.
                Should take as input a tuple of (Instruction, list(Qubit), list(Clbit)).
                Instructions for which the function returns False are ignored in the
                computation of the circuit depth.
                By default filters out "directives", such as barrier or snapshot.

        Returns:
            int: Depth of circuit.

        Notes:
            The circuit depth and the DAG depth need not be the
            same.
        """
        # Assign each bit in the circuit a unique integer
        # to index into op_stack.
        bit_indices: dict[Qubit | Clbit, int] = {
            bit: idx for idx, bit in enumerate(self.qubits + self.clbits)
        }

        # If no bits, return 0
        if not bit_indices:
            return 0

        # A list that holds the height of each qubit
        # and classical bit.
        op_stack = [0] * len(bit_indices)

        # Here we are playing a modified version of
        # Tetris where we stack gates, but multi-qubit
        # gates, or measurements have a block for each
        # qubit or cbit that are connected by a virtual
        # line so that they all stacked at the same depth.
        # Conditional gates act on all cbits in the register
        # they are conditioned on.
        # The max stack height is the circuit depth.
        for instruction in self._data:
            levels = []
            reg_ints = []
            for ind, reg in enumerate(instruction.qubits + instruction.clbits):
                # Add to the stacks of the qubits and
                # cbits used in the gate.
                reg_ints.append(bit_indices[reg])
                if filter_function(instruction):
                    levels.append(op_stack[reg_ints[ind]] + 1)
                else:
                    levels.append(op_stack[reg_ints[ind]])
            # Assuming here that there is no conditional
            # snapshots or barriers ever.
            if getattr(instruction.operation, "condition", None):
                # Controls operate over all bits of a classical register
                # or over a single bit
                if isinstance(instruction.operation.condition[0], Clbit):
                    condition_bits = [instruction.operation.condition[0]]
                else:
                    condition_bits = instruction.operation.condition[0]
                for cbit in condition_bits:
                    idx = bit_indices[cbit]
                    if idx not in reg_ints:
                        reg_ints.append(idx)
                        levels.append(op_stack[idx] + 1)

            if getattr(instruction.operation, 'name') == 'global_phase':
                levels = [0]

            max_level = max(levels)
            for ind in reg_ints:
                op_stack[ind] = max_level

        return max(op_stack)
```

Pull request description:
Fix `QuantumCircuit.depth` with zero-operands and `Expr` nodes (backport #12429)
### Summary

This causes `QuantumCircuit.depth` to correctly handle cases where a circuit instruction has zero operands (such as `GlobalPhaseGate`), and to treat classical bits and real-time variables used inside `Expr` conditions as part of the depth calculations.  This is in line with `DAGCircuit`.

This commit still does not add the same `recurse` argument from `DAGCircuit.depth`, because the arguments for not adding it to `QuantumCircuit.depth` at the time still hold; there is no clear meaning to it for general control flow from a user's perspective, and it was only added to the `DAGCircuit` methods because there it is more of a proxy for optimising over all possible inner blocks.




### Details and comments

Fix #12426.
<hr>This is an automatic backport of pull request #12429 done by [Mergify](https://mergify.com).

Patch:
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
index a157f0437..5d650f47c 100644
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -45,7 +45,7 @@ from qiskit.circuit.parameter import Parameter
 from qiskit.circuit.exceptions import CircuitError
 from . import _classical_resource_map
 from ._utils import sort_parameters
-from .controlflow import ControlFlowOp
+from .controlflow import ControlFlowOp, _builder_utils
 from .controlflow.builder import CircuitScopeInterface, ControlFlowBuilderBlock
 from .controlflow.break_loop import BreakLoopOp, BreakLoopPlaceholder
 from .controlflow.continue_loop import ContinueLoopOp, ContinueLoopPlaceholder
@@ -3307,6 +3307,9 @@ class QuantumCircuit:
     ) -> int:
         """Return circuit depth (i.e., length of critical path).
 
+        .. warning::
+            This operation is not well defined if the circuit contains control-flow operations.
+
         Args:
             filter_function: A function to decide which instructions count to increase depth.
                 Should take as a single positional input a :class:`CircuitInstruction`.
@@ -3332,59 +3335,40 @@ class QuantumCircuit:
 
                 assert qc.depth(lambda instr: len(instr.qubits) > 1) == 1
         """
-        # Assign each bit in the circuit a unique integer
-        # to index into op_stack.
-        bit_indices: dict[Qubit | Clbit, int] = {
-            bit: idx for idx, bit in enumerate(self.qubits + self.clbits)
+        obj_depths = {
+            obj: 0 for objects in (self.qubits, self.clbits, self.iter_vars()) for obj in objects
         }
 
-        # If no bits, return 0
-        if not bit_indices:
-            return 0
+        def update_from_expr(objects, node):
+            for var in expr.iter_vars(node):
+                if var.standalone:
+                    objects.add(var)
+                else:
+                    objects.update(_builder_utils.node_resources(var).clbits)
 
-        # A list that holds the height of each qubit
-        # and classical bit.
-        op_stack = [0] * len(bit_indices)
-
-        # Here we are playing a modified version of
-        # Tetris where we stack gates, but multi-qubit
-        # gates, or measurements have a block for each
-        # qubit or cbit that are connected by a virtual
-        # line so that they all stacked at the same depth.
-        # Conditional gates act on all cbits in the register
-        # they are conditioned on.
-        # The max stack height is the circuit depth.
         for instruction in self._data:
-            levels = []
-            reg_ints = []
-            for ind, reg in enumerate(instruction.qubits + instruction.clbits):
-                # Add to the stacks of the qubits and
-                # cbits used in the gate.
-                reg_ints.append(bit_indices[reg])
-                if filter_function(instruction):
-                    levels.append(op_stack[reg_ints[ind]] + 1)
-                else:
-                    levels.append(op_stack[reg_ints[ind]])
-            # Assuming here that there is no conditional
-            # snapshots or barriers ever.
-            if getattr(instruction.operation, "condition", None):
-                # Controls operate over all bits of a classical register
-                # or over a single bit
-                if isinstance(instruction.operation.condition[0], Clbit):
-                    condition_bits = [instruction.operation.condition[0]]
+            objects = set(itertools.chain(instruction.qubits, instruction.clbits))
+            if (condition := getattr(instruction.operation, "condition", None)) is not None:
+                objects.update(_builder_utils.condition_resources(condition).clbits)
+                if isinstance(condition, expr.Expr):
+                    update_from_expr(objects, condition)
                 else:
-                    condition_bits = instruction.operation.condition[0]
-                for cbit in condition_bits:
-                    idx = bit_indices[cbit]
-                    if idx not in reg_ints:
-                        reg_ints.append(idx)
-                        levels.append(op_stack[idx] + 1)
-
-            max_level = max(levels)
-            for ind in reg_ints:
-                op_stack[ind] = max_level
-
-        return max(op_stack)
+                    objects.update(_builder_utils.condition_resources(condition).clbits)
+            elif isinstance(instruction.operation, SwitchCaseOp):
+                update_from_expr(objects, expr.lift(instruction.operation.target))
+            elif isinstance(instruction.operation, Store):
+                update_from_expr(objects, instruction.operation.lvalue)
+                update_from_expr(objects, instruction.operation.rvalue)
+
+            # If we're counting this as adding to depth, do so.  If not, it still functions as a
+            # data synchronisation point between the objects (think "barrier"), so the depths still
+            # get updated to match the current max over the affected objects.
+            new_depth = max((obj_depths[obj] for obj in objects), default=0)
+            if filter_function(instruction):
+                new_depth += 1
+            for obj in objects:
+                obj_depths[obj] = new_depth
+        return max(obj_depths.values(), default=0)
 
     def width(self) -> int:
         """Return number of qubits plus clbits in circuit.
diff --git a/releasenotes/notes/fix-qc-depth-0q-cdcc9aa14e237e68.yaml b/releasenotes/notes/fix-qc-depth-0q-cdcc9aa14e237e68.yaml
new file mode 100644
index 000000000..a0744b3dd
--- /dev/null
+++ b/releasenotes/notes/fix-qc-depth-0q-cdcc9aa14e237e68.yaml
@@ -0,0 +1,8 @@
+---
+fixes:
+  - |
+    :meth:`.QuantumCircuit.depth` will now correctly handle operations that
+    do not have operands, such as :class:`.GlobalPhaseGate`.
+  - |
+    :meth:`.QuantumCircuit.depth` will now count the variables and clbits
+    used in real-time expressions as part of the depth calculation.


Test patch:
diff --git a/test/python/circuit/test_circuit_properties.py b/test/python/circuit/test_circuit_properties.py
index 481f2fe3c..d51dd0c75 100644
--- a/test/python/circuit/test_circuit_properties.py
+++ b/test/python/circuit/test_circuit_properties.py
@@ -17,7 +17,8 @@ import numpy as np
 
 from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, pulse
 from qiskit.circuit import Clbit
-from qiskit.circuit.library import RXGate, RYGate
+from qiskit.circuit.classical import expr, types
+from qiskit.circuit.library import RXGate, RYGate, GlobalPhaseGate
 from qiskit.circuit.exceptions import CircuitError
 from test import QiskitTestCase  # pylint: disable=wrong-import-order
 
@@ -638,6 +639,58 @@ class TestCircuitProperties(QiskitTestCase):
         circ.measure(1, 0)
         self.assertEqual(circ.depth(lambda x: circ.qubits[0] in x.qubits), 3)
 
+    def test_circuit_depth_0_operands(self):
+        """Test that the depth can be found even with zero-bit operands."""
+        qc = QuantumCircuit(2, 2)
+        qc.append(GlobalPhaseGate(0.0), [], [])
+        qc.append(GlobalPhaseGate(0.0), [], [])
+        qc.append(GlobalPhaseGate(0.0), [], [])
+        self.assertEqual(qc.depth(), 0)
+        qc.measure([0, 1], [0, 1])
+        self.assertEqual(qc.depth(), 1)
+
+    def test_circuit_depth_expr_condition(self):
+        """Test that circuit depth respects `Expr` conditions in `IfElseOp`."""
+        # Note that the "depth" of control-flow operations is not well defined, so the assertions
+        # here are quite weak.  We're mostly aiming to match legacy behaviour of `c_if` for cases
+        # where there's a single instruction within the conditional.
+        qc = QuantumCircuit(2, 2)
+        a = qc.add_input("a", types.Bool())
+        with qc.if_test(a):
+            qc.x(0)
+        with qc.if_test(expr.logic_and(a, qc.clbits[0])):
+            qc.x(1)
+        self.assertEqual(qc.depth(), 2)
+        qc.measure([0, 1], [0, 1])
+        self.assertEqual(qc.depth(), 3)
+
+    def test_circuit_depth_expr_store(self):
+        """Test that circuit depth respects `Store`."""
+        qc = QuantumCircuit(3, 3)
+        a = qc.add_input("a", types.Bool())
+        qc.h(0)
+        qc.cx(0, 1)
+        qc.measure([0, 1], [0, 1])
+        # Note that `Store` is a "directive", so doesn't increase the depth by default, but does
+        # cause qubits 0,1; clbits 0,1 and 'a' to all be depth 3 at this point.
+        qc.store(a, qc.clbits[0])
+        qc.store(a, expr.logic_and(a, qc.clbits[1]))
+        # ... so this use of 'a' should make it depth 4.
+        with qc.if_test(a):
+            qc.x(2)
+        self.assertEqual(qc.depth(), 4)
+
+    def test_circuit_depth_switch(self):
+        """Test that circuit depth respects the `target` of `SwitchCaseOp`."""
+        qc = QuantumCircuit(QuantumRegister(3, "q"), ClassicalRegister(3, "c"))
+        a = qc.add_input("a", types.Uint(3))
+
+        with qc.switch(expr.bit_and(a, qc.cregs[0])) as case:
+            with case(case.DEFAULT):
+                qc.x(0)
+        qc.measure(1, 0)
+        self.assertEqual(qc.depth(), 2)
+
     def test_circuit_size_empty(self):
         """Circuit.size should return 0 for an empty circuit."""
         size = 4