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] [B]
description = 'magnetic field' description = 'magnetic field'
class = secop_psi.cryoltd.MainMagfield class = secop_psi.cryoltd.MainField
channel = Main channel = Main
constraint = 80000 constraint = 80000
target.max = 35000 target.max = 35000
mode = 'PERSISTENT'
hw_units = T hw_units = T
# A_to_G is needed for ramp rate # A_to_G is needed for ramp rate
A_to_G = 285.73 A_to_G = 285.73
@ -21,6 +22,7 @@ ramp.max = 412
overshoot = {'o': 1, 't': 180} overshoot = {'o': 1, 't': 180}
degauss = {'s': 500, 'd': 30, 'f': 5, 't': 120} degauss = {'s': 500, 'd': 30, 'f': 5, 't': 120}
tolerance = 5 tolerance = 5
wait_stable_field = 180
[Bx] [Bx]
description = 'magnetic field x component' 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 re
import time import time
from secop.core import HasIO, StringIO, Readable, Drivable, Parameter, Command, \ from secop.core import HasIO, StringIO, Readable, Drivable, Parameter, Command, \
Module, Property, Attached, Enum, IDLE, BUSY, ERROR Module, Property, Attached, Enum, IDLE, BUSY, ERROR, Done
from secop.features import HasLimits
from secop.errors import ConfigError, BadValueError, HardwareError 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 # floating point value followed with unit
VALUE_UNIT = re.compile(r'([-0-9.E]*\d|inf)([A-Za-z/%]*)$') 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) cmd = cmd.replace('<CH>', self.channel)
reply = self.main.communicate(cmd) reply = self.main.communicate(cmd)
if not reply.startswith(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): def block(self, pname, value=None, delay=10):
self.block_until[pname] = time.time() + 10 self.block_until[pname] = time.time() + delay
if value is not None: if value is not None:
setattr(self, pname, value) setattr(self, pname, value)
@ -183,14 +185,26 @@ class Temperature(Channel, Readable):
self.main.register_module(self, value=self.channel) 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 = '' _status_text = ''
_ready_text = '' _ready_text = ''
_error_text = '' _error_text = ''
_last_error = '' _last_error = ''
_rate_units = '' _rate_units = ''
_next_target = None
_last_target = None
hw_units = Property('hardware units: A or T', EnumType(A=0, T=1)) 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)) A_to_G = Property('A_to_G = "Gauss Value / Amps Value"', FloatRange(0))
tolerance = Parameter('tolerance', FloatRange(0), readonly=False, default=1) 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 if dt.min < 1e-99: # make limits symmetric
dt.min = - dt.max dt.min = - dt.max
min_, max_ = self.target_limits 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 dt = self.parameters['ramp'].datatype
if self.ramp == 0: # unconfigured: take max. if self.ramp == 0: # unconfigured: take max.
self.ramp = dt.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): def to_gauss(self, value):
value, unit = VALUE_UNIT.match(value).groups() value, unit = VALUE_UNIT.match(value).groups()
@ -237,57 +259,34 @@ class Magfield(HasLimits, Channel, Drivable):
_rate_units=('<CH>_Rate Units', str), _rate_units=('<CH>_Rate Units', str),
current=('<CH>_PSU Output', self.to_gauss), current=('<CH>_PSU Output', self.to_gauss),
voltage='<CH>_Voltage', 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), setpoint=('<CH>_Setpoint', self.to_gauss),
switch_heater=('<CH>_Heater', self.cvt_switch_heater), 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), approach_mode=('<CH>_Approach', self.cvt_approach_mode),
) )
def cvt_error(self, text): def cvt_error(self, text):
if text != self._last_error: if text != self._last_error:
self._last_error = text self._last_error = text
self.log.error(text)
return text return text
return self._error_text return self._error_text
def trigger_update(self): def trigger_update(self):
# called after treating result of GetAll message # called after treating result of GetAll message
if self._error_text: if self._error_text:
status = ERROR, '%s while %s' % (self._error_text, self._status_text) if self.status[0] == ERROR:
elif self._ready_text == 'TRUE': errtxt = self._error_text
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: else:
if self.hw_units == 'A': errtxt = '%s while %s' % (self._error_text, self.status[1])
self.sendcmd('Set:<CH>:Sweep %gA' % (target / self.A_to_G)) # self.stop_machine((ERROR, errtxt))
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
value = Parameter('magnetic field in the coil', FloatRange(unit='G')) value = Parameter('magnetic field in the coil', FloatRange(unit='G'))
setpoint = Parameter('setpoint', FloatRange(unit='G'), default=0) 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): def write_ramp(self, ramp):
if self._rate_units != 'A/s': if self._rate_units != 'A/s':
@ -297,48 +296,63 @@ class Magfield(HasLimits, Channel, Drivable):
def write_target(self, target): def write_target(self, target):
self.reset_error() self.reset_error()
self.check_limits(target) super().write_target(target)
self.write_ramp(self.ramp) return Done
def start_sweep(self, target):
if self.approach_mode == self.approach_mode.OVERSHOOT: if self.approach_mode == self.approach_mode.OVERSHOOT:
o = self.overshoot['o'] o = self.overshoot['o']
if (target - self.value) * o < 0: if (target - self.value) * o < 0:
self.write_overshoot(dict(self.overshoot, o=-o)) self.write_overshoot(dict(self.overshoot, o=-o))
self.block('_error_text', '') self.write_ramp(self.ramp)
if self._ready_text == 'FALSE': if self.hw_units == 'A':
if target != self._last_target or abs(self.value - target) > self.tolerance: self.sendcmd('Set:<CH>:Sweep %gA' % (target / self.A_to_G))
self.status = BUSY, 'aborting'
self.sendcmd('Set:<CH>:Abort')
self._next_target = target
else: else:
self._next_target = target self.sendcmd('Set:<CH>:Sweep %gT' % (target / 10000))
self.trigger_update() # update status self.block('_ready_text', 'FALSE')
return target
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( def wait_ready(self, sm):
# DISABLED=0, if self._ready_text == 'FALSE':
PERSISTENT=30, return Retry
SEMIPERSISTENT=31, return super().start_field_change
DRIVEN=50,
)
mode = Parameter('persistent mode', EnumType(Mode), readonly=False, default=30)
mode_map = Mapped(DRIVEN=0, PERSISTENT=1, SEMIPERSISTENT=2)
def write_mode(self, value): def start_ramp_to_target(self, sm):
code = self.mode_map(value) self.start_sweep(sm.target)
self.sendcmd('Set:<CH>:SetPM %d' % code) return self.ramp_to_target # -> stabilize_field
self.block('mode')
return value 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 @staticmethod
def cvt_mode(text): def cvt_cs_mode(text):
text = text.lower() text = text.lower()
if 'off' in text: if 'off' in text:
if '0' in text: if '0' in text:
return 30 return CsMode.PERSISTENT
return 31 return CsMode.SEMIPERSISTENT
return 50 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( ApproachMode = Enum(
DIRECT=0, DIRECT=0,
@ -393,12 +407,8 @@ class Magfield(HasLimits, Channel, Drivable):
(value['s'] * 1e-4, value['d'], value['f'] * 1e-4, value['t'])) (value['s'] * 1e-4, value['d'], value['f'] * 1e-4, value['t']))
return value return value
current = Parameter(
'leads current (in units of field)', FloatRange(unit='$'))
voltage = Parameter( voltage = Parameter(
'voltage over leads', FloatRange(unit='V')) 'voltage over leads', FloatRange(unit='V'))
switch_heater = Parameter(
'voltage over leads', EnumType(OFF=0, ON=1))
@staticmethod @staticmethod
def cvt_switch_heater(text): def cvt_switch_heater(text):
@ -419,29 +429,94 @@ class Magfield(HasLimits, Channel, Drivable):
self._error_text = '' self._error_text = ''
class MainMagfield(Magfield): class MainField(BaseMagfield, magfield.Magfield):
checked_modules = None checked_modules = None
def earlyInit(self): def earlyInit(self):
super().earlyInit() super().earlyInit()
self.checked_modules = [] self.checked_modules = []
def check_limits(self, value):
super().check_limits(value)
self.check_combined(None, 0, value)
# TODO: turn into a property # TODO: turn into a property
constraint = Parameter('product check', FloatRange(unit='G^2'), default=80000) constraint = Parameter('product check', FloatRange(unit='G^2'), default=80000)
def check_combined(self, obj, value, main_target): 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)) for o in self.checked_modules))
if sumvalue2 * max(self.value ** 2, main_target) > self.constraint ** 2: if sumvalue2 * max(self.value ** 2, main_target ** 2) > self.constraint ** 2:
raise BadValueError('outside constraint (B * Bxyz > %g G^2' * self.constraint) raise BadValueError('outside constraint (B * Bxyz > %g G^2' % self.constraint)
def check_limits(self, value): def check_limits(self, value):
super().check_limits(value) super().check_limits(value)
self.check_combined(None, 0, value) self.check_combined(None, 0, value)
mode = Parameter(datatype=EnumType(PersistencyMode))
class ComponentField(Magfield): def write_mode(self, mode):
check_against = Attached(MainMagfield) 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): def initModule(self):
super().initModule() super().initModule()
@ -495,9 +570,9 @@ class Compressor(Channel, Drivable):
def write_target(self, value): def write_target(self, value):
if value: if value:
self.sendcmd('SetCompressor:Start <CH>') self.sendcmd('Set:Compressor:Start <CH>')
else: else:
self.sendcmd('SetCompressor:Stop <CH>') self.sendcmd('Set:Compressor:Stop <CH>')
self.block('target') self.block('target')
self.read_status() self.read_status()
return value return value
@ -507,6 +582,3 @@ class Compressor(Channel, Drivable):
"""reset error""" """reset error"""
self.sendcmd('Set:Compressor:Reset <CH>') self.sendcmd('Set:Compressor:Reset <CH>')
self._error_text = '' self._error_text = ''