From 7be29bc4d1f16d99b2ac7ddc357697ce595d17b2 Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Mon, 6 Jul 2026 10:17:23 +0200 Subject: [PATCH] state as of 6.7.2026 - use new function matches_with_string_precision - chk_crvpts instead of get_crvpts and then check Change-Id: I724f23edfa6b5e269950073641b6e852cfa03713 --- cfg/lakeshore370_test_cfg.py | 10 +-- frappy/lib/__init__.py | 12 ++++ frappy_psi/channelswitcher.py | 5 +- frappy_psi/lakeshore.py | 113 ++++++++++++++++++++-------------- frappy_psi/lakeshore370.py | 12 +++- 5 files changed, 96 insertions(+), 56 deletions(-) diff --git a/cfg/lakeshore370_test_cfg.py b/cfg/lakeshore370_test_cfg.py index 4e4ca1cd..0197a076 100644 --- a/cfg/lakeshore370_test_cfg.py +++ b/cfg/lakeshore370_test_cfg.py @@ -3,9 +3,11 @@ Node('ls370test.psi.ch', interface='tcp://5000', ) -### temperature monitoring lakeshore (tmon) ### +IO('io_treg', 'dil2-ts:3001') +IO('io_tmon', 'dil2-ts:3003') -IO('io_tmon', 'dil3-ts:3003') + +### temperature monitoring lakeshore (tmon) ### Mod('tmon', 'frappy_psi.lakeshore370.Device', @@ -88,8 +90,6 @@ Mod('onek', ### temperature regulating lakeshore (treg) ### -IO('io_treg', 'dil3-ts:3001') - Mod('treg', 'frappy_psi.lakeshore370.Device', 'regulating lakeshore 370', @@ -161,4 +161,4 @@ Mod('relais', # io = 'io', # channel = 5, # switcher = 'switcher', -# ) \ No newline at end of file +# ) diff --git a/frappy/lib/__init__.py b/frappy/lib/__init__.py index c577fe7e..4e6a92e3 100644 --- a/frappy/lib/__init__.py +++ b/frappy/lib/__init__.py @@ -32,6 +32,7 @@ import traceback from configparser import ConfigParser from os import environ, path from pathlib import Path +from decimal import Decimal SECoP_DEFAULT_PORT = 10767 @@ -496,3 +497,14 @@ def delayed_import(modname): except Exception: return _Raiser(modname) return module + + +def matches_with_string_precision(floatstring, value: float) -> bool: + """check if floatstring is equal to value within precision derived from floatstring + + the result is tolerant to any deviation less than one unit of the last given digit + created with the help of Claude AI + """ + target = Decimal(floatstring) + quantizer = Decimal(1).scaleb(target.as_tuple().exponent) + return abs(Decimal(str(value)) - target) < quantizer \ No newline at end of file diff --git a/frappy_psi/channelswitcher.py b/frappy_psi/channelswitcher.py index 8cd9e8f6..5cb06b16 100644 --- a/frappy_psi/channelswitcher.py +++ b/frappy_psi/channelswitcher.py @@ -38,7 +38,7 @@ switcher=sw import time from frappy.datatypes import IntRange, BoolType, FloatRange -from frappy.core import Attached, Property, Drivable, Parameter, Readable +from frappy.core import Attached, Property, Drivable, Parameter, Readable, ERROR class ChannelSwitcher(Drivable): @@ -116,8 +116,9 @@ class ChannelSwitcher(Drivable): self.status = 'IDLE', 'measure' self.value = self.target self._start_measure = self._last_measure = now - chan.read_value() chan.read_status() + if chan.status[0] < ERROR: + chan.read_value() if self.measure_delay > self._time_tol: return self.status else: diff --git a/frappy_psi/lakeshore.py b/frappy_psi/lakeshore.py index bf78431e..f193ace7 100644 --- a/frappy_psi/lakeshore.py +++ b/frappy_psi/lakeshore.py @@ -15,6 +15,7 @@ # Module authors: # Oksana Shliakhtun # Markus Zolliker +# Anik Stark # ***************************************************************************** """base classes for various lakeshore temperature monitors/controllers""" @@ -31,11 +32,11 @@ from frappy.datatypes import IntRange, FloatRange, StringType, \ from frappy.errors import CommunicationFailedError, ConfigError, \ HardwareError, DisabledError, ImpossibleError, secop_error, SECoPError from frappy.lib.units import NumberWithUnit, format_with_unit -from frappy.lib import formatStatusBits +from frappy.lib import formatStatusBits, matches_with_string_precision from frappy.lib.enum import EnumMember from frappy_psi.calcurve import CalCurve from frappy_psi.convergence import HasConvergence -from frappy.mixins import HasOutputModule, HasControlledBy +from frappy.ctrlby import HasOutputModule, HasControlledBy from frappy.extparams import StructParam @@ -269,18 +270,13 @@ class Device(HasLscIO, Module): """preliminary check: check if the ends of the curve are matching the stored ones""" npnt = len(request.points) numbers = [1, npnt] - points = [request.points[0], request.points[npnt-1]] if npnt < self.max_curve_length: numbers.append(npnt + 1) - points.append((0, 0)) - stored_points = self.get_crvpts(no, *numbers) - for pairs in zip(points, stored_points): - if not self.is_equal(*pairs): - self.log.info('not equal %r', pairs) - self.log.info('requested points %r', points) - self.log.info('stored points %r', stored_points) - return False - return True + mismatch = self.chk_crvpts(no, numbers, request.points) + if mismatch is None: + return True + self.log.info('%s', *mismatch) + return False def find_curve(self, request): """try to find curve and return required action @@ -341,21 +337,20 @@ class Device(HasLscIO, Module): sensors = request.new_sensors request.new_sensors = set() request.install_sensors(sensors) - given = request.points[request.pointer:request.pointer + self.cmds_per_line] - first = request.pointer + 1 + num1 = request.pointer + 1 + ngiven = list(range(num1, min(len(request.points) + 1, num1 + self.cmds_per_line))) no = request.curve_no - for n, (pt, ptg) in enumerate(zip(self.get_crvpts(no, *range(first, first + len(given))), given)): - if not self.is_equal(pt, ptg): - self.log.info('reply (%g, %g) does not match given point %d (%g,%g)', - *pt, first + n, *ptg) - request.invalidate = no - request.set_curve_no(self.get_empty()) - self.log.info('%s has changed, create #%d', request.crvhdr.sn, request.curve_no) - request.loading = True - return self.start_load - - request.pointer += self.cmds_per_line - return self.check_points + mismatch = self.chk_crvpts(no, ngiven, request.points) + if not mismatch: + request.pointer += self.cmds_per_line + # continue soon + return self.check_points + self.log.info('%s', mismatch) + request.invalidate = no + request.set_curve_no(self.get_empty()) + self.log.info('%s has changed, create #%d', request.crvhdr.sn, request.curve_no) + request.loading = True + return self.start_load def start_load(self, request): """start loading a curve @@ -436,7 +431,7 @@ class Device(HasLscIO, Module): return abs(v1 - v2) < eps return True - def get_crvpts(self, curve_no, *numbers): + def get_crvpts(self, curve_no, *numbers): # TODO: remove as obsolete """read curve points :param curve_no: curve number @@ -449,6 +444,29 @@ class Device(HasLscIO, Module): replies.extend(self.communicate(';'.join(cmds)).split(';')) return [[float(v) for v in xy.split(',')] for xy in replies] + def chk_crvpts(self, curve_no, numbers, values): + """check curve points + + :param curve_no: curve number + :param numbers: pint numbers of points to check (starting at 1) + :param values: list or numpy array of points (x, y) (indices starting at 0) + :return: None on success, first mismatch (idx, strvalue, value) on failure + + Note: for checking the point after the end of the table to be (0,0) an + index == len(values) may be given + """ + for i in range(0, len(numbers), self.cmds_per_line): + pntno = numbers[i:i+self.cmds_per_line] + cmds = [f'CRVPT?{curve_no},{n}' for n in pntno] + replies = self.communicate(';'.join(cmds)).split(';') + for pair, no in zip(replies, pntno): + given = values[no - 1] if no <= len(values) else (0, 0) + for strvalue, value in zip(pair.split(','), given): + if not matches_with_string_precision(strvalue, value): + x, y = given + return f'point {no}: {pair} does not match given point ({x:g},{y:g})' + return None + def get_headers(self): """get all headers @@ -689,27 +707,23 @@ class Sensor(SensorBase): return rdgst, raw, value def read_status(self): - status, self.value, self.raw = self.get_data() - if self._raw_error: - self.announceUpdate('raw', err=self._raw_error) - if self._value_error: - self.announceUpdate('value', err=self._value_error) + status, value, raw = self.get_data() + self.announceUpdate('raw', raw, self._raw_error) + self.announceUpdate('value', value, self._value_error) return status @nopoll def read_raw(self): - self.status, self.value, raw = self.get_data() - if self._value_error: - self.announceUpdate('value', err=self._value_error) + self.status, value, raw = self.get_data() + self.announceUpdate('value', value, self._value_error) if self._raw_error: raise self._raw_error return raw @nopoll def read_value(self): - self.status, value, self.raw = self.get_data() - if self._raw_error: - self.announceUpdate('raw', err=self._raw_error) + self.status, value, raw = self.get_data() + self.announceUpdate('raw', raw, err=self._raw_error) if self._value_error: raise self._value_error return value @@ -783,7 +797,6 @@ class Output(Base, HasControlledBy, Writable): sorted_factors = None errorstatus = None _desired_max_power = None - _control_loop = None # a loop module object when controlled, None when in manual mode power_offset = 0 # offset for closed_loop def configure(self): @@ -793,6 +806,10 @@ class Output(Base, HasControlledBy, Writable): """ raise NotImplementedError + def get_control_loop(self): + """get controlling module object or None""" + return None if self.controlled_by == 'self' else self.secNode.modules[self.controlled_by.name] + def fix_heater_range(self): """switch on heater range, if off""" @@ -897,8 +914,10 @@ class MainOutput(Output): self.heater_ranges[irng] / self.resistance))) self._power_scale = user_current ** 2 * self.heater_ranges[irng] / 1e4 self.command(f'HTRSET {self.output_no}', 1 if self.resistance < 50 else 2, 0, user_current, 1) + control_loop = self.get_control_loop() + self.log.info('configure %r', control_loop) # self.command(f'CDISP {self.output_no}', 1, self.resistance, 1, 0) - if self._control_loop is None: + if control_loop is None: mode = self.query(f'OUTMODE?{self.output_no}', int) if mode != 3: # open loop self.command(f'OUTMODE {self.output_no}', 3) # control off @@ -906,9 +925,9 @@ class MainOutput(Output): self.command(f'RANGE {self.output_no}', self._htr_range) self.put_manual_power(self.target) else: - self.command(f'OUTMODE {self.output_no}', 1, self._control_loop.channel, 0) # control on + self.command(f'OUTMODE {self.output_no}', 1, control_loop.channel, 0) # control on self.command(f'RANGE {self.output_no}', self._htr_range) - self.put_manual_power(self._control_loop.power_offset) + self.put_manual_power(control_loop.power_offset) def read_max_power(self): curidx, self._user_current = self.query(f'HTRSET? {self.output_no}', int, int, float)[1:3] # ! cmd not found in ls370 manual (HTRRNG?) @@ -962,14 +981,15 @@ class AnalogOutput(Output): self.imax ** 2 / self.resistance) def configure(self): - if self._control_loop is None: + control_loop = self.get_control_loop() + if control_loop is None: # 3: open loop, 0: powerup enable off self.command(f'OUTMODE {self.output_no}', 3, 0, 0) self.put_manual_power(self.target) else: # 1: closed loop, 0: powerup enable off - self.command(f'OUTMODE {self.output_no}', 1, self._control_loop.channel, 0) - self.put_manual_power(self._control_loop.power_offset) + self.command(f'OUTMODE {self.output_no}', 1, control_loop.channel, 0) + self.put_manual_power(control_loop.power_offset) def read_htr(self): return self.query(f'AOUT?{self.output_no}', float) @@ -1017,7 +1037,8 @@ class Loop(HasConvergence, HasOutputModule, Sensor): def get_target(self): return self.query(f'SETP?{self.loop()}', float) - def set_control_active(self, active): + def write_control_active(self, active): + self.log.info('control_active %r', active) if active: controlled_by = self.name if self.output_module.controlled_by != controlled_by: diff --git a/frappy_psi/lakeshore370.py b/frappy_psi/lakeshore370.py index 73ec340b..07619800 100644 --- a/frappy_psi/lakeshore370.py +++ b/frappy_psi/lakeshore370.py @@ -199,6 +199,7 @@ class SensorBase(Ls370, ls.SensorBase, Channel): inset_params = 'enabled', 'pause', 'dwell' _curve = None tempco = None + STATUS_BIT_LABELS = 'cs_ovl vcm_ovl vmix_ovl vdif_ovl r_over r_under t_over t_under'.split() def initModule(self): # take io from switcher @@ -411,6 +412,8 @@ class TemperatureLoop(ls.Loop, Sensor, Drivable): # return 0 def set_target(self, target): + self.log.info('set_target %g out.controlled_by %s %s %r', target, self.output_module.name, + self.output_module.controlled_by, self.channel) outmode = 1 prev = self.query(f'CMODE?', int) if outmode != prev: @@ -427,6 +430,7 @@ class TemperatureLoop(ls.Loop, Sensor, Drivable): HEATER_RANGES = {8 - i: 10 ** -i for i in range(8)} + class MainOutput(ls.MainOutput): ioClass = IO model = 370 @@ -461,7 +465,8 @@ class MainOutput(ls.MainOutput): 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._htr_range = htr_range - if self._control_loop is None: + control_loop = self.get_control_loop() + if control_loop is None: mode = self.query(f'CMODE?', int) if mode != 3: # open loop self.command(f'CSET', '0', 1, 1, 1, 1, self._htr_range, self.resistance) # control off @@ -469,10 +474,11 @@ class MainOutput(ls.MainOutput): self.command('HTRRNG', self._htr_range) self.put_manual_power(self._manual_output) else: - self.command(f'CSET', self._control_loop.channel, 1, 1, 1, 1, self._htr_range, self.resistance) # control on + self.log.info('control_loop %r, %r', control_loop, control_loop.channel) + self.command(f'CSET', control_loop.channel, 1, 1, 1, 1, self._htr_range, self.resistance) # control on self.command(f'CMODE', 1) # pid self.command('HTRRNG', self._htr_range) - self.put_manual_power(self._control_loop.power_offset) + self.put_manual_power(control_loop.power_offset) def fix_heater_range(self): # switch heater range on, if needed