diff --git a/src/autoqasm/__init__.py b/src/autoqasm/__init__.py index ee61cae..c0b8477 100644 --- a/src/autoqasm/__init__.py +++ b/src/autoqasm/__init__.py @@ -50,6 +50,7 @@ def my_program(): from .api import gate, gate_calibration, main, subroutine # noqa: F401 from .instructions import QubitIdentifierType as Qubit # noqa: F401 from .program import Program, build_program, verbatim # noqa: F401 +from .program import get_program_conversion_context as _get_program_conversion_context from .transpiler import transpiler # noqa: F401 from .types import ArrayVar, BitVar, BoolVar, FloatVar, IntVar # noqa: F401 from .types import Range as range # noqa: F401 @@ -57,5 +58,5 @@ def my_program(): def __getattr__(name): if name == "qubits": - return instructions.global_qubit_register() + return _get_program_conversion_context().global_qubit_register raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/autoqasm/instructions/__init__.py b/src/autoqasm/instructions/__init__.py index c9fe02f..51fb963 100644 --- a/src/autoqasm/instructions/__init__.py +++ b/src/autoqasm/instructions/__init__.py @@ -28,4 +28,3 @@ def bell(): from .gates import * from .instructions import barrier, reset # noqa: F401 from .measurements import measure, measure_ff # noqa: F401 -from .qubits import global_qubit_register # noqa: F401 diff --git a/src/autoqasm/instructions/instructions.py b/src/autoqasm/instructions/instructions.py index b2ae93e..c2b6a92 100644 --- a/src/autoqasm/instructions/instructions.py +++ b/src/autoqasm/instructions/instructions.py @@ -23,8 +23,9 @@ from autoqasm import program as aq_program from autoqasm import types as aq_types -from autoqasm.instructions.qubits import _as_qubit_iterable, _qubit +from autoqasm.instructions.qubits import _qubit from autoqasm.types import QubitIdentifierType +from autoqasm.types.qubits import _as_qubit_iterable from braket.circuits.basis_state import BasisState, BasisStateInput diff --git a/src/autoqasm/instructions/measurements.py b/src/autoqasm/instructions/measurements.py index b73ec11..627b6de 100644 --- a/src/autoqasm/instructions/measurements.py +++ b/src/autoqasm/instructions/measurements.py @@ -29,7 +29,8 @@ def my_program(): from autoqasm import program from autoqasm import types as aq_types from autoqasm.instructions.instructions import _qubit_instruction -from autoqasm.instructions.qubits import _as_qubit_iterable, _qubit, global_qubit_register +from autoqasm.instructions.qubits import _qubit +from autoqasm.types.qubits import _as_qubit_iterable def measure( @@ -45,9 +46,10 @@ def measure( Returns: BitVar: Bit variable the measurement results are assigned to. """ - qubits = _as_qubit_iterable(qubits, default=global_qubit_register()) + ctx = program.get_program_conversion_context() + qubits = _as_qubit_iterable(qubits, default=ctx.global_qubit_register) - oqpy_program = program.get_program_conversion_context().get_oqpy_program() + oqpy_program = ctx.get_oqpy_program() bit_var_size = len(qubits) if len(qubits) > 1 else None bit_var = aq_types.BitVar( diff --git a/src/autoqasm/instructions/qubits.py b/src/autoqasm/instructions/qubits.py index 29d11ec..72bef6a 100644 --- a/src/autoqasm/instructions/qubits.py +++ b/src/autoqasm/instructions/qubits.py @@ -17,15 +17,13 @@ from __future__ import annotations import re -from collections.abc import Iterable, Iterator from functools import singledispatch from typing import Any import oqpy.base from openpulse.printer import dumps -from autoqasm import constants, errors, program -from autoqasm import types as aq_types +from autoqasm import errors, program def _get_physical_qubit_indices(qids: list[str]) -> list[int]: @@ -48,39 +46,6 @@ def _get_physical_qubit_indices(qids: list[str]) -> list[int]: return braket_qubits -def _global_qubit_register(qubit_idx_expr: int | str) -> oqpy.Qubit: - return oqpy.Qubit(f"{constants.QUBIT_REGISTER}[{qubit_idx_expr}]", needs_declaration=False) - - -class GlobalQubitRegister(oqpy.Qubit): - def __init__(self, size: int | None): - super().__init__(name=constants.QUBIT_REGISTER, size=size, needs_declaration=False) - - def __len__(self) -> int: - return self.size - - def __iter__(self) -> Iterator: - return iter(range(len(self))) - - -def global_qubit_register() -> GlobalQubitRegister: - return program.get_program_conversion_context().global_qubit_register - - -def _as_qubit_iterable( - qubits: aq_types.QubitIdentifierType | Iterable[aq_types.QubitIdentifierType] | None, - default: Iterable[aq_types.QubitIdentifierType] | None = None, -) -> Iterable[aq_types.QubitIdentifierType]: - """Normalize a qubit argument to an iterable. ``None`` maps to ``default`` (an empty - list if not provided); a single qubit identifier is wrapped in a list; iterables - (including :class:`GlobalQubitRegister`) pass through unchanged.""" - if qubits is None: - qubits = default if default is not None else [] - if aq_types.is_qubit_identifier_type(qubits) and not isinstance(qubits, GlobalQubitRegister): - return [qubits] - return qubits - - @singledispatch def _qubit(qid: Any) -> oqpy.Qubit: """Maps a given qubit representation to an oqpy qubit. @@ -91,48 +56,40 @@ def _qubit(qid: Any) -> oqpy.Qubit: Returns: Qubit: A translated oqpy qubit. """ - raise ValueError(f"invalid qubit label: '{qid}'") + raise TypeError(f"object of type '{type(qid).__name__}' cannot be used as a qubit") @_qubit.register def _(qid: bool) -> oqpy.Qubit: - raise ValueError(f"invalid qubit label: '{qid}'") - - -@_qubit.register -def _(qid: float) -> oqpy.Qubit: - raise TypeError(f"qubit index cannot be a float: '{qid}'") + raise TypeError(f"object of type '{type(qid).__name__}' cannot be used as a qubit") @_qubit.register def _(qid: int) -> oqpy.Qubit: # Integer virtual qubit, like `h(0)` - program.get_program_conversion_context().register_qubit(qid) - return _global_qubit_register(qid) - - -@_qubit.register -def _(qid: GlobalQubitRegister) -> oqpy.Qubit: - raise ValueError("qubit index must be a single value, not a list or a register") + ctx = program.get_program_conversion_context() + ctx.register_qubit(qid) + return ctx.global_qubit_register[qid] @_qubit.register def _(qid: oqpy._ClassicalVar) -> oqpy.Qubit: # Indexed by variable, such as i in range(n); h(i) - if program.get_program_conversion_context().get_declared_qubits() is None: + ctx = program.get_program_conversion_context() + if ctx.get_declared_qubits() is None: raise errors.UnknownQubitCountError() - return _global_qubit_register(qid.name) + return ctx.global_qubit_register[qid.name] @_qubit.register def _(qid: oqpy.base.OQPyExpression) -> oqpy.Qubit: # Indexed by expression, such as i in range(n); h(i + 1) - if program.get_program_conversion_context().get_declared_qubits() is None: + ctx = program.get_program_conversion_context() + if ctx.get_declared_qubits() is None: raise errors.UnknownQubitCountError() - oqpy_program = program.get_program_conversion_context().get_oqpy_program() - qubit_idx_expr = dumps(qid.to_ast(oqpy_program)) - return _global_qubit_register(qubit_idx_expr) + qubit_idx_expr = dumps(qid.to_ast(ctx.get_oqpy_program())) + return ctx.global_qubit_register[qubit_idx_expr] @_qubit.register diff --git a/src/autoqasm/operators/control_flow.py b/src/autoqasm/operators/control_flow.py index ff4db1c..73a7e83 100644 --- a/src/autoqasm/operators/control_flow.py +++ b/src/autoqasm/operators/control_flow.py @@ -22,11 +22,11 @@ import oqpy.base from autoqasm import program -from autoqasm.types import Range, is_qasm_type +from autoqasm.types import GlobalQubitRegister, Range, is_qasm_type def for_stmt( - iter: Iterable | oqpy.Range | oqpy.Qubit, + iter: Iterable | oqpy.Range, extra_test: Callable[[], Any] | None, body: Callable[[Any], None], get_state: Any, @@ -37,7 +37,7 @@ def for_stmt( """Implements a for loop. Args: - iter (Iterable | Range | Qubit): The iterable to be looped over. + iter (Iterable | Range): The iterable to be looped over. extra_test (Callable[[], Any] | None): A function to cause the loop to break if true. body (Callable[[Any], None]): The body of the for loop. get_state (Any): Unused. @@ -56,13 +56,13 @@ def for_stmt( def _oqpy_for_stmt( - iter: oqpy.Range | oqpy.Qubit, + iter: oqpy.Range | GlobalQubitRegister, body: Callable[[Any], None], opts: dict, ) -> None: """Overload of for_stmt that produces an oqpy for loop.""" ctx = program.get_program_conversion_context() - if isinstance(iter, oqpy.Qubit): + if isinstance(iter, GlobalQubitRegister): iter = Range(iter.size) def _trace(ctx): diff --git a/src/autoqasm/program/program.py b/src/autoqasm/program/program.py index 13ffd06..2131de9 100644 --- a/src/autoqasm/program/program.py +++ b/src/autoqasm/program/program.py @@ -34,11 +34,12 @@ import autoqasm.types as aq_types from autoqasm import _frame_filtering, constants, errors -from autoqasm.instructions.qubits import GlobalQubitRegister, _get_physical_qubit_indices, _qubit +from autoqasm.instructions.qubits import _get_physical_qubit_indices, _qubit from autoqasm.program.serialization_properties import ( OpenQASMSerializationProperties, SerializationProperties, ) +from autoqasm.types import GlobalQubitRegister from autoqasm.types import QubitIdentifierType as Qubit from autoqasm.types.deferred import DeferredVarMixin from braket.aws.aws_device import AwsDevice @@ -422,7 +423,7 @@ def declare_global_qubit_register(self, size: int) -> None: """ root_oqpy_program = self.get_oqpy_program(scope=ProgramScope.MAIN) self.global_qubit_register.size = size - root_oqpy_program.declare(self.global_qubit_register, to_beginning=True) + root_oqpy_program.declare(self.global_qubit_register.oqpy_var, to_beginning=True) def register_gate(self, gate_name: str, is_compiler_directive: bool = False) -> None: """Register a gate that is used in this program. @@ -650,17 +651,28 @@ def is_var_name_used(self, var_name: str) -> bool: oqpy_program = self.get_oqpy_program() return var_name in oqpy_program.declared_vars or var_name in oqpy_program.undeclared_vars - def validate_gate_targets(self, qubits: list[Any], angles: list[Any]) -> None: + def validate_gate_targets( + self, + qubits: Iterable[Qubit], + angles: Iterable[Any], + ) -> None: """Validate that the specified gate targets are valid at this point in the program. Args: - qubits (list[Any]): The list of target qubits to validate. - angles (list[Any]): The list of target angles to validate. + qubits (Iterable[QubitIdentifierType]): The target qubits to validate. + angles (Iterable[Any]): The target angles to validate. Raises: + TypeError: A target qubit is not a single qubit identifier. errors.InvalidTargetQubit: Target qubits are invalid in the current context. errors.InvalidGateDefinition: Targets are invalid in the current gate definition. """ + for qubit in qubits: + if not aq_types.is_qubit_identifier_type(qubit): + raise TypeError( + f'Invalid qubit target: "{qubit}". Target must be a single qubit, not a list or a register.' + ) + if self.in_verbatim_block and not self._gate_definitions_processing: self._validate_verbatim_target_qubits(qubits) diff --git a/src/autoqasm/types/__init__.py b/src/autoqasm/types/__init__.py index f3e2503..518b6b7 100644 --- a/src/autoqasm/types/__init__.py +++ b/src/autoqasm/types/__init__.py @@ -16,15 +16,18 @@ """ from .conversions import map_parameter_type, var_type_from_oqpy, wrap_value # noqa: F401 +from .qubits import ( # noqa: F401 + GlobalQubitRegister, + QubitIdentifierType, + is_qubit_identifier_type, +) from .types import ( # noqa: F401 ArrayVar, BitVar, BoolVar, FloatVar, IntVar, - QubitIdentifierType, Range, is_qasm_type, - is_qubit_identifier_type, make_annotations_list, ) diff --git a/src/autoqasm/types/qubits.py b/src/autoqasm/types/qubits.py new file mode 100644 index 0000000..caa6ae5 --- /dev/null +++ b/src/autoqasm/types/qubits.py @@ -0,0 +1,90 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +"""Qubit identifier types and the program-level qubit register.""" + +from __future__ import annotations + +from collections.abc import Iterable, Iterator +from typing import Any, get_args + +import oqpy +import oqpy.base + +from autoqasm import constants +from braket.registers import Qubit + +QubitIdentifierType = int | str | Qubit | oqpy._ClassicalVar | oqpy.base.OQPyExpression | oqpy.Qubit + +_QUBIT_IDENTIFIER_TYPES: tuple[type, ...] = get_args(QubitIdentifierType) + + +def is_qubit_identifier_type(qubit: Any) -> bool: + """Checks if a given object is a qubit identifier type. + + Args: + qubit (Any): The object to check. + + Returns: + bool: True if the object is a qubit identifier type, False otherwise. + """ + return isinstance(qubit, _QUBIT_IDENTIFIER_TYPES) + + +def _as_qubit_iterable( + qubits: QubitIdentifierType | Iterable[QubitIdentifierType] | None, + default: Iterable[QubitIdentifierType] | None = None, +) -> Iterable[QubitIdentifierType]: + """Normalize a qubit argument to an iterable. ``None`` maps to ``default`` (an empty + list if not provided); a single qubit identifier is wrapped in a list; iterables + (including :class:`GlobalQubitRegister`) pass through unchanged.""" + if qubits is None: + qubits = default if default is not None else [] + if is_qubit_identifier_type(qubits): + return [qubits] + return qubits + + +class GlobalQubitRegister: + def __init__(self, size: int | None = None): + self._var = oqpy.Qubit(constants.QUBIT_REGISTER, size=size, needs_declaration=False) + + @property + def name(self) -> str: + return self._var.name + + @property + def size(self) -> int | None: + return self._var.size + + @size.setter + def size(self, value: int) -> None: + self._var.size = value + + @property + def oqpy_var(self) -> oqpy.Qubit: + """The underlying oqpy variable used to declare the register in OpenQASM.""" + return self._var + + def __repr__(self) -> str: + return self.name + + def __len__(self) -> int: + return self.size + + def __iter__(self) -> Iterator[int]: + return iter(range(len(self))) + + def __getitem__(self, index: int | str) -> oqpy.Qubit: + """Returns an oqpy.Qubit referring to ``__qubits__[index]``.""" + return oqpy.Qubit(f"{self.name}[{index}]", needs_declaration=False) diff --git a/src/autoqasm/types/types.py b/src/autoqasm/types/types.py index 5c7b79e..e617ac9 100644 --- a/src/autoqasm/types/types.py +++ b/src/autoqasm/types/types.py @@ -16,7 +16,7 @@ from __future__ import annotations from collections.abc import Iterable -from typing import Any, get_args +from typing import Any import numpy as np import oqpy @@ -24,8 +24,8 @@ from openpulse import ast from autoqasm import errors, program +from autoqasm.types.qubits import GlobalQubitRegister from braket.circuits import FreeParameterExpression -from braket.registers import Qubit def is_qasm_type(val: Any) -> bool: @@ -44,6 +44,7 @@ def is_qasm_type(val: Any) -> bool: oqpy.Qubit, oqpy.base.OQPyExpression, FreeParameterExpression, + GlobalQubitRegister, ) # The input can either be a class, like oqpy.Range ... if type(val) is type: @@ -56,25 +57,6 @@ def make_annotations_list(annotations: str | Iterable[str] | None) -> list[str]: return [annotations] if isinstance(annotations, str) else annotations or [] -QubitIdentifierType = int | str | Qubit | oqpy._ClassicalVar | oqpy.base.OQPyExpression | oqpy.Qubit - -# Precompute the type tuple once: ``get_args(QubitIdentifierType)`` isn't -# free, and this is called on every gate emission. -_QUBIT_IDENTIFIER_TYPES: tuple[type, ...] = get_args(QubitIdentifierType) - - -def is_qubit_identifier_type(qubit: Any) -> bool: - """Checks if a given object is a qubit identifier type. - - Args: - qubit (Any): The object to check. - - Returns: - bool: True if the object is a qubit identifier type, False otherwise. - """ - return isinstance(qubit, _QUBIT_IDENTIFIER_TYPES) - - class Range(oqpy.Range): def __init__( self, diff --git a/test/unit_tests/autoqasm/test_api.py b/test/unit_tests/autoqasm/test_api.py index 2fe2572..10cb8f8 100644 --- a/test/unit_tests/autoqasm/test_api.py +++ b/test/unit_tests/autoqasm/test_api.py @@ -23,8 +23,10 @@ import autoqasm as aq from autoqasm import errors from autoqasm.instructions import cnot, h, measure, rx, x -from autoqasm.instructions.qubits import GlobalQubitRegister, _as_qubit_iterable +from autoqasm.instructions.qubits import _qubit +from autoqasm.types.qubits import _as_qubit_iterable from autoqasm.simulator import McmSimulator +from autoqasm.types import GlobalQubitRegister from braket.devices import LocalSimulator from braket.tasks.local_quantum_task import LocalQuantumTask @@ -685,7 +687,7 @@ def broken() -> None: """Uses invalid type for qubit index""" h(True) - with pytest.raises(ValueError): + with pytest.raises(TypeError, match="object of type 'bool' cannot be used as a qubit"): broken.build() @@ -697,10 +699,22 @@ def broken() -> None: """Uses invalid type for qubit index""" h(h) - with pytest.raises(ValueError): + with pytest.raises(TypeError): broken.build() +def test_qubit_dispatch_rejects_float() -> None: + """Direct call to _qubit with a float hits the float singledispatch arm.""" + with pytest.raises(TypeError, match="object of type 'float' cannot be used as a qubit"): + _qubit(0.5) + + +def test_qubit_dispatch_rejects_arbitrary_object() -> None: + """Direct call to _qubit with an unsupported type hits the default singledispatch arm.""" + with pytest.raises(TypeError, match="object of type 'complex' cannot be used as a qubit"): + _qubit(complex(1.2 + 3.4j)) + + def test_bit_array_name() -> None: """Tests that auto declared bits are given a reasonable name.""" @@ -1187,7 +1201,22 @@ def main(): h(aq.qubits) with pytest.raises( - ValueError, match="qubit index must be a single value, not a list or a register" + TypeError, + match=r'Invalid qubit target: "__qubits__"\. ' + r"Target must be a single qubit, not a list or a register\.", + ): + main.build() + + +def test_gate_list_not_allowed(): + @aq.main(num_qubits=2) + def main(): + h([0, 1]) + + with pytest.raises( + TypeError, + match=r'Invalid qubit target: "\[0, 1\]"\. ' + r"Target must be a single qubit, not a list or a register\.", ): main.build()