Skip to content

Commit e17af6a

Browse files
authored
[Stretch] Support stretch and duration expressions for Delay instruction. (#13853)
* WIP * Add try_const to lift. * Try multiple singletons, new one for const. * Revert "Try multiple singletons, new one for const." This reverts commit e2b3221. * Remove Bool singleton test. * Add const handling for stores, fix test bugs. * Fix formatting. * Remove Duration and Stretch for now. * Cleanup, fix const bug in index. * Fix ordering issue for types with differing const-ness. Types that have some natural order no longer have an ordering when one of them is strictly greater but has an incompatible const-ness (i.e. when the greater type is const but the other type is not). * Fix QPY serialization. We need to reject types with const=True in QPY until it supports them. For now, I've also made the Index and shift operator constructors lift their RHS to the same const-ness as the target to make it less likely that existing users of expr run into issues when serializing to older QPY versions. * Make expr.Lift default to non-const. This is probably a better default in general, since we don't really have much use for const except for timing stuff. * Revert to old test_expr_constructors.py. * Make binary_logical lift independent again. Since we're going for using a Cast node when const-ness differs, this will be fine. * Update tests, handle a few edge cases. * Fix docstring. * Remove now redundant arg from tests. * Add const testing for ordering. * Add const tests for shifts. * Add release note. * Add const store tests. * Address lint, minor cleanup. * Add Float type to classical expressions. * Allow DANGEROUS conversion from Float to Bool. I wasn't going to have this, but since we have DANGEROUS Float => Int, and we have Int => Bool, I think this makes the most sense. * Test Float ordering. * Improve error messages for using Float with logical operators. * Float tests for constructors. * Add release note. * Add Duration and Stretch classical types. A Stretch can always represent a Duration (it's just an expression without any unresolved stretch variables, in this case), so we allow implicit conversion from Duration => Stretch. The reason for a separate Duration type is to support things like Duration / Duration => Float. This is not valid for stretches in OpenQASM (to my knowledge). * Add Duration type to qiskit.circuit. Also adds support to expr.lift to create a value expression of type types.Duration from an instance of qiskit.circuit.Duration. * Block Stretch from use in binary relations. * Add type ordering tests for Duration and Stretch. Also improves testing for other types. * Test expr constructors for Duration and Stretch. * Fix lint. * Implement operators +, -, *, /. * Add expression tests for arithmetic ops. * Implement stretch support for circuit. * Initial support for writing QASM3. * Support duration and stretch expressions in Delay. * Reject const vars in add_var and add_input. Also removes the assumption that a const-type can never be an l-value in favor of just restricting l-values with const types from being added to circuits for now. We will (in a separate PR) add support for adding stretch variables to circuits, which are const. However, we may track those differently, or at least not report them as variable when users query the circuit for variables. * Implement QPY support for const-typed expressions. * Remove invalid test. This one I'd added thinking I ought to block store from using a const var target. But since I figured it's better to just restrict adding vars to the circuit that are const (and leave the decision of whether or not a const var can be an l-value till later), this test no longer makes sense. * Update QPY version 14 desc. * Fix lint. * Add serialization testing. * Test pre-v14 QPY rejects const-typed exprs. * QASM export for floats. * QPY support for floats. * Fix lint. * Settle on Duration circuit core type. * QPY serialization for durations and stretches. * Add QPY testing. I can't really test Stretch yet since we can't add them to circuits until a later PR. * QASM support for stretch and duration. The best I can do to test these right now (before Delay is made to accept timing expressions) is to use them in comparison operations (will be in a follow up commit). * Fix lint. * Add arithmetic operators to QASM. * QPY testing for arithmetic operations. * QASM testing for arithmetic operations. * Update tests for blocked const vars. We decided to punt on the idea of assigning const-typed variables, so we don't support declaring stretch expressions via add_var or the 'declarations' argument to QuantumCircuit. We also don't allow const 'inputs'. * Only test declare stretch QASM; fix lint. * Special case for stretch in add_uninitialized_var. At the moment, we track stretches as variables, but these will eventually make their way here during circuit copy etc., so we need to support them even though they're const. * Remove outdated docstring comment. * Remove outdated comment. * Support Delay duration expression in transpiler. * Improve docstring for Delay. * Error when stretch is used in a circuit during scheduling. * Don't use match since we still support Python 3.9. * Block const stores. * Fix enum match. * Fix bad merge. * Add a few more serialization tests. * Fix lint. * Add additional testing for Duration expression eval. * Fix missing round when converting expr to 'dt'. * Revert visitors.py. * Address review comments. * Improve type docs. * Revert QPY, since the old format can support constexprs. By making const-ness a property of expressions, we don't need any special serialization in QPY. That's because we assume that all `Var` expressions are non-const, and all `Value` expressions are const. And the const-ness of any expression is defined by the const-ness of its operands, e.g. when QPY reconstructs a binary operand, the constructed expression's `const` attribute gets set to `True` if both of the operands are `const`, which ultimately flows bottom-up from the `Var` and `Value` leaf nodes. * Move const-ness from Type to Expr. * Revert QPY testing, no longer needed. * Add explicit validation of const expr. * Revert stuff I didn't need to touch. * Update release note. * A few finishing touches. * Fix-up after merge. * Fix-ups after merge. * Fix lint. * Fix comment and release note. * Fixes after merge. * Fix test. * Fix lint. * Special-case Var const-ness for Stretch type. This feels like a bit of a hack, but the idea is to override a Var to report itself as a constant expression only in the case that its type is Stretch. I would argue that it's not quite as hack-y as it appears, since Stretch is probably the only kind of constant we'll ever allow in Qiskit without an in-line initializer. If ever we want to support declaring other kinds of constants (e.g. Uint), we'll probably want to introduce a `expr.Const` type whose constructor requires a const initializer expression. * Address review comments. * Update release note. * Update docs. * Add release notes and doc link. * Address review comments. * Remove Stretch type. * Remove a few more mentions of the binned stretch type. * Add docstring for Duration. * Remove Stretch type stuff. * WIP * Track stretch variables throughout circuits. * Update QASM exporter. * Fix existing tests and found bugs. * Fix format. * Simplify structural eq visitor. * Add num_identifiers. * Support QPY. * Implement QASM visit for stretch expr. * Track stretches through DAGCircuit. * Add visit_stretch to classical resource map. * Fix lint. * Some merge fixes. * Address review comments. * Remove unused import. * Represent stretch with StretchDeclaration in QASM AST. Previously, we used StretchType within a ClassicalDeclaration, but this wasn't the best fit. * Remove visitor short circuit. * Address review comments. * Address review comments. * Support division by Uint. * Add release note. * Fix lint. * Add test for QASM example. * Add missing DAG stretch plumbing. * Fix test. * Fix QPY serialization bug. * Fix lint. * Add qpy test. * Support remapping for stretch variables in compose. * Fix qpy compat test. * Add negative test for QPY stretch expr. * Add more circuit testing. * Fix tests after merge. * Add control flow builder tests. * Add circuit equality testing for stretch. * Refer to 'stretches' instead of 'stretch variables'. * Remove | None for Stretch.name * Address review comments. * Update QPY desc. * Fix num_identifiers. * Fix merge for control flow tests. * Address review comments. * Fix docstrings. * Use Union instead of | for QC delay. Python 3.9 might not define the | operator for ParameterValueType since it's an alias. * Update tests for deprecation (sorry!) * Trigger CI.
1 parent 246f5f3 commit e17af6a

12 files changed

Lines changed: 702 additions & 55 deletions

File tree

crates/circuit/src/operations.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,13 +297,14 @@ pub enum DelayUnit {
297297
MS,
298298
S,
299299
DT,
300+
EXPR,
300301
}
301302

302303
unsafe impl ::bytemuck::CheckedBitPattern for DelayUnit {
303304
type Bits = u8;
304305

305306
fn is_valid_bit_pattern(bits: &Self::Bits) -> bool {
306-
*bits < 6
307+
*bits < 7
307308
}
308309
}
309310
unsafe impl ::bytemuck::NoUninit for DelayUnit {}
@@ -320,6 +321,7 @@ impl fmt::Display for DelayUnit {
320321
DelayUnit::MS => "ms",
321322
DelayUnit::S => "s",
322323
DelayUnit::DT => "dt",
324+
DelayUnit::EXPR => "expr",
323325
}
324326
)
325327
}
@@ -335,6 +337,7 @@ impl<'py> FromPyObject<'py> for DelayUnit {
335337
"ms" => DelayUnit::MS,
336338
"s" => DelayUnit::S,
337339
"dt" => DelayUnit::DT,
340+
"expr" => DelayUnit::EXPR,
338341
unknown_unit => {
339342
return Err(PyValueError::new_err(format!(
340343
"Unit '{}' is invalid.",

qiskit/circuit/controlflow/builder.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,11 +540,15 @@ def remove_stretch(self, stretch: expr.Stretch):
540540
self._stretches_local.pop(stretch.name)
541541

542542
def get_var(self, name: str):
543+
if name in self._stretches_local:
544+
return None
543545
if (out := self._vars_local.get(name)) is not None:
544546
return out
545547
return self._parent.get_var(name)
546548

547549
def get_stretch(self, name: str):
550+
if name in self._vars_local:
551+
return None
548552
if (out := self._stretches_local.get(name)) is not None:
549553
return out
550554
return self._parent.get_stretch(name)

qiskit/circuit/delay.py

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
Delay instruction (for circuit module).
1515
"""
1616
import numpy as np
17+
18+
from qiskit.circuit.classical import expr, types
1719
from qiskit.circuit.exceptions import CircuitError
1820
from qiskit.circuit.instruction import Instruction
1921
from qiskit.circuit.gate import Gate
@@ -28,13 +30,35 @@ class Delay(Instruction):
2830

2931
_standard_instruction_type = StandardInstructionType.Delay
3032

31-
def __init__(self, duration, unit="dt"):
33+
def __init__(self, duration, unit=None):
3234
"""
3335
Args:
34-
duration: the length of time of the duration. Given in units of ``unit``.
35-
unit: the unit of the duration. Must be ``"dt"`` or an SI-prefixed seconds unit.
36+
duration: the length of time of the duration. If this is an
37+
:class:`~.expr.Expr`, it must be a constant expression of type
38+
:class:`~.types.Duration` and the ``unit`` parameter should be
39+
omitted (or MUST be "expr" if it is specified).
40+
unit: the unit of the duration, if ``duration`` is a numeric
41+
value. Must be ``"dt"``, an SI-prefixed seconds unit, or "expr".
42+
43+
Raises:
44+
CircuitError: A ``duration`` expression was specified with a resolved
45+
type that is not timing-based, or the ``unit`` was improperly specified.
3646
"""
37-
if unit not in {"s", "ms", "us", "ns", "ps", "dt"}:
47+
if isinstance(duration, expr.Expr):
48+
if unit is not None and unit != "expr":
49+
raise CircuitError(
50+
"Argument 'unit' must not be specified for a duration expression."
51+
)
52+
if duration.type.kind is not types.Duration:
53+
raise CircuitError(
54+
f"Expression of type '{duration.type}' is not valid for 'duration'."
55+
)
56+
if not duration.const:
57+
raise CircuitError("Duration expressions must be constant.")
58+
unit = "expr"
59+
elif unit is None:
60+
unit = "dt"
61+
elif unit not in {"s", "ms", "us", "ns", "ps", "dt"}:
3862
raise CircuitError(f"Unknown unit {unit} is specified.")
3963
# Double underscore to differentiate from the private attribute in
4064
# `Instruction`. This can be changed to `_unit` in 2.0 after we
@@ -88,8 +112,9 @@ def __repr__(self):
88112
"""Return the official string representing the delay."""
89113
return f"{self.__class__.__name__}(duration={self.params[0]}[unit={self.unit}])"
90114

115+
# pylint: disable=too-many-return-statements
91116
def validate_parameter(self, parameter):
92-
"""Delay parameter (i.e. duration) must be int, float or ParameterExpression."""
117+
"""Delay parameter (i.e. duration) must be Expr, int, float or ParameterExpression."""
93118
if isinstance(parameter, int):
94119
if parameter < 0:
95120
raise CircuitError(
@@ -107,6 +132,12 @@ def validate_parameter(self, parameter):
107132
raise CircuitError("Integer duration is expected for 'dt' unit.")
108133
return parameter_int
109134
return parameter
135+
elif isinstance(parameter, expr.Expr):
136+
if parameter.type.kind is not types.Duration:
137+
raise CircuitError(f"Expression duration of type '{parameter.type}' is not valid.")
138+
if not parameter.const:
139+
raise CircuitError("Duration expressions must be constant.")
140+
return parameter
110141
elif isinstance(parameter, ParameterExpression):
111142
if len(parameter.parameters) > 0:
112143
return parameter # expression has free parameters, we cannot validate it

qiskit/circuit/quantumcircuit.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4555,17 +4555,20 @@ def barrier(self, *qargs: QubitSpecifier, label=None) -> InstructionSet:
45554555

45564556
def delay(
45574557
self,
4558-
duration: ParameterValueType,
4558+
duration: Union[ParameterValueType, expr.Expr],
45594559
qarg: QubitSpecifier | None = None,
4560-
unit: str = "dt",
4560+
unit: str | None = None,
45614561
) -> InstructionSet:
45624562
"""Apply :class:`~.circuit.Delay`. If qarg is ``None``, applies to all qubits.
45634563
When applying to multiple qubits, delays with the same duration will be created.
45644564
45654565
Args:
4566-
duration (int or float or ParameterExpression): duration of the delay.
4566+
duration (Object):
4567+
duration of the delay. If this is an :class:`~.expr.Expr`, it must be
4568+
a constant expression of type :class:`~.types.Duration`.
45674569
qarg (Object): qubit argument to apply this delay.
4568-
unit (str): unit of the duration. Supported units: ``'s'``, ``'ms'``, ``'us'``,
4570+
unit (str | None): unit of the duration, unless ``duration`` is an :class:`~.expr.Expr`
4571+
in which case it must not be specified. Supported units: ``'s'``, ``'ms'``, ``'us'``,
45694572
``'ns'``, ``'ps'``, and ``'dt'``. Default is ``'dt'``, i.e. integer time unit
45704573
depending on the target backend.
45714574

qiskit/qasm3/exporter.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,8 @@ def build_duration(self, duration, unit) -> ast.Expression | None:
11511151
"""Build the expression of a given duration (if not ``None``)."""
11521152
if duration is None:
11531153
return None
1154+
if unit == "expr":
1155+
return self.build_expression(duration)
11541156
if unit == "ps":
11551157
return ast.DurationLiteral(1000 * duration, ast.DurationUnit.NANOSECOND)
11561158
unit_map = {

qiskit/transpiler/passes/scheduling/padding/base_padding.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,9 @@ def _pre_runhook(self, dag: DAGCircuit):
210210
f"The input circuit {dag.name} is not scheduled. Call one of scheduling passes "
211211
f"before running the {self.__class__.__name__} pass."
212212
)
213+
if self.property_set["time_unit"] == "stretch":
214+
# This should have already been raised during scheduling, but just in case.
215+
raise TranspilerError("Scheduling cannot run on circuits with stretch durations.")
213216
for qarg, _ in enumerate(dag.qubits):
214217
if not self.__delay_supported(qarg):
215218
logger.debug(

qiskit/transpiler/passes/scheduling/scheduling/alap.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ def run(self, dag):
3939
"""
4040
if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None:
4141
raise TranspilerError("ALAP schedule runs on physical circuits only")
42+
if self.property_set["time_unit"] == "stretch":
43+
raise TranspilerError("Scheduling cannot run on circuits with stretch durations.")
4244

4345
clbit_write_latency = self.property_set.get("clbit_write_latency", 0)
4446

qiskit/transpiler/passes/scheduling/scheduling/asap.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ def run(self, dag):
3939
"""
4040
if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None:
4141
raise TranspilerError("ASAP schedule runs on physical circuits only")
42+
if self.property_set["time_unit"] == "stretch":
43+
raise TranspilerError("Scheduling cannot run on circuits with stretch durations.")
4244

4345
clbit_write_latency = self.property_set.get("clbit_write_latency", 0)
4446

qiskit/transpiler/passes/scheduling/time_unit_conversion.py

Lines changed: 120 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
"""Unify time unit in circuit for scheduling and following passes."""
1414
from typing import Set
1515

16-
from qiskit.circuit import Delay
16+
from qiskit.circuit import Delay, Duration
17+
from qiskit.circuit.classical import expr
18+
from qiskit.circuit.duration import duration_in_dt
1719
from qiskit.dagcircuit import DAGCircuit
1820
from qiskit.transpiler.basepasses import TransformationPass
1921
from qiskit.transpiler.exceptions import TranspilerError
2022
from qiskit.transpiler.instruction_durations import InstructionDurations
2123
from qiskit.transpiler.target import Target
24+
from qiskit.utils import apply_prefix
2225

2326

2427
class TimeUnitConversion(TransformationPass):
@@ -71,52 +74,83 @@ def run(self, dag: DAGCircuit):
7174
if self._durations_provided:
7275
inst_durations.update(self.inst_durations, getattr(self.inst_durations, "dt", None))
7376

77+
# The float-value converted units for delay expressions, either all in 'dt'
78+
# or all in seconds.
79+
expression_durations = {}
80+
7481
# Choose unit
75-
if inst_durations.dt is not None:
76-
time_unit = "dt"
77-
else:
78-
# Check what units are used in delays and other instructions: dt or SI or mixed
79-
units_delay = self._units_used_in_delays(dag)
80-
if self._unified(units_delay) == "mixed":
82+
has_dt = False
83+
has_si = False
84+
85+
# We _always_ need to traverse duration expressions to convert them to
86+
# a float. But we also use the opportunity to note if they intermix cycles
87+
# and wall-time, in case we don't have a `dt` to use to unify all instruction
88+
# durations.
89+
for node in dag.op_nodes(op=Delay):
90+
if isinstance(node.op.duration, expr.Expr):
91+
if any(
92+
isinstance(x, expr.Stretch) for x in expr.iter_identifiers(node.op.duration)
93+
):
94+
# If any of the delays use a stretch expression, we can't run scheduling
95+
# passes anyway, so we bail out. In theory, we _could_ still traverse
96+
# through the stretch expression and replace any Duration value nodes it may
97+
# contain with ones of the same units, but it'd be complex and probably unuseful.
98+
self.property_set["time_unit"] = "stretch"
99+
return dag
100+
101+
visitor = _EvalDurationImpl(inst_durations.dt)
102+
duration = node.op.duration.accept(visitor)
103+
if visitor.in_cycles():
104+
has_dt = True
105+
# We need to round in case the expression evaluated to a non-integral 'dt'.
106+
duration = duration_in_dt(duration, 1.0)
107+
else:
108+
has_si = True
109+
if duration < 0:
110+
raise TranspilerError(
111+
f"Expression '{node.op.duration}' resolves to a negative duration!"
112+
)
113+
expression_durations[node._node_id] = duration
114+
else:
115+
if node.op.unit == "dt":
116+
has_dt = True
117+
else:
118+
has_si = True
119+
if inst_durations.dt is None and has_dt and has_si:
81120
raise TranspilerError(
82121
"Fail to unify time units in delays. SI units "
83122
"and dt unit must not be mixed when dt is not supplied."
84123
)
85-
units_other = inst_durations.units_used()
86-
if self._unified(units_other) == "mixed":
87-
raise TranspilerError(
88-
"Fail to unify time units in instruction_durations. SI units "
89-
"and dt unit must not be mixed when dt is not supplied."
90-
)
91124

92-
unified_unit = self._unified(units_delay | units_other)
93-
if unified_unit == "SI":
125+
if inst_durations.dt is None:
126+
# Check what units are used in other instructions: dt or SI or mixed
127+
units_other = inst_durations.units_used()
128+
unified_unit = self._unified(units_other)
129+
if unified_unit == "SI" and not has_dt:
94130
time_unit = "s"
95-
elif unified_unit == "dt":
131+
elif unified_unit == "dt" and not has_si:
96132
time_unit = "dt"
97133
else:
98134
raise TranspilerError(
99135
"Fail to unify time units. SI units "
100136
"and dt unit must not be mixed when dt is not supplied."
101137
)
138+
else:
139+
time_unit = "dt"
102140

103141
# Make instructions with local durations consistent.
104142
for node in dag.op_nodes(Delay):
105143
op = node.op.to_mutable()
106-
op.duration = inst_durations._convert_unit(op.duration, op.unit, time_unit)
144+
if node._node_id in expression_durations:
145+
op.duration = expression_durations[node._node_id]
146+
else:
147+
op.duration = inst_durations._convert_unit(op.duration, op.unit, time_unit)
107148
op.unit = time_unit
108149
dag.substitute_node(node, op)
109150

110151
self.property_set["time_unit"] = time_unit
111152
return dag
112153

113-
@staticmethod
114-
def _units_used_in_delays(dag: DAGCircuit) -> Set[str]:
115-
units_used = set()
116-
for node in dag.op_nodes(op=Delay):
117-
units_used.add(node.op.unit)
118-
return units_used
119-
120154
@staticmethod
121155
def _unified(unit_set: Set[str]) -> str:
122156
if not unit_set:
@@ -135,3 +169,65 @@ def _unified(unit_set: Set[str]) -> str:
135169
return "SI"
136170

137171
return "mixed"
172+
173+
174+
class _EvalDurationImpl(expr.ExprVisitor[float]):
175+
"""Evaluates the expression to a single float result.
176+
177+
If ``dt`` is provided or all durations are already in ``dt``, the result is in ``dt``.
178+
Otherwise, the result will be in seconds, and all durations MUST be in wall-time (SI).
179+
"""
180+
181+
__slots__ = ("dt", "has_dt", "has_si")
182+
183+
def __init__(self, dt=None):
184+
self.dt = dt
185+
self.has_dt = False
186+
self.has_si = False
187+
188+
def in_cycles(self):
189+
"""Returns ``True`` if units are 'dt' after visit."""
190+
return self.has_dt or self.dt is not None
191+
192+
def visit_value(self, node, /) -> float:
193+
if isinstance(node.value, float):
194+
return node.value
195+
if isinstance(node.value, Duration.dt):
196+
if self.has_si and self.dt is None:
197+
raise TranspilerError(
198+
"Fail to unify time units in delays. SI units "
199+
"and dt unit must not be mixed when dt is not supplied."
200+
)
201+
self.has_dt = True
202+
return node.value[0]
203+
if isinstance(node.value, Duration):
204+
if self.has_dt and self.dt is None:
205+
raise TranspilerError(
206+
"Fail to unify time units in delays. SI units "
207+
"and dt unit must not be mixed when dt is not supplied."
208+
)
209+
self.has_si = True
210+
# Setting 'divisor' to 1 when there's no 'dt' is just to simplify
211+
# the logic (we don't need to divide).
212+
divisor = self.dt if self.dt is not None else 1
213+
if isinstance(node.value, Duration.s):
214+
return node.value[0] / divisor
215+
from_unit = node.value.unit()
216+
return apply_prefix(node.value[0], from_unit) / divisor
217+
raise TranspilerError(f"invalid duration expression: {node}")
218+
219+
def visit_binary(self, node, /) -> float:
220+
left = node.left.accept(self)
221+
right = node.right.accept(self)
222+
if node.op == expr.Binary.Op.ADD:
223+
return left + right
224+
if node.op == expr.Binary.Op.SUB:
225+
return left - right
226+
if node.op == expr.Binary.Op.MUL:
227+
return left * right
228+
if node.op == expr.Binary.Op.DIV:
229+
return left / right
230+
raise TranspilerError(f"invalid duration expression: {node}")
231+
232+
def visit_cast(self, node, /) -> float:
233+
return node.operand.accept(self)

0 commit comments

Comments
 (0)