Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
43 changes: 33 additions & 10 deletions qiskit_experiments/database_service/db_analysis_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
from typing import Optional, List, Union, Dict, Any
import uuid
import copy
import math

from .database_service import DatabaseServiceV1
from .json import ExperimentEncoder, ExperimentDecoder
from .json import ExperimentEncoder, ExperimentDecoder, serialize_safe_float
from .utils import save_data, qiskit_version
from .exceptions import DbExperimentDataError
from .device_component import DeviceComponent, to_component
Expand Down Expand Up @@ -149,24 +150,27 @@ def save(self) -> None:
"Analysis result cannot be saved because no experiment service is available."
)
return
# Get DB fit data

value = self.value
result_data = {
"_value": value,
"_extra": self.extra,
"_source": self._source,
}

# Display compatible float values in in DB
if isinstance(value, (int, float, bool)):
result_data["value"] = float(value)
elif isinstance(value, FitVal):
if isinstance(value.value, (int, float)):
result_data["value"] = value.value
# Format special DB display fields
if isinstance(value, FitVal):
db_value = self._display_format(value.value)
if db_value is not None:
result_data["value"] = db_value
if isinstance(value.stderr, (int, float)):
result_data["variance"] = value.stderr ** 2
result_data["variance"] = self._display_format(value.stderr ** 2)
Comment thread
chriseclectic marked this conversation as resolved.
if isinstance(value.unit, str):
result_data["unit"] = value.unit
else:
db_value = self._display_format(value)
if db_value is not None:
result_data["value"] = db_value

new_data = {
"experiment_id": self._experiment_id,
Expand All @@ -177,7 +181,7 @@ def save(self) -> None:
"result_id": self.result_id,
"result_data": result_data,
"tags": self.tags,
"chisq": self._chisq,
"chisq": self._display_format(self._chisq),
"quality": self.quality,
"verified": self.verified,
}
Expand Down Expand Up @@ -417,6 +421,25 @@ def auto_save(self, save_val: bool) -> None:
self.save()
self._auto_save = save_val

@staticmethod
def _display_format(value):
"""Format values for supported types for display in database service"""
if value is None or isinstance(value, (int, bool, str)):
# Pass supported value types directly
return value
if isinstance(value, float):
# Safe handling on NaN float values that serialize to invalid JSON
if math.isfinite(value):
return value
else:
return serialize_safe_float(value)["__value__"]
if isinstance(value, complex):
# Convert complex floats to strings for display
return f"{value}"
# For all other value types that cannot be natively displayed
# we return the class name
return f"({type(value).__name__})"

def __str__(self):
ret = f"{type(self).__name__}"
ret += f"\n- name: {self.name}"
Expand Down
107 changes: 74 additions & 33 deletions qiskit_experiments/database_service/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
# 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.
# pylint: disable=method-hidden,too-many-return-statements
# pylint: disable=method-hidden,too-many-return-statements,c-extension-no-member

"""Experiment serialization methods."""

import json
import math
import dataclasses
import importlib
import inspect
Expand All @@ -25,37 +26,45 @@
from qiskit.quantum_info.states import Statevector, DensityMatrix


def deserialize_object(mod_name: str, class_name: str, args: Tuple, kwargs: Dict) -> Any:
"""Deserialize a class object from its init args and kwargs.

Args:
mod_name: Name of the module.
class_name: Name of the class.
args: args for class init method.
kwargs: kwargs for class init method.

Returns:
Deserialized object.

Raises:
ValueError: If unable to find the class.
"""
mod = importlib.import_module(mod_name)
for name, cls in inspect.getmembers(mod, inspect.isclass):
if name == class_name:
return cls(*args, **kwargs)
raise ValueError(f"Unable to find class {class_name} in module {mod_name}")
def serialize_safe_float(obj: any):
"""Recursively serialize basic types safely handing inf and NaN"""
if isinstance(obj, float):
if math.isfinite(obj):
return obj
else:
value = obj
if math.isnan(obj):
value = "NaN"
elif obj == math.inf:
value = "Infinity"
elif obj == -math.inf:
value = "-Infinity"
return {"__type__": "safe_float", "__value__": value}
elif isinstance(obj, (list, tuple)):
return [serialize_safe_float(i) for i in obj]
elif isinstance(obj, dict):
return {key: serialize_safe_float(val) for key, val in obj.items()}
elif isinstance(obj, complex):
return {"__type__": "complex", "__value__": serialize_safe_float([obj.real, obj.imag])}
elif isinstance(obj, np.ndarray):
value = obj.tolist()
if issubclass(obj.dtype.type, np.inexact) and not np.isfinite(obj).all():
value = serialize_safe_float(value)
return {"__type__": "array", "__value__": value}
return obj


def serialize_object(
cls: Type, args: Optional[Tuple] = None, kwargs: Optional[Dict] = None
cls: Type, args: Optional[Tuple] = None, kwargs: Optional[Dict] = None, safe_float: bool = True
) -> Dict:
"""Serialize a class object from its init args and kwargs.

Args:
cls: The object to be serialized.
args: the class init arg values for reconstruction.
kwargs: the class init kwarg values for reconstruction.
safe_float: if True check float values for NaN, inf and -inf
and cast to strings during serialization.

Returns:
Dict for serialization.
Expand All @@ -64,31 +73,57 @@ def serialize_object(
"__name__": cls.__name__,
"__module__": cls.__module__,
}
if safe_float:
args = serialize_safe_float(args)
kwargs = serialize_safe_float(kwargs)
if args:
value["__args__"] = args
if kwargs:
value["__kwargs__"] = kwargs
return {"__type__": "__object__", "__value__": value}


def deserialize_object(mod_name: str, class_name: str, args: Tuple, kwargs: Dict) -> Any:
"""Deserialize a class object from its init args and kwargs.

Args:
mod_name: Name of the module.
class_name: Name of the class.
args: args for class init method.
kwargs: kwargs for class init method.

Returns:
Deserialized object.

Raises:
ValueError: If unable to find the class.
"""
mod = importlib.import_module(mod_name)
for name, cls in inspect.getmembers(mod, inspect.isclass):
if name == class_name:
return cls(*args, **kwargs)
raise ValueError(f"Unable to find class {class_name} in module {mod_name}")


class ExperimentEncoder(json.JSONEncoder):
"""JSON Encoder for Numpy arrays and complex numbers."""

def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ
if isinstance(obj, np.ndarray):
return {"__type__": "array", "__value__": obj.tolist()}
if isinstance(obj, complex):
return {"__type__": "complex", "__value__": [obj.real, obj.imag]}
if isinstance(obj, (np.ndarray, complex)):
return serialize_safe_float(obj)
if dataclasses.is_dataclass(obj):
return serialize_object(type(obj), kwargs=dataclasses.asdict(obj))
return serialize_object(type(obj), kwargs=dataclasses.asdict(obj), safe_float=True)
if isinstance(obj, (Operator, Choi)):
return serialize_object(
type(obj),
args=(obj.data,),
kwargs={"input_dims": obj.input_dims(), "output_dims": obj.output_dims()},
safe_float=False,
)
if isinstance(obj, (Statevector, DensityMatrix)):
return serialize_object(type(obj), args=(obj.data,), kwargs={"dims": obj.dims()})
return serialize_object(
type(obj), args=(obj.data,), kwargs={"dims": obj.dims()}, safe_float=False
)
if isinstance(obj, FunctionType):
return {"__type__": "function", "__value__": obj.__name__}
try:
Expand All @@ -100,26 +135,32 @@ def default(self, obj: Any) -> Any: # pylint: disable=arguments-differ
class ExperimentDecoder(json.JSONDecoder):
"""JSON Decoder for Numpy arrays and complex numbers."""

_NaNs = {"NaN": math.nan, "Infinity": math.inf, "-Infinity": -math.inf}

def __init__(self, *args, **kwargs):
super().__init__(object_hook=self.object_hook, *args, **kwargs)

def object_hook(self, obj):
"""Object hook."""
if "__type__" in obj:
if obj["__type__"] == "complex":
obj_type = obj["__type__"]
if obj_type == "complex":
val = obj["__value__"]
return val[0] + 1j * val[1]
if obj["__type__"] == "array":
if obj_type == "array":
return np.array(obj["__value__"])
if obj["__type__"] == "function":
if obj_type == "function":
return obj["__value__"]
if obj["__type__"] == "__object__":
if obj_type == "__object__":
value = obj["__value__"]
class_name = value["__name__"]
mod_name = value["__module__"]
args = value.get("__args__", tuple())
kwargs = value.get("__kwargs__", dict())
return deserialize_object(mod_name, class_name, args, kwargs)
if obj["__type__"] == "__class_name__":
if obj_type == "safe_float":
value = obj["__value__"]
return self._NaNs.get(value, value)
if obj_type == "__class_name__":
return obj["__value__"]
return obj
25 changes: 25 additions & 0 deletions test/database_service/test_db_analysis_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from unittest import mock
import json

import math
import numpy as np

from qiskit.test import QiskitTestCase
Expand Down Expand Up @@ -126,6 +127,30 @@ def test_source(self):
self.assertIn("DbAnalysisResultV1", result.source["class"])
self.assertTrue(result.source["qiskit_version"])

def test_display_format_inf(self):
"""Test conversion of inf for display value"""
self.assertEqual(DbAnalysisResult._display_format(np.inf), "Infinity")
self.assertEqual(DbAnalysisResult._display_format(-np.inf), "-Infinity")
self.assertEqual(DbAnalysisResult._display_format(np.nan), "NaN")
self.assertEqual(DbAnalysisResult._display_format(math.inf), "Infinity")
self.assertEqual(DbAnalysisResult._display_format(-math.inf), "-Infinity")
self.assertEqual(DbAnalysisResult._display_format(math.nan), "NaN")

def test_display_format_complex(self):
"""Test conversion of db displays"""
value = DbAnalysisResult._display_format(1e-10j)
self.assertIsInstance(value, str)

def test_display_format_list(self):
"""Test conversion of db displays"""
value = DbAnalysisResult._display_format(list(range(5)))
self.assertEqual(value, "(list)")

def test_display_format_array(self):
"""Test conversion of db displays"""
value = DbAnalysisResult._display_format(np.arange(5))
self.assertEqual(value, "(ndarray)")

def _new_analysis_result(self, **kwargs):
"""Return a new analysis result."""
values = {
Expand Down