Compare commits

...

5 Commits

Author SHA1 Message Date
1111af5c1a dilsc.cfg: add heater to puck 2024-04-17 08:22:30 +02:00
efa357d6c3 ips_mercury.SimpleField0.on_restart dead locks
- use unlocke_write_action
2023-03-15 16:36:34 +01:00
4d704612fc ips_mercury.SimpleField0.on_restart dead locks
- use unlocke_write_action
2023-03-15 16:34:52 +01:00
04755c5173 dilsc version with corrected 3D limits 2023-02-15 14:31:29 +01:00
9636dc9cea state on dilsc as of 2022-10-03
vector field, but no new state machine yet
2022-11-21 14:51:02 +01:00
15 changed files with 1088 additions and 293 deletions

285
cfg/dilsc.cfg Normal file
View 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

View File

@ -3,3 +3,4 @@
logdir = ./log
piddir = ./pid
confdir = ./cfg
# comlog = True

52
cfg/magsc.cfg Normal file
View 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

View File

@ -39,7 +39,7 @@ class HasOffset(Feature):
implementation to be done in the subclass
"""
offset = PersistentParam('offset (physical value + offset = HW value)',
FloatRange(unit='deg'), readonly=False, default=0)
FloatRange(unit='$'), readonly=False, default=0)
def write_offset(self, value):
self.offset = value
@ -62,9 +62,9 @@ class HasLimits(Feature):
except for the offset
"""
abslimits = Property('abs limits (raw values)', default=(-9e99, 9e99), extname='abslimits', export=True,
datatype=TupleOf(FloatRange(unit='deg'), FloatRange(unit='deg')))
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')))
limits = PersistentParam('user limits', readonly=False, default=(-9e99, 9e99),
datatype=TupleOf(FloatRange(unit='deg'), FloatRange(unit='deg')))
datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')))
_limits = None
def apply_offset(self, sign, *values):

View File

@ -30,7 +30,7 @@ The created state object may hold variables needed for the state.
A state function may return either:
- a function for the next state to transition to
- Retry(<delay>) to keep the state and call the
- or `None` for finishing
- or `Finish` for finishing
Initialisation Code
@ -44,33 +44,23 @@ def state_x(stateobj):
... further code ...
Cleanup Function
----------------
Error Handler
-------------
cleanup=<cleanup function> as argument in StateMachine.__init__ or .start
defines a cleanup function to be called whenever the machine is stopped or
an error is raised in a state function. A cleanup function may return
either None for finishing or a further state function for continuing.
In case of stop or restart, this return value is ignored.
State Specific Cleanup Code
---------------------------
To execute state specific cleanup, the cleanup may examine the current state
(stateobj.state) in order to decide what to be done.
If a need arises, a future extension to this library may support specific
cleanup functions by means of a decorator adding the specific cleanup function
as an attribute to the state function.
handler=<handler object> as argument in StateMachine.__init__ or .start
defines a handler object to be called whenever the machine is stopped,
restarted. finished or an error is raised in a state function.
<handler>.on_error may return either None for finishing or a further state
function for continuing. The other handler methods always return None,
as there is no useful follow up state.
Threaded Use
------------
On start, a thread is started, which is waiting for a trigger event when the
machine is not active. For test purposes or special needs, the thread creation
may be disabled. :meth:`cycle` must be called periodically in this case.
machine is not active. In case the thread creation is disabled. :meth:`cycle`
must be called periodically for running the state machine.
"""
import time
@ -80,6 +70,7 @@ from logging import getLogger
from secop.lib import mkthread, UniqueObject
Finish = UniqueObject('Finish')
Stop = UniqueObject('Stop')
Restart = UniqueObject('Restart')
@ -89,6 +80,52 @@ class Retry:
self.delay = delay
class StateHandler:
"""default handlers
may be used as base class or mixin for implementing custom handlers
"""
def on_error(self, statemachine, exc):
"""called on error
:param statemachine: the state machine object
:param exc: the exception
:return: None or a state function to be executed for handling the error state
"""
statemachine.log.warning('%r raised in state %r', exc, statemachine.status_string)
def on_transition(self, statemachine, newstate):
"""called when state is changed
:param statemachine: the statemachine
:param newstate: the new state function
this method will not be called when the state is changed to None,
e.g. on finish, restart, stop or when None is returned from the error handler
"""
def on_restart(self, statemachine):
"""called on restart
:param statemachine: the state machine object
"""
def on_stop(self, statemachine):
"""called when stopped
:param statemachine: the state machine object
"""
def on_finish(self, statemachine):
"""called on finish
:param statemachine: the state machine object
"""
default_handler = StateHandler()
class StateMachine:
"""a simple, but powerful state machine"""
# class attributes are not allowed to be overriden by kwds of __init__ or :meth:`start`
@ -98,7 +135,7 @@ class StateMachine:
now = None
init = True
stopped = False
last_error = None # last exception raised or Stop or Restart
restarted = False
_last_time = 0
def __init__(self, state=None, logger=None, threaded=True, **kwds):
@ -109,9 +146,9 @@ class StateMachine:
:param threaded: whether a thread should be started (default: True)
:param kwds: any attributes for the state object
"""
self.default_delay = 0.25 # default delay when returning None
self.default_delay = 0.25 # default delay when returning Retry(None)
self.now = time.time() # avoid calling time.time several times per state
self.cleanup = self.default_cleanup # default cleanup: finish on error
self.handler = default_handler
self.log = logger or getLogger('dummy')
self._update_attributes(kwds)
self._lock = threading.RLock()
@ -120,23 +157,9 @@ class StateMachine:
self._thread_queue = queue.Queue()
self._idle_event = threading.Event()
self._thread = None
self._restart = None
if state:
self.start(state)
@staticmethod
def default_cleanup(state):
"""default cleanup
:param self: the state object
:return: None (for custom cleanup functions this might be a new state)
"""
if state.stopped: # stop or restart
verb = 'stopped' if state.stopped is Stop else 'restarted'
state.log.debug('%s in state %r', verb, state.status_string)
else:
state.log.warning('%r raised in state %r', state.last_error, state.status_string)
def _update_attributes(self, kwds):
"""update allowed attributes"""
cls = type(self)
@ -168,10 +191,12 @@ class StateMachine:
def _new_state(self, state):
self.state = state
self.init = True
self.now = time.time()
self.transition_time = self.now
self.log.debug('state: %s', self.status_string)
if state:
self.handler.on_transition(self, state)
self.init = True
self.now = time.time()
self.transition_time = self.now
def cycle(self):
"""do one cycle in the thread loop
@ -187,18 +212,26 @@ class StateMachine:
ret = self.state(self)
self.init = False
if self.stopped:
self.last_error = self.stopped
self.cleanup(self)
self.log.debug('stopped in state %r', self.status_string)
self.handler.on_stop(self)
self.stopped = False
ret = None
elif self.restarted:
self.log.debug('restarted in state %r', self.status_string)
self.handler.on_restart(self)
self.restarted = False
ret = None
except Exception as e:
self.last_error = e
ret = self.cleanup(self)
self.log.debug('called %r %sexc=%r', self.cleanup,
'ret=%r ' % ret if ret else '', e)
if ret is None:
self.log.debug('state: None after cleanup')
self.state = None
try:
ret = self.handler.on_error(self, e)
self.log.debug('called on_error with exc=%r%s', e,
' ret=%r' % ret if ret else '')
except Exception as ee:
self.log.exception('%r raised in on_error(state, %r)', ee, e)
if ret is Finish:
self.log.debug('finish in state %r', self.status_string)
self.handler.on_finish(self)
self._new_state(None)
self._idle_event.set()
return None
if callable(ret):
@ -210,12 +243,13 @@ class StateMachine:
if ret.delay is None:
return self.default_delay
return ret.delay
self.last_error = RuntimeError('return value must be callable, Retry(...) or finish')
self.handler.on_error(self, RuntimeError(
'return value must be callable, Retry(...) or Finish, not %r' % ret))
break
else:
self.last_error = RuntimeError('too many states chained - probably infinite loop')
self.cleanup(self)
self.state = None
self.handler.on_error(self, RuntimeError(
'too many states chained - probably infinite loop'))
self._new_state(None)
return None
def trigger(self, delay=0):
@ -238,9 +272,8 @@ class StateMachine:
delay = self.cycle()
def _start(self, state, **kwds):
self._restart = None
self._idle_event.clear()
self.last_error = None
self.restarted = False
self.stopped = False
self._update_attributes(kwds)
self._new_state(state)
@ -259,19 +292,18 @@ class StateMachine:
"""start with a new state
and interrupt the current state
the cleanup function will be called with state.stopped=Restart
the cleanup function will be called with state.restarted = True
:param state: the first state
:param kwds: items to put as attributes on the state machine
"""
self.log.debug('start %r', kwds)
if self.state:
self.stopped = Restart
self.restarted = True
with self._lock: # wait for running cycle finished
if self.stopped: # cleanup is not yet done
self.last_error = self.stopped
self.cleanup(self) # ignore return state on restart
self.stopped = False
if self.restarted: # on_restart is not yet called
self.handler.on_restart(self)
self.restarted = False
self._start(state, **kwds)
else:
self._start(state, **kwds)
@ -279,16 +311,15 @@ class StateMachine:
def stop(self):
"""stop machine, go to idle state
the cleanup function will be called with state.stopped=Stop
the cleanup function will be called with state.stopped = True
"""
self.log.debug('stop')
self.stopped = Stop
self.stopped = True
with self._lock:
if self.stopped: # cleanup is not yet done
self.last_error = self.stopped
self.cleanup(self) # ignore return state on restart
if self.stopped: # on_stop is not yet called
self.handler.on_stop(self)
self.stopped = False
self.state = None
self._new_state(None)
def wait(self, timeout=None):
"""wait for state machine being idle"""

View File

@ -46,6 +46,14 @@ Done = UniqueObject('Done')
indicating that the setter is triggered already"""
class DummyLock:
def __enter__(self):
return self
def __exit__(self, *args, **kwds):
return True
class HasAccessibles(HasProperties):
"""base class of Module
@ -327,6 +335,7 @@ class Module(HasAccessibles):
self.initModuleDone = False
self.startModuleDone = False
self.remoteLogHandler = None
# self.accessLock = self.updateLock = DummyLock()
self.accessLock = threading.RLock() # for read_* / write_* methods
self.updateLock = threading.RLock() # for announceUpdate
self.polledModules = [] # modules polled by thread started in self.startModules

110
secop/states.py Normal file
View 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

View File

@ -21,7 +21,7 @@
# *****************************************************************************
from secop.core import Parameter, FloatRange, BUSY, IDLE, WARN
from secop.lib.statemachine import StateMachine, Retry, Stop
from secop.lib.statemachine import StateMachine, Retry
class HasConvergence:
@ -61,9 +61,8 @@ class HasConvergence:
def cleanup(self, state):
state.default_cleanup(state)
if state.stopped:
if state.stopped is Stop: # and not Restart
self.status = WARN, 'stopped'
else:
self.status = WARN, 'stopped'
elif not state.restarted:
self.status = WARN, repr(state.last_error)
def doPoll(self):

95
secop_psi/dilsc.py Normal file
View 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()

View File

@ -21,11 +21,12 @@
"""oxford instruments mercury IPS power supply"""
import time
from secop.core import Parameter, EnumType, FloatRange, BoolType
from secop.core import Parameter, EnumType, FloatRange, BoolType, IntRange, StringType, Property, BUSY
from secop.lib.enum import Enum
from secop.errors import BadValueError, HardwareError
from secop_psi.magfield import Magfield
from secop_psi.magfield import Magfield, SimpleMagfield, Status
from secop_psi.mercury import MercuryChannel, off_on, Mapped
from secop.lib.statemachine import Retry
Action = Enum(hold=0, run_to_set=1, run_to_zero=2, clamped=3)
hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set,
@ -33,24 +34,87 @@ hold_rtoz_rtos_clmp = Mapped(HOLD=Action.hold, RTOS=Action.run_to_set,
CURRENT_CHECK_SIZE = 2
class Field(MercuryChannel, Magfield):
class SimpleField0(MercuryChannel, SimpleMagfield):
nunits = Property('number of IPS subunits', IntRange(1, 6), default=1)
action = Parameter('action', EnumType(Action), readonly=False)
setpoint = Parameter('field setpoint', FloatRange(unit='T'), default=0)
voltage = Parameter('leads voltage', FloatRange(unit='V'), default=0)
atob = Parameter('field to amp', FloatRange(0, unit='A/T'), default=0)
I1 = Parameter('master current', FloatRange(unit='A'), default=0)
I2 = Parameter('slave 2 current', FloatRange(unit='A'), default=0)
I3 = Parameter('slave 3 current', FloatRange(unit='A'), default=0)
V1 = Parameter('master voltage', FloatRange(unit='V'), default=0)
V2 = Parameter('slave 2 voltage', FloatRange(unit='V'), default=0)
V3 = Parameter('slave 3 voltage', FloatRange(unit='V'), default=0)
working_ramp = Parameter('effective ramp', FloatRange(0, unit='T/min'), default=0)
channel_type = 'PSU'
slave_currents = None
def read_value(self):
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(
'manual indication that persistent field is bad', BoolType(), readonly=False, default=False)
channel_type = 'PSU'
_field_mismatch = None
nslaves = 3
slave_currents = None
__init = True
__reset_switch_time = False
@ -58,8 +122,15 @@ class Field(MercuryChannel, Magfield):
super().doPoll()
self.read_current()
def startModule(self, start_events):
self.switch_time = [0, 0]
self.switch_heater = self.query('PSU:SIG:SWHT', off_on)
super().startModule(start_events)
# on restart, assume switch is changed long time ago, if not, the mercury
# # will complain and this will be handled in start_ramp_to_field
def read_value(self):
self.current = self.query('PSU:SIG:FLD')
current = self.query('PSU:SIG:FLD')
pf = self.query('PSU:SIG:PFLD')
if self.__init:
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:
self.forced_persistent_field = False
self._field_mismatch = False
return self.current
return current
self._field_mismatch = abs(self.persistent_field - pf) > self.tolerance
return pf
def read_current(self):
if self.slave_currents is None:
self.slave_currents = [[] for _ in range(self.nunits + 1)]
current = self.query('PSU:SIG:CURR')
if self.nunits > 1:
for i in range(self.nunits + 1):
if i:
curri = self.query('DEV:PSU.M%d:PSU:SIG:CURR' % i)
volti = self.query('DEV:PSU.M%d:PSU:SIG:VOLT' % i)
setattr(self, 'I%d' % i, curri)
setattr(self, 'V%d' % i, volti)
self.slave_currents[i].append(curri)
else:
self.slave_currents[i].append(current)
min_i = min(self.slave_currents[i])
max_i = max(self.slave_currents[i])
min_ = min(self.slave_currents[0]) / self.nunits
max_ = max(self.slave_currents[0]) / self.nunits
if len(self.slave_currents[i]) > CURRENT_CHECK_SIZE:
self.slave_currents[i] = self.slave_currents[i][-CURRENT_CHECK_SIZE:]
if i and (min_i - 1 > max_ or min_ > max_i + 1):
self.log.warning('individual currents mismatch %r', self.slave_currents)
if self.atob:
return current / self.atob
return 0
def write_persistent_field(self, value):
if self.forced_persistent_field:
self._field_mismatch = False
@ -83,18 +180,6 @@ class Field(MercuryChannel, Magfield):
raise BadValueError('persistent field does not match - set persistent field to guessed value first')
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):
value = self.query('PSU:SIG:SWHT', off_on)
now = time.time()
@ -106,74 +191,77 @@ class Field(MercuryChannel, Magfield):
return self.switch_heater
elif self.__reset_switch_time:
self.__reset_switch_time = False
self.switch_time = [None, None]
self.switch_time[value] = now
return value
def read_wait_switch_on(self):
return self.query('PSU:SWONT') * 0.001
def read_wait_switch_off(self):
return self.query('PSU:SWOFT') * 0.001
def write_switch_heater(self, value):
if value == self.read_switch_heater():
self.log.info('switch heater already %r', value)
# we do not want to restart the timer
return value
return self.change('PSU:SIG:SWHT', value, off_on)
def read_atob(self):
return self.query('PSU:ATOB')
def read_voltage(self):
return self.query('PSU:SIG:VOLT')
def read_setpoint(self):
return self.query('PSU:SIG:FSET')
def read_current(self):
if self.slave_currents is None:
self.slave_currents = [[] for _ in range(self.nslaves + 1)]
current = self.query('PSU:SIG:CURR')
for i in range(self.nslaves + 1):
if i:
curri = self.query('DEV:PSU.M%d:PSU:SIG:CURR' % i)
volti = self.query('DEV:PSU.M%d:PSU:SIG:VOLT' % i)
setattr(self, 'I%d' % i, curri)
setattr(self, 'V%d' % i, volti)
self.slave_currents[i].append(curri)
else:
self.slave_currents[i].append(current)
min_i = min(self.slave_currents[i])
max_i = max(self.slave_currents[i])
min_ = min(self.slave_currents[0]) / self.nslaves
max_ = max(self.slave_currents[0]) / self.nslaves
if len(self.slave_currents[i]) > CURRENT_CHECK_SIZE:
self.slave_currents[i] = self.slave_currents[i][-CURRENT_CHECK_SIZE:]
if i and (min_i -1 > max_ or min_ > max_i + 1):
self.log.warning('individual currents mismatch %r', self.slave_currents)
if self.atob:
return current / self.atob
return 0
def set_and_go(self, value):
self.change('PSU:SIG:FSET', value)
assert self.write_action('hold') == 'hold'
assert self.write_action('run_to_set') == 'run_to_set'
def start_ramp_to_field(self, state):
if abs(self.current - self.persistent_field) <= self.tolerance:
self.log.info('leads %g are already at %g', self.current, self.persistent_field)
return self.ramp_to_field
try:
self.set_and_go(self.persistent_field)
except (HardwareError, AssertionError):
state.switch_undef = self.switch_on_time or state.now
except (HardwareError, AssertionError) as e:
if self.switch_heater:
self.log.warn('switch is already on!')
return self.ramp_to_field
self.log.warn('wait first for switch off current=%g pf=%g', self.current, self.persistent_field)
return Retry()
self.status = Status.PREPARING, 'wait for switch off'
state.wait_for = 0
return self.wait_for_switch
return self.ramp_to_field
def wait_for_switch(self, state):
if self.now - self.switch_undef < self.wait_switch_on:
return Retry()
self.set_and_go(self.persistent_field)
return self.ramp_to_field
def start_ramp_to_target(self, state):
self.set_and_go(self.target)
state.try_cnt = 5
try:
self.set_and_go(state.target)
except (HardwareError, AssertionError) as e:
self.log.warn('switch not yet ready %r', e)
self.status = Status.PREPARING, 'wait for switch on'
state.wait_for = 1
return self.wait_for_switch
return self.ramp_to_target
def wait_for_switch(self, state):
if not state.delta(10): # wait at least 10 seconds
return Retry()
try:
# try again
self.set_and_go(self.persistent_field)
except (HardwareError, AssertionError) as e:
return Retry()
return self.ramp_to_target if state.wait_for else self.ramp_to_field
def start_ramp_to_zero(self, state):
assert self.write_action('hold') == 'hold'
assert self.write_action('run_to_zero') == 'run_to_zero'
return self.ramp_to_zero
def finish_state(self, state):
self.write_action('hold')
super().finish_state(state)
def Field(name, logger, cfgdict, srv, base=Field0):
nunits = cfgdict.get('nunits', 1)
if nunits == 1:
return base(name, logger, cfgdict, srv)
# create individual current and voltage parameters dynamically
attrs = {}
for i in range(1, nunits + 1):
attrs['I%d' % i] = Parameter('slave %s current' % i, FloatRange(unit='A'), default=0)
attrs['V%d' % i] = Parameter('slave %s voltage' % i, FloatRange(unit='V'), default=0)
return type(base.__name__.replace('0', str(nunits)), (base,), attrs)(name, logger, cfgdict, srv)
def SimpleField(name, logger, cfgdict, srv):
return Field(name, logger, cfgdict, srv, SimpleField0)

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

View File

@ -62,6 +62,7 @@ fast_slow = Mapped(ON=0, OFF=1) # maps OIs slow=ON/fast=OFF to sample_rate.slow
class IO(StringIO):
identification = [('*IDN?', r'IDN:OXFORD INSTRUMENTS:*')]
timeout = 5
class MercuryChannel(HasIO):

View File

@ -253,9 +253,10 @@ class MotorValve(PersistentMixin, Drivable):
return self.close_valve
def handle_error(self, state):
if state.stopped: # stop or restart case
if state.stopped is Stop:
self.status = WARN, 'stopped'
if state.restarted:
return
if state.stopped:
self.status = WARN, 'stopped'
return None
if state.count > 0:
state.count -= 1

View File

@ -26,7 +26,7 @@ import math
from secop.core import Drivable, Parameter, FloatRange, Done, \
Attached, Command, PersistentMixin, PersistentParam, BoolType
from secop.errors import BadValueError, SECoPError
from secop.lib.statemachine import Retry, StateMachine, Restart
from secop.lib.statemachine import Retry, StateMachine
class Error(SECoPError):
@ -186,9 +186,9 @@ class Uniax(PersistentMixin, Drivable):
def cleanup(self, state):
"""in case of error, set error status"""
if state.stopped: # stop or restart
if state.stopped is Restart:
return
if state.restarted:
return
if state.stopped:
self.status = 'IDLE', 'stopped'
self.log.warning('stopped')
else:

89
secop_psi/vector.py Normal file
View 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)))