def test_max(self): for max_val in [-1e20, -1, -0.1, 0, 0.1, 10]: n = Numbers(max_value=max_val) n.validate(n.valid_values[0]) for v in self.numbers: if v <= max_val: n.validate(v) else: with self.assertRaises(ValueError): n.validate(v) for v in self.not_numbers: with self.assertRaises(TypeError): n.validate(v) with self.assertRaises(ValueError): n.validate(float('nan'))
def test_min(): for min_val in [-1e20, -1, -0.1, 0, 0.1, 10]: n = Numbers(min_value=min_val) n.validate(n.valid_values[0]) for v in numbers: if v >= min_val: n.validate(v) else: with pytest.raises(ValueError): n.validate(v) for v in not_numbers: with pytest.raises(TypeError): n.validate(v) with pytest.raises(ValueError): n.validate(float('nan'))
def test_unlimited(self): n = Numbers() for v in self.numbers: n.validate(v) for v in self.not_numbers: with self.assertRaises(TypeError): n.validate(v) # special case - nan now raises a ValueError rather than a TypeError with self.assertRaises(ValueError): n.validate(float('nan')) n.validate(n.valid_values[0])
def test_min(self): for min_val in [-1e20, -1, -0.1, 0, 0.1, 10]: n = Numbers(min_value=min_val) for v in self.numbers: if v >= min_val: n.validate(v) else: with self.assertRaises(ValueError): n.validate(v) for v in self.not_numbers: with self.assertRaises(TypeError): n.validate(v) with self.assertRaises(ValueError): n.validate(float('nan'))
def test_range(self): n = Numbers(0.1, 3.5) for v in self.numbers: if 0.1 <= v <= 3.5: n.validate(v) else: with self.assertRaises(ValueError): n.validate(v) for v in self.not_numbers: with self.assertRaises(TypeError): n.validate(v) with self.assertRaises(ValueError): n.validate(float('nan')) self.assertEqual(repr(n), '<Numbers 0.1<=v<=3.5>')
def test_range(): n = Numbers(0.1, 3.5) for v in numbers: if 0.1 <= v <= 3.5: n.validate(v) else: with pytest.raises(ValueError): n.validate(v) for v in not_numbers: with pytest.raises(TypeError): n.validate(v) with pytest.raises(ValueError): n.validate(float('nan')) assert repr(n) == '<Numbers 0.1<=v<=3.5>'
def test_valid_values(self): val = Numbers() for vval in val.valid_values: val.validate(vval)
class Parameter(Metadatable, DeferredOperations): """ Define one generic parameter, not necessarily part of an instrument. can be settable and/or gettable. A settable Parameter has a .set method, and supports only a single value at a time (see below) A gettable Parameter has a .get method, which may return: 1. a single value 2. a sequence of values with different names (for example, raw and interpreted, I and Q, several fit parameters...) 3. an array of values all with the same name, but at different setpoints (for example, a time trace or fourier transform that was acquired in the hardware and all sent to the computer at once) 4. 2 & 3 together: a sequence of arrays. All arrays should be the same shape. 5. a sequence of differently shaped items Because .set only supports a single value, if a Parameter is both gettable AND settable, .get should return a single value too (case 1) Parameters have a .get_latest method that simply returns the most recent set or measured value. This can either be called ( param.get_latest() ) or used in a Loop as if it were a (gettable-only) parameter itself: Loop(...).each(param.get_latest) The constructor arguments change somewhat between these cases: Todo: no idea how to document such a constructor Args: name: (1&3) the local name of this parameter, should be a valid identifier, ie no spaces or special characters names: (2,4,5) a tuple of names label: (1&3) string to use as an axis label for this parameter defaults to name labels: (2,4,5) a tuple of labels units: (1&3) string that indicates units of parameter for use in axis label and snapshot shape: (3&4) a tuple of integers for the shape of array returned by .get(). shapes: (5) a tuple of tuples, each one as in `shape`. Single values should be denoted by None or () setpoints: (3,4,5) the setpoints for the returned array of values. 3&4 - a tuple of arrays. The first array is be 1D, the second 2D, etc. 5 - a tuple of tuples of arrays Defaults to integers from zero in each respective direction Each may be either a DataArray, a numpy array, or a sequence (sequences will be converted to numpy arrays) NOTE: if the setpoints will be different each measurement, leave this out and return the setpoints (with extra names) in the get. setpoint_names: (3,4,5) one identifier (like `name`) per setpoint array. Ignored if `setpoints` are DataArrays, which already have names. setpoint_labels: (3&4) one label (like `label`) per setpoint array. Overridden if `setpoints` are DataArrays and already have labels. vals: allowed values for setting this parameter (only relevant if it has a setter), defaults to Numbers() docstring (Optional[string]): 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 (bool): Prevent any update to the parameter for example if it takes too long to update """ def __init__(self, name=None, names=None, label=None, labels=None, units=None, shape=None, shapes=None, setpoints=None, setpoint_names=None, setpoint_labels=None, vals=None, docstring=None, snapshot_get=True, **kwargs): super().__init__(**kwargs) self._snapshot_get = snapshot_get self.has_get = hasattr(self, 'get') self.has_set = hasattr(self, 'set') self._meta_attrs = ['setpoint_names', 'setpoint_labels'] # always let the parameter have a single name (in fact, require this!) # even if it has names too self.name = str(name) if names is not None: # check for names first - that way you can provide both name # AND names for instrument parameters - name is how you get the # object (from the parameters dict or the delegated attributes), # and names are the items it returns self.names = names self.labels = names if labels is None else names self.units = units if units is not None else [''] * len(names) self.set_validator(vals or Anything()) self.__doc__ = os.linesep.join( ('Parameter class:', '* `names` %s' % ', '.join(self.names), '* `labels` %s' % ', '.join(self.labels), '* `units` %s' % ', '.join(self.units))) self._meta_attrs.extend(['names', 'labels', 'units']) elif name is not None: self.label = name if label is None else label self.units = units if units is not None else '' # vals / validate only applies to simple single-value parameters self.set_validator(vals) # generate default docstring self.__doc__ = os.linesep.join(( 'Parameter class:', '* `name` %s' % self.name, '* `label` %s' % self.label, # TODO is this unit s a typo? shouldnt that be unit? '* `units` %s' % self.units, '* `vals` %s' % repr(self._vals))) self._meta_attrs.extend(['name', 'label', 'units', 'vals']) else: raise ValueError('either name or names is required') if shape is not None or shapes is not None: nt = type(None) if shape is not None: if not is_sequence_of(shape, int): raise ValueError('shape must be a tuple of ints, not ' + repr(shape)) self.shape = shape depth = 1 container_str = 'tuple' else: if not is_sequence_of(shapes, int, depth=2): raise ValueError('shapes must be a tuple of tuples ' 'of ints, not ' + repr(shape)) self.shapes = shapes depth = 2 container_str = 'tuple of tuples' sp_types = (nt, DataArray, collections.Sequence, collections.Iterator) if (setpoints is not None and not is_sequence_of(setpoints, sp_types, depth)): raise ValueError( 'setpoints must be a {} of arrays'.format(container_str)) if (setpoint_names is not None and not is_sequence_of(setpoint_names, (nt, str), depth)): raise ValueError('setpoint_names must be a {} ' 'of strings'.format(container_str)) if (setpoint_labels is not None and not is_sequence_of(setpoint_labels, (nt, str), depth)): raise ValueError('setpoint_labels must be a {} ' 'of strings'.format(container_str)) self.setpoints = setpoints self.setpoint_names = setpoint_names self.setpoint_labels = setpoint_labels # 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 if docstring is not None: self.__doc__ = docstring + os.linesep + self.__doc__ self.get_latest = GetLatest(self) def __repr__(self): return named_repr(self) def __call__(self, *args): if len(args) == 0: if self.has_get: return self.get() else: raise NoCommandError('no get cmd found in' + ' Parameter {}'.format(self.name)) else: if self.has_set: self.set(*args) else: raise NoCommandError('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): if ((attr[0] == '_' and attr not in self._keep_attrs) or 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. Args: update (bool): If True, update the state by calling parameter.get(). If False, just use the latest values in memory. Returns: dict: base snapshot """ if self.has_get and self._snapshot_get and update: self.get() state = self._latest() state['__class__'] = full_class(self) if isinstance(state['ts'], datetime): state['ts'] = state['ts'].strftime('%Y-%m-%d %H:%M:%S') for attr in set(self._meta_attrs): if attr == 'instrument' and getattr(self, '_instrument', None): state.update({ 'instrument': full_class(self._instrument), 'instrument_name': self._instrument.name }) elif hasattr(self, attr): state[attr] = getattr(self, attr) return state def _save_val(self, value): self._latest_value = value self._latest_ts = datetime.now() def set_validator(self, vals): """ Set a validator `vals` for this parameter. Args: vals (Validator): validator to set """ if vals is None: self._vals = Numbers() elif isinstance(vals, Validator): self._vals = vals else: raise TypeError('vals must be a Validator') def validate(self, value): """ Validate value Args: value (any): value to validate """ if hasattr(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. Requires `start` and `stop` and (`step` or `num`) The sign of `step` is not relevant. Args: start (Union[int, float]): The starting value of the sequence. stop (Union[int, float]): The end value of the sequence. step (Optional[Union[int, float]]): Spacing between values. num (Optional[int]): Number of values to generate. Returns: SweepFixedValues: collection of parameter values to be iterated over Examples: >>> sweep(0, 10, num=5) [0.0, 2.5, 5.0, 7.5, 10.0] >>> sweep(5, 10, step=1) [5.0, 6.0, 7.0, 8.0, 9.0, 10.0] >>> sweep(15, 10.5, step=1.5) >[15.0, 13.5, 12.0, 10.5] """ 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 full_name(self): """Include the instrument name with the Parameter name if possible.""" if getattr(self, 'name', None) is None: return None try: inst_name = self._instrument.name if inst_name: return inst_name + '_' + self.name except AttributeError: pass return self.name @property def full_names(self): """Include the instrument name with the Parameter names if possible.""" if getattr(self, 'names', None) is None: return None try: inst_name = self._instrument.name if inst_name: return [inst_name + '_' + name for name in self.names] except AttributeError: pass return self.names
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. Parameters have a ``.get_latest`` method that simply returns the most recent set or measured value. This can be called ( ``param.get_latest()`` ) or used in a ``Loop`` as if it were a (gettable-only) parameter itself: ``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 identifier, ie no spaces or special characters. If this parameter is part of an Instrument or Station, this is how it will be referenced from that parent, ie ``instrument.name`` or ``instrument.parameters[name]`` instrument (Optional[Instrument]): the instrument this parameter belongs to, if any label (Optional[str]): Normally used as the axis label when this parameter is graphed, along with ``unit``. unit (Optional[str]): The unit of measure. Use ``''`` for unitless. units (Optional[str]): DEPRECATED, redirects to ``unit``. vals (Optional[Validator]): Allowed values for setting this parameter. Only relevant if settable. Defaults to ``Numbers()`` 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) self._meta_attrs.extend(['label', 'unit', '_vals']) self.label = name if label is None else label if units is not None: warn_units('Parameter', self) if unit is None: unit = units self.unit = unit if unit is not None else '' self.set_validator(vals) # generate default docstring self.__doc__ = os.linesep.join(( 'Parameter class:', '', '* `name` %s' % self.name, '* `label` %s' % self.label, '* `unit` %s' % self.unit, '* `vals` %s' % repr(self._vals))) if docstring is not None: self.__doc__ = os.linesep.join(( docstring, '', self.__doc__)) def set_validator(self, vals): """ Set a validator `vals` for this parameter. Args: vals (Validator): validator to set """ if vals is None: self._vals = Numbers() elif isinstance(vals, Validator): self._vals = vals else: raise TypeError('vals must be a Validator') 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. Requires `start` and `stop` and (`step` or `num`) The sign of `step` is not relevant. Args: start (Union[int, float]): The starting value of the sequence. stop (Union[int, float]): The end value of the sequence. step (Optional[Union[int, float]]): Spacing between values. num (Optional[int]): Number of values to generate. Returns: SweepFixedValues: collection of parameter values to be iterated over Examples: >>> sweep(0, 10, num=5) [0.0, 2.5, 5.0, 7.5, 10.0] >>> sweep(5, 10, step=1) [5.0, 6.0, 7.0, 8.0, 9.0, 10.0] >>> sweep(15, 10.5, step=1.5) >[15.0, 13.5, 12.0, 10.5] """ 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