diff --git a/docs/source/programs_and_gates.rst b/docs/source/programs_and_gates.rst index 70dde8be8..e4a15b503 100644 --- a/docs/source/programs_and_gates.rst +++ b/docs/source/programs_and_gates.rst @@ -361,21 +361,18 @@ filled in for, say, 200 values between :math:`0` and :math:`2\pi`. We demonstrat .. testcode:: parametric - # Somewhere to store each list of results - parametric_measurements = [] + # Generate a memory map for each set of parameters we want to execute with + memory_maps = [{"theta": [theta] for theta in np.linspace(0, 2 * np.pi, 200)}] - for theta in np.linspace(0, 2 * np.pi, 200): - # Set the desired parameter value in executable memory - memory_map = {"theta": [theta]} + # Batch execute of the program using each set of parameters. + # This returns a list of results for each execution, the length and order of which correspond to the memory maps we + # pass in. + parametric_measurements = qc.run_with_memory_map_batch(executable, memory_maps) - # Get the results of the run with the value we want to execute with - bitstrings = qc.run(executable, memory_map=memory_map).get_register_map().get("ro") - - # Store our results - parametric_measurements.append(bitstrings) +.. note:: -In the example here, if you called ``qc.run(executable)`` and didn't specify ``'theta'``, the program would apply -``RZ(0, qubit)`` for every execution. + :py:meth:`~QAM.run` and :py:meth:`~QAM.execute` both support executing a program a single memory map. We chose + :py:meth:`~QAM.batch_execute_with_memory_map` for this example since we had multiple sets of parameters to run. .. note:: diff --git a/poetry.lock b/poetry.lock index 797af601f..dce0c52aa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2385,32 +2385,32 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""} [[package]] name = "qcs-sdk-python" -version = "0.17.0" +version = "0.17.1" description = "Python interface for the QCS Rust SDK" optional = false python-versions = "*" files = [ - {file = "qcs_sdk_python-0.17.0-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3fb4ca8f97d02b0d94f8955e22581c54e598e67698d320c0bb6cbfd069834ce0"}, - {file = "qcs_sdk_python-0.17.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:459dc2cca93d5c9487ef893cfe1404966d927c7d9556f2d09c52eba2f87eeafa"}, - {file = "qcs_sdk_python-0.17.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:90e7ad1ad929fea6e7f25fd7607da1fb909b7790604049cd6e8db0d51e76101a"}, - {file = "qcs_sdk_python-0.17.0-cp310-none-win_amd64.whl", hash = "sha256:631ea09e71d658d43a8d324f155e0a107a38bd13b6fbc6f7617f9ce159bdd8f5"}, - {file = "qcs_sdk_python-0.17.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4dffe02fd80764c41b06cf9839f78b123d836dadba8373c48f98085044641433"}, - {file = "qcs_sdk_python-0.17.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7fdb86da63da3fa7f83b6b96acbdc5bea65f31e99cd0bb497d6bbbe23713503c"}, - {file = "qcs_sdk_python-0.17.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d1088cd9d3f6aab1fb9b1a17e04ca5cb598170e5bd1d8b1c3236704ea9f64dda"}, - {file = "qcs_sdk_python-0.17.0-cp311-none-win_amd64.whl", hash = "sha256:e22821c324e3529553e540261408b2700e701ce1b95733cedc466ce7988a2f0c"}, - {file = "qcs_sdk_python-0.17.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:3f1606606b900ed188dea6823e388cc75e4555f743905c03abf25667f0ba39a2"}, - {file = "qcs_sdk_python-0.17.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:aae0635b017e43f627e296b8b943d815285cdb2bf5fbed2cbebdff292bae31c9"}, - {file = "qcs_sdk_python-0.17.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:489010ceaaf0192b2567477f4b79b9c3aac82fa2de84ccdd2d5916f8ab018d39"}, - {file = "qcs_sdk_python-0.17.0-cp312-none-win_amd64.whl", hash = "sha256:d886f7ed24d02d6a5d16fd53c4ee37a05dccff1db9ba2558285c9ab53af731f4"}, - {file = "qcs_sdk_python-0.17.0-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:4d40da6d72e50877e239bb04a83e2647875bc1d21a6bd5160e6806df8d8e1470"}, - {file = "qcs_sdk_python-0.17.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:dddc04949909ca533e00425f9b06037f9880954028e9b66c83d59cbd3f5a77a9"}, - {file = "qcs_sdk_python-0.17.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:ea2815f34cef0c464c49c33621fe25ffafaed7ee99086512cd3c993c24016d54"}, - {file = "qcs_sdk_python-0.17.0-cp38-none-win_amd64.whl", hash = "sha256:109a3960f492d39e888c932a922971519f9fa941302d31e2e1e14d7d770c44f1"}, - {file = "qcs_sdk_python-0.17.0-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7fe28c34d7150b33534707e8783965115363d22f9c433e9849fd029f24706ea7"}, - {file = "qcs_sdk_python-0.17.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8eec185649a0e635a255ab49a62c7c5c94bccf85bb3ad886f3fe39b9b6e91f8c"}, - {file = "qcs_sdk_python-0.17.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:445bbc32086285fb1a7a2ac0bcc35e765c5c8df92909a89b1f107e4cb38127b5"}, - {file = "qcs_sdk_python-0.17.0-cp39-none-win_amd64.whl", hash = "sha256:2f5c1660297a471e4657ad0e41c142576528fbe62090692273ab7a5de98502a6"}, - {file = "qcs_sdk_python-0.17.0.tar.gz", hash = "sha256:9ee0dced1575b8b45d3e21b803fbca14310110257e2fb2d998e5bd4d564e8252"}, + {file = "qcs_sdk_python-0.17.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d0c5ad7b10fdc817e0d1b66853b6c90a1a69c474c65e76d11abb4577c79edf39"}, + {file = "qcs_sdk_python-0.17.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7ae49c6570a23478655e252be0d95ea42025950df42ea28114fa4cf0731687e3"}, + {file = "qcs_sdk_python-0.17.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0a29f2307769ebdfd9c205e89e75da8b1e61584ae99389b0bc99cc186c210089"}, + {file = "qcs_sdk_python-0.17.1-cp310-none-win_amd64.whl", hash = "sha256:76947eacaecb44110837ebe8e735451541c6829cbf7b2882f86f02066030a9a5"}, + {file = "qcs_sdk_python-0.17.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0d7dfe7bb729c94b0d555c252bb8c2cd1786133ced457a670aa3b057f572c8c1"}, + {file = "qcs_sdk_python-0.17.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:09ac551a50057495712f0f462d4d6a7505abf3c29c78fa97100882de4028493b"}, + {file = "qcs_sdk_python-0.17.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:82112980f7c0ebc932aac63fdd47d7a287ccb008e92381f302796c419b8cc65e"}, + {file = "qcs_sdk_python-0.17.1-cp311-none-win_amd64.whl", hash = "sha256:6124eafad081b536b10bbd3ff5dc418e23fcf8c5390858b25d15c86979ab3cf2"}, + {file = "qcs_sdk_python-0.17.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a8a0afda8746a13e49fec08376b44934db99e18952d9c09e54fbd758f0e2edb"}, + {file = "qcs_sdk_python-0.17.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c6fd542832ab09741793a7148b5d68898630e6f86eba0e599e42496162f7f9bb"}, + {file = "qcs_sdk_python-0.17.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:22152e265db8e6523b9be9c1816fdd4f656494a251ac8fbd4acedc8002ff19a0"}, + {file = "qcs_sdk_python-0.17.1-cp312-none-win_amd64.whl", hash = "sha256:d59669d663ab2e08b08f75b248d105a79a7a8e067721aefc485a93b9507c3947"}, + {file = "qcs_sdk_python-0.17.1-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:df5a8a5b56d491fdbb48ce2b354cbe4096ff4aecefd5b66484d03c56d1a177a7"}, + {file = "qcs_sdk_python-0.17.1-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:8d1f97ea11c4a4282cc02141ee439b0d3c3c550337e76d4a6c80ca8cb68b3732"}, + {file = "qcs_sdk_python-0.17.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:2e8919f76d32329ebd05d69f332361f37cecf8429c173552c0f1e6d4c87d4f13"}, + {file = "qcs_sdk_python-0.17.1-cp38-none-win_amd64.whl", hash = "sha256:a6d3aa110727560009ad670fb2c40df6b7136703cf5270d82e53b530d414cf3b"}, + {file = "qcs_sdk_python-0.17.1-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:96cd89ea1f4d6cb2cb3c713900043f4fa127eae6daae671be752a6cfcb438761"}, + {file = "qcs_sdk_python-0.17.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:d730c18d9d90c6ca1506ced83856b0870fd5c0e792a392815670c62223d4bda1"}, + {file = "qcs_sdk_python-0.17.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:18132ce58244f987a8cfb0f5edb40580dfab1b432cdc3519403181fb379591a5"}, + {file = "qcs_sdk_python-0.17.1-cp39-none-win_amd64.whl", hash = "sha256:47e12af249e52a9411343f2518adbab953f14655e66661154d9971d8ef6a4bb0"}, + {file = "qcs_sdk_python-0.17.1.tar.gz", hash = "sha256:0e36ef2562cbcec7cd09935ab650049e97ae10a3180d5df78894c3bf8c31b122"}, ] [package.dependencies] @@ -3288,4 +3288,4 @@ latex = ["ipython"] [metadata] lock-version = "2.0" python-versions = "^3.8,<=3.12" -content-hash = "dbd592dca3609c61903f6dcf4607a996d293614ff77e750c6cdb4c4ce8010e8b" +content-hash = "30cfab0707ba9f11a7a568bb0d026809911e4826287576886c194ef6b148c2b3" diff --git a/pyproject.toml b/pyproject.toml index ffb4a3704..7c0f2af9d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ lark = "^0.11.1" rpcq = "^3.10.0" networkx = ">=2.5" importlib-metadata = { version = ">=3.7.3,<5", python = "<3.8" } -qcs-sdk-python = "0.17.0" +qcs-sdk-python = "0.17.1" tenacity = "^8.2.2" types-python-dateutil = "^2.8.19" types-retry = "^0.9.9" diff --git a/pyquil/api/_qam.py b/pyquil/api/_qam.py index 02ebe2c8a..51974839c 100644 --- a/pyquil/api/_qam.py +++ b/pyquil/api/_qam.py @@ -15,7 +15,7 @@ ############################################################################## from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any, Generic, Mapping, Optional, TypeVar, Sequence, Union, Dict +from typing import Any, Generic, Mapping, Optional, TypeVar, Sequence, Union, Dict, List, Iterable from datetime import timedelta from deprecated import deprecated @@ -144,6 +144,22 @@ def execute( region for the run. """ + @abstractmethod + def execute_with_memory_map_batch( + self, + executable: QuantumExecutable, + memory_maps: Iterable[MemoryMap], + **kwargs: Any, + ) -> List[T]: + """ + Execute a QuantumExecutable with one or more memory_maps, returning handles to be used to retrieve results. + + How these programs are batched and executed is determined by the executor. See their respective documentation + for details. + + Returns a list of handles that can be used to fetch results with ``QAM#get_result``. + """ + @abstractmethod def get_result(self, execute_response: T) -> QAMExecutionResult: """ diff --git a/pyquil/api/_qpu.py b/pyquil/api/_qpu.py index 81b17f51c..1607e963d 100644 --- a/pyquil/api/_qpu.py +++ b/pyquil/api/_qpu.py @@ -16,7 +16,7 @@ from dataclasses import dataclass from collections import defaultdict from datetime import timedelta -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional, Union, List, Iterable import numpy as np from numpy.typing import NDArray @@ -31,13 +31,13 @@ ) from qcs_sdk import QCSClient, ResultData, ExecutionData from qcs_sdk.qpu.api import ( - submit, retrieve_results, cancel_job, ConnectionStrategy, ExecutionResult, ExecutionOptions, ExecutionOptionsBuilder, + submit_with_parameter_batch, ) from qcs_sdk.qpu.rewrite_arithmetic import build_patch_values @@ -177,6 +177,29 @@ def execute( to configure how the job is submitted and retrieved from the QPU. If unset, an appropriate default will be used. """ + memory_map = memory_map or {} + responses = self.execute_with_memory_map_batch(executable, [memory_map], execution_options) + assert len(responses) == 1, "Request to execute job with a single memory map returned multiple responses" + return responses[0] + + def execute_with_memory_map_batch( + self, + executable: QuantumExecutable, + memory_maps: Iterable[MemoryMap], + execution_options: Optional[ExecutionOptions] = None, + **__: Any, + ) -> List[QPUExecuteResponse]: + """ + Execute a compiled program on a QPU with multiple sets of `memory_maps`. + + See the documentation of `qcs_sdk.qpu.api.submit_with_parameter_batch` for more information. + + :param program: The `EncryptedProgram` to execute. + :param memory_maps: A list containing one or more mappings of symbols to their desired values. + :param execution_options: The ``ExecutionOptions`` to use. + + :returns: A list of responses with a length and order corresponding to the memory_maps given. + """ executable = executable.copy() assert isinstance( @@ -187,11 +210,14 @@ def execute( executable.ro_sources is not None ), "To run on a QPU, a program must include ``MEASURE``, ``CAPTURE``, and/or ``RAW-CAPTURE`` instructions" - memory_map = memory_map or {} - patch_values = build_patch_values(executable.recalculation_table, memory_map) + patch_values = [] + for memory_map in memory_maps: + memory_map = memory_map or {} + patch_values.append(build_patch_values(executable.recalculation_table, memory_map)) + effective_execution_options = execution_options or self.execution_options - job_id = submit( + job_ids = submit_with_parameter_batch( program=executable.program, patch_values=patch_values, quantum_processor_id=self.quantum_processor_id, @@ -199,7 +225,13 @@ def execute( execution_options=effective_execution_options, ) - return QPUExecuteResponse(_executable=executable, job_id=job_id, execution_options=effective_execution_options) + responses = [] + for job_id in job_ids: + responses.append( + QPUExecuteResponse(_executable=executable, job_id=job_id, execution_options=effective_execution_options) + ) + + return responses def cancel(self, execute_response: QPUExecuteResponse) -> None: """ diff --git a/pyquil/api/_quantum_computer.py b/pyquil/api/_quantum_computer.py index 7a9be95c7..bf52ed1c8 100644 --- a/pyquil/api/_quantum_computer.py +++ b/pyquil/api/_quantum_computer.py @@ -24,6 +24,7 @@ Any, Tuple, Iterator, + Iterable, Optional, Set, Union, @@ -141,6 +142,22 @@ def run( """ return self.qam.run(executable, memory_map, **kwargs) + def run_with_memory_map_batch( + self, executable: QuantumExecutable, memory_maps: Iterable[MemoryMap], **kwargs: Any + ) -> List[QAMExecutionResult]: + """ + Run a QuantumExecutable with one or more memory_maps, returning a list of results corresponding to the length + and order of the given MemoryMaps. + + How these programs are batched and executed is determined by the executor. See their respective documentation + for details. + + Returns a list of ``QAMExecutionResult``, which can be used to fetch + results in ``QAM#get_result``. + """ + handles = self.qam.execute_with_memory_map_batch(executable, memory_maps, **kwargs) + return [self.qam.get_result(handle) for handle in handles] + def calibrate(self, experiment: Experiment) -> List[ExperimentResult]: """ Perform readout calibration on the various multi-qubit observables involved in the provided diff --git a/pyquil/api/_qvm.py b/pyquil/api/_qvm.py index 2dc3de2e6..02ddcd950 100644 --- a/pyquil/api/_qvm.py +++ b/pyquil/api/_qvm.py @@ -14,7 +14,7 @@ # limitations under the License. ############################################################################## from dataclasses import dataclass -from typing import Any, Optional, Sequence, Tuple, Dict +from typing import Any, Optional, Sequence, Tuple, Dict, List, Iterable import numpy as np @@ -127,6 +127,17 @@ def connect(self) -> None: except ConnectionError: raise QVMNotRunning(f"No QVM server running at {self._client.qvm_url}") from ConnectionError + def execute_with_memory_map_batch( + self, executable: QuantumExecutable, memory_maps: Iterable[MemoryMap], **__: Any + ) -> List[QVMExecuteResponse]: + """ + Executes a single program on the QVM with multiple memory maps. + + This method is a convenience wrapper around QVM#execute and isn't more efficient than making multiple seperate + requests to the QVM. + """ + return [self.execute(executable, memory_map) for memory_map in memory_maps] + def execute( self, executable: QuantumExecutable, diff --git a/pyquil/pyqvm.py b/pyquil/pyqvm.py index 9dfcd1657..dcb970284 100644 --- a/pyquil/pyqvm.py +++ b/pyquil/pyqvm.py @@ -15,7 +15,7 @@ ############################################################################## import logging from abc import ABC, abstractmethod -from typing import Dict, List, Optional, Sequence, Type, Union, Any +from typing import Dict, List, Optional, Sequence, Type, Union, Any, Iterable import numpy as np from numpy.random.mtrand import RandomState @@ -221,6 +221,13 @@ def _extract_defined_gates(self) -> None: raise NotImplementedError("PyQVM does not support DEFGATE ... AS MATRIX | PAULI-SUM.") self.defined_gates[dg.name] = dg.matrix + def execute_with_memory_map_batch( + self, executable: QuantumExecutable, memory_maps: Iterable[MemoryMap], **__: Any + ) -> List["PyQVM"]: + raise NotImplementedError( + "PyQVM does not support batch execution as the state of the instance is reset at the start of each execute." + ) + def execute(self, executable: QuantumExecutable, memory_map: Optional[MemoryMap] = None, **__: Any) -> "PyQVM": """ Execute a program on the PyQVM. Note that the state of the instance is reset on each diff --git a/test/unit/test_qpu.py b/test/unit/test_qpu.py index de92ccf7b..8fd8958b9 100644 --- a/test/unit/test_qpu.py +++ b/test/unit/test_qpu.py @@ -58,13 +58,13 @@ def test_provided_execution_options(): @patch("pyquil.api._qpu.retrieve_results") -@patch("pyquil.api._qpu.submit") +@patch("pyquil.api._qpu.submit_with_parameter_batch") def test_qpu_execute( mock_submit: MagicMock, mock_retrieve_results: MagicMock, mock_encrypted_program: EncryptedProgram ): qpu = QPU(quantum_processor_id="test") - mock_submit.return_value = "some-job-id" + mock_submit.return_value = ["some-job-id"] execute_response = qpu.execute(mock_encrypted_program) mock_retrieve_results.return_value = ExecutionResults( @@ -91,13 +91,13 @@ def test_qpu_execute( @patch("pyquil.api._qpu.retrieve_results") -@patch("pyquil.api._qpu.submit") +@patch("pyquil.api._qpu.submit_with_parameter_batch") def test_qpu_execute_jagged_results( mock_submit: MagicMock, mock_retrieve_results: MagicMock, mock_encrypted_program: EncryptedProgram ): qpu = QPU(quantum_processor_id="test") - mock_submit.return_value = "some-job-id" + mock_submit.return_value = ["some-job-id"] execute_response = qpu.execute(mock_encrypted_program) mock_retrieve_results.return_value = ExecutionResults( @@ -130,7 +130,7 @@ def test_qpu_execute_jagged_results( class TestQPUExecutionOptions: @patch("pyquil.api._qpu.retrieve_results") - @patch("pyquil.api._qpu.submit") + @patch("pyquil.api._qpu.submit_with_parameter_batch") def test_submit_with_class_options( self, mock_submit: MagicMock, mock_retrieve_results: MagicMock, mock_encrypted_program: EncryptedProgram ): @@ -146,7 +146,7 @@ def test_submit_with_class_options( execution_options = execution_options_builder.build() qpu.execution_options = execution_options - mock_submit.return_value = "some-job-id" + mock_submit.return_value = ["some-job-id"] execute_response = qpu.execute(mock_encrypted_program) assert execute_response.execution_options == qpu.execution_options @@ -168,7 +168,7 @@ def test_submit_with_class_options( ) @patch("pyquil.api._qpu.retrieve_results") - @patch("pyquil.api._qpu.submit") + @patch("pyquil.api._qpu.submit_with_parameter_batch") def test_submit_with_options( self, mock_submit: MagicMock, mock_retrieve_results: MagicMock, mock_encrypted_program: EncryptedProgram ): @@ -178,7 +178,7 @@ def test_submit_with_options( """ qpu = QPU(quantum_processor_id="test") - mock_submit.return_value = "some-job-id" + mock_submit.return_value = ["some-job-id"] execution_options_builder = ExecutionOptionsBuilder() execution_options_builder.timeout_seconds = 10.0 execution_options_builder.connection_strategy = ConnectionStrategy.endpoint_id("some-endpoint-id")