diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index a8b4f7b..93336e5 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -11,25 +11,33 @@ on: jobs: build: runs-on: ${{ matrix.os }} + timeout-minutes: 30 strategy: + # don't run all the jobs at once to avoid wasting CI + max-parallel: 2 matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.9", "3.10", "3.11"] steps: + - uses: actions/checkout@v4 + - name: Install juliaup + uses: julia-actions/install-juliaup@v2 + with: + channel: '1' + - name: Update Julia registry + shell: julia --project=. --color=yes {0} + run: | + using Pkg + Pkg.Registry.update() - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Get Python Simulator - uses: actions/checkout@v4 - with: - path: python_sim - name: Install dependencies run: | pip install tox - name: Run Tests run: | - cd python_sim tox -e unit-tests - name: Upload coverage report to Codecov uses: codecov/codecov-action@v4 diff --git a/pyproject.toml b/pyproject.toml index 9944c44..190197c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,38 +19,19 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Operating System :: OS Independent" ] -dynamic = ["version", "dependencies"] +dynamic = ["version", "dependencies", "optional-dependencies"] [project.entry-points."braket.simulators"] braket_sv_v2 = "braket.simulator_v2.state_vector_simulator_v2:StateVectorSimulatorV2" braket_dm_v2 = "braket.simulator_v2.density_matrix_simulator_v2:DensityMatrixSimulatorV2" -[project.optional-dependencies] -test = [ - "amazon-braket-pennylane-plugin", - "black", - "flake8", - "flake8-rst-docstrings", - "isort", - "pre-commit", - "pylint", - "pytest==7.1.2", - "pytest-benchmark", - "pytest-cov", - "pytest-rerunfailures", - "pytest-xdist", - "sphinx", - "sphinx-rtd-theme", - "sphinxcontrib-apidoc", - "tox" -] - [tool.setuptools] include-package-data=false package-data = {"*" = ["*.json"]} [tool.setuptools.dynamic] dependencies = {file = "requirements.txt"} +optional-dependencies.test = { file = "requirements-test.txt" } [tool.isort] profile = "black" diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..5e32098 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,20 @@ +amazon-braket-pennylane-plugin +black +flake8 +flake8-rst-docstrings +isort +pre-commit +pylint +pytest==7.1.2 +pytest-benchmark +pytest-cov +pytest-rerunfailures +pytest-timeout +pytest-xdist +qiskit==1.2.0 +qiskit-braket-provider==0.4.1 +qiskit-algorithms +sphinx +sphinx-rtd-theme +sphinxcontrib-apidoc +tox diff --git a/requirements.txt b/requirements.txt index 8a3645a..40f36cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -juliacall==0.9.20 +juliacall==0.9.22 numpy amazon-braket-schemas>=1.20.2 amazon-braket-sdk>=1.83.0 diff --git a/setup.cfg b/setup.cfg index f146a85..853c49e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ test=pytest [tool:pytest] xfail_strict = true addopts = - --verbose + --verbose -n auto --durations=0 --durations-min=1 --dist worksteal testpaths = test/unit_tests [flake8] diff --git a/src/braket/juliapkg.json b/src/braket/juliapkg.json index d91d444..9ad41c3 100644 --- a/src/braket/juliapkg.json +++ b/src/braket/juliapkg.json @@ -3,8 +3,11 @@ "packages": { "BraketSimulator": { "uuid": "76d27892-9a0b-406c-98e4-7c178e9b3dff", - "url": "https://github.com/amazon-braket/BraketSimulator.jl.git", - "rev": "main" + "version": "0.0.4" + }, + "JSON3": { + "uuid": "0f8b85d8-7281-11e9-16c2-39a750bddbf1", + "version": "1.14.0" } } } diff --git a/src/braket/simulator_v2/__init__.py b/src/braket/simulator_v2/__init__.py index 731dab4..92dc40a 100644 --- a/src/braket/simulator_v2/__init__.py +++ b/src/braket/simulator_v2/__init__.py @@ -1,7 +1,6 @@ from braket.simulator_v2.density_matrix_simulator_v2 import ( # noqa: F401 DensityMatrixSimulatorV2, ) -from braket.simulator_v2.julia_import import jl, jlBraketSimulator # noqa: F401 from braket.simulator_v2.state_vector_simulator_v2 import ( # noqa: F401 StateVectorSimulatorV2, ) diff --git a/src/braket/simulator_v2/base_simulator_v2.py b/src/braket/simulator_v2/base_simulator_v2.py index 8b876fe..5ab4bc8 100644 --- a/src/braket/simulator_v2/base_simulator_v2.py +++ b/src/braket/simulator_v2/base_simulator_v2.py @@ -1,140 +1,100 @@ -import sys -import threading -import warnings +import atexit +import json from collections.abc import Sequence -from typing import Any, Optional, Union +from multiprocessing.pool import Pool +from typing import Optional, Union import numpy as np -from braket.default_simulator.result_types import TargetedResultType from braket.default_simulator.simulator import BaseLocalSimulator -from braket.device_schema import DeviceActionType -from braket.ir.jaqcd import DensityMatrix, Probability -from braket.ir.jaqcd import Program as JaqcdProgram -from braket.ir.jaqcd import StateVector +from braket.ir.jaqcd import DensityMatrix, Probability, StateVector from braket.ir.openqasm import Program as OpenQASMProgram from braket.task_result import GateModelTaskResult -from juliacall import JuliaError -from braket.simulator_v2.julia_import import jl - - -class BaseLocalSimulatorV2(BaseLocalSimulator): - def __init__(self, device): - self._device = device - - def initialize_simulation(self, **kwargs): - return - - def _jaqcd_to_jl(self, circuit_ir: JaqcdProgram, shots: int): - qubit_map = BaseLocalSimulator._map_circuit_to_contiguous_qubits(circuit_ir) - qubit_count = len(qubit_map) - - self._validate_jaqcd(circuit_ir, qubit_count, shots) - BaseLocalSimulator._validate_shots_and_ir_results( - shots, circuit_ir.results, qubit_count +from braket.simulator_v2.julia_workers import ( + _handle_julia_error, + translate_and_run, + translate_and_run_multiple, +) + +__JULIA_POOL__ = None + + +def setup_julia(): + import os + import sys + + # don't reimport if we don't have to + if "juliacall" in sys.modules: + os.environ["PYTHON_JULIACALL_HANDLE_SIGNALS"] = "yes" + return sys.modules["juliacall"].Main + else: + for k, default in ( + ("PYTHON_JULIACALL_HANDLE_SIGNALS", "yes"), + ("PYTHON_JULIACALL_THREADS", "auto"), + ("PYTHON_JULIACALL_OPTLEVEL", "3"), + # let the user's Conda/Pip handle installing things + ("JULIA_CONDAPKG_BACKEND", "Null"), + ): + os.environ[k] = os.environ.get(k, default) + + import juliacall + + jl = juliacall.Main + jl.seval("using JSON3, BraketSimulator") + sv_stock_oq3 = """ + OPENQASM 3.0; + input float theta; + qubit[2] q; + h q[0]; + cnot q; + x q[0]; + xx(theta) q; + yy(theta) q; + zz(theta) q; + #pragma braket result expectation z(q[0]) + """ + dm_stock_oq3 = """ + OPENQASM 3.0; + input float theta; + qubit[2] q; + h q[0]; + x q[0]; + cnot q; + xx(theta) q; + yy(theta) q; + zz(theta) q; + #pragma braket noise bit_flip(0.1) q[0] + #pragma braket result probability + """ + r = jl.BraketSimulator.simulate( + "braket_sv_v2", sv_stock_oq3, '{"theta": 0.1}', 0 ) - - if not shots and circuit_ir.results: - result_types = BaseLocalSimulator._translate_result_types( - circuit_ir.results - ) - BaseLocalSimulator._validate_result_types_qubits_exist( - [ - result_type - for result_type in result_types - if isinstance(result_type, TargetedResultType) - ], - qubit_count, - ) - # convert to the Julia JaqcdProgram type for dispatch - jl_program = jl.BraketSimulator.Braket.parse_raw_schema( - jl.convert(jl.String, circuit_ir.json()) + jl.JSON3.write(r) + r = jl.BraketSimulator.simulate( + "braket_dm_v2", dm_stock_oq3, '{"theta": 0.1}', 0 ) - if circuit_ir.basis_rotation_instructions: - # need to read these in explicitly due to - # Vector{Any} type of field - parsed_bris = [ - jl.BraketSimulator.Braket.JSON3.read( - bri.json(), - jl.BraketSimulator.Braket.Instruction, - ) - for bri in circuit_ir.basis_rotation_instructions - ] - jl_program = jl.BraketSimulator.Braket.Program( - jl_program.braketSchemaHeader, - # concatenate these to make sure "extra" qubits are picked up - jl.vcat(jl_program.instructions, parsed_bris), - jl_program.results, - [], - ) - return jl_program, qubit_count + jl.JSON3.write(r) + return - def run_jaqcd( - self, - circuit_ir: JaqcdProgram, - qubit_count: Any = None, - shots: int = 0, - batch_size: int = 1, # unused - ) -> GateModelTaskResult: - """Executes the circuit specified by the supplied `circuit_ir` on the simulator. - Args: - circuit_ir (Program): ir representation of a braket circuit specifying the - instructions to execute. - qubit_count (Any): Unused parameter; in signature for backwards-compatibility - shots (int): The number of times to run the circuit. - batch_size (int): Unused parameter; in signature for backwards-compatibility +def setup_pool(): + global __JULIA_POOL__ + __JULIA_POOL__ = Pool(processes=1) + __JULIA_POOL__.apply(setup_julia) + atexit.register(__JULIA_POOL__.join) + atexit.register(__JULIA_POOL__.close) + return - Returns: - GateModelTaskResult: 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. - """ - _validate_thread() - if qubit_count is not None: - warnings.warn( - f"qubit_count is deprecated for {type(self).__name__} and can be set to None" - ) - translated_ir, qubit_count = self._jaqcd_to_jl(circuit_ir, shots) - try: - r = jl.simulate(self._device, translated_ir, qubit_count, shots) - except JuliaError as e: - _handle_julia_error(e) - r.additionalMetadata.action = circuit_ir - r = _result_value_to_ndarray(r) - return r +class BaseLocalSimulatorV2(BaseLocalSimulator): + def __init__(self, device: str): + global __JULIA_POOL__ + if __JULIA_POOL__ is None: + setup_pool() + self._device = device - def _openqasm_to_jl(self, openqasm_ir: OpenQASMProgram): - # convert to the Julia OpenQasmProgram type for dispatch - jl_braket_schema_header = jl.BraketSimulator.Braket.braketSchemaHeader( - jl.convert(jl.String, openqasm_ir.braketSchemaHeader.name), - jl.convert(jl.String, openqasm_ir.braketSchemaHeader.version), - ) - if openqasm_ir.inputs: - jl_inputs = jl.Dict( - [ - ( - jl.convert(jl.String, input_key), - ( - jl.convert(jl.String, input_val) - if isinstance(input_val, str) - else jl.convert(jl.Number, input_val) - ), - ) - for (input_key, input_val) in openqasm_ir.inputs.items() - ] - ) - else: - jl_inputs = jl.nothing - jl_source = jl.convert(jl.String, openqasm_ir.source) - return jl.BraketSimulator.Braket.OpenQasmProgram( - jl_braket_schema_header, - jl_source, - jl_inputs, - ) + def initialize_simulation(self, **kwargs): + return def run_openqasm( self, @@ -157,105 +117,69 @@ def run_openqasm( as a result type when shots=0. Or, if StateVector and Amplitude result types are requested when shots>0. """ - _validate_thread() + global __JULIA_POOL__ try: - r = jl.simulate(self._device, self._openqasm_to_jl(openqasm_ir), shots) - except JuliaError as e: + jl_result = __JULIA_POOL__.apply( + translate_and_run, + [self._device, openqasm_ir, shots], + ) + except Exception as e: _handle_julia_error(e) - r.additionalMetadata.action = openqasm_ir + + result = GateModelTaskResult(**json.loads(jl_result)) + jl_result = None + result.additionalMetadata.action = openqasm_ir + # attach the result types if not shots: - r = _result_value_to_ndarray(r) + result = _result_value_to_ndarray(result) else: - r.resultTypes = [rt.type for rt in r.resultTypes] - return r - - def _ir_list_to_jl( - self, payloads: list[Union[OpenQASMProgram, JaqcdProgram]], shots: int - ): - converted_payloads = [ - ( - self._openqasm_to_jl(ir) - if isinstance(ir, OpenQASMProgram) - else self._jaqcd_to_jl(ir, shots)[0] - ) - for ir in payloads - ] - return converted_payloads + result.resultTypes = [rt.type for rt in result.resultTypes] + return result def run_multiple( self, - programs: Sequence[Union[OpenQASMProgram, JaqcdProgram]], + programs: Sequence[OpenQASMProgram], max_parallel: Optional[int] = -1, shots: Optional[int] = 0, - inputs: Optional[Union[dict, Sequence[dict]]] = None, + inputs: Optional[Union[dict, Sequence[dict]]] = {}, ) -> list[GateModelTaskResult]: """ Run the tasks specified by the given IR programs. Extra arguments will contain any additional information necessary to run the tasks, such as the extra parameters for AHS simulations. Args: - programs (Sequence[Union[OQ3Program, JaqcdProgram]]): The IR representations - of the programs + programs (Sequence[OQ3Program]): The IR representations of the programs max_parallel (Optional[int]): The maximum number of programs to run in parallel. Default is the number of logical CPUs. Returns: list[GateModelTaskResult]: A list of result objects, with the ith object being the result of the ith program. """ - _validate_thread() + global __JULIA_POOL__ try: - results = jl.simulate( - self._device, - self._ir_list_to_jl(programs, shots), - max_parallel=max_parallel, - shots=shots, - inputs=inputs, + jl_results = __JULIA_POOL__.apply( + translate_and_run_multiple, + [self._device, programs, shots, inputs], ) - except JuliaError as e: + except Exception as e: _handle_julia_error(e) + results = [ + GateModelTaskResult(**json.loads(jl_result)) for jl_result in jl_results + ] + jl_results = None + for p_ix, program in enumerate(programs): + results[p_ix].additionalMetadata.action = program + for r_ix, result in enumerate(results): - results[r_ix].additionalMetadata.action = programs[r_ix] # attach the result types if not shots: results[r_ix] = _result_value_to_ndarray(result) else: - if isinstance(programs[r_ix], OpenQASMProgram): - results[r_ix].resultTypes = [rt.type for rt in result.resultTypes] - elif isinstance(programs[r_ix], JaqcdProgram): - results[r_ix] = _result_value_to_ndarray(result) - + results[r_ix].resultTypes = [rt.type for rt in result.resultTypes] return results - def _validate_jaqcd(self, circuit_ir, qubit_count: int, shots: int): - self._validate_ir_results_compatibility( - circuit_ir.results, - device_action_type=DeviceActionType.JAQCD, - ) - try: - # the default simulator has validations that hard-codes recommendations. We need - # to override them to correspond to the v2 simulator. Temporarily catching error - # but we need to split this function out in the base. - self._validate_ir_instructions_compatibility( - circuit_ir, - device_action_type=DeviceActionType.JAQCD, - ) - except TypeError: - raise TypeError( - "Noise instructions are not supported by the state vector simulator (by default). " - "You need to use the density matrix simulator: " - 'LocalSimulator("braket_dm_v2").' - ) - - -def _validate_thread(): - if threading.current_thread() is not threading.main_thread(): - raise RuntimeError( - "Simulations must be run from the Main thread. " - "For multiple simulations, please use run_batch() instead." - ) - def _result_value_to_ndarray( task_result: GateModelTaskResult, @@ -264,22 +188,34 @@ def _result_value_to_ndarray( np.ndarray. This must be done because the wrapper Julia simulator results Python lists to comply with the pydantic specification for ResultTypeValues. """ - for result_ind, result_type in enumerate(task_result.resultTypes): - if isinstance(result_type.type, (StateVector, DensityMatrix, Probability)): - task_result.resultTypes[result_ind].value = np.asarray( - task_result.resultTypes[result_ind].value - ) - return task_result - -def _handle_julia_error(julia_error: JuliaError): - try: - python_exception = getattr(julia_error.exception, "alternate_type", None) - if python_exception is None: - error = julia_error + def reconstruct_complex(v): + if isinstance(v, list): + return complex(v[0], v[1]) else: - class_val = getattr(sys.modules["builtins"], str(python_exception)) - error = class_val(julia_error.exception.message) - except Exception: - raise julia_error - raise error + return v + + for result_ind, result_type in enumerate(task_result.resultTypes): + # Amplitude + if isinstance(result_type.value, dict): + val = task_result.resultTypes[result_ind].value + task_result.resultTypes[result_ind].value = { + k: reconstruct_complex(v) for (k, v) in val.items() + } + if isinstance(result_type.type, StateVector): + val = task_result.resultTypes[result_ind].value + # complex are stored as tuples of reals + fixed_val = [reconstruct_complex(v) for v in val] + task_result.resultTypes[result_ind].value = np.asarray(fixed_val) + if isinstance(result_type.type, DensityMatrix): + val = task_result.resultTypes[result_ind].value + # complex are stored as tuples of reals + fixed_val = [ + [reconstruct_complex(v) for v in inner_val] for inner_val in val + ] + task_result.resultTypes[result_ind].value = np.asarray(fixed_val) + if isinstance(result_type.type, Probability): + val = task_result.resultTypes[result_ind].value + task_result.resultTypes[result_ind].value = np.asarray(val) + + return task_result diff --git a/src/braket/simulator_v2/density_matrix_simulator_v2.py b/src/braket/simulator_v2/density_matrix_simulator_v2.py index 027aefb..1a849a8 100644 --- a/src/braket/simulator_v2/density_matrix_simulator_v2.py +++ b/src/braket/simulator_v2/density_matrix_simulator_v2.py @@ -6,7 +6,6 @@ ) from braket.simulator_v2.base_simulator_v2 import BaseLocalSimulatorV2 -from braket.simulator_v2.julia_import import jlBraketSimulator class DensityMatrixSimulatorV2(BaseLocalSimulatorV2): @@ -19,7 +18,7 @@ class DensityMatrixSimulatorV2(BaseLocalSimulatorV2): DEVICE_ID = "braket_dm_v2" def __init__(self): - super().__init__(jlBraketSimulator.DensityMatrixSimulator(0, 0)) + super().__init__(self.DEVICE_ID) @property def properties(self) -> GateModelSimulatorDeviceCapabilities: @@ -172,87 +171,6 @@ def properties(self) -> GateModelSimulatorDeviceCapabilities: "supportsUnassignedMeasurements": True, "disabledQubitRewiringSupported": False, }, - "braket.ir.jaqcd.program": { - "actionType": "braket.ir.jaqcd.program", - "version": ["1"], - "supportedOperations": [ - "amplitude_damping", - "bit_flip", - "ccnot", - "cnot", - "cphaseshift", - "cphaseshift00", - "cphaseshift01", - "cphaseshift10", - "cswap", - "cv", - "cy", - "cz", - "depolarizing", - "ecr", - "generalized_amplitude_damping", - "h", - "i", - "iswap", - "kraus", - "pauli_channel", - "two_qubit_pauli_channel", - "phase_flip", - "phase_damping", - "phaseshift", - "pswap", - "rx", - "ry", - "rz", - "s", - "si", - "swap", - "t", - "ti", - "two_qubit_dephasing", - "two_qubit_depolarizing", - "unitary", - "v", - "vi", - "x", - "xx", - "xy", - "y", - "yy", - "z", - "zz", - ], - "supportedResultTypes": [ - { - "name": "Sample", - "observables": observables, - "minShots": 1, - "maxShots": max_shots, - }, - { - "name": "Expectation", - "observables": observables, - "minShots": 0, - "maxShots": max_shots, - }, - { - "name": "Variance", - "observables": observables, - "minShots": 0, - "maxShots": max_shots, - }, - { - "name": "Probability", - "minShots": 0, - "maxShots": max_shots, - }, - { - "name": "DensityMatrix", - "minShots": 0, - "maxShots": 0, - }, - ], - }, }, "paradigm": {"qubitCount": qubit_count}, "deviceParameters": GateModelSimulatorDeviceParameters.schema(), diff --git a/src/braket/simulator_v2/julia_import.py b/src/braket/simulator_v2/julia_import.py deleted file mode 100644 index 94d5c6c..0000000 --- a/src/braket/simulator_v2/julia_import.py +++ /dev/null @@ -1,37 +0,0 @@ -import os -import warnings - -import juliacall - -# Check if JuliaCall is already loaded, and if so, warn the user -# about the relevant environment variables. If not loaded, -# set up sensible defaults. -# Required to avoid segfaults (https://juliapy.github.io/PythonCall.jl/dev/faq/) -if os.environ.get("PYTHON_JULIACALL_HANDLE_SIGNALS", "yes") != "yes": - warnings.warn( - "`PYTHON_JULIACALL_HANDLE_SIGNALS` environment variable " - + "is set to something other than 'yes' or ''. " - + "You will experience segfaults if running with Julia multithreading." - ) - -if os.environ.get("PYTHON_JULIACALL_THREADS", "auto") != "auto": - warnings.warn( - "`PYTHON_JULIACALL_THREADS` environment variable is set to " - + "something other than `auto`, so `amazon-braket-simulator-v2` " - + "was not able to set it." - ) - -for k, default in ( - ("PYTHON_JULIACALL_HANDLE_SIGNALS", "yes"), - ("PYTHON_JULIACALL_THREADS", "auto"), - ("PYTHON_JULIACALL_OPTLEVEL", "3"), - # let the user's Conda/Pip handle installing things - ("JULIA_CONDAPKG_BACKEND", "Null"), -): - os.environ[k] = os.environ.get(k, default) - -jl = juliacall.Base.Module() - -jl.seval("using PythonCall, BraketSimulator") -jl.seval("using PythonCall: Py, pyconvert") -jlBraketSimulator = jl.BraketSimulator diff --git a/src/braket/simulator_v2/julia_workers.py b/src/braket/simulator_v2/julia_workers.py new file mode 100644 index 0000000..6189f7d --- /dev/null +++ b/src/braket/simulator_v2/julia_workers.py @@ -0,0 +1,81 @@ +import json +import sys +from collections.abc import Sequence +from typing import List, Optional, Union + +from braket.ir.openqasm import Program as OpenQASMProgram + + +def _handle_julia_error(error): + # in case juliacall isn't loaded + if type(error).__name__ == "JuliaError": + python_exception = getattr(error.exception, "alternate_type", None) + if python_exception is None: + # convert to RuntimeError as JuliaError can't be serialized + py_error = RuntimeError( + "Unable to unwrap internal Julia exception." + f"Exception message: {str(error.exception.message)}" + ) + else: + class_val = getattr(sys.modules["builtins"], str(python_exception)) + py_error = class_val(str(error.exception.message)) + raise py_error + else: + raise error + return + + +def translate_and_run( + device_id: str, openqasm_ir: OpenQASMProgram, shots: int = 0 +) -> str: + jl = sys.modules["juliacall"].Main + jl_shots = shots + jl_inputs = json.dumps(openqasm_ir.inputs) if openqasm_ir.inputs else "{}" + try: + result = jl.BraketSimulator.simulate( + device_id, + openqasm_ir.source, + jl_inputs, + jl_shots, + ) + + except Exception as e: + _handle_julia_error(e) + + return result + + +def translate_and_run_multiple( + device_id: str, + programs: Sequence[OpenQASMProgram], + shots: int = 0, + inputs: Optional[Union[dict, Sequence[dict]]] = None, +) -> List[str]: + inputs = inputs or {} + jl = sys.modules["juliacall"].Main + irs = [program.source for program in programs] + py_inputs = {} + if len(inputs) > 1 or isinstance(inputs, dict): + py_inputs = [inputs.copy() for _ in range(len(programs))] + elif len(inputs) == 1: + py_inputs = [inputs[0].copy() for _ in range(len(programs))] + full_inputs = [] + for p_ix, program in enumerate(programs): + if program.inputs: + full_inputs.append(program.inputs | py_inputs[p_ix]) + else: + full_inputs.append(py_inputs[p_ix]) + + jl_inputs = json.dumps(full_inputs) + + try: + results = jl.BraketSimulator.simulate( + device_id, + irs, + jl_inputs, + shots, + ) + py_results = [str(result) for result in results] + except Exception as e: + _handle_julia_error(e) + return py_results diff --git a/src/braket/simulator_v2/state_vector_simulator_v2.py b/src/braket/simulator_v2/state_vector_simulator_v2.py index 990f3a2..591d47e 100644 --- a/src/braket/simulator_v2/state_vector_simulator_v2.py +++ b/src/braket/simulator_v2/state_vector_simulator_v2.py @@ -6,7 +6,6 @@ ) from braket.simulator_v2.base_simulator_v2 import BaseLocalSimulatorV2 -from braket.simulator_v2.julia_import import jlBraketSimulator class StateVectorSimulatorV2(BaseLocalSimulatorV2): @@ -19,7 +18,7 @@ class StateVectorSimulatorV2(BaseLocalSimulatorV2): DEVICE_ID = "braket_sv_v2" def __init__(self): - super().__init__(jlBraketSimulator.StateVectorSimulator(0, 0)) + super().__init__(self.DEVICE_ID) @property def properties(self) -> GateModelSimulatorDeviceCapabilities: @@ -45,86 +44,6 @@ def properties(self) -> GateModelSimulatorDeviceCapabilities: "shotsRange": [0, max_shots], }, "action": { - "braket.ir.jaqcd.program": { - "actionType": "braket.ir.jaqcd.program", - "version": ["1"], - "supportedOperations": [ - "ccnot", - "cnot", - "cphaseshift", - "cphaseshift00", - "cphaseshift01", - "cphaseshift10", - "cswap", - "cv", - "cy", - "cz", - "ecr", - "h", - "i", - "iswap", - "pswap", - "phaseshift", - "rx", - "ry", - "rz", - "s", - "si", - "swap", - "t", - "ti", - "unitary", - "v", - "vi", - "x", - "xx", - "xy", - "y", - "yy", - "z", - "zz", - ], - "supportedResultTypes": [ - { - "name": "Sample", - "observables": observables, - "minShots": 1, - "maxShots": max_shots, - }, - { - "name": "Expectation", - "observables": observables, - "minShots": 0, - "maxShots": max_shots, - }, - { - "name": "Variance", - "observables": observables, - "minShots": 0, - "maxShots": max_shots, - }, - { - "name": "Probability", - "minShots": 0, - "maxShots": max_shots, - }, - { - "name": "StateVector", - "minShots": 0, - "maxShots": 0, - }, - { - "name": "DensityMatrix", - "minShots": 0, - "maxShots": 0, - }, - { - "name": "Amplitude", - "minShots": 0, - "maxShots": 0, - }, - ], - }, "braket.ir.openqasm.program": { "actionType": "braket.ir.openqasm.program", "version": ["1"], diff --git a/test/unit_tests/braket/simulator_v2/test_density_matrix_simulator_v2.py b/test/unit_tests/braket/simulator_v2/test_density_matrix_simulator_v2.py index 6c56ee0..63458f8 100644 --- a/test/unit_tests/braket/simulator_v2/test_density_matrix_simulator_v2.py +++ b/test/unit_tests/braket/simulator_v2/test_density_matrix_simulator_v2.py @@ -12,10 +12,8 @@ # language governing permissions and limitations under the License. import cmath -import json import sys from collections import Counter, namedtuple -from unittest.mock import patch import numpy as np import pytest @@ -24,7 +22,6 @@ GateModelSimulatorDeviceParameters, ) from braket.ir.jaqcd import DensityMatrix, Expectation, Probability -from braket.ir.jaqcd import Program as JaqcdProgram from braket.ir.openqasm import Program as OpenQASMProgram from braket.task_result import AdditionalMetadata, TaskMetadata @@ -33,83 +30,50 @@ CircuitData = namedtuple("CircuitData", "circuit_ir probability_zero") -@pytest.fixture(params=["OpenQASM", "Jaqcd"]) +@pytest.fixture( + params=[ + "OpenQASM", + ] +) def ir_type(request): return request.param @pytest.fixture def noisy_circuit_2_qubit(): - return ( - JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "x", "target": 0}, - {"type": "x", "target": 1}, - {"type": "bit_flip", "target": 1, "probability": 0.1}, - ] - } - ) - ) - if ir_type == "Jaqcd" - else OpenQASMProgram( - source=""" + return OpenQASMProgram( + source=""" OPENQASM 3.0; qubit[2] q; x q; #pragma braket noise bit_flip(.1) q[1] """ - ) ) @pytest.fixture def grcs_8_qubit(ir_type): - if ir_type == "Jaqcd": - with open("test/resources/grcs_8.json") as circuit_file: - data = json.load(circuit_file) - return CircuitData( - JaqcdProgram.parse_raw(json.dumps(data["ir"])), - data["probability_zero"], - ) return CircuitData(OpenQASMProgram(source="test/resources/grcs_8.qasm"), 0.0007324) @pytest.fixture def bell_ir(ir_type): - return ( - JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "cnot", "target": 1, "control": 0}, - ] - } - ) - ) - if ir_type == "Jaqcd" - else OpenQASMProgram( - source=""" + return OpenQASMProgram( + source=""" OPENQASM 3.0; qubit[2] q; h q[0]; cnot q[0], q[1]; """ - ) ) def test_simulator_run_noisy_circuit(noisy_circuit_2_qubit, caplog): simulator = DensityMatrixSimulator() shots_count = 10000 - if isinstance(noisy_circuit_2_qubit, JaqcdProgram): - result = simulator.run(noisy_circuit_2_qubit, qubit_count=2, shots=shots_count) - else: - result = simulator.run(noisy_circuit_2_qubit, shots=shots_count) + result = simulator.run(noisy_circuit_2_qubit, shots=shots_count) assert all([len(measurement) == 2] for measurement in result.measurements) assert len(result.measurements) == shots_count @@ -131,10 +95,7 @@ def test_simulator_run_noisy_circuit(noisy_circuit_2_qubit, caplog): def test_simulator_run_bell_pair(bell_ir, caplog): simulator = DensityMatrixSimulator() shots_count = 10000 - if isinstance(bell_ir, JaqcdProgram): - result = simulator.run(bell_ir, qubit_count=2, shots=shots_count) - else: - result = simulator.run(bell_ir, shots=shots_count) + result = simulator.run(bell_ir, shots=shots_count) assert all([len(measurement) == 2] for measurement in result.measurements) assert len(result.measurements) == shots_count @@ -156,18 +117,12 @@ def test_simulator_run_bell_pair(bell_ir, caplog): def test_simulator_run_no_results_no_shots(bell_ir): simulator = DensityMatrixSimulator() with pytest.raises(ValueError): - if isinstance(bell_ir, JaqcdProgram): - simulator.run(bell_ir, qubit_count=2, shots=0) - else: - simulator.run(bell_ir, shots=0) + simulator.run(bell_ir, shots=0) def test_simulator_run_grcs_8(grcs_8_qubit): simulator = DensityMatrixSimulator() - if isinstance(grcs_8_qubit.circuit_ir, JaqcdProgram): - result = simulator.run(grcs_8_qubit.circuit_ir, qubit_count=8, shots=0) - else: - result = simulator.run(grcs_8_qubit.circuit_ir, shots=0) + result = simulator.run(grcs_8_qubit.circuit_ir, shots=0) density_matrix = result.resultTypes[0].value assert cmath.isclose( density_matrix[0][0].real, grcs_8_qubit.probability_zero, abs_tol=1e-7 @@ -325,87 +280,6 @@ def test_properties(): "supportsUnassignedMeasurements": True, "disabledQubitRewiringSupported": False, }, - "braket.ir.jaqcd.program": { - "actionType": "braket.ir.jaqcd.program", - "version": ["1"], - "supportedOperations": [ - "amplitude_damping", - "bit_flip", - "ccnot", - "cnot", - "cphaseshift", - "cphaseshift00", - "cphaseshift01", - "cphaseshift10", - "cswap", - "cv", - "cy", - "cz", - "depolarizing", - "ecr", - "generalized_amplitude_damping", - "h", - "i", - "iswap", - "kraus", - "pauli_channel", - "two_qubit_pauli_channel", - "phase_flip", - "phase_damping", - "phaseshift", - "pswap", - "rx", - "ry", - "rz", - "s", - "si", - "swap", - "t", - "ti", - "two_qubit_dephasing", - "two_qubit_depolarizing", - "unitary", - "v", - "vi", - "x", - "xx", - "xy", - "y", - "yy", - "z", - "zz", - ], - "supportedResultTypes": [ - { - "name": "Sample", - "observables": observables, - "minShots": 1, - "maxShots": max_shots, - }, - { - "name": "Expectation", - "observables": observables, - "minShots": 0, - "maxShots": max_shots, - }, - { - "name": "Variance", - "observables": observables, - "minShots": 0, - "maxShots": max_shots, - }, - { - "name": "Probability", - "minShots": 0, - "maxShots": max_shots, - }, - { - "name": "DensityMatrix", - "minShots": 0, - "maxShots": 0, - }, - ], - }, }, "paradigm": {"qubitCount": qubit_count}, "deviceParameters": GateModelSimulatorDeviceParameters.schema(), @@ -441,24 +315,6 @@ def test_openqasm_density_matrix_simulator(): @pytest.fixture def bell_ir_with_result(ir_type): def _bell_ir_with_result(targets=None): - if ir_type == "Jaqcd": - return JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "cnot", "target": 1, "control": 0}, - ], - "results": [ - { - "type": "expectation", - "observable": ["x"], - "targets": targets, - }, - ], - } - ) - ) if targets is None: observable_string = "x all" elif len(targets) == 1: @@ -480,25 +336,13 @@ def _bell_ir_with_result(targets=None): return _bell_ir_with_result -@pytest.mark.parametrize("result_type", invalid_ir_result_types) -def test_simulator_run_invalid_ir_result_types(result_type): - simulator = DensityMatrixSimulator() - ir = JaqcdProgram.parse_raw( - json.dumps( - {"instructions": [{"type": "h", "target": 0}], "results": [result_type]} - ) - ) - with pytest.raises(TypeError): - simulator.run(ir, qubit_count=2, shots=100) - - @pytest.mark.parametrize( "result_type", - ( + [ "#pragma braket result state_vector", "#pragma braket result density_matrix", '#pragma braket result amplitude "0"', - ), + ], ) def test_simulator_run_invalid_ir_result_types_openqasm(result_type): simulator = DensityMatrixSimulator() @@ -515,14 +359,6 @@ def test_simulator_run_invalid_ir_result_types_openqasm(result_type): def test_simulator_run_densitymatrix_shots(): simulator = DensityMatrixSimulator() - jaqcd = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [{"type": "h", "target": 0}], - "results": [{"type": "densitymatrix"}], - } - ) - ) qasm = OpenQASMProgram( source=""" qubit q; @@ -530,31 +366,12 @@ def test_simulator_run_densitymatrix_shots(): #pragma braket result density_matrix """ ) - with pytest.raises(ValueError): - simulator.run(jaqcd, qubit_count=2, shots=100) with pytest.raises(ValueError): simulator.run(qasm, shots=100) def test_simulator_run_result_types_shots(caplog): simulator = DensityMatrixSimulator() - jaqcd = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "cnot", "target": 1, "control": 0}, - ], - "results": [ - { - "type": "expectation", - "observable": ["z"], - "targets": [1], - } - ], - } - ) - ) qasm = OpenQASMProgram( source=""" qubit[2] q; @@ -564,36 +381,16 @@ def test_simulator_run_result_types_shots(caplog): """ ) shots_count = 100 - jaqcd_result = simulator.run(jaqcd, qubit_count=2, shots=shots_count) qasm_result = simulator.run(qasm, shots=shots_count) - for result in jaqcd_result, qasm_result: + for result in (qasm_result,): assert all([len(measurement) == 2] for measurement in result.measurements) assert len(result.measurements) == shots_count assert result.measuredQubits == [0, 1] - assert not jaqcd_result.resultTypes assert not caplog.text def test_simulator_run_result_types_shots_basis_rotation_gates(caplog): simulator = DensityMatrixSimulator() - jaqcd = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "cnot", "target": 1, "control": 0}, - ], - "basis_rotation_instructions": [{"type": "h", "target": 1}], - "results": [ - { - "type": "expectation", - "observable": ["x"], - "targets": [1], - } - ], - } - ) - ) qasm = OpenQASMProgram( source=""" qubit[2] q; @@ -603,45 +400,19 @@ def test_simulator_run_result_types_shots_basis_rotation_gates(caplog): """ ) shots_count = 1000 - jaqcd_result = simulator.run(jaqcd, qubit_count=2, shots=shots_count) qasm_result = simulator.run(qasm, shots=shots_count) - for result in jaqcd_result, qasm_result: + for result in (qasm_result,): assert all([len(measurement) == 2] for measurement in result.measurements) assert len(result.measurements) == shots_count assert result.measuredQubits == [0, 1] - assert not jaqcd_result.resultTypes assert not caplog.text -def test_simulator_run_result_types_shots_basis_rotation_gates_value_error(): - simulator = DensityMatrixSimulator() - with pytest.raises(ValueError): - ir = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "cnot", "target": 1, "control": 0}, - ], - "basis_rotation_instructions": [{"type": "foo", "target": 1}], - "results": [ - {"type": "expectation", "observable": ["x"], "targets": [1]} - ], - } - ) - ) - shots_count = 1000 - simulator.run(ir, qubit_count=2, shots=shots_count) - - @pytest.mark.parametrize("targets", [(None), ([1]), ([0])]) def test_simulator_bell_pair_result_types(bell_ir_with_result, targets, caplog): simulator = DensityMatrixSimulator() ir = bell_ir_with_result(targets) - if isinstance(ir, JaqcdProgram): - result = simulator.run(ir, qubit_count=2, shots=0) - else: - result = simulator.run(ir, shots=0) + result = simulator.run(ir, shots=0) assert len(result.resultTypes) == 1 expected_expectation = Expectation(observable=["x"], targets=targets) assert result.resultTypes[0].type == expected_expectation @@ -657,14 +428,6 @@ def test_simulator_bell_pair_result_types(bell_ir_with_result, targets, caplog): def test_simulator_fails_samples_0_shots(): simulator = DensityMatrixSimulator() - jaqcd = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [{"type": "h", "target": 0}], - "results": [{"type": "sample", "observable": ["x"], "targets": [0]}], - } - ) - ) qasm = OpenQASMProgram( source=""" qubit q; @@ -672,8 +435,6 @@ def test_simulator_fails_samples_0_shots(): #pragma braket result sample x(q) """ ) - with pytest.raises(ValueError): - simulator.run(jaqcd, qubit_count=1, shots=0) with pytest.raises(ValueError): simulator.run(qasm, shots=0) @@ -681,121 +442,42 @@ def test_simulator_fails_samples_0_shots(): @pytest.mark.parametrize( "result_types,expected", [ - ( - [ - {"type": "expectation", "observable": ["x"], "targets": [1]}, - {"type": "variance", "observable": ["x"], "targets": [1]}, - ], - [0, 1], - ), - ( - [ - {"type": "expectation", "observable": ["x"]}, - {"type": "variance", "observable": ["x"], "targets": [1]}, - ], - [[0, 0], 1], - ), - ( - [ - { - "type": "expectation", - "observable": [[[[0, 0], [1, 0]], [[1, 0], [0, 0]]]], - "targets": [1], - }, - { - "type": "variance", - "observable": [[[[0, 0], [1, 0]], [[1, 0], [0, 0]]]], - "targets": [1], - }, - ], - [0, 1], - ), - ( - [ - { - "type": "expectation", - "observable": ["x", [[[0, 0], [1, 0]], [[1, 0], [0, 0]]]], - "targets": [0, 1], - }, - { - "type": "expectation", - "observable": ["x", [[[0, 0], [1, 0]], [[1, 0], [0, 0]]]], - "targets": [0, 1], - }, - ], - [1, 1], - ), - ( - [ - {"type": "variance", "observable": ["x"], "targets": [1]}, - {"type": "expectation", "observable": ["x"]}, - { - "type": "expectation", - "observable": ["x", [[[0, 0], [1, 0]], [[1, 0], [0, 0]]]], - "targets": [0, 1], - }, - ], - [1, [0, 0], 1], - ), - ], -) -def test_simulator_valid_observables(result_types, expected): - simulator = DensityMatrixSimulator() - prog = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "cnot", "target": 1, "control": 0}, - ], - "results": result_types, - } - ) - ) - result = simulator.run(prog, qubit_count=2, shots=0) - for i in range(len(result_types)): - assert np.allclose(result.resultTypes[i].value, expected[i]) - - -@pytest.mark.parametrize( - "result_types,expected", - [ - ( + [ """ #pragma braket result expectation x(q[1]) #pragma braket result variance x(q[1]) """, [0, 1], - ), - ( + ], + [ """ #pragma braket result expectation x all #pragma braket result variance x(q[1]) """, [[0, 0], 1], - ), - ( + ], + [ """ #pragma braket result expectation hermitian([[0, 1], [1, 0]]) q[1] #pragma braket result variance hermitian([[0, 1], [1, 0]]) q[1] """, [0, 1], - ), - ( + ], + [ """ #pragma braket result expectation x(q[0]) @ hermitian([[0, 1], [1, 0]]) q[1] #pragma braket result expectation x(q[0]) @ hermitian([[0, 1], [1, 0]]) q[1] """, [1, 1], - ), - ( + ], + [ """ #pragma braket result variance x(q[1]) #pragma braket result expectation x all #pragma braket result expectation x(q[0]) @ hermitian([[0, 1], [1, 0]]) q[1] """, [1, [0, 0], 1], - ), + ], ], ) def test_simulator_valid_observables_qasm(result_types, expected, caplog): @@ -892,14 +574,6 @@ def test_measure_with_qubits_not_used(): ) def test_simulator_analytic_value_type(jaqcd_string, oq3_pragma, jaqcd_type): simulator = DensityMatrixSimulator() - jaqcd = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [{"type": "h", "target": 0}], - "results": [{"type": jaqcd_string}], - } - ) - ) qasm = OpenQASMProgram( source=f""" qubit q; @@ -907,9 +581,6 @@ def test_simulator_analytic_value_type(jaqcd_string, oq3_pragma, jaqcd_type): #pragma braket result {oq3_pragma} """ ) - result = simulator.run(jaqcd, qubit_count=2, shots=0) - assert result.resultTypes[0].type == jaqcd_type - assert isinstance(result.resultTypes[0].value, np.ndarray) result = simulator.run(qasm, shots=0) assert result.resultTypes[0].type == jaqcd_type assert isinstance(result.resultTypes[0].value, np.ndarray) @@ -931,13 +602,3 @@ def test_kraus_noise(): result = device.run(program) probabilities = result.resultTypes[0].value assert np.allclose(probabilities, [0.18, 0, 0.82, 0]) - - -@patch("braket.simulator_v2.base_simulator_v2.threading") -def test_threading(mock_threading): - program = OpenQASMProgram(source="""OPENQASM 3.0;""") - simulator = DensityMatrixSimulator() - with pytest.raises( - RuntimeError, match="Simulations must be run from the Main thread.*" - ): - simulator.run(program, shots=0) diff --git a/test/unit_tests/braket/simulator_v2/test_pennylane.py b/test/unit_tests/braket/simulator_v2/test_pennylane.py index c8b6306..f26e8ce 100644 --- a/test/unit_tests/braket/simulator_v2/test_pennylane.py +++ b/test/unit_tests/braket/simulator_v2/test_pennylane.py @@ -358,7 +358,7 @@ def test_valid_local_device_for_noise_model(backend, noise_model): @pytest.mark.parametrize( "backend, device_name", - [("braket_sv_v2", "StateVectorSimulatorV2")], + [["braket_sv_v2", "StateVectorSimulatorV2"]], ) def test_invalid_local_device_for_noise_model(backend, device_name, noise_model): with pytest.raises( diff --git a/test/unit_tests/braket/simulator_v2/test_qiskit_provider.py b/test/unit_tests/braket/simulator_v2/test_qiskit_provider.py new file mode 100644 index 0000000..c5a8830 --- /dev/null +++ b/test/unit_tests/braket/simulator_v2/test_qiskit_provider.py @@ -0,0 +1,47 @@ +import pytest +from qiskit.circuit.library import TwoLocal + +# Import some utilities +from qiskit.primitives import BackendEstimator +from qiskit.quantum_info import SparsePauliOp +from qiskit_algorithms import VQE +from qiskit_algorithms.optimizers import SLSQP +from qiskit_braket_provider import BraketLocalBackend + +# For now, simply test that this completes in reasonable +# time and doesn't hang due to Python vs Julia threading +# issues or the GIL being locked + + +@pytest.fixture +def H2_op(): + # Define the Hamiltonian operator for H2 in terms of Pauli spin operators + return SparsePauliOp.from_list( + [ + ("II", -1.052373245772859), + ("IZ", 0.39793742484318045), + ("ZI", -0.39793742484318045), + ("ZZ", -0.01128010425623538), + ("XX", 0.18093119978423156), + ] + ) + + +@pytest.fixture +def vqe(): + local_simulator = BraketLocalBackend(name="braket_sv_v2") + # Define a `BackendEstimator` with a Braket backend + qi = BackendEstimator(local_simulator, options={"seed_simulator": 42}) + qi.set_transpile_options(seed_transpiler=42) + + # Specify VQE configuration + ansatz = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz") + slsqp = SLSQP(maxiter=1) + return VQE(estimator=qi, ansatz=ansatz, optimizer=slsqp) + + +@pytest.mark.timeout(300) +def test_qiskit_vqe(H2_op, vqe): + # Find the ground state + result = vqe.compute_minimum_eigenvalue(H2_op) + assert result.eigenvalue < 0.0 diff --git a/test/unit_tests/braket/simulator_v2/test_state_vector_simulator_v2.py b/test/unit_tests/braket/simulator_v2/test_state_vector_simulator_v2.py index ee2735d..4935f57 100644 --- a/test/unit_tests/braket/simulator_v2/test_state_vector_simulator_v2.py +++ b/test/unit_tests/braket/simulator_v2/test_state_vector_simulator_v2.py @@ -12,13 +12,10 @@ # language governing permissions and limitations under the License. import cmath -import json import re - -# import re import sys from collections import Counter, namedtuple -from unittest.mock import patch +from concurrent.futures import ThreadPoolExecutor, as_completed import numpy as np import pytest @@ -27,9 +24,14 @@ GateModelSimulatorDeviceCapabilities, GateModelSimulatorDeviceParameters, ) -from braket.ir.jaqcd import Amplitude, DensityMatrix, Expectation, Probability -from braket.ir.jaqcd import Program as JaqcdProgram -from braket.ir.jaqcd import StateVector, Variance +from braket.ir.jaqcd import ( + Amplitude, + DensityMatrix, + Expectation, + Probability, + StateVector, + Variance, +) from braket.ir.openqasm import Program as OpenQASMProgram from braket.task_result import AdditionalMetadata, TaskMetadata @@ -38,59 +40,37 @@ CircuitData = namedtuple("CircuitData", "circuit_ir probability_zero") -@pytest.fixture(params=["OpenQASM", "Jaqcd"]) +@pytest.fixture( + params=[ + "OpenQASM", + ] +) def ir_type(request): return request.param @pytest.fixture def grcs_16_qubit(ir_type): - if ir_type == "Jaqcd": - with open("test/resources/grcs_16.json") as circuit_file: - data = json.load(circuit_file) - return CircuitData( - JaqcdProgram.parse_raw(json.dumps(data["ir"])), data["probability_zero"] - ) return CircuitData(OpenQASMProgram(source="test/resources/grcs_16.qasm"), 0.0000062) @pytest.fixture def bell_ir(ir_type): - return ( - JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "cnot", "target": 1, "control": 0}, - ] - } - ) - ) - if ir_type == "Jaqcd" - else OpenQASMProgram( - source=""" + return OpenQASMProgram( + source=""" OPENQASM 3.0; qubit[2] q; h q[0]; cnot q[0], q[1]; """ - ) ) @pytest.mark.parametrize("batch_size", [1]) def test_simulator_run_grcs_16(grcs_16_qubit, batch_size): simulator = StateVectorSimulator() - if isinstance(grcs_16_qubit.circuit_ir, JaqcdProgram): - result = simulator.run( - grcs_16_qubit.circuit_ir, - shots=0, - batch_size=batch_size, - ) - else: - result = simulator.run(grcs_16_qubit.circuit_ir, shots=0, batch_size=batch_size) + result = simulator.run(grcs_16_qubit.circuit_ir, shots=0, batch_size=batch_size) state_vector = result.resultTypes[0].value assert cmath.isclose( abs(state_vector[0]) ** 2, grcs_16_qubit.probability_zero, abs_tol=1e-7 @@ -101,10 +81,7 @@ def test_simulator_run_grcs_16(grcs_16_qubit, batch_size): def test_simulator_run_bell_pair(bell_ir, batch_size, caplog): simulator = StateVectorSimulator() shots_count = 10000 - if isinstance(bell_ir, JaqcdProgram): - result = simulator.run(bell_ir, shots=shots_count, batch_size=batch_size) - else: - result = simulator.run(bell_ir, shots=shots_count, batch_size=batch_size) + result = simulator.run(bell_ir, shots=shots_count, batch_size=batch_size) assert all([len(measurement) == 2] for measurement in result.measurements) assert len(result.measurements) == shots_count @@ -264,78 +241,6 @@ def test_properties(): "supportsUnassignedMeasurements": True, "disabledQubitRewiringSupported": False, }, - "braket.ir.jaqcd.program": { - "actionType": "braket.ir.jaqcd.program", - "version": ["1"], - "supportedOperations": [ - "ccnot", - "cnot", - "cphaseshift", - "cphaseshift00", - "cphaseshift01", - "cphaseshift10", - "cswap", - "cv", - "cy", - "cz", - "ecr", - "h", - "i", - "iswap", - "pswap", - "phaseshift", - "rx", - "ry", - "rz", - "s", - "si", - "swap", - "t", - "ti", - "unitary", - "v", - "vi", - "x", - "xx", - "xy", - "y", - "yy", - "z", - "zz", - ], - "supportedResultTypes": [ - { - "name": "Sample", - "observables": observables, - "minShots": 1, - "maxShots": max_shots, - }, - { - "name": "Expectation", - "observables": observables, - "minShots": 0, - "maxShots": max_shots, - }, - { - "name": "Variance", - "observables": observables, - "minShots": 0, - "maxShots": max_shots, - }, - { - "name": "Probability", - "minShots": 0, - "maxShots": max_shots, - }, - {"name": "StateVector", "minShots": 0, "maxShots": 0}, - { - "name": "DensityMatrix", - "minShots": 0, - "maxShots": 0, - }, - {"name": "Amplitude", "minShots": 0, "maxShots": 0}, - ], - }, }, "paradigm": {"qubitCount": qubit_count}, "deviceParameters": GateModelSimulatorDeviceParameters.schema(), @@ -344,10 +249,6 @@ def test_properties(): assert simulator.properties == expected_properties -# def test_alias(): -# assert StateVectorSimulator().properties == DefaultSimulator().properties - - @pytest.fixture def sv_adder(): return """ @@ -606,7 +507,7 @@ def test_invalid_standard_observable_target(): simulator.run(program, shots=0) -@pytest.mark.parametrize("shots", (0, 10)) +@pytest.mark.parametrize("shots", [0, 10]) def test_invalid_hermitian_target(shots): qasm = """ OPENQASM 3.0; @@ -634,25 +535,6 @@ def test_invalid_hermitian_target(shots): @pytest.fixture def bell_ir_with_result(ir_type): def _bell_ir_with_result(targets=None): - if ir_type == "Jaqcd": - return JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "cnot", "target": 1, "control": 0}, - ], - "results": [ - {"type": "amplitude", "states": ["11"]}, - { - "type": "expectation", - "observable": ["x"], - "targets": targets, - }, - ], - } - ) - ) if targets is None: observable_string = "x all" elif len(targets) == 1: @@ -677,45 +559,22 @@ def _bell_ir_with_result(targets=None): @pytest.fixture def circuit_noise(ir_type): - if ir_type == "Jaqcd": - return JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "cnot", "target": 1, "control": 0}, - {"type": "bit_flip", "target": 0, "probability": 0.15}, - ] - } - ) - ) - else: - return OpenQASMProgram( - source=""" - OPENQASM 3.0; - qubit[2] q; - h q[0]; - cnot q[0], q[1]; - #pragma braket noise bit_flip(.15) q[0] - #pragma braket result probability q[0] - """ - ) + return OpenQASMProgram( + source=""" + OPENQASM 3.0; + qubit[2] q; + h q[0]; + cnot q[0], q[1]; + #pragma braket noise bit_flip(.15) q[0] + #pragma braket result probability q[0] + """ + ) def test_simulator_identity(caplog): simulator = StateVectorSimulator() shots_count = 1000 programs = ( - JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "i", "target": 0}, - {"type": "i", "target": 1}, - ] - } - ) - ), OpenQASMProgram( source=""" qubit[2] q; @@ -724,16 +583,10 @@ def test_simulator_identity(caplog): ), ) for program in programs: - if isinstance(program, JaqcdProgram): - result = simulator.run( - program, - shots=shots_count, - ) - else: - result = simulator.run( - program, - shots=shots_count, - ) + result = simulator.run( + program, + shots=shots_count, + ) counter = Counter( [ "".join([str(m) for m in measurement]) @@ -752,31 +605,17 @@ def test_simulator_instructions_not_supported(circuit_noise): 'You need to use the density matrix simulator: LocalSimulator("braket_dm_v2").' ) with pytest.raises(TypeError, match=no_noise): - if isinstance(circuit_noise, JaqcdProgram): - simulator.run(circuit_noise, shots=0) - else: - simulator.run(circuit_noise, shots=0) + simulator.run(circuit_noise, shots=0) def test_simulator_run_no_results_no_shots(bell_ir): simulator = StateVectorSimulator() with pytest.raises(ValueError): - if isinstance(bell_ir, JaqcdProgram): - simulator.run(bell_ir, shots=0) - else: - simulator.run(bell_ir, shots=0) + simulator.run(bell_ir, shots=0) def test_simulator_run_amplitude_shots(): simulator = StateVectorSimulator() - jaqcd = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [{"type": "h", "target": 0}], - "results": [{"type": "amplitude", "states": ["00"]}], - } - ) - ) qasm = OpenQASMProgram( source=""" qubit q; @@ -784,25 +623,12 @@ def test_simulator_run_amplitude_shots(): #pragma braket result amplitude "00" """ ) - with pytest.raises(ValueError): - simulator.run(jaqcd, shots=100) with pytest.raises(ValueError): simulator.run(qasm, shots=100) def test_simulator_run_amplitude_no_shots_invalid_states(): simulator = StateVectorSimulator() - jaqcd = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "i", "target": 1}, - ], - "results": [{"type": "amplitude", "states": ["0"]}], - } - ) - ) qasm = OpenQASMProgram( source=""" qubit[2] q; @@ -811,22 +637,12 @@ def test_simulator_run_amplitude_no_shots_invalid_states(): #pragma braket result amplitude "0" """ ) - with pytest.raises(ValueError): - simulator.run(jaqcd, shots=0) with pytest.raises(ValueError): simulator.run(qasm, shots=0) def test_simulator_run_statevector_shots(): simulator = StateVectorSimulator() - jaqcd = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [{"type": "h", "target": 0}], - "results": [{"type": "statevector"}], - } - ) - ) qasm = OpenQASMProgram( source=""" qubit q; @@ -834,31 +650,12 @@ def test_simulator_run_statevector_shots(): #pragma braket result state_vector """ ) - with pytest.raises(ValueError): - simulator.run(jaqcd, shots=100) with pytest.raises(ValueError): simulator.run(qasm, shots=100) def test_simulator_run_result_types_shots(caplog): simulator = StateVectorSimulator() - jaqcd = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "cnot", "target": 1, "control": 0}, - ], - "results": [ - { - "type": "expectation", - "observable": ["z"], - "targets": [1], - } - ], - } - ) - ) qasm = OpenQASMProgram( source=""" qubit[2] qs; @@ -868,37 +665,17 @@ def test_simulator_run_result_types_shots(caplog): """ ) shots_count = 100 - jaqcd_result = simulator.run(jaqcd, shots=shots_count) qasm_result = simulator.run(qasm, shots=shots_count) - for result in jaqcd_result, qasm_result: + for result in (qasm_result,): assert all([len(measurement) == 2] for measurement in result.measurements) assert len(result.measurements) == shots_count assert result.measuredQubits == [0, 1] # qasm_result.resultTypes carries info back to the BDK to calculate results - assert not jaqcd_result.resultTypes assert not caplog.text def test_simulator_run_result_types_shots_basis_rotation_gates(caplog): simulator = StateVectorSimulator() - jaqcd = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "cnot", "target": 1, "control": 0}, - ], - "basis_rotation_instructions": [{"type": "h", "target": 1}], - "results": [ - { - "type": "expectation", - "observable": ["x"], - "targets": [1], - } - ], - } - ) - ) qasm = OpenQASMProgram( source=""" qubit[2] q; @@ -908,110 +685,18 @@ def test_simulator_run_result_types_shots_basis_rotation_gates(caplog): """ ) shots_count = 1000 - jaqcd_result = simulator.run(jaqcd, shots=shots_count) qasm_result = simulator.run(qasm, shots=shots_count) - for result in jaqcd_result, qasm_result: + for result in (qasm_result,): assert all([len(measurement) == 2] for measurement in result.measurements) assert len(result.measurements) == shots_count assert result.measuredQubits == [0, 1] - assert not jaqcd_result.resultTypes assert not caplog.text -def test_simulator_run_result_types_shots_basis_rotation_gates_value_error(): - # not a valid computation path for openqasm, since basis rotation instructions - # are calculated from the result types during simulation - simulator = StateVectorSimulator() - with pytest.raises(ValueError): - ir = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "cnot", "target": 1, "control": 0}, - ], - "basis_rotation_instructions": [{"type": "foo", "target": 1}], - "results": [ - {"type": "expectation", "observable": ["x"], "targets": [1]} - ], - } - ) - ) - shots_count = 1000 - simulator.run(ir, shots=shots_count) - - @pytest.mark.parametrize( "ir, qubit_count", [ - ( - JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [{"type": "z", "target": 2}], - "basis_rotation_instructions": [], - "results": [], - } - ) - ), - 1, - ), - ( - JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [{"type": "h", "target": 0}], - "basis_rotation_instructions": [{"type": "z", "target": 3}], - "results": [], - } - ) - ), - 2, - ), - ], -) -def test_simulator_run_non_contiguous_qubits(ir, qubit_count): - # not relevant for openqasm, since it handles qubit allocation - simulator = StateVectorSimulator() - shots_count = 1000 - simulator.run(ir, shots=shots_count) - - -@pytest.mark.parametrize( - "ir, qubit_count", - [ - ( - JaqcdProgram.parse_raw( - json.dumps( - { - "results": [ - {"targets": [2], "type": "expectation", "observable": ["z"]} - ], - "basis_rotation_instructions": [], - "instructions": [{"type": "z", "target": 0}], - } - ) - ), - 1, - ), - ( - JaqcdProgram.parse_raw( - json.dumps( - { - "results": [ - {"targets": [2], "type": "expectation", "observable": ["z"]} - ], - "basis_rotation_instructions": [], - "instructions": [ - {"type": "z", "target": 0}, - {"type": "z", "target": 1}, - ], - } - ) - ), - 2, - ), - ( + [ OpenQASMProgram( source=""" qubit[2] q; @@ -1020,19 +705,15 @@ def test_simulator_run_non_contiguous_qubits(ir, qubit_count): """ ), None, - ), + ], ], ) def test_simulator_run_observable_references_invalid_qubit(ir, qubit_count): simulator = StateVectorSimulator() shots_count = 0 - if isinstance(ir, JaqcdProgram): - with pytest.raises(ValueError): - simulator.run(ir, shots=shots_count) - else: - # index error since you're indexing from a logical qubit - with pytest.raises(IndexError): - simulator.run(ir, shots=shots_count) + # index error since you're indexing from a logical qubit + with pytest.raises(IndexError): + simulator.run(ir, shots=shots_count) @pytest.mark.parametrize("batch_size", [1]) @@ -1042,10 +723,7 @@ def test_simulator_bell_pair_result_types( ): simulator = StateVectorSimulator() ir = bell_ir_with_result(targets) - if isinstance(ir, JaqcdProgram): - result = simulator.run(ir, shots=0, batch_size=batch_size) - else: - result = simulator.run(ir, shots=0, batch_size=batch_size) + result = simulator.run(ir, shots=0, batch_size=batch_size) assert len(result.resultTypes) == 2 assert result.resultTypes[0].type == Amplitude(states=["11"]) assert np.isclose(result.resultTypes[0].value["11"], 1 / np.sqrt(2)) @@ -1063,14 +741,6 @@ def test_simulator_bell_pair_result_types( def test_simulator_fails_samples_0_shots(): simulator = StateVectorSimulator() - jaqcd = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [{"type": "h", "target": 0}], - "results": [{"type": "sample", "observable": ["x"], "targets": [0]}], - } - ) - ) qasm = OpenQASMProgram( source=""" qubit q; @@ -1078,91 +748,10 @@ def test_simulator_fails_samples_0_shots(): #pragma braket result sample x(q) """ ) - with pytest.raises(ValueError): - simulator.run(jaqcd, shots=0) with pytest.raises(ValueError): simulator.run(qasm, shots=0) -@pytest.mark.parametrize( - "result_types,expected", - [ - ( - [ - {"type": "expectation", "observable": ["x"], "targets": [1]}, - {"type": "variance", "observable": ["x"], "targets": [1]}, - ], - [0, 1], - ), - ( - [ - {"type": "expectation", "observable": ["x"]}, - {"type": "variance", "observable": ["x"], "targets": [1]}, - ], - [[0, 0], 1], - ), - ( - [ - { - "type": "expectation", - "observable": [[[[0, 0], [1, 0]], [[1, 0], [0, 0]]]], - "targets": [1], - }, - { - "type": "variance", - "observable": [[[[0, 0], [1, 0]], [[1, 0], [0, 0]]]], - "targets": [1], - }, - ], - [0, 1], - ), - ( - [ - { - "type": "expectation", - "observable": ["x", [[[0, 0], [1, 0]], [[1, 0], [0, 0]]]], - "targets": [0, 1], - }, - { - "type": "expectation", - "observable": ["x", [[[0, 0], [1, 0]], [[1, 0], [0, 0]]]], - "targets": [0, 1], - }, - ], - [1, 1], - ), - ( - [ - {"type": "variance", "observable": ["x"], "targets": [1]}, - {"type": "expectation", "observable": ["x"]}, - { - "type": "expectation", - "observable": ["x", [[[0, 0], [1, 0]], [[1, 0], [0, 0]]]], - "targets": [0, 1], - }, - ], - [1, [0, 0], 1], - ), - ], -) -def test_simulator_valid_observables(result_types, expected): - simulator = StateVectorSimulator() - prog = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [ - {"type": "h", "target": 0}, - {"type": "cnot", "target": 1, "control": 0}, - ], - "results": result_types, - } - ) - ) - result = simulator.run(prog, shots=0) - for i in range(len(result_types)): - assert np.allclose(result.resultTypes[i].value, expected[i]) - - @pytest.mark.parametrize( "result_types,expected", [ @@ -1274,7 +863,7 @@ def test_basis_rotation_all(caplog): @pytest.mark.parametrize( "qasm, error_string", - ( + [ ( """ qubit[2] q; @@ -1306,7 +895,7 @@ def test_basis_rotation_all(caplog): """, "Conflicting result types applied to a single qubit", ), - ), + ], ) def test_partially_overlapping_basis_rotation(qasm, error_string): with pytest.raises(ValueError, match=error_string): @@ -1464,14 +1053,6 @@ def test_rotation_parameter_expressions(operation, state_vector): ) def test_simulator_analytic_value_type(jaqcd_string, oq3_pragma, jaqcd_type): simulator = StateVectorSimulator() - jaqcd = JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [{"type": "h", "target": 0}], - "results": [{"type": jaqcd_string}], - } - ) - ) qasm = OpenQASMProgram( source=f""" qubit q; @@ -1479,9 +1060,6 @@ def test_simulator_analytic_value_type(jaqcd_string, oq3_pragma, jaqcd_type): #pragma braket result {oq3_pragma} """ ) - result = simulator.run(jaqcd, shots=0) - assert result.resultTypes[0].type == jaqcd_type - assert isinstance(result.resultTypes[0].value, np.ndarray) result = simulator.run(qasm, shots=0) assert result.resultTypes[0].type == jaqcd_type assert isinstance(result.resultTypes[0].value, np.ndarray) @@ -1517,60 +1095,6 @@ def test_unitary_pragma(): ) -@pytest.mark.parametrize( - "ir, qubit_count", - [ - ( - JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [{"type": "z", "target": 2}], - "basis_rotation_instructions": [], - "results": [], - } - ) - ), - 1, - ), - ( - JaqcdProgram.parse_raw( - json.dumps( - { - "instructions": [{"type": "h", "target": 0}], - "basis_rotation_instructions": [{"type": "z", "target": 3}], - "results": [], - } - ) - ), - 2, - ), - ], -) -def test_run_multiple_non_contiguous(ir, qubit_count): - # not relevant for openqasm, since it handles qubit allocation - simulator = StateVectorSimulator() - shots_count = 1000 - batch_size = 5 - payloads = [ir] * batch_size - simulator.run_multiple(payloads, shots=shots_count) - - -def test_noncontiguous_qubits_jaqcd_multiple_targets(): - jaqcd_program = { - "braketSchemaHeader": {"name": "braket.ir.jaqcd.program", "version": "1"}, - "instructions": [ - {"type": "x", "target": 3}, - {"type": "swap", "targets": [3, 4]}, - ], - "results": [{"type": "expectation", "observable": ["z"], "targets": [4]}], - } - prg = JaqcdProgram.parse_raw(json.dumps(jaqcd_program)) - result = StateVectorSimulator().run(prg, shots=0) - - assert result.measuredQubits == [0, 1] - assert result.resultTypes[0].value == -1 - - def test_run_multiple_single_circuit(): payload = [ OpenQASMProgram( @@ -1608,11 +1132,46 @@ def test_run_multiple(): assert np.allclose(results[2].resultTypes[0].value, np.array([0, 1])) -@patch("braket.simulator_v2.base_simulator_v2.threading") -def test_threading(mock_threading): - program = OpenQASMProgram(source="""OPENQASM 3.0;""") +@pytest.mark.timeout(300) +def test_run_single_executor(): + payload = OpenQASMProgram( + source=""" + OPENQASM 3.0; + bit[1] b; + qubit[1] q; + h q[0]; + #pragma braket result state_vector + """ + ) + pool = ThreadPoolExecutor(2) simulator = StateVectorSimulator() - with pytest.raises( - RuntimeError, match="Simulations must be run from the Main thread.*" - ): - simulator.run(program, shots=0) + fs = {pool.submit(simulator.run_openqasm, payload): ix for ix in range(10)} + for future in as_completed(fs): + results = future.result() + assert np.allclose(results.resultTypes[0].value, np.array([1, 1]) / np.sqrt(2)) + + +@pytest.mark.timeout(300) +def test_run_multiple_executor(): + payloads = [ + OpenQASMProgram( + source=f""" + OPENQASM 3.0; + bit[1] b; + qubit[1] q; + {gate} q[0]; + #pragma braket result state_vector + """ + ) + for gate in ["h", "z", "x"] + ] + pool = ThreadPoolExecutor(2) + simulator = StateVectorSimulator() + fs = {pool.submit(simulator.run_multiple, payloads): ix for ix in range(10)} + for future in as_completed(fs): + results = future.result() + assert np.allclose( + results[0].resultTypes[0].value, np.array([1, 1]) / np.sqrt(2) + ) + assert np.allclose(results[1].resultTypes[0].value, np.array([1, 0])) + assert np.allclose(results[2].resultTypes[0].value, np.array([0, 1])) diff --git a/tox.ini b/tox.ini index dc67834..8b95a6f 100644 --- a/tox.ini +++ b/tox.ini @@ -3,15 +3,15 @@ envlist = linters,docs,unit-tests [testenv:unit-tests] basepython = python3 -setenv = - JULIA_PKG_USE_CLI_GIT=true - JULIA_CONDAPKG_BACKEND="Null" # {posargs} contains additional arguments specified when invoking tox. e.g. tox -- -s -k test_foo.py deps = {[test-deps]deps} allowlist_externals = pytest +setenv = + PYTHON_JULIACALL_HANDLE_SIGNALS=yes commands = + python -c 'import juliacall' # force install of all deps, trigger precompilation pytest {posargs} --cov-report term-missing --cov-report html --cov-report xml --cov=braket extras = test @@ -56,7 +56,7 @@ skip_install = true deps = isort commands = - isort -rc . {posargs} + isort . {posargs} [testenv:isort_check] basepython = python3