Skip to content

Commit b77a822

Browse files
authored
Add representation of box (#13869)
* Add representation of `box` This adds in the base `box` control-flow construction, with support for containing instructions and having a literal delay, like the `Delay` instruction. This supports basic output to OpenQASM 3, QPY and some rudimentary support in the text and mpl drawers. The transpiler largely handles things already, since control flow is handled generically in most places. Known issues: - We expect this to be able to accept stretches in its duration, just as `Delay` can, which will need a follow-up. - We expect `Box` to support "annotations" in a future release of Qiskit. - There is currently no way in OpenQASM 3 to represent a qubit that is idle during a `box` without inserting a magic instruction on it. - IBM backends don't claim support for `box` yet, so `transpile` against a backend will fail, though you can modify the `Target` to add the instruction manually. Add tests of box * Add QPY backwards-compatibility test * Add `BoxOp.body` getter and tests
1 parent d2f4861 commit b77a822

23 files changed

Lines changed: 917 additions & 25 deletions

File tree

crates/circuit/src/dag_circuit.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2317,6 +2317,7 @@ impl DAGCircuit {
23172317
let condition_op_check = imports::CONDITION_OP_CHECK.get_bound(py);
23182318
let switch_case_op_check = imports::SWITCH_CASE_OP_CHECK.get_bound(py);
23192319
let for_loop_op_check = imports::FOR_LOOP_OP_CHECK.get_bound(py);
2320+
let box_op_check = imports::BOX_OP_CHECK.get_bound(py);
23202321
let node_match = |n1: &NodeType, n2: &NodeType| -> PyResult<bool> {
23212322
match [n1, n2] {
23222323
[NodeType::Operation(inst1), NodeType::Operation(inst2)] => {
@@ -2373,6 +2374,10 @@ impl DAGCircuit {
23732374
for_loop_op_check
23742375
.call1((n1, n2, &self_bit_indices, &other_bit_indices))?
23752376
.extract()
2377+
} else if name == "box" {
2378+
box_op_check
2379+
.call1((n1, n2, &self_bit_indices, &other_bit_indices))?
2380+
.extract()
23762381
} else {
23772382
Err(PyRuntimeError::new_err(format!(
23782383
"unhandled control-flow operation: {}",

crates/circuit/src/imports.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ pub static SWITCH_CASE_OP_CHECK: ImportOnceCell =
110110
ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_switch_case_eq");
111111
pub static FOR_LOOP_OP_CHECK: ImportOnceCell =
112112
ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_for_loop_eq");
113+
pub static BOX_OP_CHECK: ImportOnceCell =
114+
ImportOnceCell::new("qiskit.dagcircuit.dagnode", "_box_eq");
113115
pub static UUID: ImportOnceCell = ImportOnceCell::new("uuid", "UUID");
114116
pub static BARRIER: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "Barrier");
115117
pub static DELAY: ImportOnceCell = ImportOnceCell::new("qiskit.circuit", "Delay");

qiskit/circuit/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,6 +1315,7 @@ def __array__(self, dtype=None, copy=None):
13151315

13161316
from .controlflow import (
13171317
ControlFlowOp,
1318+
BoxOp,
13181319
WhileLoopOp,
13191320
ForLoopOp,
13201321
IfElseOp,

qiskit/circuit/controlflow/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
from .continue_loop import ContinueLoopOp
1919
from .break_loop import BreakLoopOp
2020

21+
from .box import BoxOp
2122
from .if_else import IfElseOp
2223
from .while_loop import WhileLoopOp
2324
from .for_loop import ForLoopOp
2425
from .switch_case import SwitchCaseOp, CASE_DEFAULT
2526

2627

27-
CONTROL_FLOW_OP_NAMES = frozenset(("for_loop", "while_loop", "if_else", "switch_case"))
28+
CONTROL_FLOW_OP_NAMES = frozenset(("for_loop", "while_loop", "if_else", "switch_case", "box"))
2829
"""Set of the instruction names of Qiskit's known control-flow operations."""
2930

3031

@@ -53,5 +54,6 @@ def get_control_flow_name_mapping():
5354
"while_loop": WhileLoopOp,
5455
"for_loop": ForLoopOp,
5556
"switch_case": SwitchCaseOp,
57+
"box": BoxOp,
5658
}
5759
return name_mapping

qiskit/circuit/controlflow/box.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2025.
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
13+
"""Simple box basic block."""
14+
15+
from __future__ import annotations
16+
17+
import typing
18+
19+
from qiskit.circuit.exceptions import CircuitError
20+
from .control_flow import ControlFlowOp
21+
22+
if typing.TYPE_CHECKING:
23+
from qiskit.circuit import QuantumCircuit
24+
25+
26+
class BoxOp(ControlFlowOp):
27+
"""A scoped "box" of operations on a circuit that are treated atomically in the greater context.
28+
29+
A "box" is a control-flow construct that is entered unconditionally. The contents of the box
30+
behave somewhat as if the start and end of the box were barriers, except it is permissible to
31+
commute operations "all the way" through the box. The box is also an explicit scope for the
32+
purposes of variables, stretches and compiler passes.
33+
34+
Typically you create this by using the builder-interface form of :meth:`.QuantumCircuit.box`.
35+
"""
36+
37+
def __init__(
38+
self,
39+
body: QuantumCircuit,
40+
duration: None = None,
41+
unit: typing.Literal["dt", "s", "ms", "us", "ns", "ps"] = "dt",
42+
label: str | None = None,
43+
):
44+
"""
45+
Default constructor of :class:`BoxOp`.
46+
47+
Args:
48+
body: the circuit to use as the body of the box. This should explicit close over any
49+
:class:`.expr.Var` variables that must be incident from the outer circuit. The
50+
expected number of qubit and clbits for the resulting instruction are inferred from
51+
the number in the circuit, even if they are idle.
52+
duration: an optional duration for the box as a whole.
53+
unit: the unit of the ``duration``.
54+
label: an optional string label for the instruction.
55+
"""
56+
super().__init__("box", body.num_qubits, body.num_clbits, [body], label=label)
57+
self.duration = duration
58+
self.unit = unit
59+
60+
@property
61+
def params(self):
62+
return self._params
63+
64+
@params.setter
65+
def params(self, parameters):
66+
# pylint: disable=cyclic-import
67+
from qiskit.circuit import QuantumCircuit
68+
69+
(body,) = parameters
70+
71+
if not isinstance(body, QuantumCircuit):
72+
raise CircuitError(
73+
"BoxOp expects a body parameter of type "
74+
f"QuantumCircuit, but received {type(body)}."
75+
)
76+
77+
if body.num_qubits != self.num_qubits or body.num_clbits != self.num_clbits:
78+
raise CircuitError(
79+
"Attempted to assign a body parameter with a num_qubits or "
80+
"num_clbits different than that of the BoxOp. "
81+
f"BoxOp num_qubits/clbits: {self.num_qubits}/{self.num_clbits} "
82+
f"Supplied body num_qubits/clbits: {body.num_qubits}/{body.num_clbits}."
83+
)
84+
85+
self._params = [body]
86+
87+
@property
88+
def body(self):
89+
"""The ``body`` :class:`.QuantumCircuit` of the operation.
90+
91+
This is the same as object returned as the sole entry in :meth:`params` and :meth:`blocks`.
92+
"""
93+
# Not settable via this property; the only meaningful way to replace a body is via
94+
# larger `QuantumCircuit` methods, or using `replace_blocks`.
95+
return self.params[0]
96+
97+
@property
98+
def blocks(self):
99+
return (self._params[0],)
100+
101+
def replace_blocks(self, blocks):
102+
(body,) = blocks
103+
return BoxOp(body, duration=self.duration, unit=self.unit, label=self.label)
104+
105+
def __eq__(self, other):
106+
return (
107+
isinstance(other, BoxOp)
108+
and self.duration == other.duration
109+
and self.unit == other.unit
110+
and super().__eq__(other)
111+
)
112+
113+
114+
class BoxContext:
115+
"""Context-manager that powers :meth:`.QuantumCircuit.box`.
116+
117+
This is not part of the public interface, and should not be instantiated by users.
118+
"""
119+
120+
__slots__ = ("_circuit", "_duration", "_unit", "_label")
121+
122+
def __init__(
123+
self,
124+
circuit: QuantumCircuit,
125+
*,
126+
duration: None = None,
127+
unit: typing.Literal["dt", "s", "ms", "us", "ns", "ps"] = "dt",
128+
label: str | None = None,
129+
):
130+
"""
131+
Args:
132+
circuit: the outermost scope of the circuit under construction.
133+
duration: the final duration of the box.
134+
unit: the unit of ``duration``.
135+
label: an optional label for the box.
136+
"""
137+
self._circuit = circuit
138+
self._duration = duration
139+
self._unit = unit
140+
self._label = label
141+
142+
def __enter__(self):
143+
# For a box to have the semantics of internal qubit alignment with a resolvable duration, we
144+
# can't allow conditional jumps to exit it. Technically an unconditional `break` or
145+
# `continue` could work, but we're not getting into that.
146+
self._circuit._push_scope(allow_jumps=False)
147+
148+
def __exit__(self, exc_type, exc_val, exc_tb):
149+
if exc_type is not None:
150+
# If we're leaving the context manager because an exception was raised, there's nothing
151+
# to do except restore the circuit state.
152+
self._circuit._pop_scope()
153+
return False
154+
scope = self._circuit._pop_scope()
155+
# Boxes do not need to pass any further resources in, because there's no jumps out of a
156+
# `box` permitted.
157+
body = scope.build(scope.qubits(), scope.clbits())
158+
self._circuit.append(
159+
BoxOp(body, duration=self._duration, unit=self._unit, label=self._label),
160+
body.qubits,
161+
body.clbits,
162+
)
163+
return False

qiskit/circuit/quantumcircuit.py

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from .controlflow import ControlFlowOp, _builder_utils
5252
from .controlflow.builder import CircuitScopeInterface, ControlFlowBuilderBlock
5353
from .controlflow.break_loop import BreakLoopOp, BreakLoopPlaceholder
54+
from .controlflow.box import BoxOp, BoxContext
5455
from .controlflow.continue_loop import ContinueLoopOp, ContinueLoopPlaceholder
5556
from .controlflow.for_loop import ForLoopOp, ForLoopContext
5657
from .controlflow.if_else import IfElseOp, IfContext
@@ -737,13 +738,14 @@ class QuantumCircuit:
737738
============================== ================================================================
738739
:class:`QuantumCircuit` method Control-flow instruction
739740
============================== ================================================================
740-
:meth:`if_test` :class:`.IfElseOp` with only a ``True`` body.
741-
:meth:`if_else` :class:`.IfElseOp` with both ``True`` and ``False`` bodies.
742-
:meth:`while_loop` :class:`.WhileLoopOp`.
743-
:meth:`switch` :class:`.SwitchCaseOp`.
744-
:meth:`for_loop` :class:`.ForLoopOp`.
745-
:meth:`break_loop` :class:`.BreakLoopOp`.
746-
:meth:`continue_loop` :class:`.ContinueLoopOp`.
741+
:meth:`if_test` :class:`.IfElseOp` with only a ``True`` body
742+
:meth:`if_else` :class:`.IfElseOp` with both ``True`` and ``False`` bodies
743+
:meth:`while_loop` :class:`.WhileLoopOp`
744+
:meth:`switch` :class:`.SwitchCaseOp`
745+
:meth:`for_loop` :class:`.ForLoopOp`
746+
:meth:`box` :class:`.BoxOp`
747+
:meth:`break_loop` :class:`.BreakLoopOp`
748+
:meth:`continue_loop` :class:`.ContinueLoopOp`
747749
============================== ================================================================
748750
749751
:class:`QuantumCircuit` has corresponding methods for all of the control-flow operations that
@@ -769,6 +771,7 @@ class QuantumCircuit:
769771
..
770772
TODO: expand the examples of the builder interface.
771773
774+
.. automethod:: box
772775
.. automethod:: break_loop
773776
.. automethod:: continue_loop
774777
.. automethod:: for_loop
@@ -6040,6 +6043,107 @@ def _pop_previous_instruction_in_scope(self) -> CircuitInstruction:
60406043
instruction = self._data.pop()
60416044
return instruction
60426045

6046+
def box(
6047+
self,
6048+
# Forbidding passing `body` by keyword is in anticipation of the constructor expanding to
6049+
# allow `annotations` to be passed as the positional argument in the context-manager form.
6050+
body: QuantumCircuit | None = None,
6051+
/,
6052+
qubits: Sequence[QubitSpecifier] | None = None,
6053+
clbits: Sequence[ClbitSpecifier] | None = None,
6054+
*,
6055+
label: str | None = None,
6056+
duration: None = None,
6057+
unit: Literal["dt", "s", "ms", "us", "ns", "ps"] = "dt",
6058+
):
6059+
"""Create a ``box`` of operations on this circuit that are treated atomically in the greater
6060+
context.
6061+
6062+
A "box" is a control-flow construct that is entered unconditionally. The contents of the
6063+
box behave somewhat as if the start and end of the box were barriers (see :meth:`barrier`),
6064+
except it is permissible to commute operations "all the way" through the box. The box is
6065+
also an explicit scope for the purposes of variables, stretches and compiler passes.
6066+
6067+
There are two forms for calling this function:
6068+
6069+
* Pass a :class:`QuantumCircuit` positionally, and the ``qubits`` and ``clbits`` it acts
6070+
on. In this form, a :class:`.BoxOp` is immediately created and appended using the circuit
6071+
as the body.
6072+
6073+
* Use in a ``with`` statement with no ``body``, ``qubits`` or ``clbits``. This is the
6074+
"builder-interface form", where you then use other :class:`QuantumCircuit` methods within
6075+
the Python ``with`` scope to add instructions to the ``box``. This is the preferred form,
6076+
and much less error prone.
6077+
6078+
Examples:
6079+
6080+
Using the builder interface to add two boxes in sequence. The two boxes in this circuit
6081+
can execute concurrently, and the second explicitly inserts a data-flow dependency on
6082+
qubit 8 for the duration of the box, even though the qubit is idle.
6083+
6084+
.. code-block:: python
6085+
6086+
from qiskit.circuit import QuantumCircuit
6087+
6088+
qc = QuantumCircuit(9)
6089+
with qc.box():
6090+
qc.cz(0, 1)
6091+
qc.cz(2, 3)
6092+
with qc.box():
6093+
qc.cz(4, 5)
6094+
qc.cz(6, 7)
6095+
qc.noop(8)
6096+
6097+
Using the explicit construction of box. This creates the same circuit as above, and
6098+
should give an indication why the previous form is preferred for interactive use.
6099+
6100+
.. code-block:: python
6101+
6102+
from qiskit.circuit import QuantumCircuit, BoxOp
6103+
6104+
body_0 = QuantumCircuit(4)
6105+
body_0.cz(0, 1)
6106+
body_0.cz(2, 3)
6107+
6108+
# Note that the qubit indices inside a body related only to the body. The
6109+
# association with qubits in the containing circuit is made by the ``qubits``
6110+
# argument to `QuantumCircuit.box`.
6111+
body_1 = QuantumCircuit(5)
6112+
body_1.cz(0, 1)
6113+
body_1.cz(2, 3)
6114+
6115+
qc = QuantumCircuit(9)
6116+
qc.box(body_0, [0, 1, 2, 3], [])
6117+
qc.box(body_1, [4, 5, 6, 7, 8], [])
6118+
6119+
Args:
6120+
body: if given, the :class:`QuantumCircuit` to use as the box's body in the explicit
6121+
construction. Not given in the context-manager form.
6122+
qubits: the qubits to apply the :class:`.BoxOp` to, in the explicit form.
6123+
clbits: the qubits to apply the :class:`.BoxOp` to, in the explicit form.
6124+
label: an optional string label for the instruction.
6125+
duration: an optional explicit duration for the :class:`.BoxOp`. Scheduling passes are
6126+
constrained to schedule the contained scope to match a given duration, including
6127+
delay insertion if required.
6128+
unit: the unit of the ``duration``.
6129+
"""
6130+
if isinstance(body, QuantumCircuit):
6131+
# Explicit-body form.
6132+
if qubits is None or clbits is None:
6133+
raise CircuitError("When using 'box' with a body, you must pass qubits and clbits.")
6134+
return self.append(
6135+
BoxOp(body, duration=duration, unit=unit, label=label),
6136+
qubits,
6137+
clbits,
6138+
copy=False,
6139+
)
6140+
# Context-manager form.
6141+
if qubits is not None or clbits is not None:
6142+
raise CircuitError(
6143+
"When using 'box' as a context manager, you cannot pass qubits or clbits."
6144+
)
6145+
return BoxContext(self, duration=duration, unit=unit, label=label)
6146+
60436147
@typing.overload
60446148
def while_loop(
60456149
self,

qiskit/dagcircuit/dagnode.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import qiskit._accelerate.circuit
2121
from qiskit.circuit import (
22+
BoxOp,
2223
Clbit,
2324
ClassicalRegister,
2425
IfElseOp,
@@ -170,11 +171,18 @@ def _for_loop_eq(node1, node2, bit_indices1, bit_indices2):
170171
)
171172

172173

174+
def _box_eq(node1, node2, bit_indices1, bit_indices2):
175+
return node1.op.duration == node2.op.duration and _circuit_to_dag(
176+
node1.op.blocks[0], node1.qargs, node1.cargs, bit_indices1
177+
) == _circuit_to_dag(node2.op.blocks[0], node2.qargs, node2.cargs, bit_indices2)
178+
179+
173180
_SEMANTIC_EQ_CONTROL_FLOW = {
174181
IfElseOp: _condition_op_eq,
175182
WhileLoopOp: _condition_op_eq,
176183
SwitchCaseOp: _switch_case_eq,
177184
ForLoopOp: _for_loop_eq,
185+
BoxOp: _box_eq,
178186
}
179187

180188
_SEMANTIC_EQ_SYMMETRIC = frozenset({"barrier", "swap", "break_loop", "continue_loop"})

0 commit comments

Comments
 (0)