Skip to content

Commit 22daa04

Browse files
authored
[Stretch] Add +, -, *, / operators for classical expressions. (#13850)
* 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. * 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. * 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. * 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. * Address review comments. * Remove unused import. * Remove visitor short circuit. * Address review comments. * Address review comments. * Support division by Uint. * Fix lint.
1 parent bad3be0 commit 22daa04

10 files changed

Lines changed: 663 additions & 4 deletions

File tree

qiskit/circuit/classical/expr/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@
134134
.. autofunction:: greater_equal
135135
.. autofunction:: shift_left
136136
.. autofunction:: shift_right
137+
.. autofunction:: add
138+
.. autofunction:: sub
139+
.. autofunction:: mul
140+
.. autofunction:: div
137141
138142
You can index into unsigned integers and bit-likes using another unsigned integer of any width.
139143
This includes in storing operations, if the target of the index is writeable.
@@ -214,6 +218,10 @@
214218
"greater",
215219
"greater_equal",
216220
"index",
221+
"add",
222+
"sub",
223+
"mul",
224+
"div",
217225
"lift_legacy_condition",
218226
]
219227

@@ -238,5 +246,9 @@
238246
shift_left,
239247
shift_right,
240248
index,
249+
add,
250+
sub,
251+
mul,
252+
div,
241253
lift_legacy_condition,
242254
)

qiskit/circuit/classical/expr/constructors.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@
3636
"shift_left",
3737
"shift_right",
3838
"index",
39+
"add",
40+
"sub",
41+
"mul",
42+
"div",
3943
"lift_legacy_condition",
4044
]
4145

@@ -564,3 +568,209 @@ def index(target: typing.Any, index: typing.Any, /) -> Expr:
564568
if target.type.kind is not types.Uint or index.type.kind is not types.Uint:
565569
raise TypeError(f"invalid types for indexing: '{target.type}' and '{index.type}'")
566570
return Index(target, index, types.Bool())
571+
572+
573+
def _binary_sum(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr:
574+
left, right = _lift_binary_operands(left, right)
575+
if left.type.kind is right.type.kind and left.type.kind in {
576+
types.Uint,
577+
types.Float,
578+
types.Duration,
579+
}:
580+
type = types.greater(left.type, right.type)
581+
return Binary(
582+
op,
583+
_coerce_lossless(left, type),
584+
_coerce_lossless(right, type),
585+
type,
586+
)
587+
raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'")
588+
589+
590+
def add(left: typing.Any, right: typing.Any, /) -> Expr:
591+
"""Create an addition expression node from the given values, resolving any implicit casts and
592+
lifting the values into :class:`Value` nodes if required.
593+
594+
Examples:
595+
Addition of two floating point numbers::
596+
597+
>>> from qiskit.circuit.classical import expr
598+
>>> expr.add(5.0, 2.0)
599+
Binary(\
600+
Binary.Op.ADD, \
601+
Value(5.0, Float()), \
602+
Value(2.0, Float()), \
603+
Float())
604+
605+
Addition of two durations::
606+
607+
>>> from qiskit.circuit import Duration
608+
>>> from qiskit.circuit.classical import expr
609+
>>> expr.add(Duration.dt(1000), Duration.dt(1000))
610+
Binary(\
611+
Binary.Op.ADD, \
612+
Value(Duration.dt(1000), Duration()), \
613+
Value(Duration.dt(1000), Duration()), \
614+
Duration())
615+
"""
616+
return _binary_sum(Binary.Op.ADD, left, right)
617+
618+
619+
def sub(left: typing.Any, right: typing.Any, /) -> Expr:
620+
"""Create a subtraction expression node from the given values, resolving any implicit casts and
621+
lifting the values into :class:`Value` nodes if required.
622+
623+
Examples:
624+
Subtraction of two floating point numbers::
625+
626+
>>> from qiskit.circuit.classical import expr
627+
>>> expr.sub(5.0, 2.0)
628+
Binary(\
629+
Binary.Op.SUB, \
630+
Value(5.0, Float()), \
631+
Value(2.0, Float()), \
632+
Float())
633+
634+
Subtraction of two durations::
635+
636+
>>> from qiskit.circuit import Duration
637+
>>> from qiskit.circuit.classical import expr
638+
>>> expr.add(Duration.dt(1000), Duration.dt(1000))
639+
Binary(\
640+
Binary.Op.SUB, \
641+
Value(Duration.dt(1000), Duration()), \
642+
Value(Duration.dt(1000), Duration()), \
643+
Duration())
644+
"""
645+
return _binary_sum(Binary.Op.SUB, left, right)
646+
647+
648+
def mul(left: typing.Any, right: typing.Any) -> Expr:
649+
"""Create a multiplication expression node from the given values, resolving any implicit casts and
650+
lifting the values into :class:`Value` nodes if required.
651+
652+
This can be used to multiply numeric operands of the same type kind, or to multiply a duration
653+
operand by a numeric operand.
654+
655+
Examples:
656+
Multiplication of two floating point numbers::
657+
658+
>>> from qiskit.circuit.classical import expr
659+
>>> expr.mul(5.0, 2.0)
660+
Binary(\
661+
Binary.Op.MUL, \
662+
Value(5.0, Float()), \
663+
Value(2.0, Float()), \
664+
Float())
665+
666+
Multiplication of a duration by a float::
667+
668+
>>> from qiskit.circuit import Duration
669+
>>> from qiskit.circuit.classical import expr
670+
>>> expr.mul(Duration.dt(1000), 0.5)
671+
Binary(\
672+
Binary.Op.MUL, \
673+
Value(Duration.dt(1000), Duration()), \
674+
Value(0.5, Float()), \
675+
Duration())
676+
"""
677+
left, right = _lift_binary_operands(left, right)
678+
type: types.Type
679+
if left.type.kind is right.type.kind is types.Duration:
680+
raise TypeError("cannot multiply two durations")
681+
if left.type.kind is right.type.kind and left.type.kind in {types.Uint, types.Float}:
682+
type = types.greater(left.type, right.type)
683+
left = _coerce_lossless(left, type)
684+
right = _coerce_lossless(right, type)
685+
elif left.type.kind is types.Duration and right.type.kind in {types.Uint, types.Float}:
686+
if not right.const:
687+
raise ValueError(
688+
f"multiplying operands '{left}' and '{right}' would result in a non-const '{left.type}'"
689+
)
690+
type = left.type
691+
elif right.type.kind is types.Duration and left.type.kind in {types.Uint, types.Float}:
692+
if not left.const:
693+
raise ValueError(
694+
f"multiplying operands '{left}' and '{right}' would result in a non-const '{right.type}'"
695+
)
696+
type = right.type
697+
else:
698+
raise TypeError(f"invalid types for '{Binary.Op.MUL}': '{left.type}' and '{right.type}'")
699+
return Binary(
700+
Binary.Op.MUL,
701+
left,
702+
right,
703+
type,
704+
)
705+
706+
707+
def div(left: typing.Any, right: typing.Any) -> Expr:
708+
"""Create a division expression node from the given values, resolving any implicit casts and
709+
lifting the values into :class:`Value` nodes if required.
710+
711+
This can be used to divide numeric operands of the same type kind, to divide a
712+
:class`~.types.Duration` operand by a numeric operand, or to divide two
713+
:class`~.types.Duration` operands which yields an expression of type
714+
:class:`~.types.Float`.
715+
716+
Examples:
717+
Division of two floating point numbers::
718+
719+
>>> from qiskit.circuit.classical import expr
720+
>>> expr.div(5.0, 2.0)
721+
Binary(\
722+
Binary.Op.DIV, \
723+
Value(5.0, Float()), \
724+
Value(2.0, Float()), \
725+
Float())
726+
727+
Division of two durations::
728+
729+
>>> from qiskit.circuit import Duration
730+
>>> from qiskit.circuit.classical import expr
731+
>>> expr.div(Duration.dt(10000), Duration.dt(1000))
732+
Binary(\
733+
Binary.Op.DIV, \
734+
Value(Duration.dt(10000), Duration()), \
735+
Value(Duration.dt(1000), Duration()), \
736+
Float())
737+
738+
739+
Division of a duration by a float::
740+
741+
>>> from qiskit.circuit import Duration
742+
>>> from qiskit.circuit.classical import expr
743+
>>> expr.div(Duration.dt(10000), 12.0)
744+
Binary(\
745+
Binary.Op.DIV, \
746+
Value(Duration.dt(10000), Duration()), \
747+
Value(12.0, types.Float()), \
748+
Duration())
749+
"""
750+
left, right = _lift_binary_operands(left, right)
751+
type: types.Type
752+
if left.type.kind is right.type.kind and left.type.kind in {
753+
types.Duration,
754+
types.Uint,
755+
types.Float,
756+
}:
757+
if left.type.kind is types.Duration:
758+
type = types.Float()
759+
elif types.order(left.type, right.type) is not types.Ordering.NONE:
760+
type = types.greater(left.type, right.type)
761+
left = _coerce_lossless(left, type)
762+
right = _coerce_lossless(right, type)
763+
elif left.type.kind is types.Duration and right.type.kind in {types.Uint, types.Float}:
764+
if not right.const:
765+
raise ValueError(
766+
f"division of '{left}' and '{right}' would result in a non-const '{left.type}'"
767+
)
768+
type = left.type
769+
else:
770+
raise TypeError(f"invalid types for '{Binary.Op.DIV}': '{left.type}' and '{right.type}'")
771+
return Binary(
772+
Binary.Op.DIV,
773+
left,
774+
right,
775+
type,
776+
)

qiskit/circuit/classical/expr/expr.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,15 @@ class Op(enum.Enum):
314314
container types (e.g. unsigned integers) as the left operand, and any integer type as the
315315
right-hand operand. In all cases, the output bit width is the same as the input, and zeros
316316
fill in the "exposed" spaces.
317+
318+
The binary arithmetic operators :data:`ADD`, :data:`SUB:, :data:`MUL`, and :data:`DIV`
319+
can be applied to two floats or two unsigned integers, which should be made to be of
320+
the same width during construction via a cast.
321+
The :data:`ADD`, :data:`SUB`, and :data:`DIV` operators can be applied on two durations
322+
yielding another duration, or a float in the case of :data:`DIV`. The :data:`MUL` operator
323+
can also be applied to a duration and a numeric type, yielding another duration. Finally,
324+
the :data:`DIV` operator can be used to divide a duration by a numeric type, yielding a
325+
duration.
317326
"""
318327

319328
# If adding opcodes, remember to add helper constructor functions in `constructors.py`
@@ -345,6 +354,14 @@ class Op(enum.Enum):
345354
"""Zero-padding bitshift to the left. ``lhs << rhs``."""
346355
SHIFT_RIGHT = 13
347356
"""Zero-padding bitshift to the right. ``lhs >> rhs``."""
357+
ADD = 14
358+
"""Addition. ``lhs + rhs``."""
359+
SUB = 15
360+
"""Subtraction. ``lhs - rhs``."""
361+
MUL = 16
362+
"""Multiplication. ``lhs * rhs``."""
363+
DIV = 17
364+
"""Division. ``lhs / rhs``."""
348365

349366
def __str__(self):
350367
return f"Binary.{super().__str__()}"

qiskit/qasm3/ast.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,10 @@ class Op(enum.Enum):
323323
NOT_EQUAL = "!="
324324
SHIFT_LEFT = "<<"
325325
SHIFT_RIGHT = ">>"
326+
ADD = "+"
327+
SUB = "-"
328+
MUL = "*"
329+
DIV = "/"
326330

327331
def __init__(self, op: Op, left: Expression, right: Expression):
328332
self.op = op

qiskit/qasm3/printer.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,12 @@
3939
ast.Unary.Op.LOGIC_NOT: _BindingPower(right=22),
4040
ast.Unary.Op.BIT_NOT: _BindingPower(right=22),
4141
#
42-
# Multiplication/division/modulo: (19, 20)
43-
# Addition/subtraction: (17, 18)
42+
# Modulo: (19, 20)
43+
ast.Binary.Op.MUL: _BindingPower(19, 20),
44+
ast.Binary.Op.DIV: _BindingPower(19, 20),
45+
#
46+
ast.Binary.Op.ADD: _BindingPower(17, 18),
47+
ast.Binary.Op.SUB: _BindingPower(17, 18),
4448
#
4549
ast.Binary.Op.SHIFT_LEFT: _BindingPower(15, 16),
4650
ast.Binary.Op.SHIFT_RIGHT: _BindingPower(15, 16),
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
features_circuits:
3+
- |
4+
The classical realtime-expressions module :mod:`qiskit.circuit.classical` can now represent
5+
arithmetic operations :func:`~.expr.add`, :func:`~.expr.sub`, :func:`~.expr.mul`,
6+
and :func:`~.expr.div` on numeric and timing operands.
7+
8+
For example::
9+
10+
from qiskit.circuit import QuantumCircuit, ClassicalRegister, Duration
11+
from qiskit.circuit.classical import expr
12+
13+
# Subtract two integers
14+
cr = ClassicalRegister(4, "cr")
15+
qc = QuantumCircuit(cr)
16+
with qc.if_test(expr.equal(expr.sub(cr, 2), 3)):
17+
pass
18+
19+
# Multiply a Duration by a Float
20+
with qc.if_test(expr.less(expr.mul(Duration.dt(200), 2.0), Duration.ns(500))):
21+
pass
22+
23+
# Divide a Duration by a Duration to get a Float
24+
with qc.if_test(expr.greater(expr.div(Duration.dt(200), Duration.dt(400)), 0.5)):
25+
pass
26+
27+
For additional examples, see the module-level documentation linked above.

0 commit comments

Comments
 (0)