From cc2ee93602faffa6a3352e2a214c15271fc0d3b7 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 3 Oct 2023 23:08:24 +0100 Subject: [PATCH] Add variable-handling methods to `QuantumCircuit` This adds all the new `QuantumCircuit` methods discussed in the variable-declaration RFC[^1], and threads the support for them through the methods that are called in turn, such as `QuantumCircuit.append`. It does yet not add support to methods such as `copy` or `compose`, which will be done in a follow-up. The APIs discussed in the RFC necessitated making `Var` nodes hashable. This is done in this commit, as it is logically connected. These nodes now have enforced immutability, which is technically a minor breaking change, but in practice required for the properties of such expressions to be tracked correctly through circuits. A helper attribute `Var.standalone` is added to unify the handling of whether a variable is an old-style existing-memory wrapper, or a new "proper" variable with its own memory. [^1]: https://github.com/Qiskit/RFCs/pull/50 --- qiskit/circuit/classical/expr/expr.py | 62 ++- qiskit/circuit/classical/types/types.py | 6 + qiskit/circuit/quantumcircuit.py | 338 +++++++++++++++- ...r-hashable-var-types-7cf2aaa00b201ae6.yaml | 5 + .../expr-var-standalone-2c1116583a2be9fd.yaml | 6 + .../circuit/classical/test_expr_properties.py | 36 +- test/python/circuit/test_circuit_vars.py | 366 ++++++++++++++++++ test/python/circuit/test_store.py | 139 ++++++- 8 files changed, 939 insertions(+), 19 deletions(-) create mode 100644 releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml create mode 100644 releasenotes/notes/expr-var-standalone-2c1116583a2be9fd.yaml create mode 100644 test/python/circuit/test_circuit_vars.py diff --git a/qiskit/circuit/classical/expr/expr.py b/qiskit/circuit/classical/expr/expr.py index 3adbacfd6926..d15a64988b83 100644 --- a/qiskit/circuit/classical/expr/expr.py +++ b/qiskit/circuit/classical/expr/expr.py @@ -115,10 +115,24 @@ class Var(Expr): 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`.""" + :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.""" + def __init__( self, var: qiskit.circuit.Clbit | qiskit.circuit.ClassicalRegister | uuid.UUID, @@ -126,27 +140,32 @@ def __init__( *, name: str | None = None, ): - self.type = type - self.var = var - """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.""" - self.name = name - """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.""" + 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 some pre-existing circuit object.""" + return isinstance(self.var, uuid.UUID) + 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) @@ -160,6 +179,23 @@ def __repr__(self): 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 + @typing.final class Value(Expr): diff --git a/qiskit/circuit/classical/types/types.py b/qiskit/circuit/classical/types/types.py index 711f82db5fc0..04266aefd410 100644 --- a/qiskit/circuit/classical/types/types.py +++ b/qiskit/circuit/classical/types/types.py @@ -89,6 +89,9 @@ 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) @@ -107,5 +110,8 @@ 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/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 971ec59016bb..5785e7cf2f40 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -16,10 +16,12 @@ from __future__ import annotations import copy +import itertools import multiprocessing as mp import warnings import typing import math +import types as _builtin_types from collections import OrderedDict, defaultdict, namedtuple from typing import ( Union, @@ -47,7 +49,7 @@ from qiskit.utils.deprecation import deprecate_func from . import _classical_resource_map from ._utils import sort_parameters -from .classical import expr +from .classical import expr, types from .parameterexpression import ParameterExpression, ParameterValueType from .quantumregister import QuantumRegister, Qubit, AncillaRegister, AncillaQubit from .classicalregister import ClassicalRegister, Clbit @@ -61,6 +63,7 @@ from .delay import Delay from .measure import Measure from .reset import Reset +from .store import Store if typing.TYPE_CHECKING: import qiskit # pylint: disable=cyclic-import @@ -138,6 +141,23 @@ 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. @@ -200,6 +220,9 @@ 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 @@ -270,6 +293,20 @@ 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 @@ -1088,6 +1125,33 @@ def ancillas(self) -> list[AncillaQubit]: """ return self._ancillas + 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`.""" + 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`).""" + 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`).""" + 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`).""" + return self._vars_capture.values() + def __and__(self, rhs: "QuantumCircuit") -> "QuantumCircuit": """Overload & to implement self.compose.""" return self.compose(rhs) @@ -1202,12 +1266,17 @@ def _resolve_classical_resource(self, specifier): def _validate_expr(self, node: expr.Expr) -> expr.Expr: for var in expr.iter_vars(node): - if isinstance(var.var, Clbit): + if var.standalone: + if not self.has_var(var): + raise CircuitError(f"Variable '{var}' is not present in this circuit.") + elif 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.") + else: + raise RuntimeError(f"unhandled Var inner type in '{var}'") return node def append( @@ -1265,8 +1334,12 @@ def append( ) # Make copy of parameterized gate instances - if hasattr(operation, "params"): - is_parameter = any(isinstance(param, Parameter) for param in operation.params) + 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): + self._validate_expr(param) if is_parameter: operation = copy.deepcopy(operation) @@ -1382,6 +1455,242 @@ def _update_parameter_table(self, instruction: CircuitInstruction): # clear cache if new parameter is added self._parameters = None + @typing.overload + def get_var(self, name: str, default: T) -> Union[expr.Var, T]: + ... + + @typing.overload + def get_var(self, name: str, default: _builtin_types.EllipsisType = ...) -> 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 + """ + + if (out := self._vars_local.get(name)) is not None: + return out + if (out := self._vars_capture.get(name)) is not None: + return out + if (out := self._vars_input.get(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 defined 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 present. + + See also: + :meth:`QuantumCircuit.get_var`: retrieve a named variable from a circuit. + """ + 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): + 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 taking 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. + initial = self._validate_expr(expr.lift(initial)) + var = self._prepare_new_var(name_or_var, initial.type) + # Store is responsible for ensuring the type safety of the initialisation. We build this + # before actually modifying any of our own state, so we don't get into an inconsistent state + # if an exception is raised later. + store = Store(var, initial) + + self._vars_local[var.name] = var + self._append(CircuitInstruction(store, (), ())) + return 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. 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._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._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: @@ -2176,6 +2485,27 @@ 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. + 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. + """ + 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``). diff --git a/releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml b/releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml new file mode 100644 index 000000000000..70a1cf81d061 --- /dev/null +++ b/releasenotes/notes/expr-hashable-var-types-7cf2aaa00b201ae6.yaml @@ -0,0 +1,5 @@ +--- +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 new file mode 100644 index 000000000000..71ec0320032e --- /dev/null +++ b/releasenotes/notes/expr-var-standalone-2c1116583a2be9fd.yaml @@ -0,0 +1,6 @@ +--- +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_properties.py b/test/python/circuit/classical/test_expr_properties.py index efda6ba37758..f8c1277cd0f4 100644 --- a/test/python/circuit/classical/test_expr_properties.py +++ b/test/python/circuit/classical/test_expr_properties.py @@ -14,11 +14,12 @@ import copy import pickle +import uuid import ddt from qiskit.test import QiskitTestCase -from qiskit.circuit import ClassicalRegister +from qiskit.circuit import ClassicalRegister, Clbit from qiskit.circuit.classical import expr, types @@ -98,3 +99,36 @@ def test_var_uuid_clone(self): 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/test_circuit_vars.py b/test/python/circuit/test_circuit_vars.py new file mode 100644 index 000000000000..fa7138df9be3 --- /dev/null +++ b/test/python/circuit/test_circuit_vars.py @@ -0,0 +1,366 @@ +# 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())) + + 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())) + + 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())) + 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())) + 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())) + 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_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_store.py b/test/python/circuit/test_store.py index 6d5c4707cbed..7977765d8e45 100644 --- a/test/python/circuit/test_store.py +++ b/test/python/circuit/test_store.py @@ -13,7 +13,7 @@ # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring from qiskit.test import QiskitTestCase -from qiskit.circuit import Store, Clbit, CircuitError +from qiskit.circuit import Store, Clbit, CircuitError, QuantumCircuit, ClassicalRegister from qiskit.circuit.classical import expr, types @@ -60,3 +60,140 @@ 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)