state on dilsc as of 2022-10-03

vector field, but no new state machine yet
This commit is contained in:
2022-11-21 14:51:02 +01:00
parent 485e81bfb0
commit 9636dc9cea
13 changed files with 1086 additions and 310 deletions

View File

@@ -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')