state on dilsc as of 2022-10-03
vector field, but no new state machine yet
This commit is contained in:
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user