Skip to content

Commit

Permalink
Merge pull request #190 from spc-group/autogain
Browse files Browse the repository at this point in the history
Autogain
  • Loading branch information
canismarko authored Apr 3, 2024
2 parents febd4f0 + 554ce7e commit 5727bce
Show file tree
Hide file tree
Showing 9 changed files with 377 additions and 119 deletions.
5 changes: 4 additions & 1 deletion environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ dependencies:

# --- Bluesky framework packages
- adl2pydm
- apstools >=1.6.16
# apstools is installed from github by pip until DG645 and SRS570 settling are released
# - apstools >=1.6.16
- area-detector-handlers
- bluesky-queueserver
- bluesky-queueserver-api
Expand All @@ -92,6 +93,7 @@ dependencies:
# - happi
# - hklpy >=1.0.3 # --- linux-64
- ophyd >=1.6.3
# pcdsdevices is installed from github by pip until multiderived_settling fix hits pyPI
- pcdsdevices # For extra signal types
- pydm >=1.18.0
- typhos
Expand Down Expand Up @@ -145,6 +147,7 @@ dependencies:
- ophyd-registry >= 0.7
- xraydb >=4.5.0
- pytest-timeout # Get rid of this if tests are not hanging
- git+https://github.com/pcdshub/pcdsdevices
# - https://github.com/BCDA-APS/adl2pydm/archive/main.zip
# --- optional Bluesky framework packages for evaluation
# - bluesky-webclient is NOT Python software, don't install it this way
Expand Down
12 changes: 12 additions & 0 deletions src/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

# from pydm.data_plugins import plugin_modules, add_plugin
import pytest
from apstools.devices.srs570_preamplifier import GainSignal
from ophyd import DynamicDeviceComponent as DDC
from ophyd import Kind
from ophyd.sim import (
Expand Down Expand Up @@ -58,7 +59,10 @@ def __init__(self, prefix, **kwargs):
super().__init__(f"{prefix}I", write_pv=f"{prefix}O", **kwargs)


# Ophyd uses a cache of signals and their corresponding fakes
# We need to add ours in so they get simulated properly.
fake_device_cache[EpicsSignalWithIO] = FakeEpicsSignalWithIO
fake_device_cache[GainSignal] = FakeEpicsSignal


@pytest.fixture()
Expand Down Expand Up @@ -103,6 +107,14 @@ def sim_ion_chamber(sim_registry):
prefix="scaler_ioc", name="I00", labels={"ion_chambers"}, ch_num=2
)
sim_registry.register(ion_chamber)
# Set metadata
preamp = ion_chamber.preamp
preamp.sensitivity_value._enum_strs = tuple(preamp.values)
preamp.sensitivity_unit._enum_strs = tuple(preamp.units)
preamp.offset_value._enum_strs = tuple(preamp.values)
preamp.offset_unit._enum_strs = tuple(preamp.offset_units)
preamp.gain_mode._enum_strs = ("LOW NOISE", "HIGH BW", "LOW DRIFT")
preamp.gain_mode.set("LOW NOISE").wait(timeout=3)
return ion_chamber


Expand Down
2 changes: 1 addition & 1 deletion src/firefly/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ def show_count_plan_window(self):
@QtCore.Slot()
def show_voltmeters_window(self):
return self.show_window(
FireflyMainWindow, ui_dir / "voltmeters.py", name="voltmeters"
PlanMainWindow, ui_dir / "voltmeters.py", name="voltmeters"
)

@QtCore.Slot()
Expand Down
2 changes: 1 addition & 1 deletion src/haven/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
)
from .instrument.device import RegexComponent # noqa: F401
from .instrument.dxp import load_dxp # noqa: F401
from .instrument.load_instrument import load_instrument # noqa: F401
from .instrument.load_instrument import aload_instrument, load_instrument # noqa: F401
from .instrument.motor import HavenMotor # noqa: F401
from .instrument.xspress import load_xspress # noqa: F401
from .motor_position import ( # noqa: F401
Expand Down
75 changes: 55 additions & 20 deletions src/haven/instrument/ion_chamber.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
import time
import warnings
from collections import OrderedDict
from typing import Dict, Generator
from typing import Dict, Generator, Optional

import numpy as np
import pint
from aioca import caget
from apstools.devices import SRS570_PreAmplifier
from apstools.devices.srs570_preamplifier import (
SRS570_PreAmplifier,
calculate_settle_time,
)
from ophyd import Component as Cpt
from ophyd import Device, EpicsSignal, EpicsSignalRO
from ophyd import FormattedComponent as FCpt
Expand Down Expand Up @@ -80,6 +84,32 @@ class Voltmeter(AnalogInput):
volts = Cpt(EpicsSignal, ".VAL", kind="normal")


class GainDerivedSignal(MultiDerivedSignal):
"""A gain level signal that incorporates dynamic settling time."""

def set(
self,
value: OphydDataType,
*,
timeout: Optional[float] = None,
settle_time: Optional[float] = "auto",
):
# Calculate an auto settling time
if settle_time == "auto":
# Determine the new values that will be set
to_write = self.calculate_on_put(mds=self, value=value) or {}
# Calculate the correct settling time
settle_time_ = calculate_settle_time(
gain_value=to_write[self.parent.sensitivity_value],
gain_unit=to_write[self.parent.sensitivity_unit],
gain_mode=self.parent.gain_mode.get(),
)
else:
settle_time_ = settle_time
# Call the actual set method to move the gain
return super().set(value, timeout=timeout, settle_time=settle_time_)


class IonChamberPreAmplifier(SRS570_PreAmplifier):
"""An SRS-570 pre-amplifier driven by an ion chamber.
Expand All @@ -96,6 +126,7 @@ class IonChamberPreAmplifier(SRS570_PreAmplifier):

values = ["1", "2", "5", "10", "20", "50", "100", "200", "500"]
units = ["pA/V", "nA/V", "uA/V", "mA/V"]
offset_units = [s.split("/")[0] for s in units]
offset_difference = -3 # How many levels higher should the offset be

def __init__(self, *args, **kwargs):
Expand All @@ -118,33 +149,37 @@ def computed_gain(self):
"""
Amplifier gain (V/A), as floating-point number.
"""
val = float(self.values[self.sensitivity_value.get()])
# Convert the sensitivity to a proper number
val_idx = int(self.sensitivity_value.get(as_string=False))
val = float(self.values[val_idx])
# Determine multiplier based on the gain unit
amps = [
1e-12, # pA
1e-9, # nA
1e-6, # µA
1e-6, # μA
1e-3, # mA
]
multiplier = amps[self.sensitivity_unit.get()]
unit_idx = int(self.sensitivity_unit.get(as_string=False))
multiplier = amps[unit_idx]
inverse_gain = val * multiplier
return 1 / inverse_gain

def update_sensitivity_text(self, *args, obj: OphydObject, **kwargs):
val = self.values[self.sensitivity_value.get()]
unit = self.units[self.sensitivity_unit.get()]
val = self.values[int(self.sensitivity_value.get(as_string=False))]
unit = self.units[int(self.sensitivity_unit.get(as_string=False))]
text = f"{val} {unit}"
self.sensitivity_text.put(text, internal=True)

def _level_to_value(self, level):
return level % len(self.values)

def _level_to_unit(self, level):
return int(level / len(self.values))
return self.units[int(level / len(self.values))]

def _get_gain_level(self, mds: MultiDerivedSignal, items: SignalToValue) -> int:
"Given a sensitivity value and unit, transform to the desired gain level."
value = items[self.sensitivity_value]
unit = items[self.sensitivity_unit]
"Given a sensitivity value and unit , transform to the desired level."
value = self.values.index(items[self.sensitivity_value])
unit = self.units.index(items[self.sensitivity_unit])
# Determine sensitivity level
new_level = value + unit * len(self.values)
# Convert to gain by inverting
Expand Down Expand Up @@ -173,14 +208,13 @@ def _put_gain_level(
raise exceptions.GainOverflow(msg)
# Return calculated gain and offset
offset_value = self.values[self._level_to_value(new_offset)]
offset_unit = self.units[self._level_to_unit(new_offset)].split("/")[0]
offset_unit = self._level_to_unit(new_offset).split("/")[0]
result = OrderedDict()
result.update({self.sensitivity_unit: self._level_to_unit(new_level)})
result.update({self.sensitivity_value: self._level_to_value(new_level)})
result.update({self.offset_value: offset_value})
result.update({self.offset_unit: offset_unit})
# # set_all=1,
# }
# result[self.set_all] = 1
return result

def _get_offset_current(
Expand All @@ -200,13 +234,15 @@ def _get_offset_current(
return 0
return current

# It's easier to calculate gains by enum index, so override the apstools signals
sensitivity_value = Cpt(EpicsSignal, "sens_num", kind="config", string=False)
sensitivity_unit = Cpt(EpicsSignal, "sens_unit", kind="config", string=False)

gain_level = Cpt(
MultiDerivedSignal,
attrs=["sensitivity_value", "sensitivity_unit", "offset_value", "offset_unit"],
GainDerivedSignal,
attrs=[
"sensitivity_value",
"sensitivity_unit",
"offset_value",
"offset_unit",
"set_all",
],
calculate_on_get=_get_gain_level,
calculate_on_put=_put_gain_level,
kind=Kind.omitted,
Expand Down Expand Up @@ -586,7 +622,6 @@ async def load_ion_chamber(
ic_idx = ch_num - 2
# 5 pre-amps per labjack
lj_num = int(ic_idx / 5)
# Only use even labjack channels since it's a differential signal
lj_chan = ic_idx % 5
# Only use this ion chamber if it has a name
try:
Expand Down
43 changes: 26 additions & 17 deletions src/haven/instrument/load_instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@


async def aload_instrument(
registry: InstrumentRegistry = default_registry, config: Mapping = None
registry: InstrumentRegistry = default_registry,
config: Mapping = None,
return_devices: bool = False,
):
"""Asynchronously load the beamline instrumentation into an instrument
registry.
Expand All @@ -48,9 +50,19 @@ async def aload_instrument(
The registry into which the ophyd devices will be placed.
config:
The beamline configuration read in from TOML files. Mostly
useful for testing.
useful for testing.
return_devices
If true, return the newly loaded devices when complete.
"""
# Clear out any existing registry entries
registry.clear()
# Make sure we have the most up-to-date configuration
# load_config.cache_clear()
# Load the configuration
if config is None:
config = load_config()
# Load devices concurrently
coros = (
*load_camera_coros(config=config),
*load_shutter_coros(config=config),
Expand All @@ -77,7 +89,12 @@ async def aload_instrument(
# motors in the registry
extra_motors = await asyncio.gather(*load_all_motor_coros(config=config))
devices.extend(extra_motors)
return devices
# Also import some simulated devices for testing
devices += load_simulated_devices(config=config)
# Filter out devices that couldn't be reached
devices = [d for d in devices if d is not None]
if return_devices:
return devices


def load_instrument(
Expand All @@ -93,6 +110,10 @@ def load_instrument(
configuration, it will create Ophyd devices and register them with
*registry*.
This function starts the asyncio event loop. If one is already
running (e.g. jupyter notebook), then use ``await
aload_instrument()`` instead.
Parameters
==========
registry:
Expand All @@ -104,23 +125,11 @@ def load_instrument(
If true, return the newly loaded devices when complete.
"""
# Clear out any existing registry entries
registry.clear()
# Make sure we have the most up-to-date configuration
# load_config.cache_clear()
# Load the configuration
if config is None:
config = load_config()
# Import devices concurrently
loop = asyncio.get_event_loop()
devices = loop.run_until_complete(
aload_instrument(registry=registry, config=config)
)
# Also import some simulated devices for testing
devices += load_simulated_devices(config=config)
# Filter out devices that couldn't be reached
coro = aload_instrument(registry=registry, config=config)
devices = loop.run_until_complete(coro)
if return_devices:
devices = [d for d in devices if d is not None]
return devices


Expand Down
Loading

0 comments on commit 5727bce

Please sign in to comment.