-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Conversation
This reverts commit e2b3221.
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).
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.
This is probably a better default in general, since we don't really have much use for const except for timing stuff.
Since we're going for using a Cast node when const-ness differs, this will be fine.
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.
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).
Also adds support to expr.lift to create a value expression of type types.Duration from an instance of qiskit.circuit.Duration.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hard to review cleanly until we're more merged, but I think the couple of comments I've got here are important to get out sooner rather than later.
I'm also thinking that circuit.Duration
ought to become a valid input type to Delay
, but if that happens (and if you've already done it, just ignore me) it'd be a follow-up anyway.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
(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, |
There was a problem hiding this comment.
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
.)
There was a problem hiding this comment.
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:
- We have uninitialized stretch variables, which are considered constants.
- We have stretch expressions, which can (must?) include uninitialized stretch variables, constrained by duration literals.
- We need a type that represents just a literal duration (e.g.
1000ns
), since there are transformations only applicable to these, likeDuration / Duration => Float
. From what I understandStretch / 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.
There was a problem hiding this comment.
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 thatDuration + Stretch => Stretch
, which certainly makes it seem likeStretch
is greater thanDuration
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 forduration
stretch < duration
- and
a / b
is undefined ifb
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 ofa
. I'd argue we just put "division" of stretches into the same vein, and not all divisions are necessarily unresolvable: ifstretch a = 10dt;
, thena / a
is mathematically well-defined to be 1 (becausea
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, butint[32]
supports only a subset of the operations ofuint[16]
(no bitwise stuff). - We could argue that
stretch
isn't a "type" at all, it's just an unknownduration
(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 declaredstretch <id>
. I'd call the initialiser expression (if present) aduration
, and interpret it as a limitduration(<stretch id>) >= <duration expr>
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Duration
and Stretch
types in classical expressions.Duration
type in classical expressions.
After chatting a bit offline, me and @jakelishman came to the conclusion that it'd likely be more appropriate to represent |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the changes - small comments only, most we can just ignore if we don't want to make the changes in this PR.
crates/circuit/src/duration.rs
Outdated
/// In Python 3.10+, you can use it in a match statement:: | ||
/// | ||
/// match duration: | ||
/// case Duration.dt(dt): | ||
/// return dt | ||
/// case Duration.s(seconds): | ||
/// return seconds / 5e-7 | ||
/// case _: | ||
/// raise ValueError("expected dt or seconds") | ||
/// | ||
/// And in Python 3.9, you can use ``isinstance`` to determine which variant | ||
/// is populated:: | ||
/// | ||
/// if isinstance(duration, Duration.dt): | ||
/// return duration[0] | ||
/// elif isinstance(duration, Duration.s): | ||
/// return duration[0] / 5e-7 | ||
/// else: | ||
/// raise ValueError("expected dt or seconds") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit of a nuisance in Python 3.9. Perhaps cleaner to add a unit
pyfunction that returns the string name, so you can dispatch on it? E.g.
#[pymethods]
impl Duration {
/// The corresponding ``unit`` of the duration.
fn unit(&self) -> &'static str {
match Self {
Duration::ns(_) => "ns",
// ...
}
}
}
then Python-space has a cheaper way of doing the dispatch programmatically, and it maybe provides a slightly easier link with the legacy (value, unit)
2-tuple.
I'd say that ^that particular one is a candidate for doing in this PR (if you think it's worthwhile), but ones that would certainly be follow-ups if we wanted them to be: this class is a sensible place to add methods like as_seconds(self) -> f64
, as_dt(self, dt_in_seconds) -> u64
, etc, which would be useful if/when we swap to using this more in the internals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 78b2951.
I also added a py_value
method to get the int or float (exposed in Python as Duration.value()
)
class StretchType(ClassicalType): | ||
"""Type information for a stretch.""" | ||
|
||
__slots__ = () | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe just semantics: my reading was we give-or-take decided to interpret the OQ3 spec as saying that stretch x;
didn't involve a type stretch
so much as just being a special keyword for a duration
-typed declaration. I'm also fine to leave it the way we have (heaven knows this AST is a jumble), but in another world: would it make more sense to handle stretch x (= Expr)?;
as a
class StretchDeclaration(Statement):
identifier: Identifier
bound: Optional[Expr]
rather than a type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah, that makes a ton more sense. We don't use this in this PR anyway, so if it's alright, I'll leave it for now and make the switch in the PR that actually does.
qiskit/qpy/__init__.py
Outdated
=========================== ========= ============================================================ | ||
Python type / class Type code Payload | ||
=========================== ========= ============================================================ | ||
``float`` ``f`` One ``double value``. | ||
|
||
=========== ========= ============================================================================ | ||
:class:`~.circuit.Duration` ``t`` One ``DURATION``. | ||
|
||
=========================== ========= ============================================================ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
circuit.Duration
is still a Python type, tbf
I think there's a risk with using t
: I think that type code is already used elsewhere to represent "tuple", and there's a risk of a clash (despite the organisational structure on type_keys.py
, the enums in there are not necessarily separate namespaces in QPY land). That said, I think EXPR_VALUE
is localised enough that it can never hit the clash, but PARAM_TYPE
would. Up to you if you think it's worth changing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might not be understanding. Are you saying that we should try to have unique characters across all of type_keys.py
in case we introduce a bug that reads/writes a byte in the wrong place? My understanding of these is that we're manually reading / writing a byte that can only be one of e.g. TypeKeys.ExprType.<KEYS>
in the place that we're reading / writing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't remember the exact circumstances, but I remember having trouble with QPY at one point because there were locations where type keys from more than one TypeKeys.<Thing>
were valid choices - I think the abstraction is leaky.
That said, given that I added the ExprType
ones after that, I'm relatively sure that it's not a problem here, so it's ok to leave.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this looks good to go now!
if unit == "dt": | ||
return cls.DT | ||
if unit == "ns": | ||
return cls.NS | ||
if unit == "us": | ||
return cls.US | ||
if unit == "ms": | ||
return cls.MS | ||
if unit == "s": | ||
return cls.S |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tbf, we could have done this stuff (and elsewhere in the file) with static-dict lookups, but it's not at all important
Summary
Adds new type
Duration
to the classical expression type system, as well as a new (and separate) coreDuration
type toqiskit.circuit
.Details and comments
Based on #13832. Here's a readable diff in the meantime.Rebased on main.Core type
The new core type
Duration
is apyclass
implemented in Rust as anenum
type. For now, it's got variants for each of the supported timing kinds defined in OpenQASM (ns
,us
,ms
,s
,dt
). All of these aref64
s, except fordt
which is au64
.Equality of
Duration
currently follows the same choices made by #13816, in that durations are not first brought to the same unit and compared for magnitude. Perhaps in the future, we can consider equality between walltimes in different units if that feels useful.Expression types
Like in QASM, the
Duration
type represents a length of time, possibly negative. Expressions of typeDuration
may be resolvable immediately, or may containstretch
expressions (to be added in #13852).To-do
Duration
representation and expression type (Value
orVar
).Duration
equality.