diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 96ad322befe..851f2f9b83b 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -19,6 +19,7 @@ __all__ = [ "lift", + "cast", "bit_not", "logic_not", "bit_and", @@ -32,6 +33,9 @@ "less_equal", "greater", "greater_equal", + "shift_left", + "shift_right", + "index", "lift_legacy_condition", ] @@ -45,9 +49,9 @@ 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 @@ -55,9 +59,7 @@ def _coerce_lossless(expr: Expr, type: types.Type) -> Expr: 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( @@ -121,6 +123,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: @@ -181,11 +186,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]: @@ -300,9 +309,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: @@ -340,6 +353,8 @@ def _equal_like(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr: if left.type.kind is not right.type.kind: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) + # Note that we don't check the return value of _coerce_lossless for these + # since 'left' and 'right' are guaranteed to be the same kind here. return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool()) @@ -384,6 +399,8 @@ def _binary_relation(op: Binary.Op, left: typing.Any, right: typing.Any) -> Expr if left.type.kind is not right.type.kind or left.type.kind is types.Bool: raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") type = types.greater(left.type, right.type) + # Note that we don't check the return value of _coerce_lossless for these + # since 'left' and 'right' are guaranteed to be the same kind here. return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool()) @@ -465,7 +482,11 @@ def _shift_like( if type is not None and type.kind is not types.Uint: raise TypeError(f"type '{type}' is not a valid bitshift operand type") if isinstance(left, Expr): - left = _coerce_lossless(left, type) if type is not None else left + if type is not None: + coerced_left = _coerce_lossless(left, type) + if coerced_left is None: + raise TypeError(f"type '{type}' cannot losslessly represent '{left.type}'") + left = coerced_left else: left = lift(left, type) right = lift(right) diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index ae38a0d97fb..2ec3630bae9 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -33,12 +33,13 @@ 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. @@ -89,12 +90,16 @@ 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", + "Float", "Uint", "Ordering", "order", @@ -105,5 +110,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 diff --git a/qiskit/circuit/classical/types/ordering.py b/qiskit/circuit/classical/types/ordering.py index 5a4365b8e14..356c0f1a5a5 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -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 @@ -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 @@ -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, } @@ -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, } diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index d20e7b5fd74..31c2bdb44ba 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -22,6 +22,7 @@ __all__ = [ "Type", "Bool", + "Float", "Uint", ] @@ -115,3 +116,21 @@ def __hash__(self): def __eq__(self, other): return isinstance(other, Uint) and self.width == other.width + + +@typing.final +class Float(Type, metaclass=_Singleton): + """An IEEE-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) diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index c723c443fef..492b57d0f33 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -242,6 +242,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",) diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index cdbb59fbf65..0101e4501ce 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -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.DOUBLE raise RuntimeError(f"unhandled expr type '{type_}'") @@ -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, /): diff --git a/qiskit/qasm3/printer.py b/qiskit/qasm3/printer.py index 221c99bc4f9..c8c25d60235 100644 --- a/qiskit/qasm3/printer.py +++ b/qiskit/qasm3/printer.py @@ -78,7 +78,7 @@ class BasicPrinter: ast.QuantumGateModifierName.POW: "pow", } - _FLOAT_WIDTH_LOOKUP = {type: str(type.value) for type in ast.FloatType} + _FLOAT_TYPE_LOOKUP = {type: f"float[{type.value}]" for type in ast.FloatType} # The visitor names include the class names, so they mix snake_case with PascalCase. # pylint: disable=invalid-name @@ -205,7 +205,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") @@ -282,6 +282,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") diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 208fa8e44c1..c16ecbb89ff 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -377,6 +377,37 @@ def open(*args): by ``num_circuits`` in the file header). There is no padding between the circuits in the data. +.. _qpy_version_14: + +Version 14 +---------- + +Version 14 adds support for additional :class:`~.types.Type` classes. + +Changes to EXPR_TYPE +~~~~~~~~~~~~~~~~~~~~ + +The following table shows the new type classes added in the version: + +====================== ========= ================================================================= +Qiskit class Type code Payload +====================== ========= ================================================================= +:class:`~.types.Float` ``f`` None. +====================== ========= ================================================================= + +Changes to EXPR_VALUE +~~~~~~~~~~~~~~~~~~~~~ + +The classical expression's type system now supports new encoding types for value literals, in +addition to the existing encodings for int and bool. The new value type encodings are below: + +=========== ========= ============================================================================ +Python type Type code Payload +=========== ========= ============================================================================ +``float`` ``f`` One ``double value``. + +=========== ========= ============================================================================ + .. _qpy_version_13: Version 13 diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 2297dbf6d34..6b9f253b367 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -1236,7 +1236,7 @@ def write_circuit( file_obj.write(metadata_raw) # Write header payload file_obj.write(registers_raw) - standalone_var_indices = value.write_standalone_vars(file_obj, circuit) + standalone_var_indices = value.write_standalone_vars(file_obj, circuit, version) else: if circuit.num_vars: raise exceptions.UnsupportedFeatureForVersion( diff --git a/qiskit/qpy/binary_io/value.py b/qiskit/qpy/binary_io/value.py index 9799fdf3f45..1e1bb61e8db 100644 --- a/qiskit/qpy/binary_io/value.py +++ b/qiskit/qpy/binary_io/value.py @@ -261,12 +261,15 @@ def __init__(self, file_obj, clbit_indices, standalone_var_indices, version): self.standalone_var_indices = standalone_var_indices self.version = version + def _write_expr_type(self, type_, /): + _write_expr_type(self.file_obj, type_, self.version) + def visit_generic(self, node, /): raise exceptions.QpyError(f"unhandled Expr object '{node}'") def visit_var(self, node, /): self.file_obj.write(type_keys.Expression.VAR) - _write_expr_type(self.file_obj, node.type) + self._write_expr_type(node.type) if node.standalone: self.file_obj.write(type_keys.ExprVar.UUID) self.file_obj.write( @@ -296,7 +299,7 @@ def visit_var(self, node, /): def visit_value(self, node, /): self.file_obj.write(type_keys.Expression.VALUE) - _write_expr_type(self.file_obj, node.type) + self._write_expr_type(node.type) if node.value is True or node.value is False: self.file_obj.write(type_keys.ExprValue.BOOL) self.file_obj.write( @@ -317,12 +320,17 @@ def visit_value(self, node, /): struct.pack(formats.EXPR_VALUE_INT_PACK, *formats.EXPR_VALUE_INT(num_bytes)) ) self.file_obj.write(buffer) + elif isinstance(node.value, float): + self.file_obj.write(type_keys.ExprValue.FLOAT) + self.file_obj.write( + struct.pack(formats.EXPR_VALUE_FLOAT_PACK, *formats.EXPR_VALUE_FLOAT(node.value)) + ) else: raise exceptions.QpyError(f"unhandled Value object '{node.value}'") def visit_cast(self, node, /): self.file_obj.write(type_keys.Expression.CAST) - _write_expr_type(self.file_obj, node.type) + self._write_expr_type(node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_CAST_PACK, *formats.EXPRESSION_CAST(node.implicit)) ) @@ -330,7 +338,7 @@ def visit_cast(self, node, /): def visit_unary(self, node, /): self.file_obj.write(type_keys.Expression.UNARY) - _write_expr_type(self.file_obj, node.type) + self._write_expr_type(node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_UNARY_PACK, *formats.EXPRESSION_UNARY(node.op.value)) ) @@ -338,7 +346,7 @@ def visit_unary(self, node, /): def visit_binary(self, node, /): self.file_obj.write(type_keys.Expression.BINARY) - _write_expr_type(self.file_obj, node.type) + self._write_expr_type(node.type) self.file_obj.write( struct.pack(formats.EXPRESSION_BINARY_PACK, *formats.EXPRESSION_BINARY(node.op.value)) ) @@ -351,7 +359,7 @@ def visit_index(self, node, /): "the 'Index' expression", required=12, target=self.version ) self.file_obj.write(type_keys.Expression.INDEX) - _write_expr_type(self.file_obj, node.type) + self._write_expr_type(node.type) node.target.accept(self) node.index.accept(self) @@ -366,7 +374,7 @@ def _write_expr( node.accept(_ExprWriter(file_obj, clbit_indices, standalone_var_indices, version)) -def _write_expr_type(file_obj, type_: types.Type): +def _write_expr_type(file_obj, type_: types.Type, version: int): if type_.kind is types.Bool: file_obj.write(type_keys.ExprType.BOOL) elif type_.kind is types.Uint: @@ -374,6 +382,12 @@ def _write_expr_type(file_obj, type_: types.Type): file_obj.write( struct.pack(formats.EXPR_TYPE_UINT_PACK, *formats.EXPR_TYPE_UINT(type_.width)) ) + elif type_.kind is types.Float: + if version < 14: + raise exceptions.UnsupportedFeatureForVersion( + "float-typed expressions", required=14, target=version + ) + file_obj.write(type_keys.ExprType.FLOAT) else: raise exceptions.QpyError(f"unhandled Type object '{type_};") @@ -680,6 +694,13 @@ def _read_expr( return expr.Value( int.from_bytes(file_obj.read(payload.num_bytes), "big", signed=True), type_ ) + if value_type_key == type_keys.ExprValue.FLOAT: + payload = formats.EXPR_VALUE_FLOAT._make( + struct.unpack( + formats.EXPR_VALUE_FLOAT_PACK, file_obj.read(formats.EXPR_VALUE_FLOAT_SIZE) + ) + ) + return expr.Value(payload.value, type_) raise exceptions.QpyError("Invalid classical-expression Value key '{value_type_key}'") if type_key == type_keys.Expression.CAST: payload = formats.EXPRESSION_CAST._make( @@ -729,6 +750,8 @@ def _read_expr_type(file_obj) -> types.Type: struct.unpack(formats.EXPR_TYPE_UINT_PACK, file_obj.read(formats.EXPR_TYPE_UINT_SIZE)) ) return types.Uint(elem.width) + if type_key == type_keys.ExprType.FLOAT: + return types.Float() raise exceptions.QpyError(f"Invalid classical-expression Type key '{type_key}'") @@ -765,7 +788,7 @@ def read_standalone_vars(file_obj, num_vars): return read_vars, var_order -def _write_standalone_var(file_obj, var, type_key): +def _write_standalone_var(file_obj, var, type_key, version): name = var.name.encode(common.ENCODE) file_obj.write( struct.pack( @@ -773,16 +796,17 @@ def _write_standalone_var(file_obj, var, type_key): *formats.EXPR_VAR_DECLARATION(var.var.bytes, type_key, len(name)), ) ) - _write_expr_type(file_obj, var.type) + _write_expr_type(file_obj, var.type, version) file_obj.write(name) -def write_standalone_vars(file_obj, circuit): +def write_standalone_vars(file_obj, circuit, version): """Write the standalone variables out from a circuit. Args: file_obj (File): the file-like object to write to. circuit (QuantumCircuit): the circuit to take the variables from. + version (int): the QPY target version. Returns: dict[expr.Var, int]: a mapping of the variables written to the index that they were written @@ -791,15 +815,15 @@ def write_standalone_vars(file_obj, circuit): index = 0 out = {} for var in circuit.iter_input_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.INPUT) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.INPUT, version) out[var] = index index += 1 for var in circuit.iter_captured_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.CAPTURE) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.CAPTURE, version) out[var] = index index += 1 for var in circuit.iter_declared_vars(): - _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.LOCAL) + _write_standalone_var(file_obj, var, type_keys.ExprVarDeclaration.LOCAL, version) out[var] = index index += 1 return out diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index c2585d5be4d..0f30e90cc34 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -25,7 +25,7 @@ from qiskit.qpy import formats, exceptions -QPY_VERSION = 13 +QPY_VERSION = 14 QPY_COMPATIBILITY_VERSION = 10 ENCODE = "utf8" diff --git a/qiskit/qpy/formats.py b/qiskit/qpy/formats.py index 7696cae94e2..45525a381eb 100644 --- a/qiskit/qpy/formats.py +++ b/qiskit/qpy/formats.py @@ -362,6 +362,10 @@ EXPR_TYPE_DISCRIMINATOR_SIZE = 1 +EXPR_TYPE_FLOAT = namedtuple("EXPR_TYPE_FLOAT", []) +EXPR_TYPE_FLOAT_PACK = "!" +EXPR_TYPE_FLOAT_SIZE = struct.calcsize(EXPR_TYPE_FLOAT_PACK) + EXPR_TYPE_BOOL = namedtuple("EXPR_TYPE_BOOL", []) EXPR_TYPE_BOOL_PACK = "!" EXPR_TYPE_BOOL_SIZE = struct.calcsize(EXPR_TYPE_BOOL_PACK) @@ -392,6 +396,10 @@ EXPR_VALUE_DISCRIMINATOR_SIZE = 1 +EXPR_VALUE_FLOAT = namedtuple("EXPR_VALUE_FLOAT", ["value"]) +EXPR_VALUE_FLOAT_PACK = "!d" +EXPR_VALUE_FLOAT_SIZE = struct.calcsize(EXPR_VALUE_FLOAT_PACK) + EXPR_VALUE_BOOL = namedtuple("EXPR_VALUE_BOOL", ["value"]) EXPR_VALUE_BOOL_PACK = "!?" EXPR_VALUE_BOOL_SIZE = struct.calcsize(EXPR_VALUE_BOOL_PACK) diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index b4dc6c4cd46..b7a497cb5c5 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -287,6 +287,7 @@ class ExprType(TypeKeyBase): BOOL = b"b" UINT = b"u" + FLOAT = b"f" @classmethod def assign(cls, obj): @@ -331,6 +332,7 @@ class ExprValue(TypeKeyBase): BOOL = b"b" INT = b"i" + FLOAT = b"f" @classmethod def assign(cls, obj): diff --git a/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml b/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml new file mode 100644 index 00000000000..02a3d1dcb16 --- /dev/null +++ b/releasenotes/notes/float-expr-02b01d9ea89ad47a.yaml @@ -0,0 +1,17 @@ +--- +features_circuits: + - | + The classical realtime-expressions module :mod:`qiskit.circuit.classical` can now represent + IEEE-754 double-precision floating point values using the new type :class:`~.types.Float`. + + The :func:`~.expr.lift` function can be used to create a value expression from a Python + float:: + + from qiskit.circuit.classical import expr + + expr.lift(5.0) + # >>> Value(5.0, Float()) + + This type is intended primarily for use in timing-related (duration and stretch) + expressions. It is not compatible with bitwise or logical operations, though it + can be used (dangerously) with these if first explicitly cast to something else. diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py index ff172d7e000..a3510a65722 100644 --- a/test/python/circuit/classical/test_expr_constructors.py +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -70,6 +70,7 @@ def test_value_lifts_python_builtins(self): self.assertEqual(expr.lift(True), expr.Value(True, types.Bool())) self.assertEqual(expr.lift(False), expr.Value(False, types.Bool())) self.assertEqual(expr.lift(7), expr.Value(7, types.Uint(3))) + self.assertEqual(expr.lift(7.0), expr.Value(7.0, types.Float())) def test_value_ensures_nonzero_width(self): self.assertEqual(expr.lift(0), expr.Value(0, types.Uint(1))) @@ -84,6 +85,8 @@ def test_value_type_representation(self): def test_value_does_not_allow_downcast(self): with self.assertRaisesRegex(TypeError, "the explicit type .* is not suitable"): expr.lift(0xFF, types.Uint(2)) + with self.assertRaisesRegex(TypeError, "the explicit type .* is not suitable"): + expr.lift(1.1, types.Uint(2)) def test_value_rejects_bad_values(self): with self.assertRaisesRegex(TypeError, "failed to infer a type"): @@ -108,6 +111,9 @@ def test_cast_allows_lossy_downcasting(self): self.assertEqual( expr.cast(base, types.Bool()), expr.Cast(base, types.Bool(), implicit=False) ) + self.assertEqual( + expr.cast(base, types.Float()), expr.Cast(base, types.Float(), implicit=False) + ) @ddt.data( (expr.bit_not, ClassicalRegister(3)), @@ -133,6 +139,11 @@ def test_bit_not_explicit(self): expr.Unary(expr.Unary.Op.BIT_NOT, expr.Var(clbit, types.Bool()), types.Bool()), ) + @ddt.data(expr.bit_not) + def test_unary_bitwise_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "cannot apply"): + function(7.0) + def test_logic_not_explicit(self): cr = ClassicalRegister(3) self.assertEqual( @@ -149,6 +160,11 @@ def test_logic_not_explicit(self): expr.Unary(expr.Unary.Op.LOGIC_NOT, expr.Var(clbit, types.Bool()), types.Bool()), ) + @ddt.data(expr.logic_not) + def test_unary_logical_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "cannot apply"): + function(7.0) + @ddt.data( (expr.bit_and, ClassicalRegister(3), ClassicalRegister(3)), (expr.bit_or, ClassicalRegister(3), ClassicalRegister(3)), @@ -161,6 +177,12 @@ def test_logic_not_explicit(self): (expr.less_equal, ClassicalRegister(3), 5), (expr.greater, 4, ClassicalRegister(3)), (expr.greater_equal, ClassicalRegister(3), 5), + (expr.equal, 8.0, 255.0), + (expr.not_equal, 8.0, 255.0), + (expr.less, 3.0, 6.0), + (expr.less_equal, 3.0, 5.0), + (expr.greater, 4.0, 3.0), + (expr.greater_equal, 3.0, 5.0), ) @ddt.unpack def test_binary_functions_lift_scalars(self, function, left, right): @@ -272,6 +294,12 @@ def test_binary_bitwise_uint_inference(self, function, opcode): def test_binary_bitwise_forbidden(self, function): with self.assertRaisesRegex(TypeError, "invalid types"): function(ClassicalRegister(3, "c"), Clbit()) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3.0, 3.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3, 3.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3.0, 3) # Unlike most other functions, the bitwise functions should error if the two bit-like types # aren't of the same width, except for the special inference for integer literals. with self.assertRaisesRegex(TypeError, "binary bitwise operations .* same width"): @@ -319,6 +347,15 @@ def test_binary_logical_explicit(self, function, opcode): ) self.assertFalse(function(False, clbit).const) + @ddt.data(expr.logic_and, expr.logic_or) + def test_binary_logic_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3.0, 3.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3, 3.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(3.0, 3) + @ddt.data( (expr.equal, expr.Binary.Op.EQUAL), (expr.not_equal, expr.Binary.Op.NOT_EQUAL), @@ -358,6 +395,17 @@ def test_binary_equal_explicit(self, function, opcode): ) self.assertFalse(function(clbit, True).const) + self.assertEqual( + function(expr.lift(7.0), 7.0), + expr.Binary( + opcode, + expr.Value(7.0, types.Float()), + expr.Value(7.0, types.Float()), + types.Bool(), + ), + ) + self.assertFalse(function(clbit, True).const) + @ddt.data(expr.equal, expr.not_equal) def test_binary_equal_forbidden(self, function): with self.assertRaisesRegex(TypeError, "invalid types"): @@ -366,6 +414,12 @@ def test_binary_equal_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(5, True) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(True, 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(5, 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(ClassicalRegister(3, "c"), 5.0) @ddt.data( (expr.less, expr.Binary.Op.LESS), @@ -396,6 +450,17 @@ def test_binary_relation_explicit(self, function, opcode): ) self.assertFalse(function(12, cr).const) + self.assertEqual( + function(expr.lift(12.0, types.Float()), expr.lift(12.0)), + expr.Binary( + opcode, + expr.Value(12.0, types.Float()), + expr.Value(12.0, types.Float()), + types.Bool(), + ), + ) + self.assertFalse(function(12, cr).const) + @ddt.data(expr.less, expr.less_equal, expr.greater, expr.greater_equal) def test_binary_relation_forbidden(self, function): with self.assertRaisesRegex(TypeError, "invalid types"): @@ -404,6 +469,12 @@ def test_binary_relation_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(Clbit(), Clbit()) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(True, 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(5, 5.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(ClassicalRegister(3, "c"), 5.0) def test_index_explicit(self): cr = ClassicalRegister(4, "c") @@ -432,6 +503,12 @@ def test_index_forbidden(self): expr.index(Clbit(), 3) with self.assertRaisesRegex(TypeError, "invalid types"): expr.index(ClassicalRegister(3, "a"), False) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(ClassicalRegister(3, "a"), 1.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(0xFFFF, 1.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + expr.index(ClassicalRegister(3, "a"), 1.0) @ddt.data( (expr.shift_left, expr.Binary.Op.SHIFT_LEFT), @@ -472,3 +549,9 @@ def test_shift_forbidden(self, function): function(ClassicalRegister(3, "c"), False) with self.assertRaisesRegex(TypeError, "invalid types"): function(Clbit(), Clbit()) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(0xFFFF, 2.0) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(255.0, 1) + with self.assertRaisesRegex(TypeError, "cannot losslessly represent"): + function(expr.lift(5.0), 3, types.Uint(8)) diff --git a/test/python/circuit/classical/test_types_ordering.py b/test/python/circuit/classical/test_types_ordering.py index 16c3791f70f..67df997d253 100644 --- a/test/python/circuit/classical/test_types_ordering.py +++ b/test/python/circuit/classical/test_types_ordering.py @@ -24,8 +24,14 @@ def test_order(self): self.assertIs(types.order(types.Bool(), types.Bool()), types.Ordering.EQUAL) + self.assertIs(types.order(types.Float(), types.Float()), types.Ordering.EQUAL) + self.assertIs(types.order(types.Bool(), types.Uint(8)), types.Ordering.NONE) + self.assertIs(types.order(types.Bool(), types.Float()), types.Ordering.NONE) self.assertIs(types.order(types.Uint(8), types.Bool()), types.Ordering.NONE) + self.assertIs(types.order(types.Uint(8), types.Float()), types.Ordering.NONE) + self.assertIs(types.order(types.Float(), types.Uint(8)), types.Ordering.NONE) + self.assertIs(types.order(types.Float(), types.Bool()), types.Ordering.NONE) def test_is_subtype(self): self.assertTrue(types.is_subtype(types.Uint(8), types.Uint(16))) @@ -36,8 +42,15 @@ def test_is_subtype(self): self.assertTrue(types.is_subtype(types.Bool(), types.Bool())) self.assertFalse(types.is_subtype(types.Bool(), types.Bool(), strict=True)) + self.assertTrue(types.is_subtype(types.Float(), types.Float())) + self.assertFalse(types.is_subtype(types.Float(), types.Float(), strict=True)) + self.assertFalse(types.is_subtype(types.Bool(), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Bool(), types.Float())) self.assertFalse(types.is_subtype(types.Uint(8), types.Bool())) + self.assertFalse(types.is_subtype(types.Uint(8), types.Float())) + self.assertFalse(types.is_subtype(types.Float(), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Float(), types.Bool())) def test_is_supertype(self): self.assertFalse(types.is_supertype(types.Uint(8), types.Uint(16))) @@ -48,14 +61,22 @@ def test_is_supertype(self): self.assertTrue(types.is_supertype(types.Bool(), types.Bool())) self.assertFalse(types.is_supertype(types.Bool(), types.Bool(), strict=True)) + self.assertTrue(types.is_supertype(types.Float(), types.Float())) + self.assertFalse(types.is_supertype(types.Float(), types.Float(), strict=True)) + self.assertFalse(types.is_supertype(types.Bool(), types.Uint(8))) + self.assertFalse(types.is_supertype(types.Bool(), types.Float())) self.assertFalse(types.is_supertype(types.Uint(8), types.Bool())) + self.assertFalse(types.is_supertype(types.Uint(8), types.Float())) + self.assertFalse(types.is_supertype(types.Float(), types.Uint(8))) + self.assertFalse(types.is_supertype(types.Float(), types.Bool())) def test_greater(self): self.assertEqual(types.greater(types.Uint(16), types.Uint(8)), types.Uint(16)) self.assertEqual(types.greater(types.Uint(8), types.Uint(16)), types.Uint(16)) self.assertEqual(types.greater(types.Uint(8), types.Uint(8)), types.Uint(8)) self.assertEqual(types.greater(types.Bool(), types.Bool()), types.Bool()) + self.assertEqual(types.greater(types.Float(), types.Float()), types.Float()) with self.assertRaisesRegex(TypeError, "no ordering"): types.greater(types.Bool(), types.Uint(8)) @@ -65,6 +86,14 @@ def test_basic_examples(self): """This is used extensively throughout the expression construction functions, but since it is public API, it should have some direct unit tests as well.""" self.assertIs(types.cast_kind(types.Bool(), types.Bool()), types.CastKind.EQUAL) + self.assertIs(types.cast_kind(types.Float(), types.Float()), types.CastKind.EQUAL) self.assertIs(types.cast_kind(types.Uint(8), types.Bool()), types.CastKind.IMPLICIT) + self.assertIs(types.cast_kind(types.Float(), types.Bool()), types.CastKind.DANGEROUS) + self.assertIs(types.cast_kind(types.Bool(), types.Uint(8)), types.CastKind.LOSSLESS) self.assertIs(types.cast_kind(types.Uint(16), types.Uint(8)), types.CastKind.DANGEROUS) + self.assertIs(types.cast_kind(types.Bool(), types.Float()), types.CastKind.LOSSLESS) + + self.assertIs(types.cast_kind(types.Uint(16), types.Uint(8)), types.CastKind.DANGEROUS) + self.assertIs(types.cast_kind(types.Uint(16), types.Float()), types.CastKind.DANGEROUS) + self.assertIs(types.cast_kind(types.Float(), types.Uint(16)), types.CastKind.DANGEROUS) diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 3fff7ddf3e0..de4b14ad310 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -1911,6 +1911,20 @@ def test_pre_v12_rejects_index(self, version): ): dump(qc, fptr, version=version) + @ddt.idata(range(QPY_COMPATIBILITY_VERSION, 14)) + def test_pre_v14_rejects_float_typed_expr(self, version): + """Test that dumping to older QPY versions rejects float-typed expressions.""" + qc = QuantumCircuit() + with qc.if_test(expr.less(1.0, 2.0)): + pass + with ( + io.BytesIO() as fptr, + self.assertRaisesRegex( + UnsupportedFeatureForVersion, "version 14 is required.*float-typed expressions" + ), + ): + dump(qc, fptr, version=version) + class TestSymengineLoadFromQPY(QiskitTestCase): """Test use of symengine in qpy set of methods.""" diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index b834e5c7a49..e275076b5cb 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -2230,6 +2230,8 @@ def _control_flow_expr_circuit(self): base.cz(1, 4) base.append(CustomCX(), [2, 4]) base.append(CustomCX(), [3, 4]) + with base.if_test(expr.less(1.0, 2.0)): + base.cx(0, 1) return base def _standalone_var_circuit(self): diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 7b438ec6823..c95cdee4b55 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -1767,6 +1767,7 @@ def test_var_use(self): qc.add_var("c", expr.bit_not(b)) # All inputs should come first, regardless of declaration order. qc.add_input("d", types.Bool()) + qc.add_var("e", expr.lift(7.5)) expected = """\ OPENQASM 3.0; @@ -1775,9 +1776,11 @@ def test_var_use(self): input uint[8] b; input bool d; uint[8] c; +float[64] e; a = !a; b = b & 8; c = ~b; +e = 7.5; """ self.assertEqual(dumps(qc), expected) diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index eeca4bb041d..a87b0372429 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -833,6 +833,17 @@ def generate_v12_expr(): return [index, shift] +def generate_v14_expr(): + """Circuits that contain expressions and types new in QPY v14.""" + from qiskit.circuit.classical import expr, types + + float_expr = QuantumCircuit(name="float_expr") + with float_expr.if_test(expr.less(1.0, 2.0)): + pass + + return [float_expr] + + def generate_circuits(version_parts, current_version, load_context=False): """Generate reference circuits. @@ -898,6 +909,8 @@ def generate_circuits(version_parts, current_version, load_context=False): if version_parts >= (1, 1, 0): output_circuits["standalone_vars.qpy"] = generate_standalone_var() output_circuits["v12_expr.qpy"] = generate_v12_expr() + if version_parts >= (2, 0, 0): + output_circuits["v14_expr.qpy"] = generate_v14_expr() return output_circuits