Skip to content

Commit

Permalink
Ensure QuantumCircuit.append validates captures in control-flow
Browse files Browse the repository at this point in the history
This adds an inner check to the control-flow operations that their
blocks do not contain input variables, and to `QuantumCircuit.append`
that any captures within blocks are validate (in the sense of the
variables existing in the outer circuit).

In order to avoid an `import` on every call to `QuantumCircuit.append`
(especially since we're already eating the cost of an extra
`isinstance` check), this reorganises the import structure of
`qiskit.circuit.controlflow` to sit strictly _before_
`qiskit.circuit.quantumcircuit` in the import tree.  Since those are key
parts of the circuit data structure, that does make sense, although by
their nature the structures are of course recursive at runtime.
  • Loading branch information
jakelishman committed Nov 29, 2023
1 parent f846aa5 commit de0201f
Show file tree
Hide file tree
Showing 10 changed files with 326 additions and 42 deletions.
9 changes: 7 additions & 2 deletions qiskit/circuit/controlflow/_builder_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@
from __future__ import annotations

import dataclasses
from typing import Iterable, Tuple, Set, Union, TypeVar
from typing import Iterable, Tuple, Set, Union, TypeVar, TYPE_CHECKING

from qiskit.circuit.classical import expr, types
from qiskit.circuit.exceptions import CircuitError
from qiskit.circuit.quantumcircuit import QuantumCircuit
from qiskit.circuit.register import Register
from qiskit.circuit.classicalregister import ClassicalRegister, Clbit
from qiskit.circuit.quantumregister import QuantumRegister

if TYPE_CHECKING:
from qiskit.circuit import QuantumCircuit

_ConditionT = TypeVar(
"_ConditionT", bound=Union[Tuple[ClassicalRegister, int], Tuple[Clbit, int], expr.Expr]
)
Expand Down Expand Up @@ -159,6 +161,9 @@ def _unify_circuit_resources_rebuild( # pylint: disable=invalid-name # (it's t
This function will always rebuild the objects into new :class:`.QuantumCircuit` instances.
"""
# pylint: disable=cyclic-import
from qiskit.circuit import QuantumCircuit

qubits, clbits = set(), set()
for circuit in circuits:
qubits.update(circuit.qubits)
Expand Down
3 changes: 2 additions & 1 deletion qiskit/circuit/controlflow/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from ._builder_utils import condition_resources, node_resources

if typing.TYPE_CHECKING:
import qiskit # pylint: disable=cyclic-import
import qiskit


class InstructionResources(typing.NamedTuple):
Expand Down Expand Up @@ -403,6 +403,7 @@ def build(
and using the minimal set of resources necessary to support them, within the enclosing
scope.
"""
# pylint: disable=cyclic-import
from qiskit.circuit import QuantumCircuit, SwitchCaseOp

# There's actually no real problem with building a scope more than once. This flag is more
Expand Down
19 changes: 14 additions & 5 deletions qiskit/circuit/controlflow/control_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,40 @@
"Container to encapsulate all control flow operations."

from __future__ import annotations

import typing
from abc import ABC, abstractmethod
from typing import Iterable

from qiskit.circuit import QuantumCircuit, Instruction
from qiskit.circuit.instruction import Instruction
from qiskit.circuit.exceptions import CircuitError

if typing.TYPE_CHECKING:
from qiskit.circuit import QuantumCircuit


class ControlFlowOp(Instruction, ABC):
"""Abstract class to encapsulate all control flow operations."""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for block in self.blocks:
if block.num_input_vars:
raise CircuitError("control-flow blocks cannot contain input variables")

@property
@abstractmethod
def blocks(self) -> tuple[QuantumCircuit, ...]:
"""Tuple of QuantumCircuits which may be executed as part of the
execution of this ControlFlowOp. May be parameterized by a loop
parameter to be resolved at run time.
"""
pass

@abstractmethod
def replace_blocks(self, blocks: Iterable[QuantumCircuit]) -> "ControlFlowOp":
def replace_blocks(self, blocks: typing.Iterable[QuantumCircuit]) -> ControlFlowOp:
"""Replace blocks and return new instruction.
Args:
blocks: Tuple of QuantumCircuits to replace in instruction.
Returns:
New ControlFlowOp with replaced blocks.
"""
pass
13 changes: 10 additions & 3 deletions qiskit/circuit/controlflow/for_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"Circuit operation representing a ``for`` loop."
"""Circuit operation representing a ``for`` loop."""

from __future__ import annotations

import warnings
from typing import Iterable, Optional, Union
from typing import Iterable, Optional, Union, TYPE_CHECKING

from qiskit.circuit.parameter import Parameter
from qiskit.circuit.exceptions import CircuitError
from qiskit.circuit.quantumcircuit import QuantumCircuit
from .control_flow import ControlFlowOp

if TYPE_CHECKING:
from qiskit.circuit import QuantumCircuit


class ForLoopOp(ControlFlowOp):
"""A circuit operation which repeatedly executes a subcircuit
Expand Down Expand Up @@ -69,6 +73,9 @@ def params(self):

@params.setter
def params(self, parameters):
# pylint: disable=cyclic-import
from qiskit.circuit import QuantumCircuit

indexset, loop_parameter, body = parameters

if not isinstance(loop_parameter, (Parameter, type(None))):
Expand Down
13 changes: 11 additions & 2 deletions qiskit/circuit/controlflow/if_else.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@

from __future__ import annotations

from typing import Optional, Union, Iterable
from typing import Optional, Union, Iterable, TYPE_CHECKING
import itertools

from qiskit.circuit import ClassicalRegister, Clbit, QuantumCircuit
from qiskit.circuit.classicalregister import ClassicalRegister, Clbit
from qiskit.circuit.classical import expr
from qiskit.circuit.instructionset import InstructionSet
from qiskit.circuit.exceptions import CircuitError
Expand All @@ -31,6 +31,9 @@
condition_resources,
)

if TYPE_CHECKING:
from qiskit.circuit import QuantumCircuit


# This is just an indication of what's actually meant to be the public API.
__all__ = ("IfElseOp",)
Expand Down Expand Up @@ -82,6 +85,9 @@ def __init__(
false_body: QuantumCircuit | None = None,
label: str | None = None,
):
# pylint: disable=cyclic-import
from qiskit.circuit import QuantumCircuit

# Type checking generally left to @params.setter, but required here for
# finding num_qubits and num_clbits.
if not isinstance(true_body, QuantumCircuit):
Expand All @@ -103,6 +109,9 @@ def params(self):

@params.setter
def params(self, parameters):
# pylint: disable=cyclic-import
from qiskit.circuit import QuantumCircuit

true_body, false_body = parameters

if not isinstance(true_body, QuantumCircuit):
Expand Down
10 changes: 8 additions & 2 deletions qiskit/circuit/controlflow/switch_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@
__all__ = ("SwitchCaseOp", "CASE_DEFAULT")

import contextlib
from typing import Union, Iterable, Any, Tuple, Optional, List, Literal
from typing import Union, Iterable, Any, Tuple, Optional, List, Literal, TYPE_CHECKING

from qiskit.circuit import ClassicalRegister, Clbit, QuantumCircuit
from qiskit.circuit.classicalregister import ClassicalRegister, Clbit
from qiskit.circuit.classical import expr, types
from qiskit.circuit.exceptions import CircuitError

from .builder import InstructionPlaceholder, InstructionResources, ControlFlowBuilderBlock
from .control_flow import ControlFlowOp
from ._builder_utils import unify_circuit_resources, partition_registers, node_resources

if TYPE_CHECKING:
from qiskit.circuit import QuantumCircuit


class _DefaultCaseType:
"""The type of the default-case singleton. This is used instead of just having
Expand Down Expand Up @@ -71,6 +74,9 @@ def __init__(
*,
label: Optional[str] = None,
):
# pylint: disable=cyclic-import
from qiskit.circuit import QuantumCircuit

if isinstance(target, expr.Expr):
if target.type.kind not in (types.Uint, types.Bool):
raise CircuitError(
Expand Down
10 changes: 9 additions & 1 deletion qiskit/circuit/controlflow/while_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@

from __future__ import annotations

from qiskit.circuit import Clbit, ClassicalRegister, QuantumCircuit
from typing import TYPE_CHECKING

from qiskit.circuit.classicalregister import Clbit, ClassicalRegister
from qiskit.circuit.classical import expr
from qiskit.circuit.exceptions import CircuitError
from ._builder_utils import validate_condition, condition_resources
from .control_flow import ControlFlowOp

if TYPE_CHECKING:
from qiskit.circuit import QuantumCircuit


class WhileLoopOp(ControlFlowOp):
"""A circuit operation which repeatedly executes a subcircuit (``body``) until
Expand Down Expand Up @@ -70,6 +75,9 @@ def params(self):

@params.setter
def params(self, parameters):
# pylint: disable=cyclic-import
from qiskit.circuit import QuantumCircuit

(body,) = parameters

if not isinstance(body, QuantumCircuit):
Expand Down

0 comments on commit de0201f

Please sign in to comment.