Skip to content

Commit

Permalink
[Stretch] Add +, -, *, / operators for classical expressions. (
Browse files Browse the repository at this point in the history
…#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.
  • Loading branch information
kevinhartman authored Mar 5, 2025
1 parent bad3be0 commit 22daa04
Show file tree
Hide file tree
Showing 10 changed files with 663 additions and 4 deletions.
12 changes: 12 additions & 0 deletions qiskit/circuit/classical/expr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@
.. autofunction:: greater_equal
.. autofunction:: shift_left
.. autofunction:: shift_right
.. autofunction:: add
.. autofunction:: sub
.. autofunction:: mul
.. autofunction:: div
You can index into unsigned integers and bit-likes using another unsigned integer of any width.
This includes in storing operations, if the target of the index is writeable.
Expand Down Expand Up @@ -214,6 +218,10 @@
"greater",
"greater_equal",
"index",
"add",
"sub",
"mul",
"div",
"lift_legacy_condition",
]

Expand All @@ -238,5 +246,9 @@
shift_left,
shift_right,
index,
add,
sub,
mul,
div,
lift_legacy_condition,
)
210 changes: 210 additions & 0 deletions qiskit/circuit/classical/expr/constructors.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
"shift_left",
"shift_right",
"index",
"add",
"sub",
"mul",
"div",
"lift_legacy_condition",
]

Expand Down Expand Up @@ -564,3 +568,209 @@ def index(target: typing.Any, index: typing.Any, /) -> Expr:
if target.type.kind is not types.Uint or index.type.kind is not types.Uint:
raise TypeError(f"invalid types for indexing: '{target.type}' and '{index.type}'")
return Index(target, index, types.Bool())


def _binary_sum(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr:
left, right = _lift_binary_operands(left, right)
if left.type.kind is right.type.kind and left.type.kind in {
types.Uint,
types.Float,
types.Duration,
}:
type = types.greater(left.type, right.type)
return Binary(
op,
_coerce_lossless(left, type),
_coerce_lossless(right, type),
type,
)
raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'")


def add(left: typing.Any, right: typing.Any, /) -> Expr:
"""Create an addition expression node from the given values, resolving any implicit casts and
lifting the values into :class:`Value` nodes if required.
Examples:
Addition of two floating point numbers::
>>> from qiskit.circuit.classical import expr
>>> expr.add(5.0, 2.0)
Binary(\
Binary.Op.ADD, \
Value(5.0, Float()), \
Value(2.0, Float()), \
Float())
Addition of two durations::
>>> from qiskit.circuit import Duration
>>> from qiskit.circuit.classical import expr
>>> expr.add(Duration.dt(1000), Duration.dt(1000))
Binary(\
Binary.Op.ADD, \
Value(Duration.dt(1000), Duration()), \
Value(Duration.dt(1000), Duration()), \
Duration())
"""
return _binary_sum(Binary.Op.ADD, left, right)


def sub(left: typing.Any, right: typing.Any, /) -> Expr:
"""Create a subtraction expression node from the given values, resolving any implicit casts and
lifting the values into :class:`Value` nodes if required.
Examples:
Subtraction of two floating point numbers::
>>> from qiskit.circuit.classical import expr
>>> expr.sub(5.0, 2.0)
Binary(\
Binary.Op.SUB, \
Value(5.0, Float()), \
Value(2.0, Float()), \
Float())
Subtraction of two durations::
>>> from qiskit.circuit import Duration
>>> from qiskit.circuit.classical import expr
>>> expr.add(Duration.dt(1000), Duration.dt(1000))
Binary(\
Binary.Op.SUB, \
Value(Duration.dt(1000), Duration()), \
Value(Duration.dt(1000), Duration()), \
Duration())
"""
return _binary_sum(Binary.Op.SUB, left, right)


def mul(left: typing.Any, right: typing.Any) -> Expr:
"""Create a multiplication expression node from the given values, resolving any implicit casts and
lifting the values into :class:`Value` nodes if required.
This can be used to multiply numeric operands of the same type kind, or to multiply a duration
operand by a numeric operand.
Examples:
Multiplication of two floating point numbers::
>>> from qiskit.circuit.classical import expr
>>> expr.mul(5.0, 2.0)
Binary(\
Binary.Op.MUL, \
Value(5.0, Float()), \
Value(2.0, Float()), \
Float())
Multiplication of a duration by a float::
>>> from qiskit.circuit import Duration
>>> from qiskit.circuit.classical import expr
>>> expr.mul(Duration.dt(1000), 0.5)
Binary(\
Binary.Op.MUL, \
Value(Duration.dt(1000), Duration()), \
Value(0.5, Float()), \
Duration())
"""
left, right = _lift_binary_operands(left, right)
type: types.Type
if left.type.kind is right.type.kind is types.Duration:
raise TypeError("cannot multiply two durations")
if left.type.kind is right.type.kind and left.type.kind in {types.Uint, types.Float}:
type = types.greater(left.type, right.type)
left = _coerce_lossless(left, type)
right = _coerce_lossless(right, type)
elif left.type.kind is types.Duration and right.type.kind in {types.Uint, types.Float}:
if not right.const:
raise ValueError(
f"multiplying operands '{left}' and '{right}' would result in a non-const '{left.type}'"
)
type = left.type
elif right.type.kind is types.Duration and left.type.kind in {types.Uint, types.Float}:
if not left.const:
raise ValueError(
f"multiplying operands '{left}' and '{right}' would result in a non-const '{right.type}'"
)
type = right.type
else:
raise TypeError(f"invalid types for '{Binary.Op.MUL}': '{left.type}' and '{right.type}'")
return Binary(
Binary.Op.MUL,
left,
right,
type,
)


def div(left: typing.Any, right: typing.Any) -> Expr:
"""Create a division expression node from the given values, resolving any implicit casts and
lifting the values into :class:`Value` nodes if required.
This can be used to divide numeric operands of the same type kind, to divide a
:class`~.types.Duration` operand by a numeric operand, or to divide two
:class`~.types.Duration` operands which yields an expression of type
:class:`~.types.Float`.
Examples:
Division of two floating point numbers::
>>> from qiskit.circuit.classical import expr
>>> expr.div(5.0, 2.0)
Binary(\
Binary.Op.DIV, \
Value(5.0, Float()), \
Value(2.0, Float()), \
Float())
Division of two durations::
>>> from qiskit.circuit import Duration
>>> from qiskit.circuit.classical import expr
>>> expr.div(Duration.dt(10000), Duration.dt(1000))
Binary(\
Binary.Op.DIV, \
Value(Duration.dt(10000), Duration()), \
Value(Duration.dt(1000), Duration()), \
Float())
Division of a duration by a float::
>>> from qiskit.circuit import Duration
>>> from qiskit.circuit.classical import expr
>>> expr.div(Duration.dt(10000), 12.0)
Binary(\
Binary.Op.DIV, \
Value(Duration.dt(10000), Duration()), \
Value(12.0, types.Float()), \
Duration())
"""
left, right = _lift_binary_operands(left, right)
type: types.Type
if left.type.kind is right.type.kind and left.type.kind in {
types.Duration,
types.Uint,
types.Float,
}:
if left.type.kind is types.Duration:
type = types.Float()
elif types.order(left.type, right.type) is not types.Ordering.NONE:
type = types.greater(left.type, right.type)
left = _coerce_lossless(left, type)
right = _coerce_lossless(right, type)
elif left.type.kind is types.Duration and right.type.kind in {types.Uint, types.Float}:
if not right.const:
raise ValueError(
f"division of '{left}' and '{right}' would result in a non-const '{left.type}'"
)
type = left.type
else:
raise TypeError(f"invalid types for '{Binary.Op.DIV}': '{left.type}' and '{right.type}'")
return Binary(
Binary.Op.DIV,
left,
right,
type,
)
17 changes: 17 additions & 0 deletions qiskit/circuit/classical/expr/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,15 @@ class Op(enum.Enum):
container types (e.g. unsigned integers) as the left operand, and any integer type as the
right-hand operand. In all cases, the output bit width is the same as the input, and zeros
fill in the "exposed" spaces.
The binary arithmetic operators :data:`ADD`, :data:`SUB:, :data:`MUL`, and :data:`DIV`
can be applied to two floats or two unsigned integers, which should be made to be of
the same width during construction via a cast.
The :data:`ADD`, :data:`SUB`, and :data:`DIV` operators can be applied on two durations
yielding another duration, or a float in the case of :data:`DIV`. The :data:`MUL` operator
can also be applied to a duration and a numeric type, yielding another duration. Finally,
the :data:`DIV` operator can be used to divide a duration by a numeric type, yielding a
duration.
"""

# If adding opcodes, remember to add helper constructor functions in `constructors.py`
Expand Down Expand Up @@ -345,6 +354,14 @@ class Op(enum.Enum):
"""Zero-padding bitshift to the left. ``lhs << rhs``."""
SHIFT_RIGHT = 13
"""Zero-padding bitshift to the right. ``lhs >> rhs``."""
ADD = 14
"""Addition. ``lhs + rhs``."""
SUB = 15
"""Subtraction. ``lhs - rhs``."""
MUL = 16
"""Multiplication. ``lhs * rhs``."""
DIV = 17
"""Division. ``lhs / rhs``."""

def __str__(self):
return f"Binary.{super().__str__()}"
Expand Down
4 changes: 4 additions & 0 deletions qiskit/qasm3/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,10 @@ class Op(enum.Enum):
NOT_EQUAL = "!="
SHIFT_LEFT = "<<"
SHIFT_RIGHT = ">>"
ADD = "+"
SUB = "-"
MUL = "*"
DIV = "/"

def __init__(self, op: Op, left: Expression, right: Expression):
self.op = op
Expand Down
8 changes: 6 additions & 2 deletions qiskit/qasm3/printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,12 @@
ast.Unary.Op.LOGIC_NOT: _BindingPower(right=22),
ast.Unary.Op.BIT_NOT: _BindingPower(right=22),
#
# Multiplication/division/modulo: (19, 20)
# Addition/subtraction: (17, 18)
# Modulo: (19, 20)
ast.Binary.Op.MUL: _BindingPower(19, 20),
ast.Binary.Op.DIV: _BindingPower(19, 20),
#
ast.Binary.Op.ADD: _BindingPower(17, 18),
ast.Binary.Op.SUB: _BindingPower(17, 18),
#
ast.Binary.Op.SHIFT_LEFT: _BindingPower(15, 16),
ast.Binary.Op.SHIFT_RIGHT: _BindingPower(15, 16),
Expand Down
27 changes: 27 additions & 0 deletions releasenotes/notes/math-expr-a71515664473fdc4.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
features_circuits:
- |
The classical realtime-expressions module :mod:`qiskit.circuit.classical` can now represent
arithmetic operations :func:`~.expr.add`, :func:`~.expr.sub`, :func:`~.expr.mul`,
and :func:`~.expr.div` on numeric and timing operands.
For example::
from qiskit.circuit import QuantumCircuit, ClassicalRegister, Duration
from qiskit.circuit.classical import expr
# Subtract two integers
cr = ClassicalRegister(4, "cr")
qc = QuantumCircuit(cr)
with qc.if_test(expr.equal(expr.sub(cr, 2), 3)):
pass
# Multiply a Duration by a Float
with qc.if_test(expr.less(expr.mul(Duration.dt(200), 2.0), Duration.ns(500))):
pass
# Divide a Duration by a Duration to get a Float
with qc.if_test(expr.greater(expr.div(Duration.dt(200), Duration.dt(400)), 0.5)):
pass
For additional examples, see the module-level documentation linked above.
Loading

0 comments on commit 22daa04

Please sign in to comment.