Skip to content

Commit

Permalink
Merge pull request #910 from spc-group/gain_settling
Browse files Browse the repository at this point in the history
Settling times for the SR-570 Preamplifier
  • Loading branch information
canismarko authored Jan 23, 2024
2 parents 1022a1f + dc5bdc2 commit 777d3cb
Show file tree
Hide file tree
Showing 2 changed files with 298 additions and 4 deletions.
154 changes: 150 additions & 4 deletions apstools/devices/srs570_preamplifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)

Expand Down
148 changes: 148 additions & 0 deletions apstools/devices/tests/test_srs570_preamplifier.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit 777d3cb

Please sign in to comment.