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 all 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
39 changes: 39 additions & 0 deletions docs/source/introducing_v4.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,45 @@ 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 reuse qubits or use dynamic control flow can emit a different 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 fit a rectangular matrix, 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.

.. warning::

It's possible to have a program that results in a rectangular matrix of readout data, but have more than one value
per memory reference per shot due to qubit reuse. In these cases, a `RegisterMatrixConversionError` will _not_
be raised, since the resulting matrix would be valid for some number of shots. It's important to be aware of this
possibility, and to still use `get_raw_readout_data` if that possibility is a concern.

.. 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.get_raw_readout_data())


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

Expand Down
56 changes: 28 additions & 28 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 @@ -28,7 +28,7 @@ rpcq = "^3.10.0"
pydantic = "^1.10.7"
networkx = ">=2.5"
importlib-metadata = { version = ">=3.7.3,<5", python = "<3.8" }
qcs-sdk-python = "0.10.8"
qcs-sdk-python = "0.11.0"
tenacity = "^8.2.2"
types-python-dateutil = "^2.8.19"
types-retry = "^0.9.9"
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
78 changes: 72 additions & 6 deletions pyquil/api/_qam.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@
# 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 datetime import timedelta

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 +43,71 @@ 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.
"""

def get_raw_readout_data(self) -> Union[RawQVMReadoutData, RawQPUReadoutData]:
"""
Get the raw result data. This will be a flattened structure derived
from :class:`qcs_sdk.qvm.QVMResultData` or :class:`qcs_sdk.qpu.QPUResultData`
depending on where the job was run. See their respective documentation
for more information on the data format.

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()

def get_register_map(self) -> Dict[str, Optional[np.ndarray]]:
"""
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 re-use qubits 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 matrix with one value per memory reference, per shot
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.

execution_duration_microseconds: Optional[int] = field(default=None)
"""Duration job held exclusive hardware access. Defaults to ``None`` when information is not available."""
.. warning::

An exception will _not_ be raised if the result data happens to fit a rectangular matrix, since
it's possible the register map is valid for some number of shots. Users should be aware of this
possibility, especially when running programs that utilize qubit reuse or dynamic control flow.

"""
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 the `register_map()` method 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.get_register_map()

@property
def execution_duration_microseconds(self) -> Optional[float]:
"""Duration job held exclusive hardware access. Defaults to ``None`` when information is not available."""
if isinstance(self.data.duration, timedelta):
return self.data.duration.total_seconds() * 1e6
return None


class QAM(ABC, Generic[T]):
Expand Down
Loading