Skip to content
3 changes: 3 additions & 0 deletions qiskit_experiments/database_service/db_analysis_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ def _from_service_data(cls, service_data: Dict) -> "DbAnalysisResultV1":
result_data = service_data.pop("result_data")
value = result_data.pop("_value")
extra = result_data.pop("_extra", {})
if extra:
chisq = extra.pop("reduced_chisq", None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The extra field is not what is saved in the chisq of the analysis result, this is just leftover metadata from certain curve fitting experiments. It should just beresult_data.pop("chisq", None) here, but since we apply the display_format function for modifying chisq for UI display we will need to add a line to save the original without modification.

I think the correct thing to do here is update line 155 of the save function to store the non-modified chisq:

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

then this line should be

Suggested change
chisq = extra.pop("reduced_chisq", None)
chisq = result_data.pop("_chisq", None)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did as you suggest and it's working. But I think the correct thing would be (in another PR) to add chisq as a parameter in create_analysis_result and update_analysis_result. Is there a reason for chisq to be different from other attributes, like quality and verified?

source = result_data.pop("_source", None)

# Initialize the result object
Expand All @@ -216,6 +218,7 @@ def _from_service_data(cls, service_data: Dict) -> "DbAnalysisResultV1":
result_id=service_data.pop("result_id"),
quality=service_data.pop("quality"),
extra=extra,
chisq=chisq,
verified=service_data.pop("verified"),
tags=service_data.pop("tags"),
service=service_data.pop("service"),
Expand Down
8 changes: 7 additions & 1 deletion qiskit_experiments/database_service/db_experiment_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,10 +871,15 @@ def load(cls, experiment_id: str, service: DatabaseServiceV1) -> "DbExperimentDa
notes=service_data.pop("notes"),
**service_data,
)

if expdata.service is None:
expdata.service = service

# Retrieve analysis results
# Maybe this isn't necessary but the repr of the class should
# be updated to show correct number of results including remote ones
expdata._retrieve_analysis_results()

# mark it as existing in the DB
expdata._created_in_db = True
return expdata
Expand Down Expand Up @@ -1326,6 +1331,8 @@ def _set_service(self, service: DatabaseServiceV1) -> None:
if self._service:
raise DbExperimentDataError("An experiment service is already being used.")
self._service = service
for result in self._analysis_results.values():
result.service = service

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the service of the results is not getting set correctly when loading from DB rather than fix it here it might be better to set the service in the DbExperimentData._retrieve_analysis_results or DbAnalysisResult._from_service_data since these will also affect other functions that retrieve results from DB, not just load.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand. Setting the service here, when the experiment service is set, will affect any function that will query about the service, not just load.

with contextlib.suppress(Exception):
self.auto_save = self._service.options.get("auto_save", False)

Expand Down Expand Up @@ -1393,7 +1400,6 @@ def __str__(self):
ret += f"\nBackend: {self.backend}"
if self.tags:
ret += f"\nTags: {self.tags}"
ret += f"\nData: {len(self._data)}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's useful to know how much data is in the container (though I removed this whole repr method from the base DB class in another PR anyway, since if we want a user to only use ExperimentData we only need a pretty print for that class)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I returned it, but I think the phrasing should be clearer

ret += f"\nAnalysis Results: {n_res}"
ret += f"\nFigures: {len(self._figures)}"
ret += "\n" + line
Expand Down
6 changes: 3 additions & 3 deletions test/fake_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ class FakeBackend(BackendV1):

def __init__(self, max_experiments=None):
configuration = QasmBackendConfiguration(
backend_name="dummy_backend",
backend_name="fake_backend",
backend_version="0",
n_qubits=int(1e6),
basis_gates=["barrier", "x", "delay", "measure"],
basis_gates=[],
gates=[],
local=True,
simulator=True,
Expand All @@ -50,7 +50,7 @@ def _default_options(cls):

def run(self, run_input, **options):
result = {
"backend_name": "Dummmy backend",
"backend_name": "fake_backend",
"backend_version": "0",
"qobj_id": uuid.uuid4().hex,
"job_id": uuid.uuid4().hex,
Expand Down
226 changes: 226 additions & 0 deletions test/fake_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# This code is part of Qiskit.
#
# (C) Copyright IBM 2021.
#
# 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.

"""Fake service class for tests."""

from typing import Optional, List, Dict, Type, Any, Union, Tuple
import copy
import json

from test.fake_backend import FakeBackend

from qiskit_experiments.database_service import DatabaseServiceV1
from qiskit_experiments.database_service.device_component import DeviceComponent

# pylint:disable=missing-raises-doc


class FakeService(DatabaseServiceV1):
"""
Extremely simple database for testing
"""

def __init__(self):
self.database = {}

def create_experiment(
self,
experiment_type: str,
backend_name: str,
metadata: Optional[Dict] = None,
experiment_id: Optional[str] = None,
parent_id: Optional[str] = None,
job_ids: Optional[List[str]] = None,
tags: Optional[List[str]] = None,
notes: Optional[str] = None,
json_encoder: Type[json.JSONEncoder] = json.JSONEncoder,
**kwargs: Any,
) -> str:
"""Create a new experiment in the database.

Args:
experiment_type: Experiment type.
backend_name: Name of the backend the experiment ran on.
metadata: Experiment metadata.
experiment_id: Experiment ID. It must be in the ``uuid4`` format.
One will be generated if not supplied.
parent_id: The experiment ID of the parent experiment.
The parent experiment must exist, must be on the same backend as the child,
and an experiment cannot be its own parent.
job_ids: IDs of experiment jobs.
tags: Tags to be associated with the experiment.
notes: Freeform notes about the experiment.
json_encoder: Custom JSON encoder to use to encode the experiment.
kwargs: Additional keywords supported by the service provider.

Returns:
Experiment ID.
"""

self.database[experiment_id] = {
"experiment_type": experiment_type,
"experiment_id": experiment_id,
"parent_id": parent_id,
"backend_name": backend_name,
"metadata": metadata,
"job_ids": job_ids,
"tags": tags,
"notes": notes,
"share_level": kwargs.get("share_level", None),
"figure_names": kwargs.get("figure_names", None),
"analysis": {},
}

return experiment_id

def update_experiment(
self,
experiment_id: str,
metadata: Optional[Dict] = None,
job_ids: Optional[List[str]] = None,
notes: Optional[str] = None,
tags: Optional[List[str]] = None,
**kwargs: Any,
) -> None:
"""Update an existing experiment.

Args:
experiment_id: Experiment ID.
metadata: Experiment metadata.
job_ids: IDs of experiment jobs.
notes: Freeform notes about the experiment.
tags: Tags to be associated with the experiment.
kwargs: Additional keywords supported by the service provider.
"""
raise Exception("not implemented")

def experiment(
self, experiment_id: str, json_decoder: Type[json.JSONDecoder] = json.JSONDecoder
) -> Dict:
"""Retrieve a previously stored experiment.

Args:
experiment_id: Experiment ID.
json_decoder: Custom JSON decoder to use to decode the retrieved experiment.

Returns:
A dictionary containing the retrieved experiment data.
"""

db_entry = copy.deepcopy(self.database[experiment_id])
db_entry["backend"] = FakeBackend()
return db_entry

def experiments(
self,
limit: Optional[int] = 10,
json_decoder: Type[json.JSONDecoder] = json.JSONDecoder,
device_components: Optional[Union[str, DeviceComponent]] = None,
experiment_type: Optional[str] = None,
backend_name: Optional[str] = None,
tags: Optional[List[str]] = None,
parent_id: Optional[str] = None,
tags_operator: Optional[str] = "OR",
**filters: Any,
) -> List[Dict]:
raise Exception("not implemented")

def delete_experiment(self, experiment_id: str) -> None:
raise Exception("not implemented")

def create_analysis_result(
self,
experiment_id: str,
result_data: Dict,
result_type: str,
device_components: Optional[Union[str, DeviceComponent]] = None,
tags: Optional[List[str]] = None,
quality: Optional[str] = None,
verified: bool = False,
result_id: Optional[str] = None,
json_encoder: Type[json.JSONEncoder] = json.JSONEncoder,
**kwargs: Any,
) -> str:
self.database[experiment_id]["analysis"][result_id] = {
"result_data": result_data,
"result_id": result_id,
"result_type": result_type,
"device_components": device_components,
"experiment_id": experiment_id,
"quality": quality,
"verified": verified,
"tags": tags,
"service": self,
}

return result_id

def update_analysis_result(
self,
result_id: str,
result_data: Optional[Dict] = None,
tags: Optional[List[str]] = None,
quality: Optional[str] = None,
verified: bool = None,
**kwargs: Any,
) -> None:
raise Exception("not implemented")

def analysis_result(
self, result_id: str, json_decoder: Type[json.JSONDecoder] = json.JSONDecoder
) -> Dict:
raise Exception("not implemented")

def analysis_results(
self,
limit: Optional[int] = 10,
json_decoder: Type[json.JSONDecoder] = json.JSONDecoder,
device_components: Optional[Union[str, DeviceComponent]] = None,
experiment_id: Optional[str] = None,
result_type: Optional[str] = None,
backend_name: Optional[str] = None,
quality: Optional[str] = None,
verified: Optional[bool] = None,
tags: Optional[List[str]] = None,
tags_operator: Optional[str] = "OR",
**filters: Any,
) -> List[Dict]:
return self.database[experiment_id]["analysis"].values()

def delete_analysis_result(self, result_id: str) -> None:
raise Exception("not implemented")

def create_figure(
self, experiment_id: str, figure: Union[str, bytes], figure_name: Optional[str]
) -> Tuple[str, int]:
return

def update_figure(
self, experiment_id: str, figure: Union[str, bytes], figure_name: str
) -> Tuple[str, int]:
raise Exception("not implemented")

def figure(
self, experiment_id: str, figure_name: str, file_name: Optional[str] = None
) -> Union[int, bytes]:
raise Exception("not implemented")

def delete_figure(
self,
experiment_id: str,
figure_name: str,
) -> None:
raise Exception("not implemented")

@property
def preferences(self) -> Dict:
return {"auto_save": False}
4 changes: 2 additions & 2 deletions test/quantum_volume/test_qv.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ def test_qv_circuits_length(self):
self.assertEqual(
len(qv_circs),
trials,
"Number of circuits generated do not match the number of trials",
"Number of circuits generated does not match the number of trials",
)

self.assertEqual(
len(qv_circs[0].qubits),
qv_exp.num_qubits,
"Number of qubits in the Quantum Volume circuit do not match the"
"Number of qubits in the Quantum Volume circuit does not match the"
" number of qubits in the experiment",
)

Expand Down
Loading