Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add control-flow builder interface #7282

Merged
merged 19 commits into from
Dec 2, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions qiskit/circuit/controlflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@


from .control_flow import ControlFlowOp
from .if_else import IfElseOp
from .continue_loop import ContinueLoopOp
from .break_loop import BreakLoopOp

from .if_else import IfElseOp
from .while_loop import WhileLoopOp
from .for_loop import ForLoopOp

from .continue_loop import ContinueLoopOp
from .break_loop import BreakLoopOp
21 changes: 20 additions & 1 deletion qiskit/circuit/controlflow/break_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from typing import Optional

from qiskit.circuit.instruction import Instruction
from .builder import InstructionPlaceholder


class BreakLoopOp(Instruction):
Expand Down Expand Up @@ -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?
kevinhartman marked this conversation as resolved.
Show resolved Hide resolved
return ((), ())
389 changes: 389 additions & 0 deletions qiskit/circuit/controlflow/builder.py

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions qiskit/circuit/controlflow/condition.py
Original file line number Diff line number Diff line change
@@ -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])
22 changes: 21 additions & 1 deletion qiskit/circuit/controlflow/continue_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from typing import Optional

from qiskit.circuit.instruction import Instruction
from .builder import InstructionPlaceholder


class ContinueLoopOp(Instruction):
Expand Down Expand Up @@ -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 ((), ())
98 changes: 98 additions & 0 deletions qiskit/circuit/controlflow/for_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,101 @@ def params(self, parameters):
@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
kevinhartman marked this conversation as resolved.
Show resolved Hide resolved
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,
loop_parameter: Optional[Parameter],
indexset: Iterable[int],
*,
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(loop_parameter, self._indexset, body, label=self._label),
tuple(body.qubits),
tuple(body.clbits),
)
return False
Loading