diff --git a/apstools/devices/srs570_preamplifier.py b/apstools/devices/srs570_preamplifier.py index 7e839a875..6d4912ef1 100644 --- a/apstools/devices/srs570_preamplifier.py +++ b/apstools/devices/srs570_preamplifier.py @@ -21,12 +21,158 @@ import pint from ophyd import Component from ophyd import EpicsSignal +from ophyd.signal import DEFAULT_WRITE_TIMEOUT from .preamp_base import PreamplifierBaseDevice logger = logging.getLogger(__name__) +gain_units = ["pA/V", "nA/V", "uA/V", "mA/V"] +gain_values = ["1", "2", "5", "10", "20", "50", "100", "200", "500"] +gain_modes = ["LOW NOISE", "HIGH BW"] + +# Settling times measured from the 25-ID-C upstream I0 chamber's SR-570 +# (sensitivity_value, sensitivity_unit, gain_mode): settle_time +settling_times = { + # pA/V + ("1", "pA/V", "HIGH BW"): 2.5, + ("2", "pA/V", "HIGH BW"): 2, + ("5", "pA/V", "HIGH BW"): 2.0, + ("10", "pA/V", "HIGH BW"): 0.5, + ("20", "pA/V", "HIGH BW"): 0.5, + ("50", "pA/V", "HIGH BW"): 0.5, + ("100", "pA/V", "HIGH BW"): 0.5, + ("200", "pA/V", "HIGH BW"): 0.3, + ("500", "pA/V", "HIGH BW"): 0.3, + ("1", "pA/V", "LOW NOISE"): 3.0, + ("2", "pA/V", "LOW NOISE"): 2.5, + ("5", "pA/V", "LOW NOISE"): 2.0, + ("10", "pA/V", "LOW NOISE"): 2.0, + ("20", "pA/V", "LOW NOISE"): 1.75, + ("50", "pA/V", "LOW NOISE"): 1.5, + ("100", "pA/V", "LOW NOISE"): 1.25, + ("200", "pA/V", "LOW NOISE"): 0.5, + ("500", "pA/V", "LOW NOISE"): 0.5, +} +settling_times.update( + { + # nA/V, high bandwidth + (gain_values[idx], "nA/V", "HIGH BW"): 0.3 + for idx in range(9) + } +) +settling_times.update( + { + # nA/V, low noise + (gain_values[idx], "nA/V", "LOW NOISE"): 0.3 + for idx in range(9) + } +) +settling_times.update( + { + # μA/V, high bandwidth + (gain_values[idx], "uA/V", "HIGH BW"): 0.3 + for idx in range(9) + } +) +settling_times.update( + { + # μA/V, low noise + (gain_values[idx], "uA/V", "LOW NOISE"): 0.3 + for idx in range(9) + } +) +settling_times.update( + { + ("1", "mA/V", "HIGH BW"): 0.3, + ("1", "mA/V", "LOW NOISE"): 0.3, + } +) + + +def calculate_settle_time(gain_value: int, gain_unit: int, gain_mode: str): + """Determine the best settle time for a given combination of parameters. + + Parameters can be strings of indexes. + + """ + # Convert indexes to string values + try: + gain_value = gain_values[gain_value] + except (TypeError, IndexError): + pass + try: + gain_unit = gain_units[gain_unit] + except (TypeError, IndexError): + pass + try: + gain_mode = gain_modes[gain_mode] + except (TypeError, IndexError): + pass + # Get calibrated settle time, or None to use the Ophyd default + return settling_times.get((gain_value, gain_unit, gain_mode)) + + +class GainSignal(EpicsSignal): + """A signal where the settling time depends on the pre-amp gain. + + Used to introduce a specific settle time when setting to account + for the amp's R–C relaxation time when changing gain. + + """ + + def set(self, value, *, timeout=DEFAULT_WRITE_TIMEOUT, settle_time="auto"): + """Set the value of the Signal and return a Status object. + + If put completion is used for this EpicsSignal, the status object will + complete once EPICS reports the put has completed. + + Otherwise the readback will be polled until equal to the set point (as + in `Signal.set`) + + Parameters + ---------- + value : any + timeout : float, optional + Maximum time to wait. + settle_time: float, optional + Delay after the set() has completed to indicate completion + to the caller. If "auto" (default), a reasonable settle + time will be chosen based on the gain mode on the pre-amp. + + Returns + ------- + st : Status + + See Also + -------- + Signal.set + EpicsSignal.set + + """ + # Determine optimal settling time. + if settle_time == "auto": + signals = [self.parent.sensitivity_value, self.parent.sensitivity_unit, self.parent.gain_mode] + args = [value if self is sig else sig.get() for sig in signals] + val, unit, mode = args + # Resolve string values to indices if provided + if val in gain_values: + val = gain_values.index(val) + if unit in gain_units: + unit = gain_units.index(unit) + if mode in gain_modes: + mode = gain_modes.index(mode) + # Low-drift mode uses the same settling times as low-noise mode + if mode == "LOW DRIFT": + mode = "LOW NOISE" + # Calculate settling time + _settle_time = calculate_settle_time(gain_value=val, gain_unit=unit, gain_mode=mode) + else: + _settle_time = settle_time + return super().set(value, timeout=timeout, settle_time=_settle_time) + + class SRS570_PreAmplifier(PreamplifierBaseDevice): """ Ophyd support for Stanford Research Systems 570 preamplifier from synApps. @@ -36,8 +182,8 @@ class SRS570_PreAmplifier(PreamplifierBaseDevice): # in the EPICS .db file. Must cast them to ``float()`` or ``int()`` # as desired. # see: https://github.com/epics-modules/ip/blob/master/ipApp/Db/SR570.db - sensitivity_value = Component(EpicsSignal, "sens_num", kind="config", string=True) - sensitivity_unit = Component(EpicsSignal, "sens_unit", kind="config", string=True) + sensitivity_value = Component(GainSignal, "sens_num", kind="config", string=True) + sensitivity_unit = Component(GainSignal, "sens_unit", kind="config", string=True) offset_on = Component(EpicsSignal, "offset_on", kind="config", string=True) offset_sign = Component(EpicsSignal, "offset_sign", kind="config", string=True) @@ -46,7 +192,7 @@ class SRS570_PreAmplifier(PreamplifierBaseDevice): offset_fine = Component(EpicsSignal, "off_u_put", kind="config", string=True) offset_cal = Component(EpicsSignal, "offset_cal", kind="config", string=True) - set_all = Component(EpicsSignal, "init.PROC", kind="config") + set_all = Component(GainSignal, "init.PROC", kind="config") bias_value = Component(EpicsSignal, "bias_put", kind="config", string=True) bias_on = Component(EpicsSignal, "bias_on", kind="config", string=True) @@ -55,7 +201,7 @@ class SRS570_PreAmplifier(PreamplifierBaseDevice): filter_lowpass = Component(EpicsSignal, "low_freq", kind="config", string=True) filter_highpass = Component(EpicsSignal, "high_freq", kind="config", string=True) - gain_mode = Component(EpicsSignal, "gain_mode", kind="config", string=True) + gain_mode = Component(GainSignal, "gain_mode", kind="config", string=True) invert = Component(EpicsSignal, "invert_on", kind="config", string=True) blank = Component(EpicsSignal, "blank_on", kind="config", string=True) diff --git a/apstools/devices/tests/test_srs570_preamplifier.py b/apstools/devices/tests/test_srs570_preamplifier.py new file mode 100644 index 000000000..2c52955d4 --- /dev/null +++ b/apstools/devices/tests/test_srs570_preamplifier.py @@ -0,0 +1,148 @@ +import pytest +from unittest import mock + +from ..srs570_preamplifier import SRS570_PreAmplifier, GainSignal, DEFAULT_WRITE_TIMEOUT + +# Known settling times measured from the I0 SR-570 at 25-ID-C +settling_times = { + # (sensitivity_value, sensitivity_unit, gain_mode): settle_time + # pA/V + (0, 0, "HIGH BW"): 2.5, # 1 pA/V + (1, 0, "HIGH BW"): 2.0, + (2, 0, "HIGH BW"): 2.0, + (3, 0, "HIGH BW"): 0.5, + (4, 0, "HIGH BW"): 0.5, + (5, 0, "HIGH BW"): 0.5, # 50 pA/V + (6, 0, "HIGH BW"): 0.5, + (7, 0, "HIGH BW"): 0.3, + (8, 0, "HIGH BW"): 0.3, + # nA/V + (0, 1, "HIGH BW"): 0.3, + (1, 1, "HIGH BW"): 0.3, # 2 nA/V + (2, 1, "HIGH BW"): 0.3, + (3, 1, "HIGH BW"): 0.3, + (4, 1, "HIGH BW"): 0.3, + (5, 1, "HIGH BW"): 0.3, + (6, 1, "HIGH BW"): 0.3, # 100 nA/V + (7, 1, "HIGH BW"): 0.3, + (8, 1, "HIGH BW"): 0.3, + # μA/V + (0, 2, "HIGH BW"): 0.3, + (1, 2, "HIGH BW"): 0.3, + (2, 2, "HIGH BW"): 0.3, # 5 μA/V + (3, 2, "HIGH BW"): 0.3, + (4, 2, "HIGH BW"): 0.3, + (5, 2, "HIGH BW"): 0.3, + (6, 2, "HIGH BW"): 0.3, + (7, 2, "HIGH BW"): 0.3, # 200 μA/V + (8, 2, "HIGH BW"): 0.3, + # mA/V + (0, 3, "HIGH BW"): 0.3, + (1, 3, "HIGH BW"): None, + (2, 3, "HIGH BW"): None, + (3, 3, "HIGH BW"): None, + (4, 3, "HIGH BW"): None, + (5, 3, "HIGH BW"): None, + (6, 3, "HIGH BW"): None, + (7, 3, "HIGH BW"): None, + (8, 3, "HIGH BW"): None, + # pA/V + (0, 0, "LOW NOISE"): 3.0, + (1, 0, "LOW NOISE"): 2.5, + (2, 0, "LOW NOISE"): 2.0, + (3, 0, "LOW NOISE"): 2.0, + (4, 0, "LOW NOISE"): 1.75, + (5, 0, "LOW NOISE"): 1.5, + (6, 0, "LOW NOISE"): 1.25, + (7, 0, "LOW NOISE"): 0.5, + (8, 0, "LOW NOISE"): 0.5, + # nA/V + (0, 1, "LOW NOISE"): 0.3, + (1, 1, "LOW NOISE"): 0.3, + (2, 1, "LOW NOISE"): 0.3, + (3, 1, "LOW NOISE"): 0.3, + (4, 1, "LOW NOISE"): 0.3, + (5, 1, "LOW NOISE"): 0.3, + (6, 1, "LOW NOISE"): 0.3, + (7, 1, "LOW NOISE"): 0.3, + (8, 1, "LOW NOISE"): 0.3, + # μA/V + (0, 2, "LOW NOISE"): 0.3, + (1, 2, "LOW NOISE"): 0.3, + (2, 2, "LOW NOISE"): 0.3, + (3, 2, "LOW NOISE"): 0.3, + (4, 2, "LOW NOISE"): 0.3, + (5, 2, "LOW NOISE"): 0.3, + (6, 2, "LOW NOISE"): 0.3, + (7, 2, "LOW NOISE"): 0.3, + (8, 2, "LOW NOISE"): 0.3, + # mA/V + (0, 3, "LOW NOISE"): 0.3, + (1, 3, "LOW NOISE"): None, + (2, 3, "LOW NOISE"): None, + (3, 3, "LOW NOISE"): None, + (4, 3, "LOW NOISE"): None, + (5, 3, "LOW NOISE"): None, + (6, 3, "LOW NOISE"): None, + (7, 3, "LOW NOISE"): None, + (8, 3, "LOW NOISE"): None, +} + +gain_units = ["pA/V", "nA/V", "uA/V", "mA/V"] +gain_values = ["1", "2", "5", "10", "20", "50", "100", "200", "500"] +gain_modes = ["LOW NOISE", "HIGH BW"] + + +@pytest.mark.parametrize("gain_mode", gain_modes) +@pytest.mark.parametrize("gain_unit", gain_units) +@pytest.mark.parametrize("gain_value", gain_values) +@mock.patch("apstools.devices.srs570_preamplifier.EpicsSignal.set") +def test_preamp_gain_settling(mocked_setter, gain_value, gain_unit, gain_mode): + """The SR-570 Pre-amp voltage spikes when changing gain. + + One solution, tested here, is to add a dynamic settling time. + + """ + value_idx = gain_values.index(gain_value) + unit_idx = gain_units.index(gain_unit) + settle_time = settling_times[ + ( + value_idx, + unit_idx, + gain_mode, + ) + ] + # We need a real pre-amp device otherwise .set isn't in the MRO + preamp = SRS570_PreAmplifier("prefix:", name="preamp") + assert isinstance(preamp.sensitivity_unit, GainSignal) + assert isinstance(preamp.sensitivity_value, GainSignal) + preamp.sensitivity_unit.get = mock.MagicMock(return_value=gain_unit) + preamp.gain_mode.get = mock.MagicMock(return_value=gain_mode) + # Set the sensitivity based on value + preamp.sensitivity_value.set(gain_value) + # Check that the EpicsSignal's ``set`` was called with correct settle_time + mocked_setter.assert_called_with(gain_value, timeout=DEFAULT_WRITE_TIMEOUT, settle_time=settle_time) + # Set the sensitivity based on value + mocked_setter.reset_mock() + assert not mocked_setter.called + preamp.sensitivity_value.set(value_idx) + # Check that the EpicsSignal's ``set`` was called with correct settle_time + mocked_setter.assert_called_with(value_idx, timeout=DEFAULT_WRITE_TIMEOUT, settle_time=settle_time) + + +@mock.patch("apstools.devices.srs570_preamplifier.EpicsSignal.set") +def test_preamp_gain_mode_settling(mocked_setter): + """The SR-570 Pre-amp also has a low drift mode, whose settling + times are the same as the low noise mode. + """ + # We need a real pre-amp device otherwise .set isn't in the MRO + preamp = SRS570_PreAmplifier("prefix:", name="preamp") + gain_unit = "pA/V" + gain_value = "500" + settle_time = 0.5 + preamp.sensitivity_unit.get = mock.MagicMock(return_value=gain_unit) + preamp.sensitivity_value.get = mock.MagicMock(return_value=gain_value) + # Set the sensitivity based on value + preamp.gain_mode.set("LOW DRIFT") + # Check that the EpicsSignal's ``set`` was called with correct settle_time + mocked_setter.assert_called_with("LOW DRIFT", timeout=DEFAULT_WRITE_TIMEOUT, settle_time=settle_time)