Skip to content

Commit 7a1d51b

Browse files
authored
[Stretch] Support Duration type in classical expressions. (#13844)
* 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. * 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. * Don't use match since we still support Python 3.9. * Fix enum match. * 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. * 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. * Address review comments. * Remove Stretch type. * Remove a few more mentions of the binned stretch type. * Add docstring for Duration. * Address review comments. * Remove unused import. * Remove visitor short circuit.
1 parent 4bef428 commit 7a1d51b

21 files changed

+523
-27
lines changed

crates/circuit/src/duration.rs

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
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+
use pyo3::prelude::*;
14+
use pyo3::IntoPyObjectExt;
15+
16+
/// A length of time used to express circuit timing.
17+
///
18+
/// It defines a group of classes which are all subclasses of itself (functionally, an
19+
/// enumeration carrying data).
20+
///
21+
/// In Python 3.10+, you can use it in a match statement::
22+
///
23+
/// match duration:
24+
/// case Duration.dt(dt):
25+
/// return dt
26+
/// case Duration.s(seconds):
27+
/// return seconds / 5e-7
28+
/// case _:
29+
/// raise ValueError("expected dt or seconds")
30+
///
31+
/// And in Python 3.9, you can use :meth:`Duration.unit` to determine which variant
32+
/// is populated::
33+
///
34+
/// if duration.unit() == "dt":
35+
/// return duration.value()
36+
/// elif duration.unit() == "s":
37+
/// return duration.value() / 5e-7
38+
/// else:
39+
/// raise ValueError("expected dt or seconds")
40+
#[pyclass(eq, module = "qiskit._accelerate.circuit")]
41+
#[derive(PartialEq, Clone, Copy, Debug)]
42+
#[allow(non_camel_case_types)]
43+
pub enum Duration {
44+
dt(i64),
45+
ns(f64),
46+
us(f64),
47+
ms(f64),
48+
s(f64),
49+
}
50+
51+
#[pymethods]
52+
impl Duration {
53+
/// The corresponding ``unit`` of the duration.
54+
fn unit(&self) -> &'static str {
55+
match self {
56+
Duration::dt(_) => "dt",
57+
Duration::us(_) => "us",
58+
Duration::ns(_) => "ns",
59+
Duration::ms(_) => "ms",
60+
Duration::s(_) => "s",
61+
}
62+
}
63+
64+
/// The ``value`` of the duration.
65+
///
66+
/// This will be a Python ``int`` if the :meth:`~Duration.unit` is ``"dt"``,
67+
/// else a ``float``.
68+
#[pyo3(name = "value")]
69+
fn py_value(&self, py: Python) -> PyResult<PyObject> {
70+
match self {
71+
Duration::dt(v) => v.into_py_any(py),
72+
Duration::us(v) | Duration::ns(v) | Duration::ms(v) | Duration::s(v) => {
73+
v.into_py_any(py)
74+
}
75+
}
76+
}
77+
}
78+
79+
impl Duration {
80+
fn __repr__(&self) -> String {
81+
match self {
82+
Duration::ns(t) => format!("Duration.ns({})", t),
83+
Duration::us(t) => format!("Duration.us({})", t),
84+
Duration::ms(t) => format!("Duration.ms({})", t),
85+
Duration::s(t) => format!("Duration.s({})", t),
86+
Duration::dt(t) => format!("Duration.dt({})", t),
87+
}
88+
}
89+
}

crates/circuit/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pub mod converters;
1717
pub mod dag_circuit;
1818
pub mod dag_node;
1919
mod dot_utils;
20+
pub mod duration;
2021
pub mod error;
2122
pub mod gate_matrix;
2223
pub mod imports;
@@ -157,6 +158,7 @@ macro_rules! impl_intopyobject_for_copy_pyclass {
157158
}
158159

159160
pub fn circuit(m: &Bound<PyModule>) -> PyResult<()> {
161+
m.add_class::<duration::Duration>()?;
160162
m.add_class::<circuit_data::CircuitData>()?;
161163
m.add_class::<circuit_instruction::CircuitInstruction>()?;
162164
m.add_class::<dag_circuit::DAGCircuit>()?;

qiskit/circuit/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -1282,6 +1282,8 @@ def __array__(self, dtype=None, copy=None):
12821282
\end{pmatrix}
12831283
"""
12841284

1285+
from qiskit._accelerate.circuit import Duration # pylint: disable=unused-import
1286+
12851287
from .exceptions import CircuitError
12861288
from . import _utils
12871289
from .quantumcircuit import QuantumCircuit

qiskit/circuit/classical/expr/constructors.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr:
109109
if type is not None:
110110
raise ValueError("use 'cast' to cast existing expressions, not 'lift'")
111111
return value
112-
from qiskit.circuit import Clbit, ClassicalRegister # pylint: disable=cyclic-import
112+
from qiskit.circuit import Clbit, ClassicalRegister, Duration # pylint: disable=cyclic-import
113113

114114
inferred: types.Type
115115
if value is True or value is False or isinstance(value, Clbit):
@@ -126,6 +126,9 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr:
126126
elif isinstance(value, float):
127127
inferred = types.Float()
128128
constructor = Value
129+
elif isinstance(value, Duration):
130+
inferred = types.Duration()
131+
constructor = Value
129132
else:
130133
raise TypeError(f"failed to infer a type for '{value}'")
131134
if type is None:

qiskit/circuit/classical/expr/visitors.py

+2
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,8 @@ def is_lvalue(node: expr.Expr, /) -> bool:
268268
the scope that attempts to write to it. This would be an access property of the containing
269269
program, however, and not an inherent property of the expression system.
270270
271+
A constant expression is never an lvalue.
272+
271273
Examples:
272274
Literal values are never l-values; there's no memory location associated with (for example)
273275
the constant ``1``::

qiskit/circuit/classical/types/__init__.py

+6-7
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,14 @@
3434
singleton instances to facilitate this.
3535
3636
The :class:`Bool` type represents :class:`.Clbit` and the literals ``True`` and ``False``, the
37-
:class:`Uint` type represents :class:`.ClassicalRegister` and Python integers, and the
38-
:class:`Float` type represents Python floats.
37+
:class:`Uint` type represents :class:`.ClassicalRegister` and Python integers, the :class:`Float`
38+
type represents Python floats, and the :class:`Duration` type represents a duration for use in
39+
timing-aware circuit operations.
3940
4041
.. autoclass:: Bool
4142
.. autoclass:: Uint
4243
.. autoclass:: Float
43-
44-
Note that :class:`Uint` defines a family of types parametrized by their width; it is not one single
45-
type, which may be slightly different to the 'classical' programming languages you are used to.
46-
44+
.. autoclass:: Duration
4745
4846
Working with types
4947
==================
@@ -99,6 +97,7 @@
9997
__all__ = [
10098
"Type",
10199
"Bool",
100+
"Duration",
102101
"Float",
103102
"Uint",
104103
"Ordering",
@@ -110,5 +109,5 @@
110109
"cast_kind",
111110
]
112111

113-
from .types import Type, Bool, Float, Uint
112+
from .types import Type, Bool, Duration, Float, Uint
114113
from .ordering import Ordering, order, is_subtype, is_supertype, greater, CastKind, cast_kind

qiskit/circuit/classical/types/ordering.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
import enum
2828

29-
from .types import Type, Bool, Float, Uint
29+
from .types import Type, Bool, Duration, Float, Uint
3030

3131

3232
# While the type system is simple, it's overkill to represent the complete partial ordering graph of
@@ -67,6 +67,7 @@ def _order_uint_uint(left: Uint, right: Uint, /) -> Ordering:
6767
(Bool, Bool): lambda _a, _b, /: Ordering.EQUAL,
6868
(Uint, Uint): _order_uint_uint,
6969
(Float, Float): lambda _a, _b, /: Ordering.EQUAL,
70+
(Duration, Duration): lambda _a, _b, /: Ordering.EQUAL,
7071
}
7172

7273

@@ -199,6 +200,7 @@ def _uint_cast(from_: Uint, to_: Uint, /) -> CastKind:
199200
(Float, Float): lambda _a, _b, /: CastKind.EQUAL,
200201
(Float, Uint): lambda _a, _b, /: CastKind.DANGEROUS,
201202
(Float, Bool): lambda _a, _b, /: CastKind.DANGEROUS,
203+
(Duration, Duration): lambda _a, _b, /: CastKind.EQUAL,
202204
}
203205

204206

qiskit/circuit/classical/types/types.py

+17
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
__all__ = [
2323
"Type",
2424
"Bool",
25+
"Duration",
2526
"Float",
2627
"Uint",
2728
]
@@ -134,3 +135,19 @@ def __hash__(self):
134135

135136
def __eq__(self, other):
136137
return isinstance(other, Float)
138+
139+
140+
@typing.final
141+
class Duration(Type, metaclass=_Singleton):
142+
"""A length of time, possibly negative."""
143+
144+
__slots__ = ()
145+
146+
def __repr__(self):
147+
return "Duration()"
148+
149+
def __hash__(self):
150+
return hash(self.__class__)
151+
152+
def __eq__(self, other):
153+
return isinstance(other, Duration)

qiskit/qasm3/ast.py

+12
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,18 @@ class BitType(ClassicalType):
173173
__slots__ = ()
174174

175175

176+
class DurationType(ClassicalType):
177+
"""Type information for a duration."""
178+
179+
__slots__ = ()
180+
181+
182+
class StretchType(ClassicalType):
183+
"""Type information for a stretch."""
184+
185+
__slots__ = ()
186+
187+
176188
class BitArrayType(ClassicalType):
177189
"""Type information for a sized number of classical bits."""
178190

qiskit/qasm3/exporter.py

+15
Original file line numberDiff line numberDiff line change
@@ -1255,6 +1255,8 @@ def _build_ast_type(type_: types.Type) -> ast.ClassicalType:
12551255
return ast.UintType(type_.width)
12561256
if type_.kind is types.Float:
12571257
return ast.FloatType.DOUBLE
1258+
if type_.kind is types.Duration:
1259+
return ast.DurationType()
12581260
raise RuntimeError(f"unhandled expr type '{type_}'")
12591261

12601262

@@ -1271,13 +1273,26 @@ def __init__(self, lookup):
12711273
def visit_var(self, node, /):
12721274
return self.lookup(node) if node.standalone else self.lookup(node.var)
12731275

1276+
# pylint: disable=too-many-return-statements
12741277
def visit_value(self, node, /):
12751278
if node.type.kind is types.Bool:
12761279
return ast.BooleanLiteral(node.value)
12771280
if node.type.kind is types.Uint:
12781281
return ast.IntegerLiteral(node.value)
12791282
if node.type.kind is types.Float:
12801283
return ast.FloatLiteral(node.value)
1284+
if node.type.kind is types.Duration:
1285+
unit = node.value.unit()
1286+
if unit == "dt":
1287+
return ast.DurationLiteral(node.value.value(), ast.DurationUnit.SAMPLE)
1288+
if unit == "ns":
1289+
return ast.DurationLiteral(node.value.value(), ast.DurationUnit.NANOSECOND)
1290+
if unit == "us":
1291+
return ast.DurationLiteral(node.value.value(), ast.DurationUnit.MICROSECOND)
1292+
if unit == "ms":
1293+
return ast.DurationLiteral(node.value.value(), ast.DurationUnit.MILLISECOND)
1294+
if unit == "s":
1295+
return ast.DurationLiteral(node.value.value(), ast.DurationUnit.SECOND)
12811296
raise RuntimeError(f"unhandled Value type '{node}'")
12821297

12831298
def visit_cast(self, node, /):

qiskit/qasm3/printer.py

+6
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ def _visit_FloatType(self, node: ast.FloatType) -> None:
210210
def _visit_BoolType(self, _node: ast.BoolType) -> None:
211211
self.stream.write("bool")
212212

213+
def _visit_DurationType(self, _node: ast.DurationType) -> None:
214+
self.stream.write("duration")
215+
216+
def _visit_StretchType(self, _node: ast.StretchType) -> None:
217+
self.stream.write("stretch")
218+
213219
def _visit_IntType(self, node: ast.IntType) -> None:
214220
self.stream.write("int")
215221
if node.size is not None:

qiskit/qpy/__init__.py

+38-11
Original file line numberDiff line numberDiff line change
@@ -382,31 +382,58 @@ def open(*args):
382382
Version 14
383383
----------
384384
385-
Version 14 adds support for additional :class:`~.types.Type` classes.
385+
Version 14 adds a new core DURATION type, as well as support for additional :class:`~.types.Type`
386+
classes.
387+
388+
DURATION
389+
~~~~~~~~
390+
391+
A :class:`~.circuit.Duration` is encoded by a single-byte ASCII ``char`` that encodes the kind of
392+
type, followed by a payload that varies depending on the type. The defined codes are:
393+
394+
============================== ========= =========================================================
395+
Qiskit class Type code Payload
396+
============================== ========= =========================================================
397+
:class:`~.circuit.Duration.dt` ``t`` One ``unsigned long long value``.
398+
399+
:class:`~.circuit.Duration.ns` ``n`` One ``double value``.
400+
401+
:class:`~.circuit.Duration.us` ``u`` One ``double value``.
402+
403+
:class:`~.circuit.Duration.ms` ``m`` One ``double value``.
404+
405+
:class:`~.circuit.Duration.s` ``s`` One ``double value``.
406+
407+
============================== ========= =========================================================
386408
387409
Changes to EXPR_TYPE
388410
~~~~~~~~~~~~~~~~~~~~
389411
390412
The following table shows the new type classes added in the version:
391413
392-
====================== ========= =================================================================
393-
Qiskit class Type code Payload
394-
====================== ========= =================================================================
395-
:class:`~.types.Float` ``f`` None.
396-
====================== ========= =================================================================
414+
========================= ========= ==============================================================
415+
Qiskit class Type code Payload
416+
========================= ========= ==============================================================
417+
:class:`~.types.Float` ``f`` None.
418+
419+
:class:`~.types.Duration` ``d`` None.
420+
421+
========================= ========= ==============================================================
397422
398423
Changes to EXPR_VALUE
399424
~~~~~~~~~~~~~~~~~~~~~
400425
401426
The classical expression's type system now supports new encoding types for value literals, in
402427
addition to the existing encodings for int and bool. The new value type encodings are below:
403428
404-
=========== ========= ============================================================================
405-
Python type Type code Payload
406-
=========== ========= ============================================================================
407-
``float`` ``f`` One ``double value``.
429+
=========================== ========= ============================================================
430+
Python type Type code Payload
431+
=========================== ========= ============================================================
432+
``float`` ``f`` One ``double value``.
408433
409-
=========== ========= ============================================================================
434+
:class:`~.circuit.Duration` ``t`` One ``DURATION``.
435+
436+
=========================== ========= ============================================================
410437
411438
.. _qpy_version_13:
412439

0 commit comments

Comments
 (0)