mirror of https://github.com/Qiskit/qiskit.git
Add control-flow builder interface (#7282)
* Add control-flow builder interface This adds a builder interface for control-flow operations on `QuantumCircuit` (such as `ForLoopOp`, `IfElseOp`, and `WhileLoopOp`). The interface uses the same circuit methods, but they are now 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(None, range(5)) as i: qc.rx(i * math.pi / 4, 0) This will produce a `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 `break_loop` and `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 `if_test` blocks. The `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. This commit also ensures that instances of `ParameterExpression` added to a circuit inside _all_ control flow instructions will correctly propagate up to the top-level circuit. * Fix linter complaints * Fix typos Co-authored-by: Kevin Hartman <kevin@hart.mn> * Add extra error check * Remove useless early return * Document qubits, clbits in ControlFlowBlockBuilder.__init__ * Remove comment that is likely to stagnate * Add extra else test * Improve developer documentation in InstructionPlaceholder * Remove expected failure from test This branch contains the fix that this test depended on. * Remove unused import * Change order of for_loop parameters This changes the parameter order of `QuantumCircuit.for_loop` to be indexset, [loop_parameter, [body, qubits, clbits]] whereas previously it was loop_parameter, indexset, [body, qubits, clbits] Similar changes were made within the constructor of `ForLoopOp` and its parameters. This is to improve ergonomics of the builder interface, where it is not generally necessary to specify a loop variable, since one is allocated for the user. Co-authored-by: Kevin Hartman <kevin@hart.mn>
This commit is contained in:
parent
efbf436297
commit
9ba51b5cc9
|
@ -14,10 +14,9 @@
|
|||
|
||||
|
||||
from .control_flow import ControlFlowOp
|
||||
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
|
||||
|
||||
from .if_else import IfElseOp
|
||||
from .while_loop import WhileLoopOp
|
||||
from .for_loop import ForLoopOp
|
||||
|
|
|
@ -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 ((), ())
|
||||
|
|
|
@ -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
|
|
@ -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])
|
|
@ -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 ((), ())
|
||||
|
|
|
@ -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 @@ class ForLoopOp(ControlFlowOp):
|
|||
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 @@ class ForLoopOp(ControlFlowOp):
|
|||
|
||||
@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 @@ class ForLoopOp(ControlFlowOp):
|
|||
# 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
|
||||
|
|
|
@ -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 @@ class IfElseOp(ControlFlowOp):
|
|||
|
||||
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 @@ class IfElseOp(ControlFlowOp):
|
|||
|
||||
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
|
||||
|
|
|
@ -16,6 +16,7 @@ from typing import Optional, Tuple, Union
|
|||
|
||||
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 @@ class WhileLoopOp(ControlFlowOp):
|
|||
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 @@ class WhileLoopOp(ControlFlowOp):
|
|||
"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
|
||||
|
|
|
@ -69,6 +69,9 @@ try:
|
|||
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 @@ class QuantumCircuit:
|
|||
# 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 @@ class QuantumCircuit:
|
|||
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 @@ class QuantumCircuit:
|
|||
"""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 @@ class QuantumCircuit:
|
|||
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
|
||||
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 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)]
|
||||
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 @@ class QuantumCircuit:
|
|||
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 @@ class QuantumCircuit:
|
|||
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)
|
||||
else:
|
||||
instr.params[param_index] = 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)
|
||||
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:
|
||||
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 @@ class QuantumCircuit:
|
|||
|
||||
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
|
||||
|
||||
return self.append(ForLoopOp(loop_parameter, indexset, body, label), qubits, clbits)
|
||||
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(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 @@ class QuantumCircuit:
|
|||
) -> 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 @@ class QuantumCircuit:
|
|||
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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -717,7 +717,7 @@ class QASM3Builder:
|
|||
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 @@ class QASM3Builder:
|
|||
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 _infer_variable_declaration(
|
|||
# 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):
|
||||
|
|
|
@ -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.
|
|
@ -949,8 +949,35 @@ class TestCircuitOperations(QiskitTestCase):
|
|||
self.assertFalse(qc1 == qc2)
|
||||
|
||||
|
||||
class TestCircuitBuilding(QiskitTestCase):
|
||||
"""QuantumCircuit tests."""
|
||||
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_append_dimension_mismatch(self):
|
||||
"""Test appending to incompatible wires."""
|
||||
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))
|
||||
|
|
|
@ -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 @@ class TestCircuitQASM3(QiskitTestCase):
|
|||
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 @@ class TestCircuitQASM3(QiskitTestCase):
|
|||
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 @@ class TestCircuitQASM3(QiskitTestCase):
|
|||
)
|
||||
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 @@ class TestCircuitQASM3(QiskitTestCase):
|
|||
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 @@ class TestCircuitQASM3(QiskitTestCase):
|
|||
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 @@ class TestCircuitQASM3(QiskitTestCase):
|
|||
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 @@ class TestQASM3ExporterFailurePaths(QiskitTestCase):
|
|||
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.*"
|
||||
|
|
|
@ -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 @@ class TestCreatingControlFlowOperations(QiskitTestCase):
|
|||
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 @@ class TestCreatingControlFlowOperations(QiskitTestCase):
|
|||
|
||||
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 @@ class TestCreatingControlFlowOperations(QiskitTestCase):
|
|||
|
||||
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 @@ class TestCreatingControlFlowOperations(QiskitTestCase):
|
|||
|
||||
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 @@ class TestCreatingControlFlowOperations(QiskitTestCase):
|
|||
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 @@ class TestCreatingControlFlowOperations(QiskitTestCase):
|
|||
|
||||
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 @@ class TestCreatingControlFlowOperations(QiskitTestCase):
|
|||
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 @@ class TestAddingControlFlowOperations(QiskitTestCase):
|
|||
|
||||
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 @@ class TestAddingControlFlowOperations(QiskitTestCase):
|
|||
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 @@ class TestAddingControlFlowOperations(QiskitTestCase):
|
|||
(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 @@ class TestAddingControlFlowOperations(QiskitTestCase):
|
|||
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)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue