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:
Jake Lishman 2021-12-02 17:06:18 +00:00 committed by GitHub
parent efbf436297
commit 9ba51b5cc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 3888 additions and 190 deletions

View File

@ -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

View File

@ -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 ((), ())

View File

@ -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

View File

@ -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])

View File

@ -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 ((), ())

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
)

View File

@ -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

View File

@ -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):

View File

@ -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.

View File

@ -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))

View File

@ -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.*"

View File

@ -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