Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Stretch] Support Duration type in classical expressions. #13844

Merged
merged 96 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from 87 commits
Commits
Show all changes
96 commits
Select commit Hold shift + click to select a range
af4ba52
WIP
kevinhartman Feb 7, 2025
e423bf9
Add try_const to lift.
kevinhartman Feb 7, 2025
0a5917b
Try multiple singletons, new one for const.
kevinhartman Feb 7, 2025
a97434d
Revert "Try multiple singletons, new one for const."
kevinhartman Feb 7, 2025
1afc965
Remove Bool singleton test.
kevinhartman Feb 7, 2025
86655f1
Add const handling for stores, fix test bugs.
kevinhartman Feb 7, 2025
aaeae9b
Fix formatting.
kevinhartman Feb 7, 2025
a2a444b
Remove Duration and Stretch for now.
kevinhartman Feb 7, 2025
8ac2dc3
Cleanup, fix const bug in index.
kevinhartman Feb 7, 2025
9f8313c
Fix ordering issue for types with differing const-ness.
kevinhartman Feb 9, 2025
db9d9cb
Fix QPY serialization.
kevinhartman Feb 10, 2025
71b7e7a
Make expr.Lift default to non-const.
kevinhartman Feb 11, 2025
2091557
Revert to old test_expr_constructors.py.
kevinhartman Feb 11, 2025
7307be9
Make binary_logical lift independent again.
kevinhartman Feb 11, 2025
9b30284
Update tests, handle a few edge cases.
kevinhartman Feb 12, 2025
ce1faf1
Fix docstring.
kevinhartman Feb 12, 2025
4fee48f
Remove now redundant arg from tests.
kevinhartman Feb 12, 2025
88ab046
Add const testing for ordering.
kevinhartman Feb 12, 2025
c5a230f
Add const tests for shifts.
kevinhartman Feb 12, 2025
7c88d88
Add release note.
kevinhartman Feb 12, 2025
c58a7b8
Add const store tests.
kevinhartman Feb 12, 2025
d9e9a8c
Address lint, minor cleanup.
kevinhartman Feb 13, 2025
4a56150
Add Float type to classical expressions.
kevinhartman Feb 12, 2025
23b5961
Allow DANGEROUS conversion from Float to Bool.
kevinhartman Feb 12, 2025
8bf2e4f
Test Float ordering.
kevinhartman Feb 12, 2025
111eb32
Improve error messages for using Float with logical operators.
kevinhartman Feb 12, 2025
a839d51
Float tests for constructors.
kevinhartman Feb 12, 2025
a19b39a
Add release note.
kevinhartman Feb 12, 2025
7f02e56
Add Duration and Stretch classical types.
kevinhartman Feb 13, 2025
55af327
Add Duration type to qiskit.circuit.
kevinhartman Feb 13, 2025
86b7af9
Block Stretch from use in binary relations.
kevinhartman Feb 13, 2025
9b3c821
Add type ordering tests for Duration and Stretch.
kevinhartman Feb 13, 2025
69eab91
Test expr constructors for Duration and Stretch.
kevinhartman Feb 13, 2025
971cc26
Fix lint.
kevinhartman Feb 13, 2025
25508bf
Reject const vars in add_var and add_input.
kevinhartman Feb 17, 2025
2c8ce43
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 17, 2025
ccf9441
Implement QPY support for const-typed expressions.
kevinhartman Feb 18, 2025
c6eab02
Remove invalid test.
kevinhartman Feb 18, 2025
edd7806
Update QPY version 14 desc.
kevinhartman Feb 18, 2025
8afa92e
Fix lint.
kevinhartman Feb 18, 2025
4e0f2df
Add serialization testing.
kevinhartman Feb 18, 2025
15ba943
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 18, 2025
81c5833
Merge branch 'const-expr' into expr-float
kevinhartman Feb 18, 2025
50c31d3
Test pre-v14 QPY rejects const-typed exprs.
kevinhartman Feb 18, 2025
7e43322
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 18, 2025
2449ee6
Merge branch 'const-expr' into expr-float
kevinhartman Feb 18, 2025
e55e189
QASM export for floats.
kevinhartman Feb 18, 2025
1d51022
QPY support for floats.
kevinhartman Feb 18, 2025
eb8f150
Fix lint.
kevinhartman Feb 18, 2025
415f62e
Merge branch 'expr-float' into expr-timing
kevinhartman Feb 18, 2025
d38f3e9
Settle on Duration circuit core type.
kevinhartman Feb 19, 2025
fd66c1d
QPY serialization for durations and stretches.
kevinhartman Feb 19, 2025
e9b6d97
Add QPY testing.
kevinhartman Feb 19, 2025
52da3cc
QASM support for stretch and duration.
kevinhartman Feb 19, 2025
a3be688
Fix lint.
kevinhartman Feb 19, 2025
07771f1
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 20, 2025
9332ed3
Merge branch 'const-expr' into expr-float
kevinhartman Feb 20, 2025
17241c6
Merge branch 'expr-float' into expr-timing
kevinhartman Feb 20, 2025
ca91bfd
Don't use match since we still support Python 3.9.
kevinhartman Feb 20, 2025
ad87e78
Fix enum match.
kevinhartman Feb 20, 2025
d3bca5f
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 24, 2025
50deb93
Revert visitors.py.
kevinhartman Feb 24, 2025
f1dc1a1
Address review comments.
kevinhartman Feb 24, 2025
1f439a0
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 25, 2025
166d7b2
Improve type docs.
kevinhartman Feb 26, 2025
10b2c8d
Merge branch 'main' of github.com:Qiskit/qiskit into const-expr
kevinhartman Feb 26, 2025
9d6cf39
Revert QPY, since the old format can support constexprs.
kevinhartman Feb 26, 2025
8021e00
Move const-ness from Type to Expr.
kevinhartman Feb 26, 2025
a15141b
Revert QPY testing, no longer needed.
kevinhartman Feb 26, 2025
ca2785d
Add explicit validation of const expr.
kevinhartman Feb 26, 2025
e902009
Revert stuff I didn't need to touch.
kevinhartman Feb 26, 2025
16475c3
Update release note.
kevinhartman Feb 26, 2025
6b5930d
A few finishing touches.
kevinhartman Feb 27, 2025
be73180
Merge branch 'const-expr-expr' into expr-float-expr
kevinhartman Feb 27, 2025
25f1693
Fix-up after merge.
kevinhartman Feb 27, 2025
ecc343b
Merge branch 'expr-float-expr' into expr-timing-expr
kevinhartman Feb 27, 2025
b32e6f0
Fix-ups after merge.
kevinhartman Feb 27, 2025
864506d
Fix lint.
kevinhartman Feb 27, 2025
6bc728a
Fix comment and release note.
kevinhartman Feb 27, 2025
2d38178
Merge branch 'expr-float' into expr-timing
kevinhartman Feb 27, 2025
42258b8
Merge branch 'main' of github.com:Qiskit/qiskit into expr-float
kevinhartman Feb 28, 2025
481700a
Merge branch 'expr-float' into expr-timing
kevinhartman Feb 28, 2025
e09762f
Special-case Var const-ness for Stretch type.
kevinhartman Feb 28, 2025
cbd55db
Address review comments.
kevinhartman Feb 28, 2025
f12afb4
Update release note.
kevinhartman Feb 28, 2025
8d059cd
Merge branch 'expr-float' into expr-timing
kevinhartman Feb 28, 2025
e05d9ff
Update docs.
kevinhartman Feb 28, 2025
0f14cca
Address review comments.
kevinhartman Mar 1, 2025
7db8222
Merge branch 'expr-float' into expr-timing
kevinhartman Mar 1, 2025
34e615b
Merge branch 'main' of github.com:Qiskit/qiskit into expr-timing
kevinhartman Mar 1, 2025
bbe67a6
Remove Stretch type.
kevinhartman Mar 2, 2025
0acc450
Remove a few more mentions of the binned stretch type.
kevinhartman Mar 2, 2025
7fea216
Add docstring for Duration.
kevinhartman Mar 2, 2025
78b2951
Address review comments.
kevinhartman Mar 3, 2025
97bbeba
Remove unused import.
kevinhartman Mar 3, 2025
49f2af8
Remove visitor short circuit.
kevinhartman Mar 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions crates/circuit/src/duration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// This code is part of Qiskit.
//
// (C) Copyright IBM 2025
//
// This code is licensed under the Apache License, Version 2.0. You may
// obtain a copy of this license in the LICENSE.txt file in the root directory
// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
//
// Any modifications or derivative works of this code must retain this
// copyright notice, and modified files need to carry a notice indicating
// that they have been altered from the originals.

use pyo3::prelude::*;

#[pyclass(eq, module = "qiskit._accelerate.circuit")]
#[derive(PartialEq, Clone, Copy, Debug)]
#[allow(non_camel_case_types)]
pub enum Duration {
dt(u64),
ns(f64),
us(f64),
ms(f64),
s(f64),
}

impl Duration {
fn __repr__(&self) -> String {
match self {
Duration::ns(t) => format!("Duration.ns({})", t),
Duration::us(t) => format!("Duration.us({})", t),
Duration::ms(t) => format!("Duration.ms({})", t),
Duration::s(t) => format!("Duration.s({})", t),
Duration::dt(t) => format!("Duration.dt({})", t),
}
}
}
2 changes: 2 additions & 0 deletions crates/circuit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod converters;
pub mod dag_circuit;
pub mod dag_node;
mod dot_utils;
pub mod duration;
pub mod error;
pub mod gate_matrix;
pub mod imports;
Expand Down Expand Up @@ -157,6 +158,7 @@ macro_rules! impl_intopyobject_for_copy_pyclass {
}

pub fn circuit(m: &Bound<PyModule>) -> PyResult<()> {
m.add_class::<duration::Duration>()?;
m.add_class::<circuit_data::CircuitData>()?;
m.add_class::<circuit_instruction::CircuitInstruction>()?;
m.add_class::<dag_circuit::DAGCircuit>()?;
Expand Down
2 changes: 2 additions & 0 deletions qiskit/circuit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,8 @@ def __array__(self, dtype=None, copy=None):
\end{pmatrix}
"""

from qiskit._accelerate.circuit import Duration # pylint: disable=unused-import

from .exceptions import CircuitError
from . import _utils
from .quantumcircuit import QuantumCircuit
Expand Down
44 changes: 30 additions & 14 deletions qiskit/circuit/classical/expr/constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

__all__ = [
"lift",
"cast",
"bit_not",
"logic_not",
"bit_and",
Expand All @@ -32,6 +33,9 @@
"less_equal",
"greater",
"greater_equal",
"shift_left",
"shift_right",
"index",
"lift_legacy_condition",
]

Expand All @@ -45,19 +49,17 @@
import qiskit


def _coerce_lossless(expr: Expr, type: types.Type) -> Expr:
def _coerce_lossless(expr: Expr, type: types.Type) -> Expr | None:
"""Coerce ``expr`` to ``type`` by inserting a suitable :class:`Cast` node, if the cast is
lossless. Otherwise, raise a ``TypeError``."""
lossless. Otherwise, return ``None``."""
kind = cast_kind(expr.type, type)
if kind is CastKind.EQUAL:
return expr
if kind is CastKind.IMPLICIT:
return Cast(expr, type, implicit=True)
if kind is CastKind.LOSSLESS:
return Cast(expr, type, implicit=False)
if kind is CastKind.DANGEROUS:
raise TypeError(f"cannot cast '{expr}' to '{type}' without loss of precision")
raise TypeError(f"no cast is defined to take '{expr}' to '{type}'")
return None


def lift_legacy_condition(
Expand Down Expand Up @@ -107,7 +109,7 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr:
if type is not None:
raise ValueError("use 'cast' to cast existing expressions, not 'lift'")
return value
from qiskit.circuit import Clbit, ClassicalRegister # pylint: disable=cyclic-import
from qiskit.circuit import Clbit, ClassicalRegister, Duration # pylint: disable=cyclic-import

inferred: types.Type
if value is True or value is False or isinstance(value, Clbit):
Expand All @@ -121,6 +123,12 @@ def lift(value: typing.Any, /, type: types.Type | None = None) -> Expr:
raise ValueError("cannot represent a negative value")
inferred = types.Uint(width=value.bit_length() or 1)
constructor = Value
elif isinstance(value, float):
inferred = types.Float()
constructor = Value
elif isinstance(value, Duration):
inferred = types.Duration()
constructor = Value
else:
raise TypeError(f"failed to infer a type for '{value}'")
if type is None:
Expand Down Expand Up @@ -181,11 +189,15 @@ def logic_not(operand: typing.Any, /) -> Expr:
>>> expr.logic_not(ClassicalRegister(3, "c"))
Unary(\
Unary.Op.LOGIC_NOT, \
Cast(Var(ClassicalRegister(3, 'c'), Uint(3)), Bool(), implicit=True), \
Cast(Var(ClassicalRegister(3, 'c'), Uint(3)), \
Bool(), implicit=True), \
Bool())
"""
operand = _coerce_lossless(lift(operand), types.Bool())
return Unary(Unary.Op.LOGIC_NOT, operand, operand.type)
operand = lift(operand)
coerced_operand = _coerce_lossless(operand, types.Bool())
if coerced_operand is None:
raise TypeError(f"cannot apply '{Unary.Op.LOGIC_NOT}' to type '{operand.type}'")
return Unary(Unary.Op.LOGIC_NOT, coerced_operand, coerced_operand.type)


def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Expr]:
Expand Down Expand Up @@ -300,9 +312,13 @@ def bit_xor(left: typing.Any, right: typing.Any, /) -> Expr:

def _binary_logical(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr:
bool_ = types.Bool()
left = _coerce_lossless(lift(left), bool_)
right = _coerce_lossless(lift(right), bool_)
return Binary(op, left, right, bool_)
left = lift(left)
right = lift(right)
coerced_left = _coerce_lossless(left, bool_)
coerced_right = _coerce_lossless(right, bool_)
if coerced_left is None or coerced_right is None:
raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'")
return Binary(op, coerced_left, coerced_right, bool_)


def logic_and(left: typing.Any, right: typing.Any, /) -> Expr:
Expand Down Expand Up @@ -337,7 +353,7 @@ def logic_or(left: typing.Any, right: typing.Any, /) -> Expr:

def _equal_like(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr:
left, right = _lift_binary_operands(left, right)
if left.type.kind is not right.type.kind:
if left.type.kind is not right.type.kind or left.type.kind is types.Stretch:
raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'")
type = types.greater(left.type, right.type)
return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool())
Expand Down Expand Up @@ -381,7 +397,7 @@ def not_equal(left: typing.Any, right: typing.Any, /) -> Expr:

def _binary_relation(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr:
left, right = _lift_binary_operands(left, right)
if left.type.kind is not right.type.kind or left.type.kind is types.Bool:
if left.type.kind is not right.type.kind or left.type.kind in {types.Bool, types.Stretch}:
raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'")
type = types.greater(left.type, right.type)
return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool())
Expand Down
12 changes: 10 additions & 2 deletions qiskit/circuit/classical/expr/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,14 @@ def __init__(
name: str | None = None,
):
super().__setattr__("type", type)
super().__setattr__("const", False)
# For now, Stretch is the only kind of const variable we allow.
# In the future, we may want to add a 'const' constructor arg here
# to let users create other kinds of constants, or perhaps introduce
# a separate expr.Const that requires a const expr initializer for this
# purpose. `QuantumCircuit.add_stretch` is the official way to create
# stretches, and makes no promise that we will track stretches using
# `Var` (it accepts just a name and returns just _some_ `Expr`).
super().__setattr__("const", type.kind is types.Stretch)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given what we talked about offline, how plausible is it to instead have stretch be represented by a new node in the Expr tree called Const?

I would like to keep the type-system association we'd newly made with the move of the const system to Expr where Var is associated with a mutable l-value, Value was an immediate value, and now Const is some compile-time resolvable object. I know it might involve relatively major changes throughout the control-flow builders, but the DAG wires I think should automatically work, because we weren't adding them anyway.

I can live with it if is too much work - Var(const=True) and Var(const=False) is still distinguishable, if not as pretty as having it encoded in the types, not just the values.

Copy link
Contributor Author

@kevinhartman kevinhartman Mar 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I'm not sure that a Const node would even be a good fit for uninitialized stretch variables. I think we will want a Const node eventually to represent const definitions with initializers, and I'd expect such a node to be constructed via expr.Const.new(name: str, type: types.Type, initializer: expr.Expr) where initializer gets validated to be a constant expression coercable to type.

I can live with it if is too much work - Var(const=True) and Var(const=False) is still distinguishable

I think that Var is not a good candidate for representing initialized constants, even with a potential const=True constructor argument, since all initialized constants require an initializer. Perhaps Var will take an initializer in Qiskit eventually, but it'd be optional and not required to be a constant expression.

That said, Var can be declared uninitialized, which IMO makes it the more natural fit for representing uninitialized stretch variables (between Var and an imagined Const).

What we may want eventually is a third expression kind, StretchVar. I didn't want to attempt to introduce that this late in the development. I think we can get away with piggybacking off of Var for now as an implementation detail. In #13852 I was planning to make QuantumCircuit.add_stretch take only a name (no var) and return just an expr.Expr, and then add separate methods for iterating and getting stretch variables + filtering them out of the normal var methods. It's unclear to me if they should still show up in the same set of captures, though.

super().__setattr__("var", var)
super().__setattr__("name", name)

Expand Down Expand Up @@ -175,6 +182,7 @@ def __eq__(self, other):
return (
isinstance(other, Var)
and self.type == other.type
and self.const == other.const
and self.var == other.var
and self.name == other.name
)
Expand All @@ -190,7 +198,7 @@ def __getstate__(self):
def __setstate__(self, state):
var, type, name = state
super().__setattr__("type", type)
super().__setattr__("const", False)
super().__setattr__("const", type.kind is types.Stretch)
super().__setattr__("var", var)
super().__setattr__("name", name)

Expand Down
5 changes: 5 additions & 0 deletions qiskit/circuit/classical/expr/visitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ def is_lvalue(node: expr.Expr, /) -> bool:
the scope that attempts to write to it. This would be an access property of the containing
program, however, and not an inherent property of the expression system.

A constant expression is never an lvalue.

Examples:
Literal values are never l-values; there's no memory location associated with (for example)
the constant ``1``::
Expand Down Expand Up @@ -297,4 +299,7 @@ def is_lvalue(node: expr.Expr, /) -> bool:
>>> expr.is_lvalue(expr.bit_and(a, b))
False
"""
if node.const:
# A constant expression is _never_ an lvalue.
return False
return node.accept(_IS_LVALUE)
19 changes: 15 additions & 4 deletions qiskit/circuit/classical/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,21 @@
heap-allocating a new version of the same thing. Where possible, the class constructors will return
singleton instances to facilitate this.

The two different types available are for Booleans (corresponding to :class:`.Clbit` and the
literals ``True`` and ``False``), and unsigned integers (corresponding to
:class:`.ClassicalRegister` and Python integers).
The :class:`Bool` type represents :class:`.Clbit` and the literals ``True`` and ``False``, the
:class:`Uint` type represents :class:`.ClassicalRegister` and Python integers, and the
:class:`Float` type represents Python floats.

.. autoclass:: Bool
.. autoclass:: Uint
.. autoclass:: Float

Note that :class:`Uint` defines a family of types parametrized by their width; it is not one single
type, which may be slightly different to the 'classical' programming languages you are used to.

There are also two duration-like types for use in circuit timing expressions.

.. autoclass:: Duration
.. autoclass:: Stretch

Working with types
==================
Expand Down Expand Up @@ -89,12 +94,18 @@
The return values from this function are an enumeration explaining the types of cast that are
allowed from the left type to the right type.

Note that casts between :class:`Float` and :class:`Uint` are considered dangerous in either
direction, and must be done explicitly.

.. autoclass:: CastKind
"""

__all__ = [
"Type",
"Bool",
"Duration",
"Float",
"Stretch",
"Uint",
"Ordering",
"order",
Expand All @@ -105,5 +116,5 @@
"cast_kind",
]

from .types import Type, Bool, Uint
from .types import Type, Bool, Duration, Float, Stretch, Uint
from .ordering import Ordering, order, is_subtype, is_supertype, greater, CastKind, cast_kind
21 changes: 15 additions & 6 deletions qiskit/circuit/classical/types/ordering.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@

import enum

from .types import Type, Bool, Uint
from .types import Type, Bool, Duration, Float, Stretch, Uint


# While the type system is simple, it's overkill to represent the complete partial ordering graph of
Expand Down Expand Up @@ -55,10 +55,6 @@ def __repr__(self):
return str(self)


def _order_bool_bool(_a: Bool, _b: Bool, /) -> Ordering:
return Ordering.EQUAL


def _order_uint_uint(left: Uint, right: Uint, /) -> Ordering:
if left.width < right.width:
return Ordering.LESS
Expand All @@ -68,8 +64,13 @@ def _order_uint_uint(left: Uint, right: Uint, /) -> Ordering:


_ORDERERS = {
(Bool, Bool): _order_bool_bool,
(Bool, Bool): lambda _a, _b, /: Ordering.EQUAL,
(Uint, Uint): _order_uint_uint,
(Float, Float): lambda _a, _b, /: Ordering.EQUAL,
(Duration, Duration): lambda _a, _b, /: Ordering.EQUAL,
(Duration, Stretch): lambda _a, _b, /: Ordering.LESS,
(Stretch, Stretch): lambda _a, _b, /: Ordering.EQUAL,
(Stretch, Duration): lambda _a, _b, /: Ordering.GREATER,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not dead certain that this is right. The introduction of the classes calls Duration "a length of time, possibly negative", and Stretch "some not-yet-known non-negative duration". By those terms, let's associate Duration with int and Stretch with uint.

By comparison to integers: no int is ever less than any uint (because no uint can ever represent all the values of an int), but int can be greater than uint if its width is enough.

I think what we might actually be after here is to make Duration the "duration expression" type, and make Stretch an explicit subtype of Duration that is non-negative. Then the ordering should be Stretch < Duration, because Stretch is a strict subtype of it. So that's backwards to what we have here, but that's expected - Stretch should be stricter than Duration.

(Note that I don't mean that Stretch inherits from Duration in the Python-space class system - OO inheritance is not a good model for type systems - just that the type-theory type Stretch is a subtype of Duration.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Starting out with this comment, since I think it's the most fundamental for us to make sure we get right!

The introduction of the classes calls Duration "a length of time, possibly negative", and Stretch "some not-yet-known non-negative duration".

I want to be clear that this is not something I've come up with, but rather something I pulled directly from the OpenQASM docs here:

Stretchable durations have variable non-negative duration that are permitted to grow as necessary to satisfy constraints.
Source: https://openqasm.com/language/delays.html#duration-and-stretch-types

and later

Negative durations are allowed, however passing a negative duration to a gate[duration] or box[duration] expression will result in an error.
Source: https://openqasm.com/language/delays.html#operations-on-durations

My assumption based on that last bit is that this must be some kind of compile-time check, separate from QASM's type system, e.g. "evaluate all compile-time expressions, and if a stretch resolves to a negative duration which gets fed into a timing-aware instruction, blow up."

In the second link, we also see this example:

duration a = 300ns;
duration b = durationof({x $0;});
stretch c;
// stretchy duration with min=300ns
stretch d = a + 2 * c;
// stretchy duration with backtracking by up to half b
stretch e = -0.5 * b + c;

Particularly, stretch d = a + 2 * c; implies that Duration + Stretch => Stretch, which certainly makes it seem like Stretch is greater than Duration in the typing.

Stepping back from that, I think we have 3 separate things going on in all of this:

  1. We have uninitialized stretch variables, which are considered constants.
  2. We have stretch expressions, which can (must?) include uninitialized stretch variables, constrained by duration literals.
  3. We need a type that represents just a literal duration (e.g. 1000ns), since there are transformations only applicable to these, like Duration / Duration => Float. From what I understand Stretch / Stretch is not defined in QASM.

On point 2., my assumption has been that stretch s = 1000dt; is valid and implies a stretch-i-ness of 0, hence the ordering relationship in this PR. If that's not the case, then I think Stretch and Duration are unordered. We can't put a Stretch into a Duration, since we'll lose point 3.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah, I wasn't thinking you were making it up - I just think it's not quite the cleanest way of representing the OQ3 spec. I see where you're coming from, I just think there's a combination of typing restrictions and value restrictions in play here, and we're not in the right one.

For your points one and two: the statement stretch d (= Expr)?; introduces a new degree of freedom into the system. If the initialisation expression is present, it's just a lower bound on the constrained value, clipped to 0 (because stretches can never be negative). That doesn't necessarily mean that the type of Expr needs to be <= stretch - it's actually kind of the other way round; as long as you can embed stretch in the valid total ordering of the type of the initialisation expression, you're fine. The statement stretch d = a + 2*c; might be mathematically clearer as stretch d >= a + 2*c;. So for:

Particularly, stretch d = a + 2 * c; implies that Duration + Stretch => Stretch, which certainly makes it seem like Stretch is greater than Duration in the typing.

I think that's a mistaken inference: a + 2*c need not be positive at all, and stretch d = -500dt; is still a valid initialiser and doesn't require -500dt to be a valid value in stretch. (In my example, there's two immediate restrictions on d: d >= -500dt from the explicit restriction, and d >= 0dt from the typing.

On point 2., my assumption has been that stretch s = 1000dt; is valid and implies a stretch-i-ness of 0,

I think this isn't right: stretch s introduces a new degree of freedom, and stretch s = 1000dt; is saying s >= 1000dt. Otherwise your a + 2*c also wouldn't have introduced a new degree of freedom - it'd be constrained entirely by a and c.

Just below your linked section, there's the line

OpenQASM and OpenPulse have a delay instruction, whose duration is defined by a duration.

So in the statement delay[<expr>], <expr> must be a valid duration. There's then a value restriction on that (as opposed to a typing one) that it must resolve (after stretch resolution and constant folding, etc) to a concrete non-negative value. That's not unprecedented in type systems: Python indexes with int, but negative values have a totally different meaning (you could consider them a different operation, also defined in terms of non-negative integers); and array indexing has an upper bound on the valid values too to make it a valid instruction, regardless of the type width.

I would say that a stretch always resolves to a valid non-negative duration. That a stretch always resolves to a duration at all is already a statement that stretch <= duration in the typing, really. duration in OQ3 is more properly a "time delta", which can also be negative. A delay takes a delta as argument, but is value-constrained to be a positive delta.


For point 3: I get what you're saying, but I don't think you necessarily need this to be in the type system, so it's a trade-off between this property or the partial ordering in terms of representable values. There's already plenty of operations that are ill-defined for certain values of a type - / of course isn't defined for 0 values on the RHS for integers. I think it's fine to admit all of:

  • a / b is valid for duration
  • stretch < duration
  • and a / b is undefined if b resolves to have degrees of freedom.

I'd accept the argument that stretch and duration could be unordered because the division operation isn't defined, but if we do that, then we have to special-case all the "cast" logic, which most naturally wants to use the partial ordering of representable values.

Using the type system to avoid unresolvable operations would be nice, but it's not universally possible, and I'd call division just one of these cases.


Final bits / clarifying:

  • it's already possible to use our type system to create unresolvable stretches - have box { delay [a] $0; delay [a + 10dt] $1; } and there's no valid resolution of a. I'd argue we just put "division" of stretches into the same vein, and not all divisions are necessarily unresolvable: if stretch a = 10dt;, then a / a is mathematically well-defined to be 1 (because a can't be zero - that's the only thing that would be a problem).
  • I think keeping the partial ordering to just mean "in terms of representable values is what we want". For example, uint[16] usefully defined as being < int[32] for the purposes of safe casting, but int[32] supports only a subset of the operations of uint[16] (no bitwise stuff).
  • We could argue that stretch isn't a "type" at all, it's just an unknown duration (though I probably wouldn't - it's harder to track them, I think).
  • I'd say that the only objects of type stretch are the exact names declared stretch <id>. I'd call the initialiser expression (if present) a duration, and interpret it as a limit duration(<stretch id>) >= <duration expr>.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From offline conversations: we actually decided that stretch needn't be a separate type from duration at all. Most of the rest of this conversation stands, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, and thanks for this comment, it was quite helpful.

the statement stretch d (= Expr)?; introduces a new degree of freedom into the system. If the initialisation expression is present, it's just a lower bound on the constrained value, clipped to 0 (because stretches can never be negative).

This is what I wasn't getting—the = operator in QASM is more or less overloaded for a stretch declaration. I'd assumed we weren't introducing a new degree of freedom at all. As you pointed out, really the stretch declaration always introduces a new degree of freedom, and optionally adds a lower bound if there's an "initializer".

All of this should be more or less handled now in the later PRs of this series.

}


Expand Down Expand Up @@ -195,8 +196,16 @@ def _uint_cast(from_: Uint, to_: Uint, /) -> CastKind:
_ALLOWED_CASTS = {
(Bool, Bool): lambda _a, _b, /: CastKind.EQUAL,
(Bool, Uint): lambda _a, _b, /: CastKind.LOSSLESS,
(Bool, Float): lambda _a, _b, /: CastKind.LOSSLESS,
(Uint, Bool): lambda _a, _b, /: CastKind.IMPLICIT,
(Uint, Uint): _uint_cast,
(Uint, Float): lambda _a, _b, /: CastKind.DANGEROUS,
(Float, Float): lambda _a, _b, /: CastKind.EQUAL,
(Float, Uint): lambda _a, _b, /: CastKind.DANGEROUS,
(Float, Bool): lambda _a, _b, /: CastKind.DANGEROUS,
(Duration, Duration): lambda _a, _b, /: CastKind.EQUAL,
(Duration, Stretch): lambda _a, _b, /: CastKind.IMPLICIT,
(Stretch, Stretch): lambda _a, _b, /: CastKind.EQUAL,
}


Expand Down
53 changes: 53 additions & 0 deletions qiskit/circuit/classical/types/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
__all__ = [
"Type",
"Bool",
"Duration",
"Float",
"Stretch",
"Uint",
]

Expand Down Expand Up @@ -115,3 +118,53 @@ def __hash__(self):

def __eq__(self, other):
return isinstance(other, Uint) and self.width == other.width


@typing.final
class Float(Type, metaclass=_Singleton):
"""An IEE-754 double-precision floating point number.
In the future, this may also be used to represent other fixed-width floats.
"""

__slots__ = ()

def __repr__(self):
return "Float()"

def __hash__(self):
return hash(self.__class__)

def __eq__(self, other):
return isinstance(other, Float)


@typing.final
class Duration(Type, metaclass=_Singleton):
"""A length of time, possibly negative."""

__slots__ = ()

def __repr__(self):
return "Duration()"

def __hash__(self):
return hash(self.__class__)

def __eq__(self, other):
return isinstance(other, Duration)


@typing.final
class Stretch(Type, metaclass=_Singleton):
"""A special type that denotes some not-yet-known non-negative duration."""

__slots__ = ()

def __repr__(self):
return "Stretch()"

def __hash__(self):
return hash(self.__class__)

def __eq__(self, other):
return isinstance(other, Stretch)
Loading
Loading