Skip to content

Commit 823dd27

Browse files
committed
Merge branch 'main' of github.com:Qiskit/qiskit into circuit-stretch
2 parents 2223d12 + b77a822 commit 823dd27

23 files changed

Lines changed: 920 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
@@ -2366,6 +2366,7 @@ impl DAGCircuit {
23662366
let condition_op_check = imports::CONDITION_OP_CHECK.get_bound(py);
23672367
let switch_case_op_check = imports::SWITCH_CASE_OP_CHECK.get_bound(py);
23682368
let for_loop_op_check = imports::FOR_LOOP_OP_CHECK.get_bound(py);
2369+
let box_op_check = imports::BOX_OP_CHECK.get_bound(py);
23692370
let node_match = |n1: &NodeType, n2: &NodeType| -> PyResult<bool> {
23702371
match [n1, n2] {
23712372
[NodeType::Operation(inst1), NodeType::Operation(inst2)] => {
@@ -2422,6 +2423,10 @@ impl DAGCircuit {
24222423
for_loop_op_check
24232424
.call1((n1, n2, &self_bit_indices, &other_bit_indices))?
24242425
.extract()
2426+
} else if name == "box" {
2427+
box_op_check
2428+
.call1((n1, n2, &self_bit_indices, &other_bit_indices))?
2429+
.extract()
24252430
} else {
24262431
Err(PyRuntimeError::new_err(format!(
24272432
"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
@@ -52,6 +52,7 @@
5252
from .controlflow import ControlFlowOp, _builder_utils
5353
from .controlflow.builder import CircuitScopeInterface, ControlFlowBuilderBlock
5454
from .controlflow.break_loop import BreakLoopOp, BreakLoopPlaceholder
55+
from .controlflow.box import BoxOp, BoxContext
5556
from .controlflow.continue_loop import ContinueLoopOp, ContinueLoopPlaceholder
5657
from .controlflow.for_loop import ForLoopOp, ForLoopContext
5758
from .controlflow.if_else import IfElseOp, IfContext
@@ -738,13 +739,14 @@ class QuantumCircuit:
738739
============================== ================================================================
739740
:class:`QuantumCircuit` method Control-flow instruction
740741
============================== ================================================================
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:`break_loop` :class:`.BreakLoopOp`.
747-
:meth:`continue_loop` :class:`.ContinueLoopOp`.
742+
:meth:`if_test` :class:`.IfElseOp` with only a ``True`` body
743+
:meth:`if_else` :class:`.IfElseOp` with both ``True`` and ``False`` bodies
744+
:meth:`while_loop` :class:`.WhileLoopOp`
745+
:meth:`switch` :class:`.SwitchCaseOp`
746+
:meth:`for_loop` :class:`.ForLoopOp`
747+
:meth:`box` :class:`.BoxOp`
748+
:meth:`break_loop` :class:`.BreakLoopOp`
749+
:meth:`continue_loop` :class:`.ContinueLoopOp`
748750
============================== ================================================================
749751
750752
:class:`QuantumCircuit` has corresponding methods for all of the control-flow operations that
@@ -770,6 +772,7 @@ class QuantumCircuit:
770772
..
771773
TODO: expand the examples of the builder interface.
772774
775+
.. automethod:: box
773776
.. automethod:: break_loop
774777
.. automethod:: continue_loop
775778
.. automethod:: for_loop
@@ -6342,6 +6345,107 @@ def _pop_previous_instruction_in_scope(self) -> CircuitInstruction:
63426345
instruction = self._data.pop()
63436346
return instruction
63446347

6348+
def box(
6349+
self,
6350+
# Forbidding passing `body` by keyword is in anticipation of the constructor expanding to
6351+
# allow `annotations` to be passed as the positional argument in the context-manager form.
6352+
body: QuantumCircuit | None = None,
6353+
/,
6354+
qubits: Sequence[QubitSpecifier] | None = None,
6355+
clbits: Sequence[ClbitSpecifier] | None = None,
6356+
*,
6357+
label: str | None = None,
6358+
duration: None = None,
6359+
unit: Literal["dt", "s", "ms", "us", "ns", "ps"] = "dt",
6360+
):
6361+
"""Create a ``box`` of operations on this circuit that are treated atomically in the greater
6362+
context.
6363+
6364+
A "box" is a control-flow construct that is entered unconditionally. The contents of the
6365+
box behave somewhat as if the start and end of the box were barriers (see :meth:`barrier`),
6366+
except it is permissible to commute operations "all the way" through the box. The box is
6367+
also an explicit scope for the purposes of variables, stretches and compiler passes.
6368+
6369+
There are two forms for calling this function:
6370+
6371+
* Pass a :class:`QuantumCircuit` positionally, and the ``qubits`` and ``clbits`` it acts
6372+
on. In this form, a :class:`.BoxOp` is immediately created and appended using the circuit
6373+
as the body.
6374+
6375+
* Use in a ``with`` statement with no ``body``, ``qubits`` or ``clbits``. This is the
6376+
"builder-interface form", where you then use other :class:`QuantumCircuit` methods within
6377+
the Python ``with`` scope to add instructions to the ``box``. This is the preferred form,
6378+
and much less error prone.
6379+
6380+
Examples:
6381+
6382+
Using the builder interface to add two boxes in sequence. The two boxes in this circuit
6383+
can execute concurrently, and the second explicitly inserts a data-flow dependency on
6384+
qubit 8 for the duration of the box, even though the qubit is idle.
6385+
6386+
.. code-block:: python
6387+
6388+
from qiskit.circuit import QuantumCircuit
6389+
6390+
qc = QuantumCircuit(9)
6391+
with qc.box():
6392+
qc.cz(0, 1)
6393+
qc.cz(2, 3)
6394+
with qc.box():
6395+
qc.cz(4, 5)
6396+
qc.cz(6, 7)
6397+
qc.noop(8)
6398+
6399+
Using the explicit construction of box. This creates the same circuit as above, and
6400+
should give an indication why the previous form is preferred for interactive use.
6401+
6402+
.. code-block:: python
6403+
6404+
from qiskit.circuit import QuantumCircuit, BoxOp
6405+
6406+
body_0 = QuantumCircuit(4)
6407+
body_0.cz(0, 1)
6408+
body_0.cz(2, 3)
6409+
6410+
# Note that the qubit indices inside a body related only to the body. The
6411+
# association with qubits in the containing circuit is made by the ``qubits``
6412+
# argument to `QuantumCircuit.box`.
6413+
body_1 = QuantumCircuit(5)
6414+
body_1.cz(0, 1)
6415+
body_1.cz(2, 3)
6416+
6417+
qc = QuantumCircuit(9)
6418+
qc.box(body_0, [0, 1, 2, 3], [])
6419+
qc.box(body_1, [4, 5, 6, 7, 8], [])
6420+
6421+
Args:
6422+
body: if given, the :class:`QuantumCircuit` to use as the box's body in the explicit
6423+
construction. Not given in the context-manager form.
6424+
qubits: the qubits to apply the :class:`.BoxOp` to, in the explicit form.
6425+
clbits: the qubits to apply the :class:`.BoxOp` to, in the explicit form.
6426+
label: an optional string label for the instruction.
6427+
duration: an optional explicit duration for the :class:`.BoxOp`. Scheduling passes are
6428+
constrained to schedule the contained scope to match a given duration, including
6429+
delay insertion if required.
6430+
unit: the unit of the ``duration``.
6431+
"""
6432+
if isinstance(body, QuantumCircuit):
6433+
# Explicit-body form.
6434+
if qubits is None or clbits is None:
6435+
raise CircuitError("When using 'box' with a body, you must pass qubits and clbits.")
6436+
return self.append(
6437+
BoxOp(body, duration=duration, unit=unit, label=label),
6438+
qubits,
6439+
clbits,
6440+
copy=False,
6441+
)
6442+
# Context-manager form.
6443+
if qubits is not None or clbits is not None:
6444+
raise CircuitError(
6445+
"When using 'box' as a context manager, you cannot pass qubits or clbits."
6446+
)
6447+
return BoxContext(self, duration=duration, unit=unit, label=label)
6448+
63456449
@typing.overload
63466450
def while_loop(
63476451
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)