diff --git a/cfg/dilsc.cfg b/cfg/dilsc.cfg new file mode 100644 index 0000000..c2bf7bf --- /dev/null +++ b/cfg/dilsc.cfg @@ -0,0 +1,279 @@ +[NODE] +id = dilsc.psi.ch +description = triton test + +[INTERFACE] +uri = tcp://5000 + +[triton] +class = secop_psi.mercury.IO +description = connection to triton software +uri = tcp://192.168.2.33:33576 + +[T_mix] +class = secop_psi.triton.TemperatureSensor +description = mix. chamber temperature +slot = T8 +io = triton + +[T_pt2head] +class = secop_psi.triton.TemperatureSensor +description = PTR2 head temperature +slot = T1 +io = triton + +[T_pt2plate] +class = secop_psi.triton.TemperatureSensor +description = PTR2 plate temperature +slot = T2 +io = triton + +[T_still] +class = secop_psi.triton.TemperatureSensor +description = still temperature +slot = T3 +io = triton + +[htr_still] +class = secop_psi.triton.HeaterOutput +description = still heater +slot = H2 +io = triton + +[T_coldpl] +class = secop_psi.triton.TemperatureSensor +description = cold plate temperature +slot = T4 +io = triton + +[T_mixcx] +class = secop_psi.triton.TemperatureSensor +description = mix. chamber cernox +slot = T5 +io = triton + +[T_pt1head] +class = secop_psi.triton.TemperatureSensor +description = PTR1 head temperature +slot = T6 +io = triton + +[T_pt1plate] +class = secop_psi.triton.TemperatureSensor +description = PTR1 plate temperature +slot = T7 +io = triton + +[T_pucksensor] +class = secop_psi.triton.TemperatureLoop +output_module = htr_pucksensor +description = puck sensor temperature +slot = TA +io = triton + +[htr_pucksensor] +class = secop_psi.triton.HeaterOutput +description = mix. chamber heater +slot = H1 +io = triton + +[T_magnet] +class = secop_psi.triton.TemperatureSensor +description = magnet temperature +slot = T13 +io = triton + +[action] +class = secop_psi.triton.Action +description = higher level scripts +io = triton +slot = DR + +[p_dump] +class = secop_psi.mercury.PressureSensor +description = dump pressure +slot = P1 +io = triton + +[p_cond] +class = secop_psi.mercury.PressureSensor +description = condenser pressure +slot = P2 +io = triton + +[p_still] +class = secop_psi.mercury.PressureSensor +description = still pressure +slot = P3 +io = triton + +[p_fore] +class = secop_psi.mercury.PressureSensor +description = pressure on the pump side +slot = P5 +io = triton + +[p_back] +class = secop_psi.mercury.PressureSensor +description = pressure on the back side of the pump +slot = P4 +io = triton + +#[itc] +#class = secop_psi.mercury.IO +#description = connection to MercuryiTC +#uri = serial:///dev/ttyUSB0 +# +#[T_still_wup] +#class = secop_psi.mercury.TemperatureLoop +#description = still warmup temperature +#slot = MB1.T1 +#io = itc +# +#[htr_still_wup] +#class = secop_psi.mercury.HeaterOutput +#description = still warmup heater +#slot = MB0.H1 +#io = itc +# +#[T_one_K] +#class = secop_psi.mercury.TemperatureLoop +#description = 1 K plate warmup temperature +#slot = DB5.T1 +#io = itc +# +#[htr_one_K] +#class = secop_psi.mercury.HeaterOutput +#description = 1 K plate warmup heater +#slot = DB3.H1 +#io = itc +# +#[T_mix_wup] +#class = secop_psi.mercury.TemperatureLoop +#description = mix. chamber warmup temperature +#slot = DB6.T1 +#io = itc +# +#[htr_mix_wup] +#class = secop_psi.mercury.HeaterOutput +#description = mix. chamber warmup heater +#slot = DB1.H1 +#io = itc +# +#[T_ivc_wup] +#class = secop_psi.mercury.TemperatureLoop +#description = IVC warmup temperature +#slot = DB7.T1 +#io = itc +# +#[htr_ivc_wup] +#class = secop_psi.mercury.HeaterOutput +#description = IVC warmup heater +#slot = DB2.H1 +#io = itc +# +#[T_cond] +#class = secop_psi.mercury.TemperatureLoop +#description = condenser temperature +#slot = DB8.T1 +#io = itc +# +#[htr_cond] +#class = secop_psi.mercury.HeaterOutput +#description = condenser heater +#slot = DB3.H1 +#io = itc + +[V1] +class = secop_psi.triton.Valve +description = valve V1 +slot = V1 +io = triton + +[V2] +class = secop_psi.triton.Valve +description = valve V2 +slot = V2 +io = triton + +[V4] +class = secop_psi.triton.Valve +description = valve V4 +slot = V4 +io = triton + +[V5] +class = secop_psi.triton.Valve +description = valve V5 +slot = V5 +io = triton + +[V9] +class = secop_psi.triton.Valve +description = valve V9 +slot = V9 +io = triton + +# [turbo] +# class = secop_psi.triton.TurboPump +# description = still turbo pump +# slot = TURB1 +# io = triton + +# [fp] +# class = secop_psi.triton.Pump +# description = still fore pump +# slot = FP +# io = triton + +# [compressor] +# class = secop_psi.triton.Pump +# description = compressor +# slot = COMP +# io = triton + +[ips] +class = secop_psi.mercury.IO +description = IPS for magnet +uri = 192.168.127.254:3001 + +[mf] +class = secop_psi.dilsc.VectorField +description = vector field +x = mfx +y = mfy +z = mfz +sphere_radius = 0.06 +cylinders = ((0.023, 0.52), (0.045, 0.08)) + +[mfx] +class = secop_psi.ips_mercury.SimpleField +description = magnetic field, x-axis +slot = GRPX +io = ips +tolerance = 0.0001 +wait_stable_field = 0 +nunits = 2 +target.max = 0.6 +ramp = 0.225 + +[mfy] +class = secop_psi.ips_mercury.SimpleField +description = magnetic field, y axis +slot = GRPY +io = ips +tolerance = 0.0001 +wait_stable_field = 0 +nunits = 2 +target.max = 0.6 +ramp = 0.225 + +[mfz] +class = secop_psi.ips_mercury.Field +description = magnetic field, z-axis +slot = GRPZ +io = ips +tolerance = 0.0001 +target.max = 5.2 +mode = DRIVEN +ramp = 0.52 diff --git a/cfg/magsc.cfg b/cfg/magsc.cfg new file mode 100644 index 0000000..0a0f3ef --- /dev/null +++ b/cfg/magsc.cfg @@ -0,0 +1,52 @@ +[NODE] +id = magsc.psi.ch +description = dilsc mag test + +[INTERFACE] +uri = tcp://5000 + +[ips] +class = secop_psi.mercury.IO +description = IPS for magnet +uri = 192.168.127.254:3001 + +[mf] +class = secop_psi.dilsc.VectorField +description = vector field +x = mfx +y = mfy +z = mfz +sphere_radius = 0.06 +cylinders = ((0.023, 0.52), (0.045, 0.08)) + +[mfx] +class = secop_psi.ips_mercury.SimpleField +description = magnetic field, x-axis +slot = GRPX +io = ips +tolerance = 0.0001 +wait_stable_field = 0 +nunits = 2 +target.max = 0.6 +ramp = 0.225 + +[mfy] +class = secop_psi.ips_mercury.SimpleField +description = magnetic field, y axis +slot = GRPY +io = ips +tolerance = 0.0001 +wait_stable_field = 0 +nunits = 2 +target.max = 0.6 +ramp = 0.225 + +[mfz] +class = secop_psi.ips_mercury.Field +description = magnetic field, z-axis +slot = GRPZ +io = ips +tolerance = 0.0001 +target.max = 5.2 +mode = DRIVEN +ramp = 0.52 diff --git a/secop/features.py b/secop/features.py index ef27224..ec0880f 100644 --- a/secop/features.py +++ b/secop/features.py @@ -39,7 +39,7 @@ class HasOffset(Feature): implementation to be done in the subclass """ offset = PersistentParam('offset (physical value + offset = HW value)', - FloatRange(unit='deg'), readonly=False, default=0) + FloatRange(unit='$'), readonly=False, default=0) def write_offset(self, value): self.offset = value @@ -62,9 +62,9 @@ class HasLimits(Feature): 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'))) + datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$'))) limits = PersistentParam('user limits', readonly=False, default=(-9e99, 9e99), - datatype=TupleOf(FloatRange(unit='deg'), FloatRange(unit='deg'))) + datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$'))) _limits = None def apply_offset(self, sign, *values): diff --git a/secop/lib/statemachine.py b/secop/lib/statemachine.py index 9fe6271..8c5c8fb 100644 --- a/secop/lib/statemachine.py +++ b/secop/lib/statemachine.py @@ -30,7 +30,7 @@ The created state object may hold variables needed for the state. A state function may return either: - a function for the next state to transition to - Retry() to keep the state and call the -- or `None` for finishing +- or `Finish` for finishing Initialisation Code @@ -44,33 +44,23 @@ def state_x(stateobj): ... further code ... -Cleanup Function ----------------- +Error Handler +------------- -cleanup= as argument in StateMachine.__init__ or .start -defines a cleanup function to be called whenever the machine is stopped or -an error is raised in a state function. A cleanup function may return -either None for finishing or a further state function for continuing. -In case of stop or restart, this return value is ignored. - - -State Specific Cleanup Code ---------------------------- - -To execute state specific cleanup, the cleanup may examine the current state -(stateobj.state) in order to decide what to be done. - -If a need arises, a future extension to this library may support specific -cleanup functions by means of a decorator adding the specific cleanup function -as an attribute to the state function. +handler= as argument in StateMachine.__init__ or .start +defines a handler object to be called whenever the machine is stopped, +restarted. finished or an error is raised in a state function. +.on_error may return either None for finishing or a further state +function for continuing. The other handler methods always return None, +as there is no useful follow up state. Threaded Use ------------ On start, a thread is started, which is waiting for a trigger event when the -machine is not active. For test purposes or special needs, the thread creation -may be disabled. :meth:`cycle` must be called periodically in this case. +machine is not active. In case the thread creation is disabled. :meth:`cycle` +must be called periodically for running the state machine. """ import time @@ -80,6 +70,7 @@ from logging import getLogger from secop.lib import mkthread, UniqueObject +Finish = UniqueObject('Finish') Stop = UniqueObject('Stop') Restart = UniqueObject('Restart') @@ -89,6 +80,52 @@ class Retry: self.delay = delay +class StateHandler: + """default handlers + + may be used as base class or mixin for implementing custom handlers + """ + def on_error(self, statemachine, exc): + """called on error + + :param statemachine: the state machine object + :param exc: the exception + :return: None or a state function to be executed for handling the error state + """ + statemachine.log.warning('%r raised in state %r', exc, statemachine.status_string) + + def on_transition(self, statemachine, newstate): + """called when state is changed + + :param statemachine: the statemachine + :param newstate: the new state function + + this method will not be called when the state is changed to None, + e.g. on finish, restart, stop or when None is returned from the error handler + """ + + def on_restart(self, statemachine): + """called on restart + + :param statemachine: the state machine object + """ + + def on_stop(self, statemachine): + """called when stopped + + :param statemachine: the state machine object + """ + + def on_finish(self, statemachine): + """called on finish + + :param statemachine: the state machine object + """ + + +default_handler = StateHandler() + + class StateMachine: """a simple, but powerful state machine""" # class attributes are not allowed to be overriden by kwds of __init__ or :meth:`start` @@ -98,7 +135,7 @@ class StateMachine: now = None init = True stopped = False - last_error = None # last exception raised or Stop or Restart + restarted = False _last_time = 0 def __init__(self, state=None, logger=None, threaded=True, **kwds): @@ -109,9 +146,9 @@ class StateMachine: :param threaded: whether a thread should be started (default: True) :param kwds: any attributes for the state object """ - self.default_delay = 0.25 # default delay when returning None + self.default_delay = 0.25 # default delay when returning Retry(None) self.now = time.time() # avoid calling time.time several times per state - self.cleanup = self.default_cleanup # default cleanup: finish on error + self.handler = default_handler self.log = logger or getLogger('dummy') self._update_attributes(kwds) self._lock = threading.RLock() @@ -120,23 +157,9 @@ class StateMachine: self._thread_queue = queue.Queue() self._idle_event = threading.Event() self._thread = None - self._restart = None if state: self.start(state) - @staticmethod - def default_cleanup(state): - """default cleanup - - :param self: the state object - :return: None (for custom cleanup functions this might be a new state) - """ - if state.stopped: # stop or restart - verb = 'stopped' if state.stopped is Stop else 'restarted' - state.log.debug('%s in state %r', verb, state.status_string) - else: - state.log.warning('%r raised in state %r', state.last_error, state.status_string) - def _update_attributes(self, kwds): """update allowed attributes""" cls = type(self) @@ -168,10 +191,12 @@ class StateMachine: def _new_state(self, state): self.state = state - self.init = True - self.now = time.time() - self.transition_time = self.now self.log.debug('state: %s', self.status_string) + if state: + self.handler.on_transition(self, state) + self.init = True + self.now = time.time() + self.transition_time = self.now def cycle(self): """do one cycle in the thread loop @@ -187,18 +212,26 @@ class StateMachine: ret = self.state(self) self.init = False if self.stopped: - self.last_error = self.stopped - self.cleanup(self) + self.log.debug('stopped in state %r', self.status_string) + self.handler.on_stop(self) self.stopped = False ret = None + elif self.restarted: + self.log.debug('restarted in state %r', self.status_string) + self.handler.on_restart(self) + self.restarted = False + ret = None except Exception as e: - self.last_error = e - ret = self.cleanup(self) - self.log.debug('called %r %sexc=%r', self.cleanup, - 'ret=%r ' % ret if ret else '', e) - if ret is None: - self.log.debug('state: None after cleanup') - self.state = None + try: + ret = self.handler.on_error(self, e) + self.log.debug('called on_error with exc=%r%s', e, + ' ret=%r' % ret if ret else '') + except Exception as ee: + self.log.exception('%r raised in on_error(state, %r)', ee, e) + if ret is Finish: + self.log.debug('finish in state %r', self.status_string) + self.handler.on_finish(self) + self._new_state(None) self._idle_event.set() return None if callable(ret): @@ -210,12 +243,13 @@ class StateMachine: if ret.delay is None: return self.default_delay return ret.delay - self.last_error = RuntimeError('return value must be callable, Retry(...) or finish') + self.handler.on_error(self, RuntimeError( + 'return value must be callable, Retry(...) or Finish, not %r' % ret)) break else: - self.last_error = RuntimeError('too many states chained - probably infinite loop') - self.cleanup(self) - self.state = None + self.handler.on_error(self, RuntimeError( + 'too many states chained - probably infinite loop')) + self._new_state(None) return None def trigger(self, delay=0): @@ -238,9 +272,8 @@ class StateMachine: delay = self.cycle() def _start(self, state, **kwds): - self._restart = None self._idle_event.clear() - self.last_error = None + self.restarted = False self.stopped = False self._update_attributes(kwds) self._new_state(state) @@ -259,19 +292,18 @@ class StateMachine: """start with a new state and interrupt the current state - the cleanup function will be called with state.stopped=Restart + the cleanup function will be called with state.restarted = True :param state: the first state :param kwds: items to put as attributes on the state machine """ self.log.debug('start %r', kwds) if self.state: - self.stopped = Restart + self.restarted = True with self._lock: # wait for running cycle finished - if self.stopped: # cleanup is not yet done - self.last_error = self.stopped - self.cleanup(self) # ignore return state on restart - self.stopped = False + if self.restarted: # on_restart is not yet called + self.handler.on_restart(self) + self.restarted = False self._start(state, **kwds) else: self._start(state, **kwds) @@ -279,16 +311,15 @@ class StateMachine: def stop(self): """stop machine, go to idle state - the cleanup function will be called with state.stopped=Stop + the cleanup function will be called with state.stopped = True """ self.log.debug('stop') - self.stopped = Stop + self.stopped = True with self._lock: - if self.stopped: # cleanup is not yet done - self.last_error = self.stopped - self.cleanup(self) # ignore return state on restart + if self.stopped: # on_stop is not yet called + self.handler.on_stop(self) self.stopped = False - self.state = None + self._new_state(None) def wait(self, timeout=None): """wait for state machine being idle""" diff --git a/secop/states.py b/secop/states.py new file mode 100644 index 0000000..f6d7166 --- /dev/null +++ b/secop/states.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# ***************************************************************************** +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# +# ***************************************************************************** +"""mixin for modules with a statemachine""" + +from secop.lib.statemachine import StateMachine, Finish, Retry, StateHandler +from secop.core import BUSY, IDLE, ERROR, Parameter +from secop.errors import ProgrammingError + + +class status_code: + """decorator for state methods""" + def __init__(self, code, text=None): + self.code = code + self.text = text + + def __set_name__(self, owner, name): + if not issubclass(owner, HasStates): + raise ProgrammingError('when using decorator "status_code", %s must inherit HasStates' % owner.__name__) + self.cls = owner + self.name = name + if 'statusMap' not in owner.__dict__: + # we need a copy on each inheritance level + owner.statusMap = dict(owner.statusMap) + owner.statusMap[name] = self.code, name.replace('_', ' ') if self.text is None else self.text + setattr(owner, name, self.func) + + def __call__(self, func): + self.func = func + return self + + +class HasStates(StateHandler): + status = Parameter() # make sure this is a parameter + skip_consecutive_status_changes = False + _state_machine = None + _next_cycle = 0 + statusMap = {} + + def init_state_machine(self, fullstatus=True, **kwds): + self._state_machine = StateMachine( + logger=self.log, + threaded=False, + handler=self, + default_delay=1e-3, # small but not zero (avoid infinite loop) + **kwds) + + def initModule(self): + super().initModule() + self.init_state_machine() + + def on_error(self, statemachine, exc): + """called on error""" + error = '%r in %s' % (exc, statemachine.status_string) + self.log.error('%s', error) + return self.final_status(ERROR, error) + + def on_transition(self, statemachine, newstate): + if not self.skip_consecutive_status_changes: + self.set_status_from_state(newstate) + + def set_status_from_state(self, newstate): + name = newstate.__name__ + status = self.statusMap.get(name) + if status is None: + status = BUSY, name.replace('_', ' ') + if status != self.status: + self.status = status + + def doPoll(self): + super().doPoll() + now = self.pollInfo.last_main + if now > self._next_cycle: + delay = self._state_machine.cycle() + if delay is None: + self._next_cycle = 0 + else: + if self.skip_consecutive_status_changes: + self.set_status_from_state(self._state_machine.state) + self._next_cycle = now + delay + + def start_state(self, start_state, fast_poll=True, **kwds): + self._state_machine.start(start_state, **kwds) + if fast_poll is not None: + self.setFastPoll(fast_poll) + + def final_status(self, code=IDLE, text='', fast_poll=False): + self.status = code, text + if fast_poll is not None: + self.setFastPoll(fast_poll) + return Finish diff --git a/secop_psi/convergence.py b/secop_psi/convergence.py index 385a098..32b3a0e 100644 --- a/secop_psi/convergence.py +++ b/secop_psi/convergence.py @@ -21,7 +21,7 @@ # ***************************************************************************** from secop.core import Parameter, FloatRange, BUSY, IDLE, WARN -from secop.lib.statemachine import StateMachine, Retry, Stop +from secop.lib.statemachine import StateMachine, Retry class HasConvergence: @@ -61,9 +61,8 @@ class HasConvergence: def cleanup(self, state): state.default_cleanup(state) if state.stopped: - if state.stopped is Stop: # and not Restart - self.status = WARN, 'stopped' - else: + self.status = WARN, 'stopped' + elif not state.restarted: self.status = WARN, repr(state.last_error) def doPoll(self): diff --git a/secop_psi/dilsc.py b/secop_psi/dilsc.py new file mode 100644 index 0000000..bc9d493 --- /dev/null +++ b/secop_psi/dilsc.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""vector field""" + +import math +from secop.core import Drivable, Done, BUSY, IDLE, ERROR, Parameter, TupleOf, ArrayOf, FloatRange +from secop.errors import BadValueError +from secop_psi.vector import Vector +from secop.states import HasStates, Retry + + +DECREASE = 1 +INCREASE = 2 + + +class VectorField(HasStates, Vector, Drivable): + sphere_radius = Parameter('max. spehere', datatype=FloatRange(0, 0.7, unit='T'), readonly=True, default=0.6) + cylinders = Parameter('allowed cylinders (list of radius and height)', + datatype=ArrayOf(TupleOf(FloatRange(0, 0.6, unit='T'), FloatRange(0, 5.2, unit='T')), 1, 9), + readonly=True, default=((0.23, 5.2), (0.45, 0.8))) + + def initModule(self): + super().initModule() + # override check_limits of the components with a check for restrictions on the vector + for idx, component in enumerate(self.components): + + def outer_check(target, vector=self, i=idx, inner_check=component.check_limits): + inner_check(target) + if vector: + value = [c.value - math.copysign(c.tolerance, c.value) + for c in vector.components] + value[i] = target + vector.check_limits(value) + + component.check_limits = outer_check + + def merge_status(self): + return self.status + + def check_limits(self, value): + """check if value is within one of the safe shapes""" + if sum((v ** 2 for v in value)) <= self.sphere_radius ** 2: + return + for r, h in self.cylinders: + if sum(v ** 2 for v in value[0:2]) <= r ** 2 and abs(value[2]) <= h: + return + raise BadValueError('vector %s does not fit in any limiting shape' % repr(value)) + + def write_target(self, value): + """initiate target change""" + # check limits first + for target, component in zip(value, self.components): + # check against limits of individual components + component.check_limits(target, vector=None) # no outer check here! + self.check_limits(value) + for target, component in zip(value, self.components): + if target * component.value < 0: + # change sign: drive to zero first + target = 0 + if abs(target) > abs(component.value): + target = component.value + component.write_target(target) + self.start_state(self.ramp_down, target=value) + return value + + def ramp_down(self, state): + for target, component in zip(state.target, self.components): + if component.isDriving(): + return Retry() + for target, component in zip(state.target, self.components): + component.write_target(target) + return self.final_ramp + + def final_ramp(self, state): + for component in self.components: + if component.isDriving(): + return Retry() + return self.final_status() diff --git a/secop_psi/ips_mercury.py b/secop_psi/ips_mercury.py index f74972a..29c233d 100644 --- a/secop_psi/ips_mercury.py +++ b/secop_psi/ips_mercury.py @@ -21,11 +21,12 @@ """oxford instruments mercury IPS power supply""" import time -from secop.core import Parameter, EnumType, FloatRange, BoolType +from secop.core import Parameter, EnumType, FloatRange, BoolType, IntRange, StringType, Property, BUSY from secop.lib.enum import Enum from secop.errors import BadValueError, HardwareError -from secop_psi.magfield import Magfield +from secop_psi.magfield import Magfield, SimpleMagfield, Status from secop_psi.mercury import MercuryChannel, off_on, Mapped +from secop.lib.statemachine import Retry Action = Enum(hold=0, run_to_set=1, run_to_zero=2, clamped=3) hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set, @@ -33,55 +34,18 @@ hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set, CURRENT_CHECK_SIZE = 2 -class Field(MercuryChannel, Magfield): +class SimpleField0(MercuryChannel, SimpleMagfield): + nunits = Property('number of IPS subunits', IntRange(1, 6), default=1) action = Parameter('action', EnumType(Action), readonly=False) setpoint = Parameter('field setpoint', FloatRange(unit='T'), default=0) voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0) atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0) - I1 = Parameter('master current', FloatRange(unit='A'), default=0) - I2 = Parameter('slave 2 current', FloatRange(unit='A'), default=0) - I3 = Parameter('slave 3 current', FloatRange(unit='A'), default=0) - V1 = Parameter('master voltage', FloatRange(unit='V'), default=0) - V2 = Parameter('slave 2 voltage', FloatRange(unit='V'), default=0) - V3 = Parameter('slave 3 voltage', FloatRange(unit='V'), default=0) - forced_persistent_field = Parameter( - 'manual indication that persistent field is bad', BoolType(), readonly=False, default=False) - + working_ramp = Parameter('effective ramp', FloatRange(0, unit='T/min'), default=0) channel_type = 'PSU' - _field_mismatch = None - nslaves = 3 slave_currents = None - __init = True - __reset_switch_time = False - - def doPoll(self): - super().doPoll() - self.read_current() def read_value(self): - self.current = self.query('PSU:SIG:FLD') - pf = self.query('PSU:SIG:PFLD') - if self.__init: - self.__init = False - self.persistent_field = pf - if self.switch_heater == self.switch_heater.on or self._field_mismatch is None: - self.forced_persistent_field = False - self._field_mismatch = False - return self.current - self._field_mismatch = abs(self.persistent_field - pf) > self.tolerance - return pf - - def write_persistent_field(self, value): - if self.forced_persistent_field: - self._field_mismatch = False - return value - raise BadValueError('changing persistent field needs forced_persistent_field=True') - - def write_target(self, target): - if self._field_mismatch: - self.forced_persistent_field = True - raise BadValueError('persistent field does not match - set persistent field to guessed value first') - return super().write_target(target) + return self.query('PSU:SIG:FLD') def read_ramp(self): return self.query('PSU:SIG:RFST') @@ -95,6 +59,124 @@ class Field(MercuryChannel, Magfield): def write_action(self, value): return self.change('PSU:ACTN', value, hold_rtoz_rtos_clmp) + def read_atob(self): + return self.query('PSU:ATOB') + + def read_voltage(self): + return self.query('PSU:SIG:VOLT') + + def read_working_ramp(self): + return self.query('PSU:SIG:RFLD') + + def read_setpoint(self): + return self.query('PSU:SIG:FSET') + + def set_and_go(self, value): + self.setpoint = self.change('PSU:SIG:FSET', value) + assert self.write_action('hold') == 'hold' + assert self.write_action('run_to_set') == 'run_to_set' + + def start_ramp_to_target(self, state): + # if self.action != 'hold': + # assert self.write_action('hold') == 'hold' + # return Retry() + self.set_and_go(state.target) + state.try_cnt = 5 + return self.ramp_to_target + + def ramp_to_target(self, state): + try: + return super().ramp_to_target(state) + except HardwareError: + state.try_cnt -= 1 + if state.try_cnt < 0: + raise + self.set_and_go(state.target) + return Retry() + + def final_status(self, *args, **kwds): + self.write_action('hold') + return super().final_status(*args, **kwds) + + def on_restart(self, state): + self.write_action('hold') + return super().on_restart(state) + + +class Field0(SimpleField0, Magfield): + wait_switch_on = Parameter( + 'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=True, default=60) + wait_switch_off = Parameter( + 'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=True, default=60) + forced_persistent_field = Parameter( + 'manual indication that persistent field is bad', BoolType(), readonly=False, default=False) + + _field_mismatch = None + __init = True + __reset_switch_time = False + + def doPoll(self): + super().doPoll() + self.read_current() + + def startModule(self, start_events): + self.switch_time = [0, 0] + self.switch_heater = self.query('PSU:SIG:SWHT', off_on) + super().startModule(start_events) + # on restart, assume switch is changed long time ago, if not, the mercury + # # will complain and this will be handled in start_ramp_to_field + + def read_value(self): + current = self.query('PSU:SIG:FLD') + pf = self.query('PSU:SIG:PFLD') + if self.__init: + self.__init = False + self.persistent_field = pf + if self.switch_heater == self.switch_heater.on or self._field_mismatch is None: + self.forced_persistent_field = False + self._field_mismatch = False + return current + self._field_mismatch = abs(self.persistent_field - pf) > self.tolerance + return pf + + def read_current(self): + if self.slave_currents is None: + self.slave_currents = [[] for _ in range(self.nunits + 1)] + current = self.query('PSU:SIG:CURR') + if self.nunits > 1: + for i in range(self.nunits + 1): + if i: + curri = self.query('DEV:PSU.M%d:PSU:SIG:CURR' % i) + volti = self.query('DEV:PSU.M%d:PSU:SIG:VOLT' % i) + setattr(self, 'I%d' % i, curri) + setattr(self, 'V%d' % i, volti) + self.slave_currents[i].append(curri) + else: + self.slave_currents[i].append(current) + min_i = min(self.slave_currents[i]) + max_i = max(self.slave_currents[i]) + min_ = min(self.slave_currents[0]) / self.nunits + max_ = max(self.slave_currents[0]) / self.nunits + if len(self.slave_currents[i]) > CURRENT_CHECK_SIZE: + self.slave_currents[i] = self.slave_currents[i][-CURRENT_CHECK_SIZE:] + if i and (min_i - 1 > max_ or min_ > max_i + 1): + self.log.warning('individual currents mismatch %r', self.slave_currents) + if self.atob: + return current / self.atob + return 0 + + def write_persistent_field(self, value): + if self.forced_persistent_field: + self._field_mismatch = False + return value + raise BadValueError('changing persistent field needs forced_persistent_field=True') + + def write_target(self, target): + if self._field_mismatch: + self.forced_persistent_field = True + raise BadValueError('persistent field does not match - set persistent field to guessed value first') + return super().write_target(target) + def read_switch_heater(self): value = self.query('PSU:SIG:SWHT', off_on) now = time.time() @@ -106,74 +188,77 @@ class Field(MercuryChannel, Magfield): return self.switch_heater elif self.__reset_switch_time: self.__reset_switch_time = False - self.switch_time = [None, None] + self.switch_time[value] = now return value + def read_wait_switch_on(self): + return self.query('PSU:SWONT') * 0.001 + + def read_wait_switch_off(self): + return self.query('PSU:SWOFT') * 0.001 + def write_switch_heater(self, value): + if value == self.read_switch_heater(): + self.log.info('switch heater already %r', value) + # we do not want to restart the timer + return value return self.change('PSU:SIG:SWHT', value, off_on) - def read_atob(self): - return self.query('PSU:ATOB') - - def read_voltage(self): - return self.query('PSU:SIG:VOLT') - - def read_setpoint(self): - return self.query('PSU:SIG:FSET') - - def read_current(self): - if self.slave_currents is None: - self.slave_currents = [[] for _ in range(self.nslaves + 1)] - current = self.query('PSU:SIG:CURR') - for i in range(self.nslaves + 1): - if i: - curri = self.query('DEV:PSU.M%d:PSU:SIG:CURR' % i) - volti = self.query('DEV:PSU.M%d:PSU:SIG:VOLT' % i) - setattr(self, 'I%d' % i, curri) - setattr(self, 'V%d' % i, volti) - self.slave_currents[i].append(curri) - else: - self.slave_currents[i].append(current) - min_i = min(self.slave_currents[i]) - max_i = max(self.slave_currents[i]) - min_ = min(self.slave_currents[0]) / self.nslaves - max_ = max(self.slave_currents[0]) / self.nslaves - if len(self.slave_currents[i]) > CURRENT_CHECK_SIZE: - self.slave_currents[i] = self.slave_currents[i][-CURRENT_CHECK_SIZE:] - if i and (min_i -1 > max_ or min_ > max_i + 1): - self.log.warning('individual currents mismatch %r', self.slave_currents) - if self.atob: - return current / self.atob - return 0 - - def set_and_go(self, value): - self.change('PSU:SIG:FSET', value) - assert self.write_action('hold') == 'hold' - assert self.write_action('run_to_set') == 'run_to_set' - def start_ramp_to_field(self, state): + if abs(self.current - self.persistent_field) <= self.tolerance: + self.log.info('leads %g are already at %g', self.current, self.persistent_field) + return self.ramp_to_field try: self.set_and_go(self.persistent_field) - except (HardwareError, AssertionError): - state.switch_undef = self.switch_on_time or state.now + except (HardwareError, AssertionError) as e: + if self.switch_heater: + self.log.warn('switch is already on!') + return self.ramp_to_field + self.log.warn('wait first for switch off current=%g pf=%g', self.current, self.persistent_field) + return Retry() + self.status = Status.PREPARING, 'wait for switch off' + state.wait_for = 0 return self.wait_for_switch return self.ramp_to_field - def wait_for_switch(self, state): - if self.now - self.switch_undef < self.wait_switch_on: - return Retry() - self.set_and_go(self.persistent_field) - return self.ramp_to_field - def start_ramp_to_target(self, state): - self.set_and_go(self.target) + state.try_cnt = 5 + try: + self.set_and_go(state.target) + except (HardwareError, AssertionError) as e: + self.log.warn('switch not yet ready %r', e) + self.status = Status.PREPARING, 'wait for switch on' + state.wait_for = 1 + return self.wait_for_switch return self.ramp_to_target + def wait_for_switch(self, state): + if not state.delta(10): # wait at least 10 seconds + return Retry() + try: + # try again + self.set_and_go(self.persistent_field) + except (HardwareError, AssertionError) as e: + return Retry() + return self.ramp_to_target if state.wait_for else self.ramp_to_field + def start_ramp_to_zero(self, state): assert self.write_action('hold') == 'hold' assert self.write_action('run_to_zero') == 'run_to_zero' return self.ramp_to_zero - def finish_state(self, state): - self.write_action('hold') - super().finish_state(state) + +def Field(name, logger, cfgdict, srv, base=Field0): + nunits = cfgdict.get('nunits', 1) + if nunits == 1: + return base(name, logger, cfgdict, srv) + # create individual current and voltage parameters dynamically + attrs = {} + for i in range(1, nunits + 1): + attrs['I%d' % i] = Parameter('slave %s current' % i, FloatRange(unit='A'), default=0) + attrs['V%d' % i] = Parameter('slave %s voltage' % i, FloatRange(unit='V'), default=0) + return type(base.__name__.replace('0', str(nunits)), (base,), attrs)(name, logger, cfgdict, srv) + + +def SimpleField(name, logger, cfgdict, srv): + return Field(name, logger, cfgdict, srv, SimpleField0) \ No newline at end of file diff --git a/secop_psi/magfield.py b/secop_psi/magfield.py index ef90bc2..27879b6 100644 --- a/secop_psi/magfield.py +++ b/secop_psi/magfield.py @@ -20,12 +20,12 @@ """generic persistent magnet driver""" import time -from secop.core import Drivable, Parameter, Done +from secop.core import Drivable, Parameter, Done, IDLE, BUSY, ERROR from secop.datatypes import FloatRange, EnumType, ArrayOf, TupleOf, StatusType from secop.features import HasLimits -from secop.errors import ConfigError, ProgrammingError +from secop.errors import ConfigError, ProgrammingError, HardwareError from secop.lib.enum import Enum -from secop.lib.statemachine import Retry, StateMachine +from secop.states import Retry, HasStates, status_code UNLIMITED = FloatRange() @@ -48,25 +48,98 @@ OFF = 0 ON = 1 -class Magfield(HasLimits, Drivable): +class SimpleMagfield(HasStates, HasLimits, Drivable): value = Parameter('magnetic field', datatype=FloatRange(unit='T')) + ramp = Parameter( + 'ramp rate for field', FloatRange(unit='$/min'), readonly=False) + tolerance = Parameter( + 'tolerance', FloatRange(0, unit='$'), readonly=False, default=0.0002) + trained = Parameter( + 'trained field (positive)', + TupleOf(FloatRange(-99, 0, unit='$'), FloatRange(0, unit='$')), + readonly=False, default=(0, 0)) + wait_stable_field = Parameter( + 'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=31) + + _last_target = None + + def checkProperties(self): + dt = self.parameters['target'].datatype + max_ = dt.max + if max_ == UNLIMITED.max: + raise ConfigError('target.max not configured') + if dt.min == UNLIMITED.min: # not given: assume bipolar symmetric + dt.min = -max_ + super().checkProperties() + + def stop(self): + """keep field at current value""" + # let the state machine do the needed steps to finish + self.write_target(self.value) + + def onInterrupt(self, state): + self.log.info('interrupt target=%g', state.target) + + def write_target(self, target): + self.check_limits(target) + self.start_state(self.start_field_change, target=target) + return target + + def get_progress(self, state, min_ramp): + """calculate the inverse slope sec/Tesla + + and return the time needed for a tolerance step + """ + result = True + if state.init: + state.tol_time = 5 # default minimum stabilize time when tol_time can not be calculated + else: + t, v = state.prev_point + dif = abs(v - self.value) + tdif = (state.now - t) + if dif > self.tolerance: + state.tol_time = tdif * self.tolerance / dif + state.prev_point = state.now, self.value + elif tdif > self.tolerance * 60 / min_ramp: + # real slope is less than 0.001 * ramp -> no progress + result = False + else: + return True + state.prev_point = state.now, self.value + return result + + @status_code(BUSY, 'start ramp to target') + def start_field_change(self, state): + self.setFastPoll(True, 1.0) + return self.start_ramp_to_target + + @status_code(BUSY, 'ramping field') + def ramp_to_target(self, state): + # Remarks: assume there is a ramp limiting feature + if abs(self.value - state.target) > self.tolerance: + if self.get_progress(state, self.ramp * 0.01): + return Retry() + raise HardwareError('no progress') + state.stabilize_start = time.time() + return self.stabilize_field + + @status_code(BUSY, 'stabilizing field') + def stabilize_field(self, state): + if state.now - state.stabilize_start < self.wait_stable_field: + return Retry() + return self.final_status() + + +class Magfield(SimpleMagfield): status = Parameter(datatype=StatusType(Status)) mode = Parameter( 'persistent mode', EnumType(Mode), readonly=False, default=Mode.PERSISTENT) - tolerance = Parameter( - 'tolerance', FloatRange(0, unit='$'), readonly=False, default=0.0002) switch_heater = Parameter('switch heater', EnumType(off=OFF, on=ON), readonly=False, default=0) persistent_field = Parameter( 'persistent field', FloatRange(unit='$'), readonly=False) current = Parameter( 'leads current (in units of field)', FloatRange(unit='$')) - ramp = Parameter( - 'ramp rate for field', FloatRange(unit='$/min'), readonly=False) - trained = Parameter( - 'trained field (positive)', - TupleOf(FloatRange(-99, 0, unit='$'), FloatRange(0, unit='$')), - readonly=False, default=(0, 0)) # TODO: time_to_target # profile = Parameter( # 'ramp limit table', ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))), @@ -81,15 +154,11 @@ class Magfield(HasLimits, Drivable): 'wait time to ensure switch is off', FloatRange(0, unit='s'), readonly=False, default=61) wait_stable_leads = Parameter( 'wait time to ensure current is stable', FloatRange(0, unit='s'), readonly=False, default=6) - wait_stable_field = Parameter( - 'wait time to ensure field is stable', FloatRange(0, unit='s'), readonly=False, default=31) persistent_limit = Parameter( 'above this limit, lead currents are not driven to 0', FloatRange(0, unit='$'), readonly=False, default=99) - _state = None __init = True - _last_target = None switch_time = None, None def doPoll(self): @@ -102,86 +171,67 @@ class Magfield(HasLimits, Drivable): else: self._last_target = self.persistent_field else: - self.read_value() - self._state.cycle() + super().doPoll() - def checkProperties(self): - dt = self.parameters['target'].datatype - max_ = dt.max - if max_ == UNLIMITED.max: - raise ConfigError('target.max not configured') - if dt.min == UNLIMITED.min: # not given: assume bipolar symmetric - dt.min = -max_ - super().checkProperties() - - def initModule(self): - super().initModule() + def initStateMachine(self): + super().initStateMachine() self.registerCallbacks(self) # for update_switch_heater - self._state = StateMachine(logger=self.log, threaded=False, cleanup=self.cleanup_state) + + def write_mode(self, value): + self.start_state(self.start_field_change, target=self.target, mode=value) + return value def write_target(self, target): self.check_limits(target) - self.target = target - if not self._state.is_active: - # as long as the state machine is still running, it takes care of changing targets - self._state.start(self.start_field_change) - self.doPoll() - return Done + self.start_state(self.start_field_change, target=target, mode=self.mode) + return target - def write_mode(self, value): - self.mode = value - if not self._state.is_active: - self._state.start(self.start_field_change) - self.doPoll() - return Done - - def cleanup_state(self, state): - self.status = Status.ERROR, repr(state.last_error) - self.log.error('in state %s: %r', state.state.__name__, state.last_error) - self.setFastPoll(False) + def onError(self, state): if self.switch_heater != 0: self.persistent_field = self.read_value() - if self.mode != Mode.DRIVEN: + if state.mode != Mode.DRIVEN: self.log.warning('turn switch heater off') self.write_switch_heater(0) + return super().onError(state) - def stop(self): - """keep field at current value""" - # let the state machine do the needed steps to finish - self.write_target(self.value) - + @status_code('PREPARING') def start_field_change(self, state): self.setFastPoll(True, 1.0) - self.status = Status.PREPARING, 'changed target field' - if (self.target == self._last_target and - abs(self.target - self.persistent_field) <= self.tolerance): # short cut + if state.target == self.persistent_field or ( + state.target == self._last_target and + abs(state.target - self.persistent_field) <= self.tolerance): # short cut return self.check_switch_off + if self.switch_heater: + return self.start_switch_on return self.start_ramp_to_field + @status_code('PREPARING') def start_ramp_to_field(self, state): """start ramping current to persistent field - should return ramp_to_field + initiate ramp to persistent field (with corresponding ramp rate) + the implementation should return ramp_to_field """ raise NotImplementedError + @status_code('PREPARING', 'ramp leads to match field') def ramp_to_field(self, state): - """ramping, wait for current at persistent field""" - if (self.target == self._last_target and - abs(self.target - self.persistent_field) <= self.tolerance): # short cut - return self.check_switch_off - if abs(self.current - self.persistent_field) > self.tolerance: - if state.init: - self.status = Status.PREPARING, 'ramping leads current to field' - return Retry() - state.stabilize_start = time.time() + if state.init: + state.stabilize_start = 0 + progress = self.get_progress(state, self.ramp) + dif = abs(self.current - self.persistent_field) + if dif > self.tolerance: + if progress: + state.stabilize_start = None + return Retry() + raise HardwareError('no progress') + if state.stabilize_start is None: + state.stabilize_start = state.now return self.stabilize_current + @status_code('PREPARING') def stabilize_current(self, state): - """wait for stable current at persistent field""" - if state.now - state.stabilize_start < self.wait_stable_leads: - if state.init: - self.status = Status.PREPARING, 'stabilizing leads current' + if state.now - state.stabilize_start < max(state.tol_time, self.wait_stable_leads): return Retry() return self.start_switch_on @@ -189,13 +239,14 @@ class Magfield(HasLimits, Drivable): """is called whenever switch heater was changed""" switch_time = self.switch_time[value] if switch_time is None: + self.log.info('restart switch_timer %r', value) switch_time = time.time() self.switch_time = [None, None] self.switch_time[value] = switch_time + @status_code('PREPARING') def start_switch_on(self, state): - """switch heater on""" - if self.switch_heater == 0: + if self.read_switch_heater() == 0: self.status = Status.PREPARING, 'turn switch heater on' try: self.write_switch_heater(True) @@ -204,76 +255,69 @@ class Magfield(HasLimits, Drivable): return Retry() else: self.status = Status.PREPARING, 'wait for heater on' - return self.switch_on + return self.wait_for_switch_on - def switch_on(self, state): - """wait for switch heater open""" - if (self.target == self._last_target and - abs(self.target - self.persistent_field) <= self.tolerance): # short cut + @status_code('PREPARING') + def wait_for_switch_on(self, state): + if (state.target == self._last_target and + abs(state.target - self.persistent_field) <= self.tolerance): # short cut return self.check_switch_off - self.read_switch_heater() + self.read_switch_heater() # trigger switch_time setting if self.switch_time[ON] is None: self.log.warning('switch turned off manually?') return self.start_switch_on if state.now - self.switch_time[ON] < self.wait_switch_on: + if state.delta(10): + self.log.info('waited for %g sec', state.now - self.switch_time[ON]) return Retry() - self._last_target = self.target + self._last_target = state.target return self.start_ramp_to_target + @status_code('RAMPING') def start_ramp_to_target(self, state): - """start ramping current to target + """start ramping current to target field - should return ramp_to_target + initiate ramp to target + the implementation should return ramp_to_target """ raise NotImplementedError + @status_code('RAMPING') def ramp_to_target(self, state): - """ramp field to target""" - if self.target != self._last_target: # target was changed - self._last_target = self.target - return self.start_ramp_to_target + if state.init: + state.stabilize_start = 0 self.persistent_field = self.value + dif = abs(self.value - state.target) # Remarks: assume there is a ramp limiting feature - if abs(self.value - self.target) > self.tolerance: - if state.init: - self.status = Status.RAMPING, 'ramping field' - return Retry() - state.stabilize_start = time.time() + if dif > self.tolerance: + if self.get_progress(state, self.ramp * 0.001): + state.stabilize_start = None + return Retry() + raise HardwareError('no progress') + if state.stabilize_start is None: + state.stabilize_start = state.now return self.stabilize_field + @status_code('STABILIZING') def stabilize_field(self, state): - """stabilize field""" - if self.target != self._last_target: # target was changed - self._last_target = self.target - return self.start_ramp_to_target self.persistent_field = self.value - if state.now - state.stabilize_start < self.wait_stable_field: - if state.init: - self.status = Status.STABILIZING, 'stabilizing field' + if state.now - state.stabilize_start < max(state.tol_time, self.wait_stable_field): return Retry() return self.check_switch_off def check_switch_off(self, state): - if self.mode == Mode.DRIVEN: - self.status = Status.PREPARED, 'driven' - return self.finish_state + if state.mode == Mode.DRIVEN: + return self.final_status(Status.PREPARED, 'driven') return self.start_switch_off + @status_code('FINALIZING') def start_switch_off(self, state): - """turn off switch heater""" if self.switch_heater == 1: - self.status = Status.FINALIZING, 'turn switch heater off' self.write_switch_heater(False) - else: - self.status = Status.FINALIZING, 'wait for heater off' - return self.switch_off + return self.wait_for_switch_off - def switch_off(self, state): - """wait for switch heater closed""" - if self.target != self._last_target or self.mode == Mode.DRIVEN: - # target or mode has changed -> redo - self._last_target = None - return self.start_switch_on + @status_code('FINALIZING') + def wait_for_switch_off(self, state): self.persistent_field = self.value self.read_switch_heater() if self.switch_time[OFF] is None: @@ -282,35 +326,25 @@ class Magfield(HasLimits, Drivable): if state.now - self.switch_time[OFF] < self.wait_switch_off: return Retry() if abs(self.value) > self.persistent_limit: - self.status = Status.IDLE, 'leads current at field, switch off' - return self.finish_state + return self.final_status(Status.IDLE, 'leads current at field, switch off') return self.start_ramp_to_zero + @status_code('FINALIZING') def start_ramp_to_zero(self, state): - """start ramping current to target + """start ramping current to zero initiate ramp to zero (with corresponding ramp rate) - should return ramp_to_zero + the implementation should return ramp_to_zero """ raise NotImplementedError + @status_code('FINALIZING') def ramp_to_zero(self, state): - """ramp field to zero""" - if self.target != self._last_target or self.mode == Mode.DRIVEN: - # target or mode has changed -> redo - self._last_target = None - return self.start_field_change + """[FINALIZING] ramp field to zero""" if abs(self.current) > self.tolerance: - if state.init: - self.status = Status.FINALIZING, 'ramp leads to zero' - return Retry() - if self.mode == Mode.DISABLED and self.persistent_field == 0: - self.status = Status.DISABLED, 'disabled' - else: - self.status = Status.IDLE, 'persistent mode' - return self.finish_state - - def finish_state(self, state): - """finish""" - self.setFastPoll(False) - return None + if self.get_progress(state, self.ramp): + return Retry() + raise HardwareError('no progress') + if state.mode == Mode.DISABLED and self.persistent_field == 0: + return self.final_status(Status.DISABLED, 'disabled') + return self.final_status(Status.IDLE, 'persistent mode') diff --git a/secop_psi/mercury.py b/secop_psi/mercury.py index deb4a9e..a3fb9b8 100644 --- a/secop_psi/mercury.py +++ b/secop_psi/mercury.py @@ -62,6 +62,7 @@ fast_slow = Mapped(ON=0, OFF=1) # maps OIs slow=ON/fast=OFF to sample_rate.slow class IO(StringIO): identification = [('*IDN?', r'IDN:OXFORD INSTRUMENTS:*')] + timeout = 5 class MercuryChannel(HasIO): diff --git a/secop_psi/motorvalve.py b/secop_psi/motorvalve.py index 0c7344f..5479a23 100644 --- a/secop_psi/motorvalve.py +++ b/secop_psi/motorvalve.py @@ -253,9 +253,10 @@ class MotorValve(PersistentMixin, Drivable): return self.close_valve def handle_error(self, state): - if state.stopped: # stop or restart case - if state.stopped is Stop: - self.status = WARN, 'stopped' + if state.restarted: + return + if state.stopped: + self.status = WARN, 'stopped' return None if state.count > 0: state.count -= 1 diff --git a/secop_psi/uniax.py b/secop_psi/uniax.py index e4e0813..51e2728 100644 --- a/secop_psi/uniax.py +++ b/secop_psi/uniax.py @@ -26,7 +26,7 @@ import math from secop.core import Drivable, Parameter, FloatRange, Done, \ Attached, Command, PersistentMixin, PersistentParam, BoolType from secop.errors import BadValueError, SECoPError -from secop.lib.statemachine import Retry, StateMachine, Restart +from secop.lib.statemachine import Retry, StateMachine class Error(SECoPError): @@ -186,9 +186,9 @@ class Uniax(PersistentMixin, Drivable): def cleanup(self, state): """in case of error, set error status""" - if state.stopped: # stop or restart - if state.stopped is Restart: - return + if state.restarted: + return + if state.stopped: self.status = 'IDLE', 'stopped' self.log.warning('stopped') else: diff --git a/secop_psi/vector.py b/secop_psi/vector.py new file mode 100644 index 0000000..ff143ab --- /dev/null +++ b/secop_psi/vector.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# ***************************************************************************** +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""generic 3D vector""" + +from secop.core import Attached, Drivable, Readable, Parameter +from secop.datatypes import FloatRange, TupleOf, StatusType, Enum + + +class VectorRd(Readable): + """generic readable vector""" + value = Parameter(datatype=TupleOf(FloatRange(), FloatRange(), FloatRange())) + x = Attached() + y = Attached() + z = Attached() + pollFuncs = None + components = None + + def initModule(self): + super().initModule() + members = [] + status_codes = {} # collect all possible status codes + components = [] + for name in 'xyz': + component = getattr(self, name) + members.append(component.parameters['value'].datatype.copy()) + components.append(component) + for code in component.status[0].enum.members: + status_codes[int(code)] = code.name + self.parameters['value'].datatype = TupleOf(*members) + self.parameters['status'].datatype = StatusType(Enum( + 'status', **{k: v for v, k in status_codes.items()})) + self.components = components + + def doPoll(self): + for component in self.components: + component.doPoll() + # update + component.pollInfo.last_main = self.pollInfo.last_main + self.value = self.merge_value() + self.status = self.merge_status() + + def merge_value(self): + return [c.value for c in self.components] + + def merge_status(self): + status = -1, '' + for c in self.components: + if c.status[0] > status[0]: + status = c.status + return status + + def read_value(self): + return tuple((c.read_value() for c in self.components)) + + def read_status(self): + [c.read_status() for c in self.components] + return self.merge_status() + + +class Vector(Drivable, VectorRd): + """generic drivable vector""" + target = Parameter(datatype=TupleOf(FloatRange(), FloatRange(), FloatRange())) + + def initModule(self): + super().initModule() + members = [] + for component in self.components: + members.append(component.parameters['target'].datatype.copy()) + self.parameters['target'].datatype = TupleOf(*members) + + def write_target(self, value): + return tuple((c.write_target(v) for v, c in zip(value, self.components))) \ No newline at end of file