diff --git a/qiskit/circuit/controlflow/__init__.py b/qiskit/circuit/controlflow/__init__.py index f76c4da72aa5..89483aacf21b 100644 --- a/qiskit/circuit/controlflow/__init__.py +++ b/qiskit/circuit/controlflow/__init__.py @@ -14,10 +14,9 @@ from .control_flow import ControlFlowOp -from .if_else import IfElseOp +from .continue_loop import ContinueLoopOp +from .break_loop import BreakLoopOp +from .if_else import IfElseOp from .while_loop import WhileLoopOp from .for_loop import ForLoopOp - -from .continue_loop import ContinueLoopOp -from .break_loop import BreakLoopOp diff --git a/qiskit/circuit/controlflow/break_loop.py b/qiskit/circuit/controlflow/break_loop.py index 6682d983e9e7..c4d7d2860d20 100644 --- a/qiskit/circuit/controlflow/break_loop.py +++ b/qiskit/circuit/controlflow/break_loop.py @@ -15,6 +15,7 @@ from typing import Optional from qiskit.circuit.instruction import Instruction +from .builder import InstructionPlaceholder class BreakLoopOp(Instruction): @@ -43,5 +44,23 @@ class BreakLoopOp(Instruction): """ def __init__(self, num_qubits: int, num_clbits: int, label: Optional[str] = None): - super().__init__("break_loop", num_qubits, num_clbits, [], label=label) + + +class BreakLoopPlaceholder(InstructionPlaceholder): + """A placeholder instruction for use in control-flow context managers, when the number of qubits + and clbits is not yet known.""" + + def __init__(self, *, label: Optional[str] = None): + super().__init__("break_loop", 0, 0, [], label=label) + + def concrete_instruction(self, qubits, clbits): + return ( + self._copy_mutable_properties(BreakLoopOp(len(qubits), len(clbits), label=self.label)), + tuple(qubits), + tuple(clbits), + ) + + def placeholder_resources(self): + # Is it just me, or does this look like an owl? + return ((), ()) diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py new file mode 100644 index 000000000000..f6487687bdb0 --- /dev/null +++ b/qiskit/circuit/controlflow/builder.py @@ -0,0 +1,389 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Builder types for the basic control-flow constructs.""" + +# This file is in circuit.controlflow rather than the root of circuit because the constructs here +# are only intended to be localised to constructing the control flow instructions. We anticipate +# having a far more complete builder of all circuits, with more classical control and creation, in +# the future. + + +import abc +import typing +from typing import Callable, Iterable, List, FrozenSet, Tuple, Union + +from qiskit.circuit.classicalregister import Clbit +from qiskit.circuit.exceptions import CircuitError +from qiskit.circuit.instruction import Instruction +from qiskit.circuit.quantumregister import Qubit + +if typing.TYPE_CHECKING: + import qiskit # pylint: disable=cyclic-import + + +class InstructionPlaceholder(Instruction, abc.ABC): + """A fake instruction that lies about its number of qubits and clbits. + + These instances are used to temporarily represent control-flow instructions during the builder + process, when their lengths cannot be known until the end of the block. This is necessary to + allow constructs like:: + + with qc.for_loop(None, range(5)): + qc.h(0) + qc.measure(0, 0) + qc.break_loop().c_if(0, 0) + + since ``qc.break_loop()`` needs to return a (mostly) functional + :obj:`~qiskit.circuit.Instruction` in order for :meth:`.InstructionSet.c_if` to work correctly. + + When appending a placeholder instruction into a circuit scope, you should create the + placeholder, and then ask it what resources it should be considered as using from the start by + calling :meth:`.InstructionPlaceholder.placeholder_instructions`. This set will be a subset of + the final resources it asks for, but it is used for initialising resources that *must* be + supplied, such as the bits used in the conditions of placeholder ``if`` statements. + """ + + _directive = True + + @abc.abstractmethod + def concrete_instruction( + self, qubits: FrozenSet[Qubit], clbits: FrozenSet[Clbit] + ) -> Tuple[Instruction, Tuple[Qubit, ...], Tuple[Clbit, ...]]: + """Get a concrete, complete instruction that is valid to act over all the given resources. + + The returned resources may not be the full width of the given resources, but will certainly + be a subset of them; this can occur if (for example) a placeholder ``if`` statement is + present, but does not itself contain any placeholder instructions. For resource efficiency, + the returned :obj:`.IfElseOp` will not unnecessarily span all resources, but only the ones + that it needs. + + Any condition added in by a call to :obj:`.Instruction.c_if` will be propagated through, but + set properties like ``duration`` will not; it doesn't make sense for control-flow operations + to have pulse scheduling on them. + + Args: + qubits: The qubits the created instruction should be defined across. + clbits: The clbits the created instruction should be defined across. + + Returns: + Instruction: a full version of the relevant control-flow instruction. This is a + "proper" instruction instance, as if it had been defined with the correct number of + qubits and clbits from the beginning. + """ + raise NotImplementedError + + @abc.abstractmethod + def placeholder_resources(self) -> Tuple[Tuple[Qubit, ...], Tuple[Clbit, ...]]: + """Get the qubit and clbit resources that this placeholder instruction should be considered + as using before construction. + + This will likely not include *all* resources after the block has been built, but using the + output of this method ensures that all resources will pass through a + :meth:`.QuantumCircuit.append` call, even if they come from a placeholder, and consequently + will be tracked by the scope managers. + + Returns: + A 2-tuple of the quantum and classical resources this placeholder instruction will + certainly use. + """ + raise NotImplementedError + + def _copy_mutable_properties(self, instruction: Instruction) -> Instruction: + """Copy mutable properties from ourselves onto a non-placeholder instruction. + + The mutable properties are expected to be things like ``condition``, added onto a + placeholder by the :meth:`c_if` method. This mutates ``instruction``, and returns the same + instance that was passed. This is mostly intended to make writing concrete versions of + :meth:`.concrete_instruction` easy. + + The complete list of mutations is: + + * ``condition``, added by :meth:`c_if`. + + Args: + instruction: the concrete instruction instance to be mutated. + + Returns: + The same instruction instance that was passed, but mutated to propagate the tracked + changes to this class. + """ + # In general the tuple creation should be a no-op, because ``tuple(t) is t`` for tuples. + instruction.condition = None if self.condition is None else tuple(self.condition) + return instruction + + # Provide some better error messages, just in case something goes wrong during development and + # the placeholder type leaks out to somewhere visible. + + def assemble(self): + raise CircuitError("Cannot assemble a placeholder instruction.") + + def qasm(self): + raise CircuitError("Cannot convert a placeholder instruction to OpenQASM 2") + + def repeat(self, n): + raise CircuitError("Cannot repeat a placeholder instruction.") + + +class ControlFlowBuilderBlock: + """A lightweight scoped block for holding instructions within a control-flow builder context. + + This class is designed only to be used by :obj:`.QuantumCircuit` as an internal context for + control-flow builder instructions, and in general should never be instantiated by any code other + than that. + + Note that the instructions that are added to this scope may not be valid yet, so this elides + some of the type-checking of :obj:`.QuantumCircuit` until those things are known. + + The general principle of the resource tracking through these builder blocks is that every + necessary resource should pass through an :meth:`.append` call, so that at the point that + :meth:`.build` is called, the scope knows all the concrete resources that it requires. However, + the scope can also contain "placeholder" instructions, which may need extra resources filling in + from outer scopes (such as a ``break`` needing to know the width of its containing ``for`` + loop). This means that :meth:`.build` takes all the *containing* scope's resources as well. + This does not break the "all resources pass through an append" rule, because the containing + scope will only begin to build its instructions once it has received them all. + + In short, :meth:`.append` adds resources, and :meth:`.build` may use only a subset of the extra + ones passed. This ensures that all instructions know about all the resources they need, even in + the case of ``break``, but do not block any resources that they do *not* need. + """ + + __slots__ = ( + "instructions", + "qubits", + "clbits", + "_allow_jumps", + "_resource_requester", + "_built", + ) + + def __init__( + self, + qubits: Iterable[Qubit], + clbits: Iterable[Clbit], + *, + resource_requester: Callable, + allow_jumps: bool = True, + ): + """ + Args: + qubits: Any qubits this scope should consider itself as using from the beginning. + clbits: Any clbits this scope should consider itself as using from the beginning. Along + with ``qubits``, this is useful for things such as ``if`` and ``while`` loop + builders, where the classical condition has associated resources, and is known when + this scope is created. + allow_jumps: Whether this builder scope should allow ``break`` and ``continue`` + statements within it. This is intended to help give sensible error messages when + dangerous behaviour is encountered, such as using ``break`` inside an ``if`` context + manager that is not within a ``for`` manager. This can only be safe if the user is + going to place the resulting :obj:`.QuantumCircuit` inside a :obj:`.ForLoopOp` that + 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. + 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. + """ + self.instructions: List[Tuple[Instruction, Tuple[Qubit, ...], Tuple[Clbit, ...]]] = [] + self.qubits = set(qubits) + self.clbits = set(clbits) + self._allow_jumps = allow_jumps + self._resource_requester = resource_requester + self._built = False + + @property + def allow_jumps(self): + """Whether this builder scope should allow ``break`` and ``continue`` statements within it. + + This is intended to help give sensible error messages when dangerous behaviour is + encountered, such as using ``break`` inside an ``if`` context manager that is not within a + ``for`` manager. This can only be safe if the user is going to place the resulting + :obj:`.QuantumCircuit` inside a :obj:`.ForLoopOp` that 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. + """ + return self._allow_jumps + + def append( + self, + operation: Instruction, + qubits: Iterable[Qubit], + clbits: Iterable[Clbit], + ) -> Instruction: + """Add an instruction into the scope, keeping track of the qubits and clbits that have been + used in total.""" + if not self._allow_jumps: + # pylint: disable=cyclic-import + from .break_loop import BreakLoopOp, BreakLoopPlaceholder + from .continue_loop import ContinueLoopOp, ContinueLoopPlaceholder + + forbidden = (BreakLoopOp, BreakLoopPlaceholder, ContinueLoopOp, ContinueLoopPlaceholder) + if isinstance(operation, forbidden): + raise CircuitError( + f"The current builder scope cannot take a '{operation.name}'" + " because it is not in a loop." + ) + + qubits = tuple(qubits) + clbits = tuple(clbits) + self.instructions.append((operation, qubits, clbits)) + self.qubits.update(qubits) + self.clbits.update(clbits) + return operation + + def request_classical_resource(self, specifier): + """Resolve a single classical resource specifier into a concrete resource, raising an error + if the specifier is invalid, and track it as now being used in scope. + + 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 that measurement operations on this circuit use. + + Returns: + Union[Clbit, ClassicalRegister]: the requested resource, resolved into a concrete + instance of :obj:`.Clbit` or :obj:`.ClassicalRegister`. + + Raises: + CircuitError: if the resource is not present in this circuit, or if the integer index + passed is out-of-bounds. + """ + if self._built: + raise CircuitError("Cannot add resources after the scope has been built.") + # Allow the inner resolve to propagate exceptions. + resource = self._resource_requester(specifier) + self.add_bits((resource,) if isinstance(resource, Clbit) else resource) + return resource + + def peek(self) -> Tuple[Instruction, Tuple[Qubit, ...], Tuple[Clbit, ...]]: + """Get the value of the most recent instruction tuple in this scope.""" + if not self.instructions: + raise CircuitError("This scope contains no instructions.") + return self.instructions[-1] + + def pop(self) -> Tuple[Instruction, Tuple[Qubit, ...], Tuple[Clbit, ...]]: + """Get the value of the most recent instruction tuple in this scope, and remove it from this + object.""" + if not self.instructions: + raise CircuitError("This scope contains no instructions.") + operation, qubits, clbits = self.instructions.pop() + return (operation, qubits, clbits) + + def add_bits(self, bits: Iterable[Union[Qubit, Clbit]]): + """Add extra bits to this scope that are not associated with any concrete instruction yet. + + This is useful for expanding a scope's resource width when it may contain ``break`` or + ``continue`` statements, or when its width needs to be expanded to match another scope's + width (as in the case of :obj:`.IfElseOp`). + + Args: + bits: The qubits and clbits that should be added to a scope. It is not an error if + there are duplicates, either within the iterable or with the bits currently in + scope. + + Raises: + TypeError: if the provided bit is of an incorrect type. + """ + for bit in bits: + if isinstance(bit, Qubit): + self.qubits.add(bit) + elif isinstance(bit, Clbit): + self.clbits.add(bit) + else: + raise TypeError(f"Can only add qubits or classical bits, but received '{bit}'.") + + def build( + self, all_qubits: FrozenSet[Qubit], all_clbits: FrozenSet[Clbit] + ) -> "qiskit.circuit.QuantumCircuit": + """Build this scoped block into a complete :obj:`~QuantumCircuit` instance. + + This will build a circuit which contains all of the necessary qubits and clbits and no + others. + + The ``qubits`` and ``clbits`` arguments should be sets that contains all the resources in + the outer scope; these will be passed down to inner placeholder instructions, so they can + apply themselves across the whole scope should they need to. The resulting + :obj:`.QuantumCircuit` will be defined over a (nonstrict) subset of these resources. This + is used to let ``break`` and ``continue`` span all resources, even if they are nested within + several :obj:`.IfElsePlaceholder` objects, without requiring :obj:`.IfElsePlaceholder` + objects *without* any ``break`` or ``continue`` statements to be full-width. + + Args: + all_qubits: all the qubits in the containing scope of this block. The block may expand + to use some or all of these qubits, but will never gain qubits that are not in this + set. + all_clbits: all the clbits in the containing scope of this block. The block may expand + to use some or all of these clbits, but will never gain clbits that are not in this + set. + + Returns: + A circuit containing concrete versions of all the instructions that were in the scope, + and using the minimal set of resources necessary to support them, within the enclosing + scope. + """ + from qiskit.circuit import QuantumCircuit + + # There's actually no real problem with building a scope more than once. This flag is more + # so _other_ operations, which aren't safe can be forbidden, such as mutating instructions + # that may have been built into other objects. + self._built = True + + potential_qubits = all_qubits - self.qubits + potential_clbits = all_clbits - self.clbits + + # We start off by only giving the QuantumCircuit the qubits we _know_ it will need, and add + # more later as needed. + out = QuantumCircuit(list(self.qubits), list(self.clbits)) + + for operation, qubits, clbits in self.instructions: + if isinstance(operation, InstructionPlaceholder): + operation, qubits, clbits = operation.concrete_instruction(all_qubits, all_clbits) + # We want to avoid iterating over the tuples unnecessarily if there's no chance + # we'll need to add bits to the circuit. + if potential_qubits and qubits: + add_qubits = potential_qubits.intersection(qubits) + if add_qubits: + potential_qubits -= add_qubits + out.add_bits(add_qubits) + if potential_clbits and clbits: + add_clbits = potential_clbits.intersection(clbits) + if add_clbits: + potential_clbits -= add_clbits + out.add_bits(add_clbits) + # We already did the broadcasting and checking when the first call to + # QuantumCircuit.append happened (which the user wrote), and added the instruction into + # this scope. We just need to finish the job now. + out._append(operation, qubits, clbits) + + return out + + def copy(self) -> "ControlFlowBuilderBlock": + """Return a semi-shallow copy of this builder block. + + The instruction lists and sets of qubits and clbits will be new instances (so mutations will + not propagate), but any :obj:`.Instruction` instances within them will not be copied. + + Returns: + a semi-shallow copy of this object. + """ + out = type(self).__new__(type(self)) + out.instructions = self.instructions.copy() + out.qubits = self.qubits.copy() + out.clbits = self.clbits.copy() + out._allow_jumps = self._allow_jumps + return out diff --git a/qiskit/circuit/controlflow/condition.py b/qiskit/circuit/controlflow/condition.py new file mode 100644 index 000000000000..b79de192a19e --- /dev/null +++ b/qiskit/circuit/controlflow/condition.py @@ -0,0 +1,59 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Functions for dealing with classical conditions.""" + +from typing import Tuple, Union + +from qiskit.circuit.classicalregister import ClassicalRegister, Clbit +from qiskit.circuit.exceptions import CircuitError + + +def validate_condition( + condition: Tuple[Union[ClassicalRegister, Clbit], int] +) -> Tuple[Union[ClassicalRegister, Clbit], int]: + """Validate that a condition is in a valid format and return it, but raise if it is invalid. + + Args: + condition: the condition to be tested for validity. + + Raises: + CircuitError: if the condition is not in a valid format. + + Returns: + The same condition as passed, if it was valid. + """ + try: + bits, value = condition + if isinstance(bits, (ClassicalRegister, Clbit)) and isinstance(value, int): + return (bits, value) + except (TypeError, ValueError): + pass + raise CircuitError( + "A classical condition should be a 2-tuple of `(ClassicalRegister | Clbit, int)`," + f" but received '{condition!r}'." + ) + + +def condition_bits(condition: Tuple[Union[ClassicalRegister, Clbit], int]) -> Tuple[Clbit, ...]: + """Return the classical resources used by ``condition`` as a tuple of :obj:`.Clbit`. + + This is useful when the exact set of bits is required, rather than the logical grouping of + :obj:`.ClassicalRegister`, such as when determining circuit blocking. + + Args: + condition: the valid condition to extract the bits from. + + Returns: + a tuple of all classical bits used in the condition. + """ + return (condition[0],) if isinstance(condition[0], Clbit) else tuple(condition[0]) diff --git a/qiskit/circuit/controlflow/continue_loop.py b/qiskit/circuit/controlflow/continue_loop.py index d4ab03e933ba..9e0416902c28 100644 --- a/qiskit/circuit/controlflow/continue_loop.py +++ b/qiskit/circuit/controlflow/continue_loop.py @@ -15,6 +15,7 @@ from typing import Optional from qiskit.circuit.instruction import Instruction +from .builder import InstructionPlaceholder class ContinueLoopOp(Instruction): @@ -43,5 +44,24 @@ class ContinueLoopOp(Instruction): """ def __init__(self, num_qubits: int, num_clbits: int, label: Optional[str] = None): - super().__init__("continue_loop", num_qubits, num_clbits, [], label=label) + + +class ContinueLoopPlaceholder(InstructionPlaceholder): + """A placeholder instruction for use in control-flow context managers, when the number of qubits + and clbits is not yet known.""" + + def __init__(self, *, label: Optional[str] = None): + super().__init__("continue_loop", 0, 0, [], label=label) + + def concrete_instruction(self, qubits, clbits): + return ( + self._copy_mutable_properties( + ContinueLoopOp(len(qubits), len(clbits), label=self.label) + ), + tuple(qubits), + tuple(clbits), + ) + + def placeholder_resources(self): + return ((), ()) diff --git a/qiskit/circuit/controlflow/for_loop.py b/qiskit/circuit/controlflow/for_loop.py index b387693512d2..f728f0d04d36 100644 --- a/qiskit/circuit/controlflow/for_loop.py +++ b/qiskit/circuit/controlflow/for_loop.py @@ -27,9 +27,9 @@ class ForLoopOp(ControlFlowOp): the set of integer values provided in ``indexset``. Parameters: + indexset: A collection of integers to loop over. loop_parameter: The placeholder parameterizing ``body`` to which the values from ``indexset`` will be assigned. - indexset: A collection of integers to loop over. body: The loop body to be repeatedly executed. label: An optional label for identifying the instruction. @@ -51,8 +51,8 @@ class ForLoopOp(ControlFlowOp): def __init__( self, - loop_parameter: Union[Parameter, None], indexset: Iterable[int], + loop_parameter: Union[Parameter, None], body: QuantumCircuit, label: Optional[str] = None, ): @@ -60,7 +60,7 @@ def __init__( num_clbits = body.num_clbits super().__init__( - "for_loop", num_qubits, num_clbits, [loop_parameter, indexset, body], label=label + "for_loop", num_qubits, num_clbits, [indexset, loop_parameter, body], label=label ) @property @@ -69,7 +69,7 @@ def params(self): @params.setter def params(self, parameters): - loop_parameter, indexset, body = parameters + indexset, loop_parameter, body = parameters if not isinstance(loop_parameter, (Parameter, type(None))): raise CircuitError( @@ -111,8 +111,106 @@ def params(self, parameters): # Preserve ranges so that they can be exported as OpenQASM3 ranges. indexset = indexset if isinstance(indexset, range) else tuple(indexset) - self._params = [loop_parameter, indexset, body] + self._params = [indexset, loop_parameter, body] @property def blocks(self): return (self._params[2],) + + +class ForLoopContext: + """A context manager for building up ``for`` loops onto circuits in a natural order, without + having to construct the loop body first. + + Within the block, a lot of the bookkeeping is done for you; you do not need to keep track of + which qubits and clbits you are using, for example, and a loop parameter will be allocated for + you, if you do not supply one yourself. All normal methods of accessing the qubits on the + underlying :obj:`~QuantumCircuit` will work correctly, and resolve into correct accesses within + the interior block. + + You generally should never need to instantiate this object directly. Instead, use + :obj:`.QuantumCircuit.for_loop` in its context-manager form, i.e. by not supplying a ``body`` or + sets of qubits and clbits. + + Example usage:: + + import math + from qiskit import QuantumCircuit + qc = QuantumCircuit(2, 1) + + with qc.for_loop(None, range(5)) as i: + qc.rx(i * math.pi/4, 0) + qc.cx(0, 1) + qc.measure(0, 0) + qc.break_loop().c_if(0) + + This context should almost invariably be created by a :meth:`.QuantumCircuit.for_loop` call, and + the resulting instance is a "friend" of the calling circuit. The context will manipulate the + circuit's defined scopes when it is entered (by pushing a new scope onto the stack) and exited + (by popping its scope, building it, and appending the resulting :obj:`.ForLoopOp`). + """ + + # Class-level variable keep track of the number of auto-generated loop variables, so we don't + # get naming clashes. + _generated_loop_parameters = 0 + + __slots__ = ( + "_circuit", + "_generate_loop_parameter", + "_loop_parameter", + "_indexset", + "_label", + "_used", + ) + + def __init__( + self, + circuit: QuantumCircuit, + indexset: Iterable[int], + loop_parameter: Optional[Parameter] = None, + *, + label: Optional[str] = None, + ): + self._circuit = circuit + self._generate_loop_parameter = loop_parameter is None + self._loop_parameter = loop_parameter + # We can pass through `range` instances because OpenQASM 3 has native support for this type + # of iterator set. + self._indexset = indexset if isinstance(indexset, range) else tuple(indexset) + self._label = label + self._used = False + + def __enter__(self): + if self._used: + raise CircuitError("A for-loop context manager cannot be re-entered.") + self._used = True + self._circuit._push_scope() + if self._generate_loop_parameter: + self._loop_parameter = Parameter(f"_loop_i_{self._generated_loop_parameters}") + type(self)._generated_loop_parameters += 1 + return self._loop_parameter + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is not None: + # If we're leaving the context manager because an exception was raised, there's nothing + # to do except restore the circuit state. + self._circuit._pop_scope() + return False + scope = self._circuit._pop_scope() + # Loops do not need to pass any further resources in, because this scope itself defines the + # extent of ``break`` and ``continue`` statements. + body = scope.build(scope.qubits, scope.clbits) + # We always bind the loop parameter if the user gave it to us, even if it isn't actually + # used, because they requested we do that by giving us a parameter. However, if they asked + # us to auto-generate a parameter, then we only add it if they actually used it, to avoid + # using unnecessary resources. + if self._generate_loop_parameter and self._loop_parameter not in body.parameters: + loop_parameter = None + else: + loop_parameter = self._loop_parameter + self._circuit.append( + ForLoopOp(self._indexset, loop_parameter, body, label=self._label), + tuple(body.qubits), + tuple(body.clbits), + ) + return False diff --git a/qiskit/circuit/controlflow/if_else.py b/qiskit/circuit/controlflow/if_else.py index f90694c798b6..74329e91341b 100644 --- a/qiskit/circuit/controlflow/if_else.py +++ b/qiskit/circuit/controlflow/if_else.py @@ -15,11 +15,18 @@ from typing import Optional, Tuple, Union -from qiskit.circuit import Clbit, ClassicalRegister, QuantumCircuit +from qiskit.circuit import ClassicalRegister, Clbit, QuantumCircuit, Qubit +from qiskit.circuit.instructionset import InstructionSet from qiskit.circuit.exceptions import CircuitError +from .builder import ControlFlowBuilderBlock, InstructionPlaceholder +from .condition import validate_condition, condition_bits from .control_flow import ControlFlowOp +# This is just an indication of what's actually meant to be the public API. +__all__ = ("IfElseOp",) + + class IfElseOp(ControlFlowOp): """A circuit operation which executes a program (``true_body``) if a provided condition (``condition``) evaluates to true, and @@ -61,11 +68,7 @@ class IfElseOp(ControlFlowOp): def __init__( self, - condition: Union[ - Tuple[ClassicalRegister, int], - Tuple[Clbit, int], - Tuple[Clbit, bool], - ], + condition: Tuple[Union[ClassicalRegister, Clbit], int], true_body: QuantumCircuit, false_body: Optional[QuantumCircuit] = None, label: Optional[str] = None, @@ -83,28 +86,7 @@ def __init__( super().__init__("if_else", num_qubits, num_clbits, [true_body, false_body], label=label) - try: - lhs, rhs = condition - except (TypeError, ValueError) as err: - raise CircuitError( - "IfElseOp expects a condition argument as either a " - "Tuple[ClassicalRegister, int], a Tuple[Clbit, bool] or " - f"a Tuple[Clbit, int], but received {condition} of type " - f"{type(condition)}." - ) from err - - if not ( - (isinstance(lhs, ClassicalRegister) and isinstance(rhs, int)) - or (isinstance(lhs, Clbit) and isinstance(rhs, (int, bool))) - ): - raise CircuitError( - "IfElseOp expects a condition argument as either a " - "Tuple[ClassicalRegister, int], a Tuple[Clbit, bool] or " - f"a Tuple[Clbit, int], but receieved a {type(condition)}" - f"[{type(lhs)}, {type(rhs)}]." - ) - - self.condition = condition + self.condition = validate_condition(condition) @property def params(self): @@ -154,6 +136,358 @@ def blocks(self): def c_if(self, classical, val): raise NotImplementedError( - "WhileLoopOp cannot be classically controlled through Instruction.c_if. " - "Please use an IfElseOp instead." + "IfElseOp cannot be classically controlled through Instruction.c_if. " + "Please nest it in an IfElseOp instead." ) + + +class IfElsePlaceholder(InstructionPlaceholder): + """A placeholder instruction to use in control-flow context managers, when calculating the + number of resources this instruction should block is deferred until the construction of the + outer loop. + + This generally should not be instantiated manually; only :obj:`.IfContext` and + :obj:`.ElseContext` should do it when they need to defer creation of the concrete instruction. + """ + + def __init__( + self, + condition: Tuple[Union[ClassicalRegister, Clbit], int], + true_block: ControlFlowBuilderBlock, + false_block: Optional[ControlFlowBuilderBlock] = None, + *, + label: Optional[str] = None, + ): + """ + Args: + condition: the condition to execute the true block on. This has the same semantics as + the ``condition`` argument to :obj:`.IfElseOp`. + true_block: the unbuilt scope block that will become the "true" branch at creation time. + false_block: if given, the unbuilt scope block that will become the "false" branch at + creation time. + label: the label to give the operator when it is created. + """ + # These are protected names because we're not trying to clash with parent attributes. + self.__true_block = true_block + self.__false_block: Optional[ControlFlowBuilderBlock] = false_block + self.__resources = self._placeholder_resources() + qubits, clbits = self.__resources + super().__init__("if_else", len(qubits), len(clbits), [], label=label) + # Set the condition after super().__init__() has initialised it to None. + self.condition = validate_condition(condition) + + def with_false_block(self, false_block: ControlFlowBuilderBlock) -> "IfElsePlaceholder": + """Return a new placeholder instruction, with the false block set to the given value, + updating the bits used by both it and the true body, if necessary. + + It is an error to try and set the false block on a placeholder that already has one. + + Args: + false_block: The (unbuilt) instruction scope to set the false body to. + + Returns: + A new placeholder, with ``false_block`` set to the given input, and both true and false + blocks expanded to account for all resources. + + Raises: + CircuitError: if the false block of this placeholder instruction is already set. + """ + if self.__false_block is not None: + raise CircuitError(f"false block is already set to {self.__false_block}") + true_block = self.__true_block.copy() + true_bits = true_block.qubits | true_block.clbits + false_bits = false_block.qubits | false_block.clbits + true_block.add_bits(false_bits - true_bits) + false_block.add_bits(true_bits - false_bits) + return type(self)(self.condition, true_block, false_block, label=self.label) + + def _placeholder_resources(self) -> Tuple[Tuple[Qubit, ...], Tuple[Clbit, ...]]: + """Get the placeholder resources (see :meth:`.placeholder_resources`). + + This is a separate function because we use the resources during the initialisation to + determine how we should set our ``num_qubits`` and ``num_clbits``, so we implement the + public version as a cache access for efficiency. + """ + if self.__false_block is None: + return tuple(self.__true_block.qubits), tuple(self.__true_block.clbits) + return ( + tuple(self.__true_block.qubits | self.__false_block.qubits), + tuple(self.__true_block.clbits | self.__false_block.clbits), + ) + + def placeholder_resources(self): + # Tuple and Bit are both immutable, so the resource cache is completely immutable. + return self.__resources + + def concrete_instruction(self, qubits, clbits): + current_qubits = self.__true_block.qubits + current_clbits = self.__true_block.clbits + if self.__false_block is not None: + current_qubits = current_qubits | self.__false_block.qubits + current_clbits = current_clbits | self.__false_block.clbits + all_bits = qubits | clbits + current_bits = current_qubits | current_clbits + if current_bits - all_bits: + # This _shouldn't_ trigger if the context managers are being used correctly, but is here + # to make any potential logic errors noisy. + raise CircuitError( + "This block contains bits that are not in the operands sets:" + f" {current_bits - all_bits!r}" + ) + true_body = self.__true_block.build(qubits, clbits) + false_body = ( + None if self.__false_block is None else self.__false_block.build(qubits, clbits) + ) + # The bodies are not compelled to use all the resources that the + # ControlFlowBuilderBlock.build calls get passed, but they do need to be as wide as each + # other. Now we ensure that they are. + true_body, false_body = _unify_circuit_bits(true_body, false_body) + return ( + self._copy_mutable_properties( + IfElseOp(self.condition, true_body, false_body, label=self.label) + ), + tuple(true_body.qubits), + tuple(true_body.clbits), + ) + + def c_if(self, classical, val): + raise NotImplementedError( + "IfElseOp cannot be classically controlled through Instruction.c_if. " + "Please nest it in another IfElseOp instead." + ) + + +class IfContext: + """A context manager for building up ``if`` statements onto circuits in a natural order, without + having to construct the statement body first. + + The return value of this context manager can be used immediately following the block to create + an attached ``else`` statement. + + This context should almost invariably be created by a :meth:`.QuantumCircuit.if_test` call, and + the resulting instance is a "friend" of the calling circuit. The context will manipulate the + circuit's defined scopes when it is entered (by pushing a new scope onto the stack) and exited + (by popping its scope, building it, and appending the resulting :obj:`.IfElseOp`). + """ + + __slots__ = ("_appended_instructions", "_circuit", "_condition", "_in_loop", "_label") + + def __init__( + self, + circuit: QuantumCircuit, + condition: Tuple[Union[ClassicalRegister, Clbit], int], + *, + in_loop: bool, + label: Optional[str] = None, + ): + self._circuit = circuit + self._condition = validate_condition(condition) + self._label = label + self._appended_instructions = None + self._in_loop = in_loop + + # Only expose the necessary public interface, and make it read-only. If Python had friend + # classes, or a "protected" access modifier, that's what we'd use (since these are only + # necessary for ElseContext), but alas. + + @property + def circuit(self) -> QuantumCircuit: + """Get the circuit that this context manager is attached to.""" + return self._circuit + + @property + def condition(self) -> Tuple[Union[ClassicalRegister, Clbit], int]: + """Get the expression that this statement is conditioned on.""" + return self._condition + + @property + def appended_instructions(self) -> Union[InstructionSet, None]: + """Get the instruction set that was created when this block finished. If the block has not + yet finished, then this will be ``None``.""" + return self._appended_instructions + + @property + def in_loop(self) -> bool: + """Whether this context manager is enclosed within a loop.""" + return self._in_loop + + def __enter__(self): + self._circuit._push_scope(clbits=condition_bits(self._condition), allow_jumps=self._in_loop) + return ElseContext(self) + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is not None: + # If we're leaving the context manager because an exception was raised, there's nothing + # to do except restore the circuit state. + self._circuit._pop_scope() + return False + true_block = self._circuit._pop_scope() + if self._in_loop: + # It's possible that we don't actually have any placeholder instructions in our scope, + # but we still need to emit a placeholder instruction here in case we get an ``else`` + # attached which _does_ gain them. We emit a placeholder to defer defining the + # resources we use until the containing loop concludes, to support ``break``. + operation = IfElsePlaceholder(self._condition, true_block, label=self._label) + self._appended_instructions = self._circuit.append( + operation, *operation.placeholder_resources() + ) + else: + # If we're not in a loop, we don't need to be worried about passing in any outer-scope + # resources because there can't be anything that will consume them. + true_body = true_block.build(true_block.qubits, true_block.clbits) + self._appended_instructions = self._circuit.append( + IfElseOp(self._condition, true_body=true_body, false_body=None, label=self._label), + tuple(true_body.qubits), + tuple(true_body.clbits), + ) + return False + + +class ElseContext: + """A context manager for building up an ``else`` statements onto circuits in a natural order, + without having to construct the statement body first. + + Instances of this context manager should only ever be gained as the output of the + :obj:`.IfContext` manager, so they know what they refer to. Instances of this context are + "friends" of the circuit that created the :obj:`.IfContext` that in turn created this object. + The context will manipulate the circuit's defined scopes when it is entered (by popping the old + :obj:`.IfElseOp` if it exists and pushing a new scope onto the stack) and exited (by popping its + scope, building it, and appending the resulting :obj:`.IfElseOp`). + """ + + __slots__ = ("_if_block", "_if_clbits", "_if_context", "_if_qubits", "_used") + + def __init__(self, if_context: IfContext): + # We want to avoid doing any processing until we're actually used, because the `if` block + # likely isn't finished yet, and we want to have as small a penalty a possible if you don't + # use an `else` branch. + self._if_block = None + self._if_qubits = None + self._if_clbits = None + self._if_context = if_context + self._used = False + + def __enter__(self): + if self._used: + raise CircuitError("Cannot re-use an 'else' context.") + self._used = True + appended_instructions = self._if_context.appended_instructions + circuit = self._if_context.circuit + if appended_instructions is None: + raise CircuitError("Cannot attach an 'else' branch to an incomplete 'if' block.") + if len(appended_instructions) != 1: + # I'm not even sure how you'd get this to trigger, but just in case... + raise CircuitError("Cannot attach an 'else' to a broadcasted 'if' block.") + appended = appended_instructions[0] + operation, _, _ = circuit._peek_previous_instruction_in_scope() + if appended is not operation: + raise CircuitError( + "The 'if' block is not the most recent instruction in the circuit." + f" Expected to find: {appended!r}, but instead found: {operation!r}." + ) + ( + self._if_block, + self._if_qubits, + self._if_clbits, + ) = circuit._pop_previous_instruction_in_scope() + circuit._push_scope(self._if_qubits, self._if_clbits, allow_jumps=self._if_context.in_loop) + + def __exit__(self, exc_type, exc_val, exc_tb): + circuit = self._if_context.circuit + if exc_type is not None: + # If we're leaving the context manager because an exception was raised, we need to + # restore the "if" block we popped off. At that point, it's safe to re-use this context + # manager, assuming nothing else untoward happened to the circuit, but that's checked by + # the __enter__ method. + circuit._pop_scope() + circuit.append(self._if_block, self._if_qubits, self._if_clbits) + self._used = False + return False + + false_block = circuit._pop_scope() + # `if_block` is a placeholder if this context is in a loop, and a concrete instruction if it + # is not. + if isinstance(self._if_block, IfElsePlaceholder): + if_block = self._if_block.with_false_block(false_block) + circuit.append(if_block, *if_block.placeholder_resources()) + else: + # In this case, we need to update both true_body and false_body to have exactly the same + # widths. Passing extra resources to `ControlFlowBuilderBlock.build` doesn't _compel_ + # the resulting object to use them (because it tries to be minimal), so it's best to + # pass it nothing extra (allows some fast path constructions), and add all necessary + # bits onto the circuits at the end. + true_body = self._if_block.blocks[0] + false_body = false_block.build(false_block.qubits, false_block.clbits) + true_body, false_body = _unify_circuit_bits(true_body, false_body) + circuit.append( + IfElseOp( + self._if_context.condition, + true_body, + false_body, + label=self._if_block.label, + ), + tuple(true_body.qubits), + tuple(true_body.clbits), + ) + return False + + +def _unify_circuit_bits( + true_body: QuantumCircuit, false_body: Optional[QuantumCircuit] +) -> Tuple[QuantumCircuit, Union[QuantumCircuit, None]]: + """ + Ensure that ``true_body`` and ``false_body`` have all the same qubits and clbits, and that they + are defined in the same order. The order is important for binding when the bodies are used in + the 3-tuple :obj:`.Instruction` context. + + This function will preferentially try to mutate ``true_body`` and ``false_body`` if they share + an ordering, but if not, it will rebuild two new circuits. This is to avoid coupling too + tightly to the inner class; there is no real support for deleting or re-ordering bits within a + :obj:`.QuantumCircuit` context, and we don't want to rely on the *current* behaviour of the + private APIs, since they are very liable to change. No matter the method used, two circuits + with unified bits are returned. + """ + if false_body is None: + return true_body, false_body + # These may be returned as inner lists, so take care to avoid mutation. + true_qubits, true_clbits = true_body.qubits, true_body.clbits + n_true_qubits, n_true_clbits = len(true_qubits), len(true_clbits) + false_qubits, false_clbits = false_body.qubits, false_body.clbits + n_false_qubits, n_false_clbits = len(false_qubits), len(false_clbits) + # Attempt to determine if the two resource lists can simply be extended to be equal. The + # messiness with comparing lengths first is to avoid doing multiple full-list comparisons. + if n_true_qubits <= n_false_qubits and true_qubits == false_qubits[:n_true_qubits]: + true_body.add_bits(false_qubits[n_true_qubits:]) + elif n_false_qubits < n_true_qubits and false_qubits == true_qubits[:n_false_qubits]: + false_body.add_bits(true_qubits[n_false_qubits:]) + else: + return _unify_circuit_bits_rebuild(true_body, false_body) + if n_true_clbits <= n_false_clbits and true_clbits == false_clbits[:n_true_clbits]: + true_body.add_bits(false_clbits[n_true_clbits:]) + elif n_false_clbits < n_true_clbits and false_clbits == true_clbits[:n_false_clbits]: + false_body.add_bits(true_clbits[n_false_clbits:]) + else: + return _unify_circuit_bits_rebuild(true_body, false_body) + return true_body, false_body + + +def _unify_circuit_bits_rebuild( + true_body: QuantumCircuit, false_body: QuantumCircuit +) -> Tuple[QuantumCircuit, QuantumCircuit]: + """ + Ensure that ``true_body`` and ``false_body`` have all the same qubits and clbits, and that they + are defined in the same order. The order is important for binding when the bodies are used in + the 3-tuple :obj:`.Instruction` context. + + This function will always rebuild the two parameters into new :obj:`.QuantumCircuit` instances. + """ + qubits = list(set(true_body.qubits).union(false_body.qubits)) + clbits = list(set(true_body.clbits).union(false_body.clbits)) + # We use the inner `_append` method because everything is already resolved. + true_out = QuantumCircuit(qubits, clbits) + for data in true_body.data: + true_out._append(*data) + false_out = QuantumCircuit(qubits, clbits) + for data in false_body.data: + false_out._append(*data) + return true_out, false_out diff --git a/qiskit/circuit/controlflow/while_loop.py b/qiskit/circuit/controlflow/while_loop.py index 8dd32d7dcb7b..3f62268daa68 100644 --- a/qiskit/circuit/controlflow/while_loop.py +++ b/qiskit/circuit/controlflow/while_loop.py @@ -16,6 +16,7 @@ from qiskit.circuit import Clbit, ClassicalRegister, QuantumCircuit from qiskit.circuit.exceptions import CircuitError +from .condition import validate_condition, condition_bits from .control_flow import ControlFlowOp @@ -64,29 +65,7 @@ def __init__( num_clbits = body.num_clbits super().__init__("while_loop", num_qubits, num_clbits, [body], label=label) - - try: - lhs, rhs = condition - except TypeError as err: - raise CircuitError( - "WhileLoopOp expects a condition argument as either a " - "Tuple[ClassicalRegister, int], a Tuple[Clbit, bool] or " - f"a Tuple[Clbit, int], but received {condition} of type " - f"{type(condition)}." - ) from err - - if not ( - (isinstance(lhs, ClassicalRegister) and isinstance(rhs, int)) - or (isinstance(lhs, Clbit) and isinstance(rhs, (int, bool))) - ): - raise CircuitError( - "WhileLoopOp expects a condition argument as either a " - "Tuple[ClassicalRegister, int], a Tuple[Clbit, bool] or " - f"a Tuple[Clbit, int], but receieved a {type(condition)}" - f"[{type(lhs)}, {type(rhs)}]." - ) - - self.condition = condition + self.condition = validate_condition(condition) @property def params(self): @@ -121,3 +100,67 @@ def c_if(self, classical, val): "WhileLoopOp cannot be classically controlled through Instruction.c_if. " "Please use an IfElseOp instead." ) + + +class WhileLoopContext: + """A context manager for building up while loops onto circuits in a natural order, without + having to construct the loop body first. + + Within the block, a lot of the bookkeeping is done for you; you do not need to keep track of + which qubits and clbits you are using, for example. All normal methods of accessing the qubits + on the underlying :obj:`~QuantumCircuit` will work correctly, and resolve into correct accesses + within the interior block. + + You generally should never need to instantiate this object directly. Instead, use + :obj:`.QuantumCircuit.while_loop` in its context-manager form, i.e. by not supplying a ``body`` + or sets of qubits and clbits. + + Example usage:: + + from qiskit.circuit import QuantumCircuit, Clbit, Qubit + bits = [Qubit(), Qubit(), Clbit()] + qc = QuantumCircuit(bits) + + with qc.while_loop((bits[2], 0)): + qc.h(0) + qc.cx(0, 1) + qc.measure(0, 0) + """ + + __slots__ = ("_circuit", "_condition", "_label") + + def __init__( + self, + circuit: QuantumCircuit, + condition: Union[ + Tuple[ClassicalRegister, int], + Tuple[Clbit, int], + Tuple[Clbit, bool], + ], + *, + label: Optional[str] = None, + ): + + self._circuit = circuit + self._condition = validate_condition(condition) + self._label = label + + def __enter__(self): + self._circuit._push_scope(clbits=condition_bits(self._condition)) + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is not None: + # If we're leaving the context manager because an exception was raised, there's nothing + # to do except restore the circuit state. + self._circuit._pop_scope() + return False + scope = self._circuit._pop_scope() + # Loops do not need to pass any further resources in, because this scope itself defines the + # extent of ``break`` and ``continue`` statements. + body = scope.build(scope.qubits, scope.clbits) + self._circuit.append( + WhileLoopOp(self._condition, body, label=self._label), + body.qubits, + body.clbits, + ) + return False diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index cd18dcee6a46..5551b64ac023 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -69,6 +69,9 @@ except Exception: # pylint: disable=broad-except HAS_PYGMENTS = False +if typing.TYPE_CHECKING: + import qiskit # pylint: disable=cyclic-import + BitLocations = namedtuple("BitLocations", ("index", "registers")) @@ -243,6 +246,14 @@ def __init__( # in the order they were applied. self._data = [] + # A stack to hold the instruction sets that are being built up during for-, if- and + # while-block construction. These are stored as a stripped down sequence of instructions, + # and sets of qubits and clbits, rather than a full QuantumCircuit instance because the + # builder interfaces need to wait until they are completed before they can fill in things + # like `break` and `continue`. This is because these instructions need to "operate" on the + # full width of bits, but the builder interface won't know what bits are used until the end. + self._control_flow_scopes = [] + self.qregs = [] self.cregs = [] self._qubits = [] @@ -1209,9 +1220,15 @@ def append( 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=self._resolve_classical_resource) - for (qarg, carg) in instruction.broadcast_arguments(expanded_qargs, expanded_cargs): - instructions.add(self._append(instruction, qarg, carg), qarg, carg) + if self._control_flow_scopes: + appender = self._control_flow_scopes[-1].append + requester = self._control_flow_scopes[-1].request_classical_resource + else: + appender = self._append + requester = self._resolve_classical_resource + instructions = InstructionSet(resource_requester=requester) + for qarg, carg in instruction.broadcast_arguments(expanded_qargs, expanded_cargs): + instructions.add(appender(instruction, qarg, carg), qarg, carg) return instructions def _append( @@ -1220,6 +1237,16 @@ def _append( """Append an instruction to the end of the circuit, modifying the circuit in place. + .. note:: + + This function may be used by callers other than :obj:`.QuantumCircuit` when the caller + is sure that all error-checking, broadcasting and scoping has already been performed, + and the only reference to the circuit the instructions are being appended to is within + that same function. In particular, it is not safe to call + :meth:`QuantumCircuit._append` on a circuit that is received by a function argument. + This is because :meth:`.QuantumCircuit._append` will not recognise the scoping + constructs of the control-flow builder interface. + Args: instruction: Instruction instance to append qargs: qubits to attach instruction to @@ -1253,26 +1280,26 @@ def _append( return instruction def _update_parameter_table(self, instruction: Instruction) -> Instruction: - for param_index, param in enumerate(instruction.params): - if isinstance(param, ParameterExpression): - current_parameters = self._parameter_table - - for parameter in param.parameters: - if parameter in current_parameters: - if not self._check_dup_param_spec( - self._parameter_table[parameter], instruction, param_index - ): - self._parameter_table[parameter].append((instruction, param_index)) - else: - if parameter.name in self._parameter_table.get_names(): - raise CircuitError( - f"Name conflict on adding parameter: {parameter.name}" - ) - self._parameter_table[parameter] = [(instruction, param_index)] + if isinstance(param, (ParameterExpression, QuantumCircuit)): + # Scoped constructs like the control-flow ops use QuantumCircuit as a parameter. + atomic_parameters = set(param.parameters) + else: + atomic_parameters = set() + + for parameter in atomic_parameters: + if parameter in self._parameter_table: + if not self._check_dup_param_spec( + self._parameter_table[parameter], instruction, param_index + ): + self._parameter_table[parameter].append((instruction, param_index)) + else: + if parameter.name in self._parameter_table.get_names(): + raise CircuitError(f"Name conflict on adding parameter: {parameter.name}") + self._parameter_table[parameter] = [(instruction, param_index)] - # clear cache if new parameter is added - self._parameters = None + # clear cache if new parameter is added + self._parameters = None return instruction @@ -1356,7 +1383,7 @@ def add_register(self, *regs: Union[Register, int, Sequence[Bit]]) -> None: else: raise CircuitError("expected a register") - def add_bits(self, bits: Sequence[Bit]) -> None: + def add_bits(self, bits: Iterable[Bit]) -> None: """Add Bits to the circuit.""" duplicate_bits = set(self._qubit_indices).union(self._clbit_indices).intersection(bits) if duplicate_bits: @@ -2551,18 +2578,37 @@ def _assign_parameter(self, parameter: Parameter, value: ParameterValueType) -> parameter (ParameterExpression): Parameter to be bound value (Union(ParameterExpression, float, int)): A numeric or parametric expression to replace instances of ``parameter``. + + Raises: + RuntimeError: if some internal logic error has caused the circuit instruction sequence + and the parameter table to become out of sync, and the table now contains a + reference to a value that cannot be assigned. """ # parameter might be in global phase only if parameter in self._parameter_table.keys(): for instr, param_index in self._parameter_table[parameter]: - new_param = instr.params[param_index].assign(parameter, value) - # if fully bound, validate - if len(new_param.parameters) == 0: - instr.params[param_index] = instr.validate_parameter(new_param) + assignee = instr.params[param_index] + # Normal ParameterExpression. + if isinstance(assignee, ParameterExpression): + new_param = assignee.assign(parameter, value) + # if fully bound, validate + if len(new_param.parameters) == 0: + instr.params[param_index] = instr.validate_parameter(new_param) + else: + instr.params[param_index] = new_param + + self._rebind_definition(instr, parameter, value) + # Scoped block of a larger instruction. + elif isinstance(assignee, QuantumCircuit): + # It's possible that someone may re-use a loop body, so we need to mutate the + # parameter vector with a new circuit, rather than mutating the body. + instr.params[param_index] = assignee.assign_parameters({parameter: value}) else: - instr.params[param_index] = new_param - - self._rebind_definition(instr, parameter, value) + raise RuntimeError( # pragma: no cover + "The ParameterTable or data of this QuantumCircuit have become out-of-sync." + f"\nParameterTable: {self._parameter_table}" + f"\nData: {self.data}" + ) if isinstance(value, ParameterExpression): entry = self._parameter_table.pop(parameter) @@ -4005,104 +4051,386 @@ def pauli( return self.append(PauliGate(pauli_string), qubits, []) + def _push_scope( + self, qubits: Iterable[Qubit] = (), clbits: Iterable[Clbit] = (), allow_jumps: bool = True + ): + """Add a scope for collecting instructions into this circuit. + + This should only be done by the control-flow context managers, which will handle cleaning up + after themselves at the end as well. + + Args: + qubits: Any qubits that this scope should automatically use. + clbits: Any clbits that this scope should automatically use. + allow_jumps: Whether this scope allows jumps to be used within it. + """ + # pylint: disable=cyclic-import + from qiskit.circuit.controlflow.builder import ControlFlowBuilderBlock + + self._control_flow_scopes.append( + ControlFlowBuilderBlock( + qubits, + clbits, + resource_requester=self._resolve_classical_resource, + allow_jumps=allow_jumps, + ) + ) + + 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 + synchronise the creation and deletion of stack elements.""" + return self._control_flow_scopes.pop() + + def _peek_previous_instruction_in_scope( + self, + ) -> Tuple[Instruction, Sequence[Qubit], Sequence[Clbit]]: + """Return the instruction 3-tuple of the most recent instruction in the current scope, even + if that scope is currently under construction. + + This function is only intended for use by the control-flow ``if``-statement builders, which + may need to modify a previous instruction.""" + if self._control_flow_scopes: + return self._control_flow_scopes[-1].peek() + if not self._data: + raise CircuitError("This circuit contains no instructions.") + return self._data[-1] + + def _pop_previous_instruction_in_scope( + self, + ) -> Tuple[Instruction, Sequence[Qubit], Sequence[Clbit]]: + """Return the instruction 3-tuple of the most recent instruction in the current scope, even + if that scope is currently under construction, and remove it from that scope. + + This function is only intended for use by the control-flow ``if``-statement builders, which + may need to replace a previous instruction with another. + """ + if self._control_flow_scopes: + return self._control_flow_scopes[-1].pop() + if not self._data: + raise CircuitError("This circuit contains no instructions.") + instruction, qubits, clbits = self._data.pop() + self._update_parameter_table_on_instruction_removal(instruction) + return instruction, qubits, clbits + + def _update_parameter_table_on_instruction_removal(self, instruction: Instruction) -> None: + """Update the :obj:`.ParameterTable` of this circuit given that an instance of the given + ``instruction`` has just been removed from the circuit. + + .. note:: + + This does not account for the possibility for the same instruction instance being added + more than once to the circuit. At the time of writing (2021-11-17, main commit 271a82f) + there is a defensive ``deepcopy`` of parameterised instructions inside + :meth:`.QuantumCircuit.append`, so this should be safe. Trying to account for it would + involve adding a potentially quadratic-scaling loop to check each entry in ``data``. + """ + atomic_parameters = set() + for parameter in instruction.params: + if isinstance(parameter, (ParameterExpression, QuantumCircuit)): + atomic_parameters.update(parameter.parameters) + for atomic_parameter in atomic_parameters: + entries = self._parameter_table[atomic_parameter] + new_entries = [ + (entry_instruction, entry_index) + for entry_instruction, entry_index in entries + if entry_instruction is not instruction + ] + if not new_entries: + del self._parameter_table[atomic_parameter] + # Invalidate cache. + self._parameters = None + else: + self._parameter_table[atomic_parameter] = new_entries + + @typing.overload def while_loop( self, - condition: Union[ - Tuple[ClassicalRegister, int], - Tuple[Clbit, int], - Tuple[Clbit, bool], - ], + condition: Tuple[Union[ClassicalRegister, Clbit], int], + body: None, + qubits: None, + clbits: None, + *, + label: Optional[str], + ) -> "qiskit.circuit.controlflow.while_loop.WhileLoopContext": + ... + + @typing.overload + def while_loop( + self, + condition: Tuple[Union[ClassicalRegister, Clbit], int], body: "QuantumCircuit", qubits: Sequence[QubitSpecifier], clbits: Sequence[ClbitSpecifier], - label: Optional[str] = None, + *, + label: Optional[str], ) -> InstructionSet: - """Apply :class:`~qiskit.circuit.controlflow.WhileLoopOp`. + ... + + def while_loop(self, condition, body=None, qubits=None, clbits=None, *, label=None): + """Create a ``while`` loop on this circuit. + + There are two forms for calling this function. If called with all its arguments (with the + possible exception of ``label``), it will create a + :obj:`~qiskit.circuit.controlflow.WhileLoopOp` with the given ``body``. If ``body`` (and + ``qubits`` and ``clbits``) are *not* passed, then this acts as a context manager, which + will automatically build a :obj:`~qiskit.circuit.controlflow.WhileLoopOp` when the scope + finishes. In this form, you do not need to keep track of the qubits or clbits you are + using, because the scope will handle it for you. + + Example usage:: + + from qiskit.circuit import QuantumCircuit, Clbit, Qubit + bits = [Qubit(), Qubit(), Clbit()] + qc = QuantumCircuit(bits) + + with qc.while_loop((bits[2], 0)): + qc.h(0) + qc.cx(0, 1) + qc.measure(0, 0) Args: - condition: A condition to be checked prior to executing ``body``. Can - be specified as either a tuple of a ``ClassicalRegister`` to be - tested for equality with a given ``int``, or as a tuple of a - ``Clbit`` to be compared to either a ``bool`` or an ``int``. - body: The loop body to be repeatedly executed. - qubits: The circuit qubits over which the loop body should be run. - clbits: The circuit clbits over which the loop body should be run. - label: The string label of the instruction in the circuit. + condition (Tuple[Union[ClassicalRegister, Clbit], int]): An equality condition to be + checked prior to executing ``body``. The left-hand side of the condition must be a + :obj:`~ClassicalRegister` or a :obj:`~Clbit`, and the right-hand side must be an + integer or boolean. + body (Optional[QuantumCircuit]): The loop body to be repeatedly executed. Omit this to + use the context-manager mode. + qubits (Optional[Sequence[Qubit]]): The circuit qubits over which the loop body should + be run. Omit this to use the context-manager mode. + clbits (Optional[Sequence[Clbit]]): The circuit clbits over which the loop body should + be run. Omit this to use the context-manager mode. + label (Optional[str]): The string label of the instruction in the circuit. Returns: - A handle to the instruction created. + InstructionSet or WhileLoopContext: If used in context-manager mode, then this should be + used as a ``with`` resource, which will infer the block content and operands on exit. + If the full form is used, then this returns a handle to the instructions created. + + Raises: + CircuitError: if an incorrect calling convention is used. """ # pylint: disable=cyclic-import - from qiskit.circuit.controlflow.while_loop import WhileLoopOp + from qiskit.circuit.controlflow.while_loop import WhileLoopOp, WhileLoopContext + + if body is None: + if qubits is not None or clbits is not None: + raise CircuitError( + "When using 'while_loop' as a context manager," + " you cannot pass qubits or clbits." + ) + return WhileLoopContext(self, condition, label=label) + elif qubits is None or clbits is None: + raise CircuitError( + "When using 'while_loop' with a body, you must pass qubits and clbits." + ) return self.append(WhileLoopOp(condition, body, label), qubits, clbits) + @typing.overload def for_loop( self, - loop_parameter: Union[Parameter, None], indexset: Iterable[int], + loop_parameter: Optional[Parameter], + body: None, + qubits: None, + clbits: None, + *, + label: Optional[str], + ) -> "qiskit.circuit.controlflow.for_loop.ForLoopContext": + ... + + @typing.overload + def for_loop( + self, + indexset: Iterable[int], + loop_parameter: Union[Parameter, None], body: "QuantumCircuit", qubits: Sequence[QubitSpecifier], clbits: Sequence[ClbitSpecifier], - label: Optional[str] = None, + *, + label: Optional[str], ) -> InstructionSet: - """Apply :class:`~qiskit.circuit.controlflow.ForLoopOp`. + ... + + def for_loop( + self, indexset, loop_parameter=None, body=None, qubits=None, clbits=None, *, label=None + ): + """Create a ``for`` loop on this circuit. + + There are two forms for calling this function. If called with all its arguments (with the + possible exception of ``label``), it will create a + :obj:`~qiskit.circuit.controlflow.ForLoopOp` with the given ``body``. If ``body`` (and + ``qubits`` and ``clbits``) are *not* passed, then this acts as a context manager, which, + when entered, provides a loop variable (unless one is given, in which case it will be + reused) and will automatically build a :obj:`~qiskit.circuit.controlflow.ForLoopOp` when the + scope finishes. In this form, you do not need to keep track of the qubits or clbits you are + using, because the scope will handle it for you. + + For example:: + + from qiskit import QuantumCircuit + qc = QuantumCircuit(2, 1) + + with qc.for_loop(range(5)) as i: + qc.h(0) + qc.cx(0, 1) + qc.measure(0, 0) + qc.break_loop().c_if(0) Args: - loop_parameter: The placeholder parameterizing ``body`` to which - the values from ``indexset`` will be assigned. If None is - provided, ``body`` will be repeated for each of the values - in ``indexset`` but the values will not be assigned to ``body``. - indexset: A collection of integers to loop over. - body: The loop body to be repeatedly executed. - qubits: The circuit qubits over which the loop body should be run. - clbits: The circuit clbits over which the loop body should be run. - label: The string label of the instruction in the circuit. + indexset (Iterable[int]): A collection of integers to loop over. Always necessary. + loop_parameter (Optional[Parameter]): The parameter used within ``body`` to which + the values from ``indexset`` will be assigned. In the context-manager form, if this + argument is not supplied, then a loop parameter will be allocated for you and + returned as the value of the ``with`` statement. This will only be bound into the + circuit if it is used within the body. + + If this argument is ``None`` in the manual form of this method, ``body`` will be + repeated once for each of the items in ``indexset`` but their values will be + ignored. + body (Optional[QuantumCircuit]): The loop body to be repeatedly executed. Omit this to + use the context-manager mode. + qubits (Optional[Sequence[QubitSpecifier]]): The circuit qubits over which the loop body + should be run. Omit this to use the context-manager mode. + clbits (Optional[Sequence[ClbitSpecifier]]): The circuit clbits over which the loop body + should be run. Omit this to use the context-manager mode. + label (Optional[str]): The string label of the instruction in the circuit. Returns: - A handle to the instruction created. + InstructionSet or ForLoopContext: depending on the call signature, either a context + manager for creating the for loop (it will automatically be added to the circuit at the + end of the block), or an :obj:`~InstructionSet` handle to the appended loop operation. + + Raises: + CircuitError: if an incorrect calling convention is used. """ # pylint: disable=cyclic-import - from qiskit.circuit.controlflow.for_loop import ForLoopOp + 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( + "When using 'for_loop' as a context manager, you cannot pass qubits or clbits." + ) + return ForLoopContext(self, indexset, loop_parameter, label=label) + elif qubits is None or clbits is None: + raise CircuitError( + "When using 'for_loop' with a body, you must pass qubits and clbits." + ) - return self.append(ForLoopOp(loop_parameter, indexset, body, label), qubits, clbits) + return self.append(ForLoopOp(indexset, loop_parameter, body, label), qubits, clbits) + @typing.overload def if_test( self, - condition: Union[ - Tuple[ClassicalRegister, int], - Tuple[Clbit, int], - Tuple[Clbit, bool], - ], + condition: Tuple[Union[ClassicalRegister, Clbit], int], + true_body: None, + qubits: None, + clbits: None, + *, + label: Optional[str], + ) -> "qiskit.circuit.controlflow.if_else.IfContext": + ... + + @typing.overload + def if_test( + self, + condition: Tuple[Union[ClassicalRegister, Clbit], int], true_body: "QuantumCircuit", qubits: Sequence[QubitSpecifier], clbits: Sequence[ClbitSpecifier], + *, label: Optional[str] = None, ) -> InstructionSet: - """Apply :class:`~qiskit.circuit.controlflow.IfElseOp` without a ``false_body``. + ... + + def if_test( + self, + condition, + true_body=None, + qubits=None, + clbits=None, + *, + label=None, + ): + """Create an ``if`` statement on this circuit. + + There are two forms for calling this function. If called with all its arguments (with the + possible exception of ``label``), it will create a + :obj:`~qiskit.circuit.controlflow.IfElseOp` with the given ``true_body``, and there will be + no branch for the ``false`` condition (see also the :meth:`.if_else` method). However, if + ``true_body`` (and ``qubits`` and ``clbits``) are *not* passed, then this acts as a context + manager, which can be used to build ``if`` statements. The return value of the ``with`` + statement is a chainable context manager, which can be used to create subsequent ``else`` + blocks. In this form, you do not need to keep track of the qubits or clbits you are using, + because the scope will handle it for you. + + For example:: + + from qiskit.circuit import QuantumCircuit, Qubit, Clbit + bits = [Qubit(), Qubit(), Qubit(), Clbit(), Clbit()] + qc = QuantumCircuit(bits) + + qc.h(0) + qc.cx(0, 1) + qc.measure(0, 0) + qc.h(0) + qc.cx(0, 1) + qc.measure(0, 1) + + with qc.if_test((bits[3], 0)) as else_: + qc.x(2) + with else_: + qc.h(2) + qc.z(2) Args: - condition: A condition to be evaluated at circuit runtime which, - if true, will trigger the evaluation of ``true_body``. Can be - specified as either a tuple of a ``ClassicalRegister`` to be - tested for equality with a given ``int``, or as a tuple of a - ``Clbit`` to be compared to either a ``bool`` or an ``int``. - true_body: The circuit body to be run if ``condition`` is true. - qubits: The circuit qubits over which the if/else should be run. - clbits: The circuit clbits over which the if/else should be run. - label: The string label of the instruction in the circuit. + condition (Tuple[Union[ClassicalRegister, Clbit], int]): A condition to be evaluated at + circuit runtime which, if true, will trigger the evaluation of ``true_body``. Can be + specified as either a tuple of a ``ClassicalRegister`` to be tested for equality + with a given ``int``, or as a tuple of a ``Clbit`` to be compared to either a + ``bool`` or an ``int``. + true_body (Optional[QuantumCircuit]): The circuit body to be run if ``condition`` is + true. + qubits (Optional[Sequence[QubitSpecifier]]): The circuit qubits over which the if/else + should be run. + clbits (Optional[Sequence[ClbitSpecifier]]): The circuit clbits over which the if/else + should be run. + label (Optional[str]): The string label of the instruction in the circuit. + + Returns: + InstructionSet or IfContext: depending on the call signature, either a context + manager for creating the ``if`` block (it will automatically be added to the circuit at + the end of the block), or an :obj:`~InstructionSet` handle to the appended conditional + operation. Raises: CircuitError: If the provided condition references Clbits outside the enclosing circuit. + CircuitError: if an incorrect calling convention is used. Returns: A handle to the instruction created. """ # pylint: disable=cyclic-import - from qiskit.circuit.controlflow.if_else import IfElseOp + from qiskit.circuit.controlflow.if_else import IfElseOp, IfContext condition = (self._resolve_classical_resource(condition[0]), condition[1]) + + if true_body is None: + if qubits is not None or clbits is not None: + raise CircuitError( + "When using 'if_test' as a context manager, you cannot pass qubits or clbits." + ) + # We can only allow jumps if we're in a loop block, but the default path (no scopes) + # also allows adding jumps to support the more verbose internal mode. + in_loop = bool(self._control_flow_scopes and self._control_flow_scopes[-1].allow_jumps) + return IfContext(self, condition, in_loop=in_loop, label=label) + elif qubits is None or clbits is None: + raise CircuitError("When using 'if_test' with a body, you must pass qubits and clbits.") + return self.append(IfElseOp(condition, true_body, None, label), qubits, clbits) def if_else( @@ -4120,6 +4448,23 @@ def if_else( ) -> InstructionSet: """Apply :class:`~qiskit.circuit.controlflow.IfElseOp`. + .. note:: + + This method does not have an associated context-manager form, because it is already + handled by the :meth:`.if_test` method. You can use the ``else`` part of that with + something such as:: + + from qiskit.circuit import QuantumCircuit, Qubit, Clbit + bits = [Qubit(), Qubit(), Clbit()] + qc = QuantumCircuit(bits) + qc.h(0) + qc.cx(0, 1) + qc.measure(0, 0) + with qc.if_test((bits[2], 0)) as else_: + qc.h(0) + with else_: + qc.x(0) + Args: condition: A condition to be evaluated at circuit runtime which, if true, will trigger the evaluation of ``true_body``. Can be @@ -4146,25 +4491,61 @@ def if_else( return self.append(IfElseOp(condition, true_body, false_body, label), qubits, clbits) def break_loop(self) -> InstructionSet: - """Apply :class:`~qiskit.circuit.controlflow.BreakLoop`. + """Apply :class:`~qiskit.circuit.controlflow.BreakLoopOp`. + + .. warning:: + + If you are using the context-manager "builder" forms of :meth:`.if_test`, + :meth:`.for_loop` or :meth:`.while_loop`, you can only call this method if you are + within a loop context, because otherwise the "resource width" of the operation cannot be + determined. This would quickly lead to invalid circuits, and so if you are trying to + construct a reusable loop body (without the context managers), you must also use the + non-context-manager form of :meth:`.if_test` and :meth:`.if_else`. Take care that the + :obj:`.BreakLoopOp` instruction must span all the resources of its containing loop, not + just the immediate scope. Returns: A handle to the instruction created. + + Raises: + 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 + from qiskit.circuit.controlflow.break_loop import BreakLoopOp, BreakLoopPlaceholder + if self._control_flow_scopes: + operation = BreakLoopPlaceholder() + return self.append(operation, *operation.placeholder_resources()) return self.append(BreakLoopOp(self.num_qubits, self.num_clbits), self.qubits, self.clbits) def continue_loop(self) -> InstructionSet: - """Apply :class:`~qiskit.circuit.controlflow.ContinueLoop`. + """Apply :class:`~qiskit.circuit.controlflow.ContinueLoopOp`. + + .. warning:: + + If you are using the context-manager "builder" forms of :meth:`.if_test`, + :meth:`.for_loop` or :meth:`.while_loop`, you can only call this method if you are + within a loop context, because otherwise the "resource width" of the operation cannot be + determined. This would quickly lead to invalid circuits, and so if you are trying to + construct a reusable loop body (without the context managers), you must also use the + non-context-manager form of :meth:`.if_test` and :meth:`.if_else`. Take care that the + :obj:`.ContinueLoopOp` instruction must span all the resources of its containing loop, + not just the immediate scope. Returns: A handle to the instruction created. + + Raises: + 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 + from qiskit.circuit.controlflow.continue_loop import ContinueLoopOp, ContinueLoopPlaceholder + if self._control_flow_scopes: + operation = ContinueLoopPlaceholder() + return self.append(operation, *operation.placeholder_resources()) return self.append( ContinueLoopOp(self.num_qubits, self.num_clbits), self.qubits, self.clbits ) diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index 63e35281f2ca..5bdc6e5b8b21 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -559,12 +559,12 @@ class ForLoopStatement(Statement): def __init__( self, - parameter: Identifier, indexset: Union[Identifier, IndexSet, Range], + parameter: Identifier, body: ProgramBlock, ): - self.parameter = parameter self.indexset = indexset + self.parameter = parameter self.body = body diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index 0b71689d1fae..5642abab3e26 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -717,7 +717,7 @@ def build_for_loop( self, instruction: ForLoopOp, qubits: Iterable[Qubit], clbits: Iterable[Clbit] ) -> ast.ForLoopStatement: """Build a :obj:`.ForLoopOp` into a :obj:`.ast.ForLoopStatement`.""" - loop_parameter, indexset, loop_circuit = instruction.params + indexset, loop_parameter, loop_circuit = instruction.params if loop_parameter is None: # The loop parameter is implicitly declared by the ``for`` loop (see also # _infer_parameter_declaration), so it doesn't matter that we haven't declared this, @@ -744,7 +744,7 @@ def build_for_loop( self.push_scope(loop_circuit, qubits, clbits) body_ast = self.build_program_block(loop_circuit) self.pop_scope() - return ast.ForLoopStatement(loop_parameter_ast, indexset_ast, body_ast) + return ast.ForLoopStatement(indexset_ast, loop_parameter_ast, body_ast) def build_integer(self, value) -> ast.Integer: """Build an integer literal, raising a :obj:`.QASM3ExporterError` if the input is not @@ -865,8 +865,8 @@ def is_loop_variable(circuit, parameter): # at all the places it's used in the circuit. for instruction, index in circuit._parameter_table[parameter]: if isinstance(instruction, ForLoopOp): - # The parameters of ForLoopOp are (loop_parameter, indexset, body). - if index == 0: + # The parameters of ForLoopOp are (indexset, loop_parameter, body). + if index == 1: return True if isinstance(instruction, ControlFlowOp): if is_loop_variable(instruction.params[index], parameter): diff --git a/releasenotes/notes/control-flow-builder-interface-63910843f8bea5e0.yaml b/releasenotes/notes/control-flow-builder-interface-63910843f8bea5e0.yaml new file mode 100644 index 000000000000..b86fad3757c4 --- /dev/null +++ b/releasenotes/notes/control-flow-builder-interface-63910843f8bea5e0.yaml @@ -0,0 +1,30 @@ +--- +features: + - | + There is a new builder interface for control-flow operations on :obj:`.QuantumCircuit`, such as the new :obj:`.ForLoopOp`, :obj:`.IfElseOp`, and :obj:`.WhileLoopOp`. + The interface uses the same circuit methods, and they are overloaded so that if the ``body`` parameter is not given, they return a context manager. + Entering one of these context managers pushes a scope into the circuit, and captures all gate calls (and other scopes) and the resources these use, and builds up the relevant operation at the end. + For example, you can now do:: + + qc = QuantumCircuit(2, 2) + with qc.for_loop(range(5)) as i: + qc.rx(i * math.pi / 4, 0) + + This will produce a :obj:`.ForLoopOp` on ``qc``, which knows that qubit 0 is the only resource used within the loop body. + These context managers can be nested, and will correctly determine their widths. + You can use :meth:`.QuantumCircuit.break_loop` and :meth:`.QuantumCircuit.continue_loop` within a context, and it will expand to be the correct width for its containing loop, even if it is nested in further :meth:`.QuantumCircuit.if_test` blocks. + + The :meth:`~.QuantumCircuit.if_test` context manager provides a chained manager which, if desired, can be used to create an ``else`` block, such as by:: + + qreg = QuantumRegister(2) + creg = ClassicalRegister(2) + qc = QuantumCircuit(qreg, creg) + qc.h(0) + qc.cx(0, 1) + qc.measure(0, 0) + with qc.if_test((creg, 0)) as else_: + qc.x(1) + with else_: + qc.z(1) + + The manager will ensure that the ``if`` and ``else`` bodies are defined over the same set of resources. diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 648b1a725d28..c14c32e0a297 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -949,8 +949,35 @@ def test_compare_a_circuit_with_none(self): self.assertFalse(qc1 == qc2) -class TestCircuitBuilding(QiskitTestCase): - """QuantumCircuit tests.""" - - def test_append_dimension_mismatch(self): - """Test appending to incompatible wires.""" +class TestCircuitPrivateOperations(QiskitTestCase): + """Direct tests of some of the private methods of QuantumCircuit. These do not represent + functionality that we want to expose to users, but there are some cases where private methods + are used internally (similar to "protected" access in .NET or "friend" access in C++), and we + want to make sure they work in those cases.""" + + def test_previous_instruction_in_scope_failures(self): + """Test the failure paths of the peek and pop methods for retrieving the most recent + instruction in a scope.""" + test = QuantumCircuit(1, 1) + with self.assertRaisesRegex(CircuitError, r"This circuit contains no instructions\."): + test._peek_previous_instruction_in_scope() + with self.assertRaisesRegex(CircuitError, r"This circuit contains no instructions\."): + test._pop_previous_instruction_in_scope() + with test.for_loop(range(2)): + with self.assertRaisesRegex(CircuitError, r"This scope contains no instructions\."): + test._peek_previous_instruction_in_scope() + with self.assertRaisesRegex(CircuitError, r"This scope contains no instructions\."): + test._pop_previous_instruction_in_scope() + + def test_pop_previous_instruction_removes_parameters(self): + """Test that the private "pop instruction" method removes parameters from the parameter + table if that instruction is the only instance.""" + x, y = Parameter("x"), Parameter("y") + test = QuantumCircuit(1, 1) + test.rx(y, 0) + last_instructions = test.u(x, y, 0, 0) + self.assertEqual({x, y}, set(test.parameters)) + + instruction, _, _ = test._pop_previous_instruction_in_scope() + self.assertEqual(list(last_instructions), [instruction]) + self.assertEqual({y}, set(test.parameters)) diff --git a/test/python/circuit/test_circuit_qasm3.py b/test/python/circuit/test_circuit_qasm3.py index f1b304d55bc8..84c592cda353 100644 --- a/test/python/circuit/test_circuit_qasm3.py +++ b/test/python/circuit/test_circuit_qasm3.py @@ -15,8 +15,8 @@ # We can't really help how long the lines output by the exporter are in some cases. # pylint: disable=line-too-long -import unittest from io import StringIO +import unittest import ddt @@ -848,7 +848,7 @@ def test_simple_for_loop(self): loop_body.continue_loop() qc = QuantumCircuit(2) - qc.for_loop(parameter, [0, 3, 4], loop_body, [1], []) + qc.for_loop([0, 3, 4], parameter, loop_body, [1], []) qc.x(0) qr_name = qc.qregs[0].name @@ -884,11 +884,11 @@ def test_nested_for_loop(self): outer_body.h(0) outer_body.rz(outer_parameter, 1) # Note we reverse the order of the bits here to test that this is traced. - outer_body.for_loop(inner_parameter, range(1, 5, 2), inner_body, [1, 0], []) + outer_body.for_loop(range(1, 5, 2), inner_parameter, inner_body, [1, 0], []) outer_body.continue_loop() qc = QuantumCircuit(2) - qc.for_loop(outer_parameter, range(4), outer_body, [0, 1], []) + qc.for_loop(range(4), outer_parameter, outer_body, [0, 1], []) qc.x(0) qr_name = qc.qregs[0].name @@ -916,9 +916,6 @@ def test_nested_for_loop(self): ) self.assertEqual(dumps(qc), expected_qasm) - # This test _should_ pass, but the inner "regular" parameter won't get declared in the global - # scope until gh-7280 is closed. "expectedFailure" seems to be ignored by stestr. - @unittest.expectedFailure def test_regular_parameter_in_nested_for_loop(self): """Test that a for loop nested inside another outputs the expected result, including defining parameters that are used in nested loop scopes.""" @@ -935,11 +932,11 @@ def test_regular_parameter_in_nested_for_loop(self): outer_body.h(0) outer_body.h(1) # Note we reverse the order of the bits here to test that this is traced. - outer_body.for_loop(inner_parameter, range(1, 5, 2), inner_body, [1, 0], []) + outer_body.for_loop(range(1, 5, 2), inner_parameter, inner_body, [1, 0], []) outer_body.continue_loop() qc = QuantumCircuit(2) - qc.for_loop(outer_parameter, range(4), outer_body, [0, 1], []) + qc.for_loop(range(4), outer_parameter, outer_body, [0, 1], []) qc.x(0) qr_name = qc.qregs[0].name @@ -975,7 +972,7 @@ def test_for_loop_with_no_parameter(self): loop_body.h(0) qc = QuantumCircuit(2) - qc.for_loop(None, [0, 3, 4], loop_body, [1], []) + qc.for_loop([0, 3, 4], None, loop_body, [1], []) qr_name = qc.qregs[0].name expected_qasm = "\n".join( @@ -1304,7 +1301,7 @@ def test_custom_gate_used_in_loop_scope(self): loop_body.append(custom_gate, [0]) qc = QuantumCircuit(1) - qc.for_loop(parameter_b, range(2), loop_body, [0], []) + qc.for_loop(range(2), parameter_b, loop_body, [0], []) expected_qasm = "\n".join( [ @@ -1593,7 +1590,7 @@ def test_disallow_for_loops_with_non_integers(self, indices): index sets.""" loop_body = QuantumCircuit() qc = QuantumCircuit(2, 2) - qc.for_loop(None, indices, loop_body, [], []) + qc.for_loop(indices, None, loop_body, [], []) exporter = Exporter() with self.assertRaisesRegex( QASM3ExporterError, r"The values in QASM 3 'for' loops must all be integers.*" diff --git a/test/python/circuit/test_control_flow.py b/test/python/circuit/test_control_flow.py index b3cd2c61aab8..fce065f43e25 100644 --- a/test/python/circuit/test_control_flow.py +++ b/test/python/circuit/test_control_flow.py @@ -12,6 +12,8 @@ """Test operations on control flow for dynamic QuantumCircuits.""" +import math + from ddt import ddt, data from qiskit.test import QiskitTestCase @@ -59,10 +61,10 @@ def test_while_loop_invalid_instantiation(self): body = QuantumCircuit(3, 1) condition = (body.clbits[0], True) - with self.assertRaisesRegex(CircuitError, r"condition argument as either a Tuple"): + with self.assertRaisesRegex(CircuitError, r"A classical condition should be a 2-tuple"): _ = WhileLoopOp(0, body) - with self.assertRaisesRegex(CircuitError, r"condition argument as either a Tuple"): + with self.assertRaisesRegex(CircuitError, r"A classical condition should be a 2-tuple"): _ = WhileLoopOp((Clbit(), None), body) with self.assertRaisesRegex(CircuitError, r"of type QuantumCircuit"): @@ -88,14 +90,14 @@ def test_for_loop_iterable_instantiation(self): body.rx(loop_parameter, 0) - op = ForLoopOp(loop_parameter, indexset, body) + op = ForLoopOp(indexset, loop_parameter, body) self.assertIsInstance(op, ControlFlowOp) self.assertIsInstance(op, Instruction) self.assertEqual(op.name, "for_loop") self.assertEqual(op.num_qubits, 3) self.assertEqual(op.num_clbits, 1) - self.assertEqual(op.params, [loop_parameter, tuple(range(0, 10, 2)), body]) + self.assertEqual(op.params, [tuple(range(0, 10, 2)), loop_parameter, body]) self.assertEqual(op.blocks, (body,)) def test_for_loop_range_instantiation(self): @@ -106,14 +108,14 @@ def test_for_loop_range_instantiation(self): body.rx(loop_parameter, 0) - op = ForLoopOp(loop_parameter, indexset, body) + op = ForLoopOp(indexset, loop_parameter, body) self.assertIsInstance(op, ControlFlowOp) self.assertIsInstance(op, Instruction) self.assertEqual(op.name, "for_loop") self.assertEqual(op.num_qubits, 3) self.assertEqual(op.num_clbits, 1) - self.assertEqual(op.params, [loop_parameter, indexset, body]) + self.assertEqual(op.params, [indexset, loop_parameter, body]) self.assertEqual(op.blocks, (body,)) def test_for_loop_no_parameter_instantiation(self): @@ -124,14 +126,14 @@ def test_for_loop_no_parameter_instantiation(self): body.rx(3.14, 0) - op = ForLoopOp(loop_parameter, indexset, body) + op = ForLoopOp(indexset, loop_parameter, body) self.assertIsInstance(op, ControlFlowOp) self.assertIsInstance(op, Instruction) self.assertEqual(op.name, "for_loop") self.assertEqual(op.num_qubits, 3) self.assertEqual(op.num_clbits, 1) - self.assertEqual(op.params, [loop_parameter, indexset, body]) + self.assertEqual(op.params, [indexset, loop_parameter, body]) self.assertEqual(op.blocks, (body,)) def test_for_loop_invalid_instantiation(self): @@ -143,10 +145,10 @@ def test_for_loop_invalid_instantiation(self): body.rx(loop_parameter, 0) with self.assertWarnsRegex(UserWarning, r"loop_parameter was not found"): - _ = ForLoopOp(Parameter("foo"), indexset, body) + _ = ForLoopOp(indexset, Parameter("foo"), body) with self.assertRaisesRegex(CircuitError, r"to be of type QuantumCircuit"): - _ = ForLoopOp(loop_parameter, indexset, RXGate(loop_parameter)) + _ = ForLoopOp(indexset, loop_parameter, RXGate(loop_parameter)) def test_for_loop_invalid_params_setter(self): """Verify we catch invalid param settings for ForLoopOp.""" @@ -156,22 +158,22 @@ def test_for_loop_invalid_params_setter(self): body.rx(loop_parameter, 0) - op = ForLoopOp(loop_parameter, indexset, body) + op = ForLoopOp(indexset, loop_parameter, body) with self.assertWarnsRegex(UserWarning, r"loop_parameter was not found"): - op.params = [Parameter("foo"), indexset, body] + op.params = [indexset, Parameter("foo"), body] with self.assertRaisesRegex(CircuitError, r"to be of type QuantumCircuit"): - op.params = [loop_parameter, indexset, RXGate(loop_parameter)] + op.params = [indexset, loop_parameter, RXGate(loop_parameter)] bad_body = QuantumCircuit(2, 1) with self.assertRaisesRegex( CircuitError, r"num_clbits different than that of the ForLoopOp" ): - op.params = [loop_parameter, indexset, bad_body] + op.params = [indexset, loop_parameter, bad_body] with self.assertRaisesRegex(CircuitError, r"to be either of type Parameter or None"): - _ = ForLoopOp("foo", indexset, body) + _ = ForLoopOp(indexset, "foo", body) @data( (Clbit(), True), @@ -220,10 +222,10 @@ def test_if_else_invalid_instantiation(self): true_body = QuantumCircuit(3, 1) false_body = QuantumCircuit(3, 1) - with self.assertRaisesRegex(CircuitError, r"condition argument as either a Tuple"): + with self.assertRaisesRegex(CircuitError, r"A classical condition should be a 2-tuple"): _ = IfElseOp(1, true_body, false_body) - with self.assertRaisesRegex(CircuitError, r"condition argument as either a Tuple"): + with self.assertRaisesRegex(CircuitError, r"A classical condition should be a 2-tuple"): _ = IfElseOp((1, 2), true_body, false_body) with self.assertRaisesRegex(CircuitError, r"true_body parameter of type QuantumCircuit"): @@ -343,13 +345,13 @@ def test_appending_for_loop_op(self): body.rx(loop_parameter, [0, 1, 2]) - op = ForLoopOp(loop_parameter, indexset, body) + op = ForLoopOp(indexset, loop_parameter, body) qc = QuantumCircuit(5, 2) qc.append(op, [1, 2, 3], [1]) self.assertEqual(qc.data[0][0].name, "for_loop") - self.assertEqual(qc.data[0][0].params, [loop_parameter, indexset, body]) + self.assertEqual(qc.data[0][0].params, [indexset, loop_parameter, body]) self.assertEqual(qc.data[0][1], qc.qubits[1:4]) self.assertEqual(qc.data[0][2], [qc.clbits[1]]) @@ -362,10 +364,10 @@ def test_quantumcircuit_for_loop_op(self): body.rx(loop_parameter, [0, 1, 2]) qc = QuantumCircuit(5, 2) - qc.for_loop(loop_parameter, indexset, body, [1, 2, 3], [1]) + qc.for_loop(indexset, loop_parameter, body, [1, 2, 3], [1]) self.assertEqual(qc.data[0][0].name, "for_loop") - self.assertEqual(qc.data[0][0].params, [loop_parameter, indexset, body]) + self.assertEqual(qc.data[0][0].params, [indexset, loop_parameter, body]) self.assertEqual(qc.data[0][1], qc.qubits[1:4]) self.assertEqual(qc.data[0][2], [qc.clbits[1]]) @@ -416,8 +418,8 @@ def test_quantumcircuit_if_else_op(self, condition): (ClassicalRegister(3, "test_creg"), 3), (ClassicalRegister(3, "test_creg"), True), ) - def test_quantumcircuit_if__op(self, condition): - """Verify we can append a IfElseOp to a QuantumCircuit via qc.if_.""" + def test_quantumcircuit_if_test_op(self, condition): + """Verify we can append a IfElseOp to a QuantumCircuit via qc.if_test.""" true_body = QuantumCircuit(3, 1) qc = QuantumCircuit(5, 2) @@ -503,3 +505,103 @@ def test_no_c_if_for_while_loop_if_else(self): qc.if_else((qc.clbits[0], False), body, body, [qc.qubits[0]], []).c_if( qc.clbits[0], True ) + + def test_nested_parameters_are_recognised(self): + """Verify that parameters added inside a control-flow operator get added to the outer + circuit table.""" + x, y = Parameter("x"), Parameter("y") + + with self.subTest("if/else"): + body1 = QuantumCircuit(1, 1) + body1.rx(x, 0) + body2 = QuantumCircuit(1, 1) + body2.rx(y, 0) + + main = QuantumCircuit(1, 1) + main.if_else((main.clbits[0], 0), body1, body2, [0], [0]) + self.assertEqual({x, y}, set(main.parameters)) + + with self.subTest("while"): + body = QuantumCircuit(1, 1) + body.rx(x, 0) + + main = QuantumCircuit(1, 1) + main.while_loop((main.clbits[0], 0), body, [0], [0]) + self.assertEqual({x}, set(main.parameters)) + + with self.subTest("for"): + body = QuantumCircuit(1, 1) + body.rx(x, 0) + + main = QuantumCircuit(1, 1) + main.for_loop(range(1), None, body, [0], [0]) + self.assertEqual({x}, set(main.parameters)) + + def test_nested_parameters_can_be_assigned(self): + """Verify that parameters added inside a control-flow operator can be assigned by calls to + the outer circuit.""" + x, y = Parameter("x"), Parameter("y") + + with self.subTest("if/else"): + body1 = QuantumCircuit(1, 1) + body1.rx(x, 0) + body2 = QuantumCircuit(1, 1) + body2.rx(y, 0) + + test = QuantumCircuit(1, 1) + test.if_else((test.clbits[0], 0), body1, body2, [0], [0]) + self.assertEqual({x, y}, set(test.parameters)) + assigned = test.assign_parameters({x: math.pi, y: 0.5 * math.pi}) + self.assertEqual(set(), set(assigned.parameters)) + + expected = QuantumCircuit(1, 1) + expected.if_else( + (expected.clbits[0], 0), + body1.assign_parameters({x: math.pi}), + body2.assign_parameters({y: 0.5 * math.pi}), + [0], + [0], + ) + + self.assertEqual(assigned, expected) + + with self.subTest("while"): + body = QuantumCircuit(1, 1) + body.rx(x, 0) + + test = QuantumCircuit(1, 1) + test.while_loop((test.clbits[0], 0), body, [0], [0]) + self.assertEqual({x}, set(test.parameters)) + assigned = test.assign_parameters({x: math.pi}) + self.assertEqual(set(), set(assigned.parameters)) + + expected = QuantumCircuit(1, 1) + expected.while_loop( + (expected.clbits[0], 0), + body.assign_parameters({x: math.pi}), + [0], + [0], + ) + + self.assertEqual(assigned, expected) + + with self.subTest("for"): + body = QuantumCircuit(1, 1) + body.rx(x, 0) + + test = QuantumCircuit(1, 1) + test.for_loop(range(1), None, body, [0], [0]) + self.assertEqual({x}, set(test.parameters)) + assigned = test.assign_parameters({x: math.pi}) + self.assertEqual(set(), set(assigned.parameters)) + + expected = QuantumCircuit(1, 1) + expected.for_loop( + range(1), + None, + body.assign_parameters({x: math.pi}), + [0], + [0], + ) + + self.assertEqual(assigned, expected) diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py new file mode 100644 index 000000000000..8972a4ce1b4f --- /dev/null +++ b/test/python/circuit/test_control_flow_builders.py @@ -0,0 +1,2200 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Test operations on the builder interfaces for control flow in dynamic QuantumCircuits.""" + +import math + +import ddt + +from qiskit.circuit import ( + ClassicalRegister, + Clbit, + Measure, + Parameter, + QuantumCircuit, + QuantumRegister, + Qubit, +) +from qiskit.circuit.controlflow import ForLoopOp, IfElseOp, WhileLoopOp, BreakLoopOp, ContinueLoopOp +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 + + +class SentinelException(Exception): + """An exception that we know was raised deliberately.""" + + +@ddt.ddt +class TestControlFlowBuilders(QiskitTestCase): + """Test that the control-flow builder interfaces work, and manage resources correctly.""" + + def assertCircuitsEquivalent(self, a, b): + """Assert that two circuits (``a`` and ``b``) contain all the same qubits and clbits, and + then have the same instructions in order, recursing into nested control-flow constructs. + + Relying on ``QuantumCircuit.__eq__`` doesn't work reliably in all cases here, because we + don't care about the order that the builder interface chooses for resources in the inner + blocks. This order is non-deterministic, because internally it uses sets for efficiency, + and the order of iteration through a set is dependent on the hash seed. Instead, we just + need to be a bit more explicit about what we care about. This isn't a full method for + comparing if two circuits are equivalent, but for the restricted cases used in these tests, + where we deliberately construct the expected result to be equal in the good case, it should + test all that is needed. + """ + + self.assertIsInstance(a, QuantumCircuit) + self.assertIsInstance(b, QuantumCircuit) + + # For our purposes here, we don't care about the order bits were added. + self.assertEqual(set(a.qubits), set(b.qubits)) + self.assertEqual(set(a.clbits), set(b.clbits)) + self.assertEqual(len(a.data), len(b.data)) + + for (a_op, a_qubits, a_clbits), (b_op, b_qubits, b_clbits) in zip(a.data, b.data): + # Make sure that the operations are the same. + self.assertEqual(type(a_op), type(b_op)) + self.assertEqual(hasattr(a_op, "condition"), hasattr(b_op, "condition")) + if hasattr(a_op, "condition") and not isinstance(a_op, (IfElseOp, WhileLoopOp)): + self.assertEqual(a_op.condition, b_op.condition) + self.assertEqual(hasattr(a_op, "label"), hasattr(b_op, "label")) + if hasattr(a_op, "condition"): + self.assertEqual(a_op.label, b_op.label) + # If it's a block op, we don't care what order the resources are specified in. + if isinstance(a_op, WhileLoopOp): + self.assertEqual(set(a_qubits), set(b_qubits)) + self.assertEqual(set(a_clbits), set(b_clbits)) + self.assertEqual(a_op.condition, b_op.condition) + self.assertCircuitsEquivalent(a_op.blocks[0], b_op.blocks[0]) + elif isinstance(a_op, ForLoopOp): + self.assertEqual(set(a_qubits), set(b_qubits)) + self.assertEqual(set(a_clbits), set(b_clbits)) + a_indexset, a_loop_parameter, a_body = a_op.params + b_indexset, b_loop_parameter, b_body = b_op.params + self.assertEqual(a_loop_parameter is None, b_loop_parameter is None) + self.assertEqual(a_indexset, b_indexset) + if a_loop_parameter is not None: + a_body = a_body.assign_parameters({a_loop_parameter: b_loop_parameter}) + self.assertCircuitsEquivalent(a_body, b_body) + elif isinstance(a_op, IfElseOp): + self.assertEqual(set(a_qubits), set(b_qubits)) + self.assertEqual(set(a_clbits), set(b_clbits)) + self.assertEqual(a_op.condition, b_op.condition) + self.assertEqual(len(a_op.blocks), len(b_op.blocks)) + self.assertCircuitsEquivalent(a_op.blocks[0], b_op.blocks[0]) + if len(a_op.blocks) > 1: + self.assertCircuitsEquivalent(a_op.blocks[1], b_op.blocks[1]) + elif isinstance(a_op, (BreakLoopOp, ContinueLoopOp)): + self.assertEqual(set(a_qubits), set(b_qubits)) + self.assertEqual(set(a_clbits), set(b_clbits)) + else: + # For any other op, we care that the resources are the same, and in the same order, + # but we don't mind what sort of iterable they're contained in. + self.assertEqual(tuple(a_qubits), tuple(b_qubits)) + self.assertEqual(tuple(a_clbits), tuple(b_clbits)) + self.assertEqual(a_op, b_op) + + def test_if_simple(self): + """Test a simple if statement builds correctly, in the midst of other instructions.""" + qubits = [Qubit(), Qubit()] + clbits = [Clbit(), Clbit()] + + test = QuantumCircuit(qubits, clbits) + test.h(0) + test.measure(0, 0) + with test.if_test((clbits[0], 0)): + test.x(0) + test.h(0) + test.measure(0, 1) + with test.if_test((clbits[1], 0)): + test.h(1) + test.cx(1, 0) + + if_true0 = QuantumCircuit([qubits[0], clbits[0]]) + if_true0.x(qubits[0]) + + if_true1 = QuantumCircuit([qubits[0], qubits[1], clbits[1]]) + if_true1.h(qubits[1]) + if_true1.cx(qubits[1], qubits[0]) + + expected = QuantumCircuit(qubits, clbits) + expected.h(qubits[0]) + expected.measure(qubits[0], clbits[0]) + expected.if_test((clbits[0], 0), if_true0, [qubits[0]], [clbits[0]]) + expected.h(qubits[0]) + expected.measure(qubits[0], clbits[1]) + expected.if_test((clbits[1], 0), if_true1, [qubits[0], qubits[1]], [clbits[1]]) + + self.assertCircuitsEquivalent(test, expected) + + def test_if_register(self): + """Test a simple if statement builds correctly, when using a register as the condition. + This requires the builder to unpack all the bits from the register to use as resources.""" + qr = QuantumRegister(2) + cr = ClassicalRegister(2) + + test = QuantumCircuit(qr, cr) + test.measure(qr, cr) + with test.if_test((cr, 0)): + test.x(0) + + if_true0 = QuantumCircuit([qr[0]], cr) + if_true0.x(qr[0]) + + expected = QuantumCircuit(qr, cr) + expected.measure(qr, cr) + expected.if_test((cr, 0), if_true0, [qr[0]], [cr[:]]) + + self.assertCircuitsEquivalent(test, expected) + + def test_if_else_simple(self): + """Test a simple if/else statement builds correctly, in the midst of other instructions. + This test has paired if and else blocks the same natural width.""" + qubits = [Qubit(), Qubit()] + clbits = [Clbit(), Clbit()] + + test = QuantumCircuit(qubits, clbits) + test.h(0) + test.measure(0, 0) + with test.if_test((clbits[0], 0)) as else_: + test.x(0) + with else_: + test.z(0) + test.h(0) + test.measure(0, 1) + with test.if_test((clbits[1], 0)) as else_: + test.h(1) + test.cx(1, 0) + with else_: + test.h(0) + test.h(1) + + # Both the if and else blocks in this circuit are the same natural width to begin with. + if_true0 = QuantumCircuit([qubits[0], clbits[0]]) + if_true0.x(qubits[0]) + if_false0 = QuantumCircuit([qubits[0], clbits[0]]) + if_false0.z(qubits[0]) + + if_true1 = QuantumCircuit([qubits[0], qubits[1], clbits[1]]) + if_true1.h(qubits[1]) + if_true1.cx(qubits[1], qubits[0]) + if_false1 = QuantumCircuit([qubits[0], qubits[1], clbits[1]]) + if_false1.h(qubits[0]) + if_false1.h(qubits[1]) + + expected = QuantumCircuit(qubits, clbits) + expected.h(qubits[0]) + expected.measure(qubits[0], clbits[0]) + expected.if_else((clbits[0], 0), if_true0, if_false0, [qubits[0]], [clbits[0]]) + expected.h(qubits[0]) + expected.measure(qubits[0], clbits[1]) + expected.if_else((clbits[1], 0), if_true1, if_false1, [qubits[0], qubits[1]], [clbits[1]]) + + self.assertCircuitsEquivalent(test, expected) + + def test_if_else_resources_expand_true_superset_false(self): + """Test that the resources of the if and else bodies come out correctly if the true body + needs a superset of the resources of the false body.""" + qubits = [Qubit(), Qubit()] + clbits = [Clbit(), Clbit()] + + test = QuantumCircuit(qubits, clbits) + test.h(0) + test.measure(0, 0) + with test.if_test((clbits[0], 0)) as else_: + test.x(0) + test.measure(1, 1) + with else_: + test.z(0) + + if_true0 = QuantumCircuit(qubits, clbits) + if_true0.x(qubits[0]) + if_true0.measure(qubits[1], clbits[1]) + # The false body doesn't actually use qubits[1] or clbits[1], but it still needs to contain + # them so the bodies match. + if_false0 = QuantumCircuit(qubits, clbits) + if_false0.z(qubits[0]) + + expected = QuantumCircuit(qubits, clbits) + expected.h(qubits[0]) + expected.measure(qubits[0], clbits[0]) + expected.if_else((clbits[0], 0), if_true0, if_false0, qubits, clbits) + + self.assertCircuitsEquivalent(test, expected) + + def test_if_else_resources_expand_false_superset_true(self): + """Test that the resources of the if and else bodies come out correctly if the false body + needs a superset of the resources of the true body. This requires that the manager + correctly adds resources to the true body after it has been created.""" + qubits = [Qubit(), Qubit()] + clbits = [Clbit(), Clbit()] + + test = QuantumCircuit(qubits, clbits) + test.h(0) + test.measure(0, 0) + with test.if_test((clbits[0], 0)) as else_: + test.x(0) + with else_: + test.z(0) + test.measure(1, 1) + + # The true body doesn't actually use qubits[1] or clbits[1], but it still needs to contain + # them so the bodies match. + if_true0 = QuantumCircuit(qubits, clbits) + if_true0.x(qubits[0]) + if_false0 = QuantumCircuit(qubits, clbits) + if_false0.z(qubits[0]) + if_false0.measure(qubits[1], clbits[1]) + + expected = QuantumCircuit(qubits, clbits) + expected.h(qubits[0]) + expected.measure(qubits[0], clbits[0]) + expected.if_else((clbits[0], 0), if_true0, if_false0, qubits, clbits) + + self.assertCircuitsEquivalent(test, expected) + + def test_if_else_resources_expand_true_false_symmetric_difference(self): + """Test that the resources of the if and else bodies come out correctly if the sets of + resources for the true body and the false body have some overlap, but neither is a subset of + the other. This tests that the flow of resources from block to block is simultaneously + bidirectional.""" + qubits = [Qubit(), Qubit()] + clbits = [Clbit(), Clbit()] + + test = QuantumCircuit(qubits, clbits) + test.h(0) + test.measure(0, 0) + with test.if_test((clbits[0], 0)) as else_: + test.x(0) + with else_: + test.z(1) + + # The true body doesn't use qubits[1] and the false body doesn't use qubits[0]. + if_true0 = QuantumCircuit(qubits, [clbits[0]]) + if_true0.x(qubits[0]) + if_false0 = QuantumCircuit(qubits, [clbits[0]]) + if_false0.z(qubits[1]) + + expected = QuantumCircuit(qubits, clbits) + expected.h(qubits[0]) + expected.measure(qubits[0], clbits[0]) + expected.if_else((clbits[0], 0), if_true0, if_false0, qubits, [clbits[0]]) + + self.assertCircuitsEquivalent(test, expected) + + def test_if_else_empty_branches(self): + """Test that the context managers can cope with a body being empty.""" + qubits = [Qubit()] + clbits = [Clbit()] + + cond = (clbits[0], 0) + test = QuantumCircuit(qubits, clbits) + # Sole empty if. + with test.if_test(cond): + pass + # Normal if with an empty else body. + with test.if_test(cond) as else_: + test.x(0) + with else_: + pass + # Empty if with a normal else body. + with test.if_test(cond) as else_: + pass + with else_: + test.x(0) + # Both empty. + with test.if_test(cond) as else_: + pass + with else_: + pass + + empty_with_qubit = QuantumCircuit([qubits[0], clbits[0]]) + empty = QuantumCircuit([clbits[0]]) + only_x = QuantumCircuit([qubits[0], clbits[0]]) + only_x.x(qubits[0]) + + expected = QuantumCircuit(qubits, clbits) + expected.if_test(cond, empty, [], [clbits[0]]) + expected.if_else(cond, only_x, empty_with_qubit, [qubits[0]], [clbits[0]]) + expected.if_else(cond, empty_with_qubit, only_x, [qubits[0]], [clbits[0]]) + expected.if_else(cond, empty, empty, [], [clbits[0]]) + + self.assertCircuitsEquivalent(test, expected) + + def test_if_else_nested(self): + """Test that the if and else context managers can be nested, and don't interfere with each + other.""" + qubits = [Qubit(), Qubit(), Qubit()] + clbits = [Clbit(), Clbit(), Clbit()] + + outer_cond = (clbits[0], 0) + inner_cond = (clbits[2], 1) + + with self.subTest("if (if) else"): + test = QuantumCircuit(qubits, clbits) + with test.if_test(outer_cond) as else_: + with test.if_test(inner_cond): + test.h(0) + with else_: + test.h(1) + + inner_true = QuantumCircuit([qubits[0], clbits[2]]) + inner_true.h(qubits[0]) + + outer_true = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[2]]) + outer_true.if_test(inner_cond, inner_true, [qubits[0]], [clbits[2]]) + outer_false = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[2]]) + outer_false.h(qubits[1]) + + expected = QuantumCircuit(qubits, clbits) + expected.if_else( + outer_cond, outer_true, outer_false, [qubits[0], qubits[1]], [clbits[0], clbits[2]] + ) + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("if (if else) else"): + test = QuantumCircuit(qubits, clbits) + with test.if_test(outer_cond) as outer_else: + with test.if_test(inner_cond) as inner_else: + test.h(0) + with inner_else: + test.h(2) + with outer_else: + test.h(1) + + inner_true = QuantumCircuit([qubits[0], qubits[2], clbits[2]]) + inner_true.h(qubits[0]) + inner_false = QuantumCircuit([qubits[0], qubits[2], clbits[2]]) + inner_false.h(qubits[2]) + + outer_true = QuantumCircuit(qubits, [clbits[0], clbits[2]]) + outer_true.if_else( + inner_cond, inner_true, inner_false, [qubits[0], qubits[2]], [clbits[2]] + ) + outer_false = QuantumCircuit(qubits, [clbits[0], clbits[2]]) + outer_false.h(qubits[1]) + + expected = QuantumCircuit(qubits, clbits) + expected.if_else(outer_cond, outer_true, outer_false, qubits, [clbits[0], clbits[2]]) + self.assertCircuitsEquivalent(test, expected) + + def test_break_continue_expand_to_match_arguments_simple(self): + """Test that ``break`` and ``continue`` statements expand to include all resources in the + containing loop for simple cases with unconditional breaks.""" + qubits = [Qubit(), Qubit()] + clbits = [Clbit(), Clbit()] + + with self.subTest("for"): + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + test.break_loop() + test.h(0) + test.continue_loop() + test.measure(1, 0) + + body = QuantumCircuit(qubits, [clbits[0]]) + body.break_loop() + body.h(qubits[0]) + body.continue_loop() + body.measure(qubits[1], clbits[0]) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop(range(2), None, body, qubits, [clbits[0]]) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("while"): + cond = (clbits[0], 0) + test = QuantumCircuit(qubits, clbits) + with test.while_loop(cond): + test.break_loop() + test.h(0) + test.continue_loop() + test.measure(1, 0) + + body = QuantumCircuit(qubits, [clbits[0]]) + body.break_loop() + body.h(qubits[0]) + body.continue_loop() + body.measure(qubits[1], clbits[0]) + + expected = QuantumCircuit(qubits, clbits) + expected.while_loop(cond, body, qubits, [clbits[0]]) + + self.assertCircuitsEquivalent(test, expected) + + @ddt.data(QuantumCircuit.break_loop, QuantumCircuit.continue_loop) + def test_break_continue_accept_c_if(self, loop_operation): + """Test that ``break`` and ``continue`` statements accept :meth:`.Instruction.c_if` calls, + and that these propagate through correctly.""" + qubits = [Qubit(), Qubit()] + clbits = [Clbit(), Clbit()] + + with self.subTest("for"): + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + test.h(0) + loop_operation(test).c_if(1, 0) + + body = QuantumCircuit([qubits[0]], [clbits[1]]) + body.h(qubits[0]) + loop_operation(body).c_if(clbits[1], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop(range(2), None, body, [qubits[0]], [clbits[1]]) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("while"): + cond = (clbits[0], 0) + test = QuantumCircuit(qubits, clbits) + with test.while_loop(cond): + test.h(0) + loop_operation(test).c_if(1, 0) + + body = QuantumCircuit([qubits[0]], clbits) + body.h(qubits[0]) + loop_operation(body).c_if(clbits[1], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.while_loop(cond, body, [qubits[0]], clbits) + + self.assertCircuitsEquivalent(test, expected) + + @ddt.data(QuantumCircuit.break_loop, QuantumCircuit.continue_loop) + def test_break_continue_only_expand_to_nearest_loop(self, loop_operation): + """Test that a ``break`` or ``continue`` nested in more than one loop only expands as far as + the inner loop scope, not further.""" + qubits = [Qubit(), Qubit(), Qubit()] + clbits = [Clbit(), Clbit(), Clbit()] + cond_inner = (clbits[0], 0) + cond_outer = (clbits[1], 0) + + with self.subTest("for for"): + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + test.measure(1, 1) + with test.for_loop(range(2)): + test.h(0) + loop_operation(test) + loop_operation(test) + + inner_body = QuantumCircuit([qubits[0]]) + inner_body.h(qubits[0]) + loop_operation(inner_body) + + outer_body = QuantumCircuit([qubits[0], qubits[1], clbits[1]]) + outer_body.measure(qubits[1], clbits[1]) + outer_body.for_loop(range(2), None, inner_body, [qubits[0]], []) + loop_operation(outer_body) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop(range(2), None, outer_body, [qubits[0], qubits[1]], [clbits[1]]) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("for while"): + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + test.measure(1, 1) + with test.while_loop(cond_inner): + test.h(0) + loop_operation(test) + loop_operation(test) + + inner_body = QuantumCircuit([qubits[0], clbits[0]]) + inner_body.h(qubits[0]) + loop_operation(inner_body) + + outer_body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1]]) + outer_body.measure(qubits[1], clbits[1]) + outer_body.while_loop(cond_inner, inner_body, [qubits[0]], [clbits[0]]) + loop_operation(outer_body) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop( + range(2), None, outer_body, [qubits[0], qubits[1]], [clbits[0], clbits[1]] + ) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("while for"): + test = QuantumCircuit(qubits, clbits) + with test.while_loop(cond_outer): + test.measure(1, 1) + with test.for_loop(range(2)): + test.h(0) + loop_operation(test) + loop_operation(test) + + inner_body = QuantumCircuit([qubits[0]]) + inner_body.h(qubits[0]) + loop_operation(inner_body) + + outer_body = QuantumCircuit([qubits[0], qubits[1], clbits[1]]) + outer_body.measure(qubits[1], clbits[1]) + outer_body.for_loop(range(2), None, inner_body, [qubits[0]], []) + loop_operation(outer_body) + + expected = QuantumCircuit(qubits, clbits) + expected.while_loop(cond_outer, outer_body, [qubits[0], qubits[1]], [clbits[1]]) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("while while"): + test = QuantumCircuit(qubits, clbits) + with test.while_loop(cond_outer): + test.measure(1, 1) + with test.while_loop(cond_inner): + test.h(0) + loop_operation(test) + loop_operation(test) + + inner_body = QuantumCircuit([qubits[0], clbits[0]]) + inner_body.h(qubits[0]) + loop_operation(inner_body) + + outer_body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1]]) + outer_body.measure(qubits[1], clbits[1]) + outer_body.while_loop(cond_inner, inner_body, [qubits[0]], [clbits[0]]) + loop_operation(outer_body) + + expected = QuantumCircuit(qubits, clbits) + expected.while_loop( + cond_outer, outer_body, [qubits[0], qubits[1]], [clbits[0], clbits[1]] + ) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("for (for, for)"): + # This test is specifically to check that multiple inner loops with different numbers of + # variables to each ``break`` still expand correctly. + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + test.measure(1, 1) + with test.for_loop(range(2)): + test.h(0) + loop_operation(test) + with test.for_loop(range(2)): + test.h(2) + loop_operation(test) + loop_operation(test) + + inner_body1 = QuantumCircuit([qubits[0]]) + inner_body1.h(qubits[0]) + loop_operation(inner_body1) + + inner_body2 = QuantumCircuit([qubits[2]]) + inner_body2.h(qubits[2]) + loop_operation(inner_body2) + + outer_body = QuantumCircuit([qubits[0], qubits[1], qubits[2], clbits[1]]) + outer_body.measure(qubits[1], clbits[1]) + outer_body.for_loop(range(2), None, inner_body1, [qubits[0]], []) + outer_body.for_loop(range(2), None, inner_body2, [qubits[2]], []) + loop_operation(outer_body) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop(range(2), None, outer_body, qubits, [clbits[1]]) + + self.assertCircuitsEquivalent(test, expected) + + @ddt.data(QuantumCircuit.break_loop, QuantumCircuit.continue_loop) + def test_break_continue_nested_in_if(self, loop_operation): + """Test that ``break`` and ``continue`` work correctly when inside an ``if`` block within a + loop. This includes testing that multiple different ``if`` statements with and without + ``break`` expand to the correct number of arguments. + + This is a very important case; it requires that the :obj:`.IfElseOp` is not built until the + loop builds, and that the width expands to include everything that the loop knows about, not + just the inner context. We test both ``if`` and ``if/else`` paths separately, because the + chaining of the context managers allows lots of possibilities for super weird edge cases. + + There are several tests that build up in complexity to aid debugging if something goes + wrong; the aim is that there will be more information available depending on which of the + subtests pass and which fail. + """ + qubits = [Qubit(), Qubit(), Qubit()] + clbits = [Clbit(), Clbit(), Clbit(), Clbit()] + cond_inner = (clbits[0], 0) + cond_outer = (clbits[1], 0) + + with self.subTest("for/if"): + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + with test.if_test(cond_inner): + loop_operation(test) + # The second empty `if` is to test that only blocks that _need_ to expand to be the + # full width of the loop do so. + with test.if_test(cond_inner): + pass + test.h(0).c_if(2, 0) + + true_body1 = QuantumCircuit([qubits[0], clbits[0], clbits[2]]) + loop_operation(true_body1) + + true_body2 = QuantumCircuit([clbits[0]]) + + loop_body = QuantumCircuit([qubits[0], clbits[0], clbits[2]]) + loop_body.if_test(cond_inner, true_body1, [qubits[0]], [clbits[0], clbits[2]]) + loop_body.if_test(cond_inner, true_body2, [], [clbits[0]]) + loop_body.h(qubits[0]).c_if(clbits[2], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop(range(2), None, loop_body, [qubits[0]], [clbits[0], clbits[2]]) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("for/else"): + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + with test.if_test(cond_inner) as else_: + test.h(1) + with else_: + loop_operation(test) + with test.if_test(cond_inner) as else_: + pass + with else_: + pass + test.h(0).c_if(2, 0) + + true_body1 = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[2]]) + true_body1.h(qubits[1]) + false_body1 = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[2]]) + loop_operation(false_body1) + + true_body2 = QuantumCircuit([clbits[0]]) + false_body2 = QuantumCircuit([clbits[0]]) + + loop_body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[2]]) + loop_body.if_else( + cond_inner, true_body1, false_body1, [qubits[0], qubits[1]], [clbits[0], clbits[2]] + ) + loop_body.if_else(cond_inner, true_body2, false_body2, [], [clbits[0]]) + loop_body.h(qubits[0]).c_if(clbits[2], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop( + range(2), None, loop_body, [qubits[0], qubits[1]], [clbits[0], clbits[2]] + ) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("while/if"): + test = QuantumCircuit(qubits, clbits) + with test.while_loop(cond_outer): + with test.if_test(cond_inner): + loop_operation(test) + with test.if_test(cond_inner): + pass + test.h(0).c_if(2, 0) + + true_body1 = QuantumCircuit([qubits[0], clbits[0], clbits[1], clbits[2]]) + loop_operation(true_body1) + + true_body2 = QuantumCircuit([clbits[0]]) + + loop_body = QuantumCircuit([qubits[0], clbits[0], clbits[1], clbits[2]]) + loop_body.if_test( + cond_inner, true_body1, [qubits[0]], [clbits[0], clbits[1], clbits[2]] + ) + loop_body.if_test(cond_inner, true_body2, [], [clbits[0]]) + loop_body.h(qubits[0]).c_if(clbits[2], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.while_loop( + cond_outer, loop_body, [qubits[0]], [clbits[0], clbits[1], clbits[2]] + ) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("while/else"): + test = QuantumCircuit(qubits, clbits) + with test.while_loop(cond_outer): + with test.if_test(cond_inner) as else_: + test.h(1) + with else_: + loop_operation(test) + with test.if_test(cond_inner) as else_: + pass + with else_: + pass + test.h(0).c_if(2, 0) + + true_body1 = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1], clbits[2]]) + true_body1.h(qubits[1]) + false_body1 = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1], clbits[2]]) + loop_operation(false_body1) + + true_body2 = QuantumCircuit([clbits[0]]) + false_body2 = QuantumCircuit([clbits[0]]) + + loop_body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1], clbits[2]]) + loop_body.if_else( + cond_inner, + true_body1, + false_body1, + [qubits[0], qubits[1]], + [clbits[0], clbits[1], clbits[2]], + ) + loop_body.if_else(cond_inner, true_body2, false_body2, [], [clbits[0]]) + loop_body.h(qubits[0]).c_if(clbits[2], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.while_loop( + cond_outer, loop_body, [qubits[0], qubits[1]], [clbits[0], clbits[1], clbits[2]] + ) + + self.assertCircuitsEquivalent(test, expected) + + @ddt.data(QuantumCircuit.break_loop, QuantumCircuit.continue_loop) + def test_break_continue_deeply_nested_in_if(self, loop_operation): + """Test that ``break`` and ``continue`` work correctly when inside more than one ``if`` + block within a loop. This includes testing that multiple different ``if`` statements with + and without ``break`` expand to the correct number of arguments. + + These are the deepest tests, hitting all parts of the deferred builder scopes. We test both + ``if`` and ``if/else`` paths at various levels of the scoping to try and account for as many + weird edge cases with the deferred behaviour as possible. We try to make sure, particularly + in the most complicated examples, that there are resources added before and after every + single scope, to try and catch all possibilities of where resources may be missed. + + There are several tests that build up in complexity to aid debugging if something goes + wrong; the aim is that there will be more information available depending on which of the + subtests pass and which fail. + """ + # These are deliberately more than is absolutely needed so we can detect if extra resources + # are being erroneously included as well. + qubits = [Qubit() for _ in [None] * 20] + clbits = [Clbit() for _ in [None] * 20] + cond_inner = (clbits[0], 0) + cond_outer = (clbits[1], 0) + cond_loop = (clbits[2], 0) + + with self.subTest("for/if/if"): + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + # outer true 1 + with test.if_test(cond_outer): + # inner true 1 + with test.if_test(cond_inner): + loop_operation(test) + # inner true 2 + with test.if_test(cond_inner): + test.h(0).c_if(3, 0) + test.h(1).c_if(4, 0) + # outer true 2 + with test.if_test(cond_outer): + test.h(2).c_if(5, 0) + test.h(3).c_if(6, 0) + + inner_true_body1 = QuantumCircuit(qubits[:4], clbits[:2], clbits[3:7]) + loop_operation(inner_true_body1) + + inner_true_body2 = QuantumCircuit([qubits[0], clbits[0], clbits[3]]) + inner_true_body2.h(qubits[0]).c_if(clbits[3], 0) + + outer_true_body1 = QuantumCircuit(qubits[:4], clbits[:2], clbits[3:7]) + outer_true_body1.if_test( + cond_inner, inner_true_body1, qubits[:4], clbits[:2] + clbits[3:7] + ) + outer_true_body1.if_test( + cond_inner, inner_true_body2, [qubits[0]], [clbits[0], clbits[3]] + ) + outer_true_body1.h(qubits[1]).c_if(clbits[4], 0) + + outer_true_body2 = QuantumCircuit([qubits[2], clbits[1], clbits[5]]) + outer_true_body2.h(qubits[2]).c_if(clbits[5], 0) + + loop_body = QuantumCircuit(qubits[:4], clbits[:2] + clbits[3:7]) + loop_body.if_test(cond_outer, outer_true_body1, qubits[:4], clbits[:2] + clbits[3:7]) + loop_body.if_test(cond_outer, outer_true_body2, [qubits[2]], [clbits[1], clbits[5]]) + loop_body.h(qubits[3]).c_if(clbits[6], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop(range(2), None, loop_body, qubits[:4], clbits[:2] + clbits[3:7]) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("for/if/else"): + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + # outer 1 + with test.if_test(cond_outer): + # inner 1 + with test.if_test(cond_inner) as inner1_else: + test.h(0).c_if(3, 0) + with inner1_else: + loop_operation(test).c_if(4, 0) + # inner 2 + with test.if_test(cond_inner) as inner2_else: + test.h(1).c_if(5, 0) + with inner2_else: + test.h(2).c_if(6, 0) + test.h(3).c_if(7, 0) + # outer 2 + with test.if_test(cond_outer) as outer2_else: + test.h(4).c_if(8, 0) + with outer2_else: + test.h(5).c_if(9, 0) + test.h(6).c_if(10, 0) + + inner1_true = QuantumCircuit(qubits[:7], clbits[:2], clbits[3:11]) + inner1_true.h(qubits[0]).c_if(clbits[3], 0) + inner1_false = QuantumCircuit(qubits[:7], clbits[:2], clbits[3:11]) + loop_operation(inner1_false).c_if(clbits[4], 0) + + inner2_true = QuantumCircuit([qubits[1], qubits[2], clbits[0], clbits[5], clbits[6]]) + inner2_true.h(qubits[1]).c_if(clbits[5], 0) + inner2_false = QuantumCircuit([qubits[1], qubits[2], clbits[0], clbits[5], clbits[6]]) + inner2_false.h(qubits[2]).c_if(clbits[6], 0) + + outer1_true = QuantumCircuit(qubits[:7], clbits[:2], clbits[3:11]) + outer1_true.if_else( + cond_inner, inner1_true, inner1_false, qubits[:7], clbits[:2] + clbits[3:11] + ) + outer1_true.if_else( + cond_inner, + inner2_true, + inner2_false, + qubits[1:3], + [clbits[0], clbits[5], clbits[6]], + ) + outer1_true.h(qubits[3]).c_if(clbits[7], 0) + + outer2_true = QuantumCircuit([qubits[4], qubits[5], clbits[1], clbits[8], clbits[9]]) + outer2_true.h(qubits[4]).c_if(clbits[8], 0) + outer2_false = QuantumCircuit([qubits[4], qubits[5], clbits[1], clbits[8], clbits[9]]) + outer2_false.h(qubits[5]).c_if(clbits[9], 0) + + loop_body = QuantumCircuit(qubits[:7], clbits[:2], clbits[3:11]) + loop_body.if_test(cond_outer, outer1_true, qubits[:7], clbits[:2] + clbits[3:11]) + loop_body.if_else( + cond_outer, + outer2_true, + outer2_false, + qubits[4:6], + [clbits[1], clbits[8], clbits[9]], + ) + loop_body.h(qubits[6]).c_if(clbits[10], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop(range(2), None, loop_body, qubits[:7], clbits[:2] + clbits[3:11]) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("for/else/else"): + # Look on my works, ye Mighty, and despair! + + # (but also hopefully this is less hubristic pretension and more a useful stress test) + + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + test.h(0).c_if(3, 0) + + # outer 1 + with test.if_test(cond_outer) as outer1_else: + test.h(1).c_if(4, 0) + with outer1_else: + test.h(2).c_if(5, 0) + + # outer 2 (nesting the inner condition in the 'if') + with test.if_test(cond_outer) as outer2_else: + test.h(3).c_if(6, 0) + + # inner 21 + with test.if_test(cond_inner) as inner21_else: + loop_operation(test) + with inner21_else: + test.h(4).c_if(7, 0) + + # inner 22 + with test.if_test(cond_inner) as inner22_else: + test.h(5).c_if(8, 0) + with inner22_else: + loop_operation(test) + + test.h(6).c_if(9, 0) + with outer2_else: + test.h(7).c_if(10, 0) + + # inner 23 + with test.if_test(cond_inner) as inner23_else: + test.h(8).c_if(11, 0) + with inner23_else: + test.h(9).c_if(12, 0) + + # outer 3 (nesting the inner condition in an 'else' branch) + with test.if_test(cond_outer) as outer3_else: + test.h(10).c_if(13, 0) + with outer3_else: + test.h(11).c_if(14, 0) + + # inner 31 + with test.if_test(cond_inner) as inner31_else: + loop_operation(test) + with inner31_else: + test.h(12).c_if(15, 0) + + # inner 32 + with test.if_test(cond_inner) as inner32_else: + test.h(13).c_if(16, 0) + with inner32_else: + loop_operation(test) + + # inner 33 + with test.if_test(cond_inner) as inner33_else: + test.h(14).c_if(17, 0) + with inner33_else: + test.h(15).c_if(18, 0) + + test.h(16).c_if(19, 0) + # End of test "for" loop. + + # No `clbits[2]` here because that's only used in `cond_loop`, for while loops. + loop_qubits = qubits[:17] + loop_clbits = clbits[:2] + clbits[3:20] + loop_bits = loop_qubits + loop_clbits + + outer1_true = QuantumCircuit([qubits[1], qubits[2], clbits[1], clbits[4], clbits[5]]) + outer1_true.h(qubits[1]).c_if(clbits[4], 0) + outer1_false = QuantumCircuit([qubits[1], qubits[2], clbits[1], clbits[4], clbits[5]]) + outer1_false.h(qubits[2]).c_if(clbits[5], 0) + + inner21_true = QuantumCircuit(loop_bits) + loop_operation(inner21_true) + inner21_false = QuantumCircuit(loop_bits) + inner21_false.h(qubits[4]).c_if(clbits[7], 0) + + inner22_true = QuantumCircuit(loop_bits) + inner22_true.h(qubits[5]).c_if(clbits[8], 0) + inner22_false = QuantumCircuit(loop_bits) + loop_operation(inner22_false) + + inner23_true = QuantumCircuit(qubits[8:10], [clbits[0], clbits[11], clbits[12]]) + inner23_true.h(qubits[8]).c_if(clbits[11], 0) + inner23_false = QuantumCircuit(qubits[8:10], [clbits[0], clbits[11], clbits[12]]) + inner23_false.h(qubits[9]).c_if(clbits[12], 0) + + outer2_true = QuantumCircuit(loop_bits) + outer2_true.h(qubits[3]).c_if(clbits[6], 0) + outer2_true.if_else(cond_inner, inner21_true, inner21_false, loop_qubits, loop_clbits) + outer2_true.if_else(cond_inner, inner22_true, inner22_false, loop_qubits, loop_clbits) + outer2_true.h(qubits[6]).c_if(clbits[9], 0) + outer2_false = QuantumCircuit(loop_bits) + outer2_false.h(qubits[7]).c_if(clbits[10], 0) + outer2_false.if_else( + cond_inner, + inner23_true, + inner23_false, + [qubits[8], qubits[9]], + [clbits[0], clbits[11], clbits[12]], + ) + + inner31_true = QuantumCircuit(loop_bits) + loop_operation(inner31_true) + inner31_false = QuantumCircuit(loop_bits) + inner31_false.h(qubits[12]).c_if(clbits[15], 0) + + inner32_true = QuantumCircuit(loop_bits) + inner32_true.h(qubits[13]).c_if(clbits[16], 0) + inner32_false = QuantumCircuit(loop_bits) + loop_operation(inner32_false) + + inner33_true = QuantumCircuit(qubits[14:16], [clbits[0], clbits[17], clbits[18]]) + inner33_true.h(qubits[14]).c_if(clbits[17], 0) + inner33_false = QuantumCircuit(qubits[14:16], [clbits[0], clbits[17], clbits[18]]) + inner33_false.h(qubits[15]).c_if(clbits[18], 0) + + outer3_true = QuantumCircuit(loop_bits) + outer3_true.h(qubits[10]).c_if(clbits[13], 0) + outer3_false = QuantumCircuit(loop_bits) + outer3_false.h(qubits[11]).c_if(clbits[14], 0) + outer3_false.if_else(cond_inner, inner31_true, inner31_false, loop_qubits, loop_clbits) + outer3_false.if_else(cond_inner, inner32_true, inner32_false, loop_qubits, loop_clbits) + outer3_false.if_else( + cond_inner, + inner33_true, + inner33_false, + qubits[14:16], + [clbits[0], clbits[17], clbits[18]], + ) + + loop_body = QuantumCircuit(loop_bits) + loop_body.h(qubits[0]).c_if(clbits[3], 0) + loop_body.if_else( + cond_outer, + outer1_true, + outer1_false, + qubits[1:3], + [clbits[1], clbits[4], clbits[5]], + ) + loop_body.if_else(cond_outer, outer2_true, outer2_false, loop_qubits, loop_clbits) + loop_body.if_else(cond_outer, outer3_true, outer3_false, loop_qubits, loop_clbits) + loop_body.h(qubits[16]).c_if(clbits[19], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop(range(2), None, loop_body, loop_qubits, loop_clbits) + + self.assertCircuitsEquivalent(test, expected) + + # And now we repeat everything for "while" loops... Trying to parameterize the test over + # 'for/while' mostly just ends up in it being a bit illegible, because so many parameters + # vary in the explicit construction form. These tests are just copies of the above tests, + # but with `while_loop(cond_loop)` instead of `for_loop(range(2))`, and the corresponding + # clbit ranges updated to include `clbits[2]` from the condition. + + with self.subTest("while/if/if"): + test = QuantumCircuit(qubits, clbits) + with test.while_loop(cond_loop): + # outer true 1 + with test.if_test(cond_outer): + # inner true 1 + with test.if_test(cond_inner): + loop_operation(test) + # inner true 2 + with test.if_test(cond_inner): + test.h(0).c_if(3, 0) + test.h(1).c_if(4, 0) + # outer true 2 + with test.if_test(cond_outer): + test.h(2).c_if(5, 0) + test.h(3).c_if(6, 0) + + inner_true_body1 = QuantumCircuit(qubits[:4], clbits[:7]) + loop_operation(inner_true_body1) + + inner_true_body2 = QuantumCircuit([qubits[0], clbits[0], clbits[3]]) + inner_true_body2.h(qubits[0]).c_if(clbits[3], 0) + + outer_true_body1 = QuantumCircuit(qubits[:4], clbits[:7]) + outer_true_body1.if_test(cond_inner, inner_true_body1, qubits[:4], clbits[:7]) + outer_true_body1.if_test( + cond_inner, inner_true_body2, [qubits[0]], [clbits[0], clbits[3]] + ) + outer_true_body1.h(qubits[1]).c_if(clbits[4], 0) + + outer_true_body2 = QuantumCircuit([qubits[2], clbits[1], clbits[5]]) + outer_true_body2.h(qubits[2]).c_if(clbits[5], 0) + + loop_body = QuantumCircuit(qubits[:4], clbits[:7]) + loop_body.if_test(cond_outer, outer_true_body1, qubits[:4], clbits[:7]) + loop_body.if_test(cond_outer, outer_true_body2, [qubits[2]], [clbits[1], clbits[5]]) + loop_body.h(qubits[3]).c_if(clbits[6], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.while_loop(cond_loop, loop_body, qubits[:4], clbits[:7]) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("while/if/else"): + test = QuantumCircuit(qubits, clbits) + with test.while_loop(cond_loop): + # outer 1 + with test.if_test(cond_outer): + # inner 1 + with test.if_test(cond_inner) as inner1_else: + test.h(0).c_if(3, 0) + with inner1_else: + loop_operation(test).c_if(4, 0) + # inner 2 + with test.if_test(cond_inner) as inner2_else: + test.h(1).c_if(5, 0) + with inner2_else: + test.h(2).c_if(6, 0) + test.h(3).c_if(7, 0) + # outer 2 + with test.if_test(cond_outer) as outer2_else: + test.h(4).c_if(8, 0) + with outer2_else: + test.h(5).c_if(9, 0) + test.h(6).c_if(10, 0) + + inner1_true = QuantumCircuit(qubits[:7], clbits[:11]) + inner1_true.h(qubits[0]).c_if(clbits[3], 0) + inner1_false = QuantumCircuit(qubits[:7], clbits[:11]) + loop_operation(inner1_false).c_if(clbits[4], 0) + + inner2_true = QuantumCircuit([qubits[1], qubits[2], clbits[0], clbits[5], clbits[6]]) + inner2_true.h(qubits[1]).c_if(clbits[5], 0) + inner2_false = QuantumCircuit([qubits[1], qubits[2], clbits[0], clbits[5], clbits[6]]) + inner2_false.h(qubits[2]).c_if(clbits[6], 0) + + outer1_true = QuantumCircuit(qubits[:7], clbits[:11]) + outer1_true.if_else(cond_inner, inner1_true, inner1_false, qubits[:7], clbits[:11]) + outer1_true.if_else( + cond_inner, + inner2_true, + inner2_false, + qubits[1:3], + [clbits[0], clbits[5], clbits[6]], + ) + outer1_true.h(qubits[3]).c_if(clbits[7], 0) + + outer2_true = QuantumCircuit([qubits[4], qubits[5], clbits[1], clbits[8], clbits[9]]) + outer2_true.h(qubits[4]).c_if(clbits[8], 0) + outer2_false = QuantumCircuit([qubits[4], qubits[5], clbits[1], clbits[8], clbits[9]]) + outer2_false.h(qubits[5]).c_if(clbits[9], 0) + + loop_body = QuantumCircuit(qubits[:7], clbits[:11]) + loop_body.if_test(cond_outer, outer1_true, qubits[:7], clbits[:11]) + loop_body.if_else( + cond_outer, + outer2_true, + outer2_false, + qubits[4:6], + [clbits[1], clbits[8], clbits[9]], + ) + loop_body.h(qubits[6]).c_if(clbits[10], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.while_loop(cond_loop, loop_body, qubits[:7], clbits[:11]) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("while/else/else"): + test = QuantumCircuit(qubits, clbits) + with test.while_loop(cond_loop): + test.h(0).c_if(3, 0) + + # outer 1 + with test.if_test(cond_outer) as outer1_else: + test.h(1).c_if(4, 0) + with outer1_else: + test.h(2).c_if(5, 0) + + # outer 2 (nesting the inner condition in the 'if') + with test.if_test(cond_outer) as outer2_else: + test.h(3).c_if(6, 0) + + # inner 21 + with test.if_test(cond_inner) as inner21_else: + loop_operation(test) + with inner21_else: + test.h(4).c_if(7, 0) + + # inner 22 + with test.if_test(cond_inner) as inner22_else: + test.h(5).c_if(8, 0) + with inner22_else: + loop_operation(test) + + test.h(6).c_if(9, 0) + with outer2_else: + test.h(7).c_if(10, 0) + + # inner 23 + with test.if_test(cond_inner) as inner23_else: + test.h(8).c_if(11, 0) + with inner23_else: + test.h(9).c_if(12, 0) + + # outer 3 (nesting the inner condition in an 'else' branch) + with test.if_test(cond_outer) as outer3_else: + test.h(10).c_if(13, 0) + with outer3_else: + test.h(11).c_if(14, 0) + + # inner 31 + with test.if_test(cond_inner) as inner31_else: + loop_operation(test) + with inner31_else: + test.h(12).c_if(15, 0) + + # inner 32 + with test.if_test(cond_inner) as inner32_else: + test.h(13).c_if(16, 0) + with inner32_else: + loop_operation(test) + + # inner 33 + with test.if_test(cond_inner) as inner33_else: + test.h(14).c_if(17, 0) + with inner33_else: + test.h(15).c_if(18, 0) + + test.h(16).c_if(19, 0) + # End of test "for" loop. + + # No `clbits[2]` here because that's only used in `cond_loop`, for while loops. + loop_qubits = qubits[:17] + loop_clbits = clbits[:20] + loop_bits = loop_qubits + loop_clbits + + outer1_true = QuantumCircuit([qubits[1], qubits[2], clbits[1], clbits[4], clbits[5]]) + outer1_true.h(qubits[1]).c_if(clbits[4], 0) + outer1_false = QuantumCircuit([qubits[1], qubits[2], clbits[1], clbits[4], clbits[5]]) + outer1_false.h(qubits[2]).c_if(clbits[5], 0) + + inner21_true = QuantumCircuit(loop_bits) + loop_operation(inner21_true) + inner21_false = QuantumCircuit(loop_bits) + inner21_false.h(qubits[4]).c_if(clbits[7], 0) + + inner22_true = QuantumCircuit(loop_bits) + inner22_true.h(qubits[5]).c_if(clbits[8], 0) + inner22_false = QuantumCircuit(loop_bits) + loop_operation(inner22_false) + + inner23_true = QuantumCircuit(qubits[8:10], [clbits[0], clbits[11], clbits[12]]) + inner23_true.h(qubits[8]).c_if(clbits[11], 0) + inner23_false = QuantumCircuit(qubits[8:10], [clbits[0], clbits[11], clbits[12]]) + inner23_false.h(qubits[9]).c_if(clbits[12], 0) + + outer2_true = QuantumCircuit(loop_bits) + outer2_true.h(qubits[3]).c_if(clbits[6], 0) + outer2_true.if_else(cond_inner, inner21_true, inner21_false, loop_qubits, loop_clbits) + outer2_true.if_else(cond_inner, inner22_true, inner22_false, loop_qubits, loop_clbits) + outer2_true.h(qubits[6]).c_if(clbits[9], 0) + outer2_false = QuantumCircuit(loop_bits) + outer2_false.h(qubits[7]).c_if(clbits[10], 0) + outer2_false.if_else( + cond_inner, + inner23_true, + inner23_false, + [qubits[8], qubits[9]], + [clbits[0], clbits[11], clbits[12]], + ) + + inner31_true = QuantumCircuit(loop_bits) + loop_operation(inner31_true) + inner31_false = QuantumCircuit(loop_bits) + inner31_false.h(qubits[12]).c_if(clbits[15], 0) + + inner32_true = QuantumCircuit(loop_bits) + inner32_true.h(qubits[13]).c_if(clbits[16], 0) + inner32_false = QuantumCircuit(loop_bits) + loop_operation(inner32_false) + + inner33_true = QuantumCircuit(qubits[14:16], [clbits[0], clbits[17], clbits[18]]) + inner33_true.h(qubits[14]).c_if(clbits[17], 0) + inner33_false = QuantumCircuit(qubits[14:16], [clbits[0], clbits[17], clbits[18]]) + inner33_false.h(qubits[15]).c_if(clbits[18], 0) + + outer3_true = QuantumCircuit(loop_bits) + outer3_true.h(qubits[10]).c_if(clbits[13], 0) + outer3_false = QuantumCircuit(loop_bits) + outer3_false.h(qubits[11]).c_if(clbits[14], 0) + outer3_false.if_else(cond_inner, inner31_true, inner31_false, loop_qubits, loop_clbits) + outer3_false.if_else(cond_inner, inner32_true, inner32_false, loop_qubits, loop_clbits) + outer3_false.if_else( + cond_inner, + inner33_true, + inner33_false, + qubits[14:16], + [clbits[0], clbits[17], clbits[18]], + ) + + loop_body = QuantumCircuit(loop_bits) + loop_body.h(qubits[0]).c_if(clbits[3], 0) + loop_body.if_else( + cond_outer, + outer1_true, + outer1_false, + qubits[1:3], + [clbits[1], clbits[4], clbits[5]], + ) + loop_body.if_else(cond_outer, outer2_true, outer2_false, loop_qubits, loop_clbits) + loop_body.if_else(cond_outer, outer3_true, outer3_false, loop_qubits, loop_clbits) + loop_body.h(qubits[16]).c_if(clbits[19], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.while_loop(cond_loop, loop_body, loop_qubits, loop_clbits) + + self.assertCircuitsEquivalent(test, expected) + + def test_for_handles_iterables_correctly(self): + """Test that the ``indexset`` in ``for`` loops is handled the way we expect. In general, + this means all iterables are consumed into a tuple on first access, except for ``range`` + which is passed through as-is.""" + bits = [Qubit(), Clbit()] + expected_indices = (3, 9, 1) + + with self.subTest("list"): + test = QuantumCircuit(bits) + with test.for_loop(list(expected_indices)): + pass + instruction, _, _ = test.data[-1] + self.assertIsInstance(instruction, ForLoopOp) + indices, _, _ = instruction.params + self.assertEqual(indices, expected_indices) + + with self.subTest("tuple"): + test = QuantumCircuit(bits) + with test.for_loop(tuple(expected_indices)): + pass + instruction, _, _ = test.data[-1] + self.assertIsInstance(instruction, ForLoopOp) + indices, _, _ = instruction.params + self.assertEqual(indices, expected_indices) + + with self.subTest("consumable"): + + def consumable(): + yield from expected_indices + + test = QuantumCircuit(bits) + with test.for_loop(consumable()): + pass + instruction, _, _ = test.data[-1] + self.assertIsInstance(instruction, ForLoopOp) + indices, _, _ = instruction.params + self.assertEqual(indices, expected_indices) + + with self.subTest("range"): + range_indices = range(0, 8, 2) + + test = QuantumCircuit(bits) + with test.for_loop(range_indices): + pass + instruction, _, _ = test.data[-1] + self.assertIsInstance(instruction, ForLoopOp) + indices, _, _ = instruction.params + self.assertEqual(indices, range_indices) + + def test_for_returns_a_given_parameter(self): + """Test that the ``for``-loop manager returns the parameter that we gave it.""" + parameter = Parameter("x") + test = QuantumCircuit(1, 1) + with test.for_loop((0, 1), parameter) as test_parameter: + pass + self.assertIs(test_parameter, parameter) + + def test_for_binds_parameter_to_op(self): + """Test that the ``for`` manager binds a parameter to the resulting :obj:`.ForLoopOp` if a + user-generated one is given, or if a generated parameter is used. Generated parameters that + are not used should not be bound.""" + parameter = Parameter("x") + + with self.subTest("passed and used"): + circuit = QuantumCircuit(1, 1) + with circuit.for_loop((0, 0.5 * math.pi), parameter) as received_parameter: + circuit.rx(received_parameter, 0) + self.assertIs(parameter, received_parameter) + instruction = circuit.data[-1][0] + self.assertIsInstance(instruction, ForLoopOp) + _, bound_parameter, _ = instruction.params + self.assertIs(bound_parameter, parameter) + + with self.subTest("passed and unused"): + circuit = QuantumCircuit(1, 1) + with circuit.for_loop((0, 0.5 * math.pi), parameter) as received_parameter: + circuit.x(0) + self.assertIs(parameter, received_parameter) + instruction = circuit.data[-1][0] + self.assertIsInstance(instruction, ForLoopOp) + _, bound_parameter, _ = instruction.params + self.assertIs(parameter, received_parameter) + + with self.subTest("generated and used"): + circuit = QuantumCircuit(1, 1) + with circuit.for_loop((0, 0.5 * math.pi)) as received_parameter: + circuit.rx(received_parameter, 0) + self.assertIsInstance(received_parameter, Parameter) + instruction = circuit.data[-1][0] + self.assertIsInstance(instruction, ForLoopOp) + _, bound_parameter, _ = instruction.params + self.assertIs(bound_parameter, received_parameter) + + with self.subTest("generated and used in deferred-build if"): + circuit = QuantumCircuit(1, 1) + with circuit.for_loop((0, 0.5 * math.pi)) as received_parameter: + with circuit.if_test((0, 0)): + circuit.rx(received_parameter, 0) + circuit.break_loop() + self.assertIsInstance(received_parameter, Parameter) + instruction = circuit.data[-1][0] + self.assertIsInstance(instruction, ForLoopOp) + _, bound_parameter, _ = instruction.params + self.assertIs(bound_parameter, received_parameter) + + with self.subTest("generated and used in deferred-build else"): + circuit = QuantumCircuit(1, 1) + with circuit.for_loop((0, 0.5 * math.pi)) as received_parameter: + with circuit.if_test((0, 0)) as else_: + pass + with else_: + circuit.rx(received_parameter, 0) + circuit.break_loop() + self.assertIsInstance(received_parameter, Parameter) + instruction = circuit.data[-1][0] + self.assertIsInstance(instruction, ForLoopOp) + _, bound_parameter, _ = instruction.params + self.assertIs(bound_parameter, received_parameter) + + def test_for_does_not_bind_generated_parameter_if_unused(self): + """Test that the ``for`` manager does not bind a generated parameter into the resulting + :obj:`.ForLoopOp` if the parameter was not used.""" + test = QuantumCircuit(1, 1) + with test.for_loop(range(2)) as generated_parameter: + pass + instruction = test.data[-1][0] + self.assertIsInstance(instruction, ForLoopOp) + _, bound_parameter, _ = instruction.params + self.assertIsNot(generated_parameter, None) + self.assertIs(bound_parameter, None) + + def test_for_allocates_parameters(self): + """Test that the ``for``-loop manager allocates a parameter if it is given ``None``, and + that it always allocates new parameters.""" + test = QuantumCircuit(1, 1) + with test.for_loop(range(2)) as outer_parameter: + with test.for_loop(range(2)) as inner_parameter: + pass + with test.for_loop(range(2)) as final_parameter: + pass + self.assertIsInstance(outer_parameter, Parameter) + self.assertIsInstance(inner_parameter, Parameter) + self.assertIsInstance(final_parameter, Parameter) + self.assertNotEqual(outer_parameter, inner_parameter) + self.assertNotEqual(outer_parameter, final_parameter) + self.assertNotEqual(inner_parameter, final_parameter) + + def test_access_of_resources_from_direct_append(self): + """Test that direct calls to :obj:`.QuantumCircuit.append` within a builder block still + collect all the relevant resources.""" + qubits = [Qubit(), Qubit()] + clbits = [Clbit(), Clbit()] + cond = (clbits[0], 0) + + with self.subTest("if"): + test = QuantumCircuit(qubits, clbits) + with test.if_test(cond): + test.append(Measure(), [qubits[1]], [clbits[1]]) + + true_body = QuantumCircuit([qubits[1]], clbits) + true_body.measure(qubits[1], clbits[1]) + expected = QuantumCircuit(qubits, clbits) + expected.if_test(cond, true_body, [qubits[1]], clbits) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("else"): + test = QuantumCircuit(qubits, clbits) + with test.if_test(cond) as else_: + pass + with else_: + test.append(Measure(), [qubits[1]], [clbits[1]]) + + true_body = QuantumCircuit([qubits[1]], clbits) + false_body = QuantumCircuit([qubits[1]], clbits) + false_body.measure(qubits[1], clbits[1]) + expected = QuantumCircuit(qubits, clbits) + expected.if_else(cond, true_body, false_body, [qubits[1]], clbits) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("for"): + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + test.append(Measure(), [qubits[1]], [clbits[1]]) + + body = QuantumCircuit([qubits[1]], [clbits[1]]) + body.measure(qubits[1], clbits[1]) + expected = QuantumCircuit(qubits, clbits) + expected.for_loop(range(2), None, body, [qubits[1]], [clbits[1]]) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("while"): + test = QuantumCircuit(qubits, clbits) + with test.while_loop(cond): + test.append(Measure(), [qubits[1]], [clbits[1]]) + + body = QuantumCircuit([qubits[1]], clbits) + body.measure(qubits[1], clbits[1]) + expected = QuantumCircuit(qubits, clbits) + expected.while_loop(cond, body, [qubits[1]], clbits) + + self.assertCircuitsEquivalent(test, expected) + + def test_access_of_clbit_from_c_if(self): + """Test that resources added from a call to :meth:`.InstructionSet.c_if` propagate through + the context managers correctly.""" + qubits = [Qubit(), Qubit()] + clbits = [Clbit(), Clbit()] + bits = qubits + clbits + cond = (clbits[0], 0) + + with self.subTest("if"): + test = QuantumCircuit(bits) + with test.if_test(cond): + test.h(0).c_if(1, 0) + + body = QuantumCircuit([qubits[0]], clbits) + body.h(qubits[0]).c_if(clbits[1], 0) + expected = QuantumCircuit(bits) + expected.if_test(cond, body, [qubits[0]], clbits) + + with self.subTest("else"): + test = QuantumCircuit(bits) + with test.if_test(cond) as else_: + pass + with else_: + test.h(0).c_if(1, 0) + + true_body = QuantumCircuit([qubits[0]], clbits) + false_body = QuantumCircuit([qubits[0]], clbits) + false_body.h(qubits[0]).c_if(clbits[1], 0) + expected = QuantumCircuit(bits) + expected.if_else(cond, true_body, false_body, [qubits[0]], clbits) + + with self.subTest("for"): + test = QuantumCircuit(bits) + with test.for_loop(range(2)): + test.h(0).c_if(1, 0) + + body = QuantumCircuit([qubits[0]], clbits) + body.h(qubits[0]).c_if(clbits[1], 0) + expected = QuantumCircuit(bits) + expected.for_loop(range(2), None, body, [qubits[0]], clbits) + + with self.subTest("while"): + test = QuantumCircuit(bits) + with test.while_loop(cond): + test.h(0).c_if(1, 0) + + body = QuantumCircuit([qubits[0]], clbits) + body.h(qubits[0]).c_if(clbits[1], 0) + expected = QuantumCircuit(bits) + expected.while_loop(cond, body, [qubits[0]], clbits) + + with self.subTest("if inside for"): + test = QuantumCircuit(bits) + with test.for_loop(range(2)): + with test.if_test(cond): + test.h(0).c_if(1, 0) + + true_body = QuantumCircuit([qubits[0]], clbits) + true_body.h(qubits[0]).c_if(clbits[1], 0) + body = QuantumCircuit([qubits[0]], clbits) + body.if_test(cond, body, [qubits[0]], clbits) + expected = QuantumCircuit(bits) + expected.for_loop(range(2), None, body, [qubits[0]], clbits) + + def test_access_of_classicalregister_from_c_if(self): + """Test that resources added from a call to :meth:`.InstructionSet.c_if` propagate through + the context managers correctly.""" + qubits = [Qubit(), Qubit()] + creg = ClassicalRegister(2) + clbits = [Clbit()] + all_clbits = list(clbits) + list(creg) + cond = (clbits[0], 0) + + with self.subTest("if"): + test = QuantumCircuit(qubits, clbits, creg) + with test.if_test(cond): + test.h(0).c_if(creg, 0) + + body = QuantumCircuit([qubits[0]], clbits, creg) + body.h(qubits[0]).c_if(creg, 0) + expected = QuantumCircuit(qubits, clbits, creg) + expected.if_test(cond, body, [qubits[0]], all_clbits) + + with self.subTest("else"): + test = QuantumCircuit(qubits, clbits, creg) + with test.if_test(cond) as else_: + pass + with else_: + test.h(0).c_if(1, 0) + + true_body = QuantumCircuit([qubits[0]], clbits, creg) + false_body = QuantumCircuit([qubits[0]], clbits, creg) + false_body.h(qubits[0]).c_if(creg, 0) + expected = QuantumCircuit(qubits, clbits, creg) + expected.if_else(cond, true_body, false_body, [qubits[0]], all_clbits) + + with self.subTest("for"): + test = QuantumCircuit(qubits, clbits, creg) + with test.for_loop(range(2)): + test.h(0).c_if(1, 0) + + body = QuantumCircuit([qubits[0]], clbits, creg) + body.h(qubits[0]).c_if(creg, 0) + expected = QuantumCircuit(qubits, clbits, creg) + expected.for_loop(range(2), None, body, [qubits[0]], all_clbits) + + with self.subTest("while"): + test = QuantumCircuit(qubits, clbits, creg) + with test.while_loop(cond): + test.h(0).c_if(creg, 0) + + body = QuantumCircuit([qubits[0]], clbits, creg) + body.h(qubits[0]).c_if(creg, 0) + expected = QuantumCircuit(qubits, clbits, creg) + expected.while_loop(cond, body, [qubits[0]], all_clbits) + + with self.subTest("if inside for"): + test = QuantumCircuit(qubits, clbits, creg) + with test.for_loop(range(2)): + with test.if_test(cond): + test.h(0).c_if(creg, 0) + + true_body = QuantumCircuit([qubits[0]], clbits, creg) + true_body.h(qubits[0]).c_if(creg, 0) + body = QuantumCircuit([qubits[0]], clbits, creg) + body.if_test(cond, body, [qubits[0]], all_clbits) + expected = QuantumCircuit(qubits, clbits, creg) + expected.for_loop(range(2), None, body, [qubits[0]], all_clbits) + + def test_accept_broadcast_gates(self): + """Test that the context managers accept gates that are broadcast during their addition to + the scope.""" + qubits = [Qubit(), Qubit(), Qubit()] + clbits = [Clbit(), Clbit(), Clbit()] + cond = (clbits[0], 0) + + with self.subTest("if"): + test = QuantumCircuit(qubits, clbits) + with test.if_test(cond): + test.measure([0, 1], [0, 1]) + + body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1]]) + body.measure(qubits[0], clbits[0]) + body.measure(qubits[1], clbits[1]) + + expected = QuantumCircuit(qubits, clbits) + expected.if_test(cond, body, [qubits[0], qubits[1]], [clbits[0], clbits[1]]) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("else"): + test = QuantumCircuit(qubits, clbits) + with test.if_test(cond) as else_: + pass + with else_: + test.measure([0, 1], [0, 1]) + + true_body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1]]) + false_body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1]]) + false_body.measure(qubits[0], clbits[0]) + false_body.measure(qubits[1], clbits[1]) + + expected = QuantumCircuit(qubits, clbits) + expected.if_else( + cond, true_body, false_body, [qubits[0], qubits[1]], [clbits[0], clbits[1]] + ) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("for"): + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + test.measure([0, 1], [0, 1]) + + body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1]]) + body.measure(qubits[0], clbits[0]) + body.measure(qubits[1], clbits[1]) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop(range(2), None, body, [qubits[0], qubits[1]], [clbits[0], clbits[1]]) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("while"): + test = QuantumCircuit(qubits, clbits) + with test.while_loop(cond): + test.measure([0, 1], [0, 1]) + + body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1]]) + body.measure(qubits[0], clbits[0]) + body.measure(qubits[1], clbits[1]) + + expected = QuantumCircuit(qubits, clbits) + expected.while_loop(cond, body, [qubits[0], qubits[1]], [clbits[0], clbits[1]]) + + self.assertCircuitsEquivalent(test, expected) + + with self.subTest("if inside for"): + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + with test.if_test(cond): + test.measure([0, 1], [0, 1]) + + true_body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1]]) + true_body.measure(qubits[0], clbits[0]) + true_body.measure(qubits[1], clbits[1]) + + for_body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1]]) + for_body.if_test(cond, true_body, [qubits[0], qubits[1]], [clbits[0], clbits[1]]) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop( + range(2), None, for_body, [qubits[0], qubits[1]], [clbits[0], clbits[1]] + ) + + self.assertCircuitsEquivalent(test, expected) + + def test_labels_propagated_to_instruction(self): + """Test that labels given to the circuit-builder interface are passed through.""" + bits = [Qubit(), Clbit()] + cond = (bits[1], 0) + label = "sentinel_label" + + with self.subTest("if"): + test = QuantumCircuit(bits) + with test.if_test(cond, label=label): + pass + instruction = test.data[-1][0] + self.assertIsInstance(instruction, IfElseOp) + self.assertEqual(instruction.label, label) + + with self.subTest("if else"): + test = QuantumCircuit(bits) + with test.if_test(cond, label=label) as else_: + pass + with else_: + pass + instruction = test.data[-1][0] + self.assertIsInstance(instruction, IfElseOp) + self.assertEqual(instruction.label, label) + + with self.subTest("for"): + test = QuantumCircuit(bits) + with test.for_loop(range(2), label=label): + pass + instruction = test.data[-1][0] + self.assertIsInstance(instruction, ForLoopOp) + self.assertEqual(instruction.label, label) + + with self.subTest("while"): + test = QuantumCircuit(bits) + with test.while_loop(cond, label=label): + pass + instruction = test.data[-1][0] + self.assertIsInstance(instruction, WhileLoopOp) + self.assertEqual(instruction.label, label) + + # The tests of 'if' and 'else' inside 'for' are to ensure we're hitting the paths where the + # 'if' scope is built lazily at the completion of the 'for'. + with self.subTest("if inside for"): + test = QuantumCircuit(bits) + with test.for_loop(range(2)): + with test.if_test(cond, label=label): + # Use break to ensure that we're triggering the lazy building of 'if'. + test.break_loop() + + instruction = test.data[-1][0].blocks[0].data[-1][0] + self.assertIsInstance(instruction, IfElseOp) + self.assertEqual(instruction.label, label) + + with self.subTest("else inside for"): + test = QuantumCircuit(bits) + with test.for_loop(range(2)): + with test.if_test(cond, label=label) as else_: + # Use break to ensure that we're triggering the lazy building of 'if'. + test.break_loop() + with else_: + test.break_loop() + + instruction = test.data[-1][0].blocks[0].data[-1][0] + self.assertIsInstance(instruction, IfElseOp) + self.assertEqual(instruction.label, label) + + +@ddt.ddt +class TestControlFlowBuildersFailurePaths(QiskitTestCase): + """Tests for the failure paths of the control-flow builders.""" + + def test_if_rejects_break_continue_if_not_in_loop(self): + """Test that the ``if`` and ``else`` context managers raise a suitable exception if you try + to use a ``break`` or ``continue`` within them without being inside a loop. This is for + safety; without the loop context, the context manager will cause the wrong resources to be + assigned to the ``break``, so if you want to make a manual loop, you have to use manual + ``if`` as well. That way the onus is on you.""" + qubits = [Qubit()] + clbits = [Clbit()] + cond = (clbits[0], 0) + + message = r"The current builder scope cannot take a '.*' because it is not in a loop\." + + with self.subTest("if break"): + test = QuantumCircuit(qubits, clbits) + with test.if_test(cond): + with self.assertRaisesRegex(CircuitError, message): + test.break_loop() + + with self.subTest("if continue"): + test = QuantumCircuit(qubits, clbits) + with test.if_test(cond): + with self.assertRaisesRegex(CircuitError, message): + test.continue_loop() + + with self.subTest("else break"): + test = QuantumCircuit(qubits, clbits) + with test.if_test(cond) as else_: + pass + with else_: + with self.assertRaisesRegex(CircuitError, message): + test.break_loop() + + with self.subTest("else continue"): + test = QuantumCircuit(qubits, clbits) + with test.if_test(cond) as else_: + pass + with else_: + with self.assertRaisesRegex(CircuitError, message): + test.continue_loop() + + def test_for_rejects_reentry(self): + """Test that the ``for``-loop context manager rejects attempts to re-enter it. Since it + holds some forms of state during execution (the loop variable, which may be generated), we + can't safely re-enter it and get the expected behaviour.""" + + for_manager = QuantumCircuit(2, 2).for_loop(range(2)) + with for_manager: + pass + with self.assertRaisesRegex( + CircuitError, r"A for-loop context manager cannot be re-entered." + ): + with for_manager: + pass + + def test_cannot_enter_else_context_incorrectly(self): + """Test that various forms of using an 'else_' context manager incorrectly raise + exceptions.""" + bits = [Qubit(), Clbit()] + cond = (bits[1], 0) + + with self.subTest("not the next instruction"): + test = QuantumCircuit(bits) + with test.if_test(cond) as else_: + pass + test.h(0) + with self.assertRaisesRegex(CircuitError, "The 'if' block is not the most recent"): + with else_: + test.h(0) + + with self.subTest("inside the attached if"): + test = QuantumCircuit(bits) + with test.if_test(cond) as else_: + with self.assertRaisesRegex( + CircuitError, r"Cannot attach an 'else' branch to an incomplete 'if' block\." + ): + with else_: + test.h(0) + + with self.subTest("inner else"): + test = QuantumCircuit(bits) + with test.if_test(cond) as else1: + with test.if_test(cond): + pass + with self.assertRaisesRegex( + CircuitError, r"Cannot attach an 'else' branch to an incomplete 'if' block\." + ): + with else1: + test.h(0) + + with self.subTest("reused else"): + test = QuantumCircuit(bits) + with test.if_test(cond) as else_: + pass + with else_: + pass + with self.assertRaisesRegex(CircuitError, r"Cannot re-use an 'else' context\."): + with else_: + pass + + with self.subTest("else from an inner block"): + test = QuantumCircuit(bits) + with test.if_test(cond): + with test.if_test(cond) as else_: + pass + with self.assertRaisesRegex(CircuitError, "The 'if' block is not the most recent"): + with else_: + pass + + def test_if_placeholder_rejects_c_if(self): + """Test that the :obj:`.IfElsePlaceholder" class rejects attempts to use + :meth:`.Instruction.c_if` on it. + + It *should* be the case that you need to use private methods to get access to one of these + placeholder objects at all, because they're appended to a scope at the exit of a context + manager, so not returned from a method call. Just in case, here's a test that it correctly + rejects the dangerous method that can overwrite ``condition``. + """ + bits = [Qubit(), Clbit()] + + with self.subTest("if"): + test = QuantumCircuit(bits) + with test.for_loop(range(2)): + with test.if_test((bits[1], 0)): + test.break_loop() + # These tests need to be done before the 'for' context exits so we don't trigger the + # "can't add conditions from out-of-scope" handling. + placeholder, _, _ = test._peek_previous_instruction_in_scope() + self.assertIsInstance(placeholder, IfElsePlaceholder) + with self.assertRaisesRegex( + NotImplementedError, + r"IfElseOp cannot be classically controlled through Instruction\.c_if", + ): + placeholder.c_if(bits[1], 0) + + with self.subTest("else"): + test = QuantumCircuit(bits) + with test.for_loop(range(2)): + with test.if_test((bits[1], 0)) as else_: + pass + with else_: + test.break_loop() + # These tests need to be done before the 'for' context exits so we don't trigger the + # "can't add conditions from out-of-scope" handling. + placeholder, _, _ = test._peek_previous_instruction_in_scope() + self.assertIsInstance(placeholder, IfElsePlaceholder) + with self.assertRaisesRegex( + NotImplementedError, + r"IfElseOp cannot be classically controlled through Instruction\.c_if", + ): + placeholder.c_if(bits[1], 0) + + def test_reject_c_if_from_outside_scope(self): + """Test that the context managers reject :meth:`.InstructionSet.c_if` calls if they occur + after their scope has completed.""" + bits = [Qubit(), Clbit()] + cond = (bits[1], 0) + + with self.subTest("if"): + test = QuantumCircuit(bits) + with test.if_test(cond): + instructions = test.h(0) + with self.assertRaisesRegex( + CircuitError, r"Cannot add resources after the scope has been built\." + ): + instructions.c_if(*cond) + + with self.subTest("else"): + test = QuantumCircuit(bits) + with test.if_test(cond) as else_: + pass + with else_: + instructions = test.h(0) + with self.assertRaisesRegex( + CircuitError, r"Cannot add resources after the scope has been built\." + ): + instructions.c_if(*cond) + + with self.subTest("for"): + test = QuantumCircuit(bits) + with test.for_loop(range(2)): + instructions = test.h(0) + with self.assertRaisesRegex( + CircuitError, r"Cannot add resources after the scope has been built\." + ): + instructions.c_if(*cond) + + with self.subTest("while"): + test = QuantumCircuit(bits) + with test.while_loop(cond): + instructions = test.h(0) + with self.assertRaisesRegex( + CircuitError, r"Cannot add resources after the scope has been built\." + ): + instructions.c_if(*cond) + + with self.subTest("if inside for"): + # As a side-effect of how the lazy building of 'if' statements works, we actually + # *could* add a condition to the gate after the 'if' block as long as we were still + # within the 'for' loop. It should actually manage the resource correctly as well, but + # it's "undefined behaviour" than something we specifically want to forbid or allow. + test = QuantumCircuit(bits) + with test.for_loop(range(2)): + with test.if_test(cond): + instructions = test.h(0) + with self.assertRaisesRegex( + CircuitError, r"Cannot add resources after the scope has been built\." + ): + + instructions.c_if(*cond) + + def test_raising_inside_context_manager_leave_circuit_usable(self): + """Test that if we leave a builder by raising some sort of exception, the circuit is left in + a usable state, and extra resources have not been added to the circuit.""" + + x, y = Parameter("x"), Parameter("y") + + with self.subTest("for"): + test = QuantumCircuit(1, 1) + test.h(0) + with self.assertRaises(SentinelException): + with test.for_loop(range(2), x) as bound_x: + test.x(0) + test.rx(bound_x, 0) + test.ry(y, 0) + raise SentinelException + test.z(0) + + expected = QuantumCircuit(1, 1) + expected.h(0) + expected.z(0) + + self.assertEqual(test, expected) + # We don't want _either_ the loop variable or the loose variable to be in the circuit. + self.assertEqual(set(), set(test.parameters)) + + with self.subTest("while"): + bits = [Qubit(), Clbit()] + test = QuantumCircuit(bits) + test.h(0) + with self.assertRaises(SentinelException): + with test.while_loop((bits[1], 0)): + test.x(0) + test.rx(x, 0) + raise SentinelException + test.z(0) + + expected = QuantumCircuit(bits) + expected.h(0) + expected.z(0) + + self.assertEqual(test, expected) + self.assertEqual(set(), set(test.parameters)) + + with self.subTest("if"): + bits = [Qubit(), Clbit()] + test = QuantumCircuit(bits) + test.h(0) + with self.assertRaises(SentinelException): + with test.if_test((bits[1], 0)): + test.x(0) + test.rx(x, 0) + raise SentinelException + test.z(0) + + expected = QuantumCircuit(bits) + expected.h(0) + expected.z(0) + + self.assertEqual(test, expected) + self.assertEqual(set(), set(test.parameters)) + + with self.subTest("else"): + bits = [Qubit(), Clbit()] + test = QuantumCircuit(bits) + test.h(0) + with test.if_test((bits[1], 0)) as else_: + test.rx(x, 0) + with self.assertRaises(SentinelException): + with else_: + test.x(0) + test.rx(y, 0) + raise SentinelException + test.z(0) + + # Note that we expect the "else" manager to restore the "if" block if something errors + # out during "else" block. + true_body = QuantumCircuit(bits) + true_body.rx(x, 0) + expected = QuantumCircuit(bits) + expected.h(0) + expected.if_test((bits[1], 0), true_body, [0], [0]) + expected.z(0) + + self.assertEqual(test, expected) + self.assertEqual({x}, set(test.parameters)) + + def test_can_reuse_else_manager_after_exception(self): + """Test that the "else" context manager is usable after a first attempt to construct it + raises an exception. Normally you cannot re-enter an "else" block, but we want the user to + be able to recover from errors if they so try.""" + bits = [Qubit(), Clbit()] + test = QuantumCircuit(bits) + test.h(0) + with test.if_test((bits[1], 0)) as else_: + test.x(0) + with self.assertRaises(SentinelException): + with else_: + test.y(0) + raise SentinelException + with else_: + test.h(0) + test.z(0) + + true_body = QuantumCircuit(bits) + true_body.x(0) + false_body = QuantumCircuit(bits) + false_body.h(0) + expected = QuantumCircuit(bits) + expected.h(0) + expected.if_else((bits[1], 0), true_body, false_body, [0], [0]) + expected.z(0) + + self.assertEqual(test, expected) + + @ddt.data((None, [0]), ([0], None), ([0], [0])) + def test_context_managers_reject_passing_qubits(self, resources): + """Test that the context-manager forms of the control-flow circuit methods raise exceptions + if they are given explicit qubits or clbits.""" + test = QuantumCircuit(1, 1) + qubits, clbits = resources + with self.subTest("for"): + with self.assertRaisesRegex( + CircuitError, + r"When using 'for_loop' as a context manager, you cannot pass qubits or clbits\.", + ): + test.for_loop(range(2), None, body=None, qubits=qubits, clbits=clbits) + with self.subTest("while"): + with self.assertRaisesRegex( + CircuitError, + r"When using 'while_loop' as a context manager, you cannot pass qubits or clbits\.", + ): + test.while_loop((test.clbits[0], 0), body=None, qubits=qubits, clbits=clbits) + with self.subTest("if"): + with self.assertRaisesRegex( + CircuitError, + r"When using 'if_test' as a context manager, you cannot pass qubits or clbits\.", + ): + test.if_test((test.clbits[0], 0), true_body=None, qubits=qubits, clbits=clbits) + + @ddt.data((None, [0]), ([0], None), (None, None)) + def test_non_context_manager_calling_states_reject_missing_resources(self, resources): + """Test that the non-context-manager forms of the control-flow circuit methods raise + exceptions if they are not given explicit qubits or clbits.""" + test = QuantumCircuit(1, 1) + body = QuantumCircuit(1, 1) + qubits, clbits = resources + with self.subTest("for"): + with self.assertRaisesRegex( + CircuitError, + r"When using 'for_loop' with a body, you must pass qubits and clbits\.", + ): + test.for_loop(range(2), None, body=body, qubits=qubits, clbits=clbits) + with self.subTest("while"): + with self.assertRaisesRegex( + CircuitError, + r"When using 'while_loop' with a body, you must pass qubits and clbits\.", + ): + test.while_loop((test.clbits[0], 0), body=body, qubits=qubits, clbits=clbits) + with self.subTest("if"): + with self.assertRaisesRegex( + CircuitError, + r"When using 'if_test' with a body, you must pass qubits and clbits\.", + ): + test.if_test((test.clbits[0], 0), true_body=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])