diff --git a/CHANGELOG.md b/CHANGELOG.md index 290f79b..c650698 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## v0.1.0 (2024-05-09) +## v0.1.0 (2024-05-22) This is the initial pre-release version of AutoQASM. diff --git a/README.md b/README.md index 0484351..02a8350 100644 --- a/README.md +++ b/README.md @@ -96,13 +96,13 @@ AutoQASM can support subroutines and complex control flow. You can use the Pytho and quantum runtime side-by-side. There are rough edges at the moment, but we're actively smoothing them out! -The Amazon Braket local simulator supports AutoQASM programs as input. +AutoQASM includes a simulator which can be accessed using the Amazon Braket local simulator interface. Let's simulate the `conditional_multi_bell_states` program: ``` -from braket.devices.local_simulator import LocalSimulator +from braket.devices import LocalSimulator -device = LocalSimulator() +device = LocalSimulator("autoqasm") task = device.run(conditional_multi_bell_states, shots=100) result = task.result() ``` diff --git a/examples/1_Getting_started_with_AutoQASM.ipynb b/examples/1_Getting_started_with_AutoQASM.ipynb index e33d5b7..cd89a7d 100644 --- a/examples/1_Getting_started_with_AutoQASM.ipynb +++ b/examples/1_Getting_started_with_AutoQASM.ipynb @@ -21,7 +21,7 @@ "import matplotlib.pyplot as plt\n", "\n", "# AWS imports: Import Braket SDK modules\n", - "from braket.devices.local_simulator import LocalSimulator\n", + "from braket.devices import LocalSimulator\n", "\n", "# AutoQASM imports\n", "import autoqasm as aq\n", @@ -108,7 +108,7 @@ } ], "source": [ - "device = LocalSimulator()\n", + "device = LocalSimulator(\"autoqasm\")\n", "result = device.run(bell_state, shots=100).result()\n", "counts = Counter(result.measurements[\"return_value\"])\n", "print(\"measurement counts: \", counts)" diff --git a/examples/2_Expressing_classical_control_flow.ipynb b/examples/2_Expressing_classical_control_flow.ipynb index d4c5694..eef0d51 100644 --- a/examples/2_Expressing_classical_control_flow.ipynb +++ b/examples/2_Expressing_classical_control_flow.ipynb @@ -22,7 +22,7 @@ "import matplotlib.pyplot as plt\n", "\n", "# AWS imports: Import Braket SDK modules\n", - "from braket.devices.local_simulator import LocalSimulator\n", + "from braket.devices import LocalSimulator\n", "\n", "# AutoQASM imports\n", "import autoqasm as aq\n", @@ -167,7 +167,7 @@ } ], "source": [ - "device = LocalSimulator()\n", + "device = LocalSimulator(\"autoqasm\")\n", "result = device.run(conditioned_bell, shots=500).result()\n", "counts = Counter(result.measurements[\"return_value\"])\n", "print(\"measurement counts: \", counts)\n", diff --git a/examples/3_1_Iterative_phase_estimation.ipynb b/examples/3_1_Iterative_phase_estimation.ipynb index 57ea403..4a8a93c 100644 --- a/examples/3_1_Iterative_phase_estimation.ipynb +++ b/examples/3_1_Iterative_phase_estimation.ipynb @@ -24,7 +24,7 @@ "import matplotlib.pyplot as plt\n", "\n", "# AWS imports: Import Braket SDK modules\n", - "from braket.devices.local_simulator import LocalSimulator\n", + "from braket.devices import LocalSimulator\n", "\n", "# AutoQASM imports\n", "import autoqasm as aq\n", @@ -158,7 +158,7 @@ } ], "source": [ - "device = LocalSimulator()\n", + "device = LocalSimulator(\"autoqasm\")\n", "result = device.run(ipe, shots=100).result()\n", "counts = Counter(result.measurements[\"b0\"])\n", "print(\"measurement counts: \", counts)\n", diff --git a/examples/3_2_magic_state_distillation.ipynb b/examples/3_2_magic_state_distillation.ipynb index 0612f3e..bd5ef5d 100644 --- a/examples/3_2_magic_state_distillation.ipynb +++ b/examples/3_2_magic_state_distillation.ipynb @@ -37,7 +37,7 @@ "from collections import defaultdict\n", "\n", "# AWS imports: Import Braket SDK modules\n", - "from braket.devices.local_simulator import LocalSimulator\n", + "from braket.devices import LocalSimulator\n", "\n", "# AutoQASM imports\n", "import autoqasm as aq\n", @@ -175,7 +175,7 @@ ], "source": [ "# Get measurement result\n", - "result = LocalSimulator().run(gate_teleportation, shots=100).result()\n", + "result = LocalSimulator(\"autoqasm\").run(gate_teleportation, shots=100).result()\n", "counts = Counter(result.measurements[\"return_value\"])\n", "print(\"measurement counts: \", counts)\n", "\n", @@ -315,7 +315,7 @@ ], "source": [ "n_shots = 1000\n", - "result = LocalSimulator().run(distillation, shots=n_shots).result()\n", + "result = LocalSimulator(\"autoqasm\").run(distillation, shots=n_shots).result()\n", "counts = Counter(result.measurements[\"c\"])\n", "print(\"measurement counts: \", counts)" ] @@ -435,7 +435,7 @@ } ], "source": [ - "result = LocalSimulator().run(distillation_rus, shots=20).result()\n", + "result = LocalSimulator(\"autoqasm\").run(distillation_rus, shots=20).result()\n", "counts = Counter(result.measurements[\"c2\"])\n", "probs = {str(k): v / sum(counts.values()) for k, v in counts.items()}\n", "\n", diff --git a/setup.py b/setup.py index a06cd03..e375a95 100644 --- a/setup.py +++ b/setup.py @@ -24,12 +24,8 @@ packages=find_namespace_packages(where="src", exclude=("test",)), package_dir={"": "src"}, install_requires=[ - # Pin the latest commit of mcm-sim branch of amazon-braket/amazon-braket-sdk-python.git - # and amazon-braket/amazon-braket-default-simulator-python.git to get the version of the - # simulator that supports the mcm=True argument for Monte Carlo simulation of mid-circuit - # measurement, which AutoQASM requires. - "amazon-braket-sdk @ git+https://github.com/amazon-braket/amazon-braket-sdk-python.git@ff73de68cf6ac2d0a921e8fe62693e5b9ae2e321#egg=amazon-braket-sdk", # noqa E501 - "amazon-braket-default-simulator @ git+https://github.com/amazon-braket/amazon-braket-default-simulator-python.git@ab068c860963c29842d7649c741f88da669597eb#egg=amazon-braket-default-simulator", # noqa E501 + "amazon-braket-sdk>=1.80.0", + "amazon-braket-default-simulator>=1.23.2", "oqpy~=0.3.5", "diastatic-malt", "numpy", @@ -60,6 +56,11 @@ "tox", ], }, + entry_points={ + "braket.simulators": [ + "autoqasm = autoqasm.simulator.simulator:McmSimulator", + ] + }, include_package_data=True, url="https://github.com/amazon-braket/autoqasm", author="Amazon Web Services", diff --git a/src/autoqasm/program/program.py b/src/autoqasm/program/program.py index c97c82a..d833bfa 100644 --- a/src/autoqasm/program/program.py +++ b/src/autoqasm/program/program.py @@ -121,7 +121,7 @@ def build(self, device: Device | str | None = None) -> Program: def to_ir( self, ir_type: IRType = IRType.OPENQASM, - allow_implicit_build: bool = False, + build_if_necessary: bool = True, serialization_properties: SerializationProperties = OpenQASMSerializationProperties(), ) -> str: """Serializes the program into an intermediate representation. @@ -129,20 +129,20 @@ def to_ir( Args: ir_type (IRType): The IRType to use for converting the program to its IR representation. Defaults to IRType.OPENQASM. - allow_implicit_build (bool): Whether to allow the program to be implicitly - built as a side effect of calling this function. Defaults to False. + build_if_necessary (bool): Whether to allow the program to be implicitly + built as a side effect of calling this function. Defaults to True. serialization_properties (SerializationProperties): IR serialization configuration. Default to OpenQASMSerializationProperties(). Raises: ValueError: Raised if the supplied `ir_type` is not supported. - RuntimeError: Raised if `allow_implicit_build` is False, since a MainProgram object + RuntimeError: Raised if `build_if_necessary` is False, since a MainProgram object has not yet been built. Returns: str: A representation of the program in the `ir_type` format. """ - if not allow_implicit_build: + if not build_if_necessary: raise RuntimeError( "The AutoQASM program cannot be serialized because it has not yet been built. " "To serialize the program, first call build() to obtain a built Program object, " @@ -227,7 +227,7 @@ def make_bound_program(self, param_values: dict[str, float], strict: bool = Fals def to_ir( self, ir_type: IRType = IRType.OPENQASM, - allow_implicit_build: bool = True, + build_if_necessary: bool = True, serialization_properties: SerializationProperties = OpenQASMSerializationProperties(), ) -> str: """Serializes the program into an intermediate representation. @@ -235,7 +235,7 @@ def to_ir( Args: ir_type (IRType): The IRType to use for converting the program to its IR representation. Defaults to IRType.OPENQASM. - allow_implicit_build (bool): Whether to allow the program to be implicitly + build_if_necessary (bool): Whether to allow the program to be implicitly built as a side effect of calling this function. Defaults to True. This parameter is ignored for the Program class, since the program has already been built. diff --git a/src/autoqasm/simulator/__init__.py b/src/autoqasm/simulator/__init__.py new file mode 100644 index 0000000..f9e317f --- /dev/null +++ b/src/autoqasm/simulator/__init__.py @@ -0,0 +1,37 @@ +# 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. + +""" +This module contains a local simulator implementation which can be used to +simulate AutoQASM programs at the full OpenQASM 3.0 scope of functionality, +including programs which contain mid-circuit measurement and classical control flow. + +The recommended usage of this simulator is by instantiating the Braket LocalSimulator +object with the "autoqasm" backend: + +.. code-block:: python + + import autoqasm as aq + from braket.devices import LocalSimulator + + @aq.main + def bell_state(): + h(0) + cnot(0, 1) + return measure([0, 1]) + + device = LocalSimulator("autoqasm") + result = device.run(bell_state, shots=100).result() +""" + +from .simulator import McmSimulator # noqa: F401 diff --git a/src/autoqasm/simulator/conversion.py b/src/autoqasm/simulator/conversion.py new file mode 100644 index 0000000..31981be --- /dev/null +++ b/src/autoqasm/simulator/conversion.py @@ -0,0 +1,47 @@ +# 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. + +from functools import singledispatch +from typing import Any, Union + +import numpy as np +from braket.default_simulator.openqasm._helpers.casting import convert_bool_array_to_string +from braket.default_simulator.openqasm.parser.openqasm_ast import ( + ArrayLiteral, + BitstringLiteral, + BooleanLiteral, + FloatLiteral, + IntegerLiteral, +) + +LiteralType = Union[BooleanLiteral, IntegerLiteral, FloatLiteral, ArrayLiteral, BitstringLiteral] + + +@singledispatch +def convert_to_output(value: LiteralType) -> Any: + raise NotImplementedError(f"converting {value} to output") + + +@convert_to_output.register(IntegerLiteral) +@convert_to_output.register(FloatLiteral) +@convert_to_output.register(BooleanLiteral) +@convert_to_output.register(BitstringLiteral) +def _(value): + return value.value + + +@convert_to_output.register +def _(value: ArrayLiteral): + if isinstance(value.values[0], BooleanLiteral): + return convert_bool_array_to_string(value) + return np.array([convert_to_output(x) for x in value.values]) diff --git a/src/autoqasm/simulator/linalg_utils.py b/src/autoqasm/simulator/linalg_utils.py new file mode 100644 index 0000000..87e8565 --- /dev/null +++ b/src/autoqasm/simulator/linalg_utils.py @@ -0,0 +1,56 @@ +# 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. + +import itertools +from collections.abc import Iterable + +import numpy as np + + +def measurement_sample(prob: Iterable[float], target_count: int) -> tuple[int]: + """Draws a random measurement sample. + + Args: + prob (Iterable[float]): Probabilities of each measurement outcome. + Possible measurement outcomes range from 0 to 2**target_count-1, + meaning that len(prob) must be equal to 2**target_count. + target_count (int): Number of bits in the resulting measurement. + + Returns: + tuple[int]: Measurement outcomes 0 or 1 for each of the target_count bits. + """ + basis_states = np.array(list(itertools.product([0, 1], repeat=target_count))) + outcome_idx = np.random.choice(list(range(2**target_count)), p=prob) + return tuple(basis_states[outcome_idx]) + + +def measurement_collapse_sv( + state_vector: np.ndarray, targets: Iterable[int], outcome: np.ndarray +) -> np.ndarray: + """Collapses the state vector according to the given measurement outcome. + + Args: + state_vector (np.ndarray): The state vector prior to measurement. + targets (Iterable[int]): The qubit indices that were measured. + outcome (np.ndarray): Array of measurement outcomes 0 or 1. + + Returns: + np.ndarray: The collapsed state vector after measurement. + """ + qubit_count = int(np.log2(state_vector.size)) + state_tensor = state_vector.reshape([2] * qubit_count) + for qubit, measurement in zip(targets, outcome): + state_tensor[(slice(None),) * qubit + (int(not measurement),)] = 0 + + state_tensor /= np.linalg.norm(state_tensor) + return state_tensor.flatten() diff --git a/src/autoqasm/simulator/native_interpreter.py b/src/autoqasm/simulator/native_interpreter.py new file mode 100644 index 0000000..7da4307 --- /dev/null +++ b/src/autoqasm/simulator/native_interpreter.py @@ -0,0 +1,153 @@ +# 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. + +from copy import deepcopy +from functools import singledispatchmethod +from logging import Logger +from typing import Any, List, Optional, Union + +from braket.default_simulator.openqasm._helpers.casting import cast_to, wrap_value_into_literal +from braket.default_simulator.openqasm.interpreter import Interpreter +from braket.default_simulator.openqasm.parser.openqasm_ast import ( + ArrayLiteral, + BitType, + BooleanLiteral, + ClassicalDeclaration, + IndexedIdentifier, + IODeclaration, + IOKeyword, + QASMNode, + QuantumMeasurement, + QuantumMeasurementStatement, + QuantumReset, + QubitDeclaration, +) +from braket.default_simulator.openqasm.parser.openqasm_parser import parse +from braket.default_simulator.simulation import Simulation +from openqasm3.ast import IntegerLiteral + +from autoqasm.simulator.program_context import McmProgramContext + + +class NativeInterpreter(Interpreter): + def __init__( + self, + simulation: Simulation, + context: Optional[McmProgramContext] = None, + logger: Optional[Logger] = None, + ): + self.simulation = simulation + context = context or McmProgramContext() + super().__init__(context, logger) + + def simulate( + self, + source: str, + inputs: Optional[dict[str, Any]] = None, + is_file: bool = False, + shots: int = 1, + ) -> dict[str, Any]: + """Simulates the given program. + + Args: + source (str): The OpenQASM source program, or a filename containing the + OpenQASM source program. + inputs (Optional[dict[str, Any]]): The input parameter values to the OpenQASM + source program. Defaults to None. + is_file (bool): Whether `source` is a filename. Defaults to False. + shots (int): Number of shots of the program to simulate. Defaults to 1. + + Returns: + dict[str, Any]: Outputs of the program. + """ + if inputs: + self.context.load_inputs(inputs) + + if is_file: + with open(source, encoding="utf-8", mode="r") as f: + source = f.read() + + program = parse(source) + for _ in range(shots): + program_copy = deepcopy(program) + self.visit(program_copy) + self.context.save_output_values() + self.context.num_qubits = 0 + self.simulation.reset() + return self.context.outputs + + @singledispatchmethod + def visit(self, node: Union[QASMNode, List[QASMNode]]) -> Optional[QASMNode]: + """Generic visit function for an AST node""" + return super().visit(node) + + @visit.register + def _(self, node: QubitDeclaration) -> None: + self.logger.debug(f"Qubit declaration: {node}") + size = self.visit(node.size).value if node.size else 1 + self.context.add_qubits(node.qubit.name, size) + self.simulation.add_qubits(size) + + @visit.register + def _(self, node: QuantumMeasurement) -> Union[BooleanLiteral, ArrayLiteral]: + self.logger.debug(f"Quantum measurement: {node}") + self.simulation.evolve(self.context.pop_instructions()) + targets = self.context.get_qubits(self.visit(node.qubit)) + outcome = self.simulation.measure(targets) + if len(targets) > 1 or ( + isinstance(node.qubit, IndexedIdentifier) + and not len(node.qubit.indices[0]) == 1 + and isinstance(node.qubit.indices[0], IntegerLiteral) + ): + return ArrayLiteral([BooleanLiteral(x) for x in outcome]) + return BooleanLiteral(outcome[0]) + + @visit.register + def _(self, node: QuantumMeasurementStatement) -> Union[BooleanLiteral, ArrayLiteral]: + self.logger.debug(f"Quantum measurement statement: {node}") + outcome = self.visit(node.measure) + current_value = self.context.get_value_by_identifier(node.target) + result_type = ( + BooleanLiteral + if isinstance(current_value, BooleanLiteral) or current_value is None + else BitType(size=IntegerLiteral(len(current_value.values))) + ) + value = cast_to(result_type, outcome) + self.context.update_value(node.target, value) + + @visit.register + def _(self, node: QuantumReset) -> None: + self.logger.debug(f"Quantum reset: {node}") + self.simulation.evolve(self.context.pop_instructions()) + targets = self.context.get_qubits(self.visit(node.qubits)) + outcome = self.simulation.measure(targets) + for qubit, result in zip(targets, outcome): + if result: + self.simulation.flip(qubit) + + @visit.register + def _(self, node: IODeclaration) -> None: + self.logger.debug(f"IO Declaration: {node}") + if node.io_identifier == IOKeyword.output: + if node.identifier.name not in self.context.outputs: + self.context.add_output(node.identifier.name) + self.context.declare_variable( + node.identifier.name, + node.type, + ) + else: # IOKeyword.input: + if node.identifier.name not in self.context.inputs: + raise NameError(f"Missing input variable '{node.identifier.name}'.") + init_value = wrap_value_into_literal(self.context.inputs[node.identifier.name]) + declaration = ClassicalDeclaration(node.type, node.identifier, init_value) + self.visit(declaration) diff --git a/src/autoqasm/simulator/program_context.py b/src/autoqasm/simulator/program_context.py new file mode 100644 index 0000000..8b1f1da --- /dev/null +++ b/src/autoqasm/simulator/program_context.py @@ -0,0 +1,224 @@ +# 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. + +from __future__ import annotations + +from collections.abc import Sequence +from functools import singledispatchmethod +from typing import Optional, Union + +import numpy as np +from braket.default_simulator.openqasm._helpers.arrays import ( + convert_discrete_set_to_list, + convert_range_def_to_slice, +) +from braket.default_simulator.openqasm.circuit import Circuit +from braket.default_simulator.openqasm.parser.openqasm_ast import ( + ClassicalType, + DiscreteSet, + Identifier, + IndexedIdentifier, + IntegerLiteral, + RangeDefinition, + SymbolLiteral, +) +from braket.default_simulator.openqasm.program_context import ProgramContext, Table +from braket.default_simulator.operation import GateOperation +from sympy import Integer + +from autoqasm.simulator.conversion import convert_to_output + + +class QubitTable(Table): + def __init__(self): + super().__init__("Qubits") + + def _validate_qubit_in_range(self, qubit: int, register_name: str) -> None: + if qubit >= len(self[register_name]): + raise IndexError( + f"qubit register index `{qubit}` out of range for qubit register " + f"of length {len(self[register_name])} `{register_name}`." + ) + + @singledispatchmethod + def get_by_identifier(self, identifier: Union[Identifier, IndexedIdentifier]) -> tuple[int]: + """Convenience method to get an element with a possibly indexed identifier. + + Args: + identifier (Union[Identifier, IndexedIdentifier]): The identifier to retrieve. + + Returns: + tuple[int]: The qubit indices associated with the given identifier. + """ + if identifier.name.startswith("$"): + return (int(identifier.name[1:]),) + return self[identifier.name] + + @get_by_identifier.register + def _(self, identifier: IndexedIdentifier) -> tuple[int]: # noqa: C901 + """When identifier is an IndexedIdentifier, function returns a tuple + corresponding to the elements referenced by the indexed identifier. + + Args: + identifier (IndexedIdentifier): The indexed identifier to retrieve. + + Raises: + IndexError: Qubit register index out of range for specified register. + + Returns: + tuple[int]: The qubit indices associated with the given identifier. + """ + name = identifier.name.name + indices = self.get_qubit_indices(identifier) + primary_index = indices[0] + + if isinstance(primary_index, (IntegerLiteral, SymbolLiteral)): + if isinstance(primary_index, IntegerLiteral): + self._validate_qubit_in_range(primary_index.value, name) + target = (self[name][0] + primary_index.value,) + elif isinstance(primary_index, RangeDefinition): + target = tuple(np.array(self[name])[convert_range_def_to_slice(primary_index)]) + # Discrete set + else: + index_list = convert_discrete_set_to_list(primary_index) + for index in index_list: + if isinstance(index, int): + self._validate_qubit_in_range(index, name) + target = tuple([self[name][0] + index for index in index_list]) + + if len(indices) == 2: + # used for gate calls on registers, index will be IntegerLiteral + secondary_index = indices[1].value + target = (target[secondary_index],) + + # validate indices manually, since we use addition instead of indexing to + # accommodate symbolic indices + for q in target: + if isinstance(q, (int, Integer)) and (relative_index := q - self[name][0]) >= len( + self[name] + ): + raise IndexError( + f"qubit register index `{relative_index}` out of range for qubit register " + f"of length {len(self[name])} `{name}`." + ) + return target + + @staticmethod + def get_qubit_indices( + identifier: IndexedIdentifier, + ) -> list[IntegerLiteral | RangeDefinition | DiscreteSet]: + """Gets the qubit indices from a given indexed identifier. + + Args: + identifier (IndexedIdentifier): The identifier representing the + qubit indices. + + Raises: + IndexError: Index consists of multiple dimensions. + + Returns: + list[IntegerLiteral | RangeDefinition | DiscreteSet]: The qubit indices + corresponding to the given indexed identifier. + """ + primary_index = identifier.indices[0] + + if isinstance(primary_index, list): + if len(primary_index) != 1: + raise IndexError("Cannot index multiple dimensions for qubits.") + primary_index = primary_index[0] + + if len(identifier.indices) == 1: + return [primary_index] + elif len(identifier.indices) == 2: + # used for gate calls on registers, index will be IntegerLiteral + secondary_index = identifier.indices[1][0] + return [primary_index, secondary_index] + else: + raise IndexError("Cannot index multiple dimensions for qubits.") + + def _get_indices_length( + self, + indices: Sequence[IntegerLiteral | SymbolLiteral | RangeDefinition | DiscreteSet], + ) -> int: + last_index = indices[-1] + + if isinstance(last_index, (IntegerLiteral, SymbolLiteral)): + return 1 + elif isinstance(last_index, RangeDefinition): + buffer = np.sign(last_index.step.value) if last_index.step is not None else 1 + start = last_index.start.value if last_index.start is not None else 0 + stop = last_index.end.value + buffer + step = last_index.step.value if last_index.step is not None else 1 + return (stop - start) // step + elif isinstance(last_index, DiscreteSet): + return len(last_index.values) + else: + raise TypeError(f"tuple indices must be integers or slices, not {type(last_index)}") + + def get_qubit_size(self, identifier: Union[Identifier, IndexedIdentifier]) -> int: + """Gets the number of qubit indices for the given identifier. + + Args: + identifier (Union[Identifier, IndexedIdentifier]): The identifier representing + the qubit indices. + + Returns: + int: The number of qubit indices contained in the given identifier. + """ + if isinstance(identifier, IndexedIdentifier): + indices = self.get_qubit_indices(identifier) + return self._get_indices_length(indices) + return len(self.get_by_identifier(identifier)) + + +class McmProgramContext(ProgramContext): + def __init__(self, circuit: Optional[Circuit] = None): + """ + Args: + circuit (Optional[Circuit]): A partially-built circuit to continue building with this + context. Default: None. + """ + super(ProgramContext, self).__init__() + self.qubit_mapping = QubitTable() + self.outputs = {} + self._circuit = circuit or Circuit() + + def pop_instructions(self) -> list[GateOperation]: + """Returns the list of instructions and removes them from the context. + + Returns: + list[GateOperation]: The list of instructions from the context. + """ + instructions = self.circuit.instructions + self.circuit.instructions = [] + return instructions + + def add_output(self, output_name: str) -> None: + """Adds an output with the given name. + + Args: + output_name (str): The output name to add. + """ + self.outputs[output_name] = [] + + def save_output_values(self) -> None: + """Saves the shot data to the outputs list. If no outputs have been added + explicitly, all symbols in the current scope are added to the outputs list.""" + if not self.outputs: + self.outputs = { + v: [] + for v in self.symbol_table.current_scope + if isinstance(self.get_type(v), ClassicalType) + } + for output, shot_data in self.outputs.items(): + shot_data.append(convert_to_output(self.get_value(output))) diff --git a/src/autoqasm/simulator/simulation.py b/src/autoqasm/simulator/simulation.py new file mode 100644 index 0000000..18b0fc4 --- /dev/null +++ b/src/autoqasm/simulator/simulation.py @@ -0,0 +1,65 @@ +# 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. + +import numpy as np +from braket.default_simulator import StateVectorSimulation +from braket.default_simulator.gate_operations import PauliX +from braket.default_simulator.linalg_utils import marginal_probability + +from autoqasm.simulator.linalg_utils import measurement_collapse_sv, measurement_sample + + +class Simulation(StateVectorSimulation): + def add_qubits(self, num_qubits: int) -> None: + """Adds the given number of qubits to the simulation. + + Args: + num_qubits (int): The number of qubits to add. + """ + expanded_dims = np.expand_dims(self.state_vector, -1) + expanded_qubits = np.append( + expanded_dims, np.zeros((expanded_dims.size, 2**num_qubits - 1)), axis=-1 + ) + self._state_vector = expanded_qubits.flatten() + self._qubit_count += num_qubits + + def measure(self, targets: tuple[int]) -> tuple[int]: + """Measures the specified qubits and returns the outcome. + + Args: + targets (tuple[int]): The qubit indices to measure. + + Returns: + tuple[int]: The measurement outcomes 0 or 1 for each measured qubit. + """ + mprob = marginal_probability(self.probabilities, targets) + outcome = measurement_sample(mprob, len(targets)) + self._state_vector = measurement_collapse_sv( + self._state_vector, + targets, + outcome, + ) + return outcome + + def reset(self) -> None: + """Resets the simulation and resets the qubit count to 0.""" + self._state_vector = np.array([1], dtype=complex) + self._qubit_count = 0 + + def flip(self, target: int) -> None: + """Performs a bit flip (PauliX operation) on the specified qubit. + + Args: + target (int): The qubit index on which to perform a bit flip. + """ + self.evolve([PauliX([target])]) diff --git a/src/autoqasm/simulator/simulator.py b/src/autoqasm/simulator/simulator.py new file mode 100644 index 0000000..60a9e98 --- /dev/null +++ b/src/autoqasm/simulator/simulator.py @@ -0,0 +1,86 @@ +# 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. + +from braket.default_simulator import StateVectorSimulator +from braket.ir.openqasm import Program as OpenQASMProgram +from braket.task_result import AdditionalMetadata, TaskMetadata +from braket.tasks import GateModelQuantumTaskResult + +from autoqasm.simulator.native_interpreter import NativeInterpreter +from autoqasm.simulator.program_context import McmProgramContext +from autoqasm.simulator.simulation import Simulation + + +class McmSimulator(StateVectorSimulator): + DEVICE_ID = "autoqasm" + + def initialize_simulation(self, **kwargs) -> Simulation: + """ + Initialize simulation with mid-circuit measurement (MCM) support. + + Args: + `**kwargs`: qubit_count, shots, batch_size + + Returns: + Simulation: Initialized simulation. + """ + qubit_count = kwargs.get("qubit_count") + shots = kwargs.get("shots") + batch_size = kwargs.get("batch_size") + return Simulation(qubit_count, shots, batch_size) + + def create_program_context(self) -> McmProgramContext: + return McmProgramContext() + + def run( + self, + openqasm_ir: OpenQASMProgram, + shots: int = 0, + *, + batch_size: int = 1, + ) -> GateModelQuantumTaskResult: + """Executes the program specified by the supplied `circuit_ir` on the simulator. + + Args: + openqasm_ir (OpenQASMProgram): ir representation of a program specifying the + instructions to execute. + shots (int): The number of times to run the circuit. + batch_size (int): The size of the circuit partitions to contract, + if applying multiple gates at a time is desired; see `StateVectorSimulation`. + Must be a positive integer. + Defaults to 1, which means gates are applied one at a time without any + optimized contraction. + Returns: + GateModelQuantumTaskResult: object that represents the result + + Raises: + ValueError: If result types are not specified in the IR or sample is specified + as a result type when shots=0. Or, if StateVector and Amplitude result types + are requested when shots>0. + """ + is_file = openqasm_ir.source.endswith(".qasm") + simulation = self.initialize_simulation(qubit_count=0, shots=shots, batch_size=batch_size) + interpreter = NativeInterpreter(simulation=simulation) + + context = interpreter.simulate( + source=openqasm_ir.source, + inputs=openqasm_ir.inputs, + is_file=is_file, + shots=shots, + ) + + return GateModelQuantumTaskResult( + task_metadata=TaskMetadata.construct(id="", shots=shots), + additional_metadata=AdditionalMetadata.construct(), + measurements=context, + ) diff --git a/test/resources/inputs.qasm b/test/resources/inputs.qasm new file mode 100644 index 0000000..7d779f6 --- /dev/null +++ b/test/resources/inputs.qasm @@ -0,0 +1,8 @@ +OPENQASM 3.0; +input float theta; +output bit return_value; +qubit[1] __qubits__; +rx(theta) __qubits__[0]; +bit __bit_0__; +__bit_0__ = measure __qubits__[0]; +return_value = __bit_0__; diff --git a/test/unit_tests/autoqasm/test_api.py b/test/unit_tests/autoqasm/test_api.py index 546716d..aa932d2 100644 --- a/test/unit_tests/autoqasm/test_api.py +++ b/test/unit_tests/autoqasm/test_api.py @@ -17,17 +17,17 @@ """ import pytest -from braket.default_simulator import StateVectorSimulator -from braket.devices.local_simulator import LocalSimulator +from braket.devices import LocalSimulator from braket.tasks.local_quantum_task import LocalQuantumTask import autoqasm as aq from autoqasm import errors from autoqasm.instructions import cnot, h, measure, rx, x +from autoqasm.simulator import McmSimulator def _test_on_local_sim(program: aq.Program, inputs=None) -> None: - device = LocalSimulator(backend=StateVectorSimulator()) + device = LocalSimulator(backend=McmSimulator()) task = device.run(program, shots=10, inputs=inputs or {}) assert isinstance(task, LocalQuantumTask) assert isinstance(task.result().measurements, dict) @@ -952,11 +952,11 @@ def empty_program() -> None: def test_to_ir_implicit_build(empty_program) -> None: """Test that to_ir works as expected with and without implicit build.""" expected = """OPENQASM 3.0;""" - assert empty_program.build().to_ir(allow_implicit_build=False) == expected - assert empty_program.build().to_ir(allow_implicit_build=True) == expected - assert empty_program.to_ir(allow_implicit_build=True) == expected + assert empty_program.build().to_ir(build_if_necessary=False) == expected + assert empty_program.build().to_ir(build_if_necessary=True) == expected + assert empty_program.to_ir(build_if_necessary=True) == expected with pytest.raises(RuntimeError): - empty_program.to_ir(allow_implicit_build=False) + empty_program.to_ir(build_if_necessary=False) def test_main_no_return(): diff --git a/test/unit_tests/autoqasm/test_parameters.py b/test/unit_tests/autoqasm/test_parameters.py index 14a15ad..a0e1d24 100644 --- a/test/unit_tests/autoqasm/test_parameters.py +++ b/test/unit_tests/autoqasm/test_parameters.py @@ -16,17 +16,17 @@ import numpy as np import pytest from braket.circuits import FreeParameter -from braket.default_simulator import StateVectorSimulator -from braket.devices.local_simulator import LocalSimulator +from braket.devices import LocalSimulator from braket.tasks.local_quantum_task import LocalQuantumTask import autoqasm as aq from autoqasm import pulse from autoqasm.instructions import cnot, cphaseshift, gpi, h, measure, ms, rx, rz, x +from autoqasm.simulator import McmSimulator def _test_parametric_on_local_sim(program: aq.Program, inputs: dict[str, float]) -> np.ndarray: - device = LocalSimulator(backend=StateVectorSimulator()) + device = LocalSimulator(backend=McmSimulator()) task = device.run(program, shots=100, inputs=inputs) assert isinstance(task, LocalQuantumTask) assert isinstance(task.result().measurements, dict) diff --git a/test/unit_tests/autoqasm/test_simulator.py b/test/unit_tests/autoqasm/test_simulator.py new file mode 100644 index 0000000..7322171 --- /dev/null +++ b/test/unit_tests/autoqasm/test_simulator.py @@ -0,0 +1,193 @@ +# 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. + +import pytest +from braket.ir.openqasm import Program as OpenQASMProgram +from braket.tasks import GateModelQuantumTaskResult + +from autoqasm.simulator import McmSimulator +from autoqasm.simulator.native_interpreter import NativeInterpreter +from autoqasm.simulator.program_context import McmProgramContext +from autoqasm.simulator.simulation import Simulation + +INPUTS_QASM = "test/resources/inputs.qasm" + + +def test_simulator_run(): + program = OpenQASMProgram(source="OPENQASM 3.0;") + simulator = McmSimulator() + result = simulator.run(program) + assert isinstance(result, GateModelQuantumTaskResult) + + +def test_simulator_create_program_context(): + simulator = McmSimulator() + program_context = simulator.create_program_context() + assert isinstance(program_context, McmProgramContext) + + +@pytest.mark.parametrize( + "reset_instructions", + ( + "for int q in [0:2 - 1] {\n reset __qubits__[q];\n}", + "array[int[32], 2] __arr__ = {0, 1};\nfor int q in __arr__ {\n reset __qubits__[q];\n}", + "reset __qubits__[0];\nreset __qubits__[1];", + "reset __qubits__;", + ), +) +def test_reset(reset_instructions): + qasm = f""" + OPENQASM 3.0; + qubit[2] __qubits__; + x __qubits__[0]; + {reset_instructions} + bit[2] __bit_0__ = "00"; + __bit_0__[0] = measure __qubits__[0]; + __bit_0__[1] = measure __qubits__[1]; + """ + result = NativeInterpreter(Simulation(0, 0, 1)).simulate(qasm) + assert result["__bit_0__"] == ["00"] + + +def test_inputs_outputs(): + with open(INPUTS_QASM, encoding="utf-8", mode="r") as f: + qasm = f.read() + + result = NativeInterpreter(Simulation(1, 1, 1)).simulate(qasm, inputs={"theta": 0.0}) + assert result["return_value"] == [0] + + +def test_inputs_outputs_from_file(): + result = NativeInterpreter(Simulation(1, 1, 1)).simulate( + INPUTS_QASM, inputs={"theta": 0.0}, is_file=True + ) + assert result["return_value"] == [0] + + +def test_missing_input(): + qasm = """ + OPENQASM 3.0; + input float theta; + """ + with pytest.raises(NameError, match="Missing input variable"): + NativeInterpreter(Simulation(0, 0, 1)).simulate(qasm) + + +def test_repeated_output_declaration(): + qasm = """ + OPENQASM 3.0; + output bit return_value; + output bit return_value; + return_value = 0; + """ + result = NativeInterpreter(Simulation(0, 0, 1)).simulate(qasm) + assert result["return_value"] == [0] + + +def test_qubit_register(): + qasm = """ + OPENQASM 3.0; + qubit[2] __qubits__; + x __qubits__[1]; + bit[2] __bit_0__ = measure __qubits__; + """ + result = NativeInterpreter(Simulation(2, 1, 1)).simulate(qasm) + assert result["__bit_0__"] == ["01"] + + +def test_qubit_index_out_of_range(): + qasm = """ + OPENQASM 3.0; + qubit[2] __qubits__; + x __qubits__[4]; + """ + with pytest.raises(IndexError, match="qubit register index `4` out of range"): + NativeInterpreter(Simulation(2, 1, 1)).simulate(qasm) + + +def test_qubit_index_out_of_range_symbolic(): + qasm = """ + OPENQASM 3.0; + qubit[2] __qubits__; + h __qubits__[2+pi-pi]; + bit[2] __bit_0__ = measure __qubits__; + """ + with pytest.raises(IndexError, match="qubit register index `2` out of range"): + NativeInterpreter(Simulation(2, 1, 1)).simulate(qasm) + + +def test_physical_qubit_identifier(): + qasm = """ + OPENQASM 3.0; + x $0; + bit __bit_0__ = measure $0; + """ + result = NativeInterpreter(Simulation(1, 1, 1)).simulate(qasm) + assert result["__bit_0__"] == [1] + + +def test_qubit_register_indexing(): + qasm = """ + OPENQASM 3.0; + input int index; + qubit[2] __qubits__; + x __qubits__[index]; + h __qubits__[0:1]; + h __qubits__[{0, index}]; + bit[2] __bit_0__ = measure __qubits__; + """ + result = NativeInterpreter(Simulation(1, 1, 1)).simulate(qasm, inputs={"index": 1}) + assert result["__bit_0__"] == ["01"] + + +@pytest.mark.parametrize("invalid_index", ("1.23", "pi", "{0, pi}")) +def test_qubit_register_invalid_index(invalid_index): + qasm = f""" + OPENQASM 3.0; + qubit[2] __qubits__; + x __qubits__[{invalid_index}]; + bit[2] __bit_0__ = measure __qubits__; + """ + with pytest.raises(TypeError, match="tuple indices must be integers or slices"): + NativeInterpreter(Simulation(1, 1, 1)).simulate(qasm) + + +@pytest.mark.parametrize("multi_dimension_index", ("[0, 1]", "[0][1][2]")) +def test_qubit_register_indexing_multi_dimensions(multi_dimension_index): + qasm = f""" + OPENQASM 3.0; + qubit[2] __qubits__; + x __qubits__{multi_dimension_index}; + """ + with pytest.raises(IndexError, match="Cannot index multiple dimensions for qubits."): + NativeInterpreter(Simulation(2, 1, 1)).simulate(qasm) + + +def test_mid_circuit_measurement(): + qasm = """ + OPENQASM 3.0; + qubit[2] __qubits__; + x __qubits__[1]; + bit __bit_0__ = measure __qubits__[1]; + if (__bit_0__) { + x __qubits__[0]; + __bit_0__ = measure __qubits__[0]; + if (__bit_0__) { + x __qubits__[1]; + } + } + bit[2] __bit_1__ = measure __qubits__; + """ + result = NativeInterpreter(Simulation(2, 1, 1)).simulate(qasm) + assert result["__bit_0__"] == [1] + assert result["__bit_1__"] == ["10"] diff --git a/tox.ini b/tox.ini index 4f451ae..7aaa3d4 100644 --- a/tox.ini +++ b/tox.ini @@ -133,5 +133,5 @@ commands = [test-deps] deps = # If you need to test on a certain branch, add @ after .git - git+https://github.com/amazon-braket/amazon-braket-sdk-python.git@ff73de68cf6ac2d0a921e8fe62693e5b9ae2e321 # mcm-sim branch - git+https://github.com/amazon-braket/amazon-braket-default-simulator-python.git@ab068c860963c29842d7649c741f88da669597eb # mcm-sim branch + git+https://github.com/amazon-braket/amazon-braket-sdk-python.git + git+https://github.com/amazon-braket/amazon-braket-default-simulator-python.git