Skip to content

Commit

Permalink
estimator result version
Browse files Browse the repository at this point in the history
  • Loading branch information
jyu00 committed Nov 28, 2023
1 parent 14112fe commit e2ddeb3
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 34 deletions.
2 changes: 1 addition & 1 deletion qiskit_ibm_runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,4 @@ def result_callback(job_id, result):
QISKIT_IBM_RUNTIME_LOG_FILE = "QISKIT_IBM_RUNTIME_LOG_FILE"
"""The environment variable name that is used to set the file for the IBM Quantum logger."""

warnings.warn("You are using the experimental branch. Stability is not guaranteed.")
warnings.warn("You are using the experimental-0.2 branch. Stability is not guaranteed.")
4 changes: 2 additions & 2 deletions qiskit_ibm_runtime/base_primitive.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def _run(self, tasks: Union[list[EstimatorTask], list[SamplerTask]]) -> RuntimeJ
inputs=primitive_inputs,
options=runtime_options,
callback=options_dict.get("environment", {}).get("callback", None),
result_decoder=DEFAULT_DECODERS.get((self._program_id(), self.version)),
result_decoder=DEFAULT_DECODERS.get(self._program_id()),
)

if self._backend:
Expand All @@ -158,7 +158,7 @@ def _run(self, tasks: Union[list[EstimatorTask], list[SamplerTask]]) -> RuntimeJ
options=runtime_options,
inputs=primitive_inputs,
callback=options_dict.get("environment", {}).get("callback", None),
result_decoder=DEFAULT_DECODERS.get((self._program_id(), self.version)),
result_decoder=DEFAULT_DECODERS.get(self._program_id()),
)

@property
Expand Down
2 changes: 0 additions & 2 deletions qiskit_ibm_runtime/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,4 @@
"estimator": [ResultDecoder, EstimatorResultDecoder],
"circuit-runner": RunnerResult,
"qasm3-runner": RunnerResult,
("sampler", 2): [ResultDecoder, SamplerResultDecoder],
("estimator", 2): [ResultDecoder, EstimatorResultDecoder],
}
107 changes: 93 additions & 14 deletions qiskit_ibm_runtime/qiskit/primitives/bindings_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
"""
from __future__ import annotations

from collections.abc import Iterable, Sequence
from collections.abc import Iterable
from itertools import chain, product
from typing import Dict, List, Optional, Tuple, Union, Mapping
from typing import Dict, List, Optional, Tuple, Union, Mapping, Sequence

import numpy as np
from numpy.typing import ArrayLike, NDArray
Expand Down Expand Up @@ -102,8 +102,6 @@ def __init__(
"""
super().__init__()

if vals is None and kwvals is None and shape is None:
raise ValueError("Must specify a shape if no values are present")
if vals is None:
vals = []
if kwvals is None:
Expand Down Expand Up @@ -162,6 +160,76 @@ def vals(self) -> List[np.ndarray]:
"""The non-keyword values of this array."""
return self._vals

def as_array(self, parameters: Optional[Iterable[Parameter]] = None) -> np.ndarray:
"""Return the contents of this bindings array as a single NumPy array.
As with each :attr:`~vals` and :attr:`~kwvals` array, the parameters are indexed along the
last dimension.
The order of the :attr:`~vals` portion of this bindings array is always preserved, and
always comes first in the returned array, irrespective of whether ``parameters`` are
provided.
If ``parameters`` are provided, then they determine the order of any :attr:`~kwvals`
present in this bindings array. If :attr:`~vals` are present in addition to :attr:`~kwvals`,
then it is up to the user to ensure that their provided ``parameters`` account for this.
Parameters:
parameters: Optional parameters that determine the order of the output.
Returns:
This bindings array as a single NumPy array.
Raises:
RuntimeError: If these bindings contain multple dtypes.
KeyError: If ``parameters`` are provided that are not a superset of those in this
bindings array.
"""
dtypes = {arr.dtype for arr in self.vals}
dtypes.update(arr.dtype for arr in self.kwvals.values())
if len(dtypes) > 1:
raise RuntimeError(f"Multiple dtypes ({dtypes}) were found.")
dtype = next(iter(dtypes)) if dtypes else float

if self.num_parameters == 0 and not self.shape:
# we want this special case to look like a single binding on no parameters
return np.empty((1, 0), dtype=dtype)

ret = np.empty(shape_tuple(self.shape, self.num_parameters), dtype=dtype)

# always start by placing the vals in the returned array
pos = 0
for arr in self.vals:
size = arr.shape[-1]
ret[..., pos : pos + size] = arr
pos += size

def _param_name(parameter: Union[Parameter, str]) -> str:
"""Helper function to handle parameters or strings"""
if isinstance(parameter, Parameter):
return parameter.name
return parameter

if parameters is None:
# preserve the order of the kwvals
for arr in self.kwvals.values():
size = arr.shape[-1]
ret[..., pos : pos + size] = arr
pos += size
elif self.kwvals:
# use the order of the provided parameters
parameters = {_param_name(parameter): idx for idx, parameter in enumerate(parameters)}
for arr_params, arr in self.kwvals.items():
try:
idxs = [parameters[_param_name(param)] for param in arr_params]
except KeyError as ex:
raise KeyError(
"This bindings array has a parameter absent from the provided parameters."
) from ex
ret[..., idxs] = arr

return ret

def bind_at_idx(self, circuit: QuantumCircuit, idx: Tuple[int, ...]) -> QuantumCircuit:
"""Return the circuit bound to the values at the provided index.
Expand Down Expand Up @@ -229,26 +297,35 @@ def ravel(self) -> BindingsArray:
"""
return self.reshape(self.size)

def reshape(self, shape: Union[int, Iterable[int]]) -> BindingsArray:
def reshape(self, *shape: ShapeInput) -> BindingsArray:
"""Return a new :class:`~BindingsArray` with a different shape.
This results in a new view of the same arrays.
Args:
shape: The shape of the returned bindings array.
*shape: The shape of the returned bindings array.
Returns:
A new bindings array.
Raises:
ValueError: If the provided shape has a different product than the current size.
"""
shape = (shape, -1) if isinstance(shape, int) else (*shape, -1)
if np.prod(shape[:-1]).astype(int) != self.size:
raise ValueError("Reshaping cannot change the total number of elements.")
vals = [val.reshape(shape) for val in self._vals]
kwvals = {params: val.reshape(shape) for params, val in self._kwvals.items()}
return BindingsArray(vals, kwvals, shape[:-1])
shape = shape_tuple(shape)

# if we have a minus 1, try and replace it with with a positive number
if any(dim < 0 for dim in shape):
if (subsize := np.prod([dim for dim in shape if dim >= 0]).astype(int)) > 0:
shape = tuple(dim if dim > 0 else self.size // subsize for dim in shape)

if np.prod(shape).astype(int) != self.size:
raise ValueError(f"Reshaping cannot change the total number of elements. {shape}")

vals = [val.reshape(shape + (val.shape[-1],)) for val in self._vals]
kwvals = {
params: val.reshape(shape + (val.shape[-1],)) for params, val in self._kwvals.items()
}
return BindingsArray(vals, kwvals, shape)

@classmethod
def coerce(cls, bindings_array: BindingsArrayLike) -> BindingsArray:
Expand Down Expand Up @@ -346,15 +423,17 @@ def examine_array(*possible_shapes):
if len(parameters) > 1:
# here, the last dimension _has_ to be over parameters
examine_array(val.shape[:-1])
elif val.shape[-1] != 1:
# here, if the last dimension is not 1 then the shape is the shape
elif val.shape == () or val.shape == (1,) or val.shape[-1] != 1:
# here, if the last dimension is not 1 or shape is () or (1,) then the shape is the shape
examine_array(val.shape)
else:
# here, the last dimension could be over parameters or not
examine_array(val.shape, val.shape[:-1])

if len(vals) == 1 and len(kwvals) == 0:
examine_array(vals[0].shape[:-1])
elif len(vals) == 0 and len(kwvals) == 0:
examine_array(())
else:
for val in vals:
# here, the last dimension could be over parameters or not
Expand Down
2 changes: 2 additions & 0 deletions qiskit_ibm_runtime/qiskit_runtime_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,7 @@ def run(
f"The backend {backend.name} currently has a status of {status.status_msg}."
)

version = inputs.get("version", 1)
try:
response = self._api_client.program_run(
program_id=program_id,
Expand Down Expand Up @@ -1110,6 +1111,7 @@ def run(
result_decoder=result_decoder,
image=qrt_options.image,
service=self,
version=version,
)
return job

Expand Down
12 changes: 11 additions & 1 deletion qiskit_ibm_runtime/runtime_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def __init__(
image: Optional[str] = "",
session_id: Optional[str] = None,
tags: Optional[List] = None,
version: Optional[int] = None,
) -> None:
"""RuntimeJob constructor.
Expand All @@ -119,6 +120,7 @@ def __init__(
service: Runtime service.
session_id: Job ID of the first job in a runtime session.
tags: Tags assigned to the job.
version: Primitive version.
"""
super().__init__(backend=backend, job_id=job_id)
self._api_client = api_client
Expand All @@ -135,6 +137,7 @@ def __init__(
self._session_id = session_id
self._tags = tags
self._usage_estimation: Dict[str, Any] = {}
self._version = version

decoder = result_decoder or DEFAULT_DECODERS.get(program_id, None) or ResultDecoder
if isinstance(decoder, Sequence):
Expand Down Expand Up @@ -226,7 +229,14 @@ def result( # pylint: disable=arguments-differ
self._api_client.job_results(job_id=self.job_id())
)

return _decoder.decode(result_raw) if result_raw else None
version_param = {}
# TODO: Remove getting/setting version once it's in result metadata
if self.program_id in ["estimator"]:
if not self._version:
self._version = self.inputs.get("version", 1)
version_param["version"] = self._version

return _decoder.decode(result_raw, self._version) if result_raw else None # type: ignore

def cancel(self) -> None:
"""Cancel the job.
Expand Down
26 changes: 12 additions & 14 deletions qiskit_ibm_runtime/utils/estimator_result_decoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,20 @@ class EstimatorResultDecoder(ResultDecoder):
"""Class used to decode estimator results"""

@classmethod
def decode(cls, raw_result: str) -> EstimatorResult:
def decode( # type: ignore # pylint: disable=arguments-differ
cls, raw_result: str, version: int
) -> EstimatorResult:
"""Convert the result to EstimatorResult."""
decoded: Dict = super().decode(raw_result)
return EstimatorResult(
values=np.asarray(decoded["values"]),
metadata=decoded["metadata"],
)


class EstimatorV2ResultDecoder(ResultDecoder):
"""Class used to decode v2 estimator results"""

@classmethod
def decode(cls, raw_result: str) -> List[TaskResult]:
"""Convert the result to a list of TaskResult."""
decoded: Dict = super().decode(raw_result)
if version == 2:
out_results = []
for val, meta in zip(decoded["values"], decoded["metadata"]):
if not isinstance(val, np.ndarray):
val = np.asarray(val)
out_results.append(
TaskResult(data={"evs": val, "stds": meta.pop("standard_error")}, metadata=meta)
)
return out_results
return EstimatorResult(
values=np.asarray(decoded["values"]),
metadata=decoded["metadata"],
Expand Down

0 comments on commit e2ddeb3

Please sign in to comment.