improved cyoltd (flame magnet)

- use a statemachine
- do more control in frappy, switching from persistent to driven
  internally
- a lot of fixes
This commit is contained in:
l_samenv 2022-12-21 11:00:19 +01:00
parent 766f15beee
commit ad4a6d2e09
2 changed files with 162 additions and 88 deletions

View File

@ -10,10 +10,11 @@ io= cio
[B]
description = 'magnetic field'
class = secop_psi.cryoltd.MainMagfield
class = secop_psi.cryoltd.MainField
channel = Main
constraint = 80000
target.max = 35000
mode = 'PERSISTENT'
hw_units = T
# A_to_G is needed for ramp rate
A_to_G = 285.73
@ -21,6 +22,7 @@ ramp.max = 412
overshoot = {'o': 1, 't': 180}
degauss = {'s': 500, 'd': 30, 'f': 5, 't': 120}
tolerance = 5
wait_stable_field = 180
[Bx]
description = 'magnetic field x component'

View File

@ -29,10 +29,12 @@ changed from the client is fixed for at least 10 seconds.
import re
import time
from secop.core import HasIO, StringIO, Readable, Drivable, Parameter, Command, \
Module, Property, Attached, Enum, IDLE, BUSY, ERROR
from secop.features import HasLimits
Module, Property, Attached, Enum, IDLE, BUSY, ERROR, Done
from secop.errors import ConfigError, BadValueError, HardwareError
from secop.datatypes import FloatRange, StringType, EnumType, StructOf, OrType
from secop.datatypes import FloatRange, StringType, EnumType, StructOf
from secop.states import HasStates, status_code, Retry
from secop.features import HasTargetLimits
import secop_psi.magfield as magfield
# floating point value followed with unit
VALUE_UNIT = re.compile(r'([-0-9.E]*\d|inf)([A-Za-z/%]*)$')
@ -135,10 +137,10 @@ class Channel:
cmd = cmd.replace('<CH>', self.channel)
reply = self.main.communicate(cmd)
if not reply.startswith(cmd):
print('MISMATCH', cmd, reply)
self.log.warn('reply %r does not match command %r', reply, cmd)
def block(self, pname, value=None):
self.block_until[pname] = time.time() + 10
def block(self, pname, value=None, delay=10):
self.block_until[pname] = time.time() + delay
if value is not None:
setattr(self, pname, value)
@ -183,14 +185,26 @@ class Temperature(Channel, Readable):
self.main.register_module(self, value=self.channel)
class Magfield(HasLimits, Channel, Drivable):
CsMode = Enum(
PERSISTENT=1,
SEMIPERSISTENT=2,
DRIVEN=0,
)
PersistencyMode = Enum(
DISABLED=0,
PERSISTENT=30,
SEMIPERSISTENT=31,
DRIVEN=50,
)
class BaseMagfield(HasStates, HasTargetLimits, Channel):
_status_text = ''
_ready_text = ''
_error_text = ''
_last_error = ''
_rate_units = ''
_next_target = None
_last_target = None
hw_units = Property('hardware units: A or T', EnumType(A=0, T=1))
A_to_G = Property('A_to_G = "Gauss Value / Amps Value"', FloatRange(0))
tolerance = Parameter('tolerance', FloatRange(0), readonly=False, default=1)
@ -201,10 +215,18 @@ class Magfield(HasLimits, Channel, Drivable):
if dt.min < 1e-99: # make limits symmetric
dt.min = - dt.max
min_, max_ = self.target_limits
self.target_limits = [max(min_, dt.min), max_]
self.target_limits = [max(min_, dt.min), min(max_, dt.max)]
dt = self.parameters['ramp'].datatype
if self.ramp == 0: # unconfigured: take max.
self.ramp = dt.max
if not isinstance(self, magfield.Magfield):
# add unneeded attributes, as the appear in GetAll
self.switch_heater = None
self.current = None
def doPoll(self):
super().doPoll() # does not call cycle_machine
self.cycle_machine()
def to_gauss(self, value):
value, unit = VALUE_UNIT.match(value).groups()
@ -237,57 +259,34 @@ class Magfield(HasLimits, Channel, Drivable):
_rate_units=('<CH>_Rate Units', str),
current=('<CH>_PSU Output', self.to_gauss),
voltage='<CH>_Voltage',
working_ramp=('<CH>_Ramp Rate', self.to_gauss_min),
workingramp=('<CH>_Ramp Rate', self.to_gauss_min),
setpoint=('<CH>_Setpoint', self.to_gauss),
switch_heater=('<CH>_Heater', self.cvt_switch_heater),
mode=('<CH>_Persistent Mode', self.cvt_mode),
cs_mode=('<CH>_Persistent Mode', self.cvt_cs_mode),
approach_mode=('<CH>_Approach', self.cvt_approach_mode),
)
def cvt_error(self, text):
if text != self._last_error:
self._last_error = text
self.log.error(text)
return text
return self._error_text
def trigger_update(self):
# called after treating result of GetAll message
if self._error_text:
status = ERROR, '%s while %s' % (self._error_text, self._status_text)
elif self._ready_text == 'TRUE':
with self.accessLock: # must not be in parallel with write_target
target = self._next_target
if target is not None: # target change pending
if target == self._last_target and abs(self.value - target) <= self.tolerance:
# we are already there
self._last_target = None
status = IDLE, self._status_text
else:
if self.hw_units == 'A':
self.sendcmd('Set:<CH>:Sweep %gA' % (target / self.A_to_G))
else:
self.sendcmd('Set:<CH>:Sweep %gT' % (target / 10000))
self._next_target = None
self._last_target = target
status = BUSY, 'changed target'
else:
status = IDLE, self._status_text
elif self._next_target is None:
txt = self._status_text
if txt.startswith('Ramping Magnet'):
if txt.endswith(': ') or ' 1 seconds' in txt:
txt = 'stabilizing'
else:
txt = 'ramping'
status = BUSY, txt
else:
return # do not change status when aborting
self.status = status
if self.status[0] == ERROR:
errtxt = self._error_text
else:
errtxt = '%s while %s' % (self._error_text, self.status[1])
# self.stop_machine((ERROR, errtxt))
value = Parameter('magnetic field in the coil', FloatRange(unit='G'))
setpoint = Parameter('setpoint', FloatRange(unit='G'), default=0)
ramp = Parameter('ramp rate', FloatRange(0, unit='$/min'), default=0, readonly=False)
ramp = Parameter()
target = Parameter()
def write_ramp(self, ramp):
if self._rate_units != 'A/s':
@ -297,48 +296,63 @@ class Magfield(HasLimits, Channel, Drivable):
def write_target(self, target):
self.reset_error()
self.check_limits(target)
self.write_ramp(self.ramp)
super().write_target(target)
return Done
def start_sweep(self, target):
if self.approach_mode == self.approach_mode.OVERSHOOT:
o = self.overshoot['o']
if (target - self.value) * o < 0:
self.write_overshoot(dict(self.overshoot, o=-o))
self.block('_error_text', '')
if self._ready_text == 'FALSE':
if target != self._last_target or abs(self.value - target) > self.tolerance:
self.status = BUSY, 'aborting'
self.sendcmd('Set:<CH>:Abort')
self._next_target = target
self.write_ramp(self.ramp)
if self.hw_units == 'A':
self.sendcmd('Set:<CH>:Sweep %gA' % (target / self.A_to_G))
else:
self._next_target = target
self.trigger_update() # update status
return target
self.sendcmd('Set:<CH>:Sweep %gT' % (target / 10000))
self.block('_ready_text', 'FALSE')
working_ramp = Parameter('actual ramp rate', FloatRange(0, unit='$/min'))
def start_field_change(self, sm):
if self._ready_text == 'FALSE':
self.sendcmd('Set:<CH>:Abort')
return self.wait_ready
return super().start_field_change
Mode = Enum(
# DISABLED=0,
PERSISTENT=30,
SEMIPERSISTENT=31,
DRIVEN=50,
)
mode = Parameter('persistent mode', EnumType(Mode), readonly=False, default=30)
mode_map = Mapped(DRIVEN=0, PERSISTENT=1, SEMIPERSISTENT=2)
def wait_ready(self, sm):
if self._ready_text == 'FALSE':
return Retry
return super().start_field_change
def write_mode(self, value):
code = self.mode_map(value)
self.sendcmd('Set:<CH>:SetPM %d' % code)
self.block('mode')
return value
def start_ramp_to_target(self, sm):
self.start_sweep(sm.target)
return self.ramp_to_target # -> stabilize_field
def stabilize_field(self, sm):
if self._ready_text == 'FALSE':
# wait for overshoot/degauss/cycle
sm.stabilize_start = sm.now
return Retry
if sm.now - sm.stabilize_start < self.wait_stable_field:
return Retry
return self.end_stablilize
def end_stablilize(self, sm):
return self.final_status()
cs_mode = Parameter('persistent mode', EnumType(CsMode), readonly=False, default=0)
@staticmethod
def cvt_mode(text):
def cvt_cs_mode(text):
text = text.lower()
if 'off' in text:
if '0' in text:
return 30
return 31
return 50
return CsMode.PERSISTENT
return CsMode.SEMIPERSISTENT
return CsMode.DRIVEN
def write_cs_mode(self, value):
self.sendcmd('Set:<CH>:SetPM %d' % int(value))
self.block('cs_mode')
return value
ApproachMode = Enum(
DIRECT=0,
@ -393,12 +407,8 @@ class Magfield(HasLimits, Channel, Drivable):
(value['s'] * 1e-4, value['d'], value['f'] * 1e-4, value['t']))
return value
current = Parameter(
'leads current (in units of field)', FloatRange(unit='$'))
voltage = Parameter(
'voltage over leads', FloatRange(unit='V'))
switch_heater = Parameter(
'voltage over leads', EnumType(OFF=0, ON=1))
@staticmethod
def cvt_switch_heater(text):
@ -419,29 +429,94 @@ class Magfield(HasLimits, Channel, Drivable):
self._error_text = ''
class MainMagfield(Magfield):
class MainField(BaseMagfield, magfield.Magfield):
checked_modules = None
def earlyInit(self):
super().earlyInit()
self.checked_modules = []
def check_limits(self, value):
super().check_limits(value)
self.check_combined(None, 0, value)
# TODO: turn into a property
constraint = Parameter('product check', FloatRange(unit='G^2'), default=80000)
def check_combined(self, obj, value, main_target):
sumvalue2 = sum((max(o.value ** 2, value ** 2 if o == obj else 0)
sumvalue2 = sum(((value ** 2 if o == obj else o.value ** 2)
for o in self.checked_modules))
if sumvalue2 * max(self.value ** 2, main_target) > self.constraint ** 2:
raise BadValueError('outside constraint (B * Bxyz > %g G^2' * self.constraint)
if sumvalue2 * max(self.value ** 2, main_target ** 2) > self.constraint ** 2:
raise BadValueError('outside constraint (B * Bxyz > %g G^2' % self.constraint)
def check_limits(self, value):
super().check_limits(value)
self.check_combined(None, 0, value)
mode = Parameter(datatype=EnumType(PersistencyMode))
class ComponentField(Magfield):
check_against = Attached(MainMagfield)
def write_mode(self, mode):
self.reset_error()
super().write_mode(mode) # updates mode
return Done
@status_code('PREPARING')
def start_ramp_to_field(self, sm):
self.start_sweep(self.value)
return self.ramp_to_field # -> stabilize_current -> start_switch_on
@status_code('PREPARING')
def start_switch_on(self, sm):
self.write_cs_mode(CsMode.DRIVEN)
# self.block('switch_heater', 1, 60)
self.start_sweep(self.value)
self.block('_ready_text', 'FALSE')
return self.wait_for_switch_on # -> start_ramp_to_target -> ramp_to_target -> stabilize_field
@status_code('PREPARING')
def wait_for_switch_on(self, sm):
if self._ready_text == 'FALSE':
return Retry
self.last_target(sm.target)
return self.start_ramp_to_target
def end_stablilize(self, sm):
return self.start_switch_off
@status_code('FINALIZING')
def start_switch_off(self, sm):
if self.mode == PersistencyMode.DRIVEN:
return self.final_status(IDLE, 'driven')
if self.mode == PersistencyMode.SEMIPERSISTENT:
self.write_cs_mode(CsMode.SEMIPERSISTENT)
else: # PERSISTENT or DISABLED
self.write_cs_mode(CsMode.PERSISTENT)
self.start_sweep(sm.target)
self.block('_ready_text', 'FALSE')
self.block('switch_heater', 1)
return self.wait_for_switch_off # -> start_ramp_to_zero
@status_code('PREPARING')
def wait_for_switch_off(self, sm):
if self.switch_heater:
return Retry
self.last_target(sm.target)
if self.mode == PersistencyMode.SEMIPERSISTENT:
return self.final_status(IDLE, 'semipersistent')
return self.ramp_to_zero
@status_code('FINALIZING')
def ramp_to_zero(self, sm):
if self._ready_text == 'FALSE':
return Retry
if self.mode == PersistencyMode.DISABLED:
return self.final_status(PersistencyMode.DISABLED, 'disabled')
return self.final_status(IDLE, 'persistent')
class ComponentField(BaseMagfield, magfield.SimpleMagfield):
check_against = Attached(MainField)
# status = Parameter(datatype=EnumType(Drivable.Status, RAMPING=370, STABILIZING=380, FINALIZING=390))
def initModule(self):
super().initModule()
@ -495,9 +570,9 @@ class Compressor(Channel, Drivable):
def write_target(self, value):
if value:
self.sendcmd('SetCompressor:Start <CH>')
self.sendcmd('Set:Compressor:Start <CH>')
else:
self.sendcmd('SetCompressor:Stop <CH>')
self.sendcmd('Set:Compressor:Stop <CH>')
self.block('target')
self.read_status()
return value
@ -507,6 +582,3 @@ class Compressor(Channel, Drivable):
"""reset error"""
self.sendcmd('Set:Compressor:Reset <CH>')
self._error_text = ''