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

V4 - feat: QAMExecutionResult now has a raw_readout_data property #1631

Merged
merged 20 commits into from
Aug 29, 2023
Merged
Show file tree
Hide file tree
Changes from 13 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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ The 4.0 release of pyQuil migrates its core functionality into Rigetti's latest
- The new `QPUCompilerAPIOptions` class provides can now be used to customize how a program is compiled against a QPU.
- The `diagnostics` module has been introduced with a `get_report` function that will gather information on the currently running pyQuil
installation, perform diagnostics checks, and return a summary.
- `QAMExecutionResult` now has a `raw_readout_data` property that can be used to get the raw form of readout data returned from the executor.

### Deprecations

- The `QAMExecutionResult` `readout_data` property has been deprecated to avoid confusion with the new `raw_readout_data` property. Use the `register_map` property instead.

## 3.5.4

Expand Down
32 changes: 32 additions & 0 deletions docs/source/introducing_v4.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,38 @@ In pyQuil v4, Gateway is enabled by default and it is generally recommended to k
result = qc.qam.execution_options = execution_options


Accessing Raw Execution Data
----------------------------

In previous versions of pyQuil, readout data was always returned as a mapping of memory regions to rectangular matrices
that contained one value per memory reference, per shot. However, it shouldn't be assumed that readout data will always
fit this shape. For example, programs that use mid-circuit measurement or dynamic control flow can emit a different
MarquessV marked this conversation as resolved.
Show resolved Hide resolved
amount of values per shot, breaking the assumption that readout data will contain one value for each memory reference per shot.
In these cases, it's better to rely on the author of the program to wrangle the data into the shape they expect, so we've
made it possible to access raw readout data.

In v4, readout data continues to be accessible in the same way as before, but if the readout data generated by your program
doesn't have exactly one value per memory reference, per shot, a ``RegisterMatrixConversionError`` will be raised. In this case,
you should use the ``raw_readout_data`` property to access the raw data and build the data structure you need.

.. code:: python

import numpy as np
from pyquil.api import RegisterMatrixConversionError

def process_raw_data(raw_data) -> np.ndarray:
# Process the data into a matrix that makes sense for your
# program
...

result = qc.run(exe)

try:
matrix = result.readout_data
except RegisterMatrixConversionError:
matrix = process_raw_data(result.raw_readout_data)


Using the new QPU Compiler Backend
----------------------------------

Expand Down
4 changes: 3 additions & 1 deletion pyquil/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@
"WavefunctionSimulator",
]

from qcs_sdk import QCSClient
from qcs_sdk import QCSClient, RegisterMatrixConversionError
from qcs_sdk.qpu.api import ExecutionOptions, ExecutionOptionsBuilder, ConnectionStrategy
from qcs_sdk.qpu import RawQPUReadoutData
from qcs_sdk.qvm import RawQVMReadoutData

from pyquil.api._benchmark import BenchmarkConnection
from pyquil.api._compiler import (
Expand Down
69 changes: 63 additions & 6 deletions pyquil/api/_qam.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@
# limitations under the License.
##############################################################################
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any, Generic, Mapping, Optional, TypeVar, Sequence, Union
from dataclasses import dataclass
from typing import Any, Generic, Mapping, Optional, TypeVar, Sequence, Union, Dict

from deprecated import deprecated
import numpy as np
from qcs_sdk import ExecutionData
from qcs_sdk.qpu import RawQPUReadoutData
from qcs_sdk.qvm import RawQVMReadoutData

from pyquil.api._abstract_compiler import QuantumExecutable


Expand All @@ -37,11 +42,63 @@ class QAMExecutionResult:
executable: QuantumExecutable
"""The executable corresponding to this result."""

readout_data: Mapping[str, Optional[np.ndarray]] = field(default_factory=dict)
"""Readout data returned from the QAM, keyed on the name of the readout register or post-processing node."""
data: ExecutionData
"""
The ``ExecutionData`` returned from the job. Consider using
``QAMExecutionResult#register_map`` or ``QAMExecutionResult#raw_readout_data``
to get at the data in a more convenient format.
"""

@property
def raw_readout_data(self) -> Union[RawQVMReadoutData, RawQPUReadoutData]:
"""
Get the raw result data. This will be a flattened dictionary derived
from :class:`qcs_sdk.qvm.QVMResultData` or :class:`qcs_sdk.qpu.QPUResultData` depending on where the
job was run. See the respective
MarquessV marked this conversation as resolved.
Show resolved Hide resolved

This property should be used when running programs that use features like
mid-circuit measurement and dynamic control flow on a QPU, since they can
produce irregular result shapes that don't necessarily fit in a
rectangular matrix. If the program was run on a QVM, or doesn't use those
features, consider using the ``register_map`` property instead.
"""
return self.data.result_data.to_raw_readout_data()

execution_duration_microseconds: Optional[int] = field(default=None)
"""Duration job held exclusive hardware access. Defaults to ``None`` when information is not available."""
@property
def register_map(self) -> Dict[str, Optional[np.ndarray]]:
MarquessV marked this conversation as resolved.
Show resolved Hide resolved
"""
A mapping of a register name (ie. "ro") to a ``np.ndarray`` containing the values for the
register.

Raises a ``RegisterMatrixConversionError`` if the inner execution data for any of the
registers would result in a jagged matrix. QPU result data is captured per measure,
meaning a value is returned for every measure to a memory reference, not just once per shot.
This is often the case in programs that use mid-circuit measurement or dynamic control flow,
where measurements to the same memory reference might occur multiple times in a shot, or be
skipped conditionally. In these cases, building a rectangular ``np.ndarray`` would
necessitate making assumptions about the data that could skew the data in undesirable ways.
Instead, it's recommended to manually build a matrix from the ``QPUResultData`` available
on the ``raw_readout_data`` property.
"""
MarquessV marked this conversation as resolved.
Show resolved Hide resolved
register_map = self.data.result_data.to_register_map()
return {key: matrix.to_ndarray() for key, matrix in register_map.items()}

@property
@deprecated(
version="4.0.0",
reason=(
"This property is ambiguous now that the `raw_readout_data` property exists"
"and will be removed in future versions. Use `register_map` property instead"
),
)
def readout_data(self) -> Mapping[str, Optional[np.ndarray]]:
"""Readout data returned from the QAM, keyed on the name of the readout register or post-processing node."""
return self.register_map

@property
def execution_duration_microseconds(self) -> Optional[int]:
"""Duration job held exclusive hardware access. Defaults to ``None`` when information is not available."""
return self.data.duration.microseconds
MarquessV marked this conversation as resolved.
Show resolved Hide resolved


class QAM(ABC, Generic[T]):
Expand Down
36 changes: 17 additions & 19 deletions pyquil/api/_qpu.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@
##############################################################################
from dataclasses import dataclass
from collections import defaultdict
from datetime import timedelta
from typing import Any, Dict, Optional, Union

import numpy as np
from numpy.typing import NDArray
from qcs_sdk.qpu import ReadoutValues, QPUResultData
from rpcq.messages import ParameterSpec

from pyquil.api import QuantumExecutable, EncryptedProgram
Expand All @@ -27,7 +29,7 @@
from pyquil.quilatom import (
MemoryReference,
)
from qcs_sdk import QCSClient
from qcs_sdk import QCSClient, ResultData, ExecutionData
from qcs_sdk.qpu.api import (
submit,
retrieve_results,
Expand Down Expand Up @@ -184,7 +186,8 @@ def execute(
executable.ro_sources is not None
), "To run on a QPU, a program must include ``MEASURE``, ``CAPTURE``, and/or ``RAW-CAPTURE`` instructions"

patch_values = build_patch_values(executable.recalculation_table, memory_map or {})
memory_map = memory_map or {}
patch_values = build_patch_values(executable.recalculation_table, memory_map)

job_id = submit(
program=executable.program,
Expand All @@ -208,21 +211,16 @@ def get_result(self, execute_response: QPUExecuteResponse) -> QAMExecutionResult
execution_options=execute_response.execution_options,
)

ro_sources = execute_response._executable.ro_sources
decoded_buffers = {k: decode_buffer(v) for k, v in results.buffers.items()}

result_memory = {}
if len(decoded_buffers) != 0:
extracted = _extract_memory_regions(
execute_response._executable.memory_descriptors, ro_sources, decoded_buffers
)
for name, array in extracted.items():
result_memory[name] = array
elif not ro_sources:
result_memory["ro"] = np.zeros((0, 0), dtype=np.int64)

return QAMExecutionResult(
executable=execute_response._executable,
readout_data=result_memory,
execution_duration_microseconds=results.execution_duration_microseconds,
readout_values = {key: ReadoutValues(value.data.inner()) for key, value in results.buffers.items()}
mappings = {
mref.out(): readout_name
for mref, readout_name in execute_response._executable.ro_sources.items()
if mref.name in execute_response._executable.memory_descriptors
}
result_data = QPUResultData(mappings=mappings, readout_values=readout_values)
result_data = ResultData(result_data)
data = ExecutionData(
result_data=result_data, duration=timedelta(microseconds=results.execution_duration_microseconds or 0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why 0 instead of None?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A timedelta takes a number

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @Shadow53 in that this duration should be None if the duration was unknown, rather than filling 0.

That said, I'm confused howresults.execution_duration_microseconds could be unset in the first place: https://docs.rs/qcs-api-client-grpc/latest/qcs_api_client_grpc/models/controller/struct.ControllerJobExecutionResult.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The data gets massaged into an ExecutionResult struct that fits both QVM and QPU data. Since the former will return None for a duration, the API includes that possibility.

I wrote it in this way because it's the simplest way to appease mypy and shouldn't be a problem for QPU runs. That said, I can handle it more verbosely since it seems to have caused some confusion.

)

return QAMExecutionResult(executable=execute_response._executable, data=data)
24 changes: 14 additions & 10 deletions pyquil/api/_qvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@
# limitations under the License.
##############################################################################
from dataclasses import dataclass
from typing import Any, Mapping, Optional, Sequence, Tuple
from typing import Any, Optional, Sequence, Tuple, Dict

import numpy as np

from qcs_sdk import QCSClient, qvm
from qcs_sdk.qvm import QVMOptions
from qcs_sdk import QCSClient, qvm, ResultData, ExecutionData
from qcs_sdk.qvm import QVMOptions, QVMResultData

from pyquil._version import pyquil_version
from pyquil.api import QAM, QuantumExecutable, QAMExecutionResult, MemoryMap
Expand Down Expand Up @@ -51,7 +50,12 @@ def check_qvm_version(version: str) -> None:
@dataclass
class QVMExecuteResponse:
executable: Program
memory: Mapping[str, np.ndarray]
data: QVMResultData

@property
def memory(self) -> Dict[str, np.ndarray]:
register_map = self.data.to_register_map()
return {key: matrix.to_ndarray() for key, matrix in register_map.items()}
MarquessV marked this conversation as resolved.
Show resolved Hide resolved


class QVM(QAM[QVMExecuteResponse]):
Expand Down Expand Up @@ -150,16 +154,16 @@ def execute(
options=QVMOptions(timeout_seconds=self.timeout),
)

memory = {name: np.asarray(data.inner()) for name, data in result.memory.items()}
return QVMExecuteResponse(executable=executable, memory=memory)
return QVMExecuteResponse(executable=executable, data=result)

def get_result(self, execute_response: QVMExecuteResponse) -> QAMExecutionResult:
"""
Return the results of execution on the QVM.

Because QVM execution is synchronous, this is a no-op which returns its input.
"""
return QAMExecutionResult(executable=execute_response.executable, readout_data=execute_response.memory)

result_data = ResultData(execute_response.data)
data = ExecutionData(result_data=result_data, duration=None)
return QAMExecutionResult(executable=execute_response.executable, data=data)

def get_version_info(self) -> str:
"""
Expand Down
10 changes: 9 additions & 1 deletion pyquil/pyqvm.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

import numpy as np
from numpy.random.mtrand import RandomState
from qcs_sdk import ResultData, ExecutionData, RegisterData
from qcs_sdk.qvm import QVMResultData

from pyquil.api import QAM, QuantumExecutable, QAMExecutionResult, MemoryMap
from pyquil.paulis import PauliTerm, PauliSum
Expand Down Expand Up @@ -264,8 +266,14 @@ def get_result(self, execute_response: "PyQVM") -> QAMExecutionResult:
unused because the PyQVM, unlike other QAM's, is itself stateful.
"""
assert self.program is not None
result_data = QVMResultData.from_memory_map(
{key: RegisterData(matrix.tolist()) for key, matrix in self._memory_results.items()}
)
result_data = ResultData(result_data)
data = ExecutionData(result_data=result_data, duration=None)
return QAMExecutionResult(
executable=self.program.copy(), readout_data={k: v for k, v in self._memory_results.items()}
executable=self.program.copy(),
data=data,
)

def read_memory(self, *, region_name: str) -> np.ndarray:
Expand Down
46 changes: 40 additions & 6 deletions test/unit/test_noise.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import numpy as np
import pytest
from pytest_mock import MockerFixture
from qcs_sdk import ResultData, ExecutionData, RegisterData
from qcs_sdk.qvm import QVMResultData

from pyquil.api._qam import QAMExecutionResult
from pyquil.gates import RZ, RX, I, CZ
Expand Down Expand Up @@ -35,10 +37,10 @@
def test_pauli_kraus_map():
probabilities = [0.1, 0.2, 0.3, 0.4]
k1, k2, k3, k4 = pauli_kraus_map(probabilities)
assert np.allclose(k1, np.sqrt(0.1) * np.eye(2), atol=1 * 10 ** -8)
assert np.allclose(k2, np.sqrt(0.2) * np.array([[0, 1.0], [1.0, 0]]), atol=1 * 10 ** -8)
assert np.allclose(k3, np.sqrt(0.3) * np.array([[0, -1.0j], [1.0j, 0]]), atol=1 * 10 ** -8)
assert np.allclose(k4, np.sqrt(0.4) * np.array([[1, 0], [0, -1]]), atol=1 * 10 ** -8)
assert np.allclose(k1, np.sqrt(0.1) * np.eye(2), atol=1 * 10**-8)
assert np.allclose(k2, np.sqrt(0.2) * np.array([[0, 1.0], [1.0, 0]]), atol=1 * 10**-8)
assert np.allclose(k3, np.sqrt(0.3) * np.array([[0, -1.0j], [1.0j, 0]]), atol=1 * 10**-8)
assert np.allclose(k4, np.sqrt(0.4) * np.array([[1, 0], [0, -1]]), atol=1 * 10**-8)

two_q_pauli_kmaps = pauli_kraus_map(np.kron(probabilities, list(reversed(probabilities))))
q1_pauli_kmaps = [k1, k2, k3, k4]
Expand Down Expand Up @@ -291,8 +293,40 @@ def test_estimate_assignment_probs(mocker: MockerFixture):
mock_qc.compiler = mock_compiler
mock_qc
mock_qc.run.side_effect = [
QAMExecutionResult(executable=None, readout_data={'ro': np.array([[0]]) * int(round(p00 * trials)) + np.array([[1]]) * int(round((1 - p00) * trials))}), # I gate results
QAMExecutionResult(executable=None, readout_data={'ro': np.array([[1]]) * int(round(p11 * trials)) + np.array([[0]]) * int(round((1 - p11) * trials))}), # X gate results
QAMExecutionResult(
executable=None,
data=ExecutionData(
result_data=ResultData(
QVMResultData.from_memory_map(
{
"ro": RegisterData.from_i16(
(
np.array([[0]]) * int(round(p00 * trials))
+ np.array([[1]]) * int(round((1 - p00) * trials))
).tolist()
)
}
)
)
),
), # I gate results
QAMExecutionResult(
executable=None,
data=ExecutionData(
result_data=ResultData(
QVMResultData.from_memory_map(
{
"ro": RegisterData.from_i16(
(
np.array([[1]]) * int(round(p11 * trials))
+ np.array([[0]]) * int(round((1 - p11) * trials))
).tolist()
)
}
)
)
),
), # X gate results
]
ap_target = np.array([[p00, 1 - p11], [1 - p00, p11]])

Expand Down
Loading
Loading