diff --git a/docs/apidocs/circuit_classical.rst b/docs/apidocs/circuit_classical.rst new file mode 100644 index 000000000000..65ec7db9c122 --- /dev/null +++ b/docs/apidocs/circuit_classical.rst @@ -0,0 +1,6 @@ +.. _qiskit-circuit-classical: + +.. automodule:: qiskit.circuit.classical + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/docs/apidocs/terra.rst b/docs/apidocs/terra.rst index c78a22f68678..33795079383e 100644 --- a/docs/apidocs/terra.rst +++ b/docs/apidocs/terra.rst @@ -9,6 +9,7 @@ Qiskit Terra API Reference circuit circuit_library + circuit_classical compiler execute visualization diff --git a/qiskit/circuit/classical/__init__.py b/qiskit/circuit/classical/__init__.py new file mode 100644 index 000000000000..9bc4ac8b3f54 --- /dev/null +++ b/qiskit/circuit/classical/__init__.py @@ -0,0 +1,41 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +======================================================= +Classical expressions (:mod:`qiskit.circuit.classical`) +======================================================= + +This module contains an exploratory representation of runtime operations on classical values during +circuit execution. + +Currently, only simple expressions on bits and registers that result in a Boolean value are +supported, and these are only valid for use in the conditions of :meth:`.QuantumCircuit.if_test` +(:class:`.IfElseOp`) and :meth:`.QuantumCircuit.while_loop` (:class:`.WhileLoopOp`), and in the +target of :meth:`.QuantumCircuit.switch` (:class:`.SwitchCaseOp`). + +.. note:: + This is an exploratory module, and while we will commit to the standard Qiskit deprecation + policy within it, please be aware that the module will be deliberately limited in scope at the + start, and early versions may not evolve cleanly into the final version. It is possible that + various components of this module will be replaced (subject to deprecations) instead of improved + into a new form. + + The type system and expression tree will be expanded over time, and it is possible that the + allowed types of some operations may need to change between versions of Qiskit as the classical + processing capabilities develop. + +.. automodule:: qiskit.circuit.classical.expr +.. automodule:: qiskit.circuit.classical.types +""" + +from . import types, expr diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py new file mode 100644 index 000000000000..1c734bfde0bc --- /dev/null +++ b/qiskit/circuit/classical/expr/__init__.py @@ -0,0 +1,180 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +================================================== +Expressions (:mod:`qiskit.circuit.classical.expr`) +================================================== + +The necessary components for building expressions are all exported from the +:mod:`~.qiskit.circuit.classical.expr` namespace within :mod:`qiskit.circuit.classical`, so you can +choose whether to use qualified access (for example :class:`.expr.Value`) or import the names you +need directly and call them without the prefix. + +There are two pathways for constructing expressions. The classes that form :ref:`the +representation of the expression system ` +have constructors that perform zero type checking; it is up to the caller to ensure that they +are building valid objects. For a more user-friendly interface to direct construction, there +are helper functions associated with most of the classes that do type validation and inference. +These are described below, in :ref:`circuit-classical-expressions-expr-construction`. + +.. _circuit-classical-expressions-expr-representation: + +Representation +============== + +The expression system is based on tree representation. All nodes in the tree are final +(uninheritable) instances of the abstract base class: + +.. autoclass:: Expr + +These objects are mutable and should not be reused in a different location without a copy. + +The entry point from general circuit objects to the expression system is by wrapping the object +in a :class:`Value` node and associating a :class:`~.types.Type` with it. + +.. autoclass:: Value + +The operations traditionally associated with pre-, post- or infix operators in programming are +represented by the :class:`Unary` and :class:`Binary` nodes as appropriate. These each take an +operation type code, which are exposed as enumerations inside each class as :class:`Unary.Op` +and :class:`Binary.Op` respectively. + +.. autoclass:: Unary + :members: Op + :member-order: bysource +.. autoclass:: Binary + :members: Op + :member-order: bysource + +When constructing expressions, one must ensure that the types are valid for the operation. +Attempts to construct expressions with invalid types will raise a regular Python ``TypeError``. + +Expressions in this system are defined to act only on certain sets of types. However, values +may be cast to a suitable supertype in order to satisfy the typing requirements. In these +cases, a node in the expression tree is used to represent the promotion. In all cases where +operations note that they "implicitly cast" or "coerce" their arguments, the expression tree +must have this node representing the conversion. + +.. autoclass:: Cast + + +.. _circuit-classical-expressions-expr-construction: + +Construction +============ + +Constructing the tree representation directly is verbose and easy to make a mistake with the +typing. In many cases, much of the typing can be inferred, scalar values can automatically +be promoted to :class:`Value` instances, and any required promotions can be resolved into +suitable :class:`Cast` nodes. + +The functions and methods described in this section are a more user-friendly way to build the +expression tree, while staying close to the internal representation. All these functions will +automatically lift valid Python scalar values into corresponding :class:`Value` objects, and +will resolve any required implicit casts on your behalf. + +.. autofunction:: value + +You can manually specify casts in cases where the cast is allowed in explicit form, but may be +losslses (such as the cast of a higher precision :class:`~.types.Uint` to a lower precision one). + +.. autofunction:: cast + +There are helper constructor functions for each of the unary operations. + +.. autofunction:: bit_not +.. autofunction:: logic_not + +Similarly, the binary operations and relations have helper functions defined. + +.. autofunction:: bit_and +.. autofunction:: bit_or +.. autofunction:: logic_and +.. autofunction:: logic_or +.. autofunction:: equal +.. autofunction:: not_equal +.. autofunction:: less +.. autofunction:: less_equal +.. autofunction:: greater +.. autofunction:: greater_equal + +Qiskit's legacy method for specifying equality conditions for use in conditionals is to use a +two-tuple of a :class:`.Clbit` or :class:`.ClassicalRegister` and an integer. This represents an +exact equality condition, and there are no ways to specify any other relations. The helper function +:func:`lift_legacy` converts this legacy format into the new expression syntax. + +.. autofunction:: lift_legacy + +Working with the expression tree +================================ + +A typical consumer of the expression tree wants to recursively walk through the tree, potentially +statefully, acting on each node differently depending on its type. This is naturally a +double-dispatch problem; the logic of 'what is to be done' is likely stateful and users should be +free to define their own operations, yet each node defines 'what is being acted on'. We enable this +double dispatch by providing a base visitor class for the expression tree. + +.. autoclass:: ExprVisitor + :members: + :undoc-members: + +Consumers of the expression tree should subclass the visitor, and override the ``visit_*`` methods +that they wish to handle. Any non-overridden methods will call :meth:`~ExprVisitor.visit_generic`, +which unless overridden will raise a ``RuntimeError`` to ensure that you are aware if new nodes +have been added to the expression tree that you are not yet handling. +""" + +__all__ = [ + "ExprVisitor", + "Expr", + "Value", + "Cast", + "Unary", + "Binary", + "value", + "cast", + "bit_not", + "logic_not", + "bit_and", + "bit_or", + "bit_xor", + "logic_and", + "logic_or", + "equal", + "not_equal", + "less", + "less_equal", + "greater", + "greater_equal", + "lift_legacy", +] + +from .expr import ExprVisitor, Expr, Value, Cast, Unary, Binary +from .constructors import ( + value, + cast, + bit_not, + logic_not, + bit_and, + bit_or, + bit_xor, + logic_and, + logic_or, + equal, + not_equal, + less, + less_equal, + greater, + greater_equal, + lift_legacy, +) diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py new file mode 100644 index 000000000000..0f3fc087d8e5 --- /dev/null +++ b/qiskit/circuit/classical/expr/constructors.py @@ -0,0 +1,505 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""User-space constructor functions for the expression tree, which do some of the inference and +lifting boilerplate work.""" + +# pylint: disable=redefined-builtin,redefined-outer-name + +from __future__ import annotations + +__all__ = [ + "value", + "bit_not", + "logic_not", + "bit_and", + "bit_or", + "bit_xor", + "logic_and", + "logic_or", + "equal", + "not_equal", + "less", + "less_equal", + "greater", + "greater_equal", + "lift_legacy", +] + +import enum +import typing + +from .expr import ValueScalarT, Expr, Value, Unary, Binary, Cast +from .. import types + +if typing.TYPE_CHECKING: + import qiskit + + +class _CastKind(enum.Enum): + EQUAL = enum.auto() + """The two types are equal; no cast node is required at all.""" + IMPLICIT = enum.auto() + """The 'from' type can be cast to the 'to' type implicitly. A ``Cast(implicit=True)`` node is + the minimum required to specify this.""" + LOSSLESS = enum.auto() + """The 'from' type can be cast to the 'to' type explicitly, and the cast will be lossless. This + requires a ``Cast(implicit=False)`` node, but there's no danger from inserting one.""" + DANGEROUS = enum.auto() + """The 'from' type has a defined cast to the 'to' type, but depending on the value, it may lose + data. A user would need to manually specify casts.""" + NONE = enum.auto() + """There is no casting permitted from the 'from' type to the 'to' type.""" + + +def _uint_cast(from_: types.Uint, to_: types.Uint, /) -> _CastKind: + if from_.width == to_.width: + return _CastKind.EQUAL + if from_.width < to_.width: + return _CastKind.LOSSLESS + return _CastKind.DANGEROUS + + +_ALLOWED_CASTS = { + (types.Bool, types.Bool): lambda _a, _b, /: _CastKind.EQUAL, + (types.Bool, types.Uint): lambda _a, _b, /: _CastKind.LOSSLESS, + (types.Uint, types.Bool): lambda _a, _b, /: _CastKind.IMPLICIT, + (types.Uint, types.Uint): _uint_cast, +} + + +def _cast_kind(from_: types.Type, to_: types.Type, /) -> _CastKind: + if (coercer := _ALLOWED_CASTS.get((from_.kind, to_.kind))) is None: + return _CastKind.NONE + return coercer(from_, to_) + + +def _coerce_lossless(expr: Expr, type: types.Type) -> Expr: + """Coerce ``expr`` to ``type`` by inserting a suitable :class:`Cast` node, if the cast is + lossless. Otherwise, raise a ``TypeError``.""" + kind = _cast_kind(expr.type, type) + if kind is _CastKind.EQUAL: + return expr + if kind is _CastKind.IMPLICIT: + 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}'") + + +def lift_legacy( + condition: tuple[qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister, int], / +) -> Expr: + """Lift a legacy two-tuple equality condition into a new-style :class:`Expr`. + + Examples: + Taking an old-style conditional instruction and getting an :class:`Expr` from its + condition:: + + from qiskit.circuit import ClassicalRegister + from qiskit.circuit.library import HGate + from qiskit.circuit.classical import expr + + cr = ClassicalRegister(2) + instr = HGate() + instr.c_if(cr, 3) + + lifted = expr.lift_legacy(instr.condition) + """ + from qiskit.circuit import Clbit + + target, value = condition + if isinstance(target, Clbit): + bool_ = types.Bool() + return ( + Value(target, bool_) + if value + else Unary(Unary.Op.LOGIC_NOT, Value(target, bool_), bool_) + ) + left = Value(target, types.Uint(width=target.size)) + if value.bit_length() > target.size: + left = Cast(left, types.Uint(width=value.bit_length()), implicit=True) + right = Value(value, left.type) + return Binary(Binary.Op.EQUAL, left, right, types.Bool()) + + +def value(value: ValueScalarT, /, type: types.Type | None = None) -> Expr: + """Lift the given Python ``value`` to a :class:`Value`. + + If an explicit ``type`` is given, the typing in the output will reflect that. + + Examples: + Lifting simple scalars to be :class:`Value` instances:: + + >>> from qiskit.circuit import Clbit, ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.value(Clbit()) + Value(, Bool()) + >>> expr.value(ClassicalRegister(3, "c")) + Value(ClassicalRegister(3, "c"), Uint(3)) + + The type of the return value can be influenced, if the given value could be interpreted + losslessly as the given type (use :func:`cast` to perform a full set of casting + operations, include lossy ones):: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr, types + >>> expr.value(ClassicalRegister(3, "c"), types.Uint(5)) + Value(ClassicalRegister(3, "c"), Uint(5)) + >>> expr.value(5, types.Uint(4)) + Value(5, Uint(4)) + """ + from qiskit.circuit import Clbit, ClassicalRegister + + inferred: types.Type + if value is True or value is False or isinstance(value, Clbit): + inferred = types.Bool() + elif isinstance(value, ClassicalRegister): + inferred = types.Uint(width=value.size) + elif isinstance(value, int): + if value < 0: + raise ValueError("cannot represent a negative value") + inferred = types.Uint(width=value.bit_length() or 1) + else: + raise TypeError(f"failed to infer a type for '{value}'") + if type is None: + type = inferred + if types.is_supertype(type, inferred): + return Value(value, type) + raise TypeError( + f"the explicit type '{type}' is not suitable for representing '{value}';" + f" it must be non-strict supertype of '{inferred}'" + ) + + +def cast(operand: Expr | ValueScalarT, type: types.Type, /) -> Expr: + """Create an explicit cast from the given value to the given type. + + Examples: + Add an explicit cast node that explicitly casts a higher precision type to a lower precision + one:: + + >>> from qiskit.circuit.classical import expr, types + >>> value = expr.value(5, types.Uint(32)) + >>> expr.cast(value, types.Uint(8)) + Cast(Value(5, types.Uint(32)), types.Uint(8), implicit=False) + """ + operand = operand if isinstance(operand, Expr) else value(operand) + if _cast_kind(operand.type, type) is _CastKind.NONE: + raise TypeError(f"cannot cast '{operand}' to '{type}'") + return Cast(operand, type) + + +def bit_not(operand: Expr | ValueScalarT, /) -> Expr: + """Create a bitwise 'not' expression node from the given value, resolving any implicit casts and + lifting the value into a :class:`Value` node if required. + + Examples: + Bitwise negation of a :class:`.ClassicalRegister`:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.bit_not(ClassicalRegister(3, "c")) + Unary(Unary.Op.BIT_NOT, Value(ClassicalRegister(3, 'c'), Uint(3)), Uint(3)) + """ + operand = operand if isinstance(operand, Expr) else value(operand) + if operand.type.kind not in (types.Bool, types.Uint): + raise TypeError(f"cannot apply '{Unary.Op.BIT_NOT}' to type '{operand.type}'") + return Unary(Unary.Op.BIT_NOT, operand, operand.type) + + +def logic_not(operand: Expr | ValueScalarT, /) -> Expr: + """Create a logical 'not' expression node from the given value, resolving any implicit casts and + lifting the value into a :class:`Value` node if required. + + Examples: + Logical negation of a :class:`.ClassicalRegister`:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.logic_not(ClassicalRegister(3, "c")) + Unary(\ +Unary.Op.LOGIC_NOT, \ +Cast(Value(ClassicalRegister(3, 'c'), Uint(3)), Bool(), implicit=True), \ +Bool()) + """ + operand = operand if isinstance(operand, Expr) else value(operand) + operand = _coerce_lossless(operand, types.Bool()) + return Unary(Unary.Op.LOGIC_NOT, operand, operand.type) + + +def _binary_bitwise(op: Binary.Op, left: Expr | ValueScalarT, right: Expr | ValueScalarT) -> Expr: + # Inference for integer literals is slightly different here as a convenience, since the bitwise + # binary operations require operands of the same width. + if not (isinstance(left, int) or isinstance(right, int)): + left = left if isinstance(left, Expr) else value(left) + right = right if isinstance(right, Expr) else value(right) + elif isinstance(left, int): + right = right if isinstance(right, Expr) else value(right) + if right.type.kind is types.Uint: + if left.bit_length() > right.type.width: + raise TypeError( + f"integer literal '{left}' is wider than the other bitwise operand '{right}'" + ) + left = Value(left, right.type) + else: + left = value(left) + elif isinstance(right, int): + left = left if isinstance(left, Expr) else value(left) + if left.type.kind is types.Uint: + if right.bit_length() > left.type.width: + raise TypeError( + f"integer literal '{right}' is wider than the other bitwise operand '{left}'" + ) + right = Value(right, left.type) + else: + right = value(right) + else: + # Both are `int`, so we take our best case to make things work. + uint = types.Uint(max(left.bit_length(), right.bit_length(), 1)) + left = Value(left, uint) + right = Value(right, uint) + type: types.Type + if left.type.kind is right.type.kind is types.Bool: + type = types.Bool() + elif left.type.kind is types.Uint and right.type.kind is types.Uint: + if left.type != right.type: + raise TypeError( + "binary bitwise operations are defined between unsigned integers of the same width," + f" but got {left.type.width} and {right.type.width}." + ) + type = left.type + else: + raise TypeError(f"invalid types for '{op}': '{left.type}' and '{right.type}'") + return Binary(op, left, right, type) + + +def bit_and(left: Expr | ValueScalarT, right: Expr | ValueScalarT, /) -> Expr: + """Create a bitwise 'and' expression node from the given value, resolving any implicit casts and + lifting the values into :class:`Value` nodes if required. + + Examples: + Bitwise 'and' of a classical register and an integer literal:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.bit_and(ClassicalRegister(3, "c"), 0b111) + Binary(\ +Binary.Op.BIT_AND, \ +Value(ClassicalRegister(3, 'c'), Uint(3)), \ +Value(7, Uint(3)), \ +Uint(3)) + """ + return _binary_bitwise(Binary.Op.BIT_AND, left, right) + + +def bit_or(left: Expr | ValueScalarT, right: Expr | ValueScalarT, /) -> Expr: + """Create a bitwise 'or' expression node from the given value, resolving any implicit casts and + lifting the values into :class:`Value` nodes if required. + + Examples: + Bitwise 'or' of a classical register and an integer literal:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.bit_or(ClassicalRegister(3, "c"), 0b101) + Binary(\ +Binary.Op.BIT_OR, \ +Value(ClassicalRegister(3, 'c'), Uint(3)), \ +Value(5, Uint(3)), \ +Uint(3)) + """ + return _binary_bitwise(Binary.Op.BIT_OR, left, right) + + +def bit_xor(left: Expr | ValueScalarT, right: Expr | ValueScalarT, /) -> Expr: + """Create a bitwise 'exclusive or' expression node from the given value, resolving any implicit + casts and lifting the values into :class:`Value` nodes if required. + + Examples: + Bitwise 'exclusive or' of a classical register and an integer literal:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.bit_xor(ClassicalRegister(3, "c"), 0b101) + Binary(\ +Binary.Op.BIT_XOR, \ +Value(ClassicalRegister(3, 'c'), Uint(3)), \ +Value(5, Uint(3)), \ +Uint(3)) + """ + return _binary_bitwise(Binary.Op.BIT_XOR, left, right) + + +def _binary_logical(op: Binary.Op, left: Expr | ValueScalarT, right: Expr | ValueScalarT) -> Expr: + bool_ = types.Bool() + left = _coerce_lossless(left if isinstance(left, Expr) else value(left), bool_) + right = _coerce_lossless(right if isinstance(right, Expr) else value(right), bool_) + return Binary(op, left, right, bool_) + + +def logic_and(left: Expr | ValueScalarT, right: Expr | ValueScalarT, /) -> Expr: + """Create a logical 'and' expression node from the given value, resolving any implicit casts and + lifting the values into :class:`Value` nodes if required. + + Examples: + Logical 'and' of two classical bits:: + + >>> from qiskit.circuit import Clbit + >>> from qiskit.circuit.classical import expr + >>> expr.logical_and(Clbit(), Clbit()) + Binary(Binary.Op.LOGIC_AND, Value(, Bool()), Value(, Bool()), Bool()) + """ + return _binary_logical(Binary.Op.LOGIC_AND, left, right) + + +def logic_or(left: Expr | ValueScalarT, right: Expr | ValueScalarT, /) -> Expr: + """Create a logical 'or' expression node from the given value, resolving any implicit casts and + lifting the values into :class:`Value` nodes if required. + + Examples: + Logical 'or' of two classical bits + + >>> from qiskit.circuit import Clbit + >>> from qiskit.circuit.classical import expr + >>> expr.logical_and(Clbit(), Clbit()) + Binary(Binary.Op.LOGIC_OR, Value(, Bool()), Value(, Bool()), Bool()) + """ + return _binary_logical(Binary.Op.LOGIC_OR, left, right) + + +def _equal_like(op: Binary.Op, left: Expr | ValueScalarT, right: Expr | ValueScalarT) -> Expr: + left = left if isinstance(left, Expr) else value(left) + right = right if isinstance(right, Expr) else value(right) + 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) + return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool()) + + +def equal(left: Expr | ValueScalarT, right: Expr | ValueScalarT, /) -> Expr: + """Create an 'equal' expression node from the given value, resolving any implicit casts and + lifting the values into :class:`Value` nodes if required. + + Examples: + Equality between a classical register and an integer:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.equal(ClassicalRegister(3, "c"), 7) + Binary(Binary.Op.EQUAL, \ +Value(ClassicalRegister(3, "c"), Uint(3)), \ +Value(7, Uint(3)), \ +Uint(3)) + """ + return _equal_like(Binary.Op.EQUAL, left, right) + + +def not_equal(left: Expr | ValueScalarT, right: Expr | ValueScalarT, /) -> Expr: + """Create a 'not equal' expression node from the given value, resolving any implicit casts and + lifting the values into :class:`Value` nodes if required. + + Examples: + Inequality between a classical register and an integer:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.not_equal(ClassicalRegister(3, "c"), 7) + Binary(Binary.Op.NOT_EQUAL, \ +Value(ClassicalRegister(3, "c"), Uint(3)), \ +Value(7, Uint(3)), \ +Uint(3)) + """ + return _equal_like(Binary.Op.NOT_EQUAL, left, right) + + +def _binary_relation(op: Binary.Op, left: Expr | ValueScalarT, right: Expr | ValueScalarT) -> Expr: + left = left if isinstance(left, Expr) else value(left) + right = right if isinstance(right, Expr) else value(right) + 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) + return Binary(op, _coerce_lossless(left, type), _coerce_lossless(right, type), types.Bool()) + + +def less(left: Expr | ValueScalarT, right: Expr | ValueScalarT, /) -> Expr: + """Create a 'less than' expression node from the given value, resolving any implicit casts and + lifting the values into :class:`Value` nodes if required. + + Examples: + Query if a classical register is less than an integer:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.less(ClassicalRegister(3, "c"), 5) + Binary(Binary.Op.LESS, \ +Value(ClassicalRegister(3, "c"), Uint(3)), \ +Value(5, Uint(3)), \ +Uint(3)) + """ + return _binary_relation(Binary.Op.LESS, left, right) + + +def less_equal(left: Expr | ValueScalarT, right: Expr | ValueScalarT, /) -> Expr: + """Create a 'less than or equal to' expression node from the given value, resolving any implicit + casts and lifting the values into :class:`Value` nodes if required. + + Examples: + Query if a classical register is less than or equal to another:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.less(ClassicalRegister(3, "a"), ClassicalRegister(3, "b")) + Binary(Binary.Op.LESS_EQUAL, \ +Value(ClassicalRegister(3, "a"), Uint(3)), \ +Value(ClassicalRegister(3, "b"), Uint(3)), \ +Uint(3)) + """ + return _binary_relation(Binary.Op.LESS_EQUAL, left, right) + + +def greater(left: Expr | ValueScalarT, right: Expr | ValueScalarT, /) -> Expr: + """Create a 'greater than' expression node from the given value, resolving any implicit casts + and lifting the values into :class:`Value` nodes if required. + + Examples: + Query if a classical register is greater than an integer:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.less(ClassicalRegister(3, "c"), 5) + Binary(Binary.Op.GREATER, \ +Value(ClassicalRegister(3, "c"), Uint(3)), \ +Value(5, Uint(3)), \ +Uint(3)) + """ + return _binary_relation(Binary.Op.GREATER, left, right) + + +def greater_equal(left: Expr | ValueScalarT, right: Expr | ValueScalarT, /) -> Expr: + """Create a 'greater than or equal to' expression node from the given value, resolving any + implicit casts and lifting the values into :class:`Value` nodes if required. + + Examples: + Query if a classical register is greater than or equal to another:: + + >>> from qiskit.circuit import ClassicalRegister + >>> from qiskit.circuit.classical import expr + >>> expr.less(ClassicalRegister(3, "a"), ClassicalRegister(3, "b")) + Binary(Binary.Op.GREATER_EQUAL, \ +Value(ClassicalRegister(3, "a"), Uint(3)), \ +Value(ClassicalRegister(3, "b"), Uint(3)), \ +Uint(3)) + """ + return _binary_relation(Binary.Op.GREATER_EQUAL, left, right) diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py new file mode 100644 index 000000000000..0b96afc93ddc --- /dev/null +++ b/qiskit/circuit/classical/expr/expr.py @@ -0,0 +1,288 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Expression-tree nodes.""" + +# Given the nature of the tree representation and that there are helper functions associated with +# many of the classes whose arguments naturally share names with themselves, it's inconvenient to +# use synonyms everywhere. This goes for the builtin 'type' as well. +# pylint: disable=redefined-builtin,redefined-outer-name + +from __future__ import annotations + +__all__ = [ + "ExprVisitor", + "Expr", + "Value", + "Cast", + "Unary", + "Binary", +] + +import abc +import enum +import typing + +from .. import types + +if typing.TYPE_CHECKING: + import qiskit + +_T_co = typing.TypeVar("_T_co", covariant=True) + + +class ExprVisitor(typing.Generic[_T_co]): + """Base class for visitors to the :class:`Expr` tree. Subclasses should override whichever of + the ``visit_*`` methods that they are able to handle, and should be organised such that + non-existent methods will never be called.""" + + # pylint: disable=missing-function-docstring + + __slots__ = () + + def visit_generic(self, expr: "Expr", /) -> _T_co: # pragma: no cover + raise RuntimeError(f"expression visitor {self} has no method to handle expr {expr}") + + def visit_value(self, expr: "Value", /) -> _T_co: # pragma: no cover + return self.visit_generic(expr) + + def visit_unary(self, expr: "Unary", /) -> _T_co: # pragma: no cover + return self.visit_generic(expr) + + def visit_binary(self, expr: "Binary", /) -> _T_co: # pragma: no cover + return self.visit_generic(expr) + + def visit_cast(self, expr: "Cast", /) -> _T_co: # pragma: no cover + return self.visit_generic(expr) + + +class Expr(abc.ABC): + """Root base class of all nodes in the expression tree. The base case should never be + instantiated directly. + + This must not be subclassed by users; subclasses form the internal data of the representation of + expressions, and it does not make sense to add more outside of Qiskit library code. + + All subclasses are responsible for setting their ``type`` attribute in their ``__init__``, and + should not call the parent initialiser.""" + + __slots__ = ("type",) + + type: types.Type + + # Sentinel to prevent instantiation of the base class. + @abc.abstractmethod + def __init__(self): # pragma: no cover + pass + + def accept(self, visitor: ExprVisitor[_T_co], /) -> _T_co: # pragma: no cover + """Call the relevant ``visit_*`` method on the given :class:`ExprVisitor`. The usual entry + point for a simple visitor is to construct it, and then call :meth:`accept` on the root + object to be visited. For example:: + + expr = ... + visitor = MyVisitor() + visitor.accept(expr) + + Subclasses of :class:`Expr` should override this to call the correct virtual method on the + visitor. This implements double dispatch with the visitor.""" + return visitor.visit_generic(self) + + +@typing.final +class Cast(Expr): + """A cast from one type to another, implied by the use of an expression in a different + context.""" + + __slots__ = ("operand", "implicit") + + def __init__(self, operand: Expr, type: types.Type, implicit: bool = False): + self.type = type + self.operand = operand + self.implicit = implicit + + def accept(self, visitor, /): + return visitor.visit_cast(self) + + def __eq__(self, other): + return ( + isinstance(other, Cast) + and self.type == other.type + and self.operand == other.operand + and self.implicit == other.implicit + ) + + def __repr__(self): + return f"Cast({self.operand}, {self.type}, implicit={self.implicit})" + + +# Short-hand alias. Probably shouldn't be exported or used outside the `expr` package. +ValueScalarT = typing.Union[int, "qiskit.circuit.Clbit", "qiskit.circuit.ClassicalRegister"] + + +@typing.final +class Value(Expr): + """A single scalar value.""" + + __slots__ = ("value",) + + def __init__(self, value: ValueScalarT, type: types.Type): + self.type = type + self.value = value + + def accept(self, visitor, /): + return visitor.visit_value(self) + + def __eq__(self, other): + return isinstance(other, Value) and self.type == other.type and self.value == other.value + + def __repr__(self): + return f"Value({self.value}, {self.type})" + + +@typing.final +class Unary(Expr): + """A unary expression. + + Args: + op: The opcode describing which operation is being done. + operand: The operand of the operation. + type: The resolved type of the result. + """ + + __slots__ = ("op", "operand") + + class Op(enum.Enum): + """Enumeration of the opcodes for unary operations. + + The bitwise negation :data:`BIT_NOT` takes a single bit or an unsigned integer of known + width, and returns a value of the same type. + + The logical negation :data:`LOGIC_NOT` takes an input that is implicitly coerced to a + Boolean, and returns a Boolean. + """ + + # If adding opcodes, remember to add helper constructor functions in `constructors.py`. + # The opcode integers should be considered a public interface; they are used by + # serialisation formats that may transfer data between different versions of Qiskit. + BIT_NOT = 1 + """Bitwise negation. ``~operand``.""" + LOGIC_NOT = 2 + """Logical negation. ``!operand``.""" + + def __str__(self): + return f"Unary.{super().__str__()}" + + def __repr__(self): + return f"Unary.{super().__repr__()}" + + def __init__(self, op: Unary.Op, operand: Expr, type: types.Type): + self.op = op + self.operand = operand + self.type = type + + def accept(self, visitor, /): + return visitor.visit_unary(self) + + def __eq__(self, other): + return ( + isinstance(other, Unary) + and self.type == other.type + and self.op is other.op + and self.operand == other.operand + ) + + def __repr__(self): + return f"Unary({self.op}, {self.operand}, {self.type})" + + +@typing.final +class Binary(Expr): + """A binary expression. + + Args: + op: The opcode describing which operation is being done. + left: The left-hand operand. + right: The right-hand operand. + type: The resolved type of the result. + """ + + __slots__ = ("op", "left", "right") + + class Op(enum.Enum): + """Enumeration of the opcodes for binary operations. + + The bitwise operations :data:`BIT_AND`, :data:`BIT_OR` and :data:`BIT_XOR` apply to two + operands of the same type, which must be a single bit or an unsigned integer of fixed width. + The resultant type is the same as the two input types. + + The logical operations :data:`LOGIC_AND` and :data:`LOGIC_OR` first implicitly coerce their + arguments to Booleans, and then apply the logical operation. The resultant type is always + Boolean. + + The binary mathematical relations :data:`EQUAL`, :data:`NOT_EQUAL`, :data:`LESS`, + :data:`LESS_EQUAL`, :data:`GREATER` and :data:`GREATER_EQUAL` take unsigned integers + (with an implicit cast to make them the same width), and return a Boolean. + """ + + # If adding opcodes, remember to add helper constructor functions in `constructors.py` + # The opcode integers should be considered a public interface; they are used by + # serialisation formats that may transfer data between different versions of Qiskit. + BIT_AND = 1 + """Bitwise "and". ``lhs & rhs``.""" + BIT_OR = 2 + """Bitwise "or". ``lhs | rhs``.""" + BIT_XOR = 3 + """Bitwise "exclusive or". ``lhs ^ rhs``.""" + LOGIC_AND = 4 + """Logical "and". ``lhs && rhs``.""" + LOGIC_OR = 5 + """Logical "or". ``lhs || rhs``.""" + EQUAL = 6 + """Numeric equality. ``lhs == rhs``.""" + NOT_EQUAL = 7 + """Numeric inequality. ``lhs != rhs``.""" + LESS = 8 + """Numeric less than. ``lhs < rhs``.""" + LESS_EQUAL = 9 + """Numeric less than or equal to. ``lhs <= rhs``""" + GREATER = 10 + """Numeric greater than. ``lhs > rhs``.""" + GREATER_EQUAL = 11 + """Numeric greater than or equal to. ``lhs >= rhs``.""" + + def __str__(self): + return f"Binary.{super().__str__()}" + + def __repr__(self): + return f"Binary.{super().__repr__()}" + + def __init__(self, op: Binary.Op, left: Expr, right: Expr, type: types.Type): + self.op = op + self.left = left + self.right = right + self.type = type + + def accept(self, visitor, /): + return visitor.visit_binary(self) + + def __eq__(self, other): + return ( + isinstance(other, Binary) + and self.type == other.type + and self.op is other.op + and self.left == other.left + and self.right == other.right + ) + + def __repr__(self): + return f"Binary({self.op}, {self.left}, {self.right}, {self.type})" diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py new file mode 100644 index 000000000000..c55c724315cc --- /dev/null +++ b/qiskit/circuit/classical/types/__init__.py @@ -0,0 +1,83 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +============================================== +Typing (:mod:`qiskit.circuit.classical.types`) +============================================== + + +The type system of the expression tree is exposed through this module. This is inherently linked to +the expression system in the :mod:`~.classical.expr` module, as most expressions can only be +understood with the context of the types that they act on. + +All types inherit from an abstract base class: + +.. autoclass:: Type + +Types should be considered immutable objects, and you must not mutate them. It is permissible to +reuse a :class:`Type` that you take from another object without copying it, and generally this will +be the best approach for performance. :class:`Type` objects are designed to be small amounts of +data, and it's best to point to the same instance of the data where possible rather than +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). + +.. autoclass:: Bool +.. autoclass:: Uint + +Note that :class:`Uint` defines a family of types parametrised by their width; it is not one single +type, which may be slightly different to the 'classical' programming languages you are used to. + +There are some functions on these types exposed here as well. These are mostly expected to be used +only in manipulations of the expression tree; users who are building expressions using the +:ref:`user-facing construction interface ` should +not need to use these. + +The type system is equipped with a partial ordering, where :math:`a < b` is interpreted as +":math:`a` is a strict subtype of :math:`b`". Note that the partial ordering is a subset of the +directed graph that describes the allowed explicit casting operations between types. The partial +ordering defines when one type may be lossless directly interpreted as another. + +The low-level interface to querying the subtyping relationship is the :func:`order` function. + +.. autofunction:: order + +The return value is an enumeration :class:`Ordering` that describes what, if any, subtyping +relationship exists between the two types. + +.. autoclass:: Ordering + :member-order: bysource + +Some helper methods are then defined in terms of this low-level :func:`order` primitive: + +.. autofunction:: is_subtype +.. autofunction:: is_supertype +.. autofunction:: greater +""" + +__all__ = [ + "Type", + "Bool", + "Uint", + "Ordering", + "order", + "is_subtype", + "is_supertype", + "greater", +] + +from .types import Type, Bool, Uint +from .ordering import Ordering, order, is_subtype, is_supertype, greater diff --git a/qiskit/circuit/classical/types/ordering.py b/qiskit/circuit/classical/types/ordering.py new file mode 100644 index 000000000000..aceb9aeefbcf --- /dev/null +++ b/qiskit/circuit/classical/types/ordering.py @@ -0,0 +1,163 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tools for working with the partial ordering of the type system.""" + +from __future__ import annotations + +__all__ = [ + "Ordering", + "is_subtype", + "is_supertype", + "order", + "greater", +] + +import enum + +from .types import Type, Bool, Uint + + +# While the type system is simple, it's overkill to represent the complete partial ordering graph of +# the set of types in an explicit graph form. The strategy here is to assume that two types have no +# ordering between them, and an ordering is defined by putting a function `Type * Type -> Ordering` +# into the `_ORDERERS`. + + +class Ordering(enum.Enum): + """Enumeration listing the possible relations between two types. Types only have a partial + ordering, so it's possible for two types to have no sub-typing relationship. + + Note that the sub-/supertyping relationship is not the same as whether a type can be explicitly + cast from one to another.""" + + LESS = enum.auto() + """The left type is a strict subtype of the right type.""" + EQUAL = enum.auto() + """The two types are equal.""" + GREATER = enum.auto() + """The left type is a strict supertype of the right type.""" + NONE = enum.auto() + """There is no typing relationship between the two types.""" + + 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 + if left.width == right.width: + return Ordering.EQUAL + return Ordering.GREATER + + +_ORDERERS = { + (Bool, Bool): _order_bool_bool, + (Uint, Uint): _order_uint_uint, +} + + +def order(left: Type, right: Type, /) -> Ordering: + """Get the ordering relationship between the two types as an enumeration value. + + Examples: + Compare two :class:`Uint` types of different widths:: + + >>> from qiskit.circuit.classical import types + >>> types.order(types.Uint(8), types.Uint(16)) + Ordering.LESS + + Compare two types that have no ordering between them:: + + >>> types.order(types.Uint(8), types.Bool()) + Ordering.NONE + """ + if (orderer := _ORDERERS.get((left.kind, right.kind))) is None: + return Ordering.NONE + return orderer(left, right) + + +def is_subtype(left: Type, right: Type, /, strict: bool = False) -> bool: + r"""Does the relation :math:`\text{left} \le \text{right}` hold? If there is no ordering + relation between the two types, then this returns ``False``. If ``strict``, then the equality + is also forbidden. + + Examples: + Check if one type is a subclass of another:: + + >>> from qiskit.circuit.classical import types + >>> types.is_subtype(types.Uint(8), types.Uint(16)) + True + + Check if one type is a strict subclass of another:: + + >>> types.is_subtype(types.Bool(), types.Bool()) + True + >>> types.is_subtype(types.Bool(), types.Bool(), strict=True) + False + """ + order_ = order(left, right) + return order_ is Ordering.LESS or (not strict and order_ is Ordering.EQUAL) + + +def is_supertype(left: Type, right: Type, /, strict: bool = False) -> bool: + r"""Does the relation :math:`\text{left} \ge \text{right}` hold? If there is no ordering + relation between the two types, then this returns ``False``. If ``strict``, then the equality + is also forbidden. + + Examples: + Check if one type is a superclass of another:: + + >>> from qiskit.circuit.classical import types + >>> types.is_supertype(types.Uint(8), types.Uint(16)) + False + + Check if one type is a strict superclass of another:: + + >>> types.is_supertype(types.Bool(), types.Bool()) + True + >>> types.is_supertype(types.Bool(), types.Bool(), strict=True) + False + """ + order_ = order(left, right) + return order_ is Ordering.GREATER or (not strict and order_ is Ordering.EQUAL) + + +def greater(left: Type, right: Type, /) -> Type: + """Get the greater of the two types, assuming that there is an ordering relation between them. + Technically, this is a slightly restricted version of the concept of the 'meet' of the two + types in that the return value must be one of the inputs. In practice in the type system there + is no concept of a 'sum' type, so the 'meet' exists if and only if there is an ordering between + the two types, and is equal to the greater of the two types. + + Returns: + The greater of the two types. + + Raises: + TypeError: if there is no ordering relation between the two types. + + Examples: + Find the greater of two :class:`Uint` types:: + + >>> from qiskit.circuit.classical import types + >>> types.greater(types.Uint(8), types.Uint(16)) + types.Uint(16) + """ + order_ = order(left, right) + if order_ is Ordering.NONE: + raise TypeError(f"no ordering exists between '{left}' and '{right}'") + return left if order_ is Ordering.GREATER else right diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py new file mode 100644 index 000000000000..18057e641388 --- /dev/null +++ b/qiskit/circuit/classical/types/types.py @@ -0,0 +1,105 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Type-system definition for the expression tree.""" + +# Given the nature of the tree representation and that there are helper functions associated with +# many of the classes whose arguments naturally share names with themselves, it's inconvenient to +# use synonyms everywhere. This goes for the builtin 'type' as well. +# pylint: disable=redefined-builtin,redefined-outer-name + +from __future__ import annotations + +__all__ = [ + "Type", + "Bool", + "Uint", +] + +import typing + + +class _Singleton(type): + """Metaclass to make the child, which should take zero initialisation arguments, a singleton + object.""" + + def _get_singleton_instance(cls): + return cls._INSTANCE + + @classmethod + def __prepare__(mcs, name, bases): # pylint: disable=unused-argument + return {"__new__": mcs._get_singleton_instance} + + @staticmethod + def __new__(cls, name, bases, namespace): + out = super().__new__(cls, name, bases, namespace) + out._INSTANCE = object.__new__(out) # pylint: disable=invalid-name + return out + + +class Type: + """Root base class of all nodes in the type tree. The base case should never be instantiated + directly. + + This must not be subclassed by users; subclasses form the internal data of the representation of + expressions, and it does not make sense to add more outside of Qiskit library code.""" + + __slots__ = () + + @property + def kind(self): + """Get the kind of this type. This is exactly equal to the Python type object that defines + this type, that is ``t.kind is type(t)``, but is exposed like this to make it clear that + this a hashable enum-like discriminator you can rely on.""" + return self.__class__ + + # Enforcement of immutability. The constructor methods need to manually skip this. + + def __setattr__(self, _key, _value): + raise AttributeError(f"'{self.kind.__name__}' instances are immutable") + + def __copy__(self): + return self + + def __deepcopy__(self, _memo): + return self + + +@typing.final +class Bool(Type, metaclass=_Singleton): + """The Boolean type. This has exactly two values: ``True`` and ``False``.""" + + __slots__ = () + + def __repr__(self): + return "Bool()" + + def __eq__(self, other): + return isinstance(other, Bool) + + +@typing.final +class Uint(Type): + """An unsigned integer of fixed bit width.""" + + __slots__ = ("width",) + + def __init__(self, width: int): + if isinstance(width, int) and width <= 0: + raise ValueError("uint width must be greater than zero") + super(Type, self).__setattr__("width", width) + + def __repr__(self): + return f"Uint({self.width})" + + def __eq__(self, other): + return isinstance(other, Uint) and self.width == other.width diff --git a/test/python/circuit/classical/__init__.py b/test/python/circuit/classical/__init__.py new file mode 100644 index 000000000000..26f7536d3514 --- /dev/null +++ b/test/python/circuit/classical/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. diff --git a/test/python/circuit/classical/test_expr_constructors.py b/test/python/circuit/classical/test_expr_constructors.py new file mode 100644 index 000000000000..e35c5e1d80d0 --- /dev/null +++ b/test/python/circuit/classical/test_expr_constructors.py @@ -0,0 +1,389 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring + +import ddt + +from qiskit.circuit import Clbit, ClassicalRegister, Instruction +from qiskit.circuit.classical import expr, types +from qiskit.test import QiskitTestCase + + +@ddt.ddt +class TestExprConstructors(QiskitTestCase): + def test_lift_legacy(self): + cr = ClassicalRegister(3, "c") + clbit = Clbit() + + inst = Instruction("custom", 1, 0, []) + inst.c_if(cr, 7) + self.assertEqual( + expr.lift_legacy(inst.condition), + expr.Binary( + expr.Binary.Op.EQUAL, + expr.Value(cr, types.Uint(cr.size)), + expr.Value(7, types.Uint(cr.size)), + types.Bool(), + ), + ) + + inst = Instruction("custom", 1, 0, []) + inst.c_if(cr, 255) + self.assertEqual( + expr.lift_legacy(inst.condition), + expr.Binary( + expr.Binary.Op.EQUAL, + expr.Cast(expr.Value(cr, types.Uint(cr.size)), types.Uint(8), implicit=True), + expr.Value(255, types.Uint(8)), + types.Bool(), + ), + ) + + inst = Instruction("custom", 1, 0, []) + inst.c_if(clbit, False) + self.assertEqual( + expr.lift_legacy(inst.condition), + expr.Unary( + expr.Unary.Op.LOGIC_NOT, + expr.Value(clbit, types.Bool()), + types.Bool(), + ), + ) + + inst = Instruction("custom", 1, 0, []) + inst.c_if(clbit, True) + self.assertEqual( + expr.lift_legacy(inst.condition), + expr.Value(clbit, types.Bool()), + ) + + def test_value_lifts_qiskit_scalars(self): + cr = ClassicalRegister(3, "c") + self.assertEqual(expr.value(cr), expr.Value(cr, types.Uint(cr.size))) + + clbit = Clbit() + self.assertEqual(expr.value(clbit), expr.Value(clbit, types.Bool())) + + def test_value_lifts_python_builtins(self): + self.assertEqual(expr.value(True), expr.Value(True, types.Bool())) + self.assertEqual(expr.value(False), expr.Value(False, types.Bool())) + self.assertEqual(expr.value(7), expr.Value(7, types.Uint(3))) + + def test_value_ensures_nonzero_width(self): + self.assertEqual(expr.value(0), expr.Value(0, types.Uint(1))) + + def test_value_type_representation(self): + self.assertEqual(expr.value(5), expr.Value(5, types.Uint((5).bit_length()))) + self.assertEqual(expr.value(5, types.Uint(8)), expr.Value(5, types.Uint(8))) + + cr = ClassicalRegister(3, "c") + self.assertEqual(expr.value(cr, types.Uint(8)), expr.Value(cr, types.Uint(8))) + + def test_value_does_not_allow_downcast(self): + with self.assertRaisesRegex(TypeError, "the explicit type .* is not suitable"): + expr.value(0xFF, types.Uint(2)) + + def test_value_rejects_bad_values(self): + with self.assertRaisesRegex(TypeError, "failed to infer a type"): + expr.value("1") + with self.assertRaisesRegex(ValueError, "cannot represent a negative value"): + expr.value(-1) + + def test_cast_adds_explicit_nodes(self): + """A specific request to add a cast in means that we should respect that in the type tree, + even if the cast is a no-op.""" + base = expr.Value(5, types.Uint(8)) + self.assertEqual( + expr.cast(base, types.Uint(8)), expr.Cast(base, types.Uint(8), implicit=False) + ) + + def test_cast_allows_lossy_downcasting(self): + """An explicit 'cast' call should allow lossy casts to be performed.""" + base = expr.Value(5, types.Uint(16)) + self.assertEqual( + expr.cast(base, types.Uint(8)), expr.Cast(base, types.Uint(8), implicit=False) + ) + self.assertEqual( + expr.cast(base, types.Bool()), expr.Cast(base, types.Bool(), implicit=False) + ) + + @ddt.data( + (expr.bit_not, ClassicalRegister(3)), + (expr.logic_not, ClassicalRegister(3)), + (expr.logic_not, False), + (expr.logic_not, Clbit()), + ) + @ddt.unpack + def test_unary_functions_lift_scalars(self, function, scalar): + self.assertEqual(function(scalar), function(expr.value(scalar))) + + def test_bit_not_explicit(self): + cr = ClassicalRegister(3) + self.assertEqual( + expr.bit_not(cr), + expr.Unary( + expr.Unary.Op.BIT_NOT, expr.Value(cr, types.Uint(cr.size)), types.Uint(cr.size) + ), + ) + clbit = Clbit() + self.assertEqual( + expr.bit_not(clbit), + expr.Unary(expr.Unary.Op.BIT_NOT, expr.Value(clbit, types.Bool()), types.Bool()), + ) + + def test_logic_not_explicit(self): + cr = ClassicalRegister(3) + self.assertEqual( + expr.logic_not(cr), + expr.Unary( + expr.Unary.Op.LOGIC_NOT, + expr.Cast(expr.Value(cr, types.Uint(cr.size)), types.Bool(), implicit=True), + types.Bool(), + ), + ) + clbit = Clbit() + self.assertEqual( + expr.logic_not(clbit), + expr.Unary(expr.Unary.Op.LOGIC_NOT, expr.Value(clbit, types.Bool()), types.Bool()), + ) + + @ddt.data( + (expr.bit_and, ClassicalRegister(3), ClassicalRegister(3)), + (expr.bit_or, ClassicalRegister(3), ClassicalRegister(3)), + (expr.bit_xor, ClassicalRegister(3), ClassicalRegister(3)), + (expr.logic_and, Clbit(), True), + (expr.logic_or, False, ClassicalRegister(3)), + (expr.equal, ClassicalRegister(3), 255), + (expr.not_equal, ClassicalRegister(3), 255), + (expr.less, ClassicalRegister(3), 2), + (expr.less_equal, ClassicalRegister(3), 5), + (expr.greater, 2, ClassicalRegister(3)), + (expr.greater_equal, ClassicalRegister(3), 5), + ) + @ddt.unpack + def test_binary_functions_lift_scalars(self, function, left, right): + self.assertEqual(function(left, right), function(expr.value(left), right)) + self.assertEqual(function(left, right), function(left, expr.value(right))) + self.assertEqual(function(left, right), function(expr.value(left), expr.value(right))) + + @ddt.data( + (expr.bit_and, expr.Binary.Op.BIT_AND), + (expr.bit_or, expr.Binary.Op.BIT_OR), + (expr.bit_xor, expr.Binary.Op.BIT_XOR), + ) + @ddt.unpack + def test_binary_bitwise_explicit(self, function, opcode): + cr = ClassicalRegister(8, "c") + self.assertEqual( + function(cr, 255), + expr.Binary( + opcode, expr.Value(cr, types.Uint(8)), expr.Value(255, types.Uint(8)), types.Uint(8) + ), + ) + self.assertEqual( + function(255, cr), + expr.Binary( + opcode, expr.Value(255, types.Uint(8)), expr.Value(cr, types.Uint(8)), types.Uint(8) + ), + ) + + clbit = Clbit() + self.assertEqual( + function(True, clbit), + expr.Binary( + opcode, + expr.Value(True, types.Bool()), + expr.Value(clbit, types.Bool()), + types.Bool(), + ), + ) + self.assertEqual( + function(clbit, False), + expr.Binary( + opcode, + expr.Value(clbit, types.Bool()), + expr.Value(False, types.Bool()), + types.Bool(), + ), + ) + + @ddt.data( + (expr.bit_and, expr.Binary.Op.BIT_AND), + (expr.bit_or, expr.Binary.Op.BIT_OR), + (expr.bit_xor, expr.Binary.Op.BIT_XOR), + ) + @ddt.unpack + def test_binary_bitwise_uint_inference(self, function, opcode): + """The binary bitwise functions have specialised inference for the widths of integer + literals, since the bitwise functions require the operands to already be of exactly the same + width without promotion.""" + cr = ClassicalRegister(8, "c") + self.assertEqual( + function(cr, 5), + expr.Binary( + opcode, + expr.Value(cr, types.Uint(8)), + expr.Value(5, types.Uint(8)), # Note the inference should be Uint(8) not Uint(3). + types.Uint(8), + ), + ) + self.assertEqual( + function(5, cr), + expr.Binary( + opcode, + expr.Value(5, types.Uint(8)), + expr.Value(cr, types.Uint(8)), + types.Uint(8), + ), + ) + + # Inference between two integer literals is "best effort". This behaviour isn't super + # important to maintain if we want to change the expression system. + self.assertEqual( + function(5, 255), + expr.Binary( + opcode, + expr.Value(5, types.Uint(8)), + expr.Value(255, types.Uint(8)), + types.Uint(8), + ), + ) + + @ddt.data(expr.bit_and, expr.bit_or, expr.bit_xor) + def test_binary_bitwise_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "invalid types"): + function(ClassicalRegister(3, "c"), Clbit()) + # 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"): + function(ClassicalRegister(3, "a"), ClassicalRegister(5, "b")) + + @ddt.data( + (expr.logic_and, expr.Binary.Op.LOGIC_AND), + (expr.logic_or, expr.Binary.Op.LOGIC_OR), + ) + @ddt.unpack + def test_binary_logical_explicit(self, function, opcode): + cr = ClassicalRegister(8, "c") + clbit = Clbit() + + self.assertEqual( + function(cr, clbit), + expr.Binary( + opcode, + expr.Cast(expr.Value(cr, types.Uint(cr.size)), types.Bool(), implicit=True), + expr.Value(clbit, types.Bool()), + types.Bool(), + ), + ) + + self.assertEqual( + function(cr, 3), + expr.Binary( + opcode, + expr.Cast(expr.Value(cr, types.Uint(cr.size)), types.Bool(), implicit=True), + expr.Cast(expr.Value(3, types.Uint(2)), types.Bool(), implicit=True), + types.Bool(), + ), + ) + + self.assertEqual( + function(False, clbit), + expr.Binary( + opcode, + expr.Value(False, types.Bool()), + expr.Value(clbit, types.Bool()), + types.Bool(), + ), + ) + + @ddt.data( + (expr.equal, expr.Binary.Op.EQUAL), + (expr.not_equal, expr.Binary.Op.NOT_EQUAL), + ) + @ddt.unpack + def test_binary_equal_explicit(self, function, opcode): + cr = ClassicalRegister(8, "c") + clbit = Clbit() + + self.assertEqual( + function(cr, 255), + expr.Binary( + opcode, expr.Value(cr, types.Uint(8)), expr.Value(255, types.Uint(8)), types.Bool() + ), + ) + + self.assertEqual( + function(7, cr), + expr.Binary( + opcode, + expr.Cast(expr.Value(7, types.Uint(3)), types.Uint(8), implicit=False), + expr.Value(cr, types.Uint(8)), + types.Bool(), + ), + ) + + self.assertEqual( + function(clbit, True), + expr.Binary( + opcode, + expr.Value(clbit, types.Bool()), + expr.Value(True, types.Bool()), + types.Bool(), + ), + ) + + @ddt.data(expr.equal, expr.not_equal) + def test_binary_equal_forbidden(self, function): + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Clbit(), ClassicalRegister(3, "c")) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(ClassicalRegister(3, "c"), False) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(5, True) + + @ddt.data( + (expr.less, expr.Binary.Op.LESS), + (expr.less_equal, expr.Binary.Op.LESS_EQUAL), + (expr.greater, expr.Binary.Op.GREATER), + (expr.greater_equal, expr.Binary.Op.GREATER_EQUAL), + ) + @ddt.unpack + def test_binary_relation_explicit(self, function, opcode): + cr = ClassicalRegister(8, "c") + + self.assertEqual( + function(cr, 200), + expr.Binary( + opcode, expr.Value(cr, types.Uint(8)), expr.Value(200, types.Uint(8)), types.Bool() + ), + ) + + self.assertEqual( + function(12, cr), + expr.Binary( + opcode, + expr.Cast(expr.Value(12, types.Uint(4)), types.Uint(8), implicit=False), + expr.Value(cr, types.Uint(8)), + types.Bool(), + ), + ) + + @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"): + function(Clbit(), ClassicalRegister(3, "c")) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(ClassicalRegister(3, "c"), False) + with self.assertRaisesRegex(TypeError, "invalid types"): + function(Clbit(), Clbit()) diff --git a/test/python/circuit/classical/test_types_ordering.py b/test/python/circuit/classical/test_types_ordering.py new file mode 100644 index 000000000000..374e1ecff1b1 --- /dev/null +++ b/test/python/circuit/classical/test_types_ordering.py @@ -0,0 +1,60 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring + +from qiskit.circuit.classical import types +from qiskit.test import QiskitTestCase + + +class TestTypesOrdering(QiskitTestCase): + def test_order(self): + self.assertIs(types.order(types.Uint(8), types.Uint(16)), types.Ordering.LESS) + self.assertIs(types.order(types.Uint(16), types.Uint(8)), types.Ordering.GREATER) + self.assertIs(types.order(types.Uint(8), types.Uint(8)), types.Ordering.EQUAL) + + self.assertIs(types.order(types.Bool(), types.Bool()), types.Ordering.EQUAL) + + self.assertIs(types.order(types.Bool(), types.Uint(8)), types.Ordering.NONE) + self.assertIs(types.order(types.Uint(8), types.Bool()), types.Ordering.NONE) + + def test_is_subtype(self): + self.assertTrue(types.is_subtype(types.Uint(8), types.Uint(16))) + self.assertFalse(types.is_subtype(types.Uint(16), types.Uint(8))) + self.assertTrue(types.is_subtype(types.Uint(8), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Uint(8), types.Uint(8), strict=True)) + + self.assertTrue(types.is_subtype(types.Bool(), types.Bool())) + self.assertFalse(types.is_subtype(types.Bool(), types.Bool(), strict=True)) + + self.assertFalse(types.is_subtype(types.Bool(), types.Uint(8))) + self.assertFalse(types.is_subtype(types.Uint(8), types.Bool())) + + def test_is_supertype(self): + self.assertFalse(types.is_supertype(types.Uint(8), types.Uint(16))) + self.assertTrue(types.is_supertype(types.Uint(16), types.Uint(8))) + self.assertTrue(types.is_supertype(types.Uint(8), types.Uint(8))) + self.assertFalse(types.is_supertype(types.Uint(8), types.Uint(8), strict=True)) + + self.assertTrue(types.is_supertype(types.Bool(), types.Bool())) + self.assertFalse(types.is_supertype(types.Bool(), types.Bool(), strict=True)) + + self.assertFalse(types.is_supertype(types.Bool(), types.Uint(8))) + self.assertFalse(types.is_supertype(types.Uint(8), 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()) + with self.assertRaisesRegex(TypeError, "no ordering"): + types.greater(types.Bool(), types.Uint(8))