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 Float type in classical expressions. #13832

Merged
merged 65 commits into from
Mar 1, 2025
Merged
Show file tree
Hide file tree
Changes from 61 commits
Commits
Show all changes
65 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
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
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
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
6bc728a
Fix comment and release note.
kevinhartman Feb 27, 2025
42258b8
Merge branch 'main' of github.com:Qiskit/qiskit into expr-float
kevinhartman Feb 28, 2025
cbd55db
Address review comments.
kevinhartman Feb 28, 2025
f12afb4
Update release note.
kevinhartman Feb 28, 2025
0f14cca
Address review comments.
kevinhartman Mar 1, 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
6 changes: 5 additions & 1 deletion qiskit/circuit/classical/expr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,18 @@

These objects are mutable and should not be reused in a different location without a copy.

All :class`Expr` instances define a boolean :attr:`~Expr.const` attribute, which indicates
whether the expression can be evaluated at compile-time. Most expression classes infer this
during construction based on the const-ness of their operands.

The base for dynamic variables is the :class:`Var`, which can be either an arbitrarily typed
real-time variable, or a wrapper around a :class:`.Clbit` or :class:`.ClassicalRegister`.

.. autoclass:: Var
:members: var, name, new

Similarly, literals used in expressions (such as integers) should be lifted to :class:`Value` nodes
with associated types.
with associated types. A :class:`Value` is always considered a constant expression.

.. autoclass:: Value

Expand Down
29 changes: 23 additions & 6 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 Down Expand Up @@ -121,6 +125,9 @@ 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
else:
raise TypeError(f"failed to infer a type for '{value}'")
if type is None:
Expand Down Expand Up @@ -181,11 +188,16 @@ 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)
try:
operand = _coerce_lossless(operand, types.Bool())
return Unary(Unary.Op.LOGIC_NOT, operand, operand.type)
except TypeError as ex:
raise TypeError(f"cannot apply '{Unary.Op.BIT_NOT}' to type '{operand.type}'") from ex


def _lift_binary_operands(left: typing.Any, right: typing.Any) -> tuple[Expr, Expr]:
Expand Down Expand Up @@ -300,9 +312,14 @@ 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)
try:
left = _coerce_lossless(left, bool_)
right = _coerce_lossless(right, bool_)
return Binary(op, left, right, bool_)
except TypeError as ex:
raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") from ex


def logic_and(left: typing.Any, right: typing.Any, /) -> Expr:
Expand Down
19 changes: 16 additions & 3 deletions qiskit/circuit/classical/expr/expr.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ class Expr(abc.ABC):
All subclasses are responsible for setting their ``type`` attribute in their ``__init__``, and
should not call the parent initializer."""

__slots__ = ("type",)
__slots__ = ("type", "const")

type: types.Type
const: bool

# Sentinel to prevent instantiation of the base class.
@abc.abstractmethod
Expand Down Expand Up @@ -89,6 +90,7 @@ class Cast(Expr):

def __init__(self, operand: Expr, type: types.Type, implicit: bool = False):
self.type = type
self.const = operand.const
self.operand = operand
self.implicit = implicit

Expand All @@ -99,6 +101,7 @@ def __eq__(self, other):
return (
isinstance(other, Cast)
and self.type == other.type
and self.const == other.const
and self.operand == other.operand
and self.implicit == other.implicit
)
Expand Down Expand Up @@ -141,6 +144,7 @@ def __init__(
name: str | None = None,
):
super().__setattr__("type", type)
super().__setattr__("const", False)
super().__setattr__("var", var)
super().__setattr__("name", name)

Expand All @@ -151,8 +155,9 @@ def new(cls, name: str, type: types.Type) -> typing.Self:

@property
def standalone(self) -> bool:
"""Whether this :class:`Var` is a standalone variable that owns its storage location. If
false, this is a wrapper :class:`Var` around a pre-existing circuit object."""
"""Whether this :class:`Var` is a standalone variable that owns its storage
location, if applicable. If false, this is a wrapper :class:`Var` around a
pre-existing circuit object."""
return isinstance(self.var, uuid.UUID)

def accept(self, visitor, /):
Expand Down Expand Up @@ -185,6 +190,7 @@ def __getstate__(self):
def __setstate__(self, state):
var, type, name = state
super().__setattr__("type", type)
super().__setattr__("const", False)
super().__setattr__("var", var)
super().__setattr__("name", name)

Expand All @@ -206,6 +212,7 @@ class Value(Expr):
def __init__(self, value: typing.Any, type: types.Type):
self.type = type
self.value = value
self.const = True

def accept(self, visitor, /):
return visitor.visit_value(self)
Expand Down Expand Up @@ -257,6 +264,7 @@ def __init__(self, op: Unary.Op, operand: Expr, type: types.Type):
self.op = op
self.operand = operand
self.type = type
self.const = operand.const

def accept(self, visitor, /):
return visitor.visit_unary(self)
Expand All @@ -265,6 +273,7 @@ def __eq__(self, other):
return (
isinstance(other, Unary)
and self.type == other.type
and self.const == other.const
and self.op is other.op
and self.operand == other.operand
)
Expand Down Expand Up @@ -348,6 +357,7 @@ def __init__(self, op: Binary.Op, left: Expr, right: Expr, type: types.Type):
self.left = left
self.right = right
self.type = type
self.const = left.const and right.const

def accept(self, visitor, /):
return visitor.visit_binary(self)
Expand All @@ -356,6 +366,7 @@ def __eq__(self, other):
return (
isinstance(other, Binary)
and self.type == other.type
and self.const == other.const
and self.op is other.op
and self.left == other.left
and self.right == other.right
Expand All @@ -381,6 +392,7 @@ def __init__(self, target: Expr, index: Expr, type: types.Type):
self.target = target
self.index = index
self.type = type
self.const = target.const and index.const

def accept(self, visitor, /):
return visitor.visit_index(self)
Expand All @@ -389,6 +401,7 @@ def __eq__(self, other):
return (
isinstance(other, Index)
and self.type == other.type
and self.const == other.const
and self.target == other.target
and self.index == other.index
)
Expand Down
3 changes: 2 additions & 1 deletion qiskit/circuit/classical/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
__all__ = [
"Type",
"Bool",
"Float",
"Uint",
"Ordering",
"order",
Expand All @@ -105,5 +106,5 @@
"cast_kind",
]

from .types import Type, Bool, Uint
from .types import Type, Bool, Float, Uint
from .ordering import Ordering, order, is_subtype, is_supertype, greater, CastKind, cast_kind
14 changes: 8 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, Float, 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,9 @@ 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,
}


Expand Down Expand Up @@ -195,8 +192,13 @@ 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,
}


Expand Down
24 changes: 19 additions & 5 deletions qiskit/circuit/classical/types/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,7 @@

from __future__ import annotations

__all__ = [
"Type",
"Bool",
"Uint",
]
__all__ = ["Type", "Bool", "Float", "Uint"]

import typing

Expand Down Expand Up @@ -115,3 +111,21 @@ def __hash__(self):

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


@typing.final
class Float(Type, metaclass=_Singleton):
"""A floating point number of unspecified width.
In the future, this may also be used to represent a fixed-width float.
"""

__slots__ = ()

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

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

def __eq__(self, other):
return isinstance(other, Float)
8 changes: 8 additions & 0 deletions qiskit/qasm3/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class ClassicalType(ASTNode):
class FloatType(ClassicalType, enum.Enum):
"""Allowed values for the width of floating-point types."""

UNSPECIFIED = 0
HALF = 16
SINGLE = 32
DOUBLE = 64
Expand Down Expand Up @@ -242,6 +243,13 @@ def __init__(self, value):
self.value = value


class FloatLiteral(Expression):
__slots__ = ("value",)

def __init__(self, value):
self.value = value


class BooleanLiteral(Expression):
__slots__ = ("value",)

Expand Down
4 changes: 4 additions & 0 deletions qiskit/qasm3/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -1253,6 +1253,8 @@ def _build_ast_type(type_: types.Type) -> ast.ClassicalType:
return ast.BoolType()
if type_.kind is types.Uint:
return ast.UintType(type_.width)
if type_.kind is types.Float:
return ast.FloatType.UNSPECIFIED
raise RuntimeError(f"unhandled expr type '{type_}'")


Expand All @@ -1274,6 +1276,8 @@ def visit_value(self, node, /):
return ast.BooleanLiteral(node.value)
if node.type.kind is types.Uint:
return ast.IntegerLiteral(node.value)
if node.type.kind is types.Float:
return ast.FloatLiteral(node.value)
raise RuntimeError(f"unhandled Value type '{node}'")

def visit_cast(self, node, /):
Expand Down
9 changes: 7 additions & 2 deletions qiskit/qasm3/printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ class BasicPrinter:
ast.QuantumGateModifierName.POW: "pow",
}

_FLOAT_WIDTH_LOOKUP = {type: str(type.value) for type in ast.FloatType}
_FLOAT_TYPE_LOOKUP = {ast.FloatType.UNSPECIFIED: "float"} | {
type: f"float[{type.value}]" for type in ast.FloatType if type.value > 0
}

# The visitor names include the class names, so they mix snake_case with PascalCase.
# pylint: disable=invalid-name
Expand Down Expand Up @@ -205,7 +207,7 @@ def _visit_CalibrationGrammarDeclaration(self, node: ast.CalibrationGrammarDecla
self._write_statement(f'defcalgrammar "{node.name}"')

def _visit_FloatType(self, node: ast.FloatType) -> None:
self.stream.write(f"float[{self._FLOAT_WIDTH_LOOKUP[node]}]")
self.stream.write(self._FLOAT_TYPE_LOOKUP[node])

def _visit_BoolType(self, _node: ast.BoolType) -> None:
self.stream.write("bool")
Expand Down Expand Up @@ -282,6 +284,9 @@ def _visit_QuantumDelay(self, node: ast.QuantumDelay) -> None:
def _visit_IntegerLiteral(self, node: ast.IntegerLiteral) -> None:
self.stream.write(str(node.value))

def _visit_FloatLiteral(self, node: ast.FloatLiteral) -> None:
self.stream.write(str(node.value))

def _visit_BooleanLiteral(self, node: ast.BooleanLiteral):
self.stream.write("true" if node.value else "false")

Expand Down
Loading
Loading