From cdd6d7f1a2cd386180b778c48ec6244704c13b59 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Mon, 20 Feb 2023 14:32:30 +0100 Subject: [PATCH] improved features - remove legacy feature proposals - add Feature.featureName (in case not matching ptyhon class name) Change-Id: I7a09fc5e24067b2fde5d2c04523bc5d2172e714b --- frappy/features.py | 232 ++++++++------------------------------------- frappy/modules.py | 3 +- 2 files changed, 39 insertions(+), 196 deletions(-) diff --git a/frappy/features.py b/frappy/features.py index 1ff8878..d69b832 100644 --- a/frappy/features.py +++ b/frappy/features.py @@ -16,41 +16,52 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # Module authors: -# Enrico Faulhaber # Markus Zolliker # # ***************************************************************************** """Define Mixin Features for real Modules implemented in the server""" -from frappy.datatypes import ArrayOf, BoolType, EnumType, \ - FloatRange, StringType, StructOf, TupleOf -from frappy.core import Command, Done, Drivable, Feature, \ - Parameter, Property, PersistentParam, Readable -from frappy.errors import BadValueError, ConfigError -from frappy.lib import clamp +from frappy.datatypes import FloatRange, TupleOf +from frappy.core import Feature, Parameter, PersistentParam +from frappy.errors import BadValueError -# --- proposals, to be used at SINQ (not agreed as standard yet) --- +class HasSimpleOffset(Feature): + """has a client side offset parameter + + this is just a storage! + """ + featureName = 'HasOffset' + offset = PersistentParam('offset (physical value + offset = HW value)', + FloatRange(unit='deg'), readonly=False, default=0) + + +class HasTargetLimits(Feature): + """user limits + + implementation to be done in the subclass + according to standard + """ + target_limits = PersistentParam('user limits', readonly=False, default=(-9e99, 9e99), + datatype=TupleOf(FloatRange(unit='deg'), FloatRange(unit='deg'))) + + def check_limits(self, value): + """check if value is valid""" + min_, max_ = self.target_limits + if not min_ <= value <= max_: + raise BadValueError('limits violation: %g outside [%g, %g]' % (value, min_, max_)) + + +# --- legacy mixins, not agreed as standard --- class HasOffset(Feature): """has an offset parameter implementation to be done in the subclass """ - offset = PersistentParam('offset (physical value + offset = HW value)', - FloatRange(unit='deg'), readonly=False, default=0) - - def write_offset(self, value): - self.offset = value - if isinstance(self, HasLimits): - self.read_limits() - if isinstance(self, Readable): - self.read_value() - if isinstance(self, Drivable): - self.read_target() - self.saveParameters() - return Done + offset = Parameter('offset (physical value + offset = HW value)', + FloatRange(unit='$'), readonly=False, default=0) class HasLimits(Feature): @@ -61,185 +72,16 @@ class HasLimits(Feature): for a drivable, abslimits is roughly the same as the target datatype limits, except for the offset """ - abslimits = Property('abs limits (raw values)', default=(-9e99, 9e99), extname='abslimits', export=True, - datatype=TupleOf(FloatRange(unit='deg'), FloatRange(unit='deg'))) - limits = PersistentParam('user limits', readonly=False, default=(-9e99, 9e99), - datatype=TupleOf(FloatRange(unit='deg'), FloatRange(unit='deg'))) - _limits = None - - def apply_offset(self, sign, *values): - if isinstance(self, HasOffset): - return tuple(v + sign * self.offset for v in values) - return values + target_limits = Parameter('user limits for target', readonly=False, default=(-9e99, 9e99), + datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$'))) def earlyInit(self): super().earlyInit() - # make limits valid - _limits = self.apply_offset(1, *self.limits) - self._limits = tuple(clamp(self.abslimits[0], v, self.abslimits[1]) for v in _limits) - self.read_limits() - - def checkProperties(self): - pname = 'target' if isinstance(self, Drivable) else 'value' - dt = self.parameters[pname].datatype - min_, max_ = self.abslimits - t_min, t_max = self.apply_offset(1, dt.min, dt.max) - if t_min > max_ or t_max < min_: - raise ConfigError('abslimits not within %s range' % pname) - self.abslimits = clamp(t_min, min_, t_max), clamp(t_min, max_, t_max) - super().checkProperties() - - def read_limits(self): - return self.apply_offset(-1, *self._limits) - - def write_limits(self, value): - min_, max_ = self.apply_offset(-1, *self.abslimits) - if not min_ <= value[0] <= value[1] <= max_: - if value[0] > value[1]: - raise BadValueError('invalid interval: %r' % value) - raise BadValueError('limits not within abs limits [%g, %g]' % (min_, max_)) - self.limits = value - self.saveParameters() - return Done + dt = self.parameters['target'].datatype + self.target_limits = dt.min, dt.max def check_limits(self, value): """check if value is valid""" - min_, max_ = self.limits + min_, max_ = self.target_limits if not min_ <= value <= max_: raise BadValueError('limits violation: %g outside [%g, %g]' % (value, min_, max_)) - - -# --- not used, not tested yet --- - -class HAS_PID(Feature): - # note: implementors should either use p,i,d or pid, but ECS must be handle both cases - # note: if both p,i,d and pid are implemented, it MUST NOT matter which one gets a change, the final result should be the same - # note: if there are additional custom accessibles with the same name as an element of the struct, the above applies - # note: (i would still but them in the same group, though) - # note: if extra elements are implemented in the pid struct they MUST BE - # properly described in the description of the pid Parameter - - # parameters - use_pid = Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), ) - # pylint: disable=invalid-name - p = Parameter('proportional part of the regulation', datatype=FloatRange(0), ) - i = Parameter('(optional) integral part', datatype=FloatRange(0), optional=True) - d = Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True) - base_output = Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True) - pid = Parameter('(optional) Struct of p,i,d, minimum output value', - datatype=StructOf(p=FloatRange(0), - i=FloatRange(0), - d=FloatRange(0), - base_output=FloatRange(0), - ), optional=True, - ) # note: struct may be extended with custom elements (names should be prefixed with '_') - output = Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False) - - -class Has_PIDTable(HAS_PID): - - # parameters - use_pidtable = Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1)) - pidtable = Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0), - StructOf(p=FloatRange(0), - i=FloatRange(0), - d=FloatRange(0), - _heater_range=FloatRange(0), - _base_output=FloatRange(0),),),), optional=True) # struct may include 'heaterrange' - - -class HAS_Persistent(Feature): - #extra_Status { - # 'decoupled' : Status.IDLE+1, # to be discussed. - # 'coupling' : Status.BUSY+1, # to be discussed. - # 'coupled' : Status.BUSY+2, # to be discussed. - # 'decoupling' : Status.BUSY+3, # to be discussed. - #} - - # parameters - persistent_mode = Parameter('Use persistent mode', - datatype=EnumType(off=0,on=1), - default=0, readonly=False) - is_persistent = Parameter('current state of persistence', - datatype=BoolType(), optional=True) - # stored_value = Parameter('current persistence value, often used as the modules value', - # datatype='main', unit='$', optional=True) - # driven_value = Parameter('driven value (outside value, syncs with stored_value if non-persistent)', - # datatype='main', unit='$' ) - - -class HAS_Tolerance(Feature): - # detects IDLE status by checking if the value lies in a given window: - # tolerance is the maximum allowed deviation from target, value must lie in this interval - # for at least ´timewindow´ seconds. - - # parameters - tolerance = Parameter('Half height of the Window', - datatype=FloatRange(0), default=1, unit='$') - timewindow = Parameter('Length of the timewindow to check', - datatype=FloatRange(0), default=30, unit='s', - optional=True) - - -class HAS_Timeout(Feature): - - # parameters - timeout = Parameter('timeout for movement', - datatype=FloatRange(0), default=0, unit='s') - - -class HAS_Pause(Feature): - # just a proposal, can't agree on it.... - - @Command(argument=None, result=None) - def pause(self): - """pauses movement""" - - @Command(argument=None, result=None) - def go(self): - """continues movement or start a new one if target was change since the last pause""" - - -class HAS_Ramp(Feature): - - # parameters - ramp =Parameter('speed of movement', unit='$/min', - datatype=FloatRange(0)) - use_ramp = Parameter('use the ramping of the setpoint, or jump', - datatype=EnumType(disable_ramp=0, use_ramp=1), - optional=True) - setpoint = Parameter('currently active setpoint', - datatype=FloatRange(0), unit='$', - readonly=True, ) - - -class HAS_Speed(Feature): - - # parameters - speed = Parameter('(maximum) speed of movement (of the main value)', - unit='$/s', datatype=FloatRange(0)) - - -class HAS_Accel(HAS_Speed): - - # parameters - accel = Parameter('acceleration of movement', unit='$/s^2', - datatype=FloatRange(0)) - decel = Parameter('deceleration of movement', unit='$/s^2', - datatype=FloatRange(0), optional=True) - - -class HAS_MotorCurrents(Feature): - - # parameters - movecurrent = Parameter('Current while moving', - datatype=FloatRange(0)) - idlecurrent = Parameter('Current while idle', - datatype=FloatRange(0), optional=True) - - -class HAS_Curve(Feature): - # proposed, not yet agreed upon! - - # parameters - curve = Parameter('Calibration curve', datatype=StringType(), default='') diff --git a/frappy/modules.py b/frappy/modules.py index 74f2424..fa169d2 100644 --- a/frappy/modules.py +++ b/frappy/modules.py @@ -224,6 +224,7 @@ class Feature(HasAccessibles): a mixin with Feature as a direct base class is recognized as a SECoP feature and reported in the module property 'features' """ + featureName = None class PollInfo: @@ -358,7 +359,7 @@ class Module(HasAccessibles): b.__name__ for b in mycls.__mro__ if issubclass(Drivable, b)][0:1] # handle Features - self.features = [b.__name__ for b in mycls.__mro__ if Feature in b.__bases__] + self.features = [b.featureName or b.__name__ for b in mycls.__mro__ if Feature in b.__bases__] # handle accessibles # 1) make local copies of parameter objects