Source code for quantify_scheduler.instrument_coordinator.utility
# Repository: https://gitlab.com/quantify-os/quantify-scheduler
# Licensed according to the LICENCE file on the main branch
"""Utility functions for the instrument coordinator and components."""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
import numpy as np
import xarray
from qcodes.parameters.parameter import Parameter
if TYPE_CHECKING:
    from qcodes.instrument.base import InstrumentBase
[docs]
logger = logging.getLogger(__name__) 
[docs]
def search_settable_param(instrument: InstrumentBase, nested_parameter_name: str) -> Parameter:
    """
    Searches for a settable parameter in nested instrument hierarchies.
    For example `instrument.submodule_1.channel_1.parameter.`
    Parameters
    ----------
    instrument:
        The root QCoDeS instrument where the parameter resides.
    nested_parameter_name:
        Hierarchical nested parameter name.
    Returns
    -------
    Parameter:
    """
    root_param = instrument
    split_params = nested_parameter_name.split(".")
    def _search_next_level(
        child_parameter_name: str | Parameter, root_attr_dicts_list: list
    ) -> Parameter | None:
        if callable(child_parameter_name):
            return child_parameter_name
        for root_attr_dict in root_attr_dicts_list:
            if child_parameter_name in root_attr_dict:
                return root_attr_dict.get(child_parameter_name)
        return
    # Search for the parameter within the parameter, function
    # or submodule delegate_attrs_dict of the instrument
    for child_parameter_name in split_params:
        # On the types: _search_next_level returns either None or an object that has the
        # parameters below. Types are omitted because of their complexity.
        root_attr_dicts_list = [
            root_param.parameters,  # type: ignore
            root_param.submodules,  # type: ignore
            root_param.functions,  # type: ignore
        ]
        root_param = _search_next_level(child_parameter_name, root_attr_dicts_list)
        if root_param is None:
            break
    if not (isinstance(root_param, Parameter) or callable(root_param)):
        raise ValueError(
            f"Could not find settable parameter "
            f'"{nested_parameter_name}" in instrument "{instrument}"'
        )
    # If the return type is not a Parameter, then we assume it is a structural subtype
    # (duck typing) of a Parameter.
    return root_param  # type: ignore 
[docs]
def parameter_value_same_as_cache(
    instrument: InstrumentBase, parameter_name: str, val: object
) -> bool:
    """
    Returns whether the value of a QCoDeS parameter is the same as the value in cache.
    Parameters
    ----------
    instrument:
        The QCoDeS instrument to set the parameter on.
    parameter_name:
        Name of the parameter to set.
    val:
        Value to set it to.
    Returns
    -------
    bool
    """
    parameter = search_settable_param(instrument=instrument, nested_parameter_name=parameter_name)
    # parameter.cache() throws for non-gettable parameters if the cache is invalid.
    # This order prevents the exception.
    return parameter.cache.valid and parameter.cache() == val 
[docs]
def lazy_set(instrument: InstrumentBase, parameter_name: str, val: object) -> None:
    """
    Set the value of a QCoDeS parameter only if it is different from the value in cache.
    Parameters
    ----------
    instrument:
        The QCoDeS instrument to set the parameter on.
    parameter_name:
        Name of the parameter to set.
    val:
        Value to set it to.
    """
    parameter = search_settable_param(instrument=instrument, nested_parameter_name=parameter_name)
    # parameter.cache() throws for non-gettable parameters if the cache is invalid.
    # This order prevents the exception.
    if not parameter_value_same_as_cache(instrument, parameter_name, val):
        parameter.set(val)
    else:
        logger.info(f"Lazy set skipped setting parameter {instrument.name}.{parameter_name}") 
[docs]
def check_already_existing_acquisition(
    new_dataset: xarray.Dataset, current_dataset: xarray.Dataset
) -> None:
    """
    Verifies non-overlapping data in new_dataset and current_dataset.
    If there is, it will raise an error.
    Parameters
    ----------
    new_dataset
        New dataset.
    current_dataset
        Current dataset.
    """
    conflicting_indices_str = []
    for acq_channel, _data_array in new_dataset.items():
        if acq_channel in current_dataset:
            # The return values are two `DataArray`s with only coordinates
            # which are common in the inputs.
            common_0, common_1 = xarray.align(
                new_dataset[acq_channel], current_dataset[acq_channel], join="inner"
            )
            # We need to check if the values are `math.nan`, because if they are,
            # that means there is no value at that position (xarray standard).
            def mask_func(x: float, y: float) -> int:
                return 0 if np.isnan(x) or np.isnan(y) else 1
            conflict_mask = xarray.apply_ufunc(mask_func, common_0, common_1, vectorize=True)
            for conflict in conflict_mask:
                if conflict.values == [1]:
                    conflicting_coords = [("acq_channel", acq_channel)]
                    conflicting_coords += [(dim, conflict[dim].values) for dim in conflict.coords]
                    coords_str = [f"{dim}={coord}" for dim, coord in conflicting_coords]
                    conflicting_indices_str.append("; ".join(coords_str))
    if conflicting_indices_str:
        conflicting_indices_str = "\n".join(conflicting_indices_str)
        raise RuntimeError(
            f"Attempting to gather acquisitions. "
            f"Make sure an acq_channel, acq_index corresponds to not more than one acquisition.\n"
            f"The following indices are defined multiple times.\n"
            f"{conflicting_indices_str}"
        )