Skip to content

Commit

Permalink
feat(Experiment): added remote experiment saving (#112)
Browse files Browse the repository at this point in the history
* feat(Experiment): added remote experiment saving

Created the functions for using `qiboconnection`'s remote saving functionalities:
* Created a public function in experiment, `remote_save_experiment()`, that will send the current experiment to the database and return the id.
* Modified `execute()` method so that, depending on the `ExperimentOptions()` configuration, it sends the experiment to the database after the process is done --with default behaviour being to do so--. It leaves the freshly created id stored in the internal attribute `self._remote_saved_experiment_id` in case it needs to be recovered by the user without the need for querying by date, description or name.

* fix(Results): checked that connection is not none before saving the experiment

* fix(experiment): misspellings in docs

Fixed some misspelling in docstrings. Fixed some misspellings in conftest fixture function names.

* fix(experiment): fixed typo in experiment

Fixed typo in 'description' typing

* test(experiment): added testing for remote saving

added testing for executing an experiment with the remote saving option enabled

* fix(experiment): corrected pylint mistake

checked the variable holding the mock itself instead of the place it should be assigned to.

Co-authored-by: javi <javier.sabariego@qilimanjaro.tech>
  • Loading branch information
JavierSab and JavierSab authored Jan 17, 2023
1 parent ffef2fb commit 1e425ca
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 12 deletions.
3 changes: 3 additions & 0 deletions src/qililab/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

DEFAULT_TIMEOUT = 10 * 1000 # 10 seconds


# TODO: Distribute constants over different classes


Expand Down Expand Up @@ -98,6 +99,8 @@ class EXPERIMENT:
CONNECTION = "connection"
CIRCUITS = "circuits"
PULSE_SCHEDULES = "pulse_schedules"
REMOTE_SAVE = "remote_save"
DESCRIPTION = "description"


class SCHEMA:
Expand Down
36 changes: 28 additions & 8 deletions src/qililab/experiment/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from tqdm.auto import tqdm

from qililab.chip import Node
from qililab.config import logger
from qililab.config import __version__, logger
from qililab.constants import EXPERIMENT, RUNCARD
from qililab.execution import EXECUTION_BUILDER, Execution, ExecutionPreparation
from qililab.platform.platform import Platform
Expand Down Expand Up @@ -40,6 +40,7 @@ class Experiment:
_execution_preparation: ExecutionPreparation = field(init=False)
_schedules: list[PulseSchedule] = field(init=False)
_execution_ready: bool = field(init=False)
_remote_saved_experiment_id: int = field(init=False)

def __post_init__(self):
"""prepares the Experiment class"""
Expand Down Expand Up @@ -134,8 +135,27 @@ def execute(self) -> Results:
with self._execution:
self._execute_all_circuits_or_schedules()

if self.options.remote_save:
self.remote_save_experiment()

return self._results

def remote_save_experiment(self):
"""sends the remote save_experiment request using the provided remote connection"""
if self._remote_api.connection is not None:
logger.debug("Sending experiment and results to remote database.")
self._remote_saved_experiment_id = self._remote_api.connection.save_experiment(
name=self.options.name,
description=self.options.description,
experiment_dict=self.to_dict(),
results_dict=self._results.to_dict(),
device_id=self._remote_api.device_id,
user_id=self._remote_api.connection.user_id,
qililab_version=__version__,
favourite=False,
)
return self._remote_saved_experiment_id

def _execute_all_circuits_or_schedules(self):
"""runs the circuits (or schedules) passed as input times software average"""
try:
Expand Down Expand Up @@ -177,7 +197,7 @@ def _execute_recursive_loops(
"""Loop over all the range values defined in the Loop class and change the parameters of the chosen instruments.
Args:
loop (Loop | None): Loop class containing the the info of a Platform element and one of its parameters and
loop (Loop | None): Loop class containing the info of a Platform element and one of its parameters and
the parameter values to loop over.
results (Results): Results class containing all the execution results.
path (Path): Path where the data is stored.
Expand Down Expand Up @@ -208,7 +228,7 @@ def _process_loops(
Args:
results (Results): Results class containing all the execution results.
loops (List[Loop]): Loop class containing the the info of one or more Platform element and the
loops (List[Loop]): Loop class containing the info of one or more Platform element and the
parameter values to loop over.
depth (int): Depth of the recursive loop.
path (Path): Path where the data is stored.
Expand Down Expand Up @@ -239,7 +259,7 @@ def _update_tqdm_bar(self, loops: List[Loop], values: Tuple[float], pbar):
pbar.update()

def _set_parameter_text_and_value(self, value: float, loop: Loop):
"""set paramater text and value to print on terminal TQDM iterations"""
"""set parameter text and value to print on terminal TQDM iterations"""
parameter_text = (
loop.alias if loop.parameter == Parameter.EXTERNAL and loop.alias is not None else loop.parameter.value
)
Expand All @@ -259,7 +279,7 @@ def _update_parameters_from_loops_filtering_external_parameters(
loops: List[Loop],
):
"""Update parameters from loops filtering those loops that relates to external variables
not associated to neither platform, instrument, or gates settings
not associated to neither platform, instrument, nor gates settings
"""
filtered_loops, filtered_values = self._filter_loops_values_with_external_parameters(
values=values,
Expand All @@ -268,7 +288,7 @@ def _update_parameters_from_loops_filtering_external_parameters(
self._update_parameters_from_loops(values=filtered_values, loops=filtered_loops)

def _filter_loops_values_with_external_parameters(self, values: Tuple[float], loops: List[Loop]):
"""filter loops and values removing those with external paramaters"""
"""filter loops and values removing those with external parameters"""
if len(values) != len(loops):
raise ValueError(f"Values list length: {len(values)} differ from loops list length: {len(loops)}.")
filtered_loops = loops.copy()
Expand All @@ -285,7 +305,7 @@ def _filter_loops_values_with_external_parameters(self, values: Tuple[float], lo
def _filter_loop_value_when_parameters_is_external(
self, filtered_loops: List[Loop], filtered_values: List[float], idx: int, loop: Loop
):
"""filter loop value when parameters is external"""
"""filter loop value when parameters are external"""
if loop.parameter == Parameter.EXTERNAL:
filtered_loops.pop(idx)
filtered_values.pop(idx)
Expand All @@ -296,7 +316,7 @@ def _update_parameters_from_loops(
values: List[float],
loops: List[Loop],
):
"""update paramaters from loops"""
"""update parameters from loops"""
elements = self._get_platform_elements_from_loops(loops=loops)

for value, loop, element in zip(values, loops, elements):
Expand Down
6 changes: 6 additions & 0 deletions src/qililab/typings/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ class ExperimentOptions:
plot_y_label: str | None = None
remote_device_manual_override: bool = field(default=False)
execution_options: ExecutionOptions = ExecutionOptions()
remote_save: bool = True
description: str = ""

def to_dict(self):
"""Convert Experiment into a dictionary.
Expand All @@ -54,6 +56,8 @@ def to_dict(self):
EXPERIMENT.PLOT_Y_LABEL: self.plot_y_label,
EXPERIMENT.REMOTE_DEVICE_MANUAL_OVERRIDE: self.remote_device_manual_override,
EXPERIMENT.EXECUTION_OPTIONS: asdict(self.execution_options),
EXPERIMENT.REMOTE_SAVE: self.remote_save,
EXPERIMENT.DESCRIPTION: self.description,
}

@classmethod
Expand All @@ -79,4 +83,6 @@ def from_dict(cls, dictionary: dict):
execution_options=ExecutionOptions(**dictionary[EXPERIMENT.EXECUTION_OPTIONS])
if EXPERIMENT.EXECUTION_OPTIONS in dictionary
else ExecutionOptions(),
remote_save=dictionary.get(EXPERIMENT.REMOTE_SAVE, True),
description=dictionary.get(EXPERIMENT.DESCRIPTION, ""),
)
9 changes: 5 additions & 4 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,8 @@ def fixture_create_mocked_connection_established(
return ConnectionEstablished(
**asdict(mocked_connection_configuration),
authorisation_access_token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3O"
+ "DkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
+ "DkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Sf"
+ "lKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
api_path="/api/v1",
)

Expand All @@ -707,7 +708,7 @@ def fixture_create_mocked_api_connection(mocked_connection_established: Connecti


@pytest.fixture(name="mocked_remote_api")
def fixtuer_create_mocked_remote_api(mocked_api: API) -> RemoteAPI:
def fixture_create_mocked_remote_api(mocked_api: API) -> RemoteAPI:
"""Create a mocked remote api connection
Returns:
RemoteAPI: Remote API mocked connection
Expand All @@ -716,7 +717,7 @@ def fixtuer_create_mocked_remote_api(mocked_api: API) -> RemoteAPI:


@pytest.fixture(name="valid_remote_api")
def fixtuer_create_valid_remote_api() -> RemoteAPI:
def fixture_create_valid_remote_api() -> RemoteAPI:
"""Create a valid remote api connection
Returns:
RemoteAPI: Remote API connection
Expand All @@ -729,7 +730,7 @@ def fixtuer_create_valid_remote_api() -> RemoteAPI:


@pytest.fixture(name="second_valid_remote_api")
def fixtuer_create_second_valid_remote_api() -> RemoteAPI:
def fixture_create_second_valid_remote_api() -> RemoteAPI:
"""Create a valid remote api connection
Returns:
RemoteAPI: Remote API connection
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/experiment/test_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from qililab.constants import RESULTSDATAFRAME
from qililab.experiment import Experiment
from qililab.remote_connection.remote_api import RemoteAPI
from qililab.result.results import Results

from .aux_methods import mock_instruments
Expand All @@ -25,6 +26,48 @@
class TestExecution:
"""Unit tests checking the execution of a platform with instruments."""

@patch("qililab.remote_connection.remote_api.RemoteAPI.connection")
def test_execute_with_remote_save(
self,
mocked_remote_connection: MagicMock,
mock_open_0: MagicMock,
mock_dump_0: MagicMock,
mock_makedirs: MagicMock,
mock_open_1: MagicMock,
mock_open_2: MagicMock,
mock_dump_1: MagicMock,
mock_rs: MagicMock,
mock_pulsar: MagicMock,
mock_urllib: MagicMock,
mock_keithley: MagicMock,
nested_experiment: Experiment,
):
"""Test execute method with nested loops."""
saved_experiment_id = 0

mocked_remote_connection.save_experiment.return_value = saved_experiment_id
mock_instruments(mock_rs=mock_rs, mock_pulsar=mock_pulsar, mock_keithley=mock_keithley)

nested_experiment.options.settings.software_average = 1
nested_experiment.options.remote_save = True
nested_experiment.options.name = "TEST"
nested_experiment.options.description = "TEST desc"
nested_experiment._remote_api = RemoteAPI(connection=mocked_remote_connection, device_id=0)
nested_experiment.execute() # type: ignore
nested_experiment.to_dict()

mocked_remote_connection.save_experiment.assert_called()
assert nested_experiment._remote_saved_experiment_id == saved_experiment_id

mock_urllib.request.Request.assert_called()
mock_urllib.request.urlopen.assert_called()
mock_dump_0.assert_called()
mock_dump_1.assert_called()
mock_open_0.assert_called()
mock_open_1.assert_called()
mock_open_2.assert_called()
mock_makedirs.assert_called()

def test_execute_method_with_nested_loop(
self,
mock_open_0: MagicMock,
Expand Down

0 comments on commit 1e425ca

Please sign in to comment.