diff --git a/frappy_psi/lakeshore.py b/frappy_psi/lakeshore.py index 24d8b4c7..0719713a 100644 --- a/frappy_psi/lakeshore.py +++ b/frappy_psi/lakeshore.py @@ -16,9 +16,8 @@ # Oksana Shliakhtun # Markus Zolliker # ***************************************************************************** -"""driver for various lakeshore temperature monitors/controllers""" +"""base classes for various lakeshore temperature monitors/controllers""" -import time import math import random import threading @@ -992,265 +991,9 @@ class Loop(HasConvergence, HasOutputModule, Sensor): return self.query(f'SETP?{self.output_module.output_no}', float) -# --- MODEL 340 --- - -class IO340(IO): - timeout = 5 # needed for INCRV command - model = 340 - end_of_line = '\r' # default at SINQ. TODO: remove - - -class Device340(Device): - ioClass = IO340 - model = 340 - channels = 'ABCD' - user_curves = (21, 61) # the last curve is 60 - log_formats = True, True # log Ohm / log K is supported - _crvsav_deadline = None - - def disable_channel(self, channel): - self.communicate(f'INSET {channel},0;*OPC?') - - def finish_curve(self, request): - super().finish_curve(request) - if request.loading and not self._crvsav_deadline: - # when loading a new curve, remember for sending CRVSAV later - self._crvsav_deadline = time.time() + 600 - - def put_header(self, curve_no, name, sn, fmt, limit, coef): - self.communicate(f'CRVHDR {curve_no},{name:15s},{sn:10s},{fmt},{limit},{coef};*OPC?') - - def doPoll(self): - super().doPoll() - if self._crvsav_deadline: - # prevent flashing multiple times within short time - if time.time() > self._crvsav_deadline: - self._crvsav_deadline = None - self.communicate('CRVSAV;*OPC?') - - def is_equal(self, left, right, fixeps=(1.1e-5, 1.1e-4), significant=6): - # for whatever reason, the number of digits after decimal point for the T column is only 4 - return super().is_equal(left, right, fixeps, significant) - - -class Sensor340(Sensor): - model = 340 - intype = None # arguments for the intype command - - def get_curve_type(self, calcurve): - unit = calcurve.options.get('unit', 'Ohm') - logformat = False - range_limit = None - if unit == 'Ohm': - if calcurve.ptc: - xlim = calcurve.xrange[1] * 0.001 # 1 mA excitation - for rng, limit in enumerate( - [0, 0.001, 0.0025, 0.005, 0.01, 0.025, 0.05, - 0.1, 0.25, 0.5, 1, 2.5, 5]): - if xlim <= limit: - break - else: - rng = 13 - # we use special type here, in order to allow thermal compensation - # , , , , - self.intype = 0, 2, 2, 10, rng - else: - if calcurve.options.get('type') == 'GE': - self.intype = (10,) - else: - self.intype = (8,) # carbon and ruox are equivalent to cernox(8) - logformat = True, True - elif unit == 'V': # diode - self.intype = (1,) if calcurve.xscale[1] < 2.5 else (2,) - else: # thermocouple - self.intype = (12,) - return logformat, range_limit - - def install_sensor(self): - super().install_sensor() - if self.query(f'INSET?{self.channel}', int, int) != (1, 1): - self.command(f'INSET {self.channel}', 1, 1) - - -class Loop340(Loop, Sensor340): - pass - - -class MainOutput340(MainOutput): - model = 340 - output_no = Parameter(datatype=IntRange(1, 1)) - HTRST_MAP = { - 0: (IDLE, ''), - 1: (ERROR, 'Power supply over voltage'), - 2: (ERROR, 'Power supply under voltage'), - 3: (ERROR, 'Output digital-to-analog Converter error'), - 4: (ERROR, 'Current limit digital-to-analog converter error'), - 5: (ERROR, 'Open heater load'), - 6: (ERROR, 'Heater load less than 10 ohms') - } - _manual_output = 0.0 # TODO: check how to set this - vmax = 50 - imax = 2 - max_currents = {4 - i: 2 ** (1 - i) for i in range(4)} - SETPOINTLIMS = 1500.0 - sorted_factors = sorted([(fhtr * fcur ** 2, (i, h)) - for i, fcur in max_currents.items() - for h, fhtr in MainOutput.heater_ranges.items() - ]) - - def get_status(self): - st = self.query(f'HTRST?', int) - return self.HTRST_MAP[st] - - def configure(self): - icurrent, htr_range = self.get_best_power_idx(self._desired_max_power, 1.1) - self._power_scale = self.max_currents[icurrent] ** 2 * self.heater_ranges[htr_range] / 1e4 - self.command(f'CLIMIT {self.output_no}', self.SETPOINTLIMS, 0.0, 0.0, icurrent, htr_range) - self._htr_range = htr_range - if self._control_loop is None: - mode = self.query(f'CMODE?{self.output_no}', int) - if mode != 3: # open loop - self.command(f'CSET {self.output_no}', '0', 1, 0, 0) # control off - self.command(f'CMODE {self.output_no}', 3) # open loop - self.command('RANGE', self._htr_range) - self.put_manual_power(self._manual_output) - else: - self.command(f'CSET {self.output_no}', self._control_loop.channel, 1, 1, 0) # control on - self.command(f'CMODE {self.output_no}', 1) # pid - self.command('RANGE', self._htr_range) - self.put_manual_power(self._control_loop.power_offset) - self.command(f'CDISP {self.output_no}', 1, self.resistance, 1, 0) - - def fix_heater_range(self): - # switch heater range on, if needed - irng = self.query('RANGE?', int) - if irng != self._htr_range: - if irng: - self.log.info('output range was changed manually') - self._htr_range = irng - else: - self.log.info('output was off - switch on again') - self.command('RANGE', self._htr_range) - - def read_max_power(self): - icurrent, htr_range = self.query(f'CLIMIT? {self.output_no}', float, float, float, int, int)[3:] - htr_range = self.query('RANGE?', int) or htr_range - self._htr_range = htr_range - self._power_scale = self.max_currents[icurrent] ** 2 * self.heater_ranges[htr_range] / 1e4 - return self.calc_power(100) - - def read_htr(self): - return self.query('HTR?', float) - - -class AnalogOutput340(AnalogOutput): - model = 340 - output_no = Parameter(datatype=IntRange(2, 2), default=2) - - def configure(self): - if self._control_loop is not None: - self.put_manual_power(self.target) - else: - self.command('ANALOG 2', 0, 3, self._control_loop.channel) - self.put_manual_power(self._control_loop.power_offset) - - def put_manual_power(self, value): - if self._control_loop is None: - self.command('ANALOG 2', 0, 2, 0, 0, 0, 0, 0, self.calc_percent(value)) - else: - self.command(f'MOUT {self.output_no}', self.calc_percent(value)) - - -# --- MODELS 336, 350, 224, ... class Device2(Device): - """second generation LakeShore models""" + """second generation LakeShore models 336, 350, 224 ...""" channels = 'ABCD' user_curves = (21, 60) # the last curve is 59 max_raw_unit = 99999 TYPES = {'DT': 1, 'TG': 1, 'PT': 2, 'RF': 2, 'CX': 3, 'RX': 3, 'CC': 3, 'GE': 3, 'TC': 4} - - -# --- MODEL 336 --- - -class IO336(IO): - model = 336 - - -class Device336(Device2): - model = 336 - - -class Sensor336(Sensor): - model = 336 - - -class Loop336(Loop, Sensor336): - pass - - -class MainOutput336(MainOutput): - model = 336 - output_no = Parameter(datatype=IntRange(1, 1)) - imax = 2 - vmax = 50 - # 3 ranges only - heater_ranges = {3 - i: 10 ** -i for i in range(3)} - sorted_factors = sorted((v, i) for i, v in heater_ranges.items()) - - -class SecondaryOutput336(MainOutput336): - model = 336 - output_no = Parameter(datatype=IntRange(2, 2)) - imax = 1.414 - vmax = 35.4 - max_power = Parameter(datatype=FloatRange(0, 50, unit='W')) - - -class AnalogOutput336(AnalogOutput): - model = 336 - output_no = Parameter(datatype=IntRange(3, 4)) - - -# --- MODEL 350 --- - -class Device350(Device2): - model = 350 - - -class Sensor350(Sensor): - model = 350 - - def get_curve_type(self, calcurve): - logformat, range_limit = super().get_curve_type(calcurve) - excit = 0 - if self.intype[0] == 3 and calcurve.calibrange[0] > 0.2: - excit = 1 # TODO: add extra parameter for excitation - self.intype += (excit,) - return logformat, range_limit - - -class Loop350(Loop, Sensor350): - pass - - -class MainOutput350(Output): - model = 350 - output_no = Parameter(datatype=IntRange(1, 1)) - imax = 1.732 - max_power = Parameter(datatype=FloatRange(0, 75, unit='W')) - - -class AnalogOutput350(AnalogOutput): - model = 350 - output_no = Parameter(datatype=IntRange(3,4)) - - -# --- MODEL 224 --- - -class Device224(Device2): - model = 224 - channels = 'A', 'B', 'C1', 'C2', 'C3', 'C4', 'C5', 'D1', 'D2', 'D3', 'D4', 'D5' - - -class Sensor224(Sensor): - model = 224 diff --git a/frappy_psi/lakeshore224.py b/frappy_psi/lakeshore224.py new file mode 100644 index 00000000..a322f9d9 --- /dev/null +++ b/frappy_psi/lakeshore224.py @@ -0,0 +1,33 @@ +# ***************************************************************************** +# 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 +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""LakeShore Model 224""" + +import frappy_psi.lakeshore as ls + + +class IO(ls.IO): + model = 224 + + +class Device(ls.Device2): + model = 224 + channels = 'A', 'B', 'C1', 'C2', 'C3', 'C4', 'C5', 'D1', 'D2', 'D3', 'D4', 'D5' + + +class Sensor(ls.Sensor): + model = 224 diff --git a/frappy_psi/lakeshore336.py b/frappy_psi/lakeshore336.py new file mode 100644 index 00000000..634e0461 --- /dev/null +++ b/frappy_psi/lakeshore336.py @@ -0,0 +1,61 @@ +# ***************************************************************************** +# 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 +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""LakeShore Model 336""" + +from frappy.core import Parameter, IntRange, FloatRange +import frappy_psi.lakeshore as ls + + +class IO(ls.IO): + model = 336 + + +class Device(ls.Device2): + model = 336 + + +class Sensor(ls.Sensor): + model = 336 + + +class Loop(ls.Loop, Sensor): + pass + + +class MainOutput(ls.MainOutput): + model = 336 + output_no = Parameter(datatype=IntRange(1, 1)) + imax = 2 + vmax = 50 + # 3 ranges only + heater_ranges = {3 - i: 10 ** -i for i in range(3)} + sorted_factors = sorted((v, i) for i, v in heater_ranges.items()) + + +class SecondaryOutput336(MainOutput): + model = 336 + output_no = Parameter(datatype=IntRange(2, 2)) + imax = 1.414 + vmax = 35.4 + max_power = Parameter(datatype=FloatRange(0, 50, unit='W')) + + +class AnalogOutput(ls.AnalogOutput): + model = 336 + output_no = Parameter(datatype=IntRange(3, 4)) + diff --git a/frappy_psi/lakeshore340.py b/frappy_psi/lakeshore340.py new file mode 100644 index 00000000..b6b3c7dd --- /dev/null +++ b/frappy_psi/lakeshore340.py @@ -0,0 +1,194 @@ +# ***************************************************************************** +# 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 +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""LakeShore Model 340""" + +import time +from frappy.core import Parameter, IDLE, ERROR, IntRange +import frappy_psi.lakeshore as ls + + +class IO(ls.IO): + timeout = 5 # needed for INCRV command + model = 340 + end_of_line = '\r' # default at SINQ. TODO: remove + + +class Device(ls.Device): + ioClass = IO + model = 340 + channels = 'ABCD' + user_curves = (21, 61) # the last curve is 60 + log_formats = True, True # log Ohm / log K is supported + _crvsav_deadline = None + + def disable_channel(self, channel): + self.communicate(f'INSET {channel},0;*OPC?') + + def finish_curve(self, request): + super().finish_curve(request) + if request.loading and not self._crvsav_deadline: + # when loading a new curve, remember for sending CRVSAV later + self._crvsav_deadline = time.time() + 600 + + def put_header(self, curve_no, name, sn, fmt, limit, coef): + self.communicate(f'CRVHDR {curve_no},{name:15s},{sn:10s},{fmt},{limit},{coef};*OPC?') + + def doPoll(self): + super().doPoll() + if self._crvsav_deadline: + # prevent flashing multiple times within short time + if time.time() > self._crvsav_deadline: + self._crvsav_deadline = None + self.communicate('CRVSAV;*OPC?') + + def is_equal(self, left, right, fixeps=(1.1e-5, 1.1e-4), significant=6): + # for whatever reason, the number of digits after decimal point for the T column is only 4 + return super().is_equal(left, right, fixeps, significant) + + +class Sensor340(ls.Sensor): + model = 340 + intype = None # arguments for the intype command + + def get_curve_type(self, calcurve): + unit = calcurve.options.get('unit', 'Ohm') + logformat = False + range_limit = None + if unit == 'Ohm': + if calcurve.ptc: + xlim = calcurve.xrange[1] * 0.001 # 1 mA excitation + for rng, limit in enumerate( + [0, 0.001, 0.0025, 0.005, 0.01, 0.025, 0.05, + 0.1, 0.25, 0.5, 1, 2.5, 5]): + if xlim <= limit: + break + else: + rng = 13 + # we use special type here, in order to allow thermal compensation + # , , , , + self.intype = 0, 2, 2, 10, rng + else: + if calcurve.options.get('type') == 'GE': + self.intype = (10,) + else: + self.intype = (8,) # carbon and ruox are equivalent to cernox(8) + logformat = True, True + elif unit == 'V': # diode + self.intype = (1,) if calcurve.xscale[1] < 2.5 else (2,) + else: # thermocouple + self.intype = (12,) + return logformat, range_limit + + def install_sensor(self): + super().install_sensor() + if self.query(f'INSET?{self.channel}', int, int) != (1, 1): + self.command(f'INSET {self.channel}', 1, 1) + + +class Loop340(ls.Loop, Sensor340): + pass + + +class MainOutput340(ls.MainOutput): + ioClass = IO + model = 340 + output_no = Parameter(datatype=IntRange(1, 1)) + HTRST_MAP = { + 0: (IDLE, ''), + 1: (ERROR, 'Power supply over voltage'), + 2: (ERROR, 'Power supply under voltage'), + 3: (ERROR, 'Output digital-to-analog Converter error'), + 4: (ERROR, 'Current limit digital-to-analog converter error'), + 5: (ERROR, 'Open heater load'), + 6: (ERROR, 'Heater load less than 10 ohms') + } + _manual_output = 0.0 # TODO: check how to set this + vmax = 50 + imax = 2 + max_currents = {4 - i: 2 ** (1 - i) for i in range(4)} + SETPOINTLIMS = 1500.0 + sorted_factors = sorted([(fhtr * fcur ** 2, (i, h)) + for i, fcur in max_currents.items() + for h, fhtr in MainOutput.heater_ranges.items() + ]) + + def get_status(self): + st = self.query(f'HTRST?', int) + return self.HTRST_MAP[st] + + def configure(self): + if self._desired_max_power is None: + self.log.info(f'max_heater {self.writeDict} {self.max_heater}') + self.write_max_heater(self.max_heater) + icurrent, htr_range = self.get_best_power_idx(self._desired_max_power, 1.1) + self._power_scale = self.max_currents[icurrent] ** 2 * self.heater_ranges[htr_range] / 1e4 + self.command(f'CLIMIT {self.output_no}', self.SETPOINTLIMS, 0.0, 0.0, icurrent, htr_range) + self._htr_range = htr_range + if self._control_loop is None: + mode = self.query(f'CMODE?{self.output_no}', int) + if mode != 3: # open loop + self.command(f'CSET {self.output_no}', '0', 1, 0, 0) # control off + self.command(f'CMODE {self.output_no}', 3) # open loop + self.command('RANGE', self._htr_range) + self.put_manual_power(self._manual_output) + else: + self.command(f'CSET {self.output_no}', self._control_loop.channel, 1, 1, 0) # control on + self.command(f'CMODE {self.output_no}', 1) # pid + self.command('RANGE', self._htr_range) + self.put_manual_power(self._control_loop.power_offset) + self.command(f'CDISP {self.output_no}', 1, self.resistance, 1, 0) + + def fix_heater_range(self): + # switch heater range on, if needed + irng = self.query('RANGE?', int) + if irng != self._htr_range: + if irng: + self.log.info('output range was changed manually') + self._htr_range = irng + else: + self.log.info('output was off - switch on again') + self.command('RANGE', self._htr_range) + + def read_max_power(self): + icurrent, htr_range = self.query(f'CLIMIT? {self.output_no}', float, float, float, int, int)[3:] + htr_range = self.query('RANGE?', int) or htr_range + self._htr_range = htr_range + self._power_scale = self.max_currents[icurrent] ** 2 * self.heater_ranges[htr_range] / 1e4 + return self.calc_power(100) + + def read_htr(self): + return self.query('HTR?', float) + + +class AnalogOutput340(ls.AnalogOutput): + model = 340 + output_no = Parameter(datatype=IntRange(2, 2), default=2) + + def configure(self): + if self._control_loop is not None: + self.put_manual_power(self.target) + else: + self.command('ANALOG 2', 0, 3, self._control_loop.channel) + self.put_manual_power(self._control_loop.power_offset) + + def put_manual_power(self, value): + if self._control_loop is None: + self.command('ANALOG 2', 0, 2, 0, 0, 0, 0, 0, self.calc_percent(value)) + else: + self.command(f'MOUT {self.output_no}', self.calc_percent(value)) + diff --git a/frappy_psi/lakeshore350.py b/frappy_psi/lakeshore350.py new file mode 100644 index 00000000..1d0db9f8 --- /dev/null +++ b/frappy_psi/lakeshore350.py @@ -0,0 +1,57 @@ +# ***************************************************************************** +# 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 +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Markus Zolliker +# ***************************************************************************** +"""LakeShore Model 350""" + +from frappy.core import Parameter, IntRange, FloatRange +import frappy_psi.lakeshore as ls + + +class IO(ls.IO): + model = 350 + + +class Device(ls.Device2): + model = 350 + + +class Sensor(ls.Sensor): + model = 350 + + def get_curve_type(self, calcurve): + logformat, range_limit = super().get_curve_type(calcurve) + excit = 0 + if self.intype[0] == 3 and calcurve.calibrange[0] > 0.2: + excit = 1 # TODO: add extra parameter for excitation + self.intype += (excit,) + return logformat, range_limit + + +class Loop(ls.Loop, Sensor): + pass + + +class MainOutput(ls.Output): + model = 350 + output_no = Parameter(datatype=IntRange(1, 1)) + imax = 1.732 + max_power = Parameter(datatype=FloatRange(0, 75, unit='W')) + + +class AnalogOutput(ls.AnalogOutput): + model = 350 + output_no = Parameter(datatype=IntRange(3,4))