Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add execute_with_memory_map_batch method for QPUs #1750

Merged
merged 8 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions docs/source/programs_and_gates.rst
Original file line number Diff line number Diff line change
Expand Up @@ -361,21 +361,21 @@ 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]}
# Begin batch execution of the program using each set of parameters.
# This returns a list of references to each job, the length and order of which correspond to the memory maps we
# pass in.
handles = qc.execute_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")
# Use the handles to gather the results
parametric_measurements = [qc.get_result(handles) for handle in handles]

# 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::

Expand Down
46 changes: 23 additions & 23 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
19 changes: 18 additions & 1 deletion pyquil/api/_qam.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -144,6 +144,23 @@ 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 a handles to be used to retrieve results.
MarquessV marked this conversation as resolved.
Show resolved Hide resolved

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``.
"""

@abstractmethod
def get_result(self, execute_response: T) -> QAMExecutionResult:
"""
Expand Down
44 changes: 38 additions & 6 deletions pyquil/api/_qpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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(
Expand All @@ -187,19 +210,28 @@ 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,
client=self._client_configuration,
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:
"""
Expand Down
13 changes: 12 additions & 1 deletion pyquil/api/_qvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion pyquil/pyqvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions test/unit/test_qpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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
):
Expand All @@ -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

Expand All @@ -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
):
Expand All @@ -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")
Expand Down
Loading