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 7 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
66 changes: 61 additions & 5 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 dataclasses import dataclass
from typing import Any, Generic, Mapping, Optional, TypeVar, Sequence, Union

from deprecated import deprecated
import numpy as np
from qcs_sdk import ExecutionData
from qcs_sdk.qpu import QPUResultData
from qcs_sdk.qvm import QVMResultData

from pyquil.api._abstract_compiler import QuantumExecutable


Expand All @@ -37,11 +42,62 @@ 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[QVMResultData, QPUResultData]:
"""
Get the raw result data. This will either be a ``QVMResultData`` or ``QPUResultData``
depending on where the job was run.

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, consdier using the ``register_map`` property instead.
"""
return self.data.result_data.inner()

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) -> Mapping[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 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) for key, value in results.buffers.items()}
mappings = {
key: value.name
for key, value in execute_response._executable.ro_sources
if key 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