Compare commits
5 Commits
wip
...
dilsc_feb2
Author | SHA1 | Date | |
---|---|---|---|
1111af5c1a | |||
efa357d6c3 | |||
4d704612fc | |||
04755c5173 | |||
9636dc9cea |
285
cfg/dilsc.cfg
Normal file
285
cfg/dilsc.cfg
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
[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.HeaterOutputWithRange
|
||||||
|
description = mix. chamber heater
|
||||||
|
slot = H1,TA
|
||||||
|
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
|
||||||
|
|
||||||
|
[p_ovc]
|
||||||
|
class = secop_psi.mercury.PressureSensor
|
||||||
|
description = outer vacuum pressure
|
||||||
|
slot = P6
|
||||||
|
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.6
|
||||||
|
cylinders = ((0.23, 5.2), (0.45, 0.8))
|
||||||
|
|
||||||
|
[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
|
@ -3,3 +3,4 @@
|
|||||||
logdir = ./log
|
logdir = ./log
|
||||||
piddir = ./pid
|
piddir = ./pid
|
||||||
confdir = ./cfg
|
confdir = ./cfg
|
||||||
|
# comlog = True
|
||||||
|
52
cfg/magsc.cfg
Normal file
52
cfg/magsc.cfg
Normal file
@ -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
|
@ -39,7 +39,7 @@ class HasOffset(Feature):
|
|||||||
implementation to be done in the subclass
|
implementation to be done in the subclass
|
||||||
"""
|
"""
|
||||||
offset = PersistentParam('offset (physical value + offset = HW value)',
|
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):
|
def write_offset(self, value):
|
||||||
self.offset = value
|
self.offset = value
|
||||||
@ -62,9 +62,9 @@ class HasLimits(Feature):
|
|||||||
except for the offset
|
except for the offset
|
||||||
"""
|
"""
|
||||||
abslimits = Property('abs limits (raw values)', default=(-9e99, 9e99), extname='abslimits', export=True,
|
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),
|
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
|
_limits = None
|
||||||
|
|
||||||
def apply_offset(self, sign, *values):
|
def apply_offset(self, sign, *values):
|
||||||
|
@ -30,7 +30,7 @@ The created state object may hold variables needed for the state.
|
|||||||
A state function may return either:
|
A state function may return either:
|
||||||
- a function for the next state to transition to
|
- a function for the next state to transition to
|
||||||
- Retry(<delay>) to keep the state and call the
|
- Retry(<delay>) to keep the state and call the
|
||||||
- or `None` for finishing
|
- or `Finish` for finishing
|
||||||
|
|
||||||
|
|
||||||
Initialisation Code
|
Initialisation Code
|
||||||
@ -44,33 +44,23 @@ def state_x(stateobj):
|
|||||||
... further code ...
|
... further code ...
|
||||||
|
|
||||||
|
|
||||||
Cleanup Function
|
Error Handler
|
||||||
----------------
|
-------------
|
||||||
|
|
||||||
cleanup=<cleanup function> as argument in StateMachine.__init__ or .start
|
handler=<handler object> as argument in StateMachine.__init__ or .start
|
||||||
defines a cleanup function to be called whenever the machine is stopped or
|
defines a handler object to be called whenever the machine is stopped,
|
||||||
an error is raised in a state function. A cleanup function may return
|
restarted. finished or an error is raised in a state function.
|
||||||
either None for finishing or a further state function for continuing.
|
<handler>.on_error may return either None for finishing or a further state
|
||||||
In case of stop or restart, this return value is ignored.
|
function for continuing. The other handler methods always return None,
|
||||||
|
as there is no useful follow up state.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
Threaded Use
|
Threaded Use
|
||||||
------------
|
------------
|
||||||
|
|
||||||
On start, a thread is started, which is waiting for a trigger event when the
|
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
|
machine is not active. In case the thread creation is disabled. :meth:`cycle`
|
||||||
may be disabled. :meth:`cycle` must be called periodically in this case.
|
must be called periodically for running the state machine.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
@ -80,6 +70,7 @@ from logging import getLogger
|
|||||||
from secop.lib import mkthread, UniqueObject
|
from secop.lib import mkthread, UniqueObject
|
||||||
|
|
||||||
|
|
||||||
|
Finish = UniqueObject('Finish')
|
||||||
Stop = UniqueObject('Stop')
|
Stop = UniqueObject('Stop')
|
||||||
Restart = UniqueObject('Restart')
|
Restart = UniqueObject('Restart')
|
||||||
|
|
||||||
@ -89,6 +80,52 @@ class Retry:
|
|||||||
self.delay = delay
|
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:
|
class StateMachine:
|
||||||
"""a simple, but powerful state machine"""
|
"""a simple, but powerful state machine"""
|
||||||
# class attributes are not allowed to be overriden by kwds of __init__ or :meth:`start`
|
# class attributes are not allowed to be overriden by kwds of __init__ or :meth:`start`
|
||||||
@ -98,7 +135,7 @@ class StateMachine:
|
|||||||
now = None
|
now = None
|
||||||
init = True
|
init = True
|
||||||
stopped = False
|
stopped = False
|
||||||
last_error = None # last exception raised or Stop or Restart
|
restarted = False
|
||||||
_last_time = 0
|
_last_time = 0
|
||||||
|
|
||||||
def __init__(self, state=None, logger=None, threaded=True, **kwds):
|
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 threaded: whether a thread should be started (default: True)
|
||||||
:param kwds: any attributes for the state object
|
: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.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.log = logger or getLogger('dummy')
|
||||||
self._update_attributes(kwds)
|
self._update_attributes(kwds)
|
||||||
self._lock = threading.RLock()
|
self._lock = threading.RLock()
|
||||||
@ -120,23 +157,9 @@ class StateMachine:
|
|||||||
self._thread_queue = queue.Queue()
|
self._thread_queue = queue.Queue()
|
||||||
self._idle_event = threading.Event()
|
self._idle_event = threading.Event()
|
||||||
self._thread = None
|
self._thread = None
|
||||||
self._restart = None
|
|
||||||
if state:
|
if state:
|
||||||
self.start(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):
|
def _update_attributes(self, kwds):
|
||||||
"""update allowed attributes"""
|
"""update allowed attributes"""
|
||||||
cls = type(self)
|
cls = type(self)
|
||||||
@ -168,10 +191,12 @@ class StateMachine:
|
|||||||
|
|
||||||
def _new_state(self, state):
|
def _new_state(self, state):
|
||||||
self.state = state
|
self.state = state
|
||||||
|
self.log.debug('state: %s', self.status_string)
|
||||||
|
if state:
|
||||||
|
self.handler.on_transition(self, state)
|
||||||
self.init = True
|
self.init = True
|
||||||
self.now = time.time()
|
self.now = time.time()
|
||||||
self.transition_time = self.now
|
self.transition_time = self.now
|
||||||
self.log.debug('state: %s', self.status_string)
|
|
||||||
|
|
||||||
def cycle(self):
|
def cycle(self):
|
||||||
"""do one cycle in the thread loop
|
"""do one cycle in the thread loop
|
||||||
@ -187,18 +212,26 @@ class StateMachine:
|
|||||||
ret = self.state(self)
|
ret = self.state(self)
|
||||||
self.init = False
|
self.init = False
|
||||||
if self.stopped:
|
if self.stopped:
|
||||||
self.last_error = self.stopped
|
self.log.debug('stopped in state %r', self.status_string)
|
||||||
self.cleanup(self)
|
self.handler.on_stop(self)
|
||||||
self.stopped = False
|
self.stopped = False
|
||||||
ret = None
|
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:
|
except Exception as e:
|
||||||
self.last_error = e
|
try:
|
||||||
ret = self.cleanup(self)
|
ret = self.handler.on_error(self, e)
|
||||||
self.log.debug('called %r %sexc=%r', self.cleanup,
|
self.log.debug('called on_error with exc=%r%s', e,
|
||||||
'ret=%r ' % ret if ret else '', e)
|
' ret=%r' % ret if ret else '')
|
||||||
if ret is None:
|
except Exception as ee:
|
||||||
self.log.debug('state: None after cleanup')
|
self.log.exception('%r raised in on_error(state, %r)', ee, e)
|
||||||
self.state = None
|
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()
|
self._idle_event.set()
|
||||||
return None
|
return None
|
||||||
if callable(ret):
|
if callable(ret):
|
||||||
@ -210,12 +243,13 @@ class StateMachine:
|
|||||||
if ret.delay is None:
|
if ret.delay is None:
|
||||||
return self.default_delay
|
return self.default_delay
|
||||||
return ret.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
|
break
|
||||||
else:
|
else:
|
||||||
self.last_error = RuntimeError('too many states chained - probably infinite loop')
|
self.handler.on_error(self, RuntimeError(
|
||||||
self.cleanup(self)
|
'too many states chained - probably infinite loop'))
|
||||||
self.state = None
|
self._new_state(None)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def trigger(self, delay=0):
|
def trigger(self, delay=0):
|
||||||
@ -238,9 +272,8 @@ class StateMachine:
|
|||||||
delay = self.cycle()
|
delay = self.cycle()
|
||||||
|
|
||||||
def _start(self, state, **kwds):
|
def _start(self, state, **kwds):
|
||||||
self._restart = None
|
|
||||||
self._idle_event.clear()
|
self._idle_event.clear()
|
||||||
self.last_error = None
|
self.restarted = False
|
||||||
self.stopped = False
|
self.stopped = False
|
||||||
self._update_attributes(kwds)
|
self._update_attributes(kwds)
|
||||||
self._new_state(state)
|
self._new_state(state)
|
||||||
@ -259,19 +292,18 @@ class StateMachine:
|
|||||||
"""start with a new state
|
"""start with a new state
|
||||||
|
|
||||||
and interrupt the current 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 state: the first state
|
||||||
:param kwds: items to put as attributes on the state machine
|
:param kwds: items to put as attributes on the state machine
|
||||||
"""
|
"""
|
||||||
self.log.debug('start %r', kwds)
|
self.log.debug('start %r', kwds)
|
||||||
if self.state:
|
if self.state:
|
||||||
self.stopped = Restart
|
self.restarted = True
|
||||||
with self._lock: # wait for running cycle finished
|
with self._lock: # wait for running cycle finished
|
||||||
if self.stopped: # cleanup is not yet done
|
if self.restarted: # on_restart is not yet called
|
||||||
self.last_error = self.stopped
|
self.handler.on_restart(self)
|
||||||
self.cleanup(self) # ignore return state on restart
|
self.restarted = False
|
||||||
self.stopped = False
|
|
||||||
self._start(state, **kwds)
|
self._start(state, **kwds)
|
||||||
else:
|
else:
|
||||||
self._start(state, **kwds)
|
self._start(state, **kwds)
|
||||||
@ -279,16 +311,15 @@ class StateMachine:
|
|||||||
def stop(self):
|
def stop(self):
|
||||||
"""stop machine, go to idle state
|
"""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.log.debug('stop')
|
||||||
self.stopped = Stop
|
self.stopped = True
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if self.stopped: # cleanup is not yet done
|
if self.stopped: # on_stop is not yet called
|
||||||
self.last_error = self.stopped
|
self.handler.on_stop(self)
|
||||||
self.cleanup(self) # ignore return state on restart
|
|
||||||
self.stopped = False
|
self.stopped = False
|
||||||
self.state = None
|
self._new_state(None)
|
||||||
|
|
||||||
def wait(self, timeout=None):
|
def wait(self, timeout=None):
|
||||||
"""wait for state machine being idle"""
|
"""wait for state machine being idle"""
|
||||||
|
@ -46,6 +46,14 @@ Done = UniqueObject('Done')
|
|||||||
indicating that the setter is triggered already"""
|
indicating that the setter is triggered already"""
|
||||||
|
|
||||||
|
|
||||||
|
class DummyLock:
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args, **kwds):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class HasAccessibles(HasProperties):
|
class HasAccessibles(HasProperties):
|
||||||
"""base class of Module
|
"""base class of Module
|
||||||
|
|
||||||
@ -327,6 +335,7 @@ class Module(HasAccessibles):
|
|||||||
self.initModuleDone = False
|
self.initModuleDone = False
|
||||||
self.startModuleDone = False
|
self.startModuleDone = False
|
||||||
self.remoteLogHandler = None
|
self.remoteLogHandler = None
|
||||||
|
# self.accessLock = self.updateLock = DummyLock()
|
||||||
self.accessLock = threading.RLock() # for read_* / write_* methods
|
self.accessLock = threading.RLock() # for read_* / write_* methods
|
||||||
self.updateLock = threading.RLock() # for announceUpdate
|
self.updateLock = threading.RLock() # for announceUpdate
|
||||||
self.polledModules = [] # modules polled by thread started in self.startModules
|
self.polledModules = [] # modules polled by thread started in self.startModules
|
||||||
|
110
secop/states.py
Normal file
110
secop/states.py
Normal file
@ -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 <markus.zolliker@psi.ch>
|
||||||
|
#
|
||||||
|
# *****************************************************************************
|
||||||
|
"""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
|
@ -21,7 +21,7 @@
|
|||||||
# *****************************************************************************
|
# *****************************************************************************
|
||||||
|
|
||||||
from secop.core import Parameter, FloatRange, BUSY, IDLE, WARN
|
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:
|
class HasConvergence:
|
||||||
@ -61,9 +61,8 @@ class HasConvergence:
|
|||||||
def cleanup(self, state):
|
def cleanup(self, state):
|
||||||
state.default_cleanup(state)
|
state.default_cleanup(state)
|
||||||
if state.stopped:
|
if state.stopped:
|
||||||
if state.stopped is Stop: # and not Restart
|
|
||||||
self.status = WARN, 'stopped'
|
self.status = WARN, 'stopped'
|
||||||
else:
|
elif not state.restarted:
|
||||||
self.status = WARN, repr(state.last_error)
|
self.status = WARN, repr(state.last_error)
|
||||||
|
|
||||||
def doPoll(self):
|
def doPoll(self):
|
||||||
|
95
secop_psi/dilsc.py
Normal file
95
secop_psi/dilsc.py
Normal file
@ -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 <markus.zolliker@psi.ch>
|
||||||
|
# *****************************************************************************
|
||||||
|
"""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. sphere', 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()
|
@ -21,11 +21,12 @@
|
|||||||
"""oxford instruments mercury IPS power supply"""
|
"""oxford instruments mercury IPS power supply"""
|
||||||
|
|
||||||
import time
|
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.lib.enum import Enum
|
||||||
from secop.errors import BadValueError, HardwareError
|
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_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)
|
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,
|
hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set,
|
||||||
@ -33,24 +34,87 @@ hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set,
|
|||||||
CURRENT_CHECK_SIZE = 2
|
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)
|
action = Parameter('action', EnumType(Action), readonly=False)
|
||||||
setpoint = Parameter('field setpoint', FloatRange(unit='T'), default=0)
|
setpoint = Parameter('field setpoint', FloatRange(unit='T'), default=0)
|
||||||
voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0)
|
voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0)
|
||||||
atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0)
|
atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0)
|
||||||
I1 = Parameter('master current', FloatRange(unit='A'), default=0)
|
working_ramp = Parameter('effective ramp', FloatRange(0, unit='T/min'), default=0)
|
||||||
I2 = Parameter('slave 2 current', FloatRange(unit='A'), default=0)
|
channel_type = 'PSU'
|
||||||
I3 = Parameter('slave 3 current', FloatRange(unit='A'), default=0)
|
slave_currents = None
|
||||||
V1 = Parameter('master voltage', FloatRange(unit='V'), default=0)
|
|
||||||
V2 = Parameter('slave 2 voltage', FloatRange(unit='V'), default=0)
|
def read_value(self):
|
||||||
V3 = Parameter('slave 3 voltage', FloatRange(unit='V'), default=0)
|
return self.query('PSU:SIG:FLD')
|
||||||
|
|
||||||
|
def read_ramp(self):
|
||||||
|
return self.query('PSU:SIG:RFST')
|
||||||
|
|
||||||
|
def write_ramp(self, value):
|
||||||
|
return self.change('PSU:SIG:RFST', value)
|
||||||
|
|
||||||
|
def read_action(self):
|
||||||
|
return self.query('PSU:ACTN', hold_rtoz_rtos_clmp)
|
||||||
|
|
||||||
|
def unlocked_write_action(self, value):
|
||||||
|
return self.change('PSU:ACTN', value, hold_rtoz_rtos_clmp)
|
||||||
|
|
||||||
|
def write_action(self, value):
|
||||||
|
return self.unlocked_write_action(value)
|
||||||
|
|
||||||
|
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.unlocked_write_action(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(
|
forced_persistent_field = Parameter(
|
||||||
'manual indication that persistent field is bad', BoolType(), readonly=False, default=False)
|
'manual indication that persistent field is bad', BoolType(), readonly=False, default=False)
|
||||||
|
|
||||||
channel_type = 'PSU'
|
|
||||||
_field_mismatch = None
|
_field_mismatch = None
|
||||||
nslaves = 3
|
|
||||||
slave_currents = None
|
|
||||||
__init = True
|
__init = True
|
||||||
__reset_switch_time = False
|
__reset_switch_time = False
|
||||||
|
|
||||||
@ -58,8 +122,15 @@ class Field(MercuryChannel, Magfield):
|
|||||||
super().doPoll()
|
super().doPoll()
|
||||||
self.read_current()
|
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):
|
def read_value(self):
|
||||||
self.current = self.query('PSU:SIG:FLD')
|
current = self.query('PSU:SIG:FLD')
|
||||||
pf = self.query('PSU:SIG:PFLD')
|
pf = self.query('PSU:SIG:PFLD')
|
||||||
if self.__init:
|
if self.__init:
|
||||||
self.__init = False
|
self.__init = False
|
||||||
@ -67,10 +138,36 @@ class Field(MercuryChannel, Magfield):
|
|||||||
if self.switch_heater == self.switch_heater.on or self._field_mismatch is None:
|
if self.switch_heater == self.switch_heater.on or self._field_mismatch is None:
|
||||||
self.forced_persistent_field = False
|
self.forced_persistent_field = False
|
||||||
self._field_mismatch = False
|
self._field_mismatch = False
|
||||||
return self.current
|
return current
|
||||||
self._field_mismatch = abs(self.persistent_field - pf) > self.tolerance
|
self._field_mismatch = abs(self.persistent_field - pf) > self.tolerance
|
||||||
return pf
|
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):
|
def write_persistent_field(self, value):
|
||||||
if self.forced_persistent_field:
|
if self.forced_persistent_field:
|
||||||
self._field_mismatch = False
|
self._field_mismatch = False
|
||||||
@ -83,18 +180,6 @@ class Field(MercuryChannel, Magfield):
|
|||||||
raise BadValueError('persistent field does not match - set persistent field to guessed value first')
|
raise BadValueError('persistent field does not match - set persistent field to guessed value first')
|
||||||
return super().write_target(target)
|
return super().write_target(target)
|
||||||
|
|
||||||
def read_ramp(self):
|
|
||||||
return self.query('PSU:SIG:RFST')
|
|
||||||
|
|
||||||
def write_ramp(self, value):
|
|
||||||
return self.change('PSU:SIG:RFST', value)
|
|
||||||
|
|
||||||
def read_action(self):
|
|
||||||
return self.query('PSU:ACTN', hold_rtoz_rtos_clmp)
|
|
||||||
|
|
||||||
def write_action(self, value):
|
|
||||||
return self.change('PSU:ACTN', value, hold_rtoz_rtos_clmp)
|
|
||||||
|
|
||||||
def read_switch_heater(self):
|
def read_switch_heater(self):
|
||||||
value = self.query('PSU:SIG:SWHT', off_on)
|
value = self.query('PSU:SIG:SWHT', off_on)
|
||||||
now = time.time()
|
now = time.time()
|
||||||
@ -106,74 +191,77 @@ class Field(MercuryChannel, Magfield):
|
|||||||
return self.switch_heater
|
return self.switch_heater
|
||||||
elif self.__reset_switch_time:
|
elif self.__reset_switch_time:
|
||||||
self.__reset_switch_time = False
|
self.__reset_switch_time = False
|
||||||
self.switch_time = [None, None]
|
self.switch_time[value] = now
|
||||||
return value
|
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):
|
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)
|
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):
|
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:
|
try:
|
||||||
self.set_and_go(self.persistent_field)
|
self.set_and_go(self.persistent_field)
|
||||||
except (HardwareError, AssertionError):
|
except (HardwareError, AssertionError) as e:
|
||||||
state.switch_undef = self.switch_on_time or state.now
|
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.wait_for_switch
|
||||||
return self.ramp_to_field
|
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):
|
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
|
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):
|
def start_ramp_to_zero(self, state):
|
||||||
assert self.write_action('hold') == 'hold'
|
assert self.write_action('hold') == 'hold'
|
||||||
assert self.write_action('run_to_zero') == 'run_to_zero'
|
assert self.write_action('run_to_zero') == 'run_to_zero'
|
||||||
return self.ramp_to_zero
|
return self.ramp_to_zero
|
||||||
|
|
||||||
def finish_state(self, state):
|
|
||||||
self.write_action('hold')
|
def Field(name, logger, cfgdict, srv, base=Field0):
|
||||||
super().finish_state(state)
|
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)
|
@ -20,12 +20,12 @@
|
|||||||
"""generic persistent magnet driver"""
|
"""generic persistent magnet driver"""
|
||||||
|
|
||||||
import time
|
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.datatypes import FloatRange, EnumType, ArrayOf, TupleOf, StatusType
|
||||||
from secop.features import HasLimits
|
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.enum import Enum
|
||||||
from secop.lib.statemachine import Retry, StateMachine
|
from secop.states import Retry, HasStates, status_code
|
||||||
|
|
||||||
UNLIMITED = FloatRange()
|
UNLIMITED = FloatRange()
|
||||||
|
|
||||||
@ -48,25 +48,98 @@ OFF = 0
|
|||||||
ON = 1
|
ON = 1
|
||||||
|
|
||||||
|
|
||||||
class Magfield(HasLimits, Drivable):
|
class SimpleMagfield(HasStates, HasLimits, Drivable):
|
||||||
value = Parameter('magnetic field', datatype=FloatRange(unit='T'))
|
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))
|
status = Parameter(datatype=StatusType(Status))
|
||||||
mode = Parameter(
|
mode = Parameter(
|
||||||
'persistent mode', EnumType(Mode), readonly=False, default=Mode.PERSISTENT)
|
'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),
|
switch_heater = Parameter('switch heater', EnumType(off=OFF, on=ON),
|
||||||
readonly=False, default=0)
|
readonly=False, default=0)
|
||||||
persistent_field = Parameter(
|
persistent_field = Parameter(
|
||||||
'persistent field', FloatRange(unit='$'), readonly=False)
|
'persistent field', FloatRange(unit='$'), readonly=False)
|
||||||
current = Parameter(
|
current = Parameter(
|
||||||
'leads current (in units of field)', FloatRange(unit='$'))
|
'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
|
# TODO: time_to_target
|
||||||
# profile = Parameter(
|
# profile = Parameter(
|
||||||
# 'ramp limit table', ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))),
|
# '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 time to ensure switch is off', FloatRange(0, unit='s'), readonly=False, default=61)
|
||||||
wait_stable_leads = Parameter(
|
wait_stable_leads = Parameter(
|
||||||
'wait time to ensure current is stable', FloatRange(0, unit='s'), readonly=False, default=6)
|
'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(
|
persistent_limit = Parameter(
|
||||||
'above this limit, lead currents are not driven to 0',
|
'above this limit, lead currents are not driven to 0',
|
||||||
FloatRange(0, unit='$'), readonly=False, default=99)
|
FloatRange(0, unit='$'), readonly=False, default=99)
|
||||||
|
|
||||||
_state = None
|
|
||||||
__init = True
|
__init = True
|
||||||
_last_target = None
|
|
||||||
switch_time = None, None
|
switch_time = None, None
|
||||||
|
|
||||||
def doPoll(self):
|
def doPoll(self):
|
||||||
@ -102,86 +171,67 @@ class Magfield(HasLimits, Drivable):
|
|||||||
else:
|
else:
|
||||||
self._last_target = self.persistent_field
|
self._last_target = self.persistent_field
|
||||||
else:
|
else:
|
||||||
self.read_value()
|
super().doPoll()
|
||||||
self._state.cycle()
|
|
||||||
|
|
||||||
def checkProperties(self):
|
def initStateMachine(self):
|
||||||
dt = self.parameters['target'].datatype
|
super().initStateMachine()
|
||||||
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()
|
|
||||||
self.registerCallbacks(self) # for update_switch_heater
|
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):
|
def write_target(self, target):
|
||||||
self.check_limits(target)
|
self.check_limits(target)
|
||||||
self.target = target
|
self.start_state(self.start_field_change, target=target, mode=self.mode)
|
||||||
if not self._state.is_active:
|
return target
|
||||||
# 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
|
|
||||||
|
|
||||||
def write_mode(self, value):
|
def onError(self, state):
|
||||||
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)
|
|
||||||
if self.switch_heater != 0:
|
if self.switch_heater != 0:
|
||||||
self.persistent_field = self.read_value()
|
self.persistent_field = self.read_value()
|
||||||
if self.mode != Mode.DRIVEN:
|
if state.mode != Mode.DRIVEN:
|
||||||
self.log.warning('turn switch heater off')
|
self.log.warning('turn switch heater off')
|
||||||
self.write_switch_heater(0)
|
self.write_switch_heater(0)
|
||||||
|
return super().onError(state)
|
||||||
|
|
||||||
def stop(self):
|
@status_code('PREPARING')
|
||||||
"""keep field at current value"""
|
|
||||||
# let the state machine do the needed steps to finish
|
|
||||||
self.write_target(self.value)
|
|
||||||
|
|
||||||
def start_field_change(self, state):
|
def start_field_change(self, state):
|
||||||
self.setFastPoll(True, 1.0)
|
self.setFastPoll(True, 1.0)
|
||||||
self.status = Status.PREPARING, 'changed target field'
|
if state.target == self.persistent_field or (
|
||||||
if (self.target == self._last_target and
|
state.target == self._last_target and
|
||||||
abs(self.target - self.persistent_field) <= self.tolerance): # short cut
|
abs(state.target - self.persistent_field) <= self.tolerance): # short cut
|
||||||
return self.check_switch_off
|
return self.check_switch_off
|
||||||
|
if self.switch_heater:
|
||||||
|
return self.start_switch_on
|
||||||
return self.start_ramp_to_field
|
return self.start_ramp_to_field
|
||||||
|
|
||||||
|
@status_code('PREPARING')
|
||||||
def start_ramp_to_field(self, state):
|
def start_ramp_to_field(self, state):
|
||||||
"""start ramping current to persistent field
|
"""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
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@status_code('PREPARING', 'ramp leads to match field')
|
||||||
def ramp_to_field(self, state):
|
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:
|
if state.init:
|
||||||
self.status = Status.PREPARING, 'ramping leads current to field'
|
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()
|
return Retry()
|
||||||
state.stabilize_start = time.time()
|
raise HardwareError('no progress')
|
||||||
|
if state.stabilize_start is None:
|
||||||
|
state.stabilize_start = state.now
|
||||||
return self.stabilize_current
|
return self.stabilize_current
|
||||||
|
|
||||||
|
@status_code('PREPARING')
|
||||||
def stabilize_current(self, state):
|
def stabilize_current(self, state):
|
||||||
"""wait for stable current at persistent field"""
|
if state.now - state.stabilize_start < max(state.tol_time, self.wait_stable_leads):
|
||||||
if state.now - state.stabilize_start < self.wait_stable_leads:
|
|
||||||
if state.init:
|
|
||||||
self.status = Status.PREPARING, 'stabilizing leads current'
|
|
||||||
return Retry()
|
return Retry()
|
||||||
return self.start_switch_on
|
return self.start_switch_on
|
||||||
|
|
||||||
@ -189,13 +239,14 @@ class Magfield(HasLimits, Drivable):
|
|||||||
"""is called whenever switch heater was changed"""
|
"""is called whenever switch heater was changed"""
|
||||||
switch_time = self.switch_time[value]
|
switch_time = self.switch_time[value]
|
||||||
if switch_time is None:
|
if switch_time is None:
|
||||||
|
self.log.info('restart switch_timer %r', value)
|
||||||
switch_time = time.time()
|
switch_time = time.time()
|
||||||
self.switch_time = [None, None]
|
self.switch_time = [None, None]
|
||||||
self.switch_time[value] = switch_time
|
self.switch_time[value] = switch_time
|
||||||
|
|
||||||
|
@status_code('PREPARING')
|
||||||
def start_switch_on(self, state):
|
def start_switch_on(self, state):
|
||||||
"""switch heater on"""
|
if self.read_switch_heater() == 0:
|
||||||
if self.switch_heater == 0:
|
|
||||||
self.status = Status.PREPARING, 'turn switch heater on'
|
self.status = Status.PREPARING, 'turn switch heater on'
|
||||||
try:
|
try:
|
||||||
self.write_switch_heater(True)
|
self.write_switch_heater(True)
|
||||||
@ -204,76 +255,69 @@ class Magfield(HasLimits, Drivable):
|
|||||||
return Retry()
|
return Retry()
|
||||||
else:
|
else:
|
||||||
self.status = Status.PREPARING, 'wait for heater on'
|
self.status = Status.PREPARING, 'wait for heater on'
|
||||||
return self.switch_on
|
return self.wait_for_switch_on
|
||||||
|
|
||||||
def switch_on(self, state):
|
@status_code('PREPARING')
|
||||||
"""wait for switch heater open"""
|
def wait_for_switch_on(self, state):
|
||||||
if (self.target == self._last_target and
|
if (state.target == self._last_target and
|
||||||
abs(self.target - self.persistent_field) <= self.tolerance): # short cut
|
abs(state.target - self.persistent_field) <= self.tolerance): # short cut
|
||||||
return self.check_switch_off
|
return self.check_switch_off
|
||||||
self.read_switch_heater()
|
self.read_switch_heater() # trigger switch_time setting
|
||||||
if self.switch_time[ON] is None:
|
if self.switch_time[ON] is None:
|
||||||
self.log.warning('switch turned off manually?')
|
self.log.warning('switch turned off manually?')
|
||||||
return self.start_switch_on
|
return self.start_switch_on
|
||||||
if state.now - self.switch_time[ON] < self.wait_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()
|
return Retry()
|
||||||
self._last_target = self.target
|
self._last_target = state.target
|
||||||
return self.start_ramp_to_target
|
return self.start_ramp_to_target
|
||||||
|
|
||||||
|
@status_code('RAMPING')
|
||||||
def start_ramp_to_target(self, state):
|
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
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@status_code('RAMPING')
|
||||||
def ramp_to_target(self, state):
|
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
|
|
||||||
self.persistent_field = self.value
|
|
||||||
# Remarks: assume there is a ramp limiting feature
|
|
||||||
if abs(self.value - self.target) > self.tolerance:
|
|
||||||
if state.init:
|
if state.init:
|
||||||
self.status = Status.RAMPING, 'ramping field'
|
state.stabilize_start = 0
|
||||||
|
self.persistent_field = self.value
|
||||||
|
dif = abs(self.value - state.target)
|
||||||
|
# Remarks: assume there is a ramp limiting feature
|
||||||
|
if dif > self.tolerance:
|
||||||
|
if self.get_progress(state, self.ramp * 0.001):
|
||||||
|
state.stabilize_start = None
|
||||||
return Retry()
|
return Retry()
|
||||||
state.stabilize_start = time.time()
|
raise HardwareError('no progress')
|
||||||
|
if state.stabilize_start is None:
|
||||||
|
state.stabilize_start = state.now
|
||||||
return self.stabilize_field
|
return self.stabilize_field
|
||||||
|
|
||||||
|
@status_code('STABILIZING')
|
||||||
def stabilize_field(self, state):
|
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
|
self.persistent_field = self.value
|
||||||
if state.now - state.stabilize_start < self.wait_stable_field:
|
if state.now - state.stabilize_start < max(state.tol_time, self.wait_stable_field):
|
||||||
if state.init:
|
|
||||||
self.status = Status.STABILIZING, 'stabilizing field'
|
|
||||||
return Retry()
|
return Retry()
|
||||||
return self.check_switch_off
|
return self.check_switch_off
|
||||||
|
|
||||||
def check_switch_off(self, state):
|
def check_switch_off(self, state):
|
||||||
if self.mode == Mode.DRIVEN:
|
if state.mode == Mode.DRIVEN:
|
||||||
self.status = Status.PREPARED, 'driven'
|
return self.final_status(Status.PREPARED, 'driven')
|
||||||
return self.finish_state
|
|
||||||
return self.start_switch_off
|
return self.start_switch_off
|
||||||
|
|
||||||
|
@status_code('FINALIZING')
|
||||||
def start_switch_off(self, state):
|
def start_switch_off(self, state):
|
||||||
"""turn off switch heater"""
|
|
||||||
if self.switch_heater == 1:
|
if self.switch_heater == 1:
|
||||||
self.status = Status.FINALIZING, 'turn switch heater off'
|
|
||||||
self.write_switch_heater(False)
|
self.write_switch_heater(False)
|
||||||
else:
|
return self.wait_for_switch_off
|
||||||
self.status = Status.FINALIZING, 'wait for heater off'
|
|
||||||
return self.switch_off
|
|
||||||
|
|
||||||
def switch_off(self, state):
|
@status_code('FINALIZING')
|
||||||
"""wait for switch heater closed"""
|
def wait_for_switch_off(self, state):
|
||||||
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
|
|
||||||
self.persistent_field = self.value
|
self.persistent_field = self.value
|
||||||
self.read_switch_heater()
|
self.read_switch_heater()
|
||||||
if self.switch_time[OFF] is None:
|
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:
|
if state.now - self.switch_time[OFF] < self.wait_switch_off:
|
||||||
return Retry()
|
return Retry()
|
||||||
if abs(self.value) > self.persistent_limit:
|
if abs(self.value) > self.persistent_limit:
|
||||||
self.status = Status.IDLE, 'leads current at field, switch off'
|
return self.final_status(Status.IDLE, 'leads current at field, switch off')
|
||||||
return self.finish_state
|
|
||||||
return self.start_ramp_to_zero
|
return self.start_ramp_to_zero
|
||||||
|
|
||||||
|
@status_code('FINALIZING')
|
||||||
def start_ramp_to_zero(self, state):
|
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)
|
initiate ramp to zero (with corresponding ramp rate)
|
||||||
should return ramp_to_zero
|
the implementation should return ramp_to_zero
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@status_code('FINALIZING')
|
||||||
def ramp_to_zero(self, state):
|
def ramp_to_zero(self, state):
|
||||||
"""ramp field to zero"""
|
"""[FINALIZING] 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
|
|
||||||
if abs(self.current) > self.tolerance:
|
if abs(self.current) > self.tolerance:
|
||||||
if state.init:
|
if self.get_progress(state, self.ramp):
|
||||||
self.status = Status.FINALIZING, 'ramp leads to zero'
|
|
||||||
return Retry()
|
return Retry()
|
||||||
if self.mode == Mode.DISABLED and self.persistent_field == 0:
|
raise HardwareError('no progress')
|
||||||
self.status = Status.DISABLED, 'disabled'
|
if state.mode == Mode.DISABLED and self.persistent_field == 0:
|
||||||
else:
|
return self.final_status(Status.DISABLED, 'disabled')
|
||||||
self.status = Status.IDLE, 'persistent mode'
|
return self.final_status(Status.IDLE, 'persistent mode')
|
||||||
return self.finish_state
|
|
||||||
|
|
||||||
def finish_state(self, state):
|
|
||||||
"""finish"""
|
|
||||||
self.setFastPoll(False)
|
|
||||||
return None
|
|
||||||
|
@ -62,6 +62,7 @@ fast_slow = Mapped(ON=0, OFF=1) # maps OIs slow=ON/fast=OFF to sample_rate.slow
|
|||||||
|
|
||||||
class IO(StringIO):
|
class IO(StringIO):
|
||||||
identification = [('*IDN?', r'IDN:OXFORD INSTRUMENTS:*')]
|
identification = [('*IDN?', r'IDN:OXFORD INSTRUMENTS:*')]
|
||||||
|
timeout = 5
|
||||||
|
|
||||||
|
|
||||||
class MercuryChannel(HasIO):
|
class MercuryChannel(HasIO):
|
||||||
|
@ -253,8 +253,9 @@ class MotorValve(PersistentMixin, Drivable):
|
|||||||
return self.close_valve
|
return self.close_valve
|
||||||
|
|
||||||
def handle_error(self, state):
|
def handle_error(self, state):
|
||||||
if state.stopped: # stop or restart case
|
if state.restarted:
|
||||||
if state.stopped is Stop:
|
return
|
||||||
|
if state.stopped:
|
||||||
self.status = WARN, 'stopped'
|
self.status = WARN, 'stopped'
|
||||||
return None
|
return None
|
||||||
if state.count > 0:
|
if state.count > 0:
|
||||||
|
@ -26,7 +26,7 @@ import math
|
|||||||
from secop.core import Drivable, Parameter, FloatRange, Done, \
|
from secop.core import Drivable, Parameter, FloatRange, Done, \
|
||||||
Attached, Command, PersistentMixin, PersistentParam, BoolType
|
Attached, Command, PersistentMixin, PersistentParam, BoolType
|
||||||
from secop.errors import BadValueError, SECoPError
|
from secop.errors import BadValueError, SECoPError
|
||||||
from secop.lib.statemachine import Retry, StateMachine, Restart
|
from secop.lib.statemachine import Retry, StateMachine
|
||||||
|
|
||||||
|
|
||||||
class Error(SECoPError):
|
class Error(SECoPError):
|
||||||
@ -186,9 +186,9 @@ class Uniax(PersistentMixin, Drivable):
|
|||||||
|
|
||||||
def cleanup(self, state):
|
def cleanup(self, state):
|
||||||
"""in case of error, set error status"""
|
"""in case of error, set error status"""
|
||||||
if state.stopped: # stop or restart
|
if state.restarted:
|
||||||
if state.stopped is Restart:
|
|
||||||
return
|
return
|
||||||
|
if state.stopped:
|
||||||
self.status = 'IDLE', 'stopped'
|
self.status = 'IDLE', 'stopped'
|
||||||
self.log.warning('stopped')
|
self.log.warning('stopped')
|
||||||
else:
|
else:
|
||||||
|
89
secop_psi/vector.py
Normal file
89
secop_psi/vector.py
Normal file
@ -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 <markus.zolliker@psi.ch>
|
||||||
|
# *****************************************************************************
|
||||||
|
"""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)))
|
Loading…
x
Reference in New Issue
Block a user