Source code for quantify_core.utilities.experiment_helpers

# Repository: https://gitlab.com/quantify-os/quantify-core
# Licensed according to the LICENCE file on the main branch
"""Helpers for performing experiments."""
from __future__ import annotations
import warnings
from typing import Any, Optional, Union, Dict, List, Literal

import numpy as np
from deepdiff import DeepDiff
from qcodes.instrument import Instrument, InstrumentChannel
from qcodes.parameters import Parameter

from quantify_core.data.handling import get_latest_tuid, load_snapshot
from quantify_core.data.types import TUID
from quantify_core.visualization.pyqt_plotmon import PlotMonitor_pyqt


# pylint: disable=broad-except
[docs] def load_settings_onto_instrument( instrument: Union[Instrument, InstrumentChannel, Parameter], tuid: TUID | None = None, datadir: str = None, exception_handling: Literal["raise", "warn"] = "raise", ) -> None: """ Loads settings from a previous experiment onto a current :class:`~qcodes.instrument.Instrument`, or any of its submodules or parameters. This information is loaded from the 'snapshot.json' file in the provided experiment directory. Parameters ---------- instrument : the :class:`~qcodes.instrument.Instrument`, :class:`~qcodes.instrument.InstrumentChannel` or :class:`~qcodes.parameters.Parameter` to be configured. tuid : :class:`~quantify_core.data.types.TUID` the TUID of the experiment. If None use latest TUID. datadir : str path of the data directory. If `None`, uses `get_datadir()` to determine the data directory. exception_handling: desired behaviour if error occurs when trying to get parameter: raise exception or give warning. Raises ------ ValueError if the provided instrument has no match in the loaded snapshot. """ if tuid is None: tuid = get_latest_tuid() instruments = load_snapshot(tuid, datadir)["instruments"] instruments_numpy_array = load_snapshot(tuid, datadir, list_to_ndarray=True)[ "instruments" ] def _try_to_set_par_safe( instr_mod: Union[Instrument, InstrumentChannel], parname: str, value: Any, value_np: Any, ): """Tries to set a parameter and emits a warning if not successful.""" # do not try to set parameters that are not settable if not "set" in dir(instr_mod.parameters[parname]): return # do not set to None if value is already None. If there is a runtime # error when getting the parameter, do not try to set the parameter. try: get_val = instr_mod.parameters[parname]() except Exception as exc: if exception_handling == "raise": raise exc warnings.warn( f"Could not get value of {parname} parameter due to '{exc}'. " "We will not try to set this parameter." ) return if get_val is None and value is None: return # Make sure the parameter is actually a settable try: if isinstance(get_val, np.ndarray): instr_mod.set(parname, value_np) else: instr_mod.set(parname, value) except (RuntimeError, KeyError, ValueError, TypeError) as exc: warnings.warn( f'Parameter "{parname}" of "{instr_mod.name}" ' f'could not be set to "{value}" due to error:\n{exc}' ) parents = get_all_parents(instrument) # Find the snapshot for this instrument, submodule or parameter for parent in parents: if isinstance(parent, Instrument): if parent.name not in instruments: raise ValueError( f'Instrument "{parent.name}" not found in snapshot {datadir}:{tuid}' ) instr_mod_snap = instruments[parent.name] instr_mod_snap_numpy_array = instruments_numpy_array[parent.name] if isinstance(parent, InstrumentChannel): if parent._short_name not in instr_mod_snap["submodules"]: raise ValueError( f'Submodule "{parent.name}" not found in snapshot {datadir}:{tuid}' ) instr_mod_snap = instr_mod_snap["submodules"][parent._short_name] instr_mod_snap_numpy_array = instr_mod_snap_numpy_array["submodules"][ parent._short_name ] # If we are only setting one parameter, try to set this immediately if isinstance(parent, Parameter): if parent.name not in instr_mod_snap["parameters"]: address = ".".join([p._short_name for p in parents]) raise ValueError( f'Parameter "{address}" not found in snapshot {datadir}:{tuid}' ) value = instr_mod_snap["parameters"][parent.name]["value"] value_np = instr_mod_snap_numpy_array["parameters"][parent.name]["value"] _try_to_set_par_safe(parents[-2], parent.name, value, value_np) return def _set_params_instr_mod( instr_mod_snap: Dict, instr_mod_snap_np: Dict, instr_mod: Union[Instrument, InstrumentChannel], ): """ private function to set parameters and recursively set parameters of submodules. """ # iterate over top-level parameters for parname, par in instr_mod_snap["parameters"].items(): # Check that the parameter exists in this instrument if parname in instr_mod.parameters: value = par["value"] value_np = instr_mod_snap_np["parameters"][parname]["value"] _try_to_set_par_safe(instr_mod, parname, value, value_np) else: warnings.warn( f"Could not set parameter {parname} in {instr_mod.name}. " f"{instr_mod.name} does not possess a parameter named {parname}." ) # recursively call this function for all submodules if "submodules" in instr_mod_snap.keys(): for module_name, module_snap in instr_mod_snap["submodules"].items(): submodule = instr_mod.submodules[module_name] module_snap_np = instr_mod_snap_np["submodules"][module_name] _set_params_instr_mod( instr_mod_snap=module_snap, instr_mod_snap_np=module_snap_np, instr_mod=submodule, ) # set the top-level parameters and then recursively set parameters of submodules _set_params_instr_mod( instr_mod_snap=instr_mod_snap, instr_mod_snap_np=instr_mod_snap_numpy_array, instr_mod=instrument, )
[docs] def compare_snapshots(old_snapshot: dict, new_snapshot: dict) -> dict: """ Generate a diff between two quantify snapshots, showing which qcodes parameters have changed. This function only considers changes in numerical values of quantities and ignores type changes, such as :code:`numpy.float64` to :code:`float`, for example. We also only consider the values of qcodes parameters, and not metadata like timestamps, which always change from snapshot to snapshot. Parameters ---------- old_snapshot: The original snapshot to be compared new_snapshot: The new snapshot to be compared Return ------ : A dictionary summarising the differences between the two snapshots """ return DeepDiff( old_snapshot, new_snapshot, exclude_regex_paths=[r"\['ts'\]", r"\['raw_value'\]", r"\['IDN'\]"], ignore_numeric_type_changes=True, )
[docs] def get_all_parents(instr_mod: Union[Instrument, InstrumentChannel, Parameter]) -> List: """ Get a list of all the parent submodules and instruments of a given QCodes instrument, submodule or parameter. Parameters ----------- instr_mod: The QCodes instrument, submodule or parameter whose parents we wish to find Returns ------- : A list of all the parents of that object (and the object itself) """ if hasattr(instr_mod, "_parent"): parents = get_all_parents(instr_mod._parent) parents.append(instr_mod) elif hasattr(instr_mod, "_instrument"): parents = get_all_parents(instr_mod._instrument) parents.append(instr_mod) else: parents = [instr_mod] return parents
[docs] def create_plotmon_from_historical( tuid: Optional[TUID] = None, label: str = "" ) -> PlotMonitor_pyqt: """ Creates a plotmon using the dataset of the provided experiment denoted by the tuid in the datadir. Loads the data and draws any required figures. NB Creating a new plotmon can be slow. Consider using :func:`!PlotMonitor_pyqt.tuids_extra` to visualize dataset in the same plotmon. Parameters ---------- tuid the TUID of the experiment. label if the `tuid` is not provided, as label will be used to search for the latest dataset. Returns ------- : the plotmon """ # avoid creating a plotmon with the same name if tuid is None: tuid = get_latest_tuid(contains=label) tuid_str = str(tuid) # python attributes can't start with a number name = "_" + tuid_str # hyphens not allowed in instrument names for qcodes v0.34 and up name = name.replace("-", "_") plotmon = PlotMonitor_pyqt(name) plotmon.tuids_append(tuid) plotmon.update() # make sure everything is drawn return plotmon