diff --git a/cfg/main/flamemag.cfg b/cfg/main/flamemag.cfg index 4daf145..b631c0f 100644 --- a/cfg/main/flamemag.cfg +++ b/cfg/main/flamemag.cfg @@ -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' diff --git a/secop_psi/cryoltd.py b/secop_psi/cryoltd.py index 4abf29c..17188a6 100644 --- a/secop_psi/cryoltd.py +++ b/secop_psi/cryoltd.py @@ -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('', 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=('_Rate Units', str), current=('_PSU Output', self.to_gauss), voltage='_Voltage', - working_ramp=('_Ramp Rate', self.to_gauss_min), + workingramp=('_Ramp Rate', self.to_gauss_min), setpoint=('_Setpoint', self.to_gauss), switch_heater=('_Heater', self.cvt_switch_heater), - mode=('_Persistent Mode', self.cvt_mode), + cs_mode=('_Persistent Mode', self.cvt_cs_mode), approach_mode=('_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::Sweep %gA' % (target / self.A_to_G)) - else: - self.sendcmd('Set::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::Abort') - self._next_target = target + self.write_ramp(self.ramp) + if self.hw_units == 'A': + self.sendcmd('Set::Sweep %gA' % (target / self.A_to_G)) else: - self._next_target = target - self.trigger_update() # update status - return target + self.sendcmd('Set::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::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::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::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 ') + self.sendcmd('Set:Compressor:Start ') else: - self.sendcmd('SetCompressor:Stop ') + self.sendcmd('Set:Compressor:Stop ') self.block('target') self.read_status() return value @@ -507,6 +582,3 @@ class Compressor(Channel, Drivable): """reset error""" self.sendcmd('Set:Compressor:Reset ') self._error_text = '' - - -