From 93205417544dc03eec7024d821259d74d12adbe5 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Tue, 8 Mar 2022 08:35:41 +0100 Subject: [PATCH] result from merge with gerrit drivers in secop_psi Change-Id: I7fd8312b11f365b423e66b2417b9e54ec6558a11 --- secop_psi/ls370sim.py | 14 +- secop_psi/ppms.py | 499 +++++++++++++++++++----------------------- secop_psi/softcal.py | 64 ++---- 3 files changed, 257 insertions(+), 320 deletions(-) diff --git a/secop_psi/ls370sim.py b/secop_psi/ls370sim.py index 354f4f1..cde2d64 100644 --- a/secop_psi/ls370sim.py +++ b/secop_psi/ls370sim.py @@ -28,15 +28,16 @@ class Ls370Sim(Communicator): ('RDGR?%d', '1.0'), ('RDGST?%d', '0'), ('RDGRNG?%d', '0,5,5,0,0'), - ('INSET?%d', '1,3,3,0,0'), - ('FILTER?%d', '1,1,80'), + ('INSET?%d', '1,5,5,0,0'), + ('FILTER?%d', '1,5,80'), ] OTHER_COMMANDS = [ ('*IDN?', 'LSCI,MODEL370,370184,05302003'), - ('SCAN?', '3,0'), + ('SCAN?', '3,1'), ] def earlyInit(self): + super().earlyInit() self._data = dict(self.OTHER_COMMANDS) for fmt, v in self.CHANNEL_COMMANDS: for chan in range(1,17): @@ -44,6 +45,7 @@ class Ls370Sim(Communicator): # mkthread(self.run) def communicate(self, command): + self.comLog('> %s' % command) # simulation part, time independent for channel in range(1,17): _, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',') @@ -68,6 +70,6 @@ class Ls370Sim(Communicator): if qcmd in self._data: self._data[qcmd] = arg break - #if command.startswith('R'): - # print('> %s\t< %s' % (command, reply)) - return ';'.join(reply) + reply = ';'.join(reply) + self.comLog('< %s' % reply) + return reply diff --git a/secop_psi/ppms.py b/secop_psi/ppms.py index 5cbfe7f..1fe265f 100644 --- a/secop_psi/ppms.py +++ b/secop_psi/ppms.py @@ -33,17 +33,17 @@ Polling of value and status is done commonly for all modules. For each registere import threading import time +from ast import literal_eval # convert string as comma separated numbers into tuple -import secop.iohandler from secop.datatypes import BoolType, EnumType, \ FloatRange, IntRange, StatusType, StringType from secop.errors import HardwareError from secop.lib import clamp from secop.lib.enum import Enum -from secop.modules import Attached, Communicator, Done, \ +from secop.modules import Communicator, Done, \ Drivable, Parameter, Property, Readable -from secop.poller import Poller -from secop.io import HasIodev +from secop.io import HasIO +from secop.rwhandler import CommonReadHandler, CommonWriteHandler try: import secop_psi.ppmswindows as ppmshw @@ -52,28 +52,11 @@ except ImportError: import secop_psi.ppmssim as ppmshw -class IOHandler(secop.iohandler.IOHandler): - """IO handler for PPMS commands - - deals with typical format: - - - query command: ``?`` - - reply: ``,, ..`` - - change command: `` ,,...`` - """ - CMDARGS = ['no'] # the channel number is needed in channel commands - CMDSEPARATOR = None # no command chaining - - def __init__(self, name, querycmd, replyfmt): - changecmd = querycmd.split('?')[0] + ' ' - super().__init__(name, querycmd, replyfmt, changecmd) - - class Main(Communicator): """ppms communicator module""" pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2) - data = Parameter('internal', StringType(), poll=True, export=False, # export for test only + data = Parameter('internal', StringType(), export=True, # export for test only default="", readonly=True) class_id = Property('Quantum Design class id', StringType(), export=False) @@ -86,8 +69,6 @@ class Main(Communicator): _channel_to_index = dict(((channel, i) for i, channel in enumerate(_channel_names))) _status_bitpos = {'temp': 0, 'field': 4, 'chamber': 8, 'position': 12} - pollerClass = Poller - def earlyInit(self): super().earlyInit() self.modules = {} @@ -100,11 +81,14 @@ class Main(Communicator): def communicate(self, command): """GPIB command""" with self.lock: - self.log.debug('> %s' % command) + self.comLog('> %s' % command) reply = self._ppms_device.send(command) - self.log.debug('< %s' % reply) + self.comLog("< %s", reply) return reply + def doPoll(self): + self.read_data() + def read_data(self): mask = 1 # always get packed_status for channelname, channel in self.modules.items(): @@ -130,23 +114,27 @@ class Main(Communicator): return data # return data as string -class PpmsMixin: +class PpmsBase(HasIO, Readable): """common base for all ppms modules""" + value = Parameter(needscfg=False) + status = Parameter(needscfg=False) - iodev = Attached() - - pollerClass = Poller enabled = True # default, if no parameter enable is defined _last_settings = None # used by several modules slow_pollfactor = 1 # as this pollinterval affects only the polling of settings # it would be confusing to export it. - pollinterval = Parameter('', FloatRange(), needscfg=False, export=False) + pollinterval = Parameter(export=False) def initModule(self): super().initModule() - self._iodev.register(self) + self.io.register(self) + + def doPoll(self): + # polling is done by the main module + # and PPMS does not deliver really more fresh values when polled more often + pass def update_value_status(self, value, packed_status): # update value and status @@ -160,12 +148,18 @@ class PpmsMixin: self.value = value self.status = (self.Status.IDLE, '') + def comm_write(self, command): + """write command and check if result is OK""" + reply = self.communicate(command) + if reply != 'OK': + raise HardwareError('bad reply %r to command %r' % (reply, command)) -class Channel(PpmsMixin, HasIodev, Readable): + +class Channel(PpmsBase): """channel base class""" - value = Parameter('main value of channels', poll=False, needscfg=False) - enabled = Parameter('is this channel used?', readonly=False, poll=False, + value = Parameter('main value of channels') + enabled = Parameter('is this channel used?', readonly=False, datatype=BoolType(), default=False) channel = Property('channel name', @@ -178,22 +172,17 @@ class Channel(PpmsMixin, HasIodev, Readable): if not self.channel: self.channel = self.name - def get_settings(self, pname): - return '' - class UserChannel(Channel): """user channel""" - # pollinterval = Parameter(visibility=3) - no = Property('channel number', datatype=IntRange(0, 0), export=False, default=0) linkenable = Property('name of linked channel for enabling', datatype=StringType(), export=False, default='') def write_enabled(self, enabled): - other = self._iodev.modules.get(self.linkenable, None) + other = self.io.modules.get(self.linkenable, None) if other: other.enabled = enabled return enabled @@ -202,201 +191,172 @@ class UserChannel(Channel): class DriverChannel(Channel): """driver channel""" - drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g') - - current = Parameter('driver current', readonly=False, handler=drvout, + current = Parameter('driver current', readonly=False, datatype=FloatRange(0., 5000., unit='uA')) - powerlimit = Parameter('power limit', readonly=False, handler=drvout, + powerlimit = Parameter('power limit', readonly=False, datatype=FloatRange(0., 1000., unit='uW')) - # pollinterval = Parameter(visibility=3) - def analyze_drvout(self, no, current, powerlimit): + param_names = 'current', 'powerlimit' + + @CommonReadHandler(param_names) + def read_params(self): + no, self.current, self.powerlimit = literal_eval( + self.communicate('DRVOUT? %d' % self.no)) if self.no != no: raise HardwareError('DRVOUT command: channel number in reply does not match') - return dict(current=current, powerlimit=powerlimit) - def change_drvout(self, change): - change.readValues() - return change.current, change.powerlimit + @CommonWriteHandler(param_names) + def write_params(self, values): + """write parameters + + :param values: a dict like object containing the parameters to be written + """ + self.read_params() # make sure parameters are up to date + self.comm_write('DRVOUT %(no)d,%(current)g,%(powerlimit)g' % values) + self.read_params() # read back class BridgeChannel(Channel): """bridge channel""" - bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g') - # pylint: disable=invalid-name - ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2) - - enabled = Parameter(handler=bridge) - excitation = Parameter('excitation current', readonly=False, handler=bridge, + excitation = Parameter('excitation current', readonly=False, datatype=FloatRange(0.01, 5000., unit='uA')) - powerlimit = Parameter('power limit', readonly=False, handler=bridge, + powerlimit = Parameter('power limit', readonly=False, datatype=FloatRange(0.001, 1000., unit='uW')) - dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge, + dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, datatype=BoolType()) - readingmode = Parameter('reading mode', readonly=False, handler=bridge, - datatype=EnumType(ReadingMode)) - voltagelimit = Parameter('voltage limit', readonly=False, handler=bridge, + readingmode = Parameter('reading mode', readonly=False, + datatype=EnumType(standard=0, fast=1, highres=2)) + voltagelimit = Parameter('voltage limit', readonly=False, datatype=FloatRange(0.0001, 100., unit='mV')) - # pollinterval = Parameter(visibility=3) - def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit): + param_names = 'enabled', 'enabled', 'powerlimit', 'dcflag', 'readingmode', 'voltagelimit' + + @CommonReadHandler(param_names) + def read_params(self): + no, excitation, powerlimit, self.dcflag, self.readingmode, voltagelimit = literal_eval( + self.communicate('BRIDGE? %d' % self.no)) if self.no != no: raise HardwareError('DRVOUT command: channel number in reply does not match') - return dict( - enabled=excitation != 0 and powerlimit != 0 and voltagelimit != 0, - excitation=excitation or self.excitation, - powerlimit=powerlimit or self.powerlimit, - dcflag=dcflag, - readingmode=readingmode, - voltagelimit=voltagelimit or self.voltagelimit, - ) + self.enabled = excitation != 0 and powerlimit != 0 and voltagelimit != 0 + if excitation: + self.excitation = excitation + if powerlimit: + self.powerlimit = powerlimit + if voltagelimit: + self.voltagelimit = voltagelimit - def change_bridge(self, change): - change.readValues() - if change.enabled: - return self.no, change.excitation, change.powerlimit, change.dcflag, change.readingmode, change.voltagelimit - return self.no, 0, 0, change.dcflag, change.readingmode, 0 + @CommonWriteHandler(param_names) + def write_params(self, values): + """write parameters + + :param values: a dict like object containing the parameters to be written + """ + self.read_params() # make sure parameters are up to date + if not values['enabled']: + values['excitation'] = 0 + values['powerlimit'] = 0 + values['voltagelimit'] = 0 + self.comm_write('BRIDGE %(no)d,%(enabled)g,%(powerlimit)g,%(dcflag)d,' + '%(readingmode)d,%(voltagelimit)g' % values) + self.read_params() # read back -class Level(PpmsMixin, HasIodev, Readable): +class Level(PpmsBase): """helium level""" - level = IOHandler('level', 'LEVEL?', '%g,%d') - - value = Parameter(datatype=FloatRange(unit='%'), handler=level) - status = Parameter(handler=level) - # pollinterval = Parameter(visibility=3) + value = Parameter(datatype=FloatRange(unit='%')) channel = 'level' + def doPoll(self): + self.read_value() + def update_value_status(self, value, packed_status): pass # must be a no-op # when called from Main.read_data, value is always None # value and status is polled via settings - def analyze_level(self, level, status): + def read_value(self): # ignore 'old reading' state of the flag, as this happens only for a short time - # during measuring - return dict(value=level, status=(self.Status.IDLE, '')) + return literal_eval(self.communicate('LEVEL?'))[0] -class Chamber(PpmsMixin, HasIodev, Drivable): +class Chamber(PpmsBase, Drivable): """sample chamber handling value is an Enum, which is redundant with the status text """ - chamber = IOHandler('chamber', 'CHAMBER?', '%d') Status = Drivable.Status - # pylint: disable=invalid-name - Operation = Enum( - 'Operation', - seal_immediately=0, - purge_and_seal=1, - vent_and_seal=2, - pump_continuously=3, - vent_continuously=4, - hi_vacuum=5, - noop=10, - ) - StatusCode = Enum( - 'StatusCode', - unknown=0, - purged_and_sealed=1, - vented_and_sealed=2, - sealed_unknown=3, - purge_and_seal=4, - vent_and_seal=5, - pumping_down=6, - at_hi_vacuum=7, - pumping_continuously=8, - venting_continuously=9, - general_failure=15, - ) - - value = Parameter(description='chamber state', handler=chamber, - datatype=EnumType(StatusCode)) - target = Parameter(description='chamber command', handler=chamber, - datatype=EnumType(Operation)) - # pollinterval = Parameter(visibility=3) - - STATUS_MAP = { - StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'), - StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'), - StatusCode.sealed_unknown: (Status.WARN, 'sealed unknown'), - StatusCode.purge_and_seal: (Status.BUSY, 'purge and seal'), - StatusCode.vent_and_seal: (Status.BUSY, 'vent and seal'), - StatusCode.pumping_down: (Status.BUSY, 'pumping down'), - StatusCode.at_hi_vacuum: (Status.IDLE, 'at hi vacuum'), - StatusCode.pumping_continuously: (Status.IDLE, 'pumping continuously'), - StatusCode.venting_continuously: (Status.IDLE, 'venting continuously'), - StatusCode.general_failure: (Status.ERROR, 'general failure'), - } + code_table = [ + # valuecode, status, statusname, opcode, targetname + (0, Status.IDLE, 'unknown', 10, 'noop'), + (1, Status.IDLE, 'purged_and_sealed', 1, 'purge_and_seal'), + (2, Status.IDLE, 'vented_and_sealed', 2, 'vent_and_seal'), + (3, Status.WARN, 'sealed_unknown', 0, 'seal_immediately'), + (4, Status.BUSY, 'purge_and_seal', None, None), + (5, Status.BUSY, 'vent_and_seal', None, None), + (6, Status.BUSY, 'pumping_down', None, None), + (8, Status.IDLE, 'pumping_continuously', 3, 'pump_continuously'), + (9, Status.IDLE, 'venting_continuously', 4, 'vent_continuously'), + (15, Status.ERROR, 'general_failure', None, None), + ] + value_codes = {k: v for v, _, k, _, _ in code_table} + target_codes = {k: v for v, _, _, _, k in code_table if k} + name2opcode = {k: v for _, _, _, v, k in code_table if k} + opcode2name = {v: k for _, _, _, v, k in code_table if k} + status_map = {v: (c, k.replace('_', ' ')) for v, c, k, _, _ in code_table} + value = Parameter(description='chamber state', datatype=EnumType(**value_codes), default=0) + target = Parameter(description='chamber command', datatype=EnumType(**target_codes), default='noop') channel = 'chamber' def update_value_status(self, value, packed_status): status_code = (packed_status >> 8) & 0xf - if status_code in self.STATUS_MAP: + if status_code in self.status_map: self.value = status_code - self.status = self.STATUS_MAP[status_code] + self.status = self.status_map[status_code] else: - self.value = self.StatusCode.unknown + self.value = self.value_map['unknown'] self.status = (self.Status.ERROR, 'unknown status code %d' % status_code) - def analyze_chamber(self, target): - return dict(target=target) + def read_target(self): + opcode = int(self.communicate('CHAMBER?')) + return self.opcode2name[opcode] - def change_chamber(self, change): - # write settings, combining = and current attributes - # and request updated settings - if change.target == self.Operation.noop: - return None - return (change.target,) + def write_target(self, value): + if value == self.target.noop: + return self.target.noop + opcode = self.name2opcode[self.target.enum(value).name] + assert self.communicate('CHAMBER %d' % opcode) == 'OK' + return self.read_target() -class Temp(PpmsMixin, HasIodev, Drivable): +class Temp(PpmsBase, Drivable): """temperature""" - temp = IOHandler('temp', 'TEMP?', '%g,%g,%d') Status = Enum( Drivable.Status, RAMPING=370, STABILIZING=380, ) - # pylint: disable=invalid-name - ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1) - - value = Parameter(datatype=FloatRange(unit='K'), poll=True) - status = Parameter(datatype=StatusType(Status), poll=True) - target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False) + value = Parameter(datatype=FloatRange(unit='K')) + status = Parameter(datatype=StatusType(Status)) + target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), needscfg=False) setpoint = Parameter('intermediate set point', - datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp) + datatype=FloatRange(1.7, 402.0, unit='K')) ramp = Parameter('ramping speed', readonly=False, default=0, datatype=FloatRange(0, 20, unit='K/min')) workingramp = Parameter('intermediate ramp value', - datatype=FloatRange(0, 20, unit='K/min'), handler=temp) - approachmode = Parameter('how to approach target!', readonly=False, handler=temp, - datatype=EnumType(ApproachMode)) - # pollinterval = Parameter(visibility=3) + datatype=FloatRange(0, 20, unit='K/min'), default=0) + approachmode = Parameter('how to approach target!', readonly=False, + datatype=EnumType(fast_settle=0, no_overshoot=1), default=0) timeout = Parameter('drive timeout, in addition to ramp time', readonly=False, datatype=FloatRange(0, unit='sec'), default=3600) - - # pylint: disable=invalid-name - TempStatus = Enum( - 'TempStatus', - stable_at_target=1, - changing=2, - within_tolerance=5, - outside_tolerance=6, - filling_emptying_reservoir=7, - standby=10, - control_disabled=13, - can_not_complete=14, - general_failure=15, - ) + general_stop = Property('respect general stop', datatype=BoolType(), + default=True, value=False) STATUS_MAP = { 1: (Status.IDLE, 'stable at target'), 2: (Status.RAMPING, 'ramping'), @@ -408,8 +368,6 @@ class Temp(PpmsMixin, HasIodev, Drivable): 14: (Status.ERROR, 'can not complete'), 15: (Status.ERROR, 'general failure'), } - general_stop = Property('respect general stop', datatype=BoolType(), - default=True, value=False) channel = 'temp' _stopped = False @@ -420,6 +378,42 @@ class Temp(PpmsMixin, HasIodev, Drivable): _wait_at10 = False _ramp_at_limit = False + param_names = 'setpoint', 'workingramp', 'approachmode' + + @CommonReadHandler(param_names) + def read_params(self): + settings = literal_eval(self.communicate('TEMP?')) + if settings == self._last_settings: + # update parameters only on change, as 'ramp' and 'approachmode' are + # not always sent to the hardware + return + self.setpoint, self.workingramp, self.approachmode = self._last_settings = settings + if self.setpoint != 10 or not self._wait_at10: + self.log.debug('read back target %g %r' % (self.setpoint, self._wait_at10)) + self.target = self.setpoint + if self.workingramp != 2 or not self._ramp_at_limit: + self.log.debug('read back ramp %g %r' % (self.workingramp, self._ramp_at_limit)) + self.ramp = self.workingramp + + def _write_params(self, setpoint, ramp, approachmode): + wait_at10 = False + ramp_at_limit = False + if self.value > 11: + if setpoint <= 10: + wait_at10 = True + setpoint = 10 + elif self.value > setpoint: + if ramp >= 2: + ramp = 2 + ramp_at_limit = True + self._wait_at10 = wait_at10 + self._ramp_at_limit = ramp_at_limit + self.calc_expected(setpoint, ramp) + self.log.debug( + 'change_temp v %r s %r r %r w %r l %r' % (self.value, setpoint, ramp, wait_at10, ramp_at_limit)) + self.comm_write('TEMP %g,%g,%d' % (setpoint, ramp, approachmode)) + self.read_params() + def update_value_status(self, value, packed_status): if value is None: self.status = (self.Status.ERROR, 'invalid value') @@ -437,7 +431,7 @@ class Temp(PpmsMixin, HasIodev, Drivable): if now > self._cool_deadline: self._wait_at10 = False self._last_change = now - self.temp.write(self, 'setpoint', self.target) + self._write_params(self.target, self.ramp, self.approachmode) status = (self.Status.STABILIZING, 'waiting at 10 K') if self._last_change: # there was a change, which is not yet confirmed by hw if now > self._last_change + 5: @@ -466,41 +460,6 @@ class Temp(PpmsMixin, HasIodev, Drivable): self._expected_target_time = 0 self.status = status - def analyze_temp(self, setpoint, workingramp, approachmode): - if (setpoint, workingramp, approachmode) == self._last_settings: - # update parameters only on change, as 'ramp' and 'approachmode' are - # not always sent to the hardware - return {} - self._last_settings = setpoint, workingramp, approachmode - if setpoint != 10 or not self._wait_at10: - self.log.debug('read back target %g %r' % (setpoint, self._wait_at10)) - self.target = setpoint - if workingramp != 2 or not self._ramp_at_limit: - self.log.debug('read back ramp %g %r' % (workingramp, self._ramp_at_limit)) - self.ramp = workingramp - result = dict(setpoint=setpoint, workingramp=workingramp) - self.log.debug('analyze_temp %r %r' % (result, (self.target, self.ramp))) - return result - - def change_temp(self, change): - ramp = change.ramp - setpoint = change.setpoint - wait_at10 = False - ramp_at_limit = False - if self.value > 11: - if setpoint <= 10: - wait_at10 = True - setpoint = 10 - elif self.value > setpoint: - if ramp >= 2: - ramp = 2 - ramp_at_limit = True - self._wait_at10 = wait_at10 - self._ramp_at_limit = ramp_at_limit - self.calc_expected(setpoint, ramp) - self.log.debug('change_temp v %r s %r r %r w %r l %r' % (self.value, setpoint, ramp, wait_at10, ramp_at_limit)) - return setpoint, ramp, change.approachmode - def write_target(self, target): self._stopped = False if abs(self.target - self.value) <= 2e-5 * target and target == self.target: @@ -508,23 +467,23 @@ class Temp(PpmsMixin, HasIodev, Drivable): self._status_before_change = self.status self.status = (self.Status.BUSY, 'changed target') self._last_change = time.time() - self.temp.write(self, 'setpoint', target) + self._write_params(target, self.ramp, self.approachmode) self.log.debug('write_target %s' % repr((self.setpoint, target, self._wait_at10))) return target def write_approachmode(self, value): if self.isDriving(): - self.temp.write(self, 'approachmode', value) + self._write_params(self.setpoint, self.ramp, value) return Done self.approachmode = value - return None # do not execute TEMP command, as this would trigger an unnecessary T change + return Done # do not execute TEMP command, as this would trigger an unnecessary T change def write_ramp(self, value): if self.isDriving(): - self.temp.write(self, 'ramp', value) + self._write_params(self.setpoint, value, self.approachmode) return Done - # self.ramp = value - return None # do not execute TEMP command, as this would trigger an unnecessary T change + self.ramp = value + return Done # do not execute TEMP command, as this would trigger an unnecessary T change def calc_expected(self, target, ramp): self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp) @@ -542,10 +501,9 @@ class Temp(PpmsMixin, HasIodev, Drivable): self._stopped = True -class Field(PpmsMixin, HasIodev, Drivable): +class Field(PpmsBase, Drivable): """magnetic field""" - field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d') Status = Enum( Drivable.Status, PREPARED=150, @@ -554,20 +512,15 @@ class Field(PpmsMixin, HasIodev, Drivable): STABILIZING=380, FINALIZING=390, ) - # pylint: disable=invalid-name - PersistentMode = Enum('PersistentMode', persistent=0, driven=1) - ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2) - - value = Parameter(datatype=FloatRange(unit='T'), poll=True) - status = Parameter(datatype=StatusType(Status), poll=True) - target = Parameter(datatype=FloatRange(-15, 15, unit='T'), handler=field) - ramp = Parameter('ramping speed', readonly=False, handler=field, - datatype=FloatRange(0.064, 1.19, unit='T/min')) - approachmode = Parameter('how to approach target', readonly=False, handler=field, - datatype=EnumType(ApproachMode)) - persistentmode = Parameter('what to do after changing field', readonly=False, handler=field, - datatype=EnumType(PersistentMode)) - # pollinterval = Parameter(visibility=3) + value = Parameter(datatype=FloatRange(unit='T')) + status = Parameter(datatype=StatusType(Status)) + target = Parameter(datatype=FloatRange(-15, 15, unit='T')) # poll only one parameter + ramp = Parameter('ramping speed', readonly=False, + datatype=FloatRange(0.064, 1.19, unit='T/min'), default=0.19) + approachmode = Parameter('how to approach target', readonly=False, + datatype=EnumType(linear=0, no_overshoot=1, oscillate=2), default=0) + persistentmode = Parameter('what to do after changing field', readonly=False, + datatype=EnumType(persistent=0, driven=1), default=0) STATUS_MAP = { 1: (Status.IDLE, 'persistent mode'), @@ -587,6 +540,25 @@ class Field(PpmsMixin, HasIodev, Drivable): _last_target = None # last reached target _last_change = 0 # means no target change is pending + param_names = 'target', 'ramp', 'approachmode', 'persistentmode' + + @CommonReadHandler(param_names) + def read_params(self): + settings = literal_eval(self.communicate('FIELD?')) + # print('last_settings tt %s' % repr(self._last_settings)) + if settings == self._last_settings: + # we update parameters only on change, as 'ramp' and 'approachmode' are + # not always sent to the hardware + return + target, ramp, self.approachmode, self.persistentmode = self._last_settings = settings + self.target = round(target * 1e-4, 7) + self.ramp = ramp * 6e-3 + + def _write_params(self, target, ramp, approachmode, persistentmode): + self.comm_write('FIELD %g,%g,%d,%d' % ( + target * 1e+4, ramp / 6e-3, approachmode, persistentmode)) + self.read_params() + def update_value_status(self, value, packed_status): if value is None: self.status = (self.Status.ERROR, 'invalid value') @@ -621,19 +593,6 @@ class Field(PpmsMixin, HasIodev, Drivable): status = (status[0], 'stopping (%s)' % status[1]) self.status = status - def analyze_field(self, target, ramp, approachmode, persistentmode): - # print('last_settings tt %s' % repr(self._last_settings)) - if (target, ramp, approachmode, persistentmode) == self._last_settings: - # we update parameters only on change, as 'ramp' and 'approachmode' are - # not always sent to the hardware - return {} - self._last_settings = target, ramp, approachmode, persistentmode - return dict(target=round(target * 1e-4, 7), ramp=ramp * 6e-3, approachmode=approachmode, - persistentmode=persistentmode) - - def change_field(self, change): - return change.target * 1e+4, change.ramp / 6e-3, change.approachmode, change.persistentmode - def write_target(self, target): if abs(self.target - self.value) <= 2e-5 and target == self.target: self.target = target @@ -642,7 +601,7 @@ class Field(PpmsMixin, HasIodev, Drivable): self._stopped = False self._last_change = time.time() self.status = (self.Status.BUSY, 'changed target') - self.field.write(self, 'target', target) + self._write_params(target, self.ramp, self.approachmode, self.persistentmode) return Done def write_persistentmode(self, mode): @@ -653,19 +612,19 @@ class Field(PpmsMixin, HasIodev, Drivable): self._status_before_change = self.status self._stopped = False self.status = (self.Status.BUSY, 'changed persistent mode') - self.field.write(self, 'persistentmode', mode) + self._write_params(self.target, self.ramp, self.approachmode, mode) return Done def write_ramp(self, value): self.ramp = value if self.isDriving(): - self.field.write(self, 'ramp', value) + self._write_params(self.target, value, self.approachmode, self.persistentmode) return Done return None # do not execute FIELD command, as this would trigger a ramp up of leads current def write_approachmode(self, value): if self.isDriving(): - self.field.write(self, 'approachmode', value) + self._write_params(self.target, self.ramp, value, self.persistentmode) return Done return None # do not execute FIELD command, as this would trigger a ramp up of leads current @@ -680,20 +639,17 @@ class Field(PpmsMixin, HasIodev, Drivable): self._stopped = True -class Position(PpmsMixin, HasIodev, Drivable): +class Position(PpmsBase, Drivable): """rotator position""" - move = IOHandler('move', 'MOVE?', '%g,%g,%g') Status = Drivable.Status - value = Parameter(datatype=FloatRange(unit='deg'), poll=True) - target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), handler=move) - enabled = Parameter('is this channel used?', readonly=False, poll=False, + value = Parameter(datatype=FloatRange(unit='deg')) + target = Parameter(datatype=FloatRange(-720., 720., unit='deg')) + enabled = Parameter('is this channel used?', readonly=False, datatype=BoolType(), default=True) - speed = Parameter('motor speed', readonly=False, handler=move, + speed = Parameter('motor speed', readonly=False, default=12, datatype=FloatRange(0.8, 12, unit='deg/sec')) - # pollinterval = Parameter(visibility=3) - STATUS_MAP = { 1: (Status.IDLE, 'at target'), 5: (Status.BUSY, 'moving'), @@ -708,6 +664,23 @@ class Position(PpmsMixin, HasIodev, Drivable): _last_change = 0 _within_target = 0 # time since we are within target + param_names = 'target', 'speed' + + @CommonReadHandler(param_names) + def read_params(self): + settings = literal_eval(self.communicate('MOVE?')) + if settings == self._last_settings: + # we update parameters only on change, as 'speed' is + # not always sent to the hardware + return + self.target, _, speed = self._last_settings = settings + self.speed = (15 - speed) * 0.8 + + def _write_params(self, target, speed): + speed = int(round(min(14, max(0, 15 - speed / 0.8)), 0)) + self.comm_write('MOVE %g,%d,%d' % (target, 0, speed)) + return self.read_params() + def update_value_status(self, value, packed_status): if not self.enabled: self.status = (self.Status.DISABLED, 'disabled') @@ -745,29 +718,17 @@ class Position(PpmsMixin, HasIodev, Drivable): status = (status[0], 'stopping (%s)' % status[1]) self.status = status - def analyze_move(self, target, mode, speed): - if (target, speed) == self._last_settings: - # we update parameters only on change, as 'speed' is - # not always sent to the hardware - return {} - self._last_settings = target, speed - return dict(target=target, speed=(15 - speed) * 0.8) - - def change_move(self, change): - speed = int(round(min(14, max(0, 15 - change.speed / 0.8)), 0)) - return change.target, 0, speed - def write_target(self, target): self._stopped = False self._last_change = 0 self._status_before_change = self.status self.status = (self.Status.BUSY, 'changed target') - self.move.write(self, 'target', target) + self._write_params(target, self.speed) return Done def write_speed(self, value): if self.isDriving(): - self.move.write(self, 'speed', value) + self._write_params(self.target, value) return Done self.speed = value return None # do not execute MOVE command, as this would trigger an unnecessary move diff --git a/secop_psi/softcal.py b/secop_psi/softcal.py index 17a2f17..d3bcefb 100644 --- a/secop_psi/softcal.py +++ b/secop_psi/softcal.py @@ -22,12 +22,12 @@ import math import os -from os.path import basename, dirname, exists, join +from os.path import basename, exists, join import numpy as np from scipy.interpolate import splev, splrep # pylint: disable=import-error -from secop.core import Attached, BoolType, Parameter, Readable, StringType, FloatRange +from secop.core import Attached, BoolType, Parameter, Readable, StringType def linear(x): @@ -74,18 +74,13 @@ class Parser340(StdParser): def parse(self, line): """scan header for data format""" if self.header: - key, _, value = line.partition(':') - if value: # this is a header line, as it contains ':' - value = value.split()[0] - key = ''.join(key.split()).lower() - if key == 'dataformat': - if value == '4': - self.logx, self.logy = True, False # logOhm - elif value == '5': - self.logx, self.logy = True, True # logOhm, logK - elif value not in ('1', '2', '3'): - raise ValueError('invalid Data Format') - elif 'No.' in line: + if line.startswith("Data Format"): + dataformat = line.split(":")[1].strip()[0] + if dataformat == '4': + self.logx, self.logy = True, False # logOhm + elif dataformat == '5': + self.logx, self.logy = True, True # logOhm, logK + elif line.startswith("No."): self.header = False return super().parse(line) @@ -109,9 +104,7 @@ class CalCurve: calibname = sensopt.pop(0) _, dot, ext = basename(calibname).rpartition('.') kind = None - pathlist = os.environ.get('FRAPPY_CALIB_PATH', '').split(',') - pathlist.append(join(dirname(__file__), 'calcurves')) - for path in pathlist: + for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','): # first try without adding kind filename = join(path.strip(), calibname) if exists(filename): @@ -141,26 +134,13 @@ class CalCurve: cls, args = KINDS.get(kind, (StdParser, {})) args.update(optargs) - try: - parser = cls(**args) - with open(filename) as f: - for line in f: - parser.parse(line) - except Exception as e: - raise ValueError('calib curve %s: %s' % (calibspec, e)) + parser = cls(**args) + with open(filename) as f: + for line in f: + parser.parse(line) self.convert_x = nplog if parser.logx else linear self.convert_y = npexp if parser.logy else linear - x = np.asarray(parser.xdata) - y = np.asarray(parser.ydata) - if np.all(x[:-1] > x[1:]): # all decreasing - x = np.flip(x) - y = np.flip(y) - elif np.any(x[:-1] >= x[1:]): # some not increasing - raise ValueError('calib curve %s is not monotonic' % calibspec) - try: - self.spline = splrep(x, y, s=0, k=min(3, len(x) - 1)) - except (ValueError, TypeError): - raise ValueError('invalid calib curve %s' % calibspec) + self.spline = splrep(np.asarray(parser.xdata), np.asarray(parser.ydata), s=0) def __call__(self, value): """convert value @@ -176,23 +156,17 @@ class Sensor(Readable): calib = Parameter('calibration name', datatype=StringType(), readonly=False) abs = Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True) - value = Parameter(datatype=FloatRange(unit='K')) + value = Parameter(unit='K') pollinterval = Parameter(export=False) status = Parameter(default=(Readable.Status.ERROR, 'unintialized')) - pollerClass = None + description = 'a calibrated sensor value' _value_error = None - - def checkProperties(self): - if 'description' not in self.propertyValues: - self.description = '_' # avoid complaining about missing description - super().checkProperties() + enablePoll = False def initModule(self): self._rawsensor.registerCallbacks(self, ['status']) # auto update status self._calib = CalCurve(self.calib) - if self.description == '_': - self.description = '%r calibrated with curve %r' % (self.rawsensor, self.calib) def write_calib(self, value): self._calib = CalCurve(value) @@ -200,7 +174,7 @@ class Sensor(Readable): def update_value(self, value): if self.abs: - value = abs(float(value)) + value = abs(value) self.value = self._calib(value) self._value_error = None