frappy_psi.lakeshore: split out model dependent part

Change-Id: I02eed008c1e6e29f9d2c2a2dfb2086a62407dd60
This commit is contained in:
2026-03-18 13:45:56 +01:00
parent 733252faf7
commit a60786f983
5 changed files with 347 additions and 259 deletions
+2 -259
View File
@@ -16,9 +16,8 @@
# Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
# Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************
"""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
# <type>, <units>, <coefficient>, <excitation>, <range>
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
+33
View File
@@ -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 <markus.zolliker@psi.ch>
# *****************************************************************************
"""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
+61
View File
@@ -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 <markus.zolliker@psi.ch>
# *****************************************************************************
"""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))
+194
View File
@@ -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 <markus.zolliker@psi.ch>
# *****************************************************************************
"""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
# <type>, <units>, <coefficient>, <excitation>, <range>
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))
+57
View File
@@ -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 <markus.zolliker@psi.ch>
# *****************************************************************************
"""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))