diff --git a/qiskit_experiments/database_service/db_analysis_result.py b/qiskit_experiments/database_service/db_analysis_result.py index 9a0b132cd8..3ef001389d 100644 --- a/qiskit_experiments/database_service/db_analysis_result.py +++ b/qiskit_experiments/database_service/db_analysis_result.py @@ -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 @@ -149,7 +150,7 @@ 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, @@ -157,16 +158,19 @@ def save(self) -> None: "_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) 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, @@ -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, } @@ -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}" diff --git a/qiskit_experiments/database_service/json.py b/qiskit_experiments/database_service/json.py index ff8d1f41f5..32f4f9438c 100644 --- a/qiskit_experiments/database_service/json.py +++ b/qiskit_experiments/database_service/json.py @@ -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 @@ -25,30 +26,36 @@ 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. @@ -56,6 +63,8 @@ def serialize_object( 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. @@ -64,6 +73,9 @@ 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: @@ -71,24 +83,47 @@ def serialize_object( 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: @@ -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 diff --git a/test/database_service/test_db_analysis_result.py b/test/database_service/test_db_analysis_result.py index 85a62f14af..3504793fae 100644 --- a/test/database_service/test_db_analysis_result.py +++ b/test/database_service/test_db_analysis_result.py @@ -17,6 +17,7 @@ from unittest import mock import json +import math import numpy as np from qiskit.test import QiskitTestCase @@ -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 = {