# -*- 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 # ***************************************************************************** """generic persistent magnet driver""" import time 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, HardwareError from secop.lib.enum import Enum from secop.states import Retry, HasStates, status_code UNLIMITED = FloatRange() Mode = Enum( DISABLED=0, PERSISTENT=30, DRIVEN=50, ) Status = Enum( Drivable.Status, PREPARED=150, PREPARING=340, RAMPING=370, STABILIZING=380, FINALIZING=390, ) OFF = 0 ON = 1 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) 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='$')) # TODO: time_to_target # profile = Parameter( # 'ramp limit table', ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))), # readonly=False) # profile_training = Parameter( # 'ramp limit table when in training', # ArrayOf(TupleOf(FloatRange(unit='$'), FloatRange(unit='$/min'))), readonly=False) # TODO: the following parameters should be changed into properties after tests wait_switch_on = Parameter( 'wait time to ensure switch is on', FloatRange(0, unit='s'), readonly=False, default=61) wait_switch_off = Parameter( '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) persistent_limit = Parameter( 'above this limit, lead currents are not driven to 0', FloatRange(0, unit='$'), readonly=False, default=99) __init = True switch_time = None, None def doPoll(self): if self.__init: self.__init = False if self.read_switch_heater() and self.mode == Mode.PERSISTENT: self.read_value() # check for persistent field mismatch # switch off heater from previous live or manual intervention self.write_target(self.persistent_field) else: self._last_target = self.persistent_field else: super().doPoll() def initStateMachine(self): super().initStateMachine() self.registerCallbacks(self) # for update_switch_heater 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.start_state(self.start_field_change, target=target, mode=self.mode) return target def onError(self, state): if self.switch_heater != 0: self.persistent_field = self.read_value() if state.mode != Mode.DRIVEN: self.log.warning('turn switch heater off') self.write_switch_heater(0) return super().onError(state) @status_code('PREPARING') def start_field_change(self, state): self.setFastPoll(True, 1.0) 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 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): 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): if state.now - state.stabilize_start < max(state.tol_time, self.wait_stable_leads): return Retry() return self.start_switch_on def update_switch_heater(self, value): """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): if self.read_switch_heater() == 0: self.status = Status.PREPARING, 'turn switch heater on' try: self.write_switch_heater(True) except Exception as e: self.log.warning('write_switch_heater %r', e) return Retry() else: self.status = Status.PREPARING, 'wait for heater on' return self.wait_for_switch_on @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() # 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 = state.target return self.start_ramp_to_target @status_code('RAMPING') def start_ramp_to_target(self, state): """start ramping current to target field initiate ramp to target the implementation should return ramp_to_target """ raise NotImplementedError @status_code('RAMPING') def ramp_to_target(self, state): 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 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): self.persistent_field = self.value 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 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): if self.switch_heater == 1: self.write_switch_heater(False) return self.wait_for_switch_off @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: self.log.warning('switch turned on manually?') return self.start_switch_off if state.now - self.switch_time[OFF] < self.wait_switch_off: return Retry() if abs(self.value) > self.persistent_limit: 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 zero initiate ramp to zero (with corresponding ramp rate) the implementation should return ramp_to_zero """ raise NotImplementedError @status_code('FINALIZING') def ramp_to_zero(self, state): """[FINALIZING] ramp field to zero""" if abs(self.current) > self.tolerance: 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')