diff --git a/frappy_psi/bridge.py b/frappy_psi/bridge.py index d7399c75..1fad4c76 100644 --- a/frappy_psi/bridge.py +++ b/frappy_psi/bridge.py @@ -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?')