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
This commit is contained in:
2026-07-06 10:17:23 +02:00
parent a4ede7cfef
commit 7be29bc4d1
5 changed files with 96 additions and 56 deletions
+5 -5
View File
@@ -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',
# )
# )
+12
View File
@@ -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
+3 -2
View File
@@ -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:
+67 -46
View File
@@ -15,6 +15,7 @@
# Module authors:
# Oksana Shliakhtun <oksana.shliakhtun@psi.ch>
# Markus Zolliker <markus.zolliker@psi.ch>
# Anik Stark <anik.stark@psi.ch>
# *****************************************************************************
"""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:
+9 -3
View File
@@ -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