示例#1
0
def test_remove(some_paramspecbases):
    ps1, ps2, ps3, ps4 = some_paramspecbases

    idps = InterDependencies_(dependencies={ps1: (ps2, ps3)},
                              inferences={ps2: (ps4, )})
    idps_rem = idps.remove(ps1)
    idps_expected = InterDependencies_(inferences={ps2: (ps4, )},
                                       standalones=(ps3, ))
    assert idps_rem == idps_expected

    for p in [ps4, ps2, ps3]:
        match = re.escape(f'Cannot remove {p.name}, other parameters')
        with pytest.raises(ValueError, match=match):
            idps_rem = idps.remove(p)

    idps = InterDependencies_(dependencies={ps1: (ps3, )},
                              inferences={ps2: (ps4, )})
    idps_rem = idps.remove(ps2)
    idps_expected = InterDependencies_(dependencies={ps1: (ps3, )},
                                       standalones=(ps4, ))

    assert idps_rem == idps_expected

    idps = InterDependencies_(dependencies={ps1: (ps2, ps3)},
                              standalones=(ps4, ))
    idps_rem = idps.remove(ps4)
    idps_expected = InterDependencies_(dependencies={ps1: (ps2, ps3)})
    assert idps_rem == idps_expected

    idps = InterDependencies_(dependencies={ps1: (ps2, ps3)},
                              standalones=(ps4, ))
    idps_rem = idps.remove(ps1)
    idps_expected = InterDependencies_(standalones=(ps2, ps3, ps4))
    assert idps_rem == idps_expected
示例#2
0
class Measurement:
    """
    Measurement procedure container

    Args:
        exp: Specify the experiment to use. If not given
            the default one is used. The default experiment
            is the latest one created.
        station: The QCoDeS station to snapshot. If not given, the
            default one is used.
        name: Name of the experiment. This will be passed down to the dataset
            produced by the measurement. If not given, a default value of
            'results' is used for the dataset.
    """
    def __init__(self,
                 exp: Optional[Experiment] = None,
                 station: Optional[qc.Station] = None,
                 name: str = '') -> None:
        self.exitactions: List[Tuple[Callable, Sequence]] = []
        self.enteractions: List[Tuple[Callable, Sequence]] = []
        self.subscribers: List[Tuple[Callable, Union[MutableSequence,
                                                     MutableMapping]]] = []

        self.experiment = exp
        self.station = station
        self.name = name
        self._write_period: Optional[float] = None
        self._interdeps = InterDependencies_()
        self._parent_datasets: List[Dict] = []

    @property
    def parameters(self) -> Dict[str, ParamSpecBase]:
        return deepcopy(self._interdeps._id_to_paramspec)

    @property
    def write_period(self) -> Optional[float]:
        return self._write_period

    @write_period.setter
    def write_period(self, wp: numeric_types) -> None:
        if not isinstance(wp, Number):
            raise ValueError('The write period must be a number (of seconds).')
        wp_float = float(wp)
        if wp_float < 1e-3:
            raise ValueError('The write period must be at least 1 ms.')
        self._write_period = wp_float

    def _paramspecbase_from_strings(
        self,
        name: str,
        setpoints: Sequence[str] = None,
        basis: Sequence[str] = None
    ) -> Tuple[Tuple[ParamSpecBase, ...], Tuple[ParamSpecBase, ...]]:
        """
        Helper function to look up and get ParamSpecBases and to give a nice
        error message if the user tries to register a parameter with reference
        (setpoints, basis) to a parameter not registered with this measurement

        Called by _register_parameter only.

        Args:
            name: Name of the parameter to register
            setpoints: name(s) of the setpoint parameter(s)
            basis: name(s) of the parameter(s) that this parameter is
                inferred from
        """

        idps = self._interdeps

        # now handle setpoints
        depends_on = []
        if setpoints:
            for sp in setpoints:
                try:
                    sp_psb = idps._id_to_paramspec[sp]
                    depends_on.append(sp_psb)
                except KeyError:
                    raise ValueError(f'Unknown setpoint: {sp}.'
                                     ' Please register that parameter first.')

        # now handle inferred parameters
        inf_from = []
        if basis:
            for inff in basis:
                try:
                    inff_psb = idps._id_to_paramspec[inff]
                    inf_from.append(inff_psb)
                except KeyError:
                    raise ValueError(f'Unknown basis parameter: {inff}.'
                                     ' Please register that parameter first.')

        return tuple(depends_on), tuple(inf_from)

    def register_parent(self: T,
                        parent: DataSet,
                        link_type: str,
                        description: str = "") -> T:
        """
        Register a parent for the outcome of this measurement

        Args:
            parent: The parent dataset
            link_type: A name for the type of parent-child link
            description: A free-text description of the relationship
        """
        # we save the information in a way that is very compatible with the
        # Link object we will eventually make out of this information. We
        # cannot create a Link object just yet, because the DataSet of this
        # Measurement has not been given a GUID yet
        parent_dict = {
            'tail': parent.guid,
            'edge_type': link_type,
            'description': description
        }
        self._parent_datasets.append(parent_dict)

        return self

    def register_parameter(self: T,
                           parameter: _BaseParameter,
                           setpoints: setpoints_type = None,
                           basis: setpoints_type = None,
                           paramtype: Optional[str] = None) -> T:
        """
        Add QCoDeS Parameter to the dataset produced by running this
        measurement.

        Args:
            parameter: The parameter to add
            setpoints: The Parameter representing the setpoints for this
                parameter. If this parameter is a setpoint,
                it should be left blank
            basis: The parameters that this parameter is inferred from. If
                this parameter is not inferred from any other parameters,
                this should be left blank.
            paramtype: Type of the parameter, i.e. the SQL storage class,
                If None the paramtype will be inferred from the parameter type
                and the validator of the supplied parameter.
        """
        if not isinstance(parameter, _BaseParameter):
            raise ValueError('Can not register object of type {}. Can only '
                             'register a QCoDeS Parameter.'
                             ''.format(type(parameter)))

        paramtype = self._infer_paramtype(parameter, paramtype)
        # default to numeric
        if paramtype is None:
            paramtype = 'numeric'

        # now the parameter type must be valid
        if paramtype not in ParamSpec.allowed_types:
            raise RuntimeError("Trying to register a parameter with type "
                               f"{paramtype}. However, only "
                               f"{ParamSpec.allowed_types} are supported.")

        # perhaps users will want a different name? But the name must be unique
        # on a per-run basis
        # we also use the name below, but perhaps is is better to have
        # a more robust Parameter2String function?
        name = str(parameter)

        if isinstance(parameter, ArrayParameter):
            self._register_arrayparameter(parameter, setpoints, basis,
                                          paramtype)
        elif isinstance(parameter, ParameterWithSetpoints):
            self._register_parameter_with_setpoints(parameter, setpoints,
                                                    basis, paramtype)
        elif isinstance(parameter, MultiParameter):
            self._register_multiparameter(
                parameter,
                setpoints,
                basis,
                paramtype,
            )
        elif isinstance(parameter, Parameter):
            self._register_parameter(name, parameter.label, parameter.unit,
                                     setpoints, basis, paramtype)
        else:
            raise RuntimeError("Does not know how to register a parameter"
                               f"of type {type(parameter)}")

        return self

    @staticmethod
    def _infer_paramtype(parameter: _BaseParameter,
                         paramtype: Optional[str]) -> Optional[str]:
        """
        Infer the best parameter type to store the parameter supplied.

        Args:
            parameter: The parameter to to infer the type for
            paramtype: The initial supplied parameter type or None

        Returns:
            The inferred parameter type. If a not None parameter type is
            supplied this will be preferred over any inferred type.
            Returns None if a parameter type could not be inferred
        """
        if paramtype is not None:
            return paramtype

        if isinstance(parameter.vals, vals.Arrays):
            paramtype = 'array'
        elif isinstance(parameter, ArrayParameter):
            paramtype = 'array'
        elif isinstance(parameter.vals, vals.Strings):
            paramtype = 'text'
        elif isinstance(parameter.vals, vals.ComplexNumbers):
            paramtype = 'complex'
        # TODO should we try to figure out if parts of a multiparameter are
        # arrays or something else?
        return paramtype

    def _register_parameter(self: T, name: str, label: Optional[str],
                            unit: Optional[str],
                            setpoints: Optional[setpoints_type],
                            basis: Optional[setpoints_type],
                            paramtype: str) -> T:
        """
        Update the interdependencies object with a new group
        """

        parameter: Optional[ParamSpecBase]

        try:
            parameter = self._interdeps[name]
        except KeyError:
            parameter = None

        paramspec = ParamSpecBase(name=name,
                                  paramtype=paramtype,
                                  label=label,
                                  unit=unit)

        # We want to allow the registration of the exact same parameter twice,
        # the reason being that e.g. two ArrayParameters could share the same
        # setpoint parameter, which would then be registered along with each
        # dependent (array)parameter

        if parameter is not None and parameter != paramspec:
            raise ValueError("Parameter already registered "
                             "in this Measurement.")

        if setpoints is not None:
            sp_strings = [str(sp) for sp in setpoints]
        else:
            sp_strings = []

        if basis is not None:
            bs_strings = [str(bs) for bs in basis]
        else:
            bs_strings = []

        # get the ParamSpecBases
        depends_on, inf_from = self._paramspecbase_from_strings(
            name, sp_strings, bs_strings)

        if depends_on:
            self._interdeps = self._interdeps.extend(
                dependencies={paramspec: depends_on})
        if inf_from:
            self._interdeps = self._interdeps.extend(
                inferences={paramspec: inf_from})
        if not (depends_on or inf_from):
            self._interdeps = self._interdeps.extend(standalones=(paramspec, ))

        log.info(f'Registered {name} in the Measurement.')

        return self

    def _register_arrayparameter(
        self,
        parameter: ArrayParameter,
        setpoints: Optional[setpoints_type],
        basis: Optional[setpoints_type],
        paramtype: str,
    ) -> None:
        """
        Register an ArrayParameter and the setpoints belonging to that
        ArrayParameter
        """
        name = str(parameter)
        my_setpoints = list(setpoints) if setpoints else []
        for i in range(len(parameter.shape)):
            if parameter.setpoint_full_names is not None and \
                    parameter.setpoint_full_names[i] is not None:
                spname = parameter.setpoint_full_names[i]
            else:
                spname = f'{name}_setpoint_{i}'
            if parameter.setpoint_labels:
                splabel = parameter.setpoint_labels[i]
            else:
                splabel = ''
            if parameter.setpoint_units:
                spunit = parameter.setpoint_units[i]
            else:
                spunit = ''

            self._register_parameter(name=spname,
                                     paramtype=paramtype,
                                     label=splabel,
                                     unit=spunit,
                                     setpoints=None,
                                     basis=None)

            my_setpoints += [spname]

        self._register_parameter(name, parameter.label, parameter.unit,
                                 my_setpoints, basis, paramtype)

    def _register_parameter_with_setpoints(self,
                                           parameter: ParameterWithSetpoints,
                                           setpoints: Optional[setpoints_type],
                                           basis: Optional[setpoints_type],
                                           paramtype: str) -> None:
        """
        Register an ParameterWithSetpoints and the setpoints belonging to the
        Parameter
        """
        name = str(parameter)
        my_setpoints = list(setpoints) if setpoints else []
        for sp in parameter.setpoints:
            if not isinstance(sp, Parameter):
                raise RuntimeError("The setpoints of a "
                                   "ParameterWithSetpoints "
                                   "must be a Parameter")
            spname = sp.full_name
            splabel = sp.label
            spunit = sp.unit

            self._register_parameter(name=spname,
                                     paramtype=paramtype,
                                     label=splabel,
                                     unit=spunit,
                                     setpoints=None,
                                     basis=None)

            my_setpoints.append(spname)

        self._register_parameter(name, parameter.label, parameter.unit,
                                 my_setpoints, basis, paramtype)

    def _register_multiparameter(self, multiparameter: MultiParameter,
                                 setpoints: Optional[setpoints_type],
                                 basis: Optional[setpoints_type],
                                 paramtype: str) -> None:
        """
        Find the individual multiparameter components and their setpoints
        and register those as individual parameters
        """
        setpoints_lists = []
        for i in range(len(multiparameter.shapes)):
            shape = multiparameter.shapes[i]
            name = multiparameter.full_names[i]
            if shape == ():
                my_setpoints = setpoints
            else:
                my_setpoints = list(setpoints) if setpoints else []
                for j in range(len(shape)):
                    if multiparameter.setpoint_full_names is not None and \
                            multiparameter.setpoint_full_names[i] is not None:
                        spname = multiparameter.setpoint_full_names[i][j]
                    else:
                        spname = f'{name}_setpoint_{j}'
                    if multiparameter.setpoint_labels is not None and \
                            multiparameter.setpoint_labels[i] is not None:
                        splabel = multiparameter.setpoint_labels[i][j]
                    else:
                        splabel = ''
                    if multiparameter.setpoint_units is not None and \
                            multiparameter.setpoint_units[i] is not None:
                        spunit = multiparameter.setpoint_units[i][j]
                    else:
                        spunit = ''

                    self._register_parameter(name=spname,
                                             paramtype=paramtype,
                                             label=splabel,
                                             unit=spunit,
                                             setpoints=None,
                                             basis=None)

                    my_setpoints += [spname]

            setpoints_lists.append(my_setpoints)

        for i, setpoints in enumerate(setpoints_lists):
            self._register_parameter(multiparameter.names[i],
                                     multiparameter.labels[i],
                                     multiparameter.units[i], setpoints, basis,
                                     paramtype)

    def register_custom_parameter(self: T,
                                  name: str,
                                  label: str = None,
                                  unit: str = None,
                                  basis: setpoints_type = None,
                                  setpoints: setpoints_type = None,
                                  paramtype: str = 'numeric') -> T:
        """
        Register a custom parameter with this measurement

        Args:
            name: The name that this parameter will have in the dataset. Must
                be unique (will overwrite an existing parameter with the same
                name!)
            label: The label
            unit: The unit
            basis: A list of either QCoDeS Parameters or the names
                of parameters already registered in the measurement that
                this parameter is inferred from
            setpoints: A list of either QCoDeS Parameters or the names of
                of parameters already registered in the measurement that
                are the setpoints of this parameter
            paramtype: Type of the parameter, i.e. the SQL storage class
        """
        return self._register_parameter(name, label, unit, setpoints, basis,
                                        paramtype)

    def unregister_parameter(self, parameter: setpoints_type) -> None:
        """
        Remove a custom/QCoDeS parameter from the dataset produced by
        running this measurement
        """
        if isinstance(parameter, _BaseParameter):
            param = str(parameter)
        elif isinstance(parameter, str):
            param = parameter
        else:
            raise ValueError('Wrong input type. Must be a QCoDeS parameter or'
                             ' the name (a string) of a parameter.')

        try:
            paramspec: ParamSpecBase = self._interdeps[param]
        except KeyError:
            return

        self._interdeps = self._interdeps.remove(paramspec)

        log.info(f'Removed {param} from Measurement.')

    def add_before_run(self: T, func: Callable, args: tuple) -> T:
        """
        Add an action to be performed before the measurement.

        Args:
            func: Function to be performed
            args: The arguments to said function
        """
        # some tentative cheap checking
        nargs = len(signature(func).parameters)
        if len(args) != nargs:
            raise ValueError('Mismatch between function call signature and '
                             'the provided arguments.')

        self.enteractions.append((func, args))

        return self

    def add_after_run(self: T, func: Callable, args: tuple) -> T:
        """
        Add an action to be performed after the measurement.

        Args:
            func: Function to be performed
            args: The arguments to said function
        """
        # some tentative cheap checking
        nargs = len(signature(func).parameters)
        if len(args) != nargs:
            raise ValueError('Mismatch between function call signature and '
                             'the provided arguments.')

        self.exitactions.append((func, args))

        return self

    def add_subscriber(self: T, func: Callable,
                       state: Union[MutableSequence, MutableMapping]) -> T:
        """
        Add a subscriber to the dataset of the measurement.

        Args:
            func: A function taking three positional arguments: a list of
                tuples of parameter values, an integer, a mutable variable
                (list or dict) to hold state/writes updates to.
            state: The variable to hold the state.
        """
        self.subscribers.append((func, state))

        return self

    def run(self) -> Runner:
        """
        Returns the context manager for the experimental run
        """
        return Runner(self.enteractions,
                      self.exitactions,
                      self.experiment,
                      station=self.station,
                      write_period=self._write_period,
                      interdeps=self._interdeps,
                      name=self.name,
                      subscribers=self.subscribers,
                      parent_datasets=self._parent_datasets)