diff --git a/src/autoqasm/instructions/__init__.py b/src/autoqasm/instructions/__init__.py index 11b957e..c9fe02f 100644 --- a/src/autoqasm/instructions/__init__.py +++ b/src/autoqasm/instructions/__init__.py @@ -26,6 +26,6 @@ def bell(): """ from .gates import * -from .instructions import reset # noqa: F401 +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 3db70e4..b2ae93e 100644 --- a/src/autoqasm/instructions/instructions.py +++ b/src/autoqasm/instructions/instructions.py @@ -23,7 +23,7 @@ from autoqasm import program as aq_program from autoqasm import types as aq_types -from autoqasm.instructions.qubits import _qubit +from autoqasm.instructions.qubits import _as_qubit_iterable, _qubit from autoqasm.types import QubitIdentifierType from braket.circuits.basis_state import BasisState, BasisStateInput @@ -101,10 +101,7 @@ def _get_pos_neg_control( contains the positive-control qubits, and the second list contains the negative-control qubits. The union of the two lists is the same as the list of control qubits. """ - if control is None: - control = [] - elif aq_types.is_qubit_identifier_type(control): - control = [control] + control = _as_qubit_iterable(control) if control_state is None: control_state = [1] * len(control) @@ -123,3 +120,20 @@ def reset(target: QubitIdentifierType) -> None: target (QubitIdentifierType): The target qubit. """ _qubit_instruction("reset", [target], is_unitary=False) + + +def barrier( + qubits: aq_types.QubitIdentifierType | Iterable[aq_types.QubitIdentifierType] | None = None, +) -> None: + """Adds a barrier compiler directive on the specified qubits. If ``qubits`` is None, + the barrier applies to all qubits in the program. + + Args: + qubits (QubitIdentifierType | Iterable[QubitIdentifierType] | None): The target qubits. + If None, the barrier applies to all qubits in the program. Default is None. + """ + qubits = _as_qubit_iterable(qubits) + program_conversion_context = aq_program.get_program_conversion_context() + program_conversion_context.validate_gate_targets(qubits, []) + program_conversion_context.register_gate("barrier", is_compiler_directive=True) + program_conversion_context.get_oqpy_program().barrier([_qubit(q) for q in qubits]) diff --git a/src/autoqasm/instructions/measurements.py b/src/autoqasm/instructions/measurements.py index 1cbf5c4..b73ec11 100644 --- a/src/autoqasm/instructions/measurements.py +++ b/src/autoqasm/instructions/measurements.py @@ -29,7 +29,7 @@ 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 GlobalQubitRegister, _qubit, global_qubit_register +from autoqasm.instructions.qubits import _as_qubit_iterable, _qubit, global_qubit_register def measure( @@ -45,11 +45,7 @@ def measure( Returns: BitVar: Bit variable the measurement results are assigned to. """ - if qubits is None: - qubits = global_qubit_register() - - if aq_types.is_qubit_identifier_type(qubits) and not isinstance(qubits, GlobalQubitRegister): - qubits = [qubits] + qubits = _as_qubit_iterable(qubits, default=global_qubit_register()) oqpy_program = program.get_program_conversion_context().get_oqpy_program() diff --git a/src/autoqasm/instructions/qubits.py b/src/autoqasm/instructions/qubits.py index cf88c89..29d11ec 100644 --- a/src/autoqasm/instructions/qubits.py +++ b/src/autoqasm/instructions/qubits.py @@ -17,7 +17,7 @@ from __future__ import annotations import re -from collections.abc import Iterator +from collections.abc import Iterable, Iterator from functools import singledispatch from typing import Any @@ -25,6 +25,7 @@ from openpulse.printer import dumps from autoqasm import constants, errors, program +from autoqasm import types as aq_types def _get_physical_qubit_indices(qids: list[str]) -> list[int]: @@ -66,6 +67,20 @@ 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. diff --git a/src/autoqasm/program/program.py b/src/autoqasm/program/program.py index a416c86..13ffd06 100644 --- a/src/autoqasm/program/program.py +++ b/src/autoqasm/program/program.py @@ -424,16 +424,24 @@ def declare_global_qubit_register(self, size: int) -> None: self.global_qubit_register.size = size root_oqpy_program.declare(self.global_qubit_register, to_beginning=True) - def register_gate(self, gate_name: str) -> None: + def register_gate(self, gate_name: str, is_compiler_directive: bool = False) -> None: """Register a gate that is used in this program. Args: gate_name (str): The name of the gate being used. + is_compiler_directive (bool): Whether ``gate_name`` is a compiler directive + (e.g. ``barrier``) rather than a unitary gate. Compiler directives are exempt + from the verbatim-block native-gate restriction but are still validated at + build time against the target device's ``supportedOperations``. Default is False. Raises: errors.UnsupportedNativeGate: If the gate is being used inside a verbatim block and the gate is not a native gate of the target device. """ + if is_compiler_directive: + self._gates_used.add(gate_name) + return + if not self.in_verbatim_block: self._gates_used.add(gate_name) return diff --git a/test/unit_tests/autoqasm/test_api.py b/test/unit_tests/autoqasm/test_api.py index 81409df..2fe2572 100644 --- a/test/unit_tests/autoqasm/test_api.py +++ b/test/unit_tests/autoqasm/test_api.py @@ -23,6 +23,7 @@ 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.simulator import McmSimulator from braket.devices import LocalSimulator from braket.tasks.local_quantum_task import LocalQuantumTask @@ -1244,6 +1245,22 @@ def main(): assert main.build().to_ir() == expected_ir +def test_as_qubit_iterable_single_qubit_is_wrapped(): + assert _as_qubit_iterable(0) == [0] + assert _as_qubit_iterable("$1") == ["$1"] + + +def test_as_qubit_iterable_iterable_passes_through(): + assert _as_qubit_iterable([0, 1]) == [0, 1] + register = GlobalQubitRegister(size=3) + assert _as_qubit_iterable(register) is register + + +def test_as_qubit_iterable_none_uses_default(): + assert _as_qubit_iterable(None) == [] + assert _as_qubit_iterable(None, default=[5, 6]) == [5, 6] + + def test_subroutine_call_with_kwargs(): """Test that subroutine call works with keyword arguments""" diff --git a/test/unit_tests/autoqasm/test_barrier.py b/test/unit_tests/autoqasm/test_barrier.py new file mode 100644 index 0000000..bb5eeda --- /dev/null +++ b/test/unit_tests/autoqasm/test_barrier.py @@ -0,0 +1,78 @@ +# 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. + +"""Tests for the ``barrier`` compiler directive.""" + +import pytest + +import autoqasm as aq +from autoqasm.instructions import barrier, cnot, h + + +def test_barrier_on_qubits() -> None: + @aq.main + def program(): + h(0) + barrier([0, 1]) + cnot(0, 1) + + expected_ir = """OPENQASM 3.0; +qubit[2] __qubits__; +h __qubits__[0]; +barrier __qubits__[0], __qubits__[1]; +cnot __qubits__[0], __qubits__[1];""" + assert program.build().to_ir() == expected_ir + + +def test_barrier_single_qubit() -> None: + @aq.main + def program(): + h(0) + barrier(0) + + expected_ir = """OPENQASM 3.0; +qubit[1] __qubits__; +h __qubits__[0]; +barrier __qubits__[0];""" + assert program.build().to_ir() == expected_ir + + +def test_barrier_all_qubits() -> None: + @aq.main + def program(): + h(0) + barrier() + cnot(0, 1) + + expected_ir = """OPENQASM 3.0; +qubit[2] __qubits__; +h __qubits__[0]; +barrier; +cnot __qubits__[0], __qubits__[1];""" + assert program.build().to_ir() == expected_ir + + +def test_barrier_disallowed_inside_gate_definition() -> None: + """Barriers are compiler directives, not unitary gates, so they must not + appear inside ``@aq.gate`` bodies.""" + + @aq.gate + def bad_gate(q: aq.Qubit): + barrier(q) + + @aq.main + def program(): + bad_gate(0) + + with pytest.raises(aq.errors.InvalidGateDefinition): + program.build() diff --git a/test/unit_tests/autoqasm/test_devices.py b/test/unit_tests/autoqasm/test_devices.py index d50e25d..dc8fe22 100644 --- a/test/unit_tests/autoqasm/test_devices.py +++ b/test/unit_tests/autoqasm/test_devices.py @@ -20,7 +20,7 @@ import autoqasm as aq from autoqasm import errors -from autoqasm.instructions import cnot, cphaseshift00, h, rx, x +from autoqasm.instructions import barrier, cnot, cphaseshift00, h, rx, x from braket.aws import AwsDevice from braket.device_schema import DeviceActionType from braket.device_schema.simulators import GateModelSimulatorDeviceCapabilities @@ -169,6 +169,65 @@ def my_program(): my_program.build(device=aws_device) +def test_barrier_inside_verbatim_allowed(aws_device: Mock) -> None: + """Barriers are compiler directives, not native gates, and must be allowed + inside verbatim blocks regardless of the device's ``nativeGateSet``.""" + aws_device.properties.action[DeviceActionType.OPENQASM].supportedOperations = ["x", "barrier"] + aws_device.properties.action[DeviceActionType.OPENQASM].supportedPragmas = ["verbatim"] + aws_device.properties.paradigm.nativeGateSet = ["x"] + + @aq.main + def my_program(): + with aq.verbatim(): + x("$0") + barrier("$0") + + expected_ir = """OPENQASM 3.0; +pragma braket verbatim +box { + x $0; + barrier $0; +}""" + assert my_program.build(device=aws_device).to_ir() == expected_ir + + +def test_barrier_outside_verbatim_allowed(aws_device: Mock) -> None: + """Barriers are allowed outside verbatim blocks on devices that list + ``barrier`` in their ``supportedOperations``.""" + aws_device.properties.action[DeviceActionType.OPENQASM].supportedOperations = [ + "h", + "cnot", + "barrier", + ] + + @aq.main + def my_program(): + h(0) + barrier([0, 1]) + cnot(0, 1) + + expected_ir = """OPENQASM 3.0; +qubit[2] __qubits__; +h __qubits__[0]; +barrier __qubits__[0], __qubits__[1]; +cnot __qubits__[0], __qubits__[1];""" + assert my_program.build(device=aws_device).to_ir() == expected_ir + + +def test_barrier_not_in_supported_operations(aws_device: Mock) -> None: + """Devices that do not list ``barrier`` in ``supportedOperations`` should still + reject programs that use it.""" + aws_device.properties.action[DeviceActionType.OPENQASM].supportedOperations = ["h"] + + @aq.main + def my_program(): + h(0) + barrier(0) + + with pytest.raises(errors.UnsupportedGate): + my_program.build(device=aws_device) + + def test_supported_native_gate_inside_gate_definition(aws_device: Mock) -> None: aws_device.properties.action[DeviceActionType.OPENQASM].supportedOperations = ["h, x"] aws_device.properties.action[DeviceActionType.OPENQASM].supportedPragmas = ["verbatim"]