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 builder interface for new switch statement #9919

Merged
merged 4 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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?!)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol, it doesn't seem that long to me. Actually, it's not longer than: 6ae6634#diff-6bc399dbeb2cecdb0736e132814faa79639d77be265911ae26d3d9fe32eda3f5R130 which didn't raise an error for me. It might be something else in the regex? Either way this is kind of bogus

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

funnily enough, it's because our pylint settings allow longer method names (50 chars) than function names (31 chars)... (Well, that was true of the old .pylintrc form - not certain once we cut out all the duplication.)

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
129 changes: 18 additions & 111 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 @@ -194,7 +194,7 @@ def __init__(
# 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()
self.__resources = self._calculate_placeholder_resources()
super().__init__(
"if_else", len(self.__resources.qubits), len(self.__resources.clbits), [], label=label
)
Expand Down Expand Up @@ -232,23 +232,23 @@ def registers(self):
return self.__true_block.registers.copy()
return self.__true_block.registers | self.__false_block.registers

def _placeholder_resources(self) -> InstructionResources:
def _calculate_placeholder_resources(self) -> InstructionResources:
"""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:
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