Skip to content

Commit

Permalink
Merge branch 'main' into fake-backend-utility
Browse files Browse the repository at this point in the history
  • Loading branch information
kt474 authored Nov 2, 2024
2 parents 7408c3a + 9f2dad5 commit 82f5ca9
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 34 deletions.
2 changes: 2 additions & 0 deletions qiskit_ibm_runtime/execution_span/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@
ExecutionSpans
ShapeType
SliceSpan
TwirledSliceSpan
"""

from .double_slice_span import DoubleSliceSpan
from .execution_span import ExecutionSpan, ShapeType
from .execution_spans import ExecutionSpans
from .slice_span import SliceSpan
from .twirled_slice_span import TwirledSliceSpan
12 changes: 6 additions & 6 deletions qiskit_ibm_runtime/execution_span/double_slice_span.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ class DoubleSliceSpan(ExecutionSpan):
"""An :class:`~.ExecutionSpan` for data stored in a sliceable format.
This type of execution span references pub result data by assuming that it is a sliceable
portion of the data where the shots are the outermost slice and the rest of the data is flattened.
Therefore, for each pub dependent on this span, the constructor accepts two :class:`slice` objects,
along with the corresponding shape of the data to be sliced; in contrast to
:class:`~.SliceSpan`, this class does not assume that *all* shots for a particular set of parameter
values are contiguous in the array of data.
portion of the data where the shots are the outermost slice and the rest of the data is
flattened. Therefore, for each pub dependent on this span, the constructor accepts two
:class:`slice` objects, along with the corresponding shape of the data to be sliced; in contrast
to :class:`~.SliceSpan`, this class does not assume that *all* shots for a particular set of
parameter values are contiguous in the array of data.
Args:
start: The start time of the span, in UTC.
stop: The stop time of the span, in UTC.
data_slices: A map from pub indices to ``(shape_tuple, slice, slice)``.
data_slices: A map from pub indices to ``(shape_tuple, flat_shape_slice, shots_slice)``.
"""

def __init__(
Expand Down
92 changes: 92 additions & 0 deletions qiskit_ibm_runtime/execution_span/twirled_slice_span.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2024.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""TwirledSliceSpan"""

from __future__ import annotations

from datetime import datetime
from typing import Iterable

import math
import numpy as np
import numpy.typing as npt

from .execution_span import ExecutionSpan, ShapeType


class TwirledSliceSpan(ExecutionSpan):
"""An :class:`~.ExecutionSpan` for data stored in a sliceable format when twirling.
This type of execution span references pub result data that came from a twirled sampler
experiment which was executed by either prepending or appending an axis to paramater values
to account for twirling. Concretely, ``data_slices`` is a map from pub slices to tuples
``(twirled_shape, at_front, shape_slice, shots_slice)`` where
* ``twirled_shape`` is the shape tuple including a twirling axis, and where the last
axis is shots per randomization,
* ``at_front`` is whether ``num_randomizations`` is at the front of the tuple, as
opposed to right before the ``shots`` axis at the end,
* ``shape_slice`` is a slice of an array of shape ``twirled_shape[:-1]``, flattened,
* and ``shots_slice`` is a slice of ``twirled_shape[-1]``.
Args:
start: The start time of the span, in UTC.
stop: The stop time of the span, in UTC.
data_slices: A map from pub indices to length-4 tuples described above.
"""

def __init__(
self,
start: datetime,
stop: datetime,
data_slices: dict[int, tuple[ShapeType, bool, slice, slice]],
):
super().__init__(start, stop)
self._data_slices = data_slices

def __eq__(self, other: object) -> bool:
return isinstance(other, TwirledSliceSpan) and (
self.start == other.start
and self.stop == other.stop
and self._data_slices == other._data_slices
)

@property
def pub_idxs(self) -> list[int]:
return sorted(self._data_slices)

@property
def size(self) -> int:
size = 0
for shape, _, shape_sl, shots_sl in self._data_slices.values():
size += len(range(math.prod(shape[:-1]))[shape_sl]) * len(range(shape[-1])[shots_sl])
return size

def mask(self, pub_idx: int) -> npt.NDArray[np.bool_]:
twirled_shape, at_front, shape_sl, shots_sl = self._data_slices[pub_idx]
mask = np.zeros(twirled_shape, dtype=np.bool_)
mask.reshape((np.prod(twirled_shape[:-1]), twirled_shape[-1]))[(shape_sl, shots_sl)] = True

if at_front:
# if the first axis is over twirling samples, push them right before shots
ndim = len(twirled_shape)
mask = mask.transpose((*range(1, ndim - 1), 0, ndim - 1))
twirled_shape = twirled_shape[1:-1] + twirled_shape[:1] + twirled_shape[-1:]

# merge twirling axis and shots axis before returning
return mask.reshape((*twirled_shape[:-2], math.prod(twirled_shape[-2:])))

def filter_by_pub(self, pub_idx: int | Iterable[int]) -> "TwirledSliceSpan":
pub_idx = {pub_idx} if isinstance(pub_idx, int) else set(pub_idx)
slices = {idx: val for idx, val in self._data_slices.items() if idx in pub_idx}
return TwirledSliceSpan(self.start, self.stop, slices)
48 changes: 21 additions & 27 deletions qiskit_ibm_runtime/ibm_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ def __getattr__(self, name: str) -> Any:
"'{}' object has no attribute '{}'".format(self.__class__.__name__, name)
)
# Lazy load properties and pulse defaults and construct the target object.
self._get_properties()
self.properties()
self._get_defaults()
self._convert_to_target()
# Check if the attribute now is available on IBMBackend class due to above steps
Expand All @@ -239,16 +239,6 @@ def __getattr__(self, name: str) -> Any:
"'{}' object has no attribute '{}'".format(self.__class__.__name__, name)
)

def _get_properties(self, datetime: Optional[python_datetime] = None) -> None:
"""Gets backend properties and decodes it"""
if datetime:
datetime = local_to_utc(datetime)
if datetime or not self._properties:
api_properties = self._api_client.backend_properties(self.name, datetime=datetime)
if api_properties:
backend_properties = properties_from_server_data(api_properties)
self._properties = backend_properties

def _get_defaults(self, refresh: bool = False) -> None:
"""Gets defaults if pulse backend and decodes it"""
if (
Expand All @@ -261,21 +251,16 @@ def _get_defaults(self, refresh: bool = False) -> None:
def _convert_to_target(self, refresh: bool = False) -> None:
"""Converts backend configuration, properties and defaults to Target object"""
if refresh or not self._target:
if self.options.use_fractional_gates is None:
include_control_flow = True
include_fractional_gates = True
else:
# In IBM backend architecture as of today
# these features can be only exclusively supported.
include_control_flow = not self.options.use_fractional_gates
include_fractional_gates = self.options.use_fractional_gates

self._target = convert_to_target(
configuration=self._configuration, # type: ignore[arg-type]
properties=self._properties,
defaults=self._defaults,
include_control_flow=include_control_flow,
include_fractional_gates=include_fractional_gates,
# In IBM backend architecture as of today
# these features can be only exclusively supported.
include_control_flow=self.options.use_fractional_gates is None
or not self.options.use_fractional_gates,
include_fractional_gates=self.options.use_fractional_gates is None
or self.options.use_fractional_gates,
)

@classmethod
Expand Down Expand Up @@ -344,7 +329,7 @@ def target(self) -> Target:
Returns:
Target
"""
self._get_properties()
self.properties()
self._get_defaults()
self._convert_to_target()
return self._target
Expand All @@ -355,10 +340,19 @@ def target_history(self, datetime: Optional[python_datetime] = None) -> Target:
Returns:
Target with properties found on `datetime`
"""
self._get_properties(datetime=datetime)
self._get_defaults()
self._convert_to_target(refresh=True)
return self._target

return convert_to_target(
configuration=self._configuration, # type: ignore[arg-type]
properties=self.properties(datetime=datetime), # pylint: disable=unexpected-keyword-arg
defaults=self._defaults,
# In IBM backend architecture as of today
# these features can be only exclusively supported.
include_control_flow=self.options.use_fractional_gates is None
or not self.options.use_fractional_gates,
include_fractional_gates=self.options.use_fractional_gates
or self.options.use_fractional_gates,
)

def refresh(self) -> None:
"""Retrieve the newest backend configuration and refresh the current backend target."""
Expand All @@ -367,7 +361,7 @@ def refresh(self) -> None:
instance=self._instance,
):
self._configuration = config
self._get_properties(datetime=python_datetime.now())
self.properties(refresh=True) # pylint: disable=unexpected-keyword-arg
self._get_defaults(refresh=True)
self._convert_to_target(refresh=True)

Expand Down
18 changes: 18 additions & 0 deletions qiskit_ibm_runtime/utils/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
DoubleSliceSpan,
SliceSpan,
ExecutionSpans,
TwirledSliceSpan,
)

from .noise_learner_result import NoiseLearnerResult
Expand Down Expand Up @@ -341,6 +342,16 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ
},
}
return {"__type__": "DoubleSliceSpan", "__value__": out_val}
if isinstance(obj, TwirledSliceSpan):
out_val = {
"start": obj.start,
"stop": obj.stop,
"data_slices": {
idx: (shape, at_front, arg_sl.start, arg_sl.stop, shot_sl.start, shot_sl.stop)
for idx, (shape, at_front, arg_sl, shot_sl) in obj._data_slices.items()
},
}
return {"__type__": "TwirledSliceSpan", "__value__": out_val}
if isinstance(obj, SliceSpan):
out_val = {
"start": obj.start,
Expand Down Expand Up @@ -470,6 +481,13 @@ def object_hook(self, obj: Any) -> Any:
for idx, (shape, arg0, arg1, shot0, shot1) in obj_val["data_slices"].items()
}
return DoubleSliceSpan(**obj_val)
if obj_type == "TwirledSliceSpan":
data_slices = obj_val["data_slices"]
obj_val["data_slices"] = {
int(idx): (tuple(shape), at_start, slice(arg0, arg1), slice(shot0, shot1))
for idx, (shape, at_start, arg0, arg1, shot0, shot1) in data_slices.items()
}
return TwirledSliceSpan(**obj_val)
if obj_type == "ExecutionSpan":
new_slices = {
int(idx): (tuple(shape), slice(*sl_args))
Expand Down
4 changes: 4 additions & 0 deletions release-notes/unreleased/2011.feat.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Added :class:`.TwirledSliceSpan`, an :class:`ExecutionSpan` to be used when
twirling is enabled in the sampler. In particular, it keeps track of an extra shape
axis corresponding to twirling randomizations, and also whether this axis exists at
the front of the shape tuple, or right before the shots axis.
8 changes: 8 additions & 0 deletions test/integration/test_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ def test_backend_target_history(self):
self.assertIsNotNone(backend.target_history(datetime=datetime.now() - timedelta(30)))

@production_only
def test_properties_not_cached_target_history(self):
"""Check backend properties is not cached in target_history()."""
backend = self.backend
with self.subTest(backend=backend.name):
properties = backend.properties()
backend.target_history(datetime=datetime.now() - timedelta(60))
self.assertEqual(properties, backend.properties())

def test_backend_target_refresh(self):
"""Test refreshing the backend target."""
backend = self.backend
Expand Down
9 changes: 9 additions & 0 deletions test/unit/test_data_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
DoubleSliceSpan,
SliceSpan,
ExecutionSpans,
TwirledSliceSpan,
)

from .mock.fake_runtime_client import CustomResultRuntimeJob
Expand Down Expand Up @@ -468,6 +469,14 @@ def make_test_primitive_results(self):
datetime(2024, 8, 21),
{0: ((14,), slice(2, 3), slice(1, 9))},
),
TwirledSliceSpan(
datetime(2024, 9, 20),
datetime(2024, 3, 21),
{
0: ((14, 18, 21), True, slice(2, 3), slice(1, 9)),
2: ((18, 14, 19), False, slice(2, 3), slice(1, 9)),
},
),
]
)
}
Expand Down
Loading

0 comments on commit 82f5ca9

Please sign in to comment.