diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 62c6c524eb29..a8d57f615d7c 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -280,7 +280,6 @@ InstructionSet Operation EquivalenceLibrary - Store Control Flow Operations ----------------------- @@ -377,7 +376,6 @@ from .delay import Delay from .measure import Measure from .reset import Reset -from .store import Store from .parameter import Parameter from .parametervector import ParameterVector from .parameterexpression import ParameterExpression diff --git a/qiskit/circuit/classical/expr/__init__.py b/qiskit/circuit/classical/expr/__init__.py index 4502aa52779a..d2cd4bc5044e 100644 --- a/qiskit/circuit/classical/expr/__init__.py +++ b/qiskit/circuit/classical/expr/__init__.py @@ -39,11 +39,10 @@ These objects are mutable and should not be reused in a different location without a copy. -The base for dynamic variables is the :class:`Var`, which can be either an arbitrarily typed runtime -variable, or a wrapper around a :class:`.Clbit` or :class:`.ClassicalRegister`. +The entry point from general circuit objects to the expression system is by wrapping the object +in a :class:`Var` node and associating a :class:`~.types.Type` with it. .. autoclass:: Var - :members: var, name Similarly, literals used in comparison (such as integers) should be lifted to :class:`Value` nodes with associated types. @@ -87,18 +86,10 @@ 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:`Var` or :class:`Value` -objects, and will resolve any required implicit casts on your behalf. If you want to directly use -some scalar value as an :class:`Expr` node, you can manually :func:`lift` it yourself. +objects, and will resolve any required implicit casts on your behalf. .. autofunction:: lift -Typically you should create memory-owning :class:`Var` instances by using the -:meth:`.QuantumCircuit.add_var` method to declare them in some circuit context, since a -:class:`.QuantumCircuit` will not accept an :class:`Expr` that contains variables that are not -already declared in it, since it needs to know how to allocate the storage and how the variable will -be initialized. However, should you want to do this manually, you should use the low-level -:meth:`Var.new` call to safely generate a named variable for usage. - You can manually specify casts in cases where the cast is allowed in explicit form, but may be lossy (such as the cast of a higher precision :class:`~.types.Uint` to a lower precision one). @@ -160,11 +151,6 @@ suitable "key" functions to do the comparison. .. autofunction:: structurally_equivalent - -Some expressions have associated memory locations, and others may be purely temporary. -You can use :func:`is_lvalue` to determine whether an expression has an associated memory location. - -.. autofunction:: is_lvalue """ __all__ = [ @@ -177,7 +163,6 @@ "ExprVisitor", "iter_vars", "structurally_equivalent", - "is_lvalue", "lift", "cast", "bit_not", @@ -197,7 +182,7 @@ ] from .expr import Expr, Var, Value, Cast, Unary, Binary -from .visitors import ExprVisitor, iter_vars, structurally_equivalent, is_lvalue +from .visitors import ExprVisitor, iter_vars, structurally_equivalent from .constructors import ( lift, cast, diff --git a/qiskit/circuit/classical/expr/constructors.py b/qiskit/circuit/classical/expr/constructors.py index 64a19a2aee2a..1406a86237c5 100644 --- a/qiskit/circuit/classical/expr/constructors.py +++ b/qiskit/circuit/classical/expr/constructors.py @@ -35,27 +35,65 @@ "lift_legacy_condition", ] +import enum import typing from .expr import Expr, Var, Value, Unary, Binary, Cast -from ..types import CastKind, cast_kind 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: + kind = _cast_kind(expr.type, type) + if kind is _CastKind.EQUAL: return expr - if kind is CastKind.IMPLICIT: + if kind is _CastKind.IMPLICIT: return Cast(expr, type, implicit=True) - if kind is CastKind.LOSSLESS: + if kind is _CastKind.LOSSLESS: return Cast(expr, type, implicit=False) - if kind is CastKind.DANGEROUS: + 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}'") @@ -160,7 +198,7 @@ def cast(operand: typing.Any, type: types.Type, /) -> Expr: Cast(Value(5, types.Uint(32)), types.Uint(8), implicit=False) """ operand = lift(operand) - if cast_kind(operand.type, type) is CastKind.NONE: + if _cast_kind(operand.type, type) is _CastKind.NONE: raise TypeError(f"cannot cast '{operand}' to '{type}'") return Cast(operand, type) diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index c22870e51fee..b9e9aad4a2b7 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -31,7 +31,6 @@ import abc import enum import typing -import uuid from .. import types @@ -109,92 +108,24 @@ def __repr__(self): @typing.final class Var(Expr): - """A classical variable. - - These variables take two forms: a new-style variable that owns its storage location and has an - associated name; and an old-style variable that wraps a :class:`.Clbit` or - :class:`.ClassicalRegister` instance that is owned by some containing circuit. In general, - construction of variables for use in programs should use :meth:`Var.new` or - :meth:`.QuantumCircuit.add_var`. - - Variables are immutable after construction, so they can be used as dictionary keys.""" - - __slots__ = ("var", "name") - - var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister | uuid.UUID - """A reference to the backing data storage of the :class:`Var` instance. When lifting - old-style :class:`.Clbit` or :class:`.ClassicalRegister` instances into a :class:`Var`, - this is exactly the :class:`.Clbit` or :class:`.ClassicalRegister`. If the variable is a - new-style classical variable (one that owns its own storage separate to the old - :class:`.Clbit`/:class:`.ClassicalRegister` model), this field will be a :class:`~uuid.UUID` - to uniquely identify it.""" - name: str | None - """The name of the variable. This is required to exist if the backing :attr:`var` attribute - is a :class:`~uuid.UUID`, i.e. if it is a new-style variable, and must be ``None`` if it is - an old-style variable.""" + """A classical variable.""" + + __slots__ = ("var",) def __init__( - self, - var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister | uuid.UUID, - type: types.Type, - *, - name: str | None = None, + self, var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister, type: types.Type ): - super().__setattr__("type", type) - super().__setattr__("var", var) - super().__setattr__("name", name) - - @classmethod - def new(cls, name: str, type: types.Type) -> typing.Self: - """Generate a new named variable that owns its own backing storage.""" - return cls(uuid.uuid4(), type, name=name) - - @property - def standalone(self) -> bool: - """Whether this :class:`Var` is a standalone variable that owns its storage location. If - false, this is a wrapper :class:`Var` around a pre-existing circuit object.""" - return isinstance(self.var, uuid.UUID) + self.type = type + self.var = var def accept(self, visitor, /): return visitor.visit_var(self) - def __setattr__(self, key, value): - if hasattr(self, key): - raise AttributeError(f"'Var' object attribute '{key}' is read-only") - raise AttributeError(f"'Var' object has no attribute '{key}'") - - def __hash__(self): - return hash((self.type, self.var, self.name)) - def __eq__(self, other): - return ( - isinstance(other, Var) - and self.type == other.type - and self.var == other.var - and self.name == other.name - ) + return isinstance(other, Var) and self.type == other.type and self.var == other.var def __repr__(self): - if self.name is None: - return f"Var({self.var}, {self.type})" - return f"Var({self.var}, {self.type}, name='{self.name}')" - - def __getstate__(self): - return (self.var, self.type, self.name) - - def __setstate__(self, state): - var, type, name = state - super().__setattr__("type", type) - super().__setattr__("var", var) - super().__setattr__("name", name) - - def __copy__(self): - # I am immutable... - return self - - def __deepcopy__(self, memo): - # ... as are all my consituent parts. - return self + return f"Var({self.var}, {self.type})" @typing.final diff --git a/qiskit/circuit/classical/expr/visitors.py b/qiskit/circuit/classical/expr/visitors.py index c0c1a5894af6..07ad36a8e0e4 100644 --- a/qiskit/circuit/classical/expr/visitors.py +++ b/qiskit/circuit/classical/expr/visitors.py @@ -215,66 +215,3 @@ def structurally_equivalent( True """ return left.accept(_StructuralEquivalenceImpl(right, left_var_key, right_var_key)) - - -class _IsLValueImpl(ExprVisitor[bool]): - __slots__ = () - - def visit_var(self, node, /): - return True - - def visit_value(self, node, /): - return False - - def visit_unary(self, node, /): - return False - - def visit_binary(self, node, /): - return False - - def visit_cast(self, node, /): - return False - - -_IS_LVALUE = _IsLValueImpl() - - -def is_lvalue(node: expr.Expr, /) -> bool: - """Return whether this expression can be used in l-value positions, that is, whether it has a - well-defined location in memory, such as one that might be writeable. - - Being an l-value is a necessary but not sufficient for this location to be writeable; it is - permissible that a larger object containing this memory location may not allow writing from - the scope that attempts to write to it. This would be an access property of the containing - program, however, and not an inherent property of the expression system. - - Examples: - Literal values are never l-values; there's no memory location associated with (for example) - the constant ``1``:: - - >>> from qiskit.circuit.classical import expr - >>> expr.is_lvalue(expr.lift(2)) - False - - :class:`~.expr.Var` nodes are always l-values, because they always have some associated - memory location:: - - >>> from qiskit.circuit.classical import types - >>> from qiskit.circuit import Clbit - >>> expr.is_lvalue(expr.Var.new("a", types.Bool())) - True - >>> expr.is_lvalue(expr.lift(Clbit())) - True - - Currently there are no unary or binary operations on variables that can produce an l-value - expression, but it is likely in the future that some sort of "indexing" operation will be - added, which could produce l-values:: - - >>> a = expr.Var.new("a", types.Uint(8)) - >>> b = expr.Var.new("b", types.Uint(8)) - >>> expr.is_lvalue(a) and expr.is_lvalue(b) - True - >>> expr.is_lvalue(expr.bit_and(a, b)) - False - """ - return node.accept(_IS_LVALUE) diff --git a/qiskit/circuit/classical/types/__init__.py b/qiskit/circuit/classical/types/__init__.py index 93ab90e32166..c55c724315cc 100644 --- a/qiskit/circuit/classical/types/__init__.py +++ b/qiskit/circuit/classical/types/__init__.py @@ -15,8 +15,6 @@ Typing (:mod:`qiskit.circuit.classical.types`) ============================================== -Representation -============== 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 @@ -43,18 +41,11 @@ 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. - -Working with types -================== - 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. -Partial ordering of types -------------------------- - 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 @@ -75,20 +66,6 @@ .. autofunction:: is_subtype .. autofunction:: is_supertype .. autofunction:: greater - - -Casting between types ---------------------- - -It is common to need to cast values of one type to another type. The casting rules for this are -embedded into the :mod:`types` module. You can query the casting kinds using :func:`cast_kind`: - -.. autofunction:: cast_kind - -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. - -.. autoclass:: CastKind """ __all__ = [ @@ -100,9 +77,7 @@ "is_subtype", "is_supertype", "greater", - "CastKind", - "cast_kind", ] from .types import Type, Bool, Uint -from .ordering import Ordering, order, is_subtype, is_supertype, greater, CastKind, cast_kind +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 index b000e91cf5ed..aceb9aeefbcf 100644 --- a/qiskit/circuit/classical/types/ordering.py +++ b/qiskit/circuit/classical/types/ordering.py @@ -20,8 +20,6 @@ "is_supertype", "order", "greater", - "CastKind", - "cast_kind", ] import enum @@ -163,60 +161,3 @@ def greater(left: Type, right: Type, /) -> Type: if order_ is Ordering.NONE: raise TypeError(f"no ordering exists between '{left}' and '{right}'") return left if order_ is Ordering.GREATER else right - - -class CastKind(enum.Enum): - """A return value indicating the type of cast that can occur from one type to another.""" - - 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 :class:`~.expr.Cast` node with - ``implicit==True`` 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 :class:`~.expr.Cast`` node with ``implicit=False``, 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_: Uint, to_: Uint, /) -> CastKind: - if from_.width == to_.width: - return CastKind.EQUAL - if from_.width < to_.width: - return CastKind.LOSSLESS - return CastKind.DANGEROUS - - -_ALLOWED_CASTS = { - (Bool, Bool): lambda _a, _b, /: CastKind.EQUAL, - (Bool, Uint): lambda _a, _b, /: CastKind.LOSSLESS, - (Uint, Bool): lambda _a, _b, /: CastKind.IMPLICIT, - (Uint, Uint): _uint_cast, -} - - -def cast_kind(from_: Type, to_: Type, /) -> CastKind: - """Determine the sort of cast that is required to move from the left type to the right type. - - Examples: - - .. code-block:: python - - >>> from qiskit.circuit.classical import types - >>> types.cast_kind(types.Bool(), types.Bool()) - - >>> types.cast_kind(types.Uint(8), types.Bool()) - - >>> types.cast_kind(types.Bool(), types.Uint(8)) - - >>> types.cast_kind(types.Uint(16), types.Uint(8)) - - """ - if (coercer := _ALLOWED_CASTS.get((from_.kind, to_.kind))) is None: - return CastKind.NONE - return coercer(from_, to_) diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 04266aefd410..711f82db5fc0 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -89,9 +89,6 @@ class Bool(Type, metaclass=_Singleton): def __repr__(self): return "Bool()" - def __hash__(self): - return hash(self.__class__) - def __eq__(self, other): return isinstance(other, Bool) @@ -110,8 +107,5 @@ def __init__(self, width: int): def __repr__(self): return f"Uint({self.width})" - def __hash__(self): - return hash((self.__class__, self.width)) - def __eq__(self, other): return isinstance(other, Uint) and self.width == other.width diff --git a/qiskit/circuit/controlflow/_builder_utils.py b/qiskit/circuit/controlflow/_builder_utils.py index aa1e331eb41a..5ba5c9612c9d 100644 --- a/qiskit/circuit/controlflow/_builder_utils.py +++ b/qiskit/circuit/controlflow/_builder_utils.py @@ -15,17 +15,15 @@ from __future__ import annotations import dataclasses -from typing import Iterable, Tuple, Set, Union, TypeVar, TYPE_CHECKING +from typing import Iterable, Tuple, Set, Union, TypeVar from qiskit.circuit.classical import expr, types from qiskit.circuit.exceptions import CircuitError +from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.register import Register from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.circuit.quantumregister import QuantumRegister -if TYPE_CHECKING: - from qiskit.circuit import QuantumCircuit - _ConditionT = TypeVar( "_ConditionT", bound=Union[Tuple[ClassicalRegister, int], Tuple[Clbit, int], expr.Expr] ) @@ -161,9 +159,6 @@ def _unify_circuit_resources_rebuild( # pylint: disable=invalid-name # (it's t This function will always rebuild the objects into new :class:`.QuantumCircuit` instances. """ - # pylint: disable=cyclic-import - from qiskit.circuit import QuantumCircuit - qubits, clbits = set(), set() for circuit in circuits: qubits.update(circuit.qubits) diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index 38530c0dd8f3..7c88ef469087 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -22,10 +22,9 @@ import abc import itertools import typing -from typing import Collection, Iterable, FrozenSet, Tuple, Union, Optional, Sequence +from typing import Collection, Iterable, FrozenSet, Tuple, Union, Optional, Sequence, Callable from qiskit._accelerate.quantum_circuit import CircuitData -from qiskit.circuit.classical import expr from qiskit.circuit.classicalregister import Clbit, ClassicalRegister from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.instruction import Instruction @@ -36,7 +35,7 @@ from ._builder_utils import condition_resources, node_resources if typing.TYPE_CHECKING: - import qiskit + import qiskit # pylint: disable=cyclic-import class CircuitScopeInterface(abc.ABC): @@ -104,66 +103,6 @@ def resolve_classical_resource( or a :class:`.Clbit` that isn't actually in the circuit. """ - @abc.abstractmethod - def add_uninitialized_var(self, var: expr.Var): - """Add an uninitialized variable to the circuit scope. - - The general circuit context is responsible for ensuring the variable is initialized. These - uninitialized variables are guaranteed to be standalone. - - Args: - var: the variable to add, if valid. - - Raises: - CircuitError: if the variable cannot be added, such as because it invalidly shadows or - redefines an existing name. - """ - - @abc.abstractmethod - def remove_var(self, var: expr.Var): - """Remove a variable from the locals of this scope. - - This is only called in the case that an exception occurred while initializing the variable, - and is not exposed to users. - - Args: - var: the variable to remove. It can be assumed that this was already the subject of an - :meth:`add_uninitialized_var` call. - """ - - @abc.abstractmethod - def use_var(self, var: expr.Var): - """Called for every standalone classical runtime variable being used by some circuit - instruction. - - The given variable is guaranteed to be a stand-alone variable; bit-like resource-wrapping - variables will have been filtered out and their resources given to - :meth:`resolve_classical_resource`. - - Args: - var: the variable to validate. - - Returns: - the same variable. - - Raises: - CircuitError: if the variable is not valid for this scope. - """ - - @abc.abstractmethod - def get_var(self, name: str) -> Optional[expr.Var]: - """Get the variable (if any) in scope with the given name. - - This should call up to the parent scope if in a control-flow builder scope, in case the - variable exists in an outer scope. - - Args: - name: the name of the symbol to lookup. - - Returns: - the variable if it is found, otherwise ``None``. - """ - class InstructionResources(typing.NamedTuple): """The quantum and classical resources used within a particular instruction. @@ -329,11 +268,9 @@ class ControlFlowBuilderBlock(CircuitScopeInterface): "registers", "global_phase", "_allow_jumps", - "_parent", + "_resource_requester", "_built", "_forbidden_message", - "_vars_local", - "_vars_capture", ) def __init__( @@ -341,8 +278,8 @@ def __init__( qubits: Iterable[Qubit], clbits: Iterable[Clbit], *, - parent: CircuitScopeInterface, registers: Iterable[Register] = (), + resource_requester: Callable, allow_jumps: bool = True, forbidden_message: Optional[str] = None, ): @@ -364,7 +301,13 @@ def __init__( uses *exactly* the same set of resources. We cannot verify this from within the builder interface (and it is too expensive to do when the ``for`` op is made), so we fail safe, and require the user to use the more verbose, internal form. - parent: The scope interface of the containing scope. + resource_requester: A callback function that takes in some classical resource specifier, + and returns a concrete classical resource, if this scope is allowed to access that + resource. In almost all cases, this should be a resolver from the + :obj:`.QuantumCircuit` that this scope is contained in. See + :meth:`.QuantumCircuit._resolve_classical_resource` for the normal expected input + here, and the documentation of :obj:`.InstructionSet`, which uses this same + callback. forbidden_message: If a string is given here, a :exc:`.CircuitError` will be raised on any attempts to append instructions to the scope with this message. This is used by pseudo scopes where the state machine of the builder scopes has changed into a @@ -374,10 +317,8 @@ def __init__( self._instructions = CircuitData(qubits, clbits) self.registers = set(registers) self.global_phase = 0.0 - self._vars_local = {} - self._vars_capture = {} self._allow_jumps = allow_jumps - self._parent = parent + self._resource_requester = resource_requester self._built = False self._forbidden_message = forbidden_message @@ -421,6 +362,8 @@ def _raise_on_jump(operation): ) def append(self, instruction: CircuitInstruction) -> CircuitInstruction: + """Add an instruction into the scope, keeping track of the qubits and clbits that have been + used in total.""" if self._forbidden_message is not None: raise CircuitError(self._forbidden_message) if not self._allow_jumps: @@ -451,68 +394,25 @@ def resolve_classical_resource(self, specifier): if self._built: raise CircuitError("Cannot add resources after the scope has been built.") # Allow the inner resolve to propagate exceptions. - resource = self._parent.resolve_classical_resource(specifier) + resource = self._resource_requester(specifier) if isinstance(resource, Clbit): self.add_bits((resource,)) else: self.add_register(resource) return resource - def add_uninitialized_var(self, var: expr.Var): - if self._built: - raise CircuitError("Cannot add resources after the scope has been built.") - # We can shadow a name if it was declared in an outer scope, but only if we haven't already - # captured it ourselves yet. - if (previous := self._vars_local.get(var.name)) is not None: - if previous == var: - raise CircuitError(f"'{var}' is already present in the scope") - raise CircuitError(f"cannot add '{var}' as its name shadows the existing '{previous}'") - if var.name in self._vars_capture: - raise CircuitError(f"cannot add '{var}' as its name shadows the existing '{previous}'") - self._vars_local[var.name] = var - - def remove_var(self, var: expr.Var): - if self._built: - raise RuntimeError("exception handler 'remove_var' called after scope built") - self._vars_local.pop(var.name) - - def get_var(self, name: str): - if (out := self._vars_local.get(name)) is not None: - return out - return self._parent.get_var(name) - - def use_var(self, var: expr.Var): - if (local := self._vars_local.get(var.name)) is not None: - if local == var: - return - raise CircuitError(f"cannot use '{var}' which is shadowed by the local '{local}'") - if self._vars_capture.get(var.name) == var: - return - if self._parent.get_var(var.name) != var: - raise CircuitError(f"cannot close over '{var}', which is not in scope") - self._parent.use_var(var) - self._vars_capture[var.name] = var - - def iter_local_vars(self): - """Iterator over the variables currently declared in this scope.""" - return self._vars_local.values() - - def iter_captured_vars(self): - """Iterator over the variables currently captured in this scope.""" - return self._vars_capture.values() - def peek(self) -> CircuitInstruction: """Get the value of the most recent instruction tuple in this scope.""" - if not self._instructions: + if not self.instructions: raise CircuitError("This scope contains no instructions.") - return self._instructions[-1] + return self.instructions[-1] def pop(self) -> CircuitInstruction: """Get the value of the most recent instruction in this scope, and remove it from this object.""" - if not self._instructions: + if not self.instructions: raise CircuitError("This scope contains no instructions.") - return self._instructions.pop() + return self.instructions.pop() def add_bits(self, bits: Iterable[Union[Qubit, Clbit]]): """Add extra bits to this scope that are not associated with any concrete instruction yet. @@ -579,7 +479,6 @@ def build( and using the minimal set of resources necessary to support them, within the enclosing scope. """ - # pylint: disable=cyclic-import from qiskit.circuit import QuantumCircuit, SwitchCaseOp # There's actually no real problem with building a scope more than once. This flag is more @@ -601,12 +500,7 @@ def build( self._instructions.clbits, *self.registers, global_phase=self.global_phase, - captures=self._vars_capture.values(), ) - for var in self._vars_local.values(): - # The requisite `Store` instruction to initialise the variable will have been appended - # into the instructions. - out.add_uninitialized_var(var) # Maps placeholder index to the newly concrete instruction. placeholder_to_concrete = {} @@ -679,9 +573,6 @@ def copy(self) -> "ControlFlowBuilderBlock": out._instructions = self._instructions.copy() out.registers = self.registers.copy() out.global_phase = self.global_phase - out._vars_local = self._vars_local.copy() - out._vars_capture = self._vars_capture.copy() - out._parent = self._parent out._allow_jumps = self._allow_jumps out._forbidden_message = self._forbidden_message return out diff --git a/qiskit/circuit/controlflow/control_flow.py b/qiskit/circuit/controlflow/control_flow.py index fefa27efa27f..11ac283132f4 100644 --- a/qiskit/circuit/controlflow/control_flow.py +++ b/qiskit/circuit/controlflow/control_flow.py @@ -13,26 +13,15 @@ "Container to encapsulate all control flow operations." from __future__ import annotations - -import typing from abc import ABC, abstractmethod +from typing import Iterable -from qiskit.circuit.instruction import Instruction -from qiskit.circuit.exceptions import CircuitError - -if typing.TYPE_CHECKING: - from qiskit.circuit import QuantumCircuit +from qiskit.circuit import QuantumCircuit, Instruction class ControlFlowOp(Instruction, ABC): """Abstract class to encapsulate all control flow operations.""" - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - for block in self.blocks: - if block.num_input_vars: - raise CircuitError("control-flow blocks cannot contain input variables") - @property @abstractmethod def blocks(self) -> tuple[QuantumCircuit, ...]: @@ -40,9 +29,10 @@ def blocks(self) -> tuple[QuantumCircuit, ...]: execution of this ControlFlowOp. May be parameterized by a loop parameter to be resolved at run time. """ + pass @abstractmethod - def replace_blocks(self, blocks: typing.Iterable[QuantumCircuit]) -> ControlFlowOp: + def replace_blocks(self, blocks: Iterable[QuantumCircuit]) -> "ControlFlowOp": """Replace blocks and return new instruction. Args: blocks: Tuple of QuantumCircuits to replace in instruction. @@ -50,3 +40,4 @@ def replace_blocks(self, blocks: typing.Iterable[QuantumCircuit]) -> ControlFlow Returns: New ControlFlowOp with replaced blocks. """ + pass diff --git a/qiskit/circuit/controlflow/for_loop.py b/qiskit/circuit/controlflow/for_loop.py index 69c01aff8376..0c6f31156602 100644 --- a/qiskit/circuit/controlflow/for_loop.py +++ b/qiskit/circuit/controlflow/for_loop.py @@ -10,20 +10,16 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Circuit operation representing a ``for`` loop.""" - -from __future__ import annotations +"Circuit operation representing a ``for`` loop." import warnings -from typing import Iterable, Optional, Union, TYPE_CHECKING +from typing import Iterable, Optional, Union from qiskit.circuit.parameter import Parameter from qiskit.circuit.exceptions import CircuitError +from qiskit.circuit.quantumcircuit import QuantumCircuit from .control_flow import ControlFlowOp -if TYPE_CHECKING: - from qiskit.circuit import QuantumCircuit - class ForLoopOp(ControlFlowOp): """A circuit operation which repeatedly executes a subcircuit @@ -73,9 +69,6 @@ def params(self): @params.setter def params(self, parameters): - # pylint: disable=cyclic-import - from qiskit.circuit import QuantumCircuit - indexset, loop_parameter, body = parameters if not isinstance(loop_parameter, (Parameter, type(None))): diff --git a/qiskit/circuit/controlflow/if_else.py b/qiskit/circuit/controlflow/if_else.py index 067c13bb7914..90f44388af76 100644 --- a/qiskit/circuit/controlflow/if_else.py +++ b/qiskit/circuit/controlflow/if_else.py @@ -14,10 +14,10 @@ from __future__ import annotations -from typing import Optional, Union, Iterable, TYPE_CHECKING +from typing import Optional, Union, Iterable import itertools -from qiskit.circuit.classicalregister import ClassicalRegister, Clbit +from qiskit.circuit import ClassicalRegister, Clbit, QuantumCircuit from qiskit.circuit.classical import expr from qiskit.circuit.instructionset import InstructionSet from qiskit.circuit.exceptions import CircuitError @@ -31,9 +31,6 @@ condition_resources, ) -if TYPE_CHECKING: - from qiskit.circuit import QuantumCircuit - # This is just an indication of what's actually meant to be the public API. __all__ = ("IfElseOp",) @@ -85,9 +82,6 @@ def __init__( false_body: QuantumCircuit | None = None, label: str | None = None, ): - # pylint: disable=cyclic-import - from qiskit.circuit import QuantumCircuit - # Type checking generally left to @params.setter, but required here for # finding num_qubits and num_clbits. if not isinstance(true_body, QuantumCircuit): @@ -109,9 +103,6 @@ def params(self): @params.setter def params(self, parameters): - # pylint: disable=cyclic-import - from qiskit.circuit import QuantumCircuit - true_body, false_body = parameters if not isinstance(true_body, QuantumCircuit): diff --git a/qiskit/circuit/controlflow/switch_case.py b/qiskit/circuit/controlflow/switch_case.py index 0087d1efa083..014fb3d8c34a 100644 --- a/qiskit/circuit/controlflow/switch_case.py +++ b/qiskit/circuit/controlflow/switch_case.py @@ -17,9 +17,9 @@ __all__ = ("SwitchCaseOp", "CASE_DEFAULT") import contextlib -from typing import Union, Iterable, Any, Tuple, Optional, List, Literal, TYPE_CHECKING +from typing import Union, Iterable, Any, Tuple, Optional, List, Literal -from qiskit.circuit.classicalregister import ClassicalRegister, Clbit +from qiskit.circuit import ClassicalRegister, Clbit, QuantumCircuit from qiskit.circuit.classical import expr, types from qiskit.circuit.exceptions import CircuitError @@ -27,9 +27,6 @@ from .control_flow import ControlFlowOp from ._builder_utils import unify_circuit_resources, partition_registers, node_resources -if TYPE_CHECKING: - from qiskit.circuit import QuantumCircuit - class _DefaultCaseType: """The type of the default-case singleton. This is used instead of just having @@ -74,9 +71,6 @@ def __init__( *, label: Optional[str] = None, ): - # pylint: disable=cyclic-import - from qiskit.circuit import QuantumCircuit - if isinstance(target, expr.Expr): if target.type.kind not in (types.Uint, types.Bool): raise CircuitError( diff --git a/qiskit/circuit/controlflow/while_loop.py b/qiskit/circuit/controlflow/while_loop.py index e834b4718cee..81abe2ed0e18 100644 --- a/qiskit/circuit/controlflow/while_loop.py +++ b/qiskit/circuit/controlflow/while_loop.py @@ -14,17 +14,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING - -from qiskit.circuit.classicalregister import Clbit, ClassicalRegister +from qiskit.circuit import Clbit, ClassicalRegister, QuantumCircuit from qiskit.circuit.classical import expr from qiskit.circuit.exceptions import CircuitError from ._builder_utils import validate_condition, condition_resources from .control_flow import ControlFlowOp -if TYPE_CHECKING: - from qiskit.circuit import QuantumCircuit - class WhileLoopOp(ControlFlowOp): """A circuit operation which repeatedly executes a subcircuit (``body``) until @@ -75,9 +70,6 @@ def params(self): @params.setter def params(self, parameters): - # pylint: disable=cyclic-import - from qiskit.circuit import QuantumCircuit - (body,) = parameters if not isinstance(body, QuantumCircuit): diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 4c179a2fac68..ed1c9731764a 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -16,7 +16,6 @@ from __future__ import annotations import copy -import itertools import multiprocessing as mp import warnings import typing @@ -46,15 +45,7 @@ from qiskit.circuit.exceptions import CircuitError from . import _classical_resource_map from ._utils import sort_parameters -from .controlflow import ControlFlowOp -from .controlflow.builder import CircuitScopeInterface, ControlFlowBuilderBlock -from .controlflow.break_loop import BreakLoopOp, BreakLoopPlaceholder -from .controlflow.continue_loop import ContinueLoopOp, ContinueLoopPlaceholder -from .controlflow.for_loop import ForLoopOp, ForLoopContext -from .controlflow.if_else import IfElseOp, IfContext -from .controlflow.switch_case import SwitchCaseOp, SwitchContext -from .controlflow.while_loop import WhileLoopOp, WhileLoopContext -from .classical import expr, types +from .classical import expr from .parameterexpression import ParameterExpression, ParameterValueType from .quantumregister import QuantumRegister, Qubit, AncillaRegister, AncillaQubit from .classicalregister import ClassicalRegister, Clbit @@ -66,7 +57,6 @@ from .bit import Bit from .quantumcircuitdata import QuantumCircuitData, CircuitInstruction from .delay import Delay -from .store import Store if typing.TYPE_CHECKING: import qiskit # pylint: disable=cyclic-import @@ -145,27 +135,9 @@ class QuantumCircuit: circuit. This gets stored as free-form data in a dict in the :attr:`~qiskit.circuit.QuantumCircuit.metadata` attribute. It will not be directly used in the circuit. - inputs: any variables to declare as ``input`` runtime variables for this circuit. These - should already be existing :class:`.expr.Var` nodes that you build from somewhere else; - if you need to create the inputs as well, use :meth:`QuantumCircuit.add_input`. The - variables given in this argument will be passed directly to :meth:`add_input`. A - circuit cannot have both ``inputs`` and ``captures``. - captures: any variables that that this circuit scope should capture from a containing scope. - The variables given here will be passed directly to :meth:`add_capture`. A circuit - cannot have both ``inputs`` and ``captures``. - declarations: any variables that this circuit should declare and initialize immediately. - You can order this input so that later declarations depend on earlier ones (including - inputs or captures). If you need to depend on values that will be computed later at - runtime, use :meth:`add_var` at an appropriate point in the circuit execution. - - This argument is intended for convenient circuit initialization when you already have a - set of created variables. The variables used here will be directly passed to - :meth:`add_var`, which you can use directly if this is the first time you are creating - the variable. Raises: CircuitError: if the circuit name, if given, is not valid. - CircuitError: if both ``inputs`` and ``captures`` are given. Examples: @@ -225,9 +197,6 @@ def __init__( name: str | None = None, global_phase: ParameterValueType = 0, metadata: dict | None = None, - inputs: Iterable[expr.Var] = (), - captures: Iterable[expr.Var] = (), - declarations: Mapping[expr.Var, expr.Expr] | Iterable[Tuple[expr.Var, expr.Expr]] = (), ): if any(not isinstance(reg, (list, QuantumRegister, ClassicalRegister)) for reg in regs): # check if inputs are integers, but also allow e.g. 2.0 @@ -257,10 +226,6 @@ def __init__( self.name = name self._increment_instances() - # An explicit implementation of the circuit scope builder interface used to dispatch appends - # and the like to the relevant control-flow scope. - self._builder_api = _OuterCircuitScopeInterface(self) - self._op_start_times = None # A stack to hold the instruction sets that are being built up during for-, if- and @@ -301,20 +266,6 @@ def __init__( self._global_phase: ParameterValueType = 0 self.global_phase = global_phase - # Add classical variables. Resolve inputs and captures first because they can't depend on - # anything, but declarations might depend on them. - self._vars_input: dict[str, expr.Var] = {} - self._vars_capture: dict[str, expr.Var] = {} - self._vars_local: dict[str, expr.Var] = {} - for input_ in inputs: - self.add_input(input_) - for capture in captures: - self.add_capture(capture) - if isinstance(declarations, Mapping): - declarations = declarations.items() - for var, initial in declarations: - self.add_var(var, initial) - self.duration = None self.unit = "dt" self.metadata = {} if metadata is None else metadata @@ -919,6 +870,8 @@ def compose( lcr_1: 0 ═══════════ lcr_1: 0 ═══════════════════════ """ + # pylint: disable=cyclic-import + from qiskit.circuit.controlflow.switch_case import SwitchCaseOp if inplace and front and self._control_flow_scopes: # If we're composing onto ourselves while in a stateful control-flow builder context, @@ -943,10 +896,6 @@ def compose( # has to be strictly larger. This allows composing final measurements onto unitary circuits. if isinstance(other, QuantumCircuit): if not self.clbits and other.clbits: - if dest._control_flow_scopes: - raise CircuitError( - "cannot implicitly add clbits while within a control-flow scope" - ) dest.add_bits(other.clbits) for reg in other.cregs: dest.add_register(reg) @@ -1167,74 +1116,6 @@ def ancillas(self) -> list[AncillaQubit]: """ return self._ancillas - @property - def num_vars(self) -> int: - """The number of runtime classical variables in the circuit. - - This is the length of the :meth:`iter_vars` iterable.""" - return self.num_input_vars + self.num_captured_vars + self.num_declared_vars - - @property - def num_input_vars(self) -> int: - """The number of runtime classical variables in the circuit marked as circuit inputs. - - This is the length of the :meth:`iter_input_vars` iterable. If this is non-zero, - :attr:`num_captured_vars` must be zero.""" - return len(self._vars_input) - - @property - def num_captured_vars(self) -> int: - """The number of runtime classical variables in the circuit marked as captured from an - enclosing scope. - - This is the length of the :meth:`iter_captured_vars` iterable. If this is non-zero, - :attr:`num_input_vars` must be zero.""" - return len(self._vars_capture) - - @property - def num_declared_vars(self) -> int: - """The number of runtime classical variables in the circuit that are declared by this - circuit scope, excluding inputs or captures. - - This is the length of the :meth:`iter_declared_vars` iterable.""" - return len(self._vars_local) - - def iter_vars(self) -> typing.Iterable[expr.Var]: - """Get an iterable over all runtime classical variables in scope within this circuit. - - This method will iterate over all variables in scope. For more fine-grained iterators, see - :meth:`iter_declared_vars`, :meth:`iter_input_vars` and :meth:`iter_captured_vars`.""" - if self._control_flow_scopes: - builder = self._control_flow_scopes[-1] - return itertools.chain(builder.iter_captured_vars(), builder.iter_local_vars()) - return itertools.chain( - self._vars_input.values(), self._vars_capture.values(), self._vars_local.values() - ) - - def iter_declared_vars(self) -> typing.Iterable[expr.Var]: - """Get an iterable over all runtime classical variables that are declared with automatic - storage duration in this scope. This excludes input variables (see :meth:`iter_input_vars`) - and captured variables (see :meth:`iter_captured_vars`).""" - if self._control_flow_scopes: - return self._control_flow_scopes[-1].iter_local_vars() - return self._vars_local.values() - - def iter_input_vars(self) -> typing.Iterable[expr.Var]: - """Get an iterable over all runtime classical variables that are declared as inputs to this - circuit scope. This excludes locally declared variables (see :meth:`iter_declared_vars`) - and captured variables (see :meth:`iter_captured_vars`).""" - if self._control_flow_scopes: - return () - return self._vars_input.values() - - def iter_captured_vars(self) -> typing.Iterable[expr.Var]: - """Get an iterable over all runtime classical variables that are captured by this circuit - scope from a containing scope. This excludes input variables (see :meth:`iter_input_vars`) - and locally declared variables (see :meth:`iter_declared_vars`).""" - if self._control_flow_scopes: - return self._control_flow_scopes[-1].iter_captured_vars() - return self._vars_capture.values() - def __and__(self, rhs: "QuantumCircuit") -> "QuantumCircuit": """Overload & to implement self.compose.""" return self.compose(rhs) @@ -1307,6 +1188,56 @@ def cbit_argument_conversion(self, clbit_representation: ClbitSpecifier) -> list clbit_representation, self.clbits, self._clbit_indices, Clbit ) + def _resolve_classical_resource(self, specifier): + """Resolve a single classical resource specifier into a concrete resource, raising an error + if the specifier is invalid. + + This is slightly different to :meth:`.cbit_argument_conversion`, because it should not + unwrap :obj:`.ClassicalRegister` instances into lists, and in general it should not allow + iterables or broadcasting. It is expected to be used as a callback for things like + :meth:`.InstructionSet.c_if` to check the validity of their arguments. + + Args: + specifier (Union[Clbit, ClassicalRegister, int]): a specifier of a classical resource + present in this circuit. An ``int`` will be resolved into a :obj:`.Clbit` using the + same conventions as measurement operations on this circuit use. + + Returns: + Union[Clbit, ClassicalRegister]: the resolved resource. + + Raises: + CircuitError: if the resource is not present in this circuit, or if the integer index + passed is out-of-bounds. + """ + if isinstance(specifier, Clbit): + if specifier not in self._clbit_indices: + raise CircuitError(f"Clbit {specifier} is not present in this circuit.") + return specifier + if isinstance(specifier, ClassicalRegister): + # This is linear complexity for something that should be constant, but QuantumCircuit + # does not currently keep a hashmap of registers, and requires non-trivial changes to + # how it exposes its registers publicly before such a map can be safely stored so it + # doesn't miss updates. (Jake, 2021-11-10). + if specifier not in self.cregs: + raise CircuitError(f"Register {specifier} is not present in this circuit.") + return specifier + if isinstance(specifier, int): + try: + return self._data.clbits[specifier] + except IndexError: + raise CircuitError(f"Classical bit index {specifier} is out-of-range.") from None + raise CircuitError(f"Unknown classical resource specifier: '{specifier}'.") + + def _validate_expr(self, node: expr.Expr) -> expr.Expr: + for var in expr.iter_vars(node): + if isinstance(var.var, Clbit): + if var.var not in self._clbit_indices: + raise CircuitError(f"Clbit {var.var} is not present in this circuit.") + elif isinstance(var.var, ClassicalRegister): + if var.var not in self.cregs: + raise CircuitError(f"Register {var.var} is not present in this circuit.") + return node + def append( self, instruction: Operation | CircuitInstruction, @@ -1361,47 +1292,39 @@ def append( "Object to append must be an Operation or have a to_instruction() method." ) - circuit_scope = self._current_scope() - # Make copy of parameterized gate instances - if params := getattr(operation, "params", ()): - is_parameter = False - for param in params: - is_parameter = is_parameter or isinstance(param, Parameter) - if isinstance(param, expr.Expr): - param = _validate_expr(circuit_scope, param) + if hasattr(operation, "params"): + is_parameter = any(isinstance(param, Parameter) for param in operation.params) if is_parameter: operation = copy.deepcopy(operation) - if isinstance(operation, ControlFlowOp): - # Verify that any variable bindings are valid. Control-flow ops are already enforced - # by the class not to contain 'input' variables. - if bad_captures := { - var - for var in itertools.chain.from_iterable( - block.iter_captured_vars() for block in operation.blocks - ) - if not self.has_var(var) - }: - raise CircuitError( - f"Control-flow op attempts to capture '{bad_captures}'" - " which are not in this circuit" - ) expanded_qargs = [self.qbit_argument_conversion(qarg) for qarg in qargs or []] expanded_cargs = [self.cbit_argument_conversion(carg) for carg in cargs or []] - instructions = InstructionSet(resource_requester=circuit_scope.resolve_classical_resource) - # For Operations that are non-Instructions, we use the Instruction's default method - broadcast_iter = ( - operation.broadcast_arguments(expanded_qargs, expanded_cargs) - if isinstance(operation, Instruction) - else Instruction.broadcast_arguments(operation, expanded_qargs, expanded_cargs) - ) - for qarg, carg in broadcast_iter: - self._check_dups(qarg) - instruction = CircuitInstruction(operation, qarg, carg) - circuit_scope.append(instruction) - instructions._add_ref(circuit_scope.instructions, len(circuit_scope.instructions) - 1) + if self._control_flow_scopes: + circuit_data = self._control_flow_scopes[-1].instructions + appender = self._control_flow_scopes[-1].append + requester = self._control_flow_scopes[-1].request_classical_resource + else: + circuit_data = self._data + appender = self._append + requester = self._resolve_classical_resource + instructions = InstructionSet(resource_requester=requester) + if isinstance(operation, Instruction): + for qarg, carg in operation.broadcast_arguments(expanded_qargs, expanded_cargs): + self._check_dups(qarg) + data_idx = len(circuit_data) + appender(CircuitInstruction(operation, qarg, carg)) + instructions._add_ref(circuit_data, data_idx) + else: + # For Operations that are non-Instructions, we use the Instruction's default method + for qarg, carg in Instruction.broadcast_arguments( + operation, expanded_qargs, expanded_cargs + ): + self._check_dups(qarg) + data_idx = len(circuit_data) + appender(CircuitInstruction(operation, qarg, carg)) + instructions._add_ref(circuit_data, data_idx) return instructions # Preferred new style. @@ -1536,11 +1459,6 @@ def get_parameter(self, name: str, default: typing.Any = ...) -> Parameter: assert qc.get_parameter("my_param", None) is my_param assert qc.get_parameter("unknown_param", None) is None - - See also: - :meth:`get_var` - A similar method, but for :class:`.expr.Var` run-time variables instead of - :class:`.Parameter` compile-time parameters. """ if (parameter := self._parameter_table.parameter_from_name(name, None)) is None: if default is Ellipsis: @@ -1562,307 +1480,11 @@ def has_parameter(self, name_or_param: str | Parameter, /) -> bool: See also: :meth:`QuantumCircuit.get_parameter` Retrieve the :class:`.Parameter` instance from this circuit by name. - :meth:`QuantumCircuit.has_var` - A similar method to this, but for run-time :class:`.expr.Var` variables instead of - compile-time :class:`.Parameter`\\ s. """ if isinstance(name_or_param, str): return self.get_parameter(name_or_param, None) is not None return self.get_parameter(name_or_param.name) == name_or_param - @typing.overload - def get_var(self, name: str, default: T) -> Union[expr.Var, T]: - ... - - # The builtin `types` module has `EllipsisType`, but only from 3.10+! - @typing.overload - def get_var(self, name: str, default: type(...) = ...) -> expr.Var: - ... - - # We use a _literal_ `Ellipsis` as the marker value to leave `None` available as a default. - def get_var(self, name: str, default: typing.Any = ...): - """Retrieve a variable that is accessible in this circuit scope by name. - - Args: - name: the name of the variable to retrieve. - default: if given, this value will be returned if the variable is not present. If it - is not given, a :exc:`KeyError` is raised instead. - - Returns: - The corresponding variable. - - Raises: - KeyError: if no default is given, but the variable does not exist. - - Examples: - Retrieve a variable by name from a circuit:: - - from qiskit.circuit import QuantumCircuit - - # Create a circuit and create a variable in it. - qc = QuantumCircuit() - my_var = qc.add_var("my_var", False) - - # We can use 'my_var' as a variable, but let's say we've lost the Python object and - # need to retrieve it. - my_var_again = qc.get_var("my_var") - - assert my_var is my_var_again - - Get a variable from a circuit by name, returning some default if it is not present:: - - assert qc.get_var("my_var", None) is my_var - assert qc.get_var("unknown_variable", None) is None - - See also: - :meth:`get_parameter` - A similar method, but for :class:`.Parameter` compile-time parameters instead of - :class:`.expr.Var` run-time variables. - """ - if (out := self._current_scope().get_var(name)) is not None: - return out - if default is Ellipsis: - raise KeyError(f"no variable named '{name}' is present") - return default - - def has_var(self, name_or_var: str | expr.Var, /) -> bool: - """Check whether a variable is accessible in this scope. - - Args: - name_or_var: the variable, or name of a variable to check. If this is a - :class:`.expr.Var` node, the variable must be exactly the given one for this - function to return ``True``. - - Returns: - whether a matching variable is accessible. - - See also: - :meth:`QuantumCircuit.get_var` - Retrieve the :class:`.expr.Var` instance from this circuit by name. - :meth:`QuantumCircuit.has_parameter` - A similar method to this, but for compile-time :class:`.Parameter`\\ s instead of - run-time :class:`.expr.Var` variables. - """ - if isinstance(name_or_var, str): - return self.get_var(name_or_var, None) is not None - return self.get_var(name_or_var.name, None) == name_or_var - - def _prepare_new_var( - self, name_or_var: str | expr.Var, type_: types.Type | None, / - ) -> expr.Var: - """The common logic for preparing and validating a new :class:`~.expr.Var` for the circuit. - - The given ``type_`` can be ``None`` if the variable specifier is already a :class:`.Var`, - and must be a :class:`~.types.Type` if it is a string. The argument is ignored if the given - first argument is a :class:`.Var` already. - - Returns the validated variable, which is guaranteed to be safe to add to the circuit.""" - if isinstance(name_or_var, str): - if type_ is None: - raise CircuitError("the type must be known when creating a 'Var' from a string") - var = expr.Var.new(name_or_var, type_) - else: - var = name_or_var - if not var.standalone: - raise CircuitError( - "cannot add variables that wrap `Clbit` or `ClassicalRegister` instances." - " Use `add_bits` or `add_register` as appropriate." - ) - - # The `var` is guaranteed to have a name because we already excluded the cases where it's - # wrapping a bit/register. - if (previous := self.get_var(var.name, default=None)) is not None: - if previous == var: - raise CircuitError(f"'{var}' is already present in the circuit") - raise CircuitError(f"cannot add '{var}' as its name shadows the existing '{previous}'") - return var - - def add_var(self, name_or_var: str | expr.Var, /, initial: typing.Any) -> expr.Var: - """Add a classical variable with automatic storage and scope to this circuit. - - The variable is considered to have been "declared" at the beginning of the circuit, but it - only becomes initialized at the point of the circuit that you call this method, so it can - depend on variables defined before it. - - Args: - name_or_var: either a string of the variable name, or an existing instance of - :class:`~.expr.Var` to re-use. Variables cannot shadow names that are already in - use within the circuit. - initial: the value to initialize this variable with. If the first argument was given - as a string name, the type of the resulting variable is inferred from the initial - expression; to control this more manually, either use :meth:`.Var.new` to manually - construct a new variable with the desired type, or use :func:`.expr.cast` to cast - the initializer to the desired type. - - This must be either a :class:`~.expr.Expr` node, or a value that can be lifted to - one using :class:`.expr.lift`. - - Returns: - The created variable. If a :class:`~.expr.Var` instance was given, the exact same - object will be returned. - - Raises: - CircuitError: if the variable cannot be created due to shadowing an existing variable. - - Examples: - Define a new variable given just a name and an initializer expression:: - - from qiskit.circuit import QuantumCircuit - - qc = QuantumCircuit(2) - my_var = qc.add_var("my_var", False) - - Reuse a variable that may have been taken from a related circuit, or otherwise - constructed manually, and initialize it to some more complicated expression:: - - from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister - from qiskit.circuit.classical import expr, types - - my_var = expr.Var.new("my_var", types.Uint(8)) - - cr1 = ClassicalRegister(8, "cr1") - cr2 = ClassicalRegister(8, "cr2") - qc = QuantumCircuit(QuantumRegister(8), cr1, cr2) - - # Get some measurement results into each register. - qc.h(0) - for i in range(1, 8): - qc.cx(0, i) - qc.measure(range(8), cr1) - - qc.reset(range(8)) - qc.h(0) - for i in range(1, 8): - qc.cx(0, i) - qc.measure(range(8), cr2) - - # Now when we add the variable, it is initialized using the runtime state of the two - # classical registers we measured into above. - qc.add_var(my_var, expr.bit_and(cr1, cr2)) - """ - # Validate the initialiser first to catch cases where the variable to be declared is being - # used in the initialiser. - circuit_scope = self._current_scope() - initial = _validate_expr(circuit_scope, expr.lift(initial)) - if isinstance(name_or_var, str): - var = expr.Var.new(name_or_var, initial.type) - elif not name_or_var.standalone: - raise CircuitError( - "cannot add variables that wrap `Clbit` or `ClassicalRegister` instances." - ) - else: - var = name_or_var - circuit_scope.add_uninitialized_var(var) - try: - # Store is responsible for ensuring the type safety of the initialisation. - store = Store(var, initial) - except CircuitError: - circuit_scope.remove_var(var) - raise - circuit_scope.append(CircuitInstruction(store, (), ())) - return var - - def add_uninitialized_var(self, var: expr.Var, /): - """Add a variable with no initializer. - - In most cases, you should use :meth:`add_var` to initialize the variable. To use this - function, you must already hold a :class:`~.expr.Var` instance, as the use of the function - typically only makes sense in copying contexts. - - .. warning:: - - Qiskit makes no assertions about what an uninitialized variable will evaluate to at - runtime, and some hardware may reject this as an error. - - You should treat this function with caution, and as a low-level primitive that is useful - only in special cases of programmatically rebuilding two like circuits. - - Args: - var: the variable to add. - """ - # This function is deliberately meant to be a bit harder to find, to have a long descriptive - # name, and to be a bit less ergonomic than `add_var` (i.e. not allowing the (name, type) - # overload) to discourage people from using it when they should use `add_var`. - # - # This function exists so that there is a method to emulate `copy_empty_like`'s behaviour of - # adding uninitialised variables, which there's no obvious way around. We need to be sure - # that _some_ sort of handling of uninitialised variables is taken into account in our - # structures, so that doesn't become a huge edge case, even though we make no assertions - # about the _meaning_ if such an expression was run on hardware. - if self._control_flow_scopes: - raise CircuitError("cannot add an uninitialized variable in a control-flow scope") - if not var.standalone: - raise CircuitError("cannot add a variable wrapping a bit or register to a circuit") - self._builder_api.add_uninitialized_var(var) - - def add_capture(self, var: expr.Var): - """Add a variable to the circuit that it should capture from a scope it will be contained - within. - - This method requires a :class:`~.expr.Var` node to enforce that you've got a handle to one, - because you will need to declare the same variable using the same object into the outer - circuit. - - This is a low-level method, which is only really useful if you are manually constructing - control-flow operations. You typically will not need to call this method, assuming you - are using the builder interface for control-flow scopes (``with`` context-manager statements - for :meth:`if_test` and the other scoping constructs). The builder interface will - automatically make the inner scopes closures on your behalf by capturing any variables that - are used within them. - - Args: - var: the variable to capture from an enclosing scope. - - Raises: - CircuitError: if the variable cannot be created due to shadowing an existing variable. - """ - if self._control_flow_scopes: - # Allow manual capturing. Not sure why it'd be useful, but there's a clear expected - # behaviour here. - self._control_flow_scopes[-1].use_var(var) - return - if self._vars_input: - raise CircuitError( - "circuits with input variables cannot be enclosed, so cannot be closures" - ) - self._vars_capture[var.name] = self._prepare_new_var(var, None) - - @typing.overload - def add_input(self, name_or_var: str, type_: types.Type, /) -> expr.Var: - ... - - @typing.overload - def add_input(self, name_or_var: expr.Var, type_: None = None, /) -> expr.Var: - ... - - def add_input( # pylint: disable=missing-raises-doc - self, name_or_var: str | expr.Var, type_: types.Type | None = None, / - ) -> expr.Var: - """Register a variable as an input to the circuit. - - Args: - name_or_var: either a string name, or an existing :class:`~.expr.Var` node to use as the - input variable. - type_: if the name is given as a string, then this must be a :class:`~.types.Type` to - use for the variable. If the variable is given as an existing :class:`~.expr.Var`, - then this must not be given, and will instead be read from the object itself. - - Returns: - the variable created, or the same variable as was passed in. - - Raises: - CircuitError: if the variable cannot be created due to shadowing an existing variable. - """ - if self._control_flow_scopes: - raise CircuitError("cannot add an input variable in a control-flow scope") - if self._vars_capture: - raise CircuitError("circuits to be enclosed with captures cannot have input variables") - if isinstance(name_or_var, expr.Var) and type_ is not None: - raise ValueError("cannot give an explicit type with an existing Var") - var = self._prepare_new_var(name_or_var, type_) - self._vars_input[var.name] = var - return var - def add_register(self, *regs: Register | int | Sequence[Bit]) -> None: """Add registers.""" if not regs: @@ -2533,14 +2155,6 @@ def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit": * global phase * all the qubits and clbits, including the registers - .. warning:: - - If the circuit contains any local variable declarations (those added by the - ``declarations`` argument to the circuit constructor, or using :meth:`add_var`), they - will be **uninitialized** in the output circuit. You will need to manually add store - instructions for them (see :class:`.Store` and :meth:`.QuantumCircuit.store`) to - initialize them. - Args: name (str): Name for the copied circuit. If None, then the name stays the same. @@ -2555,18 +2169,10 @@ def copy_empty_like(self, name: str | None = None) -> "QuantumCircuit": # copy registers correctly, in copy.copy they are only copied via reference cpy.qregs = self.qregs.copy() cpy.cregs = self.cregs.copy() - cpy._builder_api = _OuterCircuitScopeInterface(cpy) cpy._ancillas = self._ancillas.copy() cpy._qubit_indices = self._qubit_indices.copy() cpy._clbit_indices = self._clbit_indices.copy() - # Note that this causes the local variables to be uninitialised, because the stores are not - # copied. This can leave the circuit in a potentially dangerous state for users if they - # don't re-add initialiser stores. - cpy._vars_local = self._vars_local.copy() - cpy._vars_input = self._vars_input.copy() - cpy._vars_capture = self._vars_capture.copy() - cpy._parameter_table = ParameterTable() for parameter in getattr(cpy.global_phase, "parameters", ()): cpy._parameter_table[parameter] = ParameterReferences( @@ -2626,31 +2232,6 @@ def reset(self, qubit: QubitSpecifier) -> InstructionSet: return self.append(Reset(), [qubit], []) - def store(self, lvalue: typing.Any, rvalue: typing.Any, /) -> InstructionSet: - """Store the result of the given runtime classical expression ``rvalue`` in the memory - location defined by ``lvalue``. - - Typically ``lvalue`` will be a :class:`~.expr.Var` node and ``rvalue`` will be some - :class:`~.expr.Expr` to write into it, but anything that :func:`.expr.lift` can raise to an - :class:`~.expr.Expr` is permissible in both places, and it will be called on them. - - Args: - lvalue: a valid specifier for a memory location in the circuit. This will typically be - a :class:`~.expr.Var` node, but you can also write to :class:`.Clbit` or - :class:`.ClassicalRegister` memory locations if your hardware supports it. The - memory location must already be present in the circuit. - rvalue: a runtime classical expression whose result should be written into the given - memory location. - - .. seealso:: - :class:`~.circuit.Store` - The backing :class:`~.circuit.Instruction` class that represents this operation. - - :meth:`add_var` - Create a new variable in the circuit that can be written to with this method. - """ - return self.append(Store(expr.lift(lvalue), expr.lift(rvalue)), (), ()) - def measure(self, qubit: QubitSpecifier, cbit: ClbitSpecifier) -> InstructionSet: r"""Measure a quantum bit (``qubit``) in the Z basis into a classical bit (``cbit``). @@ -4712,18 +4293,28 @@ def _push_scope( forbidden_message: If given, all attempts to add instructions to this scope will raise a :exc:`.CircuitError` with this message. """ + # pylint: disable=cyclic-import + from qiskit.circuit.controlflow.builder import ControlFlowBuilderBlock + + # Chain resource requests so things like registers added to inner scopes via conditions are + # requested in the outer scope as well. + if self._control_flow_scopes: + resource_requester = self._control_flow_scopes[-1].request_classical_resource + else: + resource_requester = self._resolve_classical_resource + self._control_flow_scopes.append( ControlFlowBuilderBlock( qubits, clbits, - parent=self._current_scope(), + resource_requester=resource_requester, registers=registers, allow_jumps=allow_jumps, forbidden_message=forbidden_message, ) ) - def _pop_scope(self) -> ControlFlowBuilderBlock: + def _pop_scope(self) -> "qiskit.circuit.controlflow.builder.ControlFlowBuilderBlock": """Finish a scope used in the control-flow builder interface, and return it to the caller. This should only be done by the control-flow context managers, since they naturally @@ -4793,7 +4384,7 @@ def while_loop( clbits: None, *, label: str | None, - ) -> WhileLoopContext: + ) -> "qiskit.circuit.controlflow.while_loop.WhileLoopContext": ... @typing.overload @@ -4851,11 +4442,13 @@ def while_loop(self, condition, body=None, qubits=None, clbits=None, *, label=No Raises: CircuitError: if an incorrect calling convention is used. """ - circuit_scope = self._current_scope() + # pylint: disable=cyclic-import + from qiskit.circuit.controlflow.while_loop import WhileLoopOp, WhileLoopContext + if isinstance(condition, expr.Expr): - condition = _validate_expr(circuit_scope, condition) + condition = self._validate_expr(condition) else: - condition = (circuit_scope.resolve_classical_resource(condition[0]), condition[1]) + condition = (self._resolve_classical_resource(condition[0]), condition[1]) if body is None: if qubits is not None or clbits is not None: @@ -4881,7 +4474,7 @@ def for_loop( clbits: None, *, label: str | None, - ) -> ForLoopContext: + ) -> "qiskit.circuit.controlflow.for_loop.ForLoopContext": ... @typing.overload @@ -4949,6 +4542,9 @@ def for_loop( Raises: CircuitError: if an incorrect calling convention is used. """ + # pylint: disable=cyclic-import + from qiskit.circuit.controlflow.for_loop import ForLoopOp, ForLoopContext + if body is None: if qubits is not None or clbits is not None: raise CircuitError( @@ -4971,7 +4567,7 @@ def if_test( clbits: None, *, label: str | None, - ) -> IfContext: + ) -> "qiskit.circuit.controlflow.if_else.IfContext": ... @typing.overload @@ -5054,11 +4650,13 @@ def if_test( Returns: A handle to the instruction created. """ - circuit_scope = self._current_scope() + # pylint: disable=cyclic-import + from qiskit.circuit.controlflow.if_else import IfElseOp, IfContext + if isinstance(condition, expr.Expr): - condition = _validate_expr(circuit_scope, condition) + condition = self._validate_expr(condition) else: - condition = (circuit_scope.resolve_classical_resource(condition[0]), condition[1]) + condition = (self._resolve_classical_resource(condition[0]), condition[1]) if true_body is None: if qubits is not None or clbits is not None: @@ -5121,11 +4719,13 @@ def if_else( Returns: A handle to the instruction created. """ - circuit_scope = self._current_scope() + # pylint: disable=cyclic-import + from qiskit.circuit.controlflow.if_else import IfElseOp + if isinstance(condition, expr.Expr): - condition = _validate_expr(circuit_scope, condition) + condition = self._validate_expr(condition) else: - condition = (circuit_scope.resolve_classical_resource(condition[0]), condition[1]) + condition = (self._resolve_classical_resource(condition[0]), condition[1]) return self.append(IfElseOp(condition, true_body, false_body, label), qubits, clbits) @@ -5138,7 +4738,7 @@ def switch( clbits: None, *, label: Optional[str], - ) -> SwitchContext: + ) -> "qiskit.circuit.controlflow.switch_case.SwitchContext": ... @typing.overload @@ -5204,12 +4804,13 @@ def switch(self, target, cases=None, qubits=None, clbits=None, *, label=None): Raises: CircuitError: if an incorrect calling convention is used. """ + # pylint: disable=cyclic-import + from qiskit.circuit.controlflow.switch_case import SwitchCaseOp, SwitchContext - circuit_scope = self._current_scope() if isinstance(target, expr.Expr): - target = _validate_expr(circuit_scope, target) + target = self._validate_expr(target) else: - target = circuit_scope.resolve_classical_resource(target) + target = self._resolve_classical_resource(target) if cases is None: if qubits is not None or clbits is not None: raise CircuitError( @@ -5243,6 +4844,9 @@ def break_loop(self) -> InstructionSet: CircuitError: if this method was called within a builder context, but not contained within a loop. """ + # pylint: disable=cyclic-import + from qiskit.circuit.controlflow.break_loop import BreakLoopOp, BreakLoopPlaceholder + if self._control_flow_scopes: operation = BreakLoopPlaceholder() resources = operation.placeholder_resources() @@ -5270,6 +4874,9 @@ def continue_loop(self) -> InstructionSet: CircuitError: if this method was called within a builder context, but not contained within a loop. """ + # pylint: disable=cyclic-import + from qiskit.circuit.controlflow.continue_loop import ContinueLoopOp, ContinueLoopPlaceholder + if self._control_flow_scopes: operation = ContinueLoopPlaceholder() resources = operation.placeholder_resources() @@ -5471,36 +5078,6 @@ def resolve_classical_resource(self, specifier): raise CircuitError(f"Classical bit index {specifier} is out-of-range.") from None raise CircuitError(f"Unknown classical resource specifier: '{specifier}'.") - def add_uninitialized_var(self, var): - var = self.circuit._prepare_new_var(var, None) - self.circuit._vars_local[var.name] = var - - def remove_var(self, var): - self.circuit._vars_local.pop(var.name) - - def get_var(self, name): - if (out := self.circuit._vars_local.get(name)) is not None: - return out - if (out := self.circuit._vars_capture.get(name)) is not None: - return out - return self.circuit._vars_input.get(name) - - def use_var(self, var): - if self.get_var(var.name) != var: - raise CircuitError(f"'{var}' is not present in this circuit") - - -def _validate_expr(circuit_scope: CircuitScopeInterface, node: expr.Expr) -> expr.Expr: - # This takes the `circuit_scope` object as an argument rather than being a circuit method and - # inferring it because we may want to call this several times, and we almost invariably already - # need the interface implementation for something else anyway. - for var in set(expr.iter_vars(node)): - if var.standalone: - circuit_scope.use_var(var) - else: - circuit_scope.resolve_classical_resource(var.var) - return node - class _ParameterBindsDict: __slots__ = ("mapping", "allowed_keys") diff --git a/qiskit/circuit/store.py b/qiskit/circuit/store.py deleted file mode 100644 index 100fe0e629b9..000000000000 --- a/qiskit/circuit/store.py +++ /dev/null @@ -1,87 +0,0 @@ -# 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. - -"""The 'Store' operation.""" - -from __future__ import annotations - -import typing - -from .exceptions import CircuitError -from .classical import expr, types -from .instruction import Instruction - - -def _handle_equal_types(lvalue: expr.Expr, rvalue: expr.Expr, /) -> tuple[expr.Expr, expr.Expr]: - return lvalue, rvalue - - -def _handle_implicit_cast(lvalue: expr.Expr, rvalue: expr.Expr, /) -> tuple[expr.Expr, expr.Expr]: - return lvalue, expr.Cast(rvalue, lvalue.type, implicit=True) - - -def _requires_lossless_cast(lvalue: expr.Expr, rvalue: expr.Expr, /) -> typing.NoReturn: - raise CircuitError(f"an explicit cast is required from '{rvalue.type}' to '{lvalue.type}'") - - -def _requires_dangerous_cast(lvalue: expr.Expr, rvalue: expr.Expr, /) -> typing.NoReturn: - raise CircuitError( - f"an explicit cast is required from '{rvalue.type}' to '{lvalue.type}', which may be lossy" - ) - - -def _no_cast_possible(lvalue: expr.Expr, rvalue: expr.Expr) -> typing.NoReturn: - raise CircuitError(f"no cast is possible from '{rvalue.type}' to '{lvalue.type}'") - - -_HANDLE_CAST = { - types.CastKind.EQUAL: _handle_equal_types, - types.CastKind.IMPLICIT: _handle_implicit_cast, - types.CastKind.LOSSLESS: _requires_lossless_cast, - types.CastKind.DANGEROUS: _requires_dangerous_cast, - types.CastKind.NONE: _no_cast_possible, -} - - -class Store(Instruction): - """A manual storage of some classical value to a classical memory location. - - This is a low-level primitive of the classical-expression handling (similar to how - :class:`~.circuit.Measure` is a primitive for quantum measurement), and is not safe for - subclassing. It is likely to become a special-case instruction in later versions of Qiskit - circuit and compiler internal representations.""" - - def __init__(self, lvalue: expr.Expr, rvalue: expr.Expr): - if not expr.is_lvalue(lvalue): - raise CircuitError(f"'{lvalue}' is not an l-value") - - cast_kind = types.cast_kind(rvalue.type, lvalue.type) - if (handler := _HANDLE_CAST.get(cast_kind)) is None: - raise RuntimeError(f"unhandled cast kind required: {cast_kind}") - lvalue, rvalue = handler(lvalue, rvalue) - - super().__init__("store", 0, 0, [lvalue, rvalue]) - - @property - def lvalue(self): - """Get the l-value :class:`~.expr.Expr` node that is being stored to.""" - return self.params[0] - - @property - def rvalue(self): - """Get the r-value :class:`~.expr.Expr` node that is being written into the l-value.""" - return self.params[1] - - def c_if(self, classical, val): - raise NotImplementedError( - "stores cannot be conditioned with `c_if`; use a full `if_test` context instead" - ) diff --git a/releasenotes/notes/classical-store-e64ee1286219a862.yaml b/releasenotes/notes/classical-store-e64ee1286219a862.yaml deleted file mode 100644 index 729c70a6989c..000000000000 --- a/releasenotes/notes/classical-store-e64ee1286219a862.yaml +++ /dev/null @@ -1,56 +0,0 @@ ---- -features: - - | - A :class:`.QuantumCircuit` can now contain typed classical variables:: - - from qiskit.circuit import QuantumCircuit, ClassicalRegister, QuantumRegister - from qiskit.circuit.classical import expr, types - - qr = QuantumRegister(2, "q") - cr = ClassicalRegister(2, "c") - qc = QuantumCircuit(qr, cr) - # Add two input variables to the circuit with different types. - a = qc.add_input("a", types.Bool()) - mask = qc.add_input("mask", types.Uint(2)) - - # Test whether the input variable was true at runtime. - with qc.if_test(a) as else_: - qc.x(0) - with else_: - qc.h(0) - - qc.cx(0, 1) - qc.measure(qr, cr) - - # Add a typed variable manually, initialized to the same value as the classical register. - b = qc.add_var("b", expr.lift(cr)) - - qc.reset([0, 1]) - qc.h(0) - qc.cx(0, 1) - qc.measure(qr, cr) - - # Store some calculated value into the `b` variable. - qc.store(b, expr.bit_and(b, cr)) - # Test whether we had equality, up to a mask. - with qc.if_test(expr.equal(expr.bit_and(b, mask), mask)): - qc.x(0) - - These variables can be specified either as *inputs* to the circuit, or as scoped variables. - The circuit object does not yet have support for representing typed classical-variable *outputs*, - but this will be added later when hardware and the result interfaces are in more of a position - to support it. Circuits that represent a block of an inner scope may also capture variables - from outer scopes. - - A variable is a :class:`.Var` node, which can now contain an arbitrary type, and represents a - unique memory location within its live range when added to a circuit. These can be constructed - in a circuit using :meth:`.QuantumCircuit.add_var` and :meth:`~.QuantumCircuit.add_input`, or - at a lower level using :meth:`.Var.new`. - - Variables can be manually stored to, using the :class:`.Store` instruction and its corresponding - circuit method :meth:`.QuantumCircuit.store`. This includes writing to :class:`.Clbit` and - :class:`.ClassicalRegister` instances wrapped in :class:`.Var` nodes. - - Variables can be used wherever classical expressions (see :mod:`qiskit.circuit.classical.expr`) - are valid. Currently this is the target expressions of control-flow operations, though we plan - to expand this to gate parameters in the future, as the type and expression system are expanded. diff --git a/releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml b/releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml deleted file mode 100644 index 70a1cf81d061..000000000000 --- a/releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -features: - - | - Classical types (subclasses of :class:`~classical.types.Type`) and variables (:class:`~.expr.Var`) - are now hashable. diff --git a/releasenotes/notes/expr-var-standalone-2c1116583a2be9fd.yaml b/releasenotes/notes/expr-var-standalone-2c1116583a2be9fd.yaml deleted file mode 100644 index 71ec0320032e..000000000000 --- a/releasenotes/notes/expr-var-standalone-2c1116583a2be9fd.yaml +++ /dev/null @@ -1,6 +0,0 @@ ---- -features: - - | - :class:`~.expr.Var` nodes now have a :attr:`.Var.standalone` property to quickly query whether - they are new-style memory-owning variables, or whether they wrap old-style classical memory in - the form of a :class:`.Clbit` or :class:`.ClassicalRegister`. diff --git a/test/python/circuit/classical/test_expr_helpers.py b/test/python/circuit/classical/test_expr_helpers.py index 31b4d7028a8b..f7b420c07144 100644 --- a/test/python/circuit/classical/test_expr_helpers.py +++ b/test/python/circuit/classical/test_expr_helpers.py @@ -115,30 +115,3 @@ def always_equal(_): # ``True`` instead. self.assertFalse(expr.structurally_equivalent(left, right, not_handled, not_handled)) self.assertTrue(expr.structurally_equivalent(left, right, always_equal, always_equal)) - - -@ddt.ddt -class TestIsLValue(QiskitTestCase): - @ddt.data( - expr.Var.new("a", types.Bool()), - expr.Var.new("b", types.Uint(8)), - expr.Var(Clbit(), types.Bool()), - expr.Var(ClassicalRegister(8, "cr"), types.Uint(8)), - ) - def test_happy_cases(self, lvalue): - self.assertTrue(expr.is_lvalue(lvalue)) - - @ddt.data( - expr.Value(3, types.Uint(2)), - expr.Value(False, types.Bool()), - expr.Cast(expr.Var.new("a", types.Uint(2)), types.Uint(8)), - expr.Unary(expr.Unary.Op.LOGIC_NOT, expr.Var.new("a", types.Bool()), types.Bool()), - expr.Binary( - expr.Binary.Op.LOGIC_AND, - expr.Var.new("a", types.Bool()), - expr.Var.new("b", types.Bool()), - types.Bool(), - ), - ) - def test_bad_cases(self, not_an_lvalue): - self.assertFalse(expr.is_lvalue(not_an_lvalue)) diff --git a/test/python/circuit/classical/test_expr_properties.py b/test/python/circuit/classical/test_expr_properties.py index f8c1277cd0f4..a6873153c0eb 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -14,12 +14,11 @@ import copy import pickle -import uuid import ddt from qiskit.test import QiskitTestCase -from qiskit.circuit import ClassicalRegister, Clbit +from qiskit.circuit import ClassicalRegister from qiskit.circuit.classical import expr, types @@ -57,78 +56,3 @@ def test_expr_can_be_cloned(self, obj): self.assertEqual(obj, copy.copy(obj)) self.assertEqual(obj, copy.deepcopy(obj)) self.assertEqual(obj, pickle.loads(pickle.dumps(obj))) - - def test_var_equality(self): - """Test that various types of :class:`.expr.Var` equality work as expected both in equal and - unequal cases.""" - var_a_bool = expr.Var.new("a", types.Bool()) - self.assertEqual(var_a_bool, var_a_bool) - - # Allocating a new variable should not compare equal, despite the name match. A semantic - # equality checker can choose to key these variables on only their names and types, if it - # knows that that check is valid within the semantic context. - self.assertNotEqual(var_a_bool, expr.Var.new("a", types.Bool())) - - # Manually constructing the same object with the same UUID should cause it compare equal, - # though, for serialisation ease. - self.assertEqual(var_a_bool, expr.Var(var_a_bool.var, types.Bool(), name="a")) - - # This is a badly constructed variable because it's using a different type to refer to the - # same storage location (the UUID) as another variable. It is an IR error to generate this - # sort of thing, but we can't fully be responsible for that and a pass would need to go out - # of its way to do this incorrectly, but we can still ensure that the direct equality check - # would spot the error. - self.assertNotEqual( - var_a_bool, expr.Var(var_a_bool.var, types.Uint(8), name=var_a_bool.name) - ) - - # This is also badly constructed because it uses a different name to refer to the "same" - # storage location. - self.assertNotEqual(var_a_bool, expr.Var(var_a_bool.var, types.Bool(), name="b")) - - # Obviously, two variables of different types and names should compare unequal. - self.assertNotEqual(expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(8))) - # As should two variables of the same name but different storage locations and types. - self.assertNotEqual(expr.Var.new("a", types.Bool()), expr.Var.new("a", types.Uint(8))) - - def test_var_uuid_clone(self): - """Test that :class:`.expr.Var` instances that have an associated UUID and name roundtrip - through pickle and copy operations to produce values that compare equal.""" - var_a_u8 = expr.Var.new("a", types.Uint(8)) - - self.assertEqual(var_a_u8, pickle.loads(pickle.dumps(var_a_u8))) - self.assertEqual(var_a_u8, copy.copy(var_a_u8)) - self.assertEqual(var_a_u8, copy.deepcopy(var_a_u8)) - - def test_var_standalone(self): - """Test that the ``Var.standalone`` property is set correctly.""" - self.assertTrue(expr.Var.new("a", types.Bool()).standalone) - self.assertTrue(expr.Var.new("a", types.Uint(8)).standalone) - self.assertFalse(expr.Var(Clbit(), types.Bool()).standalone) - self.assertFalse(expr.Var(ClassicalRegister(8, "cr"), types.Uint(8)).standalone) - - def test_var_hashable(self): - clbits = [Clbit(), Clbit()] - cregs = [ClassicalRegister(2, "cr1"), ClassicalRegister(2, "cr2")] - - vars_ = [ - expr.Var.new("a", types.Bool()), - expr.Var.new("b", types.Uint(16)), - expr.Var(clbits[0], types.Bool()), - expr.Var(clbits[1], types.Bool()), - expr.Var(cregs[0], types.Uint(2)), - expr.Var(cregs[1], types.Uint(2)), - ] - duplicates = [ - expr.Var(uuid.UUID(bytes=vars_[0].var.bytes), types.Bool(), name=vars_[0].name), - expr.Var(uuid.UUID(bytes=vars_[1].var.bytes), types.Uint(16), name=vars_[1].name), - expr.Var(clbits[0], types.Bool()), - expr.Var(clbits[1], types.Bool()), - expr.Var(cregs[0], types.Uint(2)), - expr.Var(cregs[1], types.Uint(2)), - ] - - # Smoke test. - self.assertEqual(vars_, duplicates) - # Actual test of hashability properties. - self.assertEqual(set(vars_ + duplicates), set(vars_)) diff --git a/test/python/circuit/classical/test_types_ordering.py b/test/python/circuit/classical/test_types_ordering.py index 58417fb17c03..374e1ecff1b1 100644 --- a/test/python/circuit/classical/test_types_ordering.py +++ b/test/python/circuit/classical/test_types_ordering.py @@ -58,13 +58,3 @@ def test_greater(self): self.assertEqual(types.greater(types.Bool(), types.Bool()), types.Bool()) with self.assertRaisesRegex(TypeError, "no ordering"): types.greater(types.Bool(), types.Uint(8)) - - -class TestTypesCastKind(QiskitTestCase): - 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.Uint(8), types.Bool()), types.CastKind.IMPLICIT) - 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) diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 1bd96149f347..9002e0e08460 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -19,7 +19,6 @@ from qiskit import BasicAer, ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit.circuit import Gate, Instruction, Measure, Parameter, Barrier from qiskit.circuit.bit import Bit -from qiskit.circuit.classical import expr, types from qiskit.circuit.classicalregister import Clbit from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.controlflow import IfElseOp @@ -411,77 +410,6 @@ def test_copy_empty_like_circuit(self): copied = qc.copy_empty_like("copy") self.assertEqual(copied.name, "copy") - def test_copy_variables(self): - """Test that a full copy of circuits including variables copies them across.""" - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Uint(8)) - c = expr.Var.new("c", types.Bool()) - d = expr.Var.new("d", types.Uint(8)) - - qc = QuantumCircuit(inputs=[a], declarations=[(c, expr.lift(False))]) - copied = qc.copy() - self.assertEqual({a}, set(copied.iter_input_vars())) - self.assertEqual({c}, set(copied.iter_declared_vars())) - self.assertEqual( - [instruction.operation for instruction in qc], - [instruction.operation for instruction in copied.data], - ) - - # Check that the original circuit is not mutated. - copied.add_input(b) - copied.add_var(d, 0xFF) - self.assertEqual({a, b}, set(copied.iter_input_vars())) - self.assertEqual({c, d}, set(copied.iter_declared_vars())) - self.assertEqual({a}, set(qc.iter_input_vars())) - self.assertEqual({c}, set(qc.iter_declared_vars())) - - qc = QuantumCircuit(captures=[b], declarations=[(a, expr.lift(False)), (c, a)]) - copied = qc.copy() - self.assertEqual({b}, set(copied.iter_captured_vars())) - self.assertEqual({a, c}, set(copied.iter_declared_vars())) - self.assertEqual( - [instruction.operation for instruction in qc], - [instruction.operation for instruction in copied.data], - ) - - # Check that the original circuit is not mutated. - copied.add_capture(d) - self.assertEqual({b, d}, set(copied.iter_captured_vars())) - self.assertEqual({b}, set(qc.iter_captured_vars())) - - def test_copy_empty_variables(self): - """Test that an empty copy of circuits including variables copies them across, but does not - initialise them.""" - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Uint(8)) - c = expr.Var.new("c", types.Bool()) - d = expr.Var.new("d", types.Uint(8)) - - qc = QuantumCircuit(inputs=[a], declarations=[(c, expr.lift(False))]) - copied = qc.copy_empty_like() - self.assertEqual({a}, set(copied.iter_input_vars())) - self.assertEqual({c}, set(copied.iter_declared_vars())) - self.assertEqual([], list(copied.data)) - - # Check that the original circuit is not mutated. - copied.add_input(b) - copied.add_var(d, 0xFF) - self.assertEqual({a, b}, set(copied.iter_input_vars())) - self.assertEqual({c, d}, set(copied.iter_declared_vars())) - self.assertEqual({a}, set(qc.iter_input_vars())) - self.assertEqual({c}, set(qc.iter_declared_vars())) - - qc = QuantumCircuit(captures=[b], declarations=[(a, expr.lift(False)), (c, a)]) - copied = qc.copy_empty_like() - self.assertEqual({b}, set(copied.iter_captured_vars())) - self.assertEqual({a, c}, set(copied.iter_declared_vars())) - self.assertEqual([], list(copied.data)) - - # Check that the original circuit is not mutated. - copied.add_capture(d) - self.assertEqual({b, d}, set(copied.iter_captured_vars())) - self.assertEqual({b}, set(qc.iter_captured_vars())) - def test_copy_empty_like_parametric_phase(self): """Test that the parameter table of an empty circuit remains valid after copying a circuit with a parametric global phase.""" diff --git a/test/python/circuit/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py deleted file mode 100644 index c09e4717af3f..000000000000 --- a/test/python/circuit/test_circuit_vars.py +++ /dev/null @@ -1,393 +0,0 @@ -# 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.test import QiskitTestCase -from qiskit.circuit import QuantumCircuit, CircuitError, Clbit, ClassicalRegister -from qiskit.circuit.classical import expr, types - - -class TestCircuitVars(QiskitTestCase): - """Tests for variable-manipulation routines on circuits. More specific functionality is likely - tested in the suites of the specific methods.""" - - def test_initialise_inputs(self): - vars_ = [expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(16))] - qc = QuantumCircuit(inputs=vars_) - self.assertEqual(set(vars_), set(qc.iter_vars())) - self.assertEqual(qc.num_vars, len(vars_)) - self.assertEqual(qc.num_input_vars, len(vars_)) - self.assertEqual(qc.num_captured_vars, 0) - self.assertEqual(qc.num_declared_vars, 0) - - def test_initialise_captures(self): - vars_ = [expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Uint(16))] - qc = QuantumCircuit(captures=vars_) - self.assertEqual(set(vars_), set(qc.iter_vars())) - self.assertEqual(qc.num_vars, len(vars_)) - self.assertEqual(qc.num_input_vars, 0) - self.assertEqual(qc.num_captured_vars, len(vars_)) - self.assertEqual(qc.num_declared_vars, 0) - - def test_initialise_declarations_iterable(self): - vars_ = [ - (expr.Var.new("a", types.Bool()), expr.lift(True)), - (expr.Var.new("b", types.Uint(16)), expr.lift(0xFFFF)), - ] - qc = QuantumCircuit(declarations=vars_) - - self.assertEqual({var for var, _initialiser in vars_}, set(qc.iter_vars())) - self.assertEqual(qc.num_vars, len(vars_)) - self.assertEqual(qc.num_input_vars, 0) - self.assertEqual(qc.num_captured_vars, 0) - self.assertEqual(qc.num_declared_vars, len(vars_)) - operations = [ - (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) - for instruction in qc.data - ] - self.assertEqual(operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_]) - - def test_initialise_declarations_mapping(self): - # Dictionary iteration order is guaranteed to be insertion order. - vars_ = { - expr.Var.new("a", types.Bool()): expr.lift(True), - expr.Var.new("b", types.Uint(16)): expr.lift(0xFFFF), - } - qc = QuantumCircuit(declarations=vars_) - - self.assertEqual(set(vars_), set(qc.iter_vars())) - operations = [ - (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) - for instruction in qc.data - ] - self.assertEqual( - operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_.items()] - ) - - def test_initialise_declarations_dependencies(self): - """Test that the cirucit initialiser can take in declarations with dependencies between - them, provided they're specified in a suitable order.""" - a = expr.Var.new("a", types.Bool()) - vars_ = [ - (a, expr.lift(True)), - (expr.Var.new("b", types.Bool()), a), - ] - qc = QuantumCircuit(declarations=vars_) - - self.assertEqual({var for var, _initialiser in vars_}, set(qc.iter_vars())) - operations = [ - (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) - for instruction in qc.data - ] - self.assertEqual(operations, [("store", lvalue, rvalue) for lvalue, rvalue in vars_]) - - def test_initialise_inputs_declarations(self): - a = expr.Var.new("a", types.Uint(16)) - b = expr.Var.new("b", types.Uint(16)) - b_init = expr.bit_and(a, 0xFFFF) - qc = QuantumCircuit(inputs=[a], declarations={b: b_init}) - - self.assertEqual({a}, set(qc.iter_input_vars())) - self.assertEqual({b}, set(qc.iter_declared_vars())) - self.assertEqual({a, b}, set(qc.iter_vars())) - self.assertEqual(qc.num_vars, 2) - self.assertEqual(qc.num_input_vars, 1) - self.assertEqual(qc.num_captured_vars, 0) - self.assertEqual(qc.num_declared_vars, 1) - operations = [ - (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) - for instruction in qc.data - ] - self.assertEqual(operations, [("store", b, b_init)]) - - def test_initialise_captures_declarations(self): - a = expr.Var.new("a", types.Uint(16)) - b = expr.Var.new("b", types.Uint(16)) - b_init = expr.bit_and(a, 0xFFFF) - qc = QuantumCircuit(captures=[a], declarations={b: b_init}) - - self.assertEqual({a}, set(qc.iter_captured_vars())) - self.assertEqual({b}, set(qc.iter_declared_vars())) - self.assertEqual({a, b}, set(qc.iter_vars())) - self.assertEqual(qc.num_vars, 2) - self.assertEqual(qc.num_input_vars, 0) - self.assertEqual(qc.num_captured_vars, 1) - self.assertEqual(qc.num_declared_vars, 1) - operations = [ - (instruction.operation.name, instruction.operation.lvalue, instruction.operation.rvalue) - for instruction in qc.data - ] - self.assertEqual(operations, [("store", b, b_init)]) - - def test_add_uninitialized_var(self): - a = expr.Var.new("a", types.Bool()) - qc = QuantumCircuit() - qc.add_uninitialized_var(a) - self.assertEqual({a}, set(qc.iter_vars())) - self.assertEqual([], list(qc.data)) - - def test_add_var_returns_good_var(self): - qc = QuantumCircuit() - a = qc.add_var("a", expr.lift(True)) - self.assertEqual(a.name, "a") - self.assertEqual(a.type, types.Bool()) - - b = qc.add_var("b", expr.Value(0xFF, types.Uint(8))) - self.assertEqual(b.name, "b") - self.assertEqual(b.type, types.Uint(8)) - - def test_add_var_returns_input(self): - """Test that the `Var` returned by `add_var` is the same as the input if `Var`.""" - a = expr.Var.new("a", types.Bool()) - qc = QuantumCircuit() - a_other = qc.add_var(a, expr.lift(True)) - self.assertIs(a, a_other) - - def test_add_input_returns_good_var(self): - qc = QuantumCircuit() - a = qc.add_input("a", types.Bool()) - self.assertEqual(a.name, "a") - self.assertEqual(a.type, types.Bool()) - - b = qc.add_input("b", types.Uint(8)) - self.assertEqual(b.name, "b") - self.assertEqual(b.type, types.Uint(8)) - - def test_add_input_returns_input(self): - """Test that the `Var` returned by `add_input` is the same as the input if `Var`.""" - a = expr.Var.new("a", types.Bool()) - qc = QuantumCircuit() - a_other = qc.add_input(a) - self.assertIs(a, a_other) - - def test_cannot_have_both_inputs_and_captures(self): - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Bool()) - - with self.assertRaisesRegex(CircuitError, "circuits with input.*cannot be closures"): - QuantumCircuit(inputs=[a], captures=[b]) - - qc = QuantumCircuit(inputs=[a]) - with self.assertRaisesRegex(CircuitError, "circuits with input.*cannot be closures"): - qc.add_capture(b) - - qc = QuantumCircuit(captures=[a]) - with self.assertRaisesRegex(CircuitError, "circuits to be enclosed.*cannot have input"): - qc.add_input(b) - - def test_cannot_add_cyclic_declaration(self): - a = expr.Var.new("a", types.Bool()) - with self.assertRaisesRegex(CircuitError, "not present in this circuit"): - QuantumCircuit(declarations=[(a, a)]) - - qc = QuantumCircuit() - with self.assertRaisesRegex(CircuitError, "not present in this circuit"): - qc.add_var(a, a) - - def test_initialise_inputs_equal_to_add_input(self): - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Uint(16)) - - qc_init = QuantumCircuit(inputs=[a, b]) - qc_manual = QuantumCircuit() - qc_manual.add_input(a) - qc_manual.add_input(b) - self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) - - qc_manual = QuantumCircuit() - a = qc_manual.add_input("a", types.Bool()) - b = qc_manual.add_input("b", types.Uint(16)) - qc_init = QuantumCircuit(inputs=[a, b]) - self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) - - def test_initialise_captures_equal_to_add_capture(self): - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Uint(16)) - - qc_init = QuantumCircuit(captures=[a, b]) - qc_manual = QuantumCircuit() - qc_manual.add_capture(a) - qc_manual.add_capture(b) - self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) - - def test_initialise_declarations_equal_to_add_var(self): - a = expr.Var.new("a", types.Bool()) - a_init = expr.lift(False) - b = expr.Var.new("b", types.Uint(16)) - b_init = expr.lift(0xFFFF) - - qc_init = QuantumCircuit(declarations=[(a, a_init), (b, b_init)]) - qc_manual = QuantumCircuit() - qc_manual.add_var(a, a_init) - qc_manual.add_var(b, b_init) - self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) - self.assertEqual(qc_init.data, qc_manual.data) - - qc_manual = QuantumCircuit() - a = qc_manual.add_var("a", a_init) - b = qc_manual.add_var("b", b_init) - qc_init = QuantumCircuit(declarations=[(a, a_init), (b, b_init)]) - self.assertEqual(list(qc_init.iter_vars()), list(qc_manual.iter_vars())) - self.assertEqual(qc_init.data, qc_manual.data) - - def test_cannot_shadow_vars(self): - """Test that exact duplicate ``Var`` nodes within different combinations of the inputs are - detected and rejected.""" - a = expr.Var.new("a", types.Bool()) - a_init = expr.lift(True) - with self.assertRaisesRegex(CircuitError, "already present"): - QuantumCircuit(inputs=[a, a]) - with self.assertRaisesRegex(CircuitError, "already present"): - QuantumCircuit(captures=[a, a]) - with self.assertRaisesRegex(CircuitError, "already present"): - QuantumCircuit(declarations=[(a, a_init), (a, a_init)]) - with self.assertRaisesRegex(CircuitError, "already present"): - QuantumCircuit(inputs=[a], declarations=[(a, a_init)]) - with self.assertRaisesRegex(CircuitError, "already present"): - QuantumCircuit(captures=[a], declarations=[(a, a_init)]) - - def test_cannot_shadow_names(self): - """Test that exact duplicate ``Var`` nodes within different combinations of the inputs are - detected and rejected.""" - a_bool1 = expr.Var.new("a", types.Bool()) - a_bool2 = expr.Var.new("a", types.Bool()) - a_uint = expr.Var.new("a", types.Uint(16)) - a_bool_init = expr.lift(True) - a_uint_init = expr.lift(0xFFFF) - - tests = [ - ((a_bool1, a_bool_init), (a_bool2, a_bool_init)), - ((a_bool1, a_bool_init), (a_uint, a_uint_init)), - ] - for (left, left_init), (right, right_init) in tests: - with self.assertRaisesRegex(CircuitError, "its name shadows"): - QuantumCircuit(inputs=(left, right)) - with self.assertRaisesRegex(CircuitError, "its name shadows"): - QuantumCircuit(captures=(left, right)) - with self.assertRaisesRegex(CircuitError, "its name shadows"): - QuantumCircuit(declarations=[(left, left_init), (right, right_init)]) - with self.assertRaisesRegex(CircuitError, "its name shadows"): - QuantumCircuit(inputs=[left], declarations=[(right, right_init)]) - with self.assertRaisesRegex(CircuitError, "its name shadows"): - QuantumCircuit(captures=[left], declarations=[(right, right_init)]) - - qc = QuantumCircuit(inputs=[left]) - with self.assertRaisesRegex(CircuitError, "its name shadows"): - qc.add_input(right) - qc = QuantumCircuit(inputs=[left]) - with self.assertRaisesRegex(CircuitError, "its name shadows"): - qc.add_var(right, right_init) - - qc = QuantumCircuit(captures=[left]) - with self.assertRaisesRegex(CircuitError, "its name shadows"): - qc.add_capture(right) - qc = QuantumCircuit(captures=[left]) - with self.assertRaisesRegex(CircuitError, "its name shadows"): - qc.add_var(right, right_init) - - qc = QuantumCircuit(inputs=[left]) - with self.assertRaisesRegex(CircuitError, "its name shadows"): - qc.add_var(right, right_init) - - qc = QuantumCircuit() - qc.add_var("a", expr.lift(True)) - with self.assertRaisesRegex(CircuitError, "its name shadows"): - qc.add_var("a", expr.lift(True)) - with self.assertRaisesRegex(CircuitError, "its name shadows"): - qc.add_var("a", expr.lift(0xFF)) - - def test_cannot_add_vars_wrapping_clbits(self): - a = expr.Var(Clbit(), types.Bool()) - with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): - QuantumCircuit(inputs=[a]) - qc = QuantumCircuit() - with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): - qc.add_input(a) - with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): - QuantumCircuit(captures=[a]) - qc = QuantumCircuit() - with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): - qc.add_capture(a) - with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): - QuantumCircuit(declarations=[(a, expr.lift(True))]) - qc = QuantumCircuit() - with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): - qc.add_var(a, expr.lift(True)) - - def test_cannot_add_vars_wrapping_cregs(self): - a = expr.Var(ClassicalRegister(8, "cr"), types.Uint(8)) - with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): - QuantumCircuit(inputs=[a]) - qc = QuantumCircuit() - with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): - qc.add_input(a) - with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): - QuantumCircuit(captures=[a]) - qc = QuantumCircuit() - with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): - qc.add_capture(a) - with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): - QuantumCircuit(declarations=[(a, expr.lift(0xFF))]) - qc = QuantumCircuit() - with self.assertRaisesRegex(CircuitError, "cannot add variables that wrap"): - qc.add_var(a, expr.lift(0xFF)) - - def test_get_var_success(self): - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Uint(8)) - - qc = QuantumCircuit(inputs=[a], declarations={b: expr.Value(0xFF, types.Uint(8))}) - self.assertIs(qc.get_var("a"), a) - self.assertIs(qc.get_var("b"), b) - - qc = QuantumCircuit(captures=[a, b]) - self.assertIs(qc.get_var("a"), a) - self.assertIs(qc.get_var("b"), b) - - qc = QuantumCircuit(declarations={a: expr.lift(True), b: expr.Value(0xFF, types.Uint(8))}) - self.assertIs(qc.get_var("a"), a) - self.assertIs(qc.get_var("b"), b) - - def test_get_var_missing(self): - qc = QuantumCircuit() - with self.assertRaises(KeyError): - qc.get_var("a") - - a = expr.Var.new("a", types.Bool()) - qc.add_input(a) - with self.assertRaises(KeyError): - qc.get_var("b") - - def test_get_var_default(self): - qc = QuantumCircuit() - self.assertIs(qc.get_var("a", None), None) - - missing = "default" - a = expr.Var.new("a", types.Bool()) - qc.add_input(a) - self.assertIs(qc.get_var("b", missing), missing) - self.assertIs(qc.get_var("b", a), a) - - def test_has_var(self): - a = expr.Var.new("a", types.Bool()) - self.assertFalse(QuantumCircuit().has_var("a")) - self.assertTrue(QuantumCircuit(inputs=[a]).has_var("a")) - self.assertTrue(QuantumCircuit(captures=[a]).has_var("a")) - self.assertTrue(QuantumCircuit(declarations={a: expr.lift(True)}).has_var("a")) - self.assertTrue(QuantumCircuit(inputs=[a]).has_var(a)) - self.assertTrue(QuantumCircuit(captures=[a]).has_var(a)) - self.assertTrue(QuantumCircuit(declarations={a: expr.lift(True)}).has_var(a)) - - # When giving an `Var`, the match must be exact, not just the name. - self.assertFalse(QuantumCircuit(inputs=[a]).has_var(expr.Var.new("a", types.Uint(8)))) - self.assertFalse(QuantumCircuit(inputs=[a]).has_var(expr.Var.new("a", types.Bool()))) diff --git a/test/python/circuit/test_control_flow.py b/test/python/circuit/test_control_flow.py index ecb5de96f793..e827b679a4a2 100644 --- a/test/python/circuit/test_control_flow.py +++ b/test/python/circuit/test_control_flow.py @@ -510,49 +510,6 @@ def test_switch_rejects_cases_after_default(self): with self.assertRaisesRegex(CircuitError, "cases after the default are unreachable"): SwitchCaseOp(creg, [(CASE_DEFAULT, case1), (1, case2)]) - def test_if_else_rejects_input_vars(self): - """Bodies must not contain input variables.""" - cond = (Clbit(), False) - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Bool()) - bad_body = QuantumCircuit(inputs=[a]) - good_body = QuantumCircuit(captures=[a], declarations=[(b, expr.lift(False))]) - - with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): - IfElseOp(cond, bad_body, None) - with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): - IfElseOp(cond, bad_body, good_body) - with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): - IfElseOp(cond, good_body, bad_body) - - def test_while_rejects_input_vars(self): - """Bodies must not contain input variables.""" - cond = (Clbit(), False) - a = expr.Var.new("a", types.Bool()) - bad_body = QuantumCircuit(inputs=[a]) - with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): - WhileLoopOp(cond, bad_body) - - def test_for_rejects_input_vars(self): - """Bodies must not contain input variables.""" - a = expr.Var.new("a", types.Bool()) - bad_body = QuantumCircuit(inputs=[a]) - with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): - ForLoopOp(range(3), None, bad_body) - - def test_switch_rejects_input_vars(self): - """Bodies must not contain input variables.""" - target = ClassicalRegister(3, "cr") - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Bool()) - bad_body = QuantumCircuit(inputs=[a]) - good_body = QuantumCircuit(captures=[a], declarations=[(b, expr.lift(False))]) - - with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): - SwitchCaseOp(target, [(0, bad_body)]) - with self.assertRaisesRegex(CircuitError, "cannot contain input variables"): - SwitchCaseOp(target, [(0, good_body), (1, bad_body)]) - @ddt class TestAddingControlFlowOperations(QiskitTestCase): @@ -917,148 +874,3 @@ def test_nested_parameters_can_be_assigned(self): ) self.assertEqual(assigned, expected) - - def test_can_add_op_with_captures_of_inputs(self): - """Test circuit methods can capture input variables.""" - outer = QuantumCircuit(1, 1) - a = outer.add_input("a", types.Bool()) - - inner = QuantumCircuit(1, 1, captures=[a]) - - outer.if_test((outer.clbits[0], False), inner.copy(), [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "if_else") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - - outer.if_else((outer.clbits[0], False), inner.copy(), inner.copy(), [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "if_else") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) - - outer.while_loop((outer.clbits[0], False), inner.copy(), [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "while_loop") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - - outer.for_loop(range(3), None, inner.copy(), [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "for_loop") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - - outer.switch(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())], [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "switch_case") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) - - def test_can_add_op_with_captures_of_captures(self): - """Test circuit methods can capture captured variables.""" - outer = QuantumCircuit(1, 1) - a = expr.Var.new("a", types.Bool()) - outer.add_capture(a) - - inner = QuantumCircuit(1, 1, captures=[a]) - - outer.if_test((outer.clbits[0], False), inner.copy(), [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "if_else") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - - outer.if_else((outer.clbits[0], False), inner.copy(), inner.copy(), [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "if_else") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) - - outer.while_loop((outer.clbits[0], False), inner.copy(), [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "while_loop") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - - outer.for_loop(range(3), None, inner.copy(), [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "for_loop") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - - outer.switch(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())], [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "switch_case") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) - - def test_can_add_op_with_captures_of_locals(self): - """Test circuit methods can capture declared variables.""" - outer = QuantumCircuit(1, 1) - a = outer.add_var("a", expr.lift(True)) - - inner = QuantumCircuit(1, 1, captures=[a]) - - outer.if_test((outer.clbits[0], False), inner.copy(), [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "if_else") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - - outer.if_else((outer.clbits[0], False), inner.copy(), inner.copy(), [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "if_else") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) - - outer.while_loop((outer.clbits[0], False), inner.copy(), [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "while_loop") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - - outer.for_loop(range(3), None, inner.copy(), [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "for_loop") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - - outer.switch(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())], [0], [0]) - added = outer.data[-1].operation - self.assertEqual(added.name, "switch_case") - self.assertEqual(set(added.blocks[0].iter_captured_vars()), {a}) - self.assertEqual(set(added.blocks[1].iter_captured_vars()), {a}) - - def test_cannot_capture_unknown_variables_methods(self): - """Control-flow operations should not be able to capture variables that don't exist in the - outer circuit.""" - outer = QuantumCircuit(1, 1) - - a = expr.Var.new("a", types.Bool()) - inner = QuantumCircuit(1, 1, captures=[a]) - - with self.assertRaisesRegex(CircuitError, "not in this circuit"): - outer.if_test((outer.clbits[0], False), inner.copy(), [0], [0]) - with self.assertRaisesRegex(CircuitError, "not in this circuit"): - outer.if_else((outer.clbits[0], False), inner.copy(), inner.copy(), [0], [0]) - with self.assertRaisesRegex(CircuitError, "not in this circuit"): - outer.while_loop((outer.clbits[0], False), inner.copy(), [0], [0]) - with self.assertRaisesRegex(CircuitError, "not in this circuit"): - outer.for_loop(range(3), None, inner.copy(), [0], [0]) - with self.assertRaisesRegex(CircuitError, "not in this circuit"): - outer.switch(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())], [0], [0]) - - def test_cannot_capture_unknown_variables_append(self): - """Control-flow operations should not be able to capture variables that don't exist in the - outer circuit.""" - outer = QuantumCircuit(1, 1) - - a = expr.Var.new("a", types.Bool()) - inner = QuantumCircuit(1, 1, captures=[a]) - - with self.assertRaisesRegex(CircuitError, "not in this circuit"): - outer.append(IfElseOp((outer.clbits[0], False), inner.copy(), None), [0], [0]) - with self.assertRaisesRegex(CircuitError, "not in this circuit"): - outer.append(IfElseOp((outer.clbits[0], False), inner.copy(), inner.copy()), [0], [0]) - with self.assertRaisesRegex(CircuitError, "not in this circuit"): - outer.append(WhileLoopOp((outer.clbits[0], False), inner.copy()), [0], [0]) - with self.assertRaisesRegex(CircuitError, "not in this circuit"): - outer.append(ForLoopOp(range(3), None, inner.copy()), [0], [0]) - with self.assertRaisesRegex(CircuitError, "not in this circuit"): - outer.append( - SwitchCaseOp(outer.clbits[0], [(False, inner.copy()), (True, inner.copy())]), - [0], - [0], - ) diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index ce8088fcd26c..aa4974f4cdec 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -10,8 +10,6 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -# pylint: disable=missing-function-docstring - """Test operations on the builder interfaces for control flow in dynamic QuantumCircuits.""" import copy @@ -27,10 +25,10 @@ QuantumCircuit, QuantumRegister, Qubit, - Store, ) from qiskit.circuit.classical import expr, types from qiskit.circuit.controlflow import ForLoopOp, IfElseOp, WhileLoopOp, SwitchCaseOp, CASE_DEFAULT +from qiskit.circuit.controlflow.builder import ControlFlowBuilderBlock from qiskit.circuit.controlflow.if_else import IfElsePlaceholder from qiskit.circuit.exceptions import CircuitError from qiskit.test import QiskitTestCase @@ -3000,251 +2998,6 @@ def test_global_phase_of_blocks(self): [i * math.pi / 7 for i in range(1, 7)], ) - def test_can_capture_input(self): - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Bool()) - base = QuantumCircuit(inputs=[a, b]) - with base.for_loop(range(3)): - base.store(a, expr.lift(True)) - self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_vars()), {a}) - - def test_can_capture_declared(self): - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Bool()) - base = QuantumCircuit(declarations=[(a, expr.lift(False)), (b, expr.lift(True))]) - with base.if_test(expr.lift(False)): - base.store(a, expr.lift(True)) - self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_vars()), {a}) - - def test_can_capture_capture(self): - # It's a bit wild to be manually building an outer circuit that's intended to be a subblock, - # but be using the control-flow builder interface internally, but eh, it should work. - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Bool()) - base = QuantumCircuit(captures=[a, b]) - with base.while_loop(expr.lift(False)): - base.store(a, expr.lift(True)) - self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_vars()), {a}) - - def test_can_capture_from_nested(self): - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Bool()) - c = expr.Var.new("c", types.Bool()) - base = QuantumCircuit(inputs=[a, b]) - with base.switch(expr.lift(False)) as case, case(case.DEFAULT): - base.add_var(c, expr.lift(False)) - with base.if_test(expr.lift(False)): - base.store(a, c) - outer_block = base.data[-1].operation.blocks[0] - inner_block = outer_block.data[-1].operation.blocks[0] - self.assertEqual(set(inner_block.iter_captured_vars()), {a, c}) - - # The containing block should have captured it as well, despite not using it explicitly. - self.assertEqual(set(outer_block.iter_captured_vars()), {a}) - self.assertEqual(set(outer_block.iter_declared_vars()), {c}) - - def test_can_manually_capture(self): - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Bool()) - base = QuantumCircuit(inputs=[a, b]) - with base.while_loop(expr.lift(False)): - # Why do this? Who knows, but it clearly has a well-defined meaning. - base.add_capture(a) - self.assertEqual(set(base.data[-1].operation.blocks[0].iter_captured_vars()), {a}) - - def test_later_blocks_do_not_inherit_captures(self): - """Neither 'if' nor 'switch' should have later blocks inherit the captures from the earlier - blocks, and the earlier blocks shouldn't be affected by later ones.""" - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Bool()) - c = expr.Var.new("c", types.Bool()) - - base = QuantumCircuit(inputs=[a, b, c]) - with base.if_test(expr.lift(False)) as else_: - base.store(a, expr.lift(False)) - with else_: - base.store(b, expr.lift(False)) - blocks = base.data[-1].operation.blocks - self.assertEqual(set(blocks[0].iter_captured_vars()), {a}) - self.assertEqual(set(blocks[1].iter_captured_vars()), {b}) - - base = QuantumCircuit(inputs=[a, b, c]) - with base.switch(expr.lift(False)) as case: - with case(0): - base.store(a, expr.lift(False)) - with case(case.DEFAULT): - base.store(b, expr.lift(False)) - blocks = base.data[-1].operation.blocks - self.assertEqual(set(blocks[0].iter_captured_vars()), {a}) - self.assertEqual(set(blocks[1].iter_captured_vars()), {b}) - - def test_blocks_have_independent_declarations(self): - """The blocks of if and switch should be separate scopes for declarations.""" - b1 = expr.Var.new("b", types.Bool()) - b2 = expr.Var.new("b", types.Bool()) - self.assertNotEqual(b1, b2) - - base = QuantumCircuit() - with base.if_test(expr.lift(False)) as else_: - base.add_var(b1, expr.lift(False)) - with else_: - base.add_var(b2, expr.lift(False)) - blocks = base.data[-1].operation.blocks - self.assertEqual(set(blocks[0].iter_declared_vars()), {b1}) - self.assertEqual(set(blocks[1].iter_declared_vars()), {b2}) - - base = QuantumCircuit() - with base.switch(expr.lift(False)) as case: - with case(0): - base.add_var(b1, expr.lift(False)) - with case(case.DEFAULT): - base.add_var(b2, expr.lift(False)) - blocks = base.data[-1].operation.blocks - self.assertEqual(set(blocks[0].iter_declared_vars()), {b1}) - self.assertEqual(set(blocks[1].iter_declared_vars()), {b2}) - - def test_can_shadow_outer_name(self): - outer = expr.Var.new("a", types.Bool()) - inner = expr.Var.new("a", types.Bool()) - base = QuantumCircuit(inputs=[outer]) - with base.if_test(expr.lift(False)): - base.add_var(inner, expr.lift(True)) - block = base.data[-1].operation.blocks[0] - self.assertEqual(set(block.iter_declared_vars()), {inner}) - self.assertEqual(set(block.iter_captured_vars()), set()) - - def test_iterators_run_over_scope(self): - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Bool()) - c = expr.Var.new("c", types.Bool()) - d = expr.Var.new("d", types.Bool()) - - base = QuantumCircuit(inputs=[a, b, c]) - self.assertEqual(set(base.iter_input_vars()), {a, b, c}) - self.assertEqual(set(base.iter_declared_vars()), set()) - self.assertEqual(set(base.iter_captured_vars()), set()) - - with base.switch(expr.lift(3)) as case: - with case(0): - # Nothing here. - self.assertEqual(set(base.iter_vars()), set()) - self.assertEqual(set(base.iter_input_vars()), set()) - self.assertEqual(set(base.iter_declared_vars()), set()) - self.assertEqual(set(base.iter_captured_vars()), set()) - - # Capture a variable. - base.store(a, expr.lift(False)) - self.assertEqual(set(base.iter_captured_vars()), {a}) - - # Declare a variable. - base.add_var(d, expr.lift(False)) - self.assertEqual(set(base.iter_declared_vars()), {d}) - self.assertEqual(set(base.iter_vars()), {a, d}) - - with case(1): - # We should have reset. - self.assertEqual(set(base.iter_vars()), set()) - self.assertEqual(set(base.iter_input_vars()), set()) - self.assertEqual(set(base.iter_declared_vars()), set()) - self.assertEqual(set(base.iter_captured_vars()), set()) - - # Capture a variable. - base.store(b, expr.lift(False)) - self.assertEqual(set(base.iter_captured_vars()), {b}) - - # Capture some more in another scope. - with base.while_loop(expr.lift(False)): - self.assertEqual(set(base.iter_vars()), set()) - base.store(c, expr.lift(False)) - self.assertEqual(set(base.iter_captured_vars()), {c}) - - self.assertEqual(set(base.iter_captured_vars()), {b, c}) - self.assertEqual(set(base.iter_vars()), {b, c}) - # And back to the outer scope. - self.assertEqual(set(base.iter_input_vars()), {a, b, c}) - self.assertEqual(set(base.iter_declared_vars()), set()) - self.assertEqual(set(base.iter_captured_vars()), set()) - - def test_get_var_respects_scope(self): - outer = expr.Var.new("a", types.Bool()) - inner = expr.Var.new("a", types.Bool()) - base = QuantumCircuit(inputs=[outer]) - self.assertEqual(base.get_var("a"), outer) - with base.if_test(expr.lift(False)) as else_: - # Before we've done anything, getting the variable should get the outer one. - self.assertEqual(base.get_var("a"), outer) - - # If we shadow it, we should get the shadowed one after. - base.add_var(inner, expr.lift(False)) - self.assertEqual(base.get_var("a"), inner) - with else_: - # In a new scope, we should see the outer one again. - self.assertEqual(base.get_var("a"), outer) - # ... until we shadow it. - base.add_var(inner, expr.lift(False)) - self.assertEqual(base.get_var("a"), inner) - self.assertEqual(base.get_var("a"), outer) - - def test_has_var_respects_scope(self): - outer = expr.Var.new("a", types.Bool()) - inner = expr.Var.new("a", types.Bool()) - base = QuantumCircuit(inputs=[outer]) - self.assertEqual(base.get_var("a"), outer) - with base.if_test(expr.lift(False)) as else_: - self.assertFalse(base.has_var("b")) - - # Before we've done anything, we should see the outer one. - self.assertTrue(base.has_var("a")) - self.assertTrue(base.has_var(outer)) - self.assertFalse(base.has_var(inner)) - - # If we shadow it, we should see the shadowed one after. - base.add_var(inner, expr.lift(False)) - self.assertTrue(base.has_var("a")) - self.assertFalse(base.has_var(outer)) - self.assertTrue(base.has_var(inner)) - with else_: - # In a new scope, we should see the outer one again. - self.assertTrue(base.has_var("a")) - self.assertTrue(base.has_var(outer)) - self.assertFalse(base.has_var(inner)) - - # ... until we shadow it. - base.add_var(inner, expr.lift(False)) - self.assertTrue(base.has_var("a")) - self.assertFalse(base.has_var(outer)) - self.assertTrue(base.has_var(inner)) - - self.assertTrue(base.has_var("a")) - self.assertTrue(base.has_var(outer)) - self.assertFalse(base.has_var(inner)) - - def test_store_to_clbit_captures_bit(self): - base = QuantumCircuit(1, 2) - with base.if_test(expr.lift(False)): - base.store(expr.lift(base.clbits[0]), expr.lift(True)) - - expected = QuantumCircuit(1, 2) - body = QuantumCircuit([expected.clbits[0]]) - body.store(expr.lift(expected.clbits[0]), expr.lift(True)) - expected.if_test(expr.lift(False), body, [], [0]) - - self.assertEqual(base, expected) - - def test_store_to_register_captures_register(self): - cr1 = ClassicalRegister(2, "cr1") - cr2 = ClassicalRegister(2, "cr2") - base = QuantumCircuit(cr1, cr2) - with base.if_test(expr.lift(False)): - base.store(expr.lift(cr1), expr.lift(3)) - - body = QuantumCircuit(cr1) - body.store(expr.lift(cr1), expr.lift(3)) - expected = QuantumCircuit(cr1, cr2) - expected.if_test(expr.lift(False), body, [], cr1[:]) - - self.assertEqual(base, expected) - @ddt.ddt class TestControlFlowBuildersFailurePaths(QiskitTestCase): @@ -3752,6 +3505,23 @@ def test_non_context_manager_calling_states_reject_missing_resources(self, resou ): test.switch(test.clbits[0], [(False, body)], qubits=qubits, clbits=clbits) + @ddt.data(None, [Clbit()], 0) + def test_builder_block_add_bits_reject_bad_bits(self, bit): + """Test that :obj:`.ControlFlowBuilderBlock` raises if something is given that is an + incorrect type. + + This isn't intended to be something users do at all; the builder block is an internal + construct only, but this keeps coverage checking happy.""" + + def dummy_requester(resource): + raise CircuitError + + builder_block = ControlFlowBuilderBlock( + qubits=(), clbits=(), resource_requester=dummy_requester + ) + with self.assertRaisesRegex(TypeError, r"Can only add qubits or classical bits.*"): + builder_block.add_bits([bit]) + def test_compose_front_inplace_invalid_within_builder(self): """Test that `QuantumCircuit.compose` raises a sensible error when called within a control-flow builder block.""" @@ -3776,124 +3546,3 @@ def test_compose_new_invalid_within_builder(self): with outer.if_test((outer.clbits[0], 1)): with self.assertRaisesRegex(CircuitError, r"Cannot emit a new composed circuit.*"): outer.compose(inner, inplace=False) - - def test_cannot_capture_variable_not_in_scope(self): - a = expr.Var.new("a", types.Bool()) - - base = QuantumCircuit(1, 1) - with base.if_test((0, True)) as else_, self.assertRaisesRegex(CircuitError, "not in scope"): - base.store(a, expr.lift(False)) - with else_, self.assertRaisesRegex(CircuitError, "not in scope"): - base.store(a, expr.lift(False)) - - base.add_input(a) - with base.while_loop((0, True)), self.assertRaisesRegex(CircuitError, "not in scope"): - base.store(expr.Var.new("a", types.Bool()), expr.lift(False)) - - with base.for_loop(range(3)): - with base.switch(base.clbits[0]) as case, case(0): - with self.assertRaisesRegex(CircuitError, "not in scope"): - base.store(expr.Var.new("a", types.Bool()), expr.lift(False)) - - def test_cannot_add_existing_variable(self): - a = expr.Var.new("a", types.Bool()) - base = QuantumCircuit() - with base.if_test(expr.lift(False)) as else_: - base.add_var(a, expr.lift(False)) - with self.assertRaisesRegex(CircuitError, "already present"): - base.add_var(a, expr.lift(False)) - with else_: - base.add_var(a, expr.lift(False)) - with self.assertRaisesRegex(CircuitError, "already present"): - base.add_var(a, expr.lift(False)) - - def test_cannot_shadow_in_same_scope(self): - a = expr.Var.new("a", types.Bool()) - base = QuantumCircuit() - with base.switch(expr.lift(3)) as case: - with case(0): - base.add_var(a, expr.lift(False)) - with self.assertRaisesRegex(CircuitError, "its name shadows"): - base.add_var(a.name, expr.lift(False)) - with case(case.DEFAULT): - base.add_var(a, expr.lift(False)) - with self.assertRaisesRegex(CircuitError, "its name shadows"): - base.add_var(a.name, expr.lift(False)) - - def test_cannot_shadow_captured_variable(self): - """It shouldn't be possible to shadow a variable that has already been captured into the - block.""" - outer = expr.Var.new("a", types.Bool()) - inner = expr.Var.new("a", types.Bool()) - - base = QuantumCircuit(inputs=[outer]) - with base.while_loop(expr.lift(True)): - # Capture the outer. - base.store(outer, expr.lift(True)) - # Attempt to shadow it. - with self.assertRaisesRegex(CircuitError, "its name shadows"): - base.add_var(inner, expr.lift(False)) - - def test_cannot_use_outer_variable_after_shadow(self): - """If we've shadowed a variable, the outer one shouldn't be visible to us for use.""" - outer = expr.Var.new("a", types.Bool()) - inner = expr.Var.new("a", types.Bool()) - - base = QuantumCircuit(inputs=[outer]) - with base.for_loop(range(3)): - # Shadow the outer. - base.add_var(inner, expr.lift(False)) - with self.assertRaisesRegex(CircuitError, "cannot use.*shadowed"): - base.store(outer, expr.lift(True)) - - def test_cannot_use_beyond_outer_shadow(self): - outer = expr.Var.new("a", types.Bool()) - inner = expr.Var.new("a", types.Bool()) - base = QuantumCircuit(inputs=[outer]) - with base.while_loop(expr.lift(True)): - # Shadow 'outer' - base.add_var(inner, expr.lift(True)) - with base.switch(expr.lift(3)) as case, case(0): - with self.assertRaisesRegex(CircuitError, "not in scope"): - # Attempt to access the shadowed variable. - base.store(outer, expr.lift(False)) - - def test_exception_during_initialisation_does_not_add_variable(self): - uint_var = expr.Var.new("a", types.Uint(16)) - bool_expr = expr.Value(False, types.Bool()) - with self.assertRaises(CircuitError): - Store(uint_var, bool_expr) - base = QuantumCircuit() - with base.while_loop(expr.lift(False)): - # Should succeed. - b = base.add_var("b", expr.lift(False)) - try: - base.add_var(uint_var, bool_expr) - except CircuitError: - pass - # Should succeed. - c = base.add_var("c", expr.lift(False)) - local_vars = set(base.iter_vars()) - self.assertEqual(local_vars, {b, c}) - - def test_cannot_use_old_var_not_in_circuit(self): - base = QuantumCircuit() - with base.if_test(expr.lift(False)) as else_: - with self.assertRaisesRegex(CircuitError, "not present"): - base.store(expr.lift(Clbit()), expr.lift(False)) - with else_: - with self.assertRaisesRegex(CircuitError, "not present"): - with base.if_test(expr.equal(ClassicalRegister(2, "c"), 3)): - pass - - def test_cannot_add_input_in_scope(self): - base = QuantumCircuit() - with base.for_loop(range(3)): - with self.assertRaisesRegex(CircuitError, "cannot add an input variable"): - base.add_input("a", types.Bool()) - - def test_cannot_add_uninitialized_in_scope(self): - base = QuantumCircuit() - with base.for_loop(range(3)): - with self.assertRaisesRegex(CircuitError, "cannot add an uninitialized variable"): - base.add_uninitialized_var(expr.Var.new("a", types.Bool())) diff --git a/test/python/circuit/test_store.py b/test/python/circuit/test_store.py deleted file mode 100644 index 7977765d8e45..000000000000 --- a/test/python/circuit/test_store.py +++ /dev/null @@ -1,199 +0,0 @@ -# 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.test import QiskitTestCase -from qiskit.circuit import Store, Clbit, CircuitError, QuantumCircuit, ClassicalRegister -from qiskit.circuit.classical import expr, types - - -class TestStoreInstruction(QiskitTestCase): - """Tests of the properties of the ``Store`` instruction itself.""" - - def test_happy_path_construction(self): - lvalue = expr.Var.new("a", types.Bool()) - rvalue = expr.lift(Clbit()) - constructed = Store(lvalue, rvalue) - self.assertIsInstance(constructed, Store) - self.assertEqual(constructed.lvalue, lvalue) - self.assertEqual(constructed.rvalue, rvalue) - - def test_implicit_cast(self): - lvalue = expr.Var.new("a", types.Bool()) - rvalue = expr.Var.new("b", types.Uint(8)) - constructed = Store(lvalue, rvalue) - self.assertIsInstance(constructed, Store) - self.assertEqual(constructed.lvalue, lvalue) - self.assertEqual(constructed.rvalue, expr.Cast(rvalue, types.Bool(), implicit=True)) - - def test_rejects_non_lvalue(self): - not_an_lvalue = expr.logic_and( - expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Bool()) - ) - rvalue = expr.lift(False) - with self.assertRaisesRegex(CircuitError, "not an l-value"): - Store(not_an_lvalue, rvalue) - - def test_rejects_explicit_cast(self): - lvalue = expr.Var.new("a", types.Uint(16)) - rvalue = expr.Var.new("b", types.Uint(8)) - with self.assertRaisesRegex(CircuitError, "an explicit cast is required"): - Store(lvalue, rvalue) - - def test_rejects_dangerous_cast(self): - lvalue = expr.Var.new("a", types.Uint(8)) - rvalue = expr.Var.new("b", types.Uint(16)) - with self.assertRaisesRegex(CircuitError, "an explicit cast is required.*may be lossy"): - Store(lvalue, rvalue) - - def test_rejects_c_if(self): - instruction = Store(expr.Var.new("a", types.Bool()), expr.Var.new("b", types.Bool())) - with self.assertRaises(NotImplementedError): - instruction.c_if(Clbit(), False) - - -class TestStoreCircuit(QiskitTestCase): - """Tests of the `QuantumCircuit.store` method and appends of `Store`.""" - - def test_produces_expected_operation(self): - a = expr.Var.new("a", types.Bool()) - value = expr.Value(True, types.Bool()) - - qc = QuantumCircuit(inputs=[a]) - qc.store(a, value) - self.assertEqual(qc.data[-1].operation, Store(a, value)) - - qc = QuantumCircuit(captures=[a]) - qc.store(a, value) - self.assertEqual(qc.data[-1].operation, Store(a, value)) - - qc = QuantumCircuit(declarations=[(a, expr.lift(False))]) - qc.store(a, value) - self.assertEqual(qc.data[-1].operation, Store(a, value)) - - def test_allows_stores_with_clbits(self): - clbits = [Clbit(), Clbit()] - a = expr.Var.new("a", types.Bool()) - qc = QuantumCircuit(clbits, inputs=[a]) - qc.store(clbits[0], True) - qc.store(expr.Var(clbits[1], types.Bool()), a) - qc.store(clbits[0], clbits[1]) - qc.store(expr.lift(clbits[0]), expr.lift(clbits[1])) - qc.store(a, expr.lift(clbits[1])) - - expected = [ - Store(expr.lift(clbits[0]), expr.lift(True)), - Store(expr.lift(clbits[1]), a), - Store(expr.lift(clbits[0]), expr.lift(clbits[1])), - Store(expr.lift(clbits[0]), expr.lift(clbits[1])), - Store(a, expr.lift(clbits[1])), - ] - actual = [instruction.operation for instruction in qc.data] - self.assertEqual(actual, expected) - - def test_allows_stores_with_cregs(self): - cregs = [ClassicalRegister(8, "cr1"), ClassicalRegister(8, "cr2")] - a = expr.Var.new("a", types.Uint(8)) - qc = QuantumCircuit(*cregs, captures=[a]) - qc.store(cregs[0], 0xFF) - qc.store(expr.Var(cregs[1], types.Uint(8)), a) - qc.store(cregs[0], cregs[1]) - qc.store(expr.lift(cregs[0]), expr.lift(cregs[1])) - qc.store(a, cregs[1]) - - expected = [ - Store(expr.lift(cregs[0]), expr.lift(0xFF)), - Store(expr.lift(cregs[1]), a), - Store(expr.lift(cregs[0]), expr.lift(cregs[1])), - Store(expr.lift(cregs[0]), expr.lift(cregs[1])), - Store(a, expr.lift(cregs[1])), - ] - actual = [instruction.operation for instruction in qc.data] - self.assertEqual(actual, expected) - - def test_lifts_values(self): - a = expr.Var.new("a", types.Bool()) - qc = QuantumCircuit(captures=[a]) - qc.store(a, True) - self.assertEqual(qc.data[-1].operation, Store(a, expr.lift(True))) - - b = expr.Var.new("b", types.Uint(16)) - qc.add_capture(b) - qc.store(b, 0xFFFF) - self.assertEqual(qc.data[-1].operation, Store(b, expr.lift(0xFFFF))) - - def test_rejects_vars_not_in_circuit(self): - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Bool()) - - qc = QuantumCircuit() - with self.assertRaisesRegex(CircuitError, "'a'.*not present"): - qc.store(expr.Var.new("a", types.Bool()), True) - - # Not the same 'a' - qc.add_input(a) - with self.assertRaisesRegex(CircuitError, "'a'.*not present"): - qc.store(expr.Var.new("a", types.Bool()), True) - with self.assertRaisesRegex(CircuitError, "'b'.*not present"): - qc.store(a, b) - - def test_rejects_bits_not_in_circuit(self): - a = expr.Var.new("a", types.Bool()) - clbit = Clbit() - qc = QuantumCircuit(captures=[a]) - with self.assertRaisesRegex(CircuitError, "not present"): - qc.store(clbit, False) - with self.assertRaisesRegex(CircuitError, "not present"): - qc.store(clbit, a) - with self.assertRaisesRegex(CircuitError, "not present"): - qc.store(a, clbit) - - def test_rejects_cregs_not_in_circuit(self): - a = expr.Var.new("a", types.Uint(8)) - creg = ClassicalRegister(8, "cr1") - qc = QuantumCircuit(captures=[a]) - with self.assertRaisesRegex(CircuitError, "not present"): - qc.store(creg, 0xFF) - with self.assertRaisesRegex(CircuitError, "not present"): - qc.store(creg, a) - with self.assertRaisesRegex(CircuitError, "not present"): - qc.store(a, creg) - - def test_rejects_non_lvalue(self): - a = expr.Var.new("a", types.Bool()) - b = expr.Var.new("b", types.Bool()) - qc = QuantumCircuit(inputs=[a, b]) - not_an_lvalue = expr.logic_and(a, b) - with self.assertRaisesRegex(CircuitError, "not an l-value"): - qc.store(not_an_lvalue, expr.lift(False)) - - def test_rejects_explicit_cast(self): - lvalue = expr.Var.new("a", types.Uint(16)) - rvalue = expr.Var.new("b", types.Uint(8)) - qc = QuantumCircuit(inputs=[lvalue, rvalue]) - with self.assertRaisesRegex(CircuitError, "an explicit cast is required"): - qc.store(lvalue, rvalue) - - def test_rejects_dangerous_cast(self): - lvalue = expr.Var.new("a", types.Uint(8)) - rvalue = expr.Var.new("b", types.Uint(16)) - qc = QuantumCircuit(inputs=[lvalue, rvalue]) - with self.assertRaisesRegex(CircuitError, "an explicit cast is required.*may be lossy"): - qc.store(lvalue, rvalue) - - def test_rejects_c_if(self): - a = expr.Var.new("a", types.Bool()) - qc = QuantumCircuit([Clbit()], inputs=[a]) - instruction_set = qc.store(a, True) - with self.assertRaises(NotImplementedError): - instruction_set.c_if(qc.clbits[0], False)