From d3cb14e05ed54b7137e207c181e4639f498f31f5 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer <3620100+rmshaffer@users.noreply.github.com> Date: Mon, 13 May 2024 15:55:50 -0400 Subject: [PATCH] Add simulator module with mcm simulator implementation --- setup.py | 7 +- src/autoqasm/simulator/__init__.py | 24 ++ src/autoqasm/simulator/conversion.py | 52 +++++ src/autoqasm/simulator/linalg_utils.py | 93 ++++++++ src/autoqasm/simulator/native_interpreter.py | 151 ++++++++++++ src/autoqasm/simulator/program_context.py | 214 ++++++++++++++++++ src/autoqasm/simulator/simulation.py | 65 ++++++ src/autoqasm/simulator/simulator.py | 114 ++++++++++ test/unit_tests/autoqasm/test_api.py | 4 +- .../autoqasm/test_native_interpreter.py | 42 ++++ test/unit_tests/autoqasm/test_parameters.py | 4 +- tox.ini | 4 +- 12 files changed, 763 insertions(+), 11 deletions(-) create mode 100644 src/autoqasm/simulator/__init__.py create mode 100644 src/autoqasm/simulator/conversion.py create mode 100644 src/autoqasm/simulator/linalg_utils.py create mode 100644 src/autoqasm/simulator/native_interpreter.py create mode 100644 src/autoqasm/simulator/program_context.py create mode 100644 src/autoqasm/simulator/simulation.py create mode 100644 src/autoqasm/simulator/simulator.py create mode 100644 test/unit_tests/autoqasm/test_native_interpreter.py diff --git a/setup.py b/setup.py index a06cd03..677d153 100644 --- a/setup.py +++ b/setup.py @@ -25,11 +25,8 @@ 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 @ git+https://github.com/amazon-braket/amazon-braket-sdk-python.git@fe601096b7bba2a1bd8700896080d93672732c4c#egg=amazon-braket-sdk", # noqa E501 + "amazon-braket-default-simulator>=1.23.2", "oqpy~=0.3.5", "diastatic-malt", "numpy", diff --git a/src/autoqasm/simulator/__init__.py b/src/autoqasm/simulator/__init__.py new file mode 100644 index 0000000..b95509e --- /dev/null +++ b/src/autoqasm/simulator/__init__.py @@ -0,0 +1,24 @@ +# 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. + +""" +TODO: Module description + +TODO: Example usage + +.. code-block:: python + + # Python code here +""" + +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..acf92e3 --- /dev/null +++ b/src/autoqasm/simulator/conversion.py @@ -0,0 +1,52 @@ +# 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 TypeError(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(BitstringLiteral) +def _(value): + return np.array(np.binary_repr(value.value, value.width)) + + +@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..0b3d50c --- /dev/null +++ b/src/autoqasm/simulator/linalg_utils.py @@ -0,0 +1,93 @@ +# 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: float, target_count: int) -> tuple[int]: + """_summary_ + + Args: + prob (float): _description_ + target_count (int): _description_ + + Returns: + tuple[int]: _description_ + """ + 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_dm( + dm_tensor: np.ndarray, targets: Iterable[int], outcomes: np.ndarray +) -> np.ndarray: + """_summary_ + + Args: + dm_tensor (np.ndarray): _description_ + targets (Iterable[int]): _description_ + outcomes (np.ndarray): _description_ + + Returns: + np.ndarray: _description_ + """ + # TODO: This needs to be modified to not delete qubits + + # move the target qubit to the front of axes + qubit_count = int(np.log2(dm_tensor.shape[0])) + unused_idxs = [idx for idx in range(qubit_count) if idx not in targets] + unused_idxs = [ + p + i * qubit_count for i in range(2) for p in unused_idxs + ] # convert indices to dm form + target_indx = [ + p + i * qubit_count for i in range(2) for p in targets + ] # convert indices to dm form + permutation = target_indx + unused_idxs + inverse_permutation = np.argsort(permutation) + + # collapse the density matrix based on measuremnt outcome + outcomes = tuple(i for _ in range(2) for i in outcomes) + new_dm_tensor = np.zeros_like(dm_tensor) + new_dm_tensor[outcomes] = np.transpose(dm_tensor, permutation)[outcomes] + new_dm_tensor = np.transpose(new_dm_tensor, inverse_permutation) + + # normalize + new_trace = np.trace(np.reshape(new_dm_tensor, (2**qubit_count, 2**qubit_count))) + new_dm_tensor = new_dm_tensor / new_trace + return new_dm_tensor + + +def measurement_collapse_sv( + state_vector: np.ndarray, targets: Iterable[int], outcome: np.ndarray +) -> np.ndarray: + """_summary_ + + Args: + state_vector (np.ndarray): _description_ + targets (Iterable[int]): _description_ + outcome (np.ndarray): _description_ + + Returns: + np.ndarray: _description_ + """ + 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..9367f06 --- /dev/null +++ b/src/autoqasm/simulator/native_interpreter.py @@ -0,0 +1,151 @@ +# 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]: + """_summary_ + + Args: + source (str): _description_ + inputs (Optional[dict[str, Any]]): _description_. Defaults to None. + is_file (bool): _description_. Defaults to False. + shots (int): _description_. Defaults to 1. + + Returns: + dict[str, Any]: _description_ + """ + 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..ec9fc8a --- /dev/null +++ b/src/autoqasm/simulator/program_context.py @@ -0,0 +1,214 @@ +# 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 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]): _description_ + + Returns: + tuple[int]: _description_ + """ + 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): _description_ + + Raises: + IndexError: Qubit register index out of range for specified register. + + Returns: + tuple[int]: _description_ + """ + 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) + 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) + 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) 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]: + """_summary_ + + Args: + identifier (IndexedIdentifier): _description_ + + Raises: + IndexError: Index consists of multiple dimensions. + + Returns: + list[IntegerLiteral | RangeDefinition | DiscreteSet]: _description_ + """ + 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) + + def get_qubit_size(self, identifier: Union[Identifier, IndexedIdentifier]) -> int: + """_summary_ + + Args: + identifier (Union[Identifier, IndexedIdentifier]): _description_ + + Returns: + int: _description_ + """ + 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. + """ + self.outputs = {} + self._circuit = circuit or Circuit() + super(ProgramContext, self).__init__() + + def pop_instructions(self) -> list[GateOperation]: + """_summary_ + + Returns: + list[GateOperation]: _description_ + """ + instructions = self.circuit.instructions + self.circuit.instructions = [] + return instructions + + def add_output(self, output_name: str) -> None: + """_summary_ + + Args: + output_name (str): _description_ + """ + self.outputs[output_name] = [] + + def save_output_values(self) -> None: + """_summary_""" + 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..25eba9b --- /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: + """_summary_ + + Args: + num_qubits (int): _description_ + """ + 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]: + """_summary_ + + Args: + targets (tuple[int]): _description_ + + Returns: + tuple[int]: _description_ + """ + 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: + """_summary_""" + self._state_vector = np.array([1], dtype=complex) + self._qubit_count = 0 + + def flip(self, target: int) -> None: + """_summary_ + + Args: + target (int): _description_ + """ + self.evolve([PauliX([target])]) diff --git a/src/autoqasm/simulator/simulator.py b/src/autoqasm/simulator/simulator.py new file mode 100644 index 0000000..d9be8ab --- /dev/null +++ b/src/autoqasm/simulator/simulator.py @@ -0,0 +1,114 @@ +# 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.default_simulator.openqasm.circuit import Circuit +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_mcm" + + 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, + ) + + def _validate_input_provided(self, circuit: Circuit) -> None: + """ + Validate that requested circuit has all input parameters provided. + + Args: + circuit (Circuit): IR for the simulator. + + Raises: + NameError: If any the specified input parameters are not provided + """ + for instruction in circuit.instructions: + possible_parameters = "_angle", "_angle_1", "_angle_2" + for parameter_name in possible_parameters: + param = getattr(instruction, parameter_name, None) + if param is not None: + try: + float(param) + except TypeError: + missing_input = param.free_symbols.pop() + raise NameError(f"Missing input variable '{missing_input}'.") + for qubit in instruction.targets: + try: + float(qubit) + except TypeError: + missing_input = qubit.free_symbols.pop() + raise NameError(f"Missing input variable '{missing_input}'.") diff --git a/test/unit_tests/autoqasm/test_api.py b/test/unit_tests/autoqasm/test_api.py index 546716d..3d7b331 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.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) diff --git a/test/unit_tests/autoqasm/test_native_interpreter.py b/test/unit_tests/autoqasm/test_native_interpreter.py new file mode 100644 index 0000000..9b56b76 --- /dev/null +++ b/test/unit_tests/autoqasm/test_native_interpreter.py @@ -0,0 +1,42 @@ +# 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 autoqasm.simulator.native_interpreter import NativeInterpreter +from autoqasm.simulator.simulation import Simulation + + +@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]; + x __qubits__[1]; + {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"] diff --git a/test/unit_tests/autoqasm/test_parameters.py b/test/unit_tests/autoqasm/test_parameters.py index 14a15ad..9dc36a9 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.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/tox.ini b/tox.ini index 4f451ae..c4ca0c6 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@fe601096b7bba2a1bd8700896080d93672732c4c # mcm-sim branch + git+https://github.com/amazon-braket/amazon-braket-default-simulator-python.git