frappy_psi.bridge: changes proposed by claude.ai

Change-Id: Id392d3e5cd8b0f3048d35d6b4146f4193bb2dcc9
This commit is contained in:
zolliker committed 2026-09-11 10:26:12 +02:00
1 parent 574fdabf36
commit 71495aa934
1 file changed
+80 -32
+80 -32
View File
@@ -21,8 +21,9 @@ import re
from bisect import bisect
from frappy.core import StringIO, HasIO, Readable, \
Parameter, FloatRange, IntRange, EnumType, \
Property, Attached, IDLE, ERROR, WARN
Property, Attached, Command, IDLE, ERROR, WARN
from frappy.lib import clamp
from frappy.errors import RangeError
def string_to_value(value):
@@ -31,7 +32,7 @@ def string_to_value(value):
:param value: value
:return: float value without units
"""
value_with_unit = re.compile(r'(\d+)([pnumkMG]?)')
value_with_unit = re.compile(r'(\d+(?:\.\d+)?)([pnumkMG]?)')
value, pfx = value_with_unit.match(value).groups()
pfx_dict = {'p': 1e-12, 'n': 1e-9, 'u': 1e-6, 'm': 1e-3, 'k': 1e3, 'M': 1e6, 'G': 1e9}
if pfx in pfx_dict:
@@ -41,20 +42,22 @@ def string_to_value(value):
def find_idx(list_of_values, target):
"""
Search for the nearest value and index from the given range for the given target.
Search for the nearest value (from above) in the given list for the given target.
:param list_of_values: range of values
:param target: target
:return: closest index and closest value
:return: (closest index, closest value), or (None, None) if nothing in the
list is >= target
"""
target = float(target)
cl_idx = None
cl_value = float('inf')
cl_value = None
best_diff = float('inf')
for idx, value in enumerate(list_of_values):
if value >= target:
diff = value - target
if diff < cl_value:
if diff < best_diff:
best_diff = diff
cl_value = value
cl_idx = idx
@@ -87,8 +90,9 @@ class Base(HasIO):
class Resistance(Base, Readable):
value = Parameter('resistance', datatype=FloatRange, unit='Ohm')
output_offset = Parameter('resistance deviation', datatype=FloatRange, unit='Ohm', readonly=False)
value = Parameter('resistance', datatype=FloatRange(unit='Ohm'))
output_offset = Parameter('resistance setpoint (RSET), subtracted to form the deviation (RDEV)',
datatype=FloatRange(unit='Ohm'), readonly=False)
phase_hold = Parameter('phase hold', EnumType('phase hold', off=0, on=1))
RES_RANGE = ['20mOhm', '200mOhm', '2Ohm', '20Ohm', '200Ohm', '2kOhm', '20kOhm', '200kOhm',
@@ -96,31 +100,62 @@ class Resistance(Base, Readable):
irange = Parameter('resistance range index', EnumType('resistance range index',
{name: idx for idx, name in enumerate(RES_RANGE)}),
readonly=False)
range = Parameter('resistance range value', FloatRange(2e-2, 2e7), unit='Ohm', readonly=False)
range = Parameter('resistance range value', FloatRange(2e-2, 2e7, unit='Ohm'), readonly=False)
TIME_CONST = ['0.3s', '1s', '3s', '10s', '30s', '100s', '300s']
itc = Parameter('time constant index',
EnumType('time const. index range',
{name: value for value, name in enumerate(TIME_CONST)}), readonly=False)
tc = Parameter('time constant value', FloatRange(1e-1, 3e2), unit='s', readonly=False)
tc = Parameter('time constant value', FloatRange(1e-1, 3e2, unit='s'), readonly=False)
EXCT_RANGE = ['0', '3uV', '10uV', '30uV', '100uV', '300uV', '1mV', '3mV', '10mV', '30mV']
iexct = Parameter('excitation index',
EnumType('excitation index range', {name: idx for idx, name in enumerate(EXCT_RANGE, start=-1)}),
readonly=False)
exct = Parameter('excitation value', FloatRange(0, 3e-2), unit='s', default=300, readonly=False)
exct = Parameter('excitation value', FloatRange(0, 3e-2, unit='V'), default=1e-4, readonly=False)
vexc = Parameter("excitation voltage real()", datatype=FloatRange, unit='V', default=0, readonly=True)
autorange = Parameter('autorange_on', EnumType('autorange', off=0, on=1),
readonly=False, default=1)
# Excitation mode (MODE command). Passive is intentionally not supported:
# RVAL? is not a trustworthy closed-loop measurement in passive mode (see
# write_excitation_mode below), so we simply refuse to select it.
EXC_MODE = {'passive': 0, 'current': 1, 'voltage': 2, 'power': 3}
excitation_mode = Parameter('excitation mode', EnumType('excitation mode', **EXC_MODE),
value=2, readonly=False)
RES_RANGE_values = [string_to_value(value) for value in RES_RANGE]
TIME_CONST_values = [string_to_value(value) for value in TIME_CONST]
EXCT_RANGE_values = [string_to_value(value) for value in EXCT_RANGE]
ioClass = BridgeIO
# TODO sensor-break detection.
# This was removed for now: doing it properly does not work purely from
# current/voltage mode. As in the original Tcl driver (sim921::upd /
# sim921::wrmode), it needs to:
# 1. Temporarily switch excitation_mode to passive (bypassing the
# public write_excitation_mode guard below -- e.g. send
# `self.query('mode 0;mode?')` directly internally, since we still
# want to keep passive off-limits as a user-selectable target).
# 2. Read IEXC? (excitation current) and compute an approximate raw
# resistance, e.g. rraw = exref * K1 / iexc - K2 * rref, where exref
# is the nominal excitation voltage for the current EXCI setting and
# rref is half the current range's full scale. See sim921::upd in
# the Tcl driver for the reference implementation; K1=18.2, K2=22
# there appear to be instrument-specific calibration figures we
# were not able to derive independently from the public manual.
# 3. Flag "no sensor" if rraw is far beyond the plausible resistance
# for the configured range (e.g. > 10x range, at the top range),
# with a debounce counter across several checks so a single noisy
# sample doesn't trip it.
# 4. Switch back to voltage/current mode afterwards.
# This disturbs whatever measurement is in progress each time it runs,
# so it should run on a slow cadence (e.g. every N polls, or only when
# something already looks suspicious), not on every poll.
def doPoll(self):
super().doPoll()
if self.autorange == 1:
@@ -177,10 +212,9 @@ class Resistance(Base, Readable):
return int(self.query('rang?'))
def write_irange(self, idx):
value = int(idx)
self.query(f'rang {value}; rang?')
self.query(f'rang {idx}; rang?')
self.read_range()
return value
return idx
def read_range(self):
"""value of the resistance range"""
@@ -194,7 +228,7 @@ class Resistance(Base, Readable):
return cl_value
def read_output_offset(self):
"""Output offset, can be set by user. This is the value subtracted from the measured value"""
"""Setpoint (RSET) subtracted from the measured value to form the deviation (RDEV)."""
return self.query('rset?')
def write_output_offset(self, output_offset):
@@ -202,12 +236,11 @@ class Resistance(Base, Readable):
def read_itc(self):
"""index of the temperature constant value according to the range"""
return self.query('tcon?')
return int(self.query('tcon?'))
def write_itc(self, itc):
self.read_itc()
value = int(itc)
return self.query(f'tcon {value}; tcon?')
return self.query(f'tcon {itc}; tcon?')
def read_tc(self):
idx = self.read_itc()
@@ -219,20 +252,12 @@ class Resistance(Base, Readable):
self.query(f'tcon {cl_idx};tcon?')
return cl_value
def read_autorange(self):
return self.autorange
def write_autorange(self, value):
self.query(f'agai {value:d};agai?')
return value
def read_iexct(self):
"""index of the excitation value according to the range"""
return int(self.query('exci?'))
def write_iexct(self, iexct):
value = int(iexct)
return self.query(f'exci {value};exci?')
return self.query(f'exci {iexct};exci?')
def write_exct(self, target):
target = float(target)
@@ -248,7 +273,10 @@ class Resistance(Base, Readable):
cl_value = value
cl_idx = idx
self.write_iexct(cl_idx)
# EXCT_RANGE_values is indexed 0..9 (list position), but the
# instrument's EXCI codes run -1..8 (index 0 == "off" == code -1;
# see `iexct`'s EnumType above, and read_exct below).
self.write_iexct(cl_idx - 1)
return cl_value
def read_exct(self):
@@ -269,13 +297,33 @@ class Resistance(Base, Readable):
def read_vexc(self):
reply = self.communicate('vexc?')
value = float(reply.strip())
return float(reply.strip())
def read_excitation_mode(self):
return int(self.query('mode?'))
def write_excitation_mode(self, value):
if value == 'passive':
raise RangeError('passive excitation mode is not supported by this driver')
self.query(f'mode {value};mode?')
return value
@Command()
def autogain(self):
"""Trigger a one-shot autogain cycle (AGAI 1).
This re-optimizes the internal preamp/servo gains for the signal
levels currently present; it takes a few seconds to complete on the
instrument. It is unrelated to range autoranging (see the
`autorange` parameter, which is a pure software flag consumed by
doPoll and never touches AGAI).
"""
self.communicate('agai 1;agai?')
class Phase(Readable):
resistance = Attached()
value = Parameter('phase', FloatRange, default=0, unit='deg')
value = Parameter('phase', FloatRange(unit='deg'), default=0)
def read_value(self):
return self.resistance.query('phas?')
@@ -283,7 +331,7 @@ class Phase(Readable):
class Deviation(Readable):
resistance = Attached()
value = Parameter('resistance deviation', FloatRange(), unit='Ohm')
value = Parameter('resistance deviation', FloatRange(unit='Ohm'))
def read_value(self):
return self.resistance.query('rdev?')