Skip to content

Commit

Permalink
Add builder interface for new switch statement
Browse files Browse the repository at this point in the history
With the addition of `switch`, we now have a second control-flow
construct that

- is not a loop, so can have lazily constructed blocks because of
  contained `break` and `continue` statements
- can have multiple blocks that need their resources unifying

For the first part, we largely just have to handle things separately to
`if`, though the placeholder instructions are much simpler because the
placeholder will never need mutating and replacing (c.f. the `else`
block).  For the second point, we move a bunch of the previously
if-specific code into a shared location in order to reuse it.

Unlike the `if` case, the `switch` builder uses nested context managers
for its cases, because this mirrors a common indentation pattern for
switch statements (at least if you're not a Linux kernel developer).

We need new special logic to reject statements that are loose in the
`switch` context but not a `case` context.  We _could_ have just ignored
that, but it feels like an easy potential mistake to make, so much
better to loudly fail than silently do the wrong thing.
  • Loading branch information
jakelishman committed Apr 6, 2023
1 parent c0e4141 commit d0fab50
Show file tree
Hide file tree
Showing 6 changed files with 962 additions and 146 deletions.
114 changes: 114 additions & 0 deletions qiskit/circuit/controlflow/_builder_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2022.
#
# 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.

"""Private utility functions that are used by the builder interfaces."""

from typing import Iterable, Tuple, Set

from qiskit.circuit.exceptions import CircuitError
from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit.circuit.register import Register
from qiskit.circuit.classicalregister import ClassicalRegister
from qiskit.circuit.quantumregister import QuantumRegister


def partition_registers(
registers: Iterable[Register],
) -> Tuple[Set[QuantumRegister], Set[ClassicalRegister]]:
"""Partition a sequence of registers into its quantum and classical registers."""
qregs = set()
cregs = set()
for register in registers:
if isinstance(register, QuantumRegister):
qregs.add(register)
elif isinstance(register, ClassicalRegister):
cregs.add(register)
else:
# Purely defensive against Terra expansion.
raise CircuitError(f"Unknown register: {register}.")
return qregs, cregs


def unify_circuit_resources(circuits: Iterable[QuantumCircuit]) -> Iterable[QuantumCircuit]:
"""
Ensure that all the given ``circuits`` have all the same qubits, clbits and registers, 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 its inputs 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, circuits with unified bits and registers
are returned.
"""
circuits = tuple(circuits)
if len(circuits) < 2:
return circuits
qubits = []
clbits = []
for circuit in circuits:
if circuit.qubits[: len(qubits)] != qubits:
return _unify_circuit_resources_rebuild(circuits)
if circuit.clbits[: len(qubits)] != clbits:
return _unify_circuit_resources_rebuild(circuits)
if circuit.num_qubits > len(qubits):
qubits = list(circuit.qubits)
if circuit.num_clbits > len(clbits):
clbits = list(circuit.clbits)
for circuit in circuits:
circuit.add_bits(qubits[circuit.num_qubits :])
circuit.add_bits(clbits[circuit.num_clbits :])
return _unify_circuit_registers(circuits)


def _unify_circuit_resources_rebuild( # pylint: disable=invalid-name # (it's too long?!)
circuits: Tuple[QuantumCircuit, ...]
) -> Tuple[QuantumCircuit, QuantumCircuit]:
"""
Ensure that all the given circuits 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 objects into new :class:`.QuantumCircuit` instances.
"""
qubits, clbits = set(), set()
for circuit in circuits:
qubits.update(circuit.qubits)
clbits.update(circuit.clbits)
qubits, clbits = list(qubits), list(clbits)

# We use the inner `_append` method because everything is already resolved in the builders.
out_circuits = []
for circuit in circuits:
out = QuantumCircuit(qubits, clbits, *circuit.qregs, *circuit.cregs)
for instruction in circuit.data:
out._append(instruction)
out_circuits.append(out)
return _unify_circuit_registers(out_circuits)


def _unify_circuit_registers(circuits: Iterable[QuantumCircuit]) -> Iterable[QuantumCircuit]:
"""
Ensure that ``true_body`` and ``false_body`` have the same registers defined within them. These
do not need to be in the same order between circuits. The two input circuits are returned,
mutated to have the same registers.
"""
circuits = tuple(circuits)
total_registers = set()
for circuit in circuits:
total_registers.update(circuit.qregs)
total_registers.update(circuit.cregs)
for circuit in circuits:
for register in total_registers - set(circuit.qregs) - set(circuit.cregs):
circuit.add_register(register)
return circuits
27 changes: 24 additions & 3 deletions qiskit/circuit/controlflow/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import abc
import itertools
import typing
from typing import Callable, Collection, Iterable, List, FrozenSet, Tuple, Union
from typing import Callable, Collection, Iterable, List, FrozenSet, Tuple, Union, Optional

from qiskit.circuit.classicalregister import Clbit, ClassicalRegister
from qiskit.circuit.exceptions import CircuitError
Expand Down Expand Up @@ -95,8 +95,13 @@ def concrete_instruction(
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.
the returned :class:`.ControlFlowOp` will not unnecessarily span all resources, but only the
ones that it needs.
.. note::
The caller of this function is responsible for ensuring that the inputs to this function
are non-strict supersets of the bits returned by :meth:`placeholder_resources`.
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
Expand Down Expand Up @@ -202,6 +207,7 @@ class ControlFlowBuilderBlock:
"_allow_jumps",
"_resource_requester",
"_built",
"_forbidden_message",
)

def __init__(
Expand All @@ -212,6 +218,7 @@ def __init__(
registers: Iterable[Register] = (),
resource_requester: Callable,
allow_jumps: bool = True,
forbidden_message: Optional[str] = None,
):
"""
Args:
Expand All @@ -238,6 +245,11 @@ def __init__(
:meth:`.QuantumCircuit._resolve_classical_resource` for the normal expected input
here, and the documentation of :obj:`.InstructionSet`, which uses this same
callback.
forbidden_message: If a string is given here, a :exc:`.CircuitError` will be raised on
any attempts to append instructions to the scope with this message. This is used by
pseudo scopes where the state machine of the builder scopes has changed into a
position where no instructions should be accepted, such as when inside a ``switch``
but outside any cases.
"""
self.instructions: List[CircuitInstruction] = []
self.qubits = set(qubits)
Expand All @@ -246,6 +258,7 @@ def __init__(
self._allow_jumps = allow_jumps
self._resource_requester = resource_requester
self._built = False
self._forbidden_message = forbidden_message

@property
def allow_jumps(self):
Expand All @@ -264,6 +277,9 @@ def allow_jumps(self):
def append(self, instruction: CircuitInstruction) -> CircuitInstruction:
"""Add an instruction into the scope, keeping track of the qubits and clbits that have been
used in total."""
if self._forbidden_message is not None:
raise CircuitError(self._forbidden_message)

if not self._allow_jumps:
# pylint: disable=cyclic-import
from .break_loop import BreakLoopOp, BreakLoopPlaceholder
Expand Down Expand Up @@ -393,6 +409,10 @@ def build(
# that may have been built into other objects.
self._built = True

if self._forbidden_message is not None:
# Reaching this implies a logic error in the builder interface.
raise RuntimeError("Cannot build a forbidden scope. Please report this as a bug.")

potential_qubits = all_qubits - self.qubits
potential_clbits = all_clbits - self.clbits

Expand Down Expand Up @@ -452,4 +472,5 @@ def copy(self) -> "ControlFlowBuilderBlock":
out.clbits = self.clbits.copy()
out.registers = self.registers.copy()
out._allow_jumps = self._allow_jumps
out._forbidden_message = self._forbidden_message
return out
125 changes: 16 additions & 109 deletions qiskit/circuit/controlflow/if_else.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@
"Circuit operation representing an ``if/else`` statement."


from typing import Optional, Tuple, Union, Iterable, Set
from typing import Optional, Tuple, Union, Iterable
import itertools

from qiskit.circuit import ClassicalRegister, Clbit, QuantumCircuit
from qiskit.circuit.instructionset import InstructionSet
from qiskit.circuit.exceptions import CircuitError
from qiskit.circuit.quantumregister import QuantumRegister
from qiskit.circuit.register import Register

from .builder import ControlFlowBuilderBlock, InstructionPlaceholder, InstructionResources
from .condition import validate_condition, condition_bits, condition_registers
from .control_flow import ControlFlowOp
from ._builder_utils import partition_registers, unify_circuit_resources


# This is just an indication of what's actually meant to be the public API.
Expand Down Expand Up @@ -240,15 +240,15 @@ def _placeholder_resources(self) -> InstructionResources:
public version as a cache access for efficiency.
"""
if self.__false_block is None:
qregs, cregs = _partition_registers(self.__true_block.registers)
qregs, cregs = partition_registers(self.__true_block.registers)
return InstructionResources(
qubits=tuple(self.__true_block.qubits),
clbits=tuple(self.__true_block.clbits),
qregs=tuple(qregs),
cregs=tuple(cregs),
)
true_qregs, true_cregs = _partition_registers(self.__true_block.registers)
false_qregs, false_cregs = _partition_registers(self.__false_block.registers)
true_qregs, true_cregs = partition_registers(self.__true_block.registers)
false_qregs, false_cregs = partition_registers(self.__false_block.registers)
return InstructionResources(
qubits=tuple(self.__true_block.qubits | self.__false_block.qubits),
clbits=tuple(self.__true_block.clbits | self.__false_block.clbits),
Expand Down Expand Up @@ -276,13 +276,15 @@ def concrete_instruction(self, qubits, clbits):
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_resources(true_body, false_body)
if self.__false_block is None:
false_body = None
else:
# 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_resources(
(true_body, self.__false_block.build(qubits, clbits))
)
return (
self._copy_mutable_properties(
IfElseOp(self.condition, true_body, false_body, label=self.label)
Expand Down Expand Up @@ -485,7 +487,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
# bits onto the circuits at the end.
true_body = self._if_instruction.operation.blocks[0]
false_body = false_block.build(false_block.qubits, false_block.clbits)
true_body, false_body = _unify_circuit_resources(true_body, false_body)
true_body, false_body = unify_circuit_resources((true_body, false_body))
circuit.append(
IfElseOp(
self._if_context.condition,
Expand All @@ -497,98 +499,3 @@ def __exit__(self, exc_type, exc_val, exc_tb):
tuple(true_body.clbits),
)
return False


def _partition_registers(
registers: Iterable[Register],
) -> Tuple[Set[QuantumRegister], Set[ClassicalRegister]]:
"""Partition a sequence of registers into its quantum and classical registers."""
qregs = set()
cregs = set()
for register in registers:
if isinstance(register, QuantumRegister):
qregs.add(register)
elif isinstance(register, ClassicalRegister):
cregs.add(register)
else:
# Purely defensive against Terra expansion.
raise CircuitError(f"Unknown register: {register}.")
return qregs, cregs


def _unify_circuit_resources(
true_body: QuantumCircuit, false_body: Optional[QuantumCircuit]
) -> Tuple[QuantumCircuit, Union[QuantumCircuit, None]]:
"""
Ensure that ``true_body`` and ``false_body`` have all the same qubits, clbits and registers, 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 and registers 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_resources_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_resources_rebuild(true_body, false_body)
return _unify_circuit_registers(true_body, false_body)


def _unify_circuit_resources_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, *true_body.qregs, *true_body.cregs)
for instruction in true_body.data:
true_out._append(instruction)
false_out = QuantumCircuit(qubits, clbits, *false_body.qregs, *false_body.cregs)
for instruction in false_body.data:
false_out._append(instruction)
return _unify_circuit_registers(true_out, false_out)


def _unify_circuit_registers(
true_body: QuantumCircuit, false_body: QuantumCircuit
) -> Tuple[QuantumCircuit, QuantumCircuit]:
"""
Ensure that ``true_body`` and ``false_body`` have the same registers defined within them. These
do not need to be in the same order between circuits. The two input circuits are returned,
mutated to have the same registers.
"""
true_registers = set(true_body.qregs) | set(true_body.cregs)
false_registers = set(false_body.qregs) | set(false_body.cregs)
for register in false_registers - true_registers:
true_body.add_register(register)
for register in true_registers - false_registers:
false_body.add_register(register)
return true_body, false_body
Loading

0 comments on commit d0fab50

Please sign in to comment.