diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000000..538fce22ee2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/qcodes/instrument/base.py b/qcodes/instrument/base.py index 12a72d2b395..e41cddb3408 100644 --- a/qcodes/instrument/base.py +++ b/qcodes/instrument/base.py @@ -10,8 +10,8 @@ from qcodes.utils.helpers import DelegateAttributes, strip_attrs, full_class from qcodes.utils.metadata import Metadatable from qcodes.utils.validators import Anything +from .parameter import Parameter from .function import Function -from .parameter import StandardParameter class InstrumentBase(Metadatable, DelegateAttributes): @@ -71,8 +71,7 @@ def get_mock_messages(self): raise ValueError("Cannot get mock messages if not in testing mode") return self.mocker.get_log_messages() - def add_parameter(self, name, parameter_class=StandardParameter, - **kwargs): + def add_parameter(self, name, parameter_class=Parameter, **kwargs): """ Bind one Parameter to this instrument. @@ -323,7 +322,7 @@ def validate_status(self, verbose=False): """ for k, p in self.parameters.items(): - if p.has_get and p.has_set: + if hasattr(p, 'get') and hasattr(p, 'set'): value = p.get() if verbose: print('validate_status: param %s: %s' % (k, value)) diff --git a/qcodes/instrument/parameter.py b/qcodes/instrument/parameter.py index 3b4cefb92d8..8d4fe186a05 100644 --- a/qcodes/instrument/parameter.py +++ b/qcodes/instrument/parameter.py @@ -6,19 +6,23 @@ by either using or subclassing one of the classes defined here, but you can also use any class with the right attributes. -TODO (alexcjohnson) update this with the real duck-typing requirements or -create an ABC for Parameter and MultiParameter - or just remove this statement -if everyone is happy to use these classes. +All parameter classes are subclassed from _BaseParameter (except +CombinedParameter). The _BaseParameter provides functionality that is common +to all parameter types, such as ramping and scaling of values, adding delays +(see documentation for details). This file defines four classes of parameters: -``Parameter``, ``ArrayParameter``, and ``MultiParameter`` must be subclassed: - -- ``Parameter`` is the base class for scalar-valued parameters, if you have - custom code to read or write a single value. Provides ``sweep`` and - ``__getitem__`` (slice notation) methods to use a settable parameter as - the swept variable in a ``Loop``. To use, fill in ``super().__init__``, - and provide a ``get`` method, a ``set`` method, or both. +- ``Parameter`` is the base class for scalar-valued parameters. + Two primary ways in which it can be used: + 1. As an ``Instrument`` parameter that sends/receives commands. Provides a + standardized interface to construct strings to pass to the + instrument's ``write`` and ``ask`` methods + 2. As a variable that stores and returns a value. For instance, for storing + of values you want to keep track of but cannot set or get electronically. + Provides ``sweep`` and ``__getitem__`` (slice notation) methods to use a + settable parameter as the swept variable in a ``Loop``. + The get/set functionality can be modified. - ``ArrayParameter`` is a base class for array-valued parameters, ie anything for which each ``get`` call returns an array of values that all have the @@ -35,22 +39,17 @@ that returns a sequence of values, and describe those values in ``super().__init__``. -``StandardParameter`` and ``ManualParameter`` can be instantiated directly: + ``CombinedParameter`` Combines several parameters into a ``MultiParameter``. + can be easily used via the ``combine`` function. + Note that it is not yet a subclass of BaseParameter. -- ``StandardParameter`` is the default class for instrument parameters - (see ``Instrument.add_parameter``). Can be gettable, settable, or both. - Provides a standardized interface to construct strings to pass to the - instrument's ``write`` and ``ask`` methods (but can also be given other - functions to execute on ``get`` or ``set``), to convert the string - responses to meaningful output, and optionally to ramp a setpoint with - stepped ``write`` calls from a single ``set``. Does not need to be - subclassed, just instantiated. -- ``ManualParameter`` is for values you want to keep track of but cannot - set or get electronically. Holds the last value it was ``set`` to, and - returns it on ``get``. """ +# TODO (alexcjohnson) update this with the real duck-typing requirements or +# create an ABC for Parameter and MultiParameter - or just remove this statement +# if everyone is happy to use these classes. + from datetime import datetime, timedelta from copy import copy import time @@ -58,26 +57,26 @@ import os import collections import warnings - +from functools import partial, wraps import numpy from qcodes.utils.deferred_operations import DeferredOperations -from qcodes.utils.helpers import (permissive_range, wait_secs, is_sequence, - is_sequence_of, DelegateAttributes, - full_class, named_repr, warn_units) +from qcodes.utils.helpers import (permissive_range, is_sequence_of, + DelegateAttributes, full_class, named_repr, + warn_units) from qcodes.utils.metadata import Metadatable -from qcodes.utils.command import Command, NoCommandError -from qcodes.utils.validators import Validator, Numbers, Ints, Enum, Strings +from qcodes.utils.command import Command +from qcodes.utils.validators import Validator, Ints, Strings, Enum from qcodes.instrument.sweep_values import SweepFixedValues from qcodes.data.data_array import DataArray class _BaseParameter(Metadatable, DeferredOperations): """ - Shared behavior for simple and multi parameters. Not intended to be used - directly, normally you should use ``StandardParameter`` or - ``ManualParameter``, or create your own subclass of ``Parameter`` or - ``MultiParameter``. + Shared behavior for all parameters. Not intended to be used + directly, normally you should use ``Parameter``, ``ArrayParameter``, + ``MultiParameter``, or ``CombinedParameter``. + Note that ``CombinedParameter`` is not yet a subclass of ``_BaseParameter`` Args: name (str): the local name of the parameter. Should be a valid @@ -94,84 +93,133 @@ class _BaseParameter(Metadatable, DeferredOperations): ``update=True``, for example if it takes too long to update. Default True. + snapshot_value (Optional[bool]): False prevents parameter value to be + stored in the snapshot. Useful if the value is large. + + step (Optional[Union[int, float]]): max increment of parameter value. + Larger changes are broken into multiple steps this size. + When combined with delays, this acts as a ramp. + + scale (Optional[float]): Scale to multiply value with before + performing set. the internally multiplied value is stored in + `raw_value`. Can account for a voltage divider. + + inter_delay (Optional[Union[int, float]]): Minimum time (in seconds) + between successive sets. If the previous set was less than this, + it will wait until the condition is met. + Can be set to 0 to go maximum speed with no errors. + + post_delay (Optional[Union[int, float]]): time (in seconds) to wait + after the *start* of each set, whether part of a sweep or not. + Can be set to 0 to go maximum speed with no errors. + + val_mapping (Optional[dict]): a bidirectional map data/readable values + to instrument codes, expressed as a dict: + ``{data_val: instrument_code}`` + For example, if the instrument uses '0' to mean 1V and '1' to mean + 10V, set val_mapping={1: '0', 10: '1'} and on the user side you + only see 1 and 10, never the coded '0' and '1' + If vals is omitted, will also construct a matching Enum validator. + NOTE: only applies to get if get_cmd is a string, and to set if + set_cmd is a string. + You can use ``val_mapping`` with ``get_parser``, in which case + ``get_parser`` acts on the return value from the instrument first, + then ``val_mapping`` is applied (in reverse). + + get_parser ( Optional[function]): function to transform the response + from get to the final output value. See also val_mapping + + set_parser (Optional[function]): function to transform the input set + value to an encoded value sent to the instrument. + See also val_mapping. + + vals (Optional[Validator]): a Validator object for this parameter + + max_val_age (Optional[float]): The max time (in seconds) to trust a + saved value obtained from get_latest(). If this parameter has not + been set or measured more recently than this, perform an + additional measurement. + metadata (Optional[dict]): extra information to include with the JSON snapshot of the parameter """ - def __init__(self, name, instrument, snapshot_get, metadata, - snapshot_value=True): + def __init__(self, name, instrument, snapshot_get=True, metadata=None, + step=None, scale=None, inter_delay=0, post_delay=0, + val_mapping=None, get_parser=None, set_parser=None, + snapshot_value=True, max_val_age=None, vals=None): super().__init__(metadata) - self._snapshot_get = snapshot_get self.name = str(name) self._instrument = instrument + self._snapshot_get = snapshot_get self._snapshot_value = snapshot_value - self.has_get = hasattr(self, 'get') - self.has_set = hasattr(self, 'set') + if not isinstance(vals, (Validator, type(None))): + raise TypeError('vals must be None or a Validator') + elif val_mapping is not None: + vals = Enum(*val_mapping.keys()) + self.vals = vals + + self.step = step + self.scale = scale + self.raw_value = None + self.inter_delay = inter_delay + self.post_delay = post_delay + + self.val_mapping = val_mapping + if val_mapping is None: + self.inverse_val_mapping = None + else: + self.inverse_val_mapping = {v: k for k, v in val_mapping.items()} - if not (self.has_get or self.has_set): - raise AttributeError('A parameter must have either a get or a ' - 'set method, or both.') + self.get_parser = get_parser + self.set_parser = set_parser # record of latest value and when it was set or measured # what exactly this means is different for different subclasses # but they all use the same attributes so snapshot is consistent. - self._latest_value = None - self._latest_ts = None - self.get_latest = GetLatest(self) + self._latest = {'value': None, 'ts': None} + self.get_latest = GetLatest(self, max_val_age=max_val_age) + + if hasattr(self, 'get'): + self.get = self._wrap_get(self.get) + if hasattr(self, 'set'): + self.set = self._wrap_set(self.set) # subclasses should extend this list with extra attributes they # want automatically included in the snapshot - self._meta_attrs = ['name', 'instrument'] + self._meta_attrs = ['name', 'instrument', 'step', 'scale', 'raw_value', + 'inter_delay', 'post_delay', 'val_mapping', 'vals'] + + # Specify time of last set operation, used when comparing to delay to + # check if additional waiting time is needed before next set + self._t_last_set = time.perf_counter() + + def __str__(self): + """Include the instrument name with the Parameter name if possible.""" + inst_name = getattr(self._instrument, 'name', '') + if inst_name: + return '{}_{}'.format(inst_name, self.name) + else: + return self.name def __repr__(self): return named_repr(self) - def __call__(self, *args): + def __call__(self, *args, **kwargs): if len(args) == 0: - if self.has_get: + if hasattr(self, 'get'): return self.get() else: raise NotImplementedError('no get cmd found in' + ' Parameter {}'.format(self.name)) else: - if self.has_set: - self.set(*args) + if hasattr(self, 'set'): + self.set(*args, **kwargs) else: raise NotImplementedError('no set cmd found in' + ' Parameter {}'.format(self.name)) - def _latest(self): - return { - 'value': self._latest_value, - 'ts': self._latest_ts - } - - # get_attrs ignores leading underscores, unless they're in this list - _keep_attrs = ['__doc__', '_vals'] - - def get_attrs(self): - """ - Attributes recreated as properties in the RemoteParameter proxy. - - Grab the names of all attributes that the RemoteParameter needs - to function like the main one (in loops etc) - - Returns: - list: All public attribute names, plus docstring and _vals - """ - out = [] - - for attr in dir(self): - # while we're keeping units as a deprecated attribute in some - # classes, avoid calling it here so we don't get spurious errors - if ((attr[0] == '_' and attr not in self._keep_attrs) or - (attr != 'units' and callable(getattr(self, attr)))): - continue - out.append(attr) - - return out - def snapshot_base(self, update=False): """ State of the parameter as a JSON-compatible dict. @@ -185,12 +233,13 @@ def snapshot_base(self, update=False): dict: base snapshot """ - if self.has_get and self._snapshot_get and self._snapshot_value and \ - update: + if hasattr(self, 'get') and self._snapshot_get \ + and self._snapshot_value and update: self.get() - state = self._latest() + state = copy(self._latest) state['__class__'] = full_class(self) + state['full_name'] = str(self) if not self._snapshot_value: state.pop('value') @@ -204,41 +253,303 @@ def snapshot_base(self, update=False): 'instrument': full_class(self._instrument), 'instrument_name': self._instrument.name }) - - elif hasattr(self, attr): - val = getattr(self, attr) - attr_strip = attr.lstrip('_') # eg _vals - do not include _ - if isinstance(val, Validator): - state[attr_strip] = repr(val) - else: - state[attr_strip] = val + else: + val = getattr(self, attr, None) + if val is not None: + attr_strip = attr.lstrip('_') # strip leading underscores + if isinstance(val, Validator): + state[attr_strip] = repr(val) + else: + state[attr_strip] = val return state - def _save_val(self, value): - self._latest_value = value - self._latest_ts = datetime.now() + def _save_val(self, value, validate=False): + if validate: + self.validate(value) + self._latest = {'value': value, 'ts': datetime.now()} + + def _wrap_get(self, get_function): + @wraps(get_function) + def get_wrapper(*args, **kwargs): + try: + # There might be cases where a .get also has args/kwargs + value = get_function(*args, **kwargs) + self.raw_value = value + + if self.get_parser is not None: + value = self.get_parser(value) + + if self.scale is not None: + # Scale values + if isinstance(self.scale, collections.Iterable): + # Scale contains multiple elements, one for each value + value = tuple(value / scale for value, scale + in zip(value, self.scale)) + elif isinstance(value, collections.Iterable): + # Use single scale for all values + value = tuple(value / self.scale for value in value) + else: + value /= self.scale + + if self.val_mapping is not None: + if value in self.inverse_val_mapping: + value = self.inverse_val_mapping[value] + elif int(value) in self.inverse_val_mapping: + value = self.inverse_val_mapping[int(value)] + else: + raise KeyError("'{}' not in val_mapping".format(value)) + + self._save_val(value) + return value + except Exception as e: + e.args = e.args + ('getting {}'.format(self),) + raise e + + return get_wrapper + + def _wrap_set(self, set_function): + @wraps(set_function) + def set_wrapper(value, **kwargs): + try: + self.validate(value) + + if self.val_mapping is not None: + # Convert set values using val_mapping dictionary + value = self.val_mapping[value] + + if self.scale is not None: + if isinstance(self.scale, collections.Iterable): + # Scale contains multiple elements, one for each value + value = tuple(val * scale for val, scale + in zip(value, self.scale)) + else: + # Use single scale for all values + value *= self.scale + + if self.set_parser is not None: + value = self.set_parser(value) + + # In some cases intermediate sweep values must be used. + # Unless `self.step` is defined, get_sweep_values will return + # a list containing only `value`. + for val in self.get_ramp_values(value, step=self.step): + + # Check if delay between set operations is required + t_elapsed = time.perf_counter() - self._t_last_set + if t_elapsed < self.inter_delay: + # Sleep until time since last set is larger than + # self.post_delay + time.sleep(self.inter_delay - t_elapsed) + + # Start timer to measure execution time of set_function + t0 = time.perf_counter() + + set_function(val, **kwargs) + self.raw_value = val + self._save_val(val, validate=(self.val_mapping is None and + self.set_parser is None)) + + # Update last set time (used for calculating delays) + self._t_last_set = time.perf_counter() + + # Check if any delay after setting is required + t_elapsed = self._t_last_set - t0 + if t_elapsed < self.post_delay: + # Sleep until total time is larger than self.post_delay + time.sleep(self.post_delay - t_elapsed) + except Exception as e: + e.args = e.args + ('setting {} to {}'.format(self, value),) + raise e + + return set_wrapper + + def get_ramp_values(self, value, step=None): + """ + Return values to sweep from current value to target value. + This method can be overridden to have a custom sweep behaviour. + It can even be overridden by a generator. + Args: + value: target value + step: maximum step size + + Returns: + List of stepped values, including target value. + """ + if step is None: + return [value] + else: + start_value = self.get_latest() + + self.validate(start_value) + + if not (isinstance(start_value, (int, float)) and + isinstance(value, (int, float))): + # something weird... parameter is numeric but one of the ends + # isn't, even though it's valid. + # probably MultiType with a mix of numeric and non-numeric types + # just set the endpoint and move on + logging.warning( + 'cannot sweep {} from {} to {} - jumping.'.format( + self.name, start_value, value)) + return [] + + # drop the initial value, we're already there + return permissive_range(start_value, value, step)[1:] + [value] + + def validate(self, value): + """ + Validate value + + Args: + value (any): value to validate + + """ + if self._instrument: + context = (getattr(self._instrument, 'name', '') or + str(self._instrument.__class__)) + '.' + self.name + else: + context = self.name + if self.vals is not None: + self.vals.validate(value, 'Parameter: ' + context) + + @property + def step(self): + return self._step + + @step.setter + def step(self, step): + """ + Configure whether this Parameter uses steps during set operations. + If step is a positive number, this is the maximum value change + allowed in one hardware call, so a single set can result in many + calls to the hardware if the starting value is far from the target. + + Args: + step (Union[int, float]): A positive number, the largest change + allowed in one call. All but the final change will attempt to + change by +/- step exactly + + Raises: + TypeError: if step is not numeric + ValueError: if step is negative + TypeError: if step is not integer for an integer parameter + TypeError: if step is not a number + """ + if step is None: + self._step = step + elif not getattr(self.vals, 'is_numeric', True): + raise TypeError('you can only step numeric parameters') + elif not isinstance(step, (int, float)): + raise TypeError('step must be a number') + elif step <= 0: + raise ValueError('step must be positive') + elif isinstance(self.vals, Ints) and not isinstance(step, int): + raise TypeError('step must be a positive int for an Ints parameter') + else: + self._step = step + + @property + def post_delay(self): + """Property that returns the delay time of this parameter""" + return self._post_delay + + @post_delay.setter + def post_delay(self, post_delay): + """ + Configure this parameter with a delay between set operations. + + Typically used in conjunction with set_step to create an effective + ramp rate, but can also be used without a step to enforce a delay + after every set. + + Args: + post_delay(Union[int, float]): the target time between set calls. + The actual time will not be shorter than this, but may be longer + if the underlying set call takes longer. + + Raises: + TypeError: If delay is not int nor float + ValueError: If delay is negative + """ + if not isinstance(post_delay, (int, float)): + raise TypeError( + 'post_delay ({}) must be a number'.format(post_delay)) + if post_delay < 0: + raise ValueError( + 'post_delay ({}) must not be negative'.format(post_delay)) + self._post_delay = post_delay + + @property + def inter_delay(self): + """Property that returns the delay time of this parameter""" + return self._inter_delay + + @inter_delay.setter + def inter_delay(self, inter_delay): + """ + Configure this parameter with a delay between set operations. + + Typically used in conjunction with set_step to create an effective + ramp rate, but can also be used without a step to enforce a delay + between sets. + Args: + inter_delay(Union[int, float]): the target time between set calls. + The actual time will not be shorter than this, but may be longer + if the underlying set call takes longer. + + Raises: + TypeError: If delay is not int nor float + ValueError: If delay is negative + """ + if not isinstance(inter_delay, (int, float)): + raise TypeError( + 'inter_delay ({}) must be a number'.format(inter_delay)) + if inter_delay < 0: + raise ValueError( + 'inter_delay ({}) must not be negative'.format(inter_delay)) + self._inter_delay = inter_delay + + # Deprecated @property def full_name(self): - """Include the instrument name with the Parameter name if possible.""" - try: - inst_name = self._instrument.name - if inst_name: - return inst_name + '_' + self.name - except AttributeError: - pass + warnings.warn('Attribute `full_name` is deprecated, please use ' + 'str(parameter)') + return str(self) - return self.name + def set_validator(self, vals): + """ + Deprecated Set a validator `vals` for this parameter. + Args: + vals (Validator): validator to set + + """ + warnings.warn( + "set_validator is deprected use `inst.vals = MyValidator` instead") + if isinstance(vals, Validator): + self.vals = vals + else: + raise TypeError('vals must be a Validator') class Parameter(_BaseParameter): """ A parameter that represents a single degree of freedom. - Not necessarily part of an instrument. - - Subclasses should define either a ``set`` method, a ``get`` method, or - both. + This is the standard parameter for Instruments, though it can also be + used as a variable, i.e. storing/retrieving a value, or be subclassed for + more complex uses. + + By default only gettable, returning its last value. + This behaviour can be modified in two ways: + 1. Providing a ``get_cmd``/``set_cmd``, which can of the following: + a. callable, with zero args for get_cmd, one arg for set_cmd + b. VISA command string + c. None, in which case it retrieves its last value for ``get_cmd``, + and stores a value for ``set_cmd`` + d. False, in which case trying to get/set will raise an error. + 2. Creating a subclass with an explicit ``get``/``set`` method. This + enables more advanced functionality. Parameters have a ``.get_latest`` method that simply returns the most recent set or measured value. This can be called ( ``param.get_latest()`` ) @@ -246,9 +557,6 @@ class Parameter(_BaseParameter): ``Loop(...).each(param.get_latest)`` - Note: If you want ``.get`` or ``.set`` to save the measurement for - ``.get_latest``, you must explicitly call ``self._save_val(value)`` - inside ``.get`` and ``.set``. Args: name (str): the local name of the parameter. Should be a valid @@ -265,41 +573,101 @@ class Parameter(_BaseParameter): unit (Optional[str]): The unit of measure. Use ``''`` for unitless. - units (Optional[str]): DEPRECATED, redirects to ``unit``. + snapshot_get (Optional[bool]): False prevents any update to the + parameter during a snapshot, even if the snapshot was called with + ``update=True``, for example if it takes too long to update. + Default True. + + snapshot_value (Optional[bool]): False prevents parameter value to be + stored in the snapshot. Useful if the value is large. + + step (Optional[Union[int, float]]): max increment of parameter value. + Larger changes are broken into multiple steps this size. + When combined with delays, this acts as a ramp. + + scale (Optional[float]): Scale to multiply value with before + performing set. the internally multiplied value is stored in + `raw_value`. Can account for a voltage divider. + + inter_delay (Optional[Union[int, float]]): Minimum time (in seconds) + between successive sets. If the previous set was less than this, + it will wait until the condition is met. + Can be set to 0 to go maximum speed with no errors. + + post_delay (Optional[Union[int, float]]): time (in seconds) to wait + after the *start* of each set, whether part of a sweep or not. + Can be set to 0 to go maximum speed with no errors. + + val_mapping (Optional[dict]): a bidirectional map data/readable values + to instrument codes, expressed as a dict: + ``{data_val: instrument_code}`` + For example, if the instrument uses '0' to mean 1V and '1' to mean + 10V, set val_mapping={1: '0', 10: '1'} and on the user side you + only see 1 and 10, never the coded '0' and '1' + If vals is omitted, will also construct a matching Enum validator. + NOTE: only applies to get if get_cmd is a string, and to set if + set_cmd is a string. + You can use ``val_mapping`` with ``get_parser``, in which case + ``get_parser`` acts on the return value from the instrument first, + then ``val_mapping`` is applied (in reverse). + + get_parser ( Optional[function]): function to transform the response + from get to the final output value. See also val_mapping + + set_parser (Optional[function]): function to transform the input set + value to an encoded value sent to the instrument. + See also val_mapping. vals (Optional[Validator]): Allowed values for setting this parameter. Only relevant if settable. Defaults to ``Numbers()`` + max_val_age (Optional[float]): The max time (in seconds) to trust a + saved value obtained from get_latest(). If this parameter has not + been set or measured more recently than this, perform an + additional measurement. + docstring (Optional[str]): documentation string for the __doc__ field of the object. The __doc__ field of the instance is used by some help systems, but not all - snapshot_get (Optional[bool]): False prevents any update to the - parameter during a snapshot, even if the snapshot was called with - ``update=True``, for example if it takes too long to update. - Default True. - metadata (Optional[dict]): extra information to include with the JSON snapshot of the parameter + """ - def __init__(self, name, instrument=None, label=None, - unit=None, units=None, vals=None, docstring=None, - snapshot_get=True, snapshot_value=True, metadata=None): - super().__init__(name, instrument, snapshot_get, metadata, - snapshot_value=snapshot_value) + def __init__(self, name, instrument=None, label=None, unit=None, + get_cmd=None, set_cmd=False, initial_value=None, + max_val_age=None, vals=None, docstring=None, **kwargs): + super().__init__(name=name, instrument=instrument, vals=vals, **kwargs) + + # Enable set/get methods if get_cmd/set_cmd is given + # Called first so super().__init__ can wrap get/set methods + if not hasattr(self, 'get') and get_cmd is not False: + if get_cmd is None: + if max_val_age is not None: + raise SyntaxError('Must have get method or specify get_cmd ' + 'when max_val_age is set') + self.get = self.get_latest + else: + exec_str = instrument.ask if instrument else None + self.get = Command(arg_count=0, cmd=get_cmd, exec_str=exec_str) + self.get = self._wrap_get(self.get) - self._meta_attrs.extend(['label', 'unit', '_vals']) + if not hasattr(self, 'set') and set_cmd is not False: + if set_cmd is None: + self.set = partial(self._save_val, validate=False) + else: + exec_str = instrument.write if instrument else None + self.set = Command(arg_count=1, cmd=set_cmd, exec_str=exec_str) + self.set = self._wrap_set(self.set) - self.label = name if label is None else label + self._meta_attrs.extend(['label', 'unit', 'vals']) - if units is not None: - warn_units('Parameter', self) - if unit is None: - unit = units + self.label = name if label is None else label self.unit = unit if unit is not None else '' - self.set_validator(vals) + if initial_value is not None: + self._save_val(initial_value, validate=True) # generate default docstring self.__doc__ = os.linesep.join(( @@ -308,7 +676,7 @@ def __init__(self, name, instrument=None, label=None, '* `name` %s' % self.name, '* `label` %s' % self.label, '* `unit` %s' % self.unit, - '* `vals` %s' % repr(self._vals))) + '* `vals` %s' % repr(self.vals))) if docstring is not None: self.__doc__ = os.linesep.join(( @@ -316,19 +684,12 @@ def __init__(self, name, instrument=None, label=None, '', self.__doc__)) - def set_validator(self, vals): + def __getitem__(self, keys): """ - Set a validator `vals` for this parameter. - - Args: - vals (Validator): validator to set + Slice a Parameter to get a SweepValues object + to iterate over during a sweep """ - if vals is None: - self._vals = Numbers() - elif isinstance(vals, Validator): - self._vals = vals - else: - raise TypeError('vals must be a Validator') + return SweepFixedValues(self, keys) def increment(self, value): """ Increment the parameter with a value @@ -338,22 +699,6 @@ def increment(self, value): """ self.set(self.get() + value) - def validate(self, value): - """ - Validate value - - Args: - value (any): value to validate - - """ - if self._instrument: - context = (getattr(self._instrument, 'name', '') or - str(self._instrument.__class__)) + '.' + self.name - else: - context = self.name - - self._vals.validate(value, 'Parameter: ' + context) - def sweep(self, start, stop, step=None, num=None): """ Create a collection of parameter values to be iterated over. @@ -381,18 +726,6 @@ def sweep(self, start, stop, step=None, num=None): return SweepFixedValues(self, start=start, stop=stop, step=step, num=num) - def __getitem__(self, keys): - """ - Slice a Parameter to get a SweepValues object - to iterate over during a sweep - """ - return SweepFixedValues(self, keys) - - @property - def units(self): - warn_units('Parameter', self) - return self.unit - class ArrayParameter(_BaseParameter): """ @@ -431,8 +764,6 @@ class ArrayParameter(_BaseParameter): unit (Optional[str]): The unit of measure. Use ``''`` for unitless. - units (Optional[str]): DEPRECATED, redirects to ``unit``. - setpoints (Optional[Tuple[setpoint_array]]): ``setpoint_array`` can be a DataArray, numpy.ndarray, or sequence. The setpoints for each dimension of the returned array. An @@ -467,26 +798,22 @@ class ArrayParameter(_BaseParameter): """ def __init__(self, name, shape, instrument=None, - label=None, unit=None, units=None, + label=None, unit=None, setpoints=None, setpoint_names=None, setpoint_labels=None, setpoint_units=None, docstring=None, snapshot_get=True, snapshot_value=True, metadata=None): super().__init__(name, instrument, snapshot_get, metadata, snapshot_value=snapshot_value) - if self.has_set: # TODO (alexcjohnson): can we support, ala Combine? + if hasattr(self, 'set'): + # TODO (alexcjohnson): can we support, ala Combine? raise AttributeError('ArrayParameters do not support set ' 'at this time.') - self._meta_attrs.extend(['setpoint_names', 'setpoint_labels', 'setpoint_units', - 'label', 'unit']) + self._meta_attrs.extend(['setpoint_names', 'setpoint_labels', + 'setpoint_units', 'label', 'unit']) self.label = name if label is None else label - - if units is not None: - warn_units('ArrayParameter', self) - if unit is None: - unit = units self.unit = unit if unit is not None else '' nt = type(None) @@ -500,7 +827,7 @@ def __init__(self, name, shape, instrument=None, sp_shape = (len(shape),) sp_types = (nt, DataArray, collections.Sequence, - collections.Iterator) + collections.Iterator, numpy.ndarray) if (setpoints is not None and not is_sequence_of(setpoints, sp_types, shape=sp_shape)): raise ValueError('setpoints must be a tuple of arrays') @@ -535,10 +862,8 @@ def __init__(self, name, shape, instrument=None, '', self.__doc__)) - @property - def units(self): - warn_units('ArrayParameter', self) - return self.unit + if not hasattr(self, 'get') and not hasattr(self, 'set'): + raise AttributeError('ArrayParameter must have a get, set or both') def _is_nested_sequence_or_none(obj, types, shapes): @@ -642,15 +967,15 @@ def __init__(self, name, names, shapes, instrument=None, super().__init__(name, instrument, snapshot_get, metadata, snapshot_value=snapshot_value) - if self.has_set: # TODO (alexcjohnson): can we support, ala Combine? - warnings.warn('MultiParameters do not fully support set ' - 'at this time.') + if hasattr(self, 'set'): + # TODO (alexcjohnson): can we support, ala Combine? + warnings.warn('MultiParameters do not support set at this time.') - self._meta_attrs.extend(['setpoint_names', 'setpoint_labels', 'setpoint_units', - 'names', 'labels', 'units']) + self._meta_attrs.extend(['setpoint_names', 'setpoint_labels', + 'setpoint_units', 'names', 'labels', 'units']) if not is_sequence_of(names, str): - raise ValueError('names must be a tuple of strings, not' + + raise ValueError('names must be a tuple of strings, not ' + repr(names)) self.names = names @@ -666,7 +991,7 @@ def __init__(self, name, names, shapes, instrument=None, self.shapes = shapes sp_types = (nt, DataArray, collections.Sequence, - collections.Iterator) + collections.Iterator, numpy.ndarray) if not _is_nested_sequence_or_none(setpoints, sp_types, shapes): raise ValueError('setpoints must be a tuple of tuples of arrays') @@ -701,6 +1026,9 @@ def __init__(self, name, names, shapes, instrument=None, '', self.__doc__)) + if not hasattr(self, 'get') and not hasattr(self, 'set'): + raise AttributeError('MultiParameter must have a get, set or both') + @property def full_names(self): """Include the instrument name with the Parameter names if possible.""" @@ -714,408 +1042,6 @@ def full_names(self): return self.names -def no_setter(*args, **kwargs): - raise NotImplementedError('This Parameter has no setter defined.') - - -def no_getter(*args, **kwargs): - raise NotImplementedError( - 'This Parameter has no getter, use .get_latest to get the most recent ' - 'set value.') - - -class StandardParameter(Parameter): - """ - Define one measurement parameter. - - Args: - name (str): the local name of this parameter - - instrument (Optional[Instrument]): the instrument this parameter - belongs to, if any - - get_cmd (Optional[Union[str, function]]): a string or function to - get this parameter. You can only use a string if an instrument is - provided, then this string will be passed to instrument.ask - - get_parser ( Optional[function]): function to transform the response - from get to the final output value. - See also val_mapping - - set_cmd (Optional[Union[str, function]]): command to set this - parameter, either: - - - a string (containing one field to .format, like "{}" etc) - you can only use a string if an instrument is provided, - this string will be passed to instrument.write - - a function (of one parameter) - - set_parser (Optional[function]): function to transform the input set - value to an encoded value sent to the instrument. - See also val_mapping - - val_mapping (Optional[dict]): a bidirectional map data/readable values - to instrument codes, expressed as a dict: - ``{data_val: instrument_code}`` - For example, if the instrument uses '0' to mean 1V and '1' to mean - 10V, set val_mapping={1: '0', 10: '1'} and on the user side you - only see 1 and 10, never the coded '0' and '1' - - If vals is omitted, will also construct a matching Enum validator. - NOTE: only applies to get if get_cmd is a string, and to set if - set_cmd is a string. - - You can use ``val_mapping`` with ``get_parser``, in which case - ``get_parser`` acts on the return value from the instrument first, - then ``val_mapping`` is applied (in reverse). - - You CANNOT use ``val_mapping`` and ``set_parser`` together - that - would just provide too many ways to do the same thing. - - vals (Optional[Validator]): a Validator object for this parameter - - delay (Optional[Union[int, float]]): time (in seconds) to wait after - the *start* of each set, whether part of a sweep or not. Can be - set to 0 to go maximum speed with no errors. - - max_delay (Optional[Union[int, float]]): If > delay, we don't emit a - warning unless the time taken during a single set is greater than - this, even though we aim for delay. - - step (Optional[Union[int, float]]): max increment of parameter value. - Larger changes are broken into multiple steps this size. - - max_val_age (Optional[Union[int, float]]): max time (in seconds) to - trust a saved value from this parameter as the starting point of - a sweep. - - **kwargs: Passed to Parameter parent class - - Raises: - NoCommandError: if get and set are not found - """ - - def __init__(self, name, instrument=None, - get_cmd=None, get_parser=None, - set_cmd=None, set_parser=None, - delay=None, max_delay=None, step=None, max_val_age=3600, - vals=None, val_mapping=None, **kwargs): - # handle val_mapping before super init because it impacts - # vals / validation in the base class - if val_mapping: - if vals is None: - vals = Enum(*val_mapping.keys()) - - self._get_mapping = {v: k for k, v in val_mapping.items()} - - if get_parser is None: - get_parser = self._valmapping_get_parser - else: - # First run get_parser, then run the result through - # val_mapping - self._get_preparser = get_parser - get_parser = self._valmapping_with_preparser - - if set_parser is None: - self._set_mapping = val_mapping - set_parser = self._set_mapping.__getitem__ - else: - raise TypeError( - 'You cannot use set_parser and val_mapping together.') - - super().__init__(name=name, instrument=instrument, vals=vals, **kwargs) - - self._meta_attrs.extend(['sweep_step', 'sweep_delay', - 'max_sweep_delay']) - - # stored value from last .set() or .get() - # normally only used by set with a sweep, to avoid - # having to call .get() for every .set() - self._max_val_age = 0 - - self._set_get(get_cmd, get_parser) - self._set_set(set_cmd, set_parser) - self.set_delay(delay, max_delay) - self.set_step(step, max_val_age) - - if not (self.has_get or self.has_set): - raise NoCommandError('neither set nor get cmd found in' + - ' Parameter {}'.format(self.name)) - - def get(self): - try: - value = self._get() - self._save_val(value) - return value - except Exception as e: - e.args = e.args + ('getting {}'.format(self.full_name),) - raise e - - def _valmapping_get_parser(self, val): - """ - Get parser to be used in the case that a val_mapping is defined - and a separate get_parser is not defined. - - Tries to match against defined strings in the mapping dictionary. If - there are no matches, we try to convert the val into an integer. - """ - - # Try and match the raw value from the instrument directly - try: - return self._get_mapping[val] - except KeyError: - pass - - # If there is no match, we can try to convert the parameter into a - # numeric value - try: - val = int(val) - return self._get_mapping[val] - except (ValueError, KeyError): - raise KeyError('Unmapped value from instrument: {!r}'.format(val)) - - def _valmapping_with_preparser(self, val): - return self._valmapping_get_parser(self._get_preparser(val)) - - def _set_get(self, get_cmd, get_parser): - exec_str = self._instrument.ask if self._instrument else None - self._get = Command(arg_count=0, cmd=get_cmd, exec_str=exec_str, - output_parser=get_parser, - no_cmd_function=no_getter) - - self.has_get = (get_cmd is not None) - - def _set_set(self, set_cmd, set_parser): - # note: this does not set the final setter functions. that's handled - # in self.set_sweep, when we choose a swept or non-swept setter. - # TODO(giulioungaretti) lies! that method does not exis. - # probably alexj left it out :( - exec_str = self._instrument.write if self._instrument else None - self._set = Command(arg_count=1, cmd=set_cmd, exec_str=exec_str, - input_parser=set_parser, no_cmd_function=no_setter) - - self.has_set = set_cmd is not None - - def _validate_and_set(self, value): - try: - clock = time.perf_counter() - self.validate(value) - self._set(value) - self._save_val(value) - if self._delay is not None: - clock, remainder = self._update_set_ts(clock) - time.sleep(remainder) - except Exception as e: - e.args = e.args + ( - 'setting {} to {}'.format(self.full_name, repr(value)),) - raise e - - def _sweep_steps(self, value): - oldest_ok_val = datetime.now() - timedelta(seconds=self._max_val_age) - state = self._latest() - if state['ts'] is None or state['ts'] < oldest_ok_val: - start_value = self.get() - else: - start_value = state['value'] - - self.validate(start_value) - - if not (isinstance(start_value, (int, float)) and - isinstance(value, (int, float))): - # something weird... parameter is numeric but one of the ends - # isn't, even though it's valid. - # probably a MultiType with a mix of numeric and non-numeric types - # just set the endpoint and move on - logging.warning('cannot sweep {} from {} to {} - jumping.'.format( - self.name, start_value, value)) - return [] - - # drop the initial value, we're already there - return permissive_range(start_value, value, self._step)[1:] - - def _update_set_ts(self, step_clock): - # calculate the delay time to the *max* delay, - # then take off up to the tolerance - tolerance = self._delay_tolerance - step_clock += self._delay - remainder = wait_secs(step_clock + tolerance) - if remainder <= tolerance: - # don't allow extra delays to compound - step_clock = time.perf_counter() - remainder = 0 - else: - remainder -= tolerance - return step_clock, remainder - - def _validate_and_sweep(self, value): - try: - self.validate(value) - step_clock = time.perf_counter() - - for step_val in self._sweep_steps(value): - self._set(step_val) - self._save_val(step_val) - if self._delay is not None: - step_clock, remainder = self._update_set_ts(step_clock) - time.sleep(remainder) - - self._set(value) - self._save_val(value) - - if self._delay is not None: - step_clock, remainder = self._update_set_ts(step_clock) - time.sleep(remainder) - except Exception as e: - e.args = e.args + ( - 'setting {} to {}'.format(self.full_name, repr(value)),) - raise e - - def set_step(self, step, max_val_age=None): - """ - Configure whether this Parameter uses steps during set operations. - If step is a positive number, this is the maximum value change - allowed in one hardware call, so a single set can result in many - calls to the hardware if the starting value is far from the target. - - Args: - step (Union[int, float]): A positive number, the largest change - allowed in one call. All but the final change will attempt to - change by +/- step exactly - - max_val_age (Optional[int]): Only used with stepping, the max time - (in seconds) to trust a saved value. If this parameter has not - been set or measured more recently than this, it will be - measured before starting to step, so we're confident in the - value we're starting from. - - Raises: - TypeError: if step is not numeric - ValueError: if step is negative - TypeError: if step is not integer for an integer parameter - TypeError: if step is not a number - TypeError: if max_val_age is not numeric - ValueError: if max_val_age is negative - """ - if not step: - # single-command setting - self.set = self._validate_and_set - - elif not self._vals.is_numeric: - raise TypeError('you can only step numeric parameters') - elif step <= 0: - raise ValueError('step must be positive') - elif (isinstance(self._vals, Ints) and - not isinstance(step, int)): - raise TypeError( - 'step must be a positive int for an Ints parameter') - elif not isinstance(step, (int, float)): - raise TypeError('step must be a number') - - else: - # stepped setting - if max_val_age is not None: - if not isinstance(max_val_age, (int, float)): - raise TypeError( - 'max_val_age must be a number') - if max_val_age < 0: - raise ValueError('max_val_age must be non-negative') - self._max_val_age = max_val_age - - self._step = step - self.set = self._validate_and_sweep - - def get_delay(self): - """Return the delay time of this parameter. Also see `set_delay` """ - return self._delay - - def set_delay(self, delay, max_delay=None): - """ - Configure this parameter with a delay between set operations. - - Typically used in conjunction with set_step to create an effective - ramp rate, but can also be used without a step to enforce a delay - after every set. - If delay and max_delay are both None or 0, we never emit warnings - no matter how long the set takes. - - Args: - delay(Union[int, float]): the target time between set calls. The - actual time will not be shorter than this, but may be longer - if the underlying set call takes longer. - - max_delay(Optional[Union[int, float]]): if given, the longest time - allowed for the underlying set call before we emit a warning. - - Raises: - TypeError: If delay is not int nor float - TypeError: If max_delay is not int nor float - ValueError: If delay is negative - ValueError: If max_delay is smaller than delay - """ - if delay is None: - delay = 0 - if not isinstance(delay, (int, float)): - raise TypeError('delay must be a number') - if delay < 0: - raise ValueError('delay must not be negative') - self._delay = delay - - if max_delay is not None: - if not isinstance(max_delay, (int, float)): - raise TypeError( - 'max_delay must be a either int or a float') - if max_delay < delay: - raise ValueError('max_delay must be no shorter than delay') - self._delay_tolerance = max_delay - delay - else: - self._delay_tolerance = 0 - - if not (self._delay or self._delay_tolerance): - # denotes that we shouldn't follow the wait code or - # emit any warnings - self._delay = None - - -class ManualParameter(Parameter): - """ - Define one parameter that reflects a manual setting / configuration. - - Args: - name (str): the local name of this parameter - - instrument (Optional[Instrument]): the instrument this applies to, - if any. - - initial_value (Optional[str]): starting value, may be None even if - None does not pass the validator. None is only allowed as an - initial value and cannot be set after initiation. - - **kwargs: Passed to Parameter parent class - """ - - def __init__(self, name, instrument=None, initial_value=None, **kwargs): - super().__init__(name=name, instrument=instrument, **kwargs) - self._meta_attrs.extend(['initial_value']) - - if initial_value is not None: - self.validate(initial_value) - self._save_val(initial_value) - - def set(self, value): - """ - Validate and saves value - - Args: - value (any): value to validate and save - """ - self.validate(value) - self._save_val(value) - - def get(self): - """ Return latest value""" - return self._latest()['value'] - - class GetLatest(DelegateAttributes, DeferredOperations): """ Wrapper for a Parameter that just returns the last set or measured value @@ -1129,17 +1055,35 @@ class GetLatest(DelegateAttributes, DeferredOperations): Args: parameter (Parameter): Parameter to be wrapped - """ - def __init__(self, parameter): + max_val_age (Optional[int]): The max time (in seconds) to trust a + saved value obtained from get_latest(). If this parameter has not + been set or measured more recently than this, perform an + additional measurement. + """ + def __init__(self, parameter, max_val_age=None): self.parameter = parameter + self.max_val_age = max_val_age delegate_attr_objects = ['parameter'] omit_delegate_attrs = ['set'] def get(self): - """ Return latest value""" - return self.parameter._latest()['value'] + """Return latest value if time since get was less than + `self.max_val_age`, otherwise perform `get()` and return result + """ + state = self.parameter._latest + if self.max_val_age is None: + # Return last value since max_val_age is not specified + return state['value'] + else: + oldest_ok_val = datetime.now() - timedelta(seconds=self.max_val_age) + if state['ts'] is None or state['ts'] < oldest_ok_val: + # Time of last get exceeds max_val_age seconds, need to + # perform new .get() + return self.parameter.get() + else: + return state['value'] def __call__(self): return self.get() @@ -1151,7 +1095,7 @@ def combine(*parameters, name, label=None, unit=None, units=None, Combine parameters into one sweepable parameter Args: - *paramters (qcodes.Parameter): the parameters to combine + *parameters (qcodes.Parameter): the parameters to combine name (str): the name of the paramter label (Optional[str]): the label of the combined parameter unit (Optional[str]): the unit of the combined parameter @@ -1298,14 +1242,14 @@ def snapshot_base(self, update=False): meta_data['unit'] = self.parameter.unit meta_data['label'] = self.parameter.label meta_data['full_name'] = self.parameter.full_name - meta_data['aggreagator'] = repr(getattr(self, 'f', None)) + meta_data['aggregator'] = repr(getattr(self, 'f', None)) for param in self.parameters: - meta_data[param.full_name] = param.snapshot() + meta_data[str(param)] = param.snapshot() return meta_data -class InstrumentRefParameter(ManualParameter): +class InstrumentRefParameter(Parameter): """ An InstrumentRefParameter @@ -1326,6 +1270,11 @@ class InstrumentRefParameter(ManualParameter): sets parameters on instruments it contains. """ + def __init__(self, *args, **kwargs): + kwargs['vals'] = kwargs.get('vals', Strings()) + super().__init__(set_cmd=None, *args, **kwargs) + + # TODO(nulinspiratie) check class works now it's subclassed from Parameter def get_instr(self): """ Returns the instance of the instrument with the name equal to the @@ -1337,16 +1286,27 @@ def get_instr(self): # of this parameter. return self._instrument.find_instrument(ref_instrument_name) - def set_validator(self, vals): - """ - Set a validator `vals` for this parameter. - Args: - vals (Validator): validator to set - """ - if vals is None: - self._vals = Strings() - elif isinstance(vals, Validator): - self._vals = vals - else: - raise TypeError('vals must be a Validator') +# Deprecated parameters +class StandardParameter(Parameter): + def __init__(self, name, instrument=None, + get_cmd=False, get_parser=None, + set_cmd=False, set_parser=None, + delay=0, max_delay=None, step=None, max_val_age=3600, + vals=None, val_mapping=None, **kwargs): + super().__init__(name, instrument=instrument, + get_cmd=get_cmd, get_parser=get_parser, + set_cmd=set_cmd, set_parser=set_parser, + post_delay=delay, step=step, max_val_age=max_val_age, + vals=vals, val_mapping=val_mapping, **kwargs) + warnings.warn('`StandardParameter` is deprecated, ' + 'use `Parameter` instead. {}'.format(self)) + + +class ManualParameter(Parameter): + def __init__(self, name, instrument=None, initial_value=None, **kwargs): + super().__init__(name=name, instrument=instrument, + get_cmd=None, set_cmd=None, + initial_value=initial_value, **kwargs) + warnings.warn('Parameter {}: `ManualParameter` is deprecated, use ' + '`Parameter` instead with `set_cmd=None`.'.format(self)) diff --git a/qcodes/tests/test_combined_par.py b/qcodes/tests/test_combined_par.py index 683ce929642..efa4cb0f394 100644 --- a/qcodes/tests/test_combined_par.py +++ b/qcodes/tests/test_combined_par.py @@ -23,6 +23,9 @@ def __init__(self, name): self.name = name self.full_name = name + def __str__(self): + return self.full_name + def set(self, value): value = value * 2 return value @@ -115,7 +118,7 @@ def testMeta(self): out["unit"] = unit out["label"] = label out["full_name"] = name - out["aggreagator"] = repr(linear) + out["aggregator"] = repr(linear) for param in sweep_values.parameters: out[param.full_name] = {} self.assertEqual(out, snap) diff --git a/qcodes/tests/test_loop.py b/qcodes/tests/test_loop.py index 6409e67e1cf..6c5a6e85ef2 100644 --- a/qcodes/tests/test_loop.py +++ b/qcodes/tests/test_loop.py @@ -462,7 +462,7 @@ def get(self): self._count -= 1 if self._count <= 0: raise _QcodesBreak - return super().get() + return self.get_latest() def reset(self): self._count = self._initial_count diff --git a/qcodes/tests/test_parameter.py b/qcodes/tests/test_parameter.py index f709eaab9a0..86c491f88ec 100644 --- a/qcodes/tests/test_parameter.py +++ b/qcodes/tests/test_parameter.py @@ -3,17 +3,18 @@ """ from collections import namedtuple from unittest import TestCase +from time import sleep from qcodes import Function from qcodes.instrument.parameter import ( Parameter, ArrayParameter, MultiParameter, ManualParameter, StandardParameter, InstrumentRefParameter) -from qcodes.utils.helpers import LogCapture -from qcodes.utils.validators import Numbers +import qcodes.utils.validators as vals from qcodes.tests.instrument_mocks import DummyInstrument class GettableParam(Parameter): + """ Parameter that keeps track of number of get operations""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._get_count = 0 @@ -24,31 +25,6 @@ def get(self): return 42 -class SimpleManualParam(Parameter): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._save_val(0) - self._v = 0 - - def get(self): - return self._v - - def set(self, v): - self._save_val(v) - self._v = v - - -class SettableParam(Parameter): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._save_val(0) - self._v = 0 - - def set(self, v): - self._save_val(v) - self._v = v - - blank_instruments = ( None, # no instrument at all namedtuple('noname', '')(), # no .name @@ -56,16 +32,36 @@ def set(self, v): ) named_instrument = namedtuple('yesname', 'name')('astro') +class MemoryParameter(Parameter): + def __init__(self, get_cmd=None, **kwargs): + self.set_values = [] + self.get_values = [] + super().__init__(set_cmd=self.add_set_value, + get_cmd=self.create_get_func(get_cmd), **kwargs) + + def add_set_value(self, value): + self.set_values.append(value) + + def create_get_func(self, func): + def get_func(): + if func is not None: + val = func() + else: + val = self._latest['value'] + self.get_values.append(val) + return val + return get_func + class TestParameter(TestCase): def test_no_name(self): with self.assertRaises(TypeError): - GettableParam() + Parameter() def test_default_attributes(self): # Test the default attributes, providing only a name name = 'repetitions' - p = GettableParam(name) + p = GettableParam(name, vals=vals.Numbers()) self.assertEqual(p.name, name) self.assertEqual(p.label, name) self.assertEqual(p.unit, '') @@ -88,7 +84,7 @@ def test_default_attributes(self): 'label': name, 'unit': '', 'value': 42, - 'vals': repr(Numbers()) + 'vals': repr(vals.Numbers()) } for k, v in snap_expected.items(): self.assertEqual(snap[k], v) @@ -101,7 +97,7 @@ def test_explicit_attributes(self): docstring = 'DOCS!' metadata = {'gain': 100} p = GettableParam(name, label=label, unit=unit, - vals=Numbers(5, 10), docstring=docstring, + vals=vals.Numbers(5, 10), docstring=docstring, snapshot_get=False, metadata=metadata) self.assertEqual(p.name, name) @@ -120,13 +116,14 @@ def test_explicit_attributes(self): # test snapshot_get by looking at _get_count self.assertEqual(p._get_count, 0) + # Snapshot should not perform get since snapshot_get is False snap = p.snapshot(update=True) self.assertEqual(p._get_count, 0) snap_expected = { 'name': name, 'label': label, 'unit': unit, - 'vals': repr(Numbers(5, 10)), + 'vals': repr(vals.Numbers(5, 10)), 'metadata': metadata } for k, v in snap_expected.items(): @@ -137,79 +134,94 @@ def test_explicit_attributes(self): 'setpoint_labels', 'full_names']: self.assertFalse(hasattr(p, attr), attr) - def test_units(self): - with LogCapture() as logs: - p = GettableParam('p', units='V') - - self.assertIn('deprecated', logs.value) - self.assertEqual(p.unit, 'V') - - with LogCapture() as logs: - self.assertEqual(p.units, 'V') - - self.assertIn('deprecated', logs.value) - - with LogCapture() as logs: - p = GettableParam('p', unit='Tesla', units='Gauss') - - self.assertIn('deprecated', logs.value) - self.assertEqual(p.unit, 'Tesla') - - with LogCapture() as logs: - self.assertEqual(p.units, 'Tesla') - - self.assertIn('deprecated', logs.value) - - def test_repr(self): - for i in [0, "foo", "", "fåil"]: - with self.subTest(i=i): - param = GettableParam(name=i) - s = param.__repr__() - st = '<{}.{}: {} at {}>'.format( - param.__module__, param.__class__.__name__, - param.name, id(param)) - self.assertEqual(s, st) + def test_snapshot_value(self): + p_snapshot = Parameter('no_snapshot', set_cmd=None, get_cmd=None, + snapshot_value=True) + p_snapshot(42) + snap = p_snapshot.snapshot() + self.assertIn('value', snap) + p_no_snapshot = Parameter('no_snapshot', set_cmd=None, get_cmd=None, + snapshot_value=False) + p_no_snapshot(42) + snap = p_no_snapshot.snapshot() + self.assertNotIn('value', snap) def test_has_set_get(self): - # you can't instantiate a Parameter directly anymore, only a subclass, - # because you need a get or a set method. - with self.assertRaises(AttributeError): - Parameter('no_get_or_set') - - gp = GettableParam('1') - self.assertTrue(gp.has_get) - self.assertFalse(gp.has_set) + # Create parameter that has no set_cmd, and get_cmd returns last value + gettable_parameter = Parameter('1', set_cmd=False, get_cmd=None) + self.assertTrue(hasattr(gettable_parameter, 'get')) + self.assertFalse(hasattr(gettable_parameter, 'set')) with self.assertRaises(NotImplementedError): - gp(1) - - sp = SettableParam('2') - self.assertFalse(sp.has_get) - self.assertTrue(sp.has_set) + gettable_parameter(1) + # Initial value is None if not explicitly set + self.assertIsNone(gettable_parameter()) + + # Create parameter that saves value during set, and has no get_cmd + settable_parameter = Parameter('2', set_cmd=None, get_cmd=False) + self.assertFalse(hasattr(settable_parameter, 'get')) + self.assertTrue(hasattr(settable_parameter, 'set')) with self.assertRaises(NotImplementedError): - sp() + settable_parameter() + settable_parameter(42) - sgp = SimpleManualParam('3') - self.assertTrue(sgp.has_get) - self.assertTrue(sgp.has_set) - sgp(22) - self.assertEqual(sgp(), 22) + settable_gettable_parameter = Parameter('3', set_cmd=None, get_cmd=None) + self.assertTrue(hasattr(settable_gettable_parameter, 'set')) + self.assertTrue(hasattr(settable_gettable_parameter, 'get')) + self.assertIsNone(settable_gettable_parameter()) + settable_gettable_parameter(22) + self.assertEqual(settable_gettable_parameter(), 22) - def test_full_name(self): + def test_str_representation(self): # three cases where only name gets used for full_name for instrument in blank_instruments: - p = GettableParam(name='fred') + p = Parameter(name='fred') p._instrument = instrument - self.assertEqual(p.full_name, 'fred') + self.assertEqual(str(p), 'fred') # and finally an instrument that really has a name - p = GettableParam(name='wilma') + p = Parameter(name='wilma') p._instrument = named_instrument - self.assertEqual(p.full_name, 'astro_wilma') + self.assertEqual(str(p), 'astro_wilma') def test_bad_validator(self): with self.assertRaises(TypeError): - GettableParam('p', vals=[1, 2, 3]) + Parameter('p', vals=[1, 2, 3]) + + def test_step_ramp(self): + p = MemoryParameter(name='test_step') + p(42) + self.assertListEqual(p.set_values, [42]) + p.step = 1 + + self.assertListEqual(p.get_ramp_values(44.5, 1), [43, 44, 44.5]) + + p(44.5) + self.assertListEqual(p.set_values, [42, 43, 44, 44.5]) + + def test_scale_raw_value(self): + p = Parameter(name='test_scale_raw_value', set_cmd=None) + p(42) + self.assertEqual(p.raw_value, 42) + + p.scale = 2 + self.assertEqual(p.raw_value, 42) # No set/get cmd performed + self.assertEqual(p(), 21) + + p(10) + self.assertEqual(p.raw_value, 20) + self.assertEqual(p(), 10) + def test_latest_value(self): + p = MemoryParameter(name='test_latest_value', get_cmd=lambda: 21) + + p(42) + self.assertEqual(p.get_latest(), 42) + self.assertListEqual(p.get_values, []) + + p.get_latest.max_val_age = 0.1 + sleep(0.2) + self.assertEqual(p.get_latest(), 21) + self.assertEqual(p.get_values, [21]) class SimpleArrayParam(ArrayParameter): def __init__(self, return_val, *args, **kwargs): @@ -302,30 +314,6 @@ def test_explicit_attrbutes(self): self.assertIn(name, p.__doc__) self.assertIn(docstring, p.__doc__) - def test_units(self): - with LogCapture() as logs: - p = SimpleArrayParam([6, 7], 'p', (2,), units='V') - - self.assertIn('deprecated', logs.value) - self.assertEqual(p.unit, 'V') - - with LogCapture() as logs: - self.assertEqual(p.units, 'V') - - self.assertIn('deprecated', logs.value) - - with LogCapture() as logs: - p = SimpleArrayParam([6, 7], 'p', (2,), - unit='Tesla', units='Gauss') - - self.assertIn('deprecated', logs.value) - self.assertEqual(p.unit, 'Tesla') - - with LogCapture() as logs: - self.assertEqual(p.units, 'Tesla') - - self.assertIn('deprecated', logs.value) - def test_has_set_get(self): name = 'array_param' shape = (3,) @@ -334,8 +322,8 @@ def test_has_set_get(self): p = SimpleArrayParam([1, 2, 3], name, shape) - self.assertTrue(p.has_get) - self.assertFalse(p.has_set) + self.assertTrue(hasattr(p, 'get')) + self.assertFalse(hasattr(p, 'set')) with self.assertRaises(AttributeError): SettableArray([1, 2, 3], name, shape) @@ -482,8 +470,8 @@ def test_has_set_get(self): p = SimpleMultiParam([0, [1, 2, 3], [[4, 5], [6, 7]]], name, names, shapes) - self.assertTrue(p.has_get) - self.assertFalse(p.has_set) + self.assertTrue(hasattr(p, 'get')) + self.assertFalse(hasattr(p, 'set')) # We allow creation of Multiparameters with set to support # instruments that already make use of them. with self.assertWarns(UserWarning): @@ -538,7 +526,7 @@ def test_bare_function(self): def doubler(x): p.set(x * 2) - f = Function('f', call_cmd=doubler, args=[Numbers(-10, 10)]) + f = Function('f', call_cmd=doubler, args=[vals.Numbers(-10, 10)]) f(4) self.assertEqual(p.get(), 8) @@ -571,20 +559,18 @@ def test_param_cmd_with_parsing(self): self.assertEqual(p(), 5) def test_settable(self): - p = StandardParameter('p', set_cmd=self.set_p) + p = Parameter('p', set_cmd=self.set_p, get_cmd=False) p(10) self.assertEqual(self._p, 10) with self.assertRaises(NotImplementedError): p() - with self.assertRaises(NotImplementedError): - p.get() - self.assertTrue(p.has_set) - self.assertFalse(p.has_get) + self.assertTrue(hasattr(p, 'set')) + self.assertFalse(hasattr(p, 'get')) def test_gettable(self): - p = StandardParameter('p', get_cmd=self.get_p) + p = Parameter('p', get_cmd=self.get_p) self._p = 21 self.assertEqual(p(), 21) @@ -592,15 +578,14 @@ def test_gettable(self): with self.assertRaises(NotImplementedError): p(10) - with self.assertRaises(NotImplementedError): - p.set(10) - self.assertTrue(p.has_get) - self.assertFalse(p.has_set) + self.assertTrue(hasattr(p, 'get')) + self.assertFalse(hasattr(p, 'set')) def test_val_mapping_basic(self): - p = StandardParameter('p', set_cmd=self.set_p, get_cmd=self.get_p, - val_mapping={'off': 0, 'on': 1}) + p = Parameter('p', set_cmd=self.set_p, get_cmd=self.get_p, + val_mapping={'off': 0, 'on': 1}, + vals=vals.Enum('off', 'on')) p('off') self.assertEqual(self._p, 0) @@ -619,17 +604,16 @@ def test_val_mapping_basic(self): p() def test_val_mapping_with_parsers(self): - # you can't use set_parser with val_mapping... just too much - # indirection since you also have set_cmd - with self.assertRaises(TypeError): - StandardParameter('p', set_cmd=self.set_p, get_cmd=self.get_p, - val_mapping={'off': 0, 'on': 1}, - set_parser=self.parse_set_p) - - # but you *can* use get_parser with val_mapping - p = StandardParameter('p', set_cmd=self.set_p_prefixed, - get_cmd=self.get_p, get_parser=self.strip_prefix, - val_mapping={'off': 0, 'on': 1}) + # set_parser with val_mapping + Parameter('p', set_cmd=self.set_p, get_cmd=self.get_p, + val_mapping={'off': 0, 'on': 1}, + set_parser=self.parse_set_p) + + # get_parser with val_mapping + p = Parameter('p', set_cmd=self.set_p_prefixed, + get_cmd=self.get_p, get_parser=self.strip_prefix, + val_mapping={'off': 0, 'on': 1}, + vals=vals.Enum('off', 'on')) p('off') self.assertEqual(self._p, 'PVAL: 0') diff --git a/qcodes/utils/helpers.py b/qcodes/utils/helpers.py index 6962fb9f7df..49a3dae90ec 100644 --- a/qcodes/utils/helpers.py +++ b/qcodes/utils/helpers.py @@ -60,7 +60,7 @@ def is_sequence(obj): We do not consider strings or unordered collections like sets to be sequences, but we do accept iterators (such as generators) """ - return (isinstance(obj, (Iterator, Sequence)) and + return (isinstance(obj, (Iterator, Sequence, np.ndarray)) and not isinstance(obj, (str, bytes, io.IOBase)))