From 755eef40e29aa1ab5ff606ce3a9c6f8081853e20 Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Mon, 11 Nov 2024 14:11:18 -0600 Subject: [PATCH 01/10] Updated ophyd-async to 0.8.0a3. --- environment.yml | 2 +- pyproject.toml | 2 +- src/haven/devices/delay.py | 75 +++++---- src/haven/devices/detectors/aravis.py | 4 +- src/haven/devices/ion_chamber.py | 4 +- src/haven/devices/labjack.py | 215 ++++++++------------------ src/haven/devices/mirrors.py | 1 + src/haven/devices/monochromator.py | 4 +- src/haven/devices/motor.py | 18 ++- src/haven/devices/scaler.py | 30 ++-- src/haven/devices/signal.py | 75 +++++---- src/haven/devices/srs570.py | 101 ++++++------ src/haven/devices/synApps.py | 21 +-- src/haven/devices/transform.py | 9 +- src/haven/devices/xray_source.py | 3 +- src/haven/tests/test_ion_chamber.py | 11 +- src/haven/tests/test_motor.py | 2 +- src/haven/tests/test_shutter.py | 4 +- src/haven/tests/test_signal.py | 20 ++- src/haven/tests/test_srs570.py | 15 +- 20 files changed, 285 insertions(+), 331 deletions(-) diff --git a/environment.yml b/environment.yml index be570477..c78822d6 100644 --- a/environment.yml +++ b/environment.yml @@ -58,7 +58,7 @@ dependencies: - bluesky-adaptive - bluesky >=1.8.1 - ophyd >=1.6.3 - - ophyd-async >= 0.6.0 + - ophyd-async >= 0.8.0a3 - apstools == 1.6.20 # Leave at 1.6.20 until this is fixed: https://github.com/BCDA-APS/apstools/issues/1022 - pcdsdevices # For extra signal types - pydm >=1.18.0 diff --git a/pyproject.toml b/pyproject.toml index 145d1492..25469391 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ "Topic :: System :: Hardware", ] keywords = ["synchrotron", "xray", "bluesky"] -dependencies = ["aioca", "aiokafka", "bluesky", "ophyd", "ophyd-async>=0.7.0", "databroker", "apsbss", "xraydb", +dependencies = ["aioca", "aiokafka", "bluesky", "ophyd", "ophyd-async>=0.8.0a3", "databroker", "apsbss", "xraydb", "mergedeep", "xrayutilities", "bluesky-queueserver-api", "tomlkit", "apstools", "databroker", "ophyd-registry", "caproto", "pcdsdevices", "strenum", "bluesky-adaptive", "tiled[client]"] diff --git a/src/haven/devices/delay.py b/src/haven/devices/delay.py index 85b7a52f..469d6d47 100644 --- a/src/haven/devices/delay.py +++ b/src/haven/devices/delay.py @@ -7,6 +7,7 @@ SignalRW, StandardReadable, SubsetEnum, + StrictEnum, T, ) from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x @@ -36,7 +37,16 @@ def epics_signal_io(datatype: Type[T], prefix: str, name: str = "") -> SignalRW[ class DG645Channel(StandardReadable): - Reference = SubsetEnum["T0", "A", "B", "C", "D", "E", "F", "G", "H"] + class Reference(StrictEnum): + T0 = "T0" + A = "A" + B = "B" + C = "C" + D = "D" + E = "E" + F = "F" + G = "G" + H = "H" def __init__(self, prefix: str, name: str = ""): with self.add_children_as_readables(ConfigSignal): @@ -46,7 +56,7 @@ def __init__(self, prefix: str, name: str = ""): class DG645Output(StandardReadable): - class Polarity(StrEnum): + class Polarity(StrictEnum): NEG = "NEG" POS = "POS" @@ -69,6 +79,36 @@ def __init__(self, prefix: str, name: str = ""): class DG645Delay(StandardReadable): + + class BaudRate(SubsetEnum): + B4800 = "4800" + B9600 = "9600" + B19200 = "19200" + B38400 = "38400" + B57600 = "57600" + B115200 = "115200" + + class TriggerSource(SubsetEnum): + INTERNAL = "Internal" + EXT_RISING_EDGE = "Ext rising edge" + EXT_FALLING_EDGE = "Ext falling edge" + SS_EXT_RISE_EDGE = "SS ext rise edge" + SS_EXT_FALL_EDGE = "SS ext fall edge" + SINGLE_SHOT = "Single shot" + LINE = "Line" + + class TriggerInhibit(SubsetEnum): + OFF = "Off" + TRIGGERS = "Triggers" + AB = "AB" + AB_CD = "AB,CD" + AB_CD_EF = "AB,CD,EF" + AB_CD_EF_GH = "AB,CD,EF,GH" + + class BurstConfig(SubsetEnum): + ALL_CYCLES = "All Cycles" + FIRST_CYCLE = "1st Cycle" + def __init__(self, prefix: str, name: str = ""): # Conventional signals with self.add_children_as_readables(ConfigSignal): @@ -82,10 +122,7 @@ def __init__(self, prefix: str, name: str = ""): self.status_checking = epics_signal_rw(bool, f"{prefix}StatusCheckingBO") self.reset_serial = epics_signal_x(f"{prefix}IfaceSerialResetBO") self.serial_state = epics_signal_io(bool, f"{prefix}IfaceSerialStateB") - self.serial_baud = epics_signal_io( - SubsetEnum["4800", "9600", "19200", "38400", "57600", "115200"], - f"{prefix}IfaceSerialBaudM", - ) + self.serial_baud = epics_signal_io(self.BaudRate, f"{prefix}IfaceSerialBaudM") self.reset_gpib = epics_signal_x(f"{prefix}IfaceGpibResetBO") self.gpib_state = epics_signal_io(bool, f"{prefix}IfaceGpibStateB") self.gpib_address = epics_signal_io(int, f"{prefix}IfaceGpibAddrL") @@ -128,36 +165,18 @@ def __init__(self, prefix: str, name: str = ""): ) # Trigger control with self.add_children_as_readables(ConfigSignal): - self.trigger_source = epics_signal_io( - SubsetEnum[ - "Internal", - "Ext rising edge", - "Ext falling edge", - "SS ext rise edge", - "SS ext fall edge", - "Single shot", - "Line", - ], - f"{prefix}TriggerSourceM", - ) - self.trigger_inhibit = epics_signal_io( - SubsetEnum["Off", "Triggers", "AB", "AB,CD", "AB,CD,EF", "AB,CD,EF,GH"], - f"{prefix}TriggerInhibitM", - ) + self.trigger_source = epics_signal_io(self.TriggerSource,f"{prefix}TriggerSourceM",) + self.trigger_inhibit = epics_signal_io(self.TriggerInhibit, f"{prefix}TriggerInhibitM") self.trigger_level = epics_signal_io(float, f"{prefix}TriggerLevelA") self.trigger_rate = epics_signal_io(float, f"{prefix}TriggerRateA") - self.trigger_advanced_mode = epics_signal_io( - bool, f"{prefix}TriggerAdvancedModeB" - ) + self.trigger_advanced_mode = epics_signal_io(bool, f"{prefix}TriggerAdvancedModeB") self.trigger_holdoff = epics_signal_io(float, f"{prefix}TriggerHoldoffA") self.trigger_prescale = epics_signal_io(int, f"{prefix}TriggerPrescaleL") # Burst settings with self.add_children_as_readables(ConfigSignal): self.burst_mode = epics_signal_io(bool, f"{prefix}BurstModeB") self.burst_count = epics_signal_io(int, f"{prefix}BurstCountL") - self.burst_config = epics_signal_io( - SubsetEnum["All Cycles", "1st Cycle"], f"{prefix}BurstConfigB" - ) + self.burst_config = epics_signal_io(self.BurstConfig, f"{prefix}BurstConfigB" ) self.burst_delay = epics_signal_io(float, f"{prefix}BurstDelayA") self.burst_period = epics_signal_io(float, f"{prefix}BurstPeriodA") super().__init__(name=name) diff --git a/src/haven/devices/detectors/aravis.py b/src/haven/devices/detectors/aravis.py index 3710c6df..edc534f4 100644 --- a/src/haven/devices/detectors/aravis.py +++ b/src/haven/devices/detectors/aravis.py @@ -4,7 +4,9 @@ from .area_detectors import HavenDetector -AravisTriggerSource = SubsetEnum["Software", "Line1"] +class AravisTriggerSource(SubsetEnum): + SOFTWARE = "Software" + LINE1 = "Line1" class AravisDetector(HavenDetector, DetectorBase): diff --git a/src/haven/devices/ion_chamber.py b/src/haven/devices/ion_chamber.py index 86b381bc..0d5b36a1 100644 --- a/src/haven/devices/ion_chamber.py +++ b/src/haven/devices/ion_chamber.py @@ -237,7 +237,7 @@ async def trigger(self, record_dark_current=False): await last_status return # Nothing to wait on yet, so trigger the scaler and stash the result - st = signal.set(self.mcs.scaler.CountState.COUNT) + st = signal.set(True) self._trigger_statuses[signal.source] = st await st @@ -249,7 +249,7 @@ async def record_dark_current(self): timeout = integration_time + DEFAULT_TIMEOUT count_signal = self.mcs.scaler.count await wait_for_value( - count_signal, self.mcs.scaler.CountState.DONE, timeout=timeout + count_signal, False, timeout=timeout ) def record_fly_reading(self, reading, **kwargs): diff --git a/src/haven/devices/labjack.py b/src/haven/devices/labjack.py index c247d330..9e5bada5 100644 --- a/src/haven/devices/labjack.py +++ b/src/haven/devices/labjack.py @@ -47,6 +47,7 @@ def __init__(self, prefix: str, name: str = ""): HintedSignal, StandardReadable, SubsetEnum, + StrictEnum, observe_value, ) from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x @@ -70,7 +71,6 @@ def __init__(self, prefix: str, name: str = ""): KIND_CONFIG_OR_NORMAL = 3 """Alternative for ``Kind.config | Kind.normal``.""" - class Input(EpicsRecordInputFields): """A generic input record. @@ -86,25 +86,6 @@ def __init__(self, prefix: str, name: str = ""): class BinaryInput(Input): - DeviceType = SubsetEnum[ - "Soft Channel", - "Raw Soft Channel", - "Async Soft Channel", - "Db State", - "asynInt32", - "asynUInt32Digital", - "asyn bi stringParm", - "asyn MPC", - "Vg307 GPIB Instrument", - "asyn Televac", - "asyn TPG261", - "lua", - "stream", - "EtherIP", - "dg535", - "Zed", - ] - def __init__(self, prefix: str, name: str = ""): with self.add_children_as_readables(): self.final_value = epics_signal_r(bool, f"{prefix}.VAL") @@ -123,54 +104,17 @@ class Output(EpicsRecordOutputFields): class BinaryOutput(Output): """A binary input on the labjack.""" - DeviceType = SubsetEnum[ - "Soft Channel", - "Raw Soft Channel", - "Async Soft Channel", - "General Time", - "Db State", - "asynInt32", - "asynUInt32Digital", - "asyn bo stringParm", - "asyn MPC", - "Vg307 GPIB Instrument", - "PZT Bug", - "asyn TPG261", - "lua", - "stream", - "EtherIP", - "dg535", - ] - def __init__(self, prefix: str, name: str = ""): with self.add_children_as_readables(): self.desired_value = epics_signal_rw(bool, f"{prefix}.VAL") self.raw_value = epics_signal_rw(float, f"{prefix}.RVAL") self.readback_value = epics_signal_r(float, f"{prefix}.RBV") + super().__init__(prefix=prefix, name=name) class AnalogOutput(Output): """An analog output on a labjack device.""" - DeviceType = SubsetEnum[ - "Soft Channel", - "Raw Soft Channel", - "Async Soft Channel", - "asynInt32", - "asynFloat64", - "asynInt64", - "IOC stats", - "asyn ao stringParm", - "asyn ao Eurotherm", - "asyn MPC", - "PZT Bug", - "asyn TPG261", - "lua", - "stream", - "EtherIP", - "dg535", - ] - def __init__(self, prefix: str, name: str = ""): with self.add_children_as_readables(): self.desired_value = epics_signal_rw(float, f"{prefix}.VAL") @@ -188,16 +132,16 @@ class AnalogInput(Input, Triggerable): """ - class DifferentialMode(StrEnum): + class DifferentialMode(StrictEnum): SINGLE_ENDED = "Single-Ended" DIFFERENTIAL = "Differential" - class TemperatureUnits(StrEnum): + class TemperatureUnits(SubsetEnum): KELVIN = "K" CELSIUS = "C" FAHRENHEIT = "F" - class Mode(StrEnum): + class Mode(SubsetEnum): VOLTS = "Volts" TYPE_B_TC = "Type B TC" TYPE_C_TC = "Type C TC" @@ -209,13 +153,13 @@ class Mode(StrEnum): TYPE_S_TC = "Type S TC" TYPE_T_TC = "Type T TC" - class Range(StrEnum): + class Range(SubsetEnum): TEN_VOLTS = "+= 10V" ONE_VOLT = "+= 1V" HUNDRED_MILLIVOLTS = "+= 0.1V" TEN_MILLIVOLTS = "+= 0.01V" - class Resolution(StrEnum): + class Resolution(SubsetEnum): DEFAULT = "Default" ONE = "1" TWO = "2" @@ -226,25 +170,6 @@ class Resolution(StrEnum): SEVEN = "7" EIGHT = "8" - DeviceType = SubsetEnum[ - "Soft Channel", - "Raw Soft Channel", - "Async Soft Channel", - "Soft Timestamp", - "General Time", - "asynInt32", - "asynInt32Average", - "asynFloat64", - "asynFloat64Average", - "asynInt64", - "IOC stats", - "IOC stats clusts", - "GPIB init/report", - "Sec Past Epoch", - "asyn ai stringParm", - "asyn ai HeidND261", - ] - def __init__(self, prefix: str, ch_num: int, name: str = ""): with self.add_children_as_readables(ConfigSignal): self.differential = epics_signal_rw( @@ -291,7 +216,7 @@ class DigitalIO(StandardReadable): """ - class Direction(StrEnum): + class Direction(SubsetEnum): INPUT = "In" OUTPUT = "Out" @@ -307,10 +232,57 @@ def __init__(self, prefix: str, ch_num: int, name: str = ""): class WaveformDigitizer(StandardReadable, Triggerable): """A feature of the Labjack devices that allows waveform capture.""" - class TriggerSource(StrEnum): + class TriggerSource(StrictEnum): INTERNAL = "Internal" EXTERNAL = "External" + class ReadWaveform(StrictEnum): + DONE = "Done" + READ = "Read" + + class FirstChannel(SubsetEnum): + ONE = "1" + TWO = "2" + THREE = "3" + FOUR = "4" + FIVE = "5" + SIX = "6" + SEVEN = "7" + EIGHT = "8" + NINE = "9" + TEN = "10" + ELEVEN = "11" + TWELVE = "12" + THIRTEEN = "13" + + class NumberOfChannels(SubsetEnum): + ONE = "1" + TWO = "2" + THREE = "3" + FOUR = "4" + FIVE = "5" + SIX = "6" + SEVEN = "7" + EIGHT = "8" + NINE = "9" + TEN = "10" + ELEVEN = "11" + TWELVE = "12" + THIRTEEN = "13" + FOURTEEN = "14" + + class Resolution(SubsetEnum): + DEFAULT = "Default" + ONE = "1" + TWO = "2" + THREE = "3" + FOUR = "4" + FIVE = "5" + SIX = "6" + SEVEN = "7" + EIGHT = "8" + + def __init__(self, prefix: str, name: str = "", waveforms=[]): with self.add_children_as_readables(): self.timebase_waveform = epics_signal_rw( @@ -321,48 +293,9 @@ def __init__(self, prefix: str, name: str = "", waveforms=[]): with self.add_children_as_readables(ConfigSignal): self.num_points = epics_signal_rw(int, f"{prefix}WaveDigNumPoints") self.dwell_time = epics_signal_rw(float, f"{prefix}WaveDigDwell") - self.first_chan = epics_signal_rw( - SubsetEnum[ - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "13", - ], - f"{prefix}WaveDigFirstChan", - ) - self.num_chans = epics_signal_rw( - SubsetEnum[ - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "10", - "11", - "12", - "13", - "14", - ], - f"{prefix}WaveDigNumChans", - ) - self.resolution = epics_signal_rw( - SubsetEnum["Default", "1", "2", "3", "4", "5", "6", "7", "8"], - f"{prefix}WaveDigResolution", - ) + self.first_chan = epics_signal_rw(self.FirstChannel, f"{prefix}WaveDigFirstChan") + self.num_chans = epics_signal_rw(self.NumberOfChannels, f"{prefix}WaveDigNumChans") + self.resolution = epics_signal_rw(self.Resolution, f"{prefix}WaveDigResolution") self.settling_time = epics_signal_rw(float, f"{prefix}WaveDigSettlingTime") self.current_point = epics_signal_rw(int, f"{prefix}WaveDigCurrentPoint") self.ext_trigger = epics_signal_rw( @@ -371,9 +304,7 @@ def __init__(self, prefix: str, name: str = "", waveforms=[]): self.ext_clock = epics_signal_rw(self.TriggerSource, f"{prefix}WaveDigExtClock") self.auto_restart = epics_signal_x(f"{prefix}WaveDigAutoRestart") self.run = epics_signal_rw(bool, f"{prefix}WaveDigRun") - self.read_waveform = epics_signal_rw( - SubsetEnum["Done", "Read"], f"{prefix}WaveDigReadWF" - ) + self.read_waveform = epics_signal_rw(self.ReadWaveform, f"{prefix}WaveDigReadWF") # Add waveforms with self.add_children_as_readables(): self.waveforms = DeviceVector( @@ -405,7 +336,7 @@ async def trigger(self): class WaveformGenerator(StandardReadable): """A feature of the Labjack devices that generates output waveforms.""" - class WaveType(StrEnum): + class WaveType(StrictEnum): USER_DEFINED = "User-defined" SINE_WAVE = "Sin wave" SQUARE_WAVE = "Square wave" @@ -415,7 +346,7 @@ class WaveType(StrEnum): TriggerSource = WaveformDigitizer.TriggerSource - class TriggerMode(StrEnum): + class TriggerMode(StrictEnum): ONE_SHOT = "One-shot" CONTINUOS = "Continuous" @@ -532,26 +463,8 @@ class LabJackBase(StandardReadable): """ Resolution = AnalogInput.Resolution - DeviceType = SubsetEnum[ - "Soft Channel", - "Raw Soft Channel", - "Async Soft Channel", - "Soft Timestamp", - "General Time", - "asynInt32", - "asynInt32Average", - "asynFloat64", - "asynFloat64Average", - "asynInt64", - "IOC stats", - "IOC stats clusts", - "GPIB init/report", - "Sec Past Epoch", - "asyn ai stringParm", - "asyn ai HeidND261", - ] - - class Model(StrEnum): + + class Model(StrictEnum): T4 = "T4" T7 = "T7" T7_PRO = "T7-Pro" diff --git a/src/haven/devices/mirrors.py b/src/haven/devices/mirrors.py index 0df96e4e..fc4d2e5f 100644 --- a/src/haven/devices/mirrors.py +++ b/src/haven/devices/mirrors.py @@ -59,6 +59,7 @@ def __init__( transform_prefix = "".join(prefix.rsplit(":", 2)) self.drive_transform = TransformRecord(f"{transform_prefix}:Drive") self.readback_transform = TransformRecord(f"{transform_prefix}:Readback") + super().__init__(name=name) class KBMirrors(Device): diff --git a/src/haven/devices/monochromator.py b/src/haven/devices/monochromator.py index eb30e634..bede4587 100644 --- a/src/haven/devices/monochromator.py +++ b/src/haven/devices/monochromator.py @@ -1,7 +1,7 @@ import logging from enum import Enum -from ophyd_async.core import ConfigSignal, StandardReadable +from ophyd_async.core import ConfigSignal, StandardReadable, StrictEnum from ophyd_async.epics.signal import epics_signal_rw from .motor import Motor @@ -12,7 +12,7 @@ class Monochromator(StandardReadable): _ophyd_labels_ = {"monochromators"} - class Mode(str, Enum): + class Mode(StrictEnum): FIXED_OFFSET = "Si(111) Fixed Offset" CHANNEL_CUT = "Si(111) Channel-cut" ML48 = "Multi-layer 4.8nm" diff --git a/src/haven/devices/motor.py b/src/haven/devices/motor.py index 7b83ca19..7353d572 100644 --- a/src/haven/devices/motor.py +++ b/src/haven/devices/motor.py @@ -3,7 +3,7 @@ from ophyd import Component as Cpt from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO, Kind -from ophyd_async.core import DEFAULT_TIMEOUT, ConfigSignal, SubsetEnum +from ophyd_async.core import DEFAULT_TIMEOUT, ConfigSignal, SubsetEnum, StrictEnum from ophyd_async.epics.motor import Motor as MotorBase from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw from ophydregistry import Registry @@ -17,6 +17,14 @@ class Motor(MotorBase): """The default motor for asynchrnous movement.""" + class Direction(StrictEnum): + POSITIVE = "Pos" + NEGATIVE = "Neg" + + class FreezeSwitch(SubsetEnum): + VARIABLE = "Variable" + FROZEN = "Frozen" + def __init__( self, prefix: str, name="", labels={"motors"}, auto_name: bool = None ) -> None: @@ -35,12 +43,8 @@ def __init__( with self.add_children_as_readables(ConfigSignal): self.description = epics_signal_rw(str, f"{prefix}.DESC") self.user_offset = epics_signal_rw(float, f"{prefix}.OFF") - self.user_offset_dir = epics_signal_rw( - SubsetEnum["Pos", "Neg"], f"{prefix}.DIR" - ) - self.offset_freeze_switch = epics_signal_rw( - SubsetEnum["Variable", "Frozen"], f"{prefix}.FOFF" - ) + self.user_offset_dir = epics_signal_rw(self.Direction, f"{prefix}.DIR") + self.offset_freeze_switch = epics_signal_rw(self.FreezeSwitch, f"{prefix}.FOFF") # Motor status signals self.motor_is_moving = epics_signal_r(int, f"{prefix}.MOVN") self.motor_done_move = epics_signal_r(int, f"{prefix}.DMOV") diff --git a/src/haven/devices/scaler.py b/src/haven/devices/scaler.py index d67aa749..376fe7b2 100644 --- a/src/haven/devices/scaler.py +++ b/src/haven/devices/scaler.py @@ -1,8 +1,6 @@ -from enum import Enum - import numpy as np from numpy.typing import NDArray -from ophyd_async.core import ConfigSignal, DeviceVector, HintedSignal, StandardReadable +from ophyd_async.core import ConfigSignal, DeviceVector, HintedSignal, StandardReadable, SubsetEnum, StrictEnum from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x from ..typing import StrEnum @@ -38,7 +36,7 @@ def __init__(self, prefix, channel_num, name=""): class MCA(StandardReadable): - class MCAMode(str, Enum): + class MCAMode(SubsetEnum): PHA = "PHA" MCS = "MCS" LIST = "List" @@ -68,37 +66,37 @@ class MultiChannelScaler(StandardReadable): _ophyd_labels_ = {"scalers"} - class ChannelAdvanceSource(str, Enum): + class ChannelAdvanceSource(SubsetEnum): INTERNAL = "Internal" EXTERNAL = "External" - class Acquiring(str, Enum): + class Acquiring(StrictEnum): DONE = "Done" ACQUIRING = "Acquiring" - class ScalerModel(str, Enum): + class ScalerModel(SubsetEnum): SIS_3801 = "SIS3801" SIS_3820 = "SIS3820" - class Channel1Source(str, Enum): + class Channel1Source(SubsetEnum): INTERNAL_CLOCK = "Int. clock" EXTERNAL = "External" - class AcquireMode(str, Enum): + class AcquireMode(SubsetEnum): MCS = "MCS" SCALER = "Scaler" - class Polarity(str, Enum): + class Polarity(StrictEnum): NORMAL = "Normal" INVERTED = "Inverted" - class OutputMode(str, Enum): + class OutputMode(SubsetEnum): MODE_0 = "Mode 0" MODE_1 = "Mode 1" MODE_2 = "Mode 2" MODE_3 = "Mode 3" - class InputMode(str, Enum): + class InputMode(SubsetEnum): MODE_0 = "Mode 0" MODE_1 = "Mode 1" MODE_2 = "Mode 2" @@ -168,14 +166,10 @@ def __init__(self, prefix, channels: list[int], name=""): class Scaler(StandardReadable): """A scaler device that has one or more channels.""" - class CountMode(StrEnum): + class CountMode(SubsetEnum): ONE_SHOT = "OneShot" AUTO_COUNT = "AutoCount" - class CountState(StrEnum): - DONE = "Done" - COUNT = "Count" - def __init__(self, prefix, channels: list[int], name=""): # Add invidiaul scaler channels with self.add_children_as_readables(): @@ -195,7 +189,7 @@ def __init__(self, prefix, channels: list[int], name=""): self.count_mode = epics_signal_rw(self.CountMode, f"{prefix}.CONT") self.preset_time = epics_signal_rw(float, f"{prefix}.TP") self.auto_count = epics_signal_rw(bool, f"{prefix}.CONT") - self.count = epics_signal_rw(self.CountState, f"{prefix}.CNT") + self.count = epics_signal_rw(bool, f"{prefix}.CNT") self.record_dark_current = epics_signal_x(f"{prefix}_offset_start.PROC") self.auto_count_delay = epics_signal_rw(float, f"{prefix}.DLY1") self.auto_count_time = epics_signal_rw(float, f"{prefix}.TP1") diff --git a/src/haven/devices/signal.py b/src/haven/devices/signal.py index 9589f08b..3f3d517b 100644 --- a/src/haven/devices/signal.py +++ b/src/haven/devices/signal.py @@ -11,8 +11,10 @@ DEFAULT_TIMEOUT, AsyncStatus, CalculatableTimeout, - ReadingValueCallback, + Callback, + DeviceConnector, SignalBackend, + SignalDatatypeT, SignalMetadata, SignalR, SignalRW, @@ -20,7 +22,8 @@ SoftSignalBackend, T, ) -from ophyd_async.epics.signal._signal import _epics_signal_backend +from ophyd_async.core._signal import _wait_for +from ophyd_async.epics.core._signal import _epics_signal_backend class DerivedSignalBackend(SoftSignalBackend): @@ -116,18 +119,14 @@ def inverse(self, values, **kw): msg += "Provide an explicit inverse transform." raise ValueError(msg) - def source(self, name: str = ""): - src = super().source(name) + def source(self, name: str, read: bool): + src = super().source(name, read) args = ",".join(self._derived_from.keys()) return f"{src}({args})" async def connect(self, timeout=DEFAULT_TIMEOUT) -> None: + # Connect this signal await super().connect(timeout=timeout) - # Ensure dependent signals are connected - connectors = ( - sig.connect(timeout=timeout) for sig in self._derived_from.values() - ) - await asyncio.gather(*connectors) # Listen for changes in the derived_from signals for sig in self._derived_from.values(): # Subscribe with a partial in case the signal's name changes @@ -139,7 +138,12 @@ def combine_readings(self, readings): severity = max([rd.get("severity", 0) for rd in readings.values()]) values = {sig: rdg["value"] for sig, rdg in readings.items()} new_value = self.inverse(values, **self._derived_from) - return self.converter.reading(new_value, timestamp, severity) + self.reading = Reading( + value=self.converter.write_value(new_value), + timestamp=timestamp, + alarm_severity=severity, + ) + return self.reading def update_readings(self, reading, signal): """Callback receives readings from derived_from signals. @@ -147,6 +151,7 @@ def update_readings(self, reading, signal): Stashes them for later recall. """ + print("UPDATING") # Stash this reading self._cached_readings.update({signal: reading[signal.name]}) # Update interested parties if we have a full set of readings @@ -163,9 +168,9 @@ def send_latest_reading(self): # We have all the readings, so update the cached values new_reading = self.combine_readings(readings) if self.callback is not None: - self.callback(new_reading, new_reading["value"]) + self.callback(new_reading) - def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None: + def set_callback(self, callback: Callback[Reading[SignalDatatypeT]] | None) -> None: super().set_callback(callback) self.send_latest_reading() @@ -173,7 +178,7 @@ async def put(self, value: Optional[T], wait=True, timeout=None): write_value = ( self.converter.write_value(value) if value is not None - else self._initial_value + else self.initial_value ) # Calculate the derived set points new_values = await self.forward(write_value, **self._derived_from) @@ -199,20 +204,9 @@ async def get_reading(self) -> Reading: # Return a proper reading for this derived value return self.combine_readings(readings) - async def get_value(self) -> T: - # Sort out which types of signals we have - gettable_signals = [ - sig for sig in self._derived_from.values() if hasattr(sig, "get_value") - ] - # Retrieve current values from signals - values = await asyncio.gather(*(sig.get_value() for sig in gettable_signals)) - values = {sig: val for sig, val in zip(gettable_signals, values)} - # Set default value of None for missing signals - for sig in self._derived_from.values(): - values.setdefault(sig, None) - # Compute the new value - new_value = self.inverse(values, **self._derived_from) - return self.converter.value(new_value) + +class DerivedSignalRW(SignalRW): + pass def derived_signal_rw( @@ -287,14 +281,14 @@ def __init__(self, prefix, name="", **kwargs): values. """ - metadata = SignalMetadata(units=units, precision=precision) backend = DerivedSignalBackend( datatype, derived_from=derived_from, forward=forward, inverse=inverse, initial_value=initial_value, - metadata=metadata, + units=units, + precision=precision, ) signal = SignalRW(backend, name=name) return signal @@ -357,13 +351,13 @@ def __init__(self, prefix, name="", **kwargs): values. """ - metadata = SignalMetadata(units=units, precision=precision) backend = DerivedSignalBackend( datatype, derived_from=derived_from, inverse=inverse, initial_value=initial_value, - metadata=metadata, + units=units, + precision=precision, ) signal = SignalR(backend, name=name) return signal @@ -422,12 +416,10 @@ def __init__(self, prefix, name="", **kwargs): values. """ - metadata = SignalMetadata() backend = DerivedSignalBackend( int, derived_from=derived_from, forward=forward, - metadata=metadata, ) signal = SignalX(backend, name=name) return signal @@ -440,14 +432,19 @@ def __init__(self, *args, trigger_value=1, **kwargs): self.trigger_value = trigger_value super().__init__(*args, **kwargs) - def trigger( - self, wait=False, timeout: CalculatableTimeout = CALCULATE_TIMEOUT - ) -> AsyncStatus: + @AsyncStatus.wrap + async def trigger( + self, wait=True, timeout: CalculatableTimeout = CALCULATE_TIMEOUT + ) -> None: """Trigger the action and return a status saying when it's done""" - if timeout is CALCULATE_TIMEOUT: + if timeout == CALCULATE_TIMEOUT: timeout = self._timeout - coro = self._backend.put(self.trigger_value, wait=wait, timeout=timeout) - return AsyncStatus(coro) + source = self._connector.backend.source(self.name, read=False) + self.log.debug(f"Putting default value to backend at source {source}") + await _wait_for( + self._connector.backend.put(self.trigger_value, wait=wait), timeout, source + ) + self.log.debug(f"Successfully put default value to backend at source {source}") def epics_signal_xval(write_pv: str, name: str = "", trigger_value=1) -> SignalXVal: diff --git a/src/haven/devices/srs570.py b/src/haven/devices/srs570.py index 42024a89..981e5540 100644 --- a/src/haven/devices/srs570.py +++ b/src/haven/devices/srs570.py @@ -26,10 +26,11 @@ Device, SignalRW, SubsetEnum, + StrictEnum, T, ) -from ophyd_async.epics.signal import epics_signal_rw, epics_signal_x -from ophyd_async.epics.signal._signal import _epics_signal_backend +from ophyd_async.epics.core import epics_signal_rw, epics_signal_x +from ophyd_async.epics.core._signal import _epics_signal_backend from .. import exceptions from .signal import derived_signal_r, derived_signal_rw @@ -42,12 +43,12 @@ gain_modes = ["LOW NOISE", "HIGH BW"] -class Sign(str, Enum): +class Sign(StrictEnum): PLUS = "+" MINUS = "-" -class Cal(str, Enum): +class Cal(StrictEnum): CAL = "CAL" UNCAL = "UNCAL" @@ -257,7 +258,7 @@ class SRS570PreAmplifier(Device): offset_difference = -3 # How many levels higher should the offset be - class FilterType(str, Enum): + class FilterType(StrictEnum): NO_FILTER = "No filter" _6DB_HIGHPASS = "6 dB highpass" _12DB_HIGHPASS = "12 dB highpass" @@ -265,12 +266,44 @@ class FilterType(str, Enum): _6DB_LOWPASS = "6 dB lowpass" _12DB_LOWPASS = "6 dB lowpass" - class GainMode(str, Enum): + class FilterLowPass(StrictEnum): + _0_03_HZ = " 0.03 Hz" + _0_1_HZ = " 0.1 Hz" + _0_3_HZ = " 0.3 Hz" + _1_HZ = " 1 Hz" + _3_HZ = " 3 Hz" + _10_HZ = " 10 Hz" + _30_HZ = " 30 Hz" + _100_HZ = "100 Hz" + _300_HZ = "300 Hz" + _1_KHZ = " 1 kHz" + _3_KHZ = " 3 kHz" + _10_KHZ = " 10 kHz" + _30_KHZ = " 30 kHz" + _100_KHZ = "100 kHz" + _300_KHZ = "300 kHz" + _1_MHZ = " 1 MHz" + + class FilterHighPass(StrictEnum): + _0_03_HZ = " 0.03 Hz" + _0_1_HZ = " 0.1 Hz" + _0_3_HZ = " 0.3 Hz" + _1_HZ = " 1 Hz" + _3_HZ = " 3 Hz" + _10_HZ = " 10 Hz" + _30_HZ = " 30 Hz" + _100_HZ = "100 Hz" + _300_HZ = "300 Hz" + _1_KHZ = " 1 kHz" + _3_KHZ = " 3 kHz" + _10_KHZ = " 10 kHz" + + class GainMode(StrictEnum): LOW_NOISE = "LOW NOISE" HIGH_BW = "HIGH BW" LOW_DRIFT = "LOW DRIFT" - class SensValue(str, Enum): + class SensValue(StrictEnum): _1 = "1" _2 = "2" _5 = "5" @@ -281,13 +314,13 @@ class SensValue(str, Enum): _200 = "200" _500 = "500" - class SensUnit(str, Enum): + class SensUnit(StrictEnum): pA_V = "pA/V" nA_V = "nA/V" uA_V = "uA/V" mA_V = "mA/V" - class OffsetUnit(str, Enum): + class OffsetUnit(StrictEnum): pA = "pA" nA = "nA" uA = "uA" @@ -313,56 +346,12 @@ def __init__(self, prefix: str, name: str = ""): self.bias_on = epics_signal_rw(bool, f"{prefix}bias_on") self.filter_type = epics_signal_rw( - SubsetEnum[ - " No filter", - " 6 dB highpass", - "12 dB highpass", - " 6 dB bandpass", - " 6 dB lowpass", - "12 dB lowpass", - ], + self.FilterType, f"{prefix}filter_type", ) self.filter_reset = epics_signal_x(f"{prefix}filter_reset.PROC") - self.filter_lowpass = epics_signal_rw( - SubsetEnum[ - " 0.03 Hz", - " 0.1 Hz", - " 0.3 Hz", - " 1 Hz", - " 3 Hz", - " 10 Hz", - " 30 Hz", - "100 Hz", - "300 Hz", - " 1 kHz", - " 3 kHz", - " 10 kHz", - " 30 kHz", - "100 kHz", - "300 kHz", - " 1 MHz", - ], - f"{prefix}low_freq", - ) - self.filter_highpass = epics_signal_rw( - SubsetEnum[ - " 0.03 Hz", - " 0.1 Hz", - " 0.3 Hz", - " 1 Hz", - " 3 Hz", - " 10 Hz", - " 30 Hz", - "100 Hz", - "300 Hz", - " 1 kHz", - " 3 kHz", - " 10 kHz", - ], - f"{prefix}high_freq", - ) - + self.filter_lowpass = epics_signal_rw(self.FilterLowPass, f"{prefix}low_freq") + self.filter_highpass = epics_signal_rw(self.FilterHighPass, f"{prefix}high_freq") self.gain_mode = gain_signal(self.GainMode, f"{prefix}gain_mode") self.invert = epics_signal_rw(bool, f"{prefix}invert_on") self.blank = epics_signal_rw(bool, f"{prefix}blank_on") diff --git a/src/haven/devices/synApps.py b/src/haven/devices/synApps.py index 508a1bc4..9e8ac6eb 100644 --- a/src/haven/devices/synApps.py +++ b/src/haven/devices/synApps.py @@ -1,12 +1,10 @@ import asyncio -from ophyd_async.core import ConfigSignal, Device, StandardReadable, SubsetEnum +from ophyd_async.core import ConfigSignal, Device, StandardReadable, SubsetEnum, StrictEnum from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x -from ..typing import StrEnum - -class AlarmStatus(StrEnum): +class AlarmStatus(SubsetEnum): NO_ALARM = "NO_ALARM" READ = "READ" WRITE = "WRITE" @@ -31,7 +29,7 @@ class AlarmStatus(StrEnum): # WRITE_ACCESS = "WRITE_ACCESS" -class AlarmSeverity(StrEnum): +class AlarmSeverity(StrictEnum): NO_ALARM = "NO_ALARM" MINOR = "MINOR" MAJOR = "MAJOR" @@ -46,11 +44,14 @@ class EpicsRecordDeviceCommonAll(StandardReadable): an EPICS client or are already provided in other support. """ - # The valid options are specific to the record type - # Subclasses should set this properly - DeviceType = SubsetEnum["None"] + # More valid options are specific to the record type + # Subclasses may override this attribute + class DeviceType(SubsetEnum): + SOFT_CHANNEL = "Soft Channel" + RAW_SOFT_CHANNEL = "Raw Soft Channel" + ASYNC_SOFT_CHANNEL = "Async Soft Channel" - class ScanInterval(StrEnum): + class ScanInterval(SubsetEnum): PASSIVE = "Passive" EVENT = "Event" IO_INTR = "I/O Intr" @@ -117,7 +118,7 @@ class EpicsRecordOutputFields(EpicsRecordDeviceCommonAll): Some fields common to EPICS output records. """ - class ModeSelect(StrEnum): + class ModeSelect(SubsetEnum): SUPERVISORY = "supervisory" CLOSED_LOOP = "closed_loop" diff --git a/src/haven/devices/transform.py b/src/haven/devices/transform.py index f1a75805..d56f44af 100644 --- a/src/haven/devices/transform.py +++ b/src/haven/devices/transform.py @@ -9,10 +9,11 @@ DeviceVector, HintedSignal, StandardReadable, + SubsetEnum, + StrictEnum, ) from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw -from ..typing import StrEnum from .synApps import EpicsRecordDeviceCommonAll, EpicsSynAppsRecordEnableMixin CHANNEL_LETTERS_LIST = "A B C D E F G H I J K L M N O P".split() @@ -34,7 +35,7 @@ class TransformRecordChannel(StandardReadable): ~reset """ - class PVValidity(StrEnum): + class PVValidity(SubsetEnum): EXT_PV_NC = "Ext PV NC" EXT_PV_OK = "Ext PV OK" LOCAL_PV = "Local PV" @@ -86,11 +87,11 @@ class TransformRecord(EpicsRecordDeviceCommonAll): :see: https://htmlpreview.github.io/?https://raw.githubusercontent.com/epics-modules/calc/R3-6-1/documentation/TransformRecord.html#Fields """ - class CalcOption(StrEnum): + class CalcOption(StrictEnum): CONDITIONAL = "Conditional" ALWAYS = "Always" - class InvalidLinkAction(StrEnum): + class InvalidLinkAction(SubsetEnum): IGNORE_ERROR = "Ignore error" DO_NOTHING = "Do Nothing" diff --git a/src/haven/devices/xray_source.py b/src/haven/devices/xray_source.py index 06919502..f012f035 100644 --- a/src/haven/devices/xray_source.py +++ b/src/haven/devices/xray_source.py @@ -7,6 +7,7 @@ Signal, StandardReadable, soft_signal_rw, + SubsetEnum, ) from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x @@ -84,7 +85,7 @@ class PlanarUndulator(StandardReadable): _ophyd_labels_ = {"xray_sources", "undulators"} - class AccessMode(str, Enum): + class AccessMode(SubsetEnum): USER = "User" OPERATOR = "Operator" MACHINE_PHYSICS = "Machine Physics" diff --git a/src/haven/tests/test_ion_chamber.py b/src/haven/tests/test_ion_chamber.py index 4ffb823a..97d9ff37 100644 --- a/src/haven/tests/test_ion_chamber.py +++ b/src/haven/tests/test_ion_chamber.py @@ -1,3 +1,4 @@ +import asyncio from unittest.mock import AsyncMock import numpy as np @@ -48,7 +49,7 @@ async def test_readables(ion_chamber): ] actual_readables = (await ion_chamber.describe()).keys() assert sorted(actual_readables) == sorted(expected_readables) - # Check confirables + # Check configurables expected_configables = [ "I0-counts_per_volt_second", "I0-voltmeter-model_name", @@ -142,6 +143,10 @@ async def test_trigger_dark_current(ion_chamber, monkeypatch): async def test_net_current_signal(ion_chamber): """Test that scaler tick counts get properly converted to ion chamber current.""" await ion_chamber.connect(mock=True) + await asyncio.gather( + ion_chamber.net_current.connect(mock=False), + ion_chamber.preamp.gain.connect(mock=False), + ) # Set the necessary dependent signals set_mock_value(ion_chamber.counts_per_volt_second, 10e6) # 100 Mhz / 10 V set_mock_value(ion_chamber.scaler_channel.net_count, int(13e6)) # 1.3V @@ -161,6 +166,10 @@ async def test_net_current_signal(ion_chamber): async def test_raw_current_signal(ion_chamber): """Test that scaler tick counts get properly converted to ion chamber current.""" await ion_chamber.connect(mock=True) + await asyncio.gather( + ion_chamber.raw_current.connect(mock=False), + ion_chamber.preamp.gain.connect(mock=False), + ) # Set the necessary dependent signals set_mock_value(ion_chamber.counts_per_volt_second, 10e6) # 100 Mhz / 10 V set_mock_value(ion_chamber.scaler_channel.raw_count, int(13e6)) # 1.3V diff --git a/src/haven/tests/test_motor.py b/src/haven/tests/test_motor.py index f9bb8cbe..25b3171c 100644 --- a/src/haven/tests/test_motor.py +++ b/src/haven/tests/test_motor.py @@ -66,7 +66,7 @@ async def test_stop_button(motor): assert motor.motor_stop.parent is motor await motor.motor_stop.trigger() mock = get_mock_put(motor.motor_stop) - mock.assert_called_once_with(1, wait=False, timeout=10.0) + mock.assert_called_once_with(1, wait=True) @pytest.mark.asyncio diff --git a/src/haven/tests/test_shutter.py b/src/haven/tests/test_shutter.py index 456a2ea4..8b37ec6f 100644 --- a/src/haven/tests/test_shutter.py +++ b/src/haven/tests/test_shutter.py @@ -36,7 +36,7 @@ async def test_shutter_setpoint(shutter): set_mock_value(shutter.readback, ShutterState.CLOSED) await status assert not open_put.called - close_put.assert_called_once_with(1, timeout=16, wait=False) + close_put.assert_called_once_with(1, wait=False) # Open the shutter open_put.reset_mock() close_put.reset_mock() @@ -46,7 +46,7 @@ async def test_shutter_setpoint(shutter): set_mock_value(shutter.readback, ShutterState.OPEN) await status assert not close_put.called - open_put.assert_called_once_with(1, timeout=18, wait=False) + open_put.assert_called_once_with(1, wait=False) async def test_shutter_check_value(shutter): diff --git a/src/haven/tests/test_signal.py b/src/haven/tests/test_signal.py index f33d2bd3..2cbf9041 100644 --- a/src/haven/tests/test_signal.py +++ b/src/haven/tests/test_signal.py @@ -1,10 +1,11 @@ import asyncio import math +from unittest.mock import MagicMock import pytest from ophyd_async.core import Device, get_mock_put from ophyd_async.core._signal import soft_signal_rw -from ophyd_async.epics.signal import epics_signal_x +from ophyd_async.epics.signal import epics_signal_x, epics_signal_rw from haven.devices.signal import derived_signal_rw, derived_signal_x @@ -82,6 +83,21 @@ async def test_derived_forward(device): assert await device.y.get_value() == pytest.approx(2 / math.sqrt(2)) +async def test_mock_subscribe(): + """This is to test the behavior of ophyd-async, not Haven.""" + base_signal = epics_signal_rw(float, "sldfkj") + derived_signal = derived_signal_rw(float, derived_from={"base": base_signal}) + callback = MagicMock() + await base_signal.connect(mock=True) + await derived_signal.connect(mock=False) + derived_signal.subscribe(callback) + callback.reset_mock() + assert not callback.called + # Now change the value and check whether the mocked signal was changed + await base_signal.set(5) + assert callback.called + + @pytest.mark.asyncio async def test_derived_defaults(device): """Does the derived signal report the derived value by default.""" @@ -155,4 +171,4 @@ async def test_signal_x_trigger(device): # Now trigger the parent mocked_put = get_mock_put(signal) await derived.trigger() - mocked_put.assert_called_once_with(None, wait=True, timeout=10.0) + mocked_put.assert_called_once_with(None, wait=True) diff --git a/src/haven/tests/test_srs570.py b/src/haven/tests/test_srs570.py index 4afc5f75..51e9156e 100644 --- a/src/haven/tests/test_srs570.py +++ b/src/haven/tests/test_srs570.py @@ -1,7 +1,8 @@ +import asyncio from unittest import mock import pytest -from ophyd_async.core import DEFAULT_TIMEOUT +from ophyd_async.core import DEFAULT_TIMEOUT, get_mock_put from haven.devices.srs570 import GainSignal, SRS570PreAmplifier @@ -9,7 +10,13 @@ @pytest.fixture() async def preamp(): preamp = SRS570PreAmplifier("255idcVEM:SR02:", name="") + # Derived signals should not be mocked await preamp.connect(mock=True) + await asyncio.gather( + preamp.gain_level.connect(mock=False), + preamp.gain.connect(mock=False), + preamp.gain_db.connect(mock=False), + ) return preamp @@ -138,8 +145,8 @@ async def test_preamp_gain_settling(gain_value, gain_unit, gain_mode, mocker, pr sleep_mock.reset_mock() await preamp.sensitivity_value.set(gain_value) # Check that the signal's ``set`` was called with correct arguments - preamp.sensitivity_value._backend.put_mock.assert_called_once_with( - gain_value, wait=True, timeout=DEFAULT_TIMEOUT + get_mock_put(preamp.sensitivity_value).assert_called_once_with( + gain_value, wait=True, ) # Check that the settle time was included sleep_mock.assert_called_once_with(settle_time) @@ -216,7 +223,7 @@ async def test_get_gain_level(preamp): await preamp.sensitivity_value.set("20") await preamp.sensitivity_unit.set("uA/V"), await preamp.offset_value.set("2"), # 2 uA/V - await preamp.offset_unit.set("uA/V"), + await preamp.offset_unit.set("uA"), # Check that the gain level moved gain_level = await preamp.gain_level.get_value() assert gain_level == 5 From c0892942b41f540271d4b63f6c0e52eec6f5d7fd Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Mon, 11 Nov 2024 14:34:53 -0600 Subject: [PATCH 02/10] Converted ``ConfigSignal`` and ``HintedSignal`` to their new StandardReadableFormat attributes. --- src/haven/devices/delay.py | 16 ++++++++-------- src/haven/devices/detectors/aravis.py | 2 +- src/haven/devices/energy_positioner.py | 4 ++-- src/haven/devices/ion_chamber.py | 6 +++--- src/haven/devices/labjack.py | 23 +++++++++++------------ src/haven/devices/monochromator.py | 6 +++--- src/haven/devices/motor.py | 6 +++--- src/haven/devices/scaler.py | 16 ++++++++-------- src/haven/devices/shutter.py | 2 +- src/haven/devices/synApps.py | 12 ++++++------ src/haven/devices/transform.py | 13 ++++++------- src/haven/devices/xray_source.py | 11 +++++------ src/haven/tests/test_ion_chamber.py | 6 ++++++ src/haven/tests/test_positioner.py | 2 +- src/haven/tests/test_signal.py | 2 +- 15 files changed, 65 insertions(+), 62 deletions(-) diff --git a/src/haven/devices/delay.py b/src/haven/devices/delay.py index 469d6d47..8b932123 100644 --- a/src/haven/devices/delay.py +++ b/src/haven/devices/delay.py @@ -2,15 +2,15 @@ from typing import Type from ophyd_async.core import ( - ConfigSignal, DeviceVector, SignalRW, StandardReadable, + StandardReadableFormat, SubsetEnum, StrictEnum, T, ) -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x class StrEnum(str, enum.Enum): @@ -49,7 +49,7 @@ class Reference(StrictEnum): H = "H" def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.reference = epics_signal_io(self.Reference, f"{prefix}ReferenceM") self.delay = epics_signal_io(float, f"{prefix}DelayA") super().__init__(name=name) @@ -61,7 +61,7 @@ class Polarity(StrictEnum): POS = "POS" def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.polarity = epics_signal_io(self.Polarity, f"{prefix}OutputPolarityB") self.amplitude = epics_signal_io(float, f"{prefix}OutputAmpA") self.offset = epics_signal_io(float, f"{prefix}OutputOffsetA") @@ -72,7 +72,7 @@ def __init__(self, prefix: str, name: str = ""): class DG645DelayOutput(DG645Output): def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.trigger_prescale = epics_signal_io(int, f"{prefix}TriggerPrescaleL") self.trigger_phase = epics_signal_io(int, f"{prefix}TriggerPhaseL") super().__init__(prefix=prefix, name=name) @@ -111,7 +111,7 @@ class BurstConfig(SubsetEnum): def __init__(self, prefix: str, name: str = ""): # Conventional signals - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.label = epics_signal_rw(str, f"{prefix}Label") self.device_id = epics_signal_r(str, f"{prefix}IdentSI") self.status = epics_signal_r(str, f"{prefix}StatusSI") @@ -164,7 +164,7 @@ def __init__(self, prefix: str, name: str = ""): } ) # Trigger control - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.trigger_source = epics_signal_io(self.TriggerSource,f"{prefix}TriggerSourceM",) self.trigger_inhibit = epics_signal_io(self.TriggerInhibit, f"{prefix}TriggerInhibitM") self.trigger_level = epics_signal_io(float, f"{prefix}TriggerLevelA") @@ -173,7 +173,7 @@ def __init__(self, prefix: str, name: str = ""): self.trigger_holdoff = epics_signal_io(float, f"{prefix}TriggerHoldoffA") self.trigger_prescale = epics_signal_io(int, f"{prefix}TriggerPrescaleL") # Burst settings - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.burst_mode = epics_signal_io(bool, f"{prefix}BurstModeB") self.burst_count = epics_signal_io(int, f"{prefix}BurstCountL") self.burst_config = epics_signal_io(self.BurstConfig, f"{prefix}BurstConfigB" ) diff --git a/src/haven/devices/detectors/aravis.py b/src/haven/devices/detectors/aravis.py index edc534f4..21f3e6e3 100644 --- a/src/haven/devices/detectors/aravis.py +++ b/src/haven/devices/detectors/aravis.py @@ -1,6 +1,6 @@ from ophyd_async.core import SubsetEnum from ophyd_async.epics.adaravis import AravisDetector as DetectorBase -from ophyd_async.epics.signal import epics_signal_rw_rbv +from ophyd_async.epics.core import epics_signal_rw_rbv from .area_detectors import HavenDetector diff --git a/src/haven/devices/energy_positioner.py b/src/haven/devices/energy_positioner.py index 2ea02e0e..103d5c21 100644 --- a/src/haven/devices/energy_positioner.py +++ b/src/haven/devices/energy_positioner.py @@ -1,6 +1,6 @@ import logging -from ophyd_async.core import HintedSignal, Signal +from ophyd_async.core import Signal, StandardReadableFormat from ..positioner import Positioner from .monochromator import Monochromator @@ -72,7 +72,7 @@ def __init__( forward=self.set_energy, inverse=self.get_energy, ) - with self.add_children_as_readables(HintedSignal): + with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): self.readback = derived_signal_r( float, derived_from={"mono": self.monochromator.energy.user_readback}, diff --git a/src/haven/devices/ion_chamber.py b/src/haven/devices/ion_chamber.py index 0d5b36a1..dbc6c30b 100644 --- a/src/haven/devices/ion_chamber.py +++ b/src/haven/devices/ion_chamber.py @@ -9,8 +9,8 @@ from ophyd_async.core import ( DEFAULT_TIMEOUT, AsyncStatus, - ConfigSignal, StandardReadable, + StandardReadableFormat, TriggerInfo, soft_signal_rw, wait_for_value, @@ -74,7 +74,7 @@ def __init__( analog_outputs=[], digital_words=[], ) - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.counts_per_volt_second = soft_signal_rw( float, initial_value=counts_per_volt_second ) @@ -107,7 +107,7 @@ def __init__( self.mcs.preset_time, self.mcs.snl_connected, ], - ConfigSignal, + StandardReadableFormat.CONFIG_SIGNAL, ) # Add calculated signals with self.add_children_as_readables(): diff --git a/src/haven/devices/labjack.py b/src/haven/devices/labjack.py index 9e5bada5..cd5773bd 100644 --- a/src/haven/devices/labjack.py +++ b/src/haven/devices/labjack.py @@ -42,15 +42,14 @@ def __init__(self, prefix: str, name: str = ""): from ophyd_async.core import ( DEFAULT_TIMEOUT, AsyncStatus, - ConfigSignal, DeviceVector, - HintedSignal, StandardReadable, + StandardReadableFormat, SubsetEnum, StrictEnum, observe_value, ) -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x from ..typing import StrEnum from .synApps import EpicsRecordInputFields, EpicsRecordOutputFields @@ -119,7 +118,7 @@ def __init__(self, prefix: str, name: str = ""): with self.add_children_as_readables(): self.desired_value = epics_signal_rw(float, f"{prefix}.VAL") self.raw_value = epics_signal_rw(int, f"{prefix}.RVAL") - with self.add_children_as_readables(HintedSignal): + with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): self.readback_value = epics_signal_r(int, f"{prefix}.RBV") super().__init__(prefix=prefix, name=name) @@ -171,7 +170,7 @@ class Resolution(SubsetEnum): EIGHT = "8" def __init__(self, prefix: str, ch_num: int, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.differential = epics_signal_rw( self.DifferentialMode, f"{prefix}AiDiff{ch_num}" ) @@ -187,7 +186,7 @@ def __init__(self, prefix: str, ch_num: int, name: str = ""): self.range = epics_signal_rw(self.Range, f"{prefix}AiRange{ch_num}") self.mode = epics_signal_rw(self.Mode, f"{prefix}AiMode{ch_num}") self.enable = epics_signal_rw(bool, f"{prefix}AiEnable{ch_num}") - with self.add_children_as_readables(HintedSignal): + with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): self.final_value = epics_signal_r(float, f"{prefix}Ai{ch_num}.VAL") self.raw_value = epics_signal_rw(int, f"{prefix}Ai{ch_num}.RVAL") super().__init__(prefix=f"{prefix}Ai{ch_num}", name=name) @@ -224,7 +223,7 @@ def __init__(self, prefix: str, ch_num: int, name: str = ""): with self.add_children_as_readables(): self.input = BinaryInput(f"{prefix}Bi{ch_num}") self.output = BinaryOutput(f"{prefix}Bo{ch_num}") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.direction = epics_signal_rw(self.Direction, f"{prefix}Bd{ch_num}") super().__init__(name=name) @@ -290,7 +289,7 @@ def __init__(self, prefix: str, name: str = "", waveforms=[]): ) self.dwell_actual = epics_signal_rw(float, f"{prefix}WaveDigDwellActual") self.total_time = epics_signal_rw(float, f"{prefix}WaveDigTotalTime") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.num_points = epics_signal_rw(int, f"{prefix}WaveDigNumPoints") self.dwell_time = epics_signal_rw(float, f"{prefix}WaveDigDwell") self.first_chan = epics_signal_rw(self.FirstChannel, f"{prefix}WaveDigFirstChan") @@ -351,7 +350,7 @@ class TriggerMode(StrictEnum): CONTINUOS = "Continuous" def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.external_trigger = epics_signal_rw( self.TriggerSource, f"{prefix}WaveGenExtTrigger" ) @@ -370,7 +369,7 @@ def __init__(self, prefix: str, name: str = ""): self.dwell = epics_signal_r(float, f"{prefix}WaveGenDwell") self.dwell_actual = epics_signal_r(float, f"{prefix}WaveGenDwellActual") self.total_time = epics_signal_r(float, f"{prefix}WaveGenTotalTime") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.num_points = epics_signal_r(int, f"{prefix}WaveGenNumPoints") self.current_point = epics_signal_r(int, f"{prefix}WaveGenCurrentPoint") @@ -393,7 +392,7 @@ def __init__(self, prefix: str, name: str = ""): self.internal_frequency = epics_signal_rw(float, f"{prefix}WaveGenIntFrequency") # Waveform specific settings - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.user_waveform_0 = epics_signal_rw( NDArray[np.float64], f"{prefix}WaveGenUserWF0" ) @@ -479,7 +478,7 @@ def __init__( analog_outputs=range(2), digital_words=["dio", "eio", "fio", "mio", "cio"], ): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.model_name = epics_signal_r(self.Model, f"{prefix}ModelName") self.firmware_version = epics_signal_r(str, f"{prefix}FirmwareVersion") self.serial_number = epics_signal_r(str, f"{prefix}SerialNumber") diff --git a/src/haven/devices/monochromator.py b/src/haven/devices/monochromator.py index bede4587..19c9fb8e 100644 --- a/src/haven/devices/monochromator.py +++ b/src/haven/devices/monochromator.py @@ -1,8 +1,8 @@ import logging from enum import Enum -from ophyd_async.core import ConfigSignal, StandardReadable, StrictEnum -from ophyd_async.epics.signal import epics_signal_rw +from ophyd_async.core import StandardReadable, StandardReadableFormat, StrictEnum +from ophyd_async.epics.core import epics_signal_rw from .motor import Motor @@ -30,7 +30,7 @@ def __init__(self, prefix: str, name: str = ""): self.vert = Motor(f"{prefix}ACS:m2") self.roll2 = Motor(f"{prefix}ACS:m5") self.pitch2 = Motor(f"{prefix}ACS:m6") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): # Transform constants, etc. self.id_tracking = epics_signal_rw(bool, f"{prefix}ID_tracking") self.id_offset = epics_signal_rw(float, f"{prefix}ID_offset") diff --git a/src/haven/devices/motor.py b/src/haven/devices/motor.py index 7353d572..0e43c08e 100644 --- a/src/haven/devices/motor.py +++ b/src/haven/devices/motor.py @@ -3,9 +3,9 @@ from ophyd import Component as Cpt from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO, Kind -from ophyd_async.core import DEFAULT_TIMEOUT, ConfigSignal, SubsetEnum, StrictEnum +from ophyd_async.core import DEFAULT_TIMEOUT, SubsetEnum, StrictEnum, StandardReadableFormat from ophyd_async.epics.motor import Motor as MotorBase -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw from ophydregistry import Registry from .motor_flyer import MotorFlyer @@ -40,7 +40,7 @@ def __init__( self._old_flyer_velocity = None self.auto_name = auto_name # Configuration signals - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.description = epics_signal_rw(str, f"{prefix}.DESC") self.user_offset = epics_signal_rw(float, f"{prefix}.OFF") self.user_offset_dir = epics_signal_rw(self.Direction, f"{prefix}.DIR") diff --git a/src/haven/devices/scaler.py b/src/haven/devices/scaler.py index 376fe7b2..8c0050e8 100644 --- a/src/haven/devices/scaler.py +++ b/src/haven/devices/scaler.py @@ -1,7 +1,7 @@ import numpy as np from numpy.typing import NDArray -from ophyd_async.core import ConfigSignal, DeviceVector, HintedSignal, StandardReadable, SubsetEnum, StrictEnum -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x +from ophyd_async.core import DeviceVector, StandardReadable, StandardReadableFormat, SubsetEnum, StrictEnum +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x from ..typing import StrEnum @@ -15,7 +15,7 @@ class ScalerChannel(StandardReadable): def __init__(self, prefix, channel_num, name=""): epics_ch_num = channel_num + 1 # EPICS is 1-indexed # Hinted signals - with self.add_children_as_readables(HintedSignal): + with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): net_suffix = ( f"_net{num_to_char((channel_num // 12))}" f".{num_to_char(channel_num % 12)}" @@ -25,7 +25,7 @@ def __init__(self, prefix, channel_num, name=""): with self.add_children_as_readables(): self.raw_count = epics_signal_r(float, f"{prefix}.S{epics_ch_num}") # Configuration signals - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.description = epics_signal_rw(str, f"{prefix}.NM{epics_ch_num}") self.is_gate = epics_signal_rw(bool, f"{prefix}.G{epics_ch_num}") self.preset_count = epics_signal_rw(float, f"{prefix}.PR{epics_ch_num}") @@ -43,10 +43,10 @@ class MCAMode(SubsetEnum): def __init__(self, prefix, name=""): # Signals - with self.add_children_as_readables(HintedSignal): + with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): self.spectrum = epics_signal_r(NDArray[np.int32], f"{prefix}.VAL") self.background = epics_signal_r(NDArray[np.int32], f"{prefix}.BG") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.mode = epics_signal_rw(self.MCAMode, f"{prefix}.MODE") super().__init__(name=name) @@ -118,7 +118,7 @@ def __init__(self, prefix, channels: list[int], name=""): self.acquiring = epics_signal_r(self.Acquiring, f"{prefix}Acquiring") self.user_led = epics_signal_rw(bool, f"{prefix}UserLED") # Config signals - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.preset_time = epics_signal_rw(float, f"{prefix}PresetReal") self.dwell_time = epics_signal_rw(float, f"{prefix}Dwell") self.prescale = epics_signal_rw(int, f"{prefix}Prescale") @@ -183,7 +183,7 @@ def __init__(self, prefix, channels: list[int], name=""): # Scaler-specific signals with self.add_children_as_readables(): self.elapsed_time = epics_signal_r(float, f"{prefix}.T") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.delay = epics_signal_rw(float, f"{prefix}.DLY") self.clock_frequency = epics_signal_rw(float, f"{prefix}.FREQ") self.count_mode = epics_signal_rw(self.CountMode, f"{prefix}.CONT") diff --git a/src/haven/devices/shutter.py b/src/haven/devices/shutter.py index 82dcfd13..8a2cc29c 100644 --- a/src/haven/devices/shutter.py +++ b/src/haven/devices/shutter.py @@ -5,7 +5,7 @@ from ophyd.utils.errors import ReadOnlyError from ophyd_async.core import soft_signal_rw -from ophyd_async.epics.signal import epics_signal_r +from ophyd_async.epics.core import epics_signal_r from ..positioner import Positioner from .signal import derived_signal_rw, epics_signal_xval diff --git a/src/haven/devices/synApps.py b/src/haven/devices/synApps.py index 9e8ac6eb..6aa95814 100644 --- a/src/haven/devices/synApps.py +++ b/src/haven/devices/synApps.py @@ -1,7 +1,7 @@ import asyncio -from ophyd_async.core import ConfigSignal, Device, StandardReadable, SubsetEnum, StrictEnum -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x +from ophyd_async.core import Device, StandardReadable, StandardReadableFormat, SubsetEnum, StrictEnum +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x class AlarmStatus(SubsetEnum): @@ -65,7 +65,7 @@ class ScanInterval(SubsetEnum): # Config signals def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.description = epics_signal_rw(str, f"{prefix}.DESC") self.scanning_rate = epics_signal_rw(self.ScanInterval, f"{prefix}.SCAN") self.device_type = epics_signal_r(self.DeviceType, f"{prefix}.DTYP") @@ -91,7 +91,7 @@ class EpicsSynAppsRecordEnableMixin(Device): """Supports ``{PV}Enable`` feature from user databases.""" def __init__(self, prefix, name=""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.enable = epics_signal_rw(int, "Enable") super().__init__(name=name) @@ -108,7 +108,7 @@ class EpicsRecordInputFields(EpicsRecordDeviceCommonAll): """ def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.input_link = epics_signal_rw(str, f"{prefix}.INP") super().__init__(prefix=prefix, name=name) @@ -123,7 +123,7 @@ class ModeSelect(SubsetEnum): CLOSED_LOOP = "closed_loop" def __init__(self, prefix: str, name: str = ""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.output_link = epics_signal_rw(str, f"{prefix}.OUT") self.desired_output_location = epics_signal_rw(str, f"{prefix}.DOL") self.output_mode_select = epics_signal_rw(self.ModeSelect, f"{prefix}.OMSL") diff --git a/src/haven/devices/transform.py b/src/haven/devices/transform.py index d56f44af..13530162 100644 --- a/src/haven/devices/transform.py +++ b/src/haven/devices/transform.py @@ -4,15 +4,14 @@ # from ophyd import Device from ophyd_async.core import ( - ConfigSignal, Device, DeviceVector, - HintedSignal, StandardReadable, + StandardReadableFormat, SubsetEnum, StrictEnum, ) -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw from .synApps import EpicsRecordDeviceCommonAll, EpicsSynAppsRecordEnableMixin @@ -45,7 +44,7 @@ def __init__(self, prefix, letter, name=""): self._ch_letter = letter with self.add_children_as_readables(): self.current_value = epics_signal_rw(float, f"{prefix}.{letter}") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.input_pv = epics_signal_rw(str, f"{prefix}.INP{letter}") self.comment = epics_signal_rw(str, f"{prefix}.CMT{letter}") self.expression = epics_signal_rw( @@ -96,7 +95,7 @@ class InvalidLinkAction(SubsetEnum): DO_NOTHING = "Do Nothing" def __init__(self, prefix, name=""): - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.units = epics_signal_rw( str, f"{prefix}.EGU", @@ -143,7 +142,7 @@ async def reset(self): *[ch.reset() for ch in channels], ) # Restore the hinted channels - self.add_readables(channels, HintedSignal) + self.add_readables(channels, StandardReadableFormat.HINTED_SIGNAL) class UserTransformN(EpicsSynAppsRecordEnableMixin, TransformRecord): @@ -159,7 +158,7 @@ class UserTransformsDevice(Device): def __init__(self, prefix, name=""): # Config attrs - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.enable = epics_signal_rw(int, f"{prefix}userTranEnable", name="enable") # Read attrs with self.add_children_as_readables(): diff --git a/src/haven/devices/xray_source.py b/src/haven/devices/xray_source.py index f012f035..6b293621 100644 --- a/src/haven/devices/xray_source.py +++ b/src/haven/devices/xray_source.py @@ -2,14 +2,13 @@ from enum import Enum, IntEnum from ophyd_async.core import ( - ConfigSignal, - HintedSignal, Signal, StandardReadable, + StandardReadableFormat, soft_signal_rw, SubsetEnum, ) -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x from ..positioner import Positioner from .signal import derived_signal_r, derived_signal_x @@ -45,11 +44,11 @@ def __init__( name: str = "", min_move: float = 0.0, ): - with self.add_children_as_readables(HintedSignal): + with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): self.readback = epics_signal_rw(float, f"{prefix}M.VAL") with self.add_children_as_readables(): self.setpoint = epics_signal_rw(float, f"{prefix}SetC.VAL") - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.units = epics_signal_r(str, f"{prefix}SetC.EGU") self.precision = epics_signal_r(int, f"{prefix}SetC.PREC") self.velocity = soft_signal_rw( @@ -99,7 +98,7 @@ def __init__(self, prefix: str, name: str = ""): self.done = epics_signal_r(bool, f"{prefix}BusyDeviceM.VAL") self.motor_drive_status = epics_signal_r(int, f"{prefix}MotorDriveStatusM.VAL") # Configuration state for the undulator - with self.add_children_as_readables(ConfigSignal): + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.harmonic_value = epics_signal_rw(int, f"{prefix}HarmonicValueC") self.total_power = epics_signal_r(float, f"{prefix}TotalPowerM.VAL") self.gap_deadband = epics_signal_rw(int, f"{prefix}DeadbandGapC") diff --git a/src/haven/tests/test_ion_chamber.py b/src/haven/tests/test_ion_chamber.py index 97d9ff37..2eb85cb0 100644 --- a/src/haven/tests/test_ion_chamber.py +++ b/src/haven/tests/test_ion_chamber.py @@ -49,6 +49,12 @@ async def test_readables(ion_chamber): ] actual_readables = (await ion_chamber.describe()).keys() assert sorted(actual_readables) == sorted(expected_readables) + # Check signal hints + expected_hints = [ + "I0-net_current", + ] + actual_hints = ion_chamber.hints['fields'] + assert sorted(actual_hints) == sorted(expected_hints) # Check configurables expected_configables = [ "I0-counts_per_volt_second", diff --git a/src/haven/tests/test_positioner.py b/src/haven/tests/test_positioner.py index 478f1f30..40e031cc 100644 --- a/src/haven/tests/test_positioner.py +++ b/src/haven/tests/test_positioner.py @@ -2,7 +2,7 @@ import pytest from ophyd_async.core import get_mock_put, set_mock_value -from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_x +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x from haven.positioner import Positioner diff --git a/src/haven/tests/test_signal.py b/src/haven/tests/test_signal.py index 2cbf9041..85ed030d 100644 --- a/src/haven/tests/test_signal.py +++ b/src/haven/tests/test_signal.py @@ -5,7 +5,7 @@ import pytest from ophyd_async.core import Device, get_mock_put from ophyd_async.core._signal import soft_signal_rw -from ophyd_async.epics.signal import epics_signal_x, epics_signal_rw +from ophyd_async.epics.core import epics_signal_x, epics_signal_rw from haven.devices.signal import derived_signal_rw, derived_signal_x From 98929bfd370213c69ec40323dda1e153f8aabadc Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Tue, 12 Nov 2024 09:33:31 -0600 Subject: [PATCH 03/10] Updated the signals on the ion chamber to have fewer hints. --- src/haven/devices/ion_chamber.py | 112 +++++++++++++++++++++------- src/haven/tests/test_ion_chamber.py | 6 +- 2 files changed, 85 insertions(+), 33 deletions(-) diff --git a/src/haven/devices/ion_chamber.py b/src/haven/devices/ion_chamber.py index dbc6c30b..434bfa05 100644 --- a/src/haven/devices/ion_chamber.py +++ b/src/haven/devices/ion_chamber.py @@ -65,24 +65,64 @@ def __init__( self._scaler_channel = scaler_channel self._voltmeter_channel = voltmeter_channel self.auto_name = auto_name - with self.add_children_as_readables(): - self.preamp = SRS570PreAmplifier(preamp_prefix) - self.voltmeter = LabJackT7( - prefix=voltmeter_prefix, - analog_inputs=[voltmeter_channel], - digital_ios=[], - analog_outputs=[], - digital_words=[], - ) with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.counts_per_volt_second = soft_signal_rw( float, initial_value=counts_per_volt_second ) - # Add subordinate devices + # Add the SRS570 pre-amplifier signals + with self.add_children_as_readables(): + self.preamp = SRS570PreAmplifier(preamp_prefix) + # Add the labjack voltmeter device + self.voltmeter = LabJackT7( + prefix=voltmeter_prefix, + analog_inputs=[voltmeter_channel], + digital_ios=[], + analog_outputs=[], + digital_words=[], + ) + self.add_readables([self.voltmeter_channel.final_value]) + self.add_readables( + [ + self.voltmeter.analog_in_resolution_all, + self.voltmeter.analog_in_sampling_rate, + self.voltmeter.analog_in_settling_time_all, + self.voltmeter_channel.description, + self.voltmeter_channel.device_type, + self.voltmeter_channel.differential, + self.voltmeter_channel.enable, + self.voltmeter_channel.high, + self.voltmeter_channel.input_link, + self.voltmeter_channel.low, + self.voltmeter_channel.mode, + self.voltmeter_channel.range, + self.voltmeter_channel.resolution, + self.voltmeter_channel.scanning_rate, + self.voltmeter_channel.temperature_units, + self.voltmeter.device_temperature, + self.voltmeter.driver_version, + self.voltmeter.firmware_version, + self.voltmeter.last_error_message, + self.voltmeter.ljm_version, + self.voltmeter.model_name, + self.voltmeter.poll_sleep_ms, + self.voltmeter.serial_number, + ], + StandardReadableFormat.CONFIG_SIGNAL, + ) + # Add scaler channel self.mcs = MultiChannelScaler( prefix=scaler_prefix, channels=[0, scaler_channel] ) - self.add_readables([self.mcs.scaler]) + self.add_readables( + [ + self.mcs.scaler.channels[0].net_count, + self.mcs.scaler.channels[0].raw_count, + self.scaler_channel.net_count, + self.scaler_channel.raw_count, + self.mcs.scaler.elapsed_time, + ], + StandardReadableFormat.UNCACHED_SIGNAL, + ) self.add_readables( [ self.mcs.acquire_mode, @@ -106,24 +146,38 @@ def __init__( self.mcs.prescale, self.mcs.preset_time, self.mcs.snl_connected, + self.mcs.scaler.clock_frequency, + self.mcs.scaler.count_mode, + self.mcs.scaler.delay, + self.mcs.scaler.preset_time, + self.mcs.scaler.channels[0].description, + self.mcs.scaler.channels[0].is_gate, + self.mcs.scaler.channels[0].offset_rate, + self.mcs.scaler.channels[0].preset_count, + self.scaler_channel.description, + self.scaler_channel.is_gate, + self.scaler_channel.offset_rate, + self.scaler_channel.preset_count, ], StandardReadableFormat.CONFIG_SIGNAL, ) # Add calculated signals - with self.add_children_as_readables(): - self.net_current = derived_signal_r( - float, - name="current", - units="A", - derived_from={ - "gain": self.preamp.gain, - "count": self.scaler_channel.net_count, - "clock_count": self.mcs.scaler.channels[0].raw_count, - "clock_frequency": self.mcs.scaler.clock_frequency, - "counts_per_volt_second": self.counts_per_volt_second, - }, - inverse=self._counts_to_amps, - ) + self.net_current = derived_signal_r( + float, + name="current", + units="A", + derived_from={ + "gain": self.preamp.gain, + "count": self.scaler_channel.net_count, + "clock_count": self.mcs.scaler.channels[0].raw_count, + "clock_frequency": self.mcs.scaler.clock_frequency, + "counts_per_volt_second": self.counts_per_volt_second, + }, + inverse=self._counts_to_amps, + ) + self.add_readables( + [self.net_current], StandardReadableFormat.HINTED_UNCACHED_SIGNAL + ) # Measured current without dark current correction self.raw_current = derived_signal_r( float, @@ -138,6 +192,7 @@ def __init__( }, inverse=self._counts_to_amps, ) + self.add_readables([self.raw_current], StandardReadableFormat.UNCACHED_SIGNAL) super().__init__(name=name) def _counts_to_amps( @@ -161,7 +216,8 @@ def _counts_to_amps( return float("nan") def __repr__(self): - return f"<{type(self).__name__}: '{self.name}' ({self.scaler_channel.raw_count.source})>" + return (f"<{type(self).__name__}: '{self.name}' " + f"({self.scaler_channel.raw_count.source})>") @property def scaler_channel(self): @@ -248,9 +304,7 @@ async def record_dark_current(self): integration_time = await self.mcs.scaler.dark_current_time.get_value() timeout = integration_time + DEFAULT_TIMEOUT count_signal = self.mcs.scaler.count - await wait_for_value( - count_signal, False, timeout=timeout - ) + await wait_for_value(count_signal, False, timeout=timeout) def record_fly_reading(self, reading, **kwargs): if self._is_flying: diff --git a/src/haven/tests/test_ion_chamber.py b/src/haven/tests/test_ion_chamber.py index 2eb85cb0..fbf75f52 100644 --- a/src/haven/tests/test_ion_chamber.py +++ b/src/haven/tests/test_ion_chamber.py @@ -40,6 +40,7 @@ async def test_readables(ion_chamber): await ion_chamber.connect(mock=True) expected_readables = [ "I0-net_current", + "I0-raw_current", "I0-voltmeter-analog_inputs-1-final_value", "I0-mcs-scaler-channels-0-net_count", "I0-mcs-scaler-channels-0-raw_count", @@ -53,7 +54,7 @@ async def test_readables(ion_chamber): expected_hints = [ "I0-net_current", ] - actual_hints = ion_chamber.hints['fields'] + actual_hints = ion_chamber.hints["fields"] assert sorted(actual_hints) == sorted(expected_hints) # Check configurables expected_configables = [ @@ -317,14 +318,11 @@ async def test_flyscan_collect(ion_chamber, trigger_info): } for (datum, timestamp) in zip(channel_numbers, expected_timestamps) ] - # Ignore the first collected data point because it's during taxiing - expected_data = sim_data[1:] # The real timestamps should be midway between PSO pulses collected = [c async for c in ion_chamber.collect_pages()] assert len(collected) == 1 collected = collected[0] # Confirm data have the right structure - raw_name = ion_chamber.scaler_channel.net_count.name assert collected["time"] == 1024 assert_allclose( collected["data"][ion_chamber.scaler_channel.raw_count.name], sim_raw_data[:6] From 8491ea53e04a5c8fc32e73b00e345fef888bd046 Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Tue, 12 Nov 2024 09:33:50 -0600 Subject: [PATCH 04/10] fix for an ophyd-async bug where device vectors didn't have parents. --- environment.yml | 3 ++- pyproject.toml | 2 +- src/haven/devices/transform.py | 10 +++------- src/haven/tests/test_signal.py | 23 ++++++++++++++++++++++- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/environment.yml b/environment.yml index c78822d6..1a5ccd19 100644 --- a/environment.yml +++ b/environment.yml @@ -58,7 +58,8 @@ dependencies: - bluesky-adaptive - bluesky >=1.8.1 - ophyd >=1.6.3 - - ophyd-async >= 0.8.0a3 + # - ophyd-async > 0.8.0a4 + - git+https://github.com/bluesky/ophyd-async.git # switch back to pip once a new release is available - apstools == 1.6.20 # Leave at 1.6.20 until this is fixed: https://github.com/BCDA-APS/apstools/issues/1022 - pcdsdevices # For extra signal types - pydm >=1.18.0 diff --git a/pyproject.toml b/pyproject.toml index 25469391..61670b97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ "Topic :: System :: Hardware", ] keywords = ["synchrotron", "xray", "bluesky"] -dependencies = ["aioca", "aiokafka", "bluesky", "ophyd", "ophyd-async>=0.8.0a3", "databroker", "apsbss", "xraydb", +dependencies = ["aioca", "aiokafka", "bluesky", "ophyd", "ophyd-async>0.8.0a4", "databroker", "apsbss", "xraydb", "mergedeep", "xrayutilities", "bluesky-queueserver-api", "tomlkit", "apstools", "databroker", "ophyd-registry", "caproto", "pcdsdevices", "strenum", "bluesky-adaptive", "tiled[client]"] diff --git a/src/haven/devices/transform.py b/src/haven/devices/transform.py index 13530162..8bb0ad95 100644 --- a/src/haven/devices/transform.py +++ b/src/haven/devices/transform.py @@ -118,12 +118,8 @@ def __init__(self, prefix, name=""): int, f"{prefix}.MAP", name="input_bitmap" ) with self.add_children_as_readables(): - self.channels = DeviceVector( - { - char: TransformRecordChannel(prefix=prefix, letter=char) - for char in CHANNEL_LETTERS_LIST - } - ) + for letter in CHANNEL_LETTERS_LIST: + setattr(self, letter, TransformRecordChannel(prefix=prefix, letter=letter)) super().__init__(prefix=prefix, name=name) # Remove dtype, it's broken for some reason @@ -131,7 +127,7 @@ def __init__(self, prefix, name=""): async def reset(self): """set all fields to default values""" - channels = self.channels.values() + channels = [getattr(self, letter) for letter in CHANNEL_LETTERS_LIST] await asyncio.gather( self.scanning_rate.set(self.ScanInterval.PASSIVE), self.description.set(self.name), diff --git a/src/haven/tests/test_signal.py b/src/haven/tests/test_signal.py index 85ed030d..2d967f20 100644 --- a/src/haven/tests/test_signal.py +++ b/src/haven/tests/test_signal.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pytest -from ophyd_async.core import Device, get_mock_put +from ophyd_async.core import Device, get_mock_put, DeviceVector from ophyd_async.core._signal import soft_signal_rw from ophyd_async.epics.core import epics_signal_x, epics_signal_rw @@ -172,3 +172,24 @@ async def test_signal_x_trigger(device): mocked_put = get_mock_put(signal) await derived.trigger() mocked_put.assert_called_once_with(None, wait=True) + + +async def test_device_vector_parent(): + class MyDevice(Device): + def __init__(self, name): + self.my_signal = soft_signal_rw(float) + self.channels = DeviceVector({ + 0: soft_signal_rw(float) + }) + super().__init__(name=name) + + my_device = MyDevice(name="my_device") + await my_device.connect(mock=True) + # Good tests + assert my_device.my_signal.name == "my_device-my_signal" + assert my_device.my_signal.parent is my_device + assert my_device.channels.name == "my_device-channels" + assert my_device.channels.parent is my_device + assert my_device.channels[0].name == "my_device-channels-0" + # Bad tests + assert my_device.channels[0].parent is my_device.channels From 678ab971eab88d5b2b4ed9d986875401064c3a20 Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Wed, 13 Nov 2024 07:43:27 -0600 Subject: [PATCH 05/10] Updated DeviceVectors to match ophyd-async==0.8.0a5. --- src/haven/devices/delay.py | 56 +++++++------- src/haven/devices/detectors/aravis.py | 1 + src/haven/devices/ion_chamber.py | 6 +- src/haven/devices/labjack.py | 31 ++++---- src/haven/devices/monochromator.py | 1 - src/haven/devices/motor.py | 13 +++- src/haven/devices/scaler.py | 10 ++- src/haven/devices/signal.py | 2 - src/haven/devices/srs570.py | 6 +- src/haven/devices/synApps.py | 8 +- src/haven/devices/transform.py | 9 ++- src/haven/devices/xray_source.py | 4 +- src/haven/positioner.py | 7 +- src/haven/tests/test_delay.py | 93 +++++++++++++---------- src/haven/tests/test_energy_positioner.py | 10 +-- src/haven/tests/test_instrument.py | 4 +- src/haven/tests/test_ion_chamber.py | 5 +- src/haven/tests/test_labjack.py | 13 ++-- src/haven/tests/test_mirrors.py | 8 +- src/haven/tests/test_signal.py | 8 +- src/haven/tests/test_srs570.py | 5 +- src/haven/typing.py | 4 - 22 files changed, 170 insertions(+), 134 deletions(-) diff --git a/src/haven/devices/delay.py b/src/haven/devices/delay.py index 8b932123..af454a0f 100644 --- a/src/haven/devices/delay.py +++ b/src/haven/devices/delay.py @@ -2,12 +2,11 @@ from typing import Type from ophyd_async.core import ( - DeviceVector, SignalRW, StandardReadable, StandardReadableFormat, - SubsetEnum, StrictEnum, + SubsetEnum, T, ) from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x @@ -108,7 +107,7 @@ class TriggerInhibit(SubsetEnum): class BurstConfig(SubsetEnum): ALL_CYCLES = "All Cycles" FIRST_CYCLE = "1st Cycle" - + def __init__(self, prefix: str, name: str = ""): # Conventional signals with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): @@ -140,43 +139,44 @@ def __init__(self, prefix: str, name: str = ""): self.gateway = epics_signal_io(str, f"{prefix}IfaceGatewayS") # Individual delay channels with self.add_children_as_readables(): - self.channels = DeviceVector( - { - "A": DG645Channel(f"{prefix}A"), - "B": DG645Channel(f"{prefix}B"), - "C": DG645Channel(f"{prefix}C"), - "D": DG645Channel(f"{prefix}D"), - "E": DG645Channel(f"{prefix}E"), - "F": DG645Channel(f"{prefix}F"), - "G": DG645Channel(f"{prefix}G"), - "H": DG645Channel(f"{prefix}H"), - } - ) + self.channel_A = DG645Channel(f"{prefix}A") + self.channel_B = DG645Channel(f"{prefix}B") + self.channel_C = DG645Channel(f"{prefix}C") + self.channel_D = DG645Channel(f"{prefix}D") + self.channel_E = DG645Channel(f"{prefix}E") + self.channel_F = DG645Channel(f"{prefix}F") + self.channel_G = DG645Channel(f"{prefix}G") + self.channel_H = DG645Channel(f"{prefix}H") # 2-channel delay outputs with self.add_children_as_readables(): - self.outputs = DeviceVector( - { - "T0": DG645Output(f"{prefix}T0"), - "AB": DG645DelayOutput(f"{prefix}AB"), - "CD": DG645DelayOutput(f"{prefix}CD"), - "EF": DG645DelayOutput(f"{prefix}EF"), - "GH": DG645DelayOutput(f"{prefix}GH"), - } - ) + self.output_T0 = DG645Output(f"{prefix}T0") + self.output_AB = DG645DelayOutput(f"{prefix}AB") + self.output_CD = DG645DelayOutput(f"{prefix}CD") + self.output_EF = DG645DelayOutput(f"{prefix}EF") + self.output_GH = DG645DelayOutput(f"{prefix}GH") # Trigger control with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): - self.trigger_source = epics_signal_io(self.TriggerSource,f"{prefix}TriggerSourceM",) - self.trigger_inhibit = epics_signal_io(self.TriggerInhibit, f"{prefix}TriggerInhibitM") + self.trigger_source = epics_signal_io( + self.TriggerSource, + f"{prefix}TriggerSourceM", + ) + self.trigger_inhibit = epics_signal_io( + self.TriggerInhibit, f"{prefix}TriggerInhibitM" + ) self.trigger_level = epics_signal_io(float, f"{prefix}TriggerLevelA") self.trigger_rate = epics_signal_io(float, f"{prefix}TriggerRateA") - self.trigger_advanced_mode = epics_signal_io(bool, f"{prefix}TriggerAdvancedModeB") + self.trigger_advanced_mode = epics_signal_io( + bool, f"{prefix}TriggerAdvancedModeB" + ) self.trigger_holdoff = epics_signal_io(float, f"{prefix}TriggerHoldoffA") self.trigger_prescale = epics_signal_io(int, f"{prefix}TriggerPrescaleL") # Burst settings with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.burst_mode = epics_signal_io(bool, f"{prefix}BurstModeB") self.burst_count = epics_signal_io(int, f"{prefix}BurstCountL") - self.burst_config = epics_signal_io(self.BurstConfig, f"{prefix}BurstConfigB" ) + self.burst_config = epics_signal_io( + self.BurstConfig, f"{prefix}BurstConfigB" + ) self.burst_delay = epics_signal_io(float, f"{prefix}BurstDelayA") self.burst_period = epics_signal_io(float, f"{prefix}BurstPeriodA") super().__init__(name=name) diff --git a/src/haven/devices/detectors/aravis.py b/src/haven/devices/detectors/aravis.py index 21f3e6e3..e865e444 100644 --- a/src/haven/devices/detectors/aravis.py +++ b/src/haven/devices/detectors/aravis.py @@ -4,6 +4,7 @@ from .area_detectors import HavenDetector + class AravisTriggerSource(SubsetEnum): SOFTWARE = "Software" LINE1 = "Line1" diff --git a/src/haven/devices/ion_chamber.py b/src/haven/devices/ion_chamber.py index 434bfa05..7b28c8ba 100644 --- a/src/haven/devices/ion_chamber.py +++ b/src/haven/devices/ion_chamber.py @@ -216,8 +216,10 @@ def _counts_to_amps( return float("nan") def __repr__(self): - return (f"<{type(self).__name__}: '{self.name}' " - f"({self.scaler_channel.raw_count.source})>") + return ( + f"<{type(self).__name__}: '{self.name}' " + f"({self.scaler_channel.raw_count.source})>" + ) @property def scaler_channel(self): diff --git a/src/haven/devices/labjack.py b/src/haven/devices/labjack.py index cd5773bd..ca9e42be 100644 --- a/src/haven/devices/labjack.py +++ b/src/haven/devices/labjack.py @@ -45,13 +45,12 @@ def __init__(self, prefix: str, name: str = ""): DeviceVector, StandardReadable, StandardReadableFormat, - SubsetEnum, StrictEnum, + SubsetEnum, observe_value, ) from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x -from ..typing import StrEnum from .synApps import EpicsRecordInputFields, EpicsRecordOutputFields __all__ = [ @@ -70,6 +69,7 @@ def __init__(self, prefix: str, name: str = ""): KIND_CONFIG_OR_NORMAL = 3 """Alternative for ``Kind.config | Kind.normal``.""" + class Input(EpicsRecordInputFields): """A generic input record. @@ -280,8 +280,7 @@ class Resolution(SubsetEnum): SIX = "6" SEVEN = "7" EIGHT = "8" - - + def __init__(self, prefix: str, name: str = "", waveforms=[]): with self.add_children_as_readables(): self.timebase_waveform = epics_signal_rw( @@ -292,9 +291,15 @@ def __init__(self, prefix: str, name: str = "", waveforms=[]): with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.num_points = epics_signal_rw(int, f"{prefix}WaveDigNumPoints") self.dwell_time = epics_signal_rw(float, f"{prefix}WaveDigDwell") - self.first_chan = epics_signal_rw(self.FirstChannel, f"{prefix}WaveDigFirstChan") - self.num_chans = epics_signal_rw(self.NumberOfChannels, f"{prefix}WaveDigNumChans") - self.resolution = epics_signal_rw(self.Resolution, f"{prefix}WaveDigResolution") + self.first_chan = epics_signal_rw( + self.FirstChannel, f"{prefix}WaveDigFirstChan" + ) + self.num_chans = epics_signal_rw( + self.NumberOfChannels, f"{prefix}WaveDigNumChans" + ) + self.resolution = epics_signal_rw( + self.Resolution, f"{prefix}WaveDigResolution" + ) self.settling_time = epics_signal_rw(float, f"{prefix}WaveDigSettlingTime") self.current_point = epics_signal_rw(int, f"{prefix}WaveDigCurrentPoint") self.ext_trigger = epics_signal_rw( @@ -303,7 +308,9 @@ def __init__(self, prefix: str, name: str = "", waveforms=[]): self.ext_clock = epics_signal_rw(self.TriggerSource, f"{prefix}WaveDigExtClock") self.auto_restart = epics_signal_x(f"{prefix}WaveDigAutoRestart") self.run = epics_signal_rw(bool, f"{prefix}WaveDigRun") - self.read_waveform = epics_signal_rw(self.ReadWaveform, f"{prefix}WaveDigReadWF") + self.read_waveform = epics_signal_rw( + self.ReadWaveform, f"{prefix}WaveDigReadWF" + ) # Add waveforms with self.add_children_as_readables(): self.waveforms = DeviceVector( @@ -518,12 +525,8 @@ def __init__( self.digital_ios = DeviceVector( {idx: DigitalIO(prefix, ch_num=idx) for idx in digital_ios} ) - self.digital_words = DeviceVector( - { - word: epics_signal_r(int, f"{prefix}{word.upper()}In") - for word in digital_words - } - ) + for word in digital_words: + setattr(self, word, epics_signal_r(int, f"{prefix}{word.upper()}In")) # Waveform devices (not read by default, should be made readable as needed) self.waveform_digitizer = WaveformDigitizer( f"{prefix}", waveforms=analog_inputs diff --git a/src/haven/devices/monochromator.py b/src/haven/devices/monochromator.py index 19c9fb8e..e59d575c 100644 --- a/src/haven/devices/monochromator.py +++ b/src/haven/devices/monochromator.py @@ -1,5 +1,4 @@ import logging -from enum import Enum from ophyd_async.core import StandardReadable, StandardReadableFormat, StrictEnum from ophyd_async.epics.core import epics_signal_rw diff --git a/src/haven/devices/motor.py b/src/haven/devices/motor.py index 0e43c08e..b2813a64 100644 --- a/src/haven/devices/motor.py +++ b/src/haven/devices/motor.py @@ -3,9 +3,14 @@ from ophyd import Component as Cpt from ophyd import EpicsMotor, EpicsSignal, EpicsSignalRO, Kind -from ophyd_async.core import DEFAULT_TIMEOUT, SubsetEnum, StrictEnum, StandardReadableFormat -from ophyd_async.epics.motor import Motor as MotorBase +from ophyd_async.core import ( + DEFAULT_TIMEOUT, + StandardReadableFormat, + StrictEnum, + SubsetEnum, +) from ophyd_async.epics.core import epics_signal_r, epics_signal_rw +from ophyd_async.epics.motor import Motor as MotorBase from ophydregistry import Registry from .motor_flyer import MotorFlyer @@ -44,7 +49,9 @@ def __init__( self.description = epics_signal_rw(str, f"{prefix}.DESC") self.user_offset = epics_signal_rw(float, f"{prefix}.OFF") self.user_offset_dir = epics_signal_rw(self.Direction, f"{prefix}.DIR") - self.offset_freeze_switch = epics_signal_rw(self.FreezeSwitch, f"{prefix}.FOFF") + self.offset_freeze_switch = epics_signal_rw( + self.FreezeSwitch, f"{prefix}.FOFF" + ) # Motor status signals self.motor_is_moving = epics_signal_r(int, f"{prefix}.MOVN") self.motor_done_move = epics_signal_r(int, f"{prefix}.DMOV") diff --git a/src/haven/devices/scaler.py b/src/haven/devices/scaler.py index 8c0050e8..5fd0aa68 100644 --- a/src/haven/devices/scaler.py +++ b/src/haven/devices/scaler.py @@ -1,10 +1,14 @@ import numpy as np from numpy.typing import NDArray -from ophyd_async.core import DeviceVector, StandardReadable, StandardReadableFormat, SubsetEnum, StrictEnum +from ophyd_async.core import ( + DeviceVector, + StandardReadable, + StandardReadableFormat, + StrictEnum, + SubsetEnum, +) from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x -from ..typing import StrEnum - def num_to_char(num): char = chr(65 + num) diff --git a/src/haven/devices/signal.py b/src/haven/devices/signal.py index 3f3d517b..45e5861c 100644 --- a/src/haven/devices/signal.py +++ b/src/haven/devices/signal.py @@ -12,10 +12,8 @@ AsyncStatus, CalculatableTimeout, Callback, - DeviceConnector, SignalBackend, SignalDatatypeT, - SignalMetadata, SignalR, SignalRW, SignalX, diff --git a/src/haven/devices/srs570.py b/src/haven/devices/srs570.py index 981e5540..2490525b 100644 --- a/src/haven/devices/srs570.py +++ b/src/haven/devices/srs570.py @@ -16,7 +16,6 @@ import logging import math from collections import OrderedDict -from enum import Enum from typing import Optional, Type from ophyd_async.core import ( @@ -25,7 +24,6 @@ CalculatableTimeout, Device, SignalRW, - SubsetEnum, StrictEnum, T, ) @@ -351,7 +349,9 @@ def __init__(self, prefix: str, name: str = ""): ) self.filter_reset = epics_signal_x(f"{prefix}filter_reset.PROC") self.filter_lowpass = epics_signal_rw(self.FilterLowPass, f"{prefix}low_freq") - self.filter_highpass = epics_signal_rw(self.FilterHighPass, f"{prefix}high_freq") + self.filter_highpass = epics_signal_rw( + self.FilterHighPass, f"{prefix}high_freq" + ) self.gain_mode = gain_signal(self.GainMode, f"{prefix}gain_mode") self.invert = epics_signal_rw(bool, f"{prefix}invert_on") self.blank = epics_signal_rw(bool, f"{prefix}blank_on") diff --git a/src/haven/devices/synApps.py b/src/haven/devices/synApps.py index 6aa95814..e9bd9227 100644 --- a/src/haven/devices/synApps.py +++ b/src/haven/devices/synApps.py @@ -1,6 +1,12 @@ import asyncio -from ophyd_async.core import Device, StandardReadable, StandardReadableFormat, SubsetEnum, StrictEnum +from ophyd_async.core import ( + Device, + StandardReadable, + StandardReadableFormat, + StrictEnum, + SubsetEnum, +) from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x diff --git a/src/haven/devices/transform.py b/src/haven/devices/transform.py index 8bb0ad95..0e36c53e 100644 --- a/src/haven/devices/transform.py +++ b/src/haven/devices/transform.py @@ -5,11 +5,10 @@ # from ophyd import Device from ophyd_async.core import ( Device, - DeviceVector, StandardReadable, StandardReadableFormat, - SubsetEnum, StrictEnum, + SubsetEnum, ) from ophyd_async.epics.core import epics_signal_r, epics_signal_rw @@ -119,7 +118,11 @@ def __init__(self, prefix, name=""): ) with self.add_children_as_readables(): for letter in CHANNEL_LETTERS_LIST: - setattr(self, letter, TransformRecordChannel(prefix=prefix, letter=letter)) + setattr( + self, + f"channel_{letter}", + TransformRecordChannel(prefix=prefix, letter=letter), + ) super().__init__(prefix=prefix, name=name) # Remove dtype, it's broken for some reason diff --git a/src/haven/devices/xray_source.py b/src/haven/devices/xray_source.py index 6b293621..5f292dd9 100644 --- a/src/haven/devices/xray_source.py +++ b/src/haven/devices/xray_source.py @@ -1,12 +1,12 @@ import logging -from enum import Enum, IntEnum +from enum import IntEnum from ophyd_async.core import ( Signal, StandardReadable, StandardReadableFormat, - soft_signal_rw, SubsetEnum, + soft_signal_rw, ) from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_x diff --git a/src/haven/positioner.py b/src/haven/positioner.py index fcaf416c..834fdd6d 100644 --- a/src/haven/positioner.py +++ b/src/haven/positioner.py @@ -74,7 +74,9 @@ def watch_done( done_event.set() @WatchableAsyncStatus.wrap - async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT): + async def set( + self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEOUT, wait=True + ): new_position = value self._set_success = True old_position, current_position, units, precision, velocity = ( @@ -127,6 +129,9 @@ async def set(self, value: float, timeout: CalculatableTimeout = CALCULATE_TIMEO # Monitor based on readback position aws = asyncio.gather(reached_setpoint.wait(), set_status) done_status = AsyncStatus(asyncio.wait_for(aws, timeout)) + # If we don't care to wait for the return value, we can end + if not wait: + return # Monitor the position of the readback value async for current_position in observe_value( self.readback, done_status=done_status diff --git a/src/haven/tests/test_delay.py b/src/haven/tests/test_delay.py index 36665c6e..baa21e9d 100644 --- a/src/haven/tests/test_delay.py +++ b/src/haven/tests/test_delay.py @@ -20,47 +20,47 @@ async def test_dg645_device(): "delay-burst_delay", "delay-burst_mode", "delay-burst_period", - "delay-channels-A-reference", - "delay-channels-A-delay", - "delay-channels-B-reference", - "delay-channels-B-delay", - "delay-channels-C-reference", - "delay-channels-C-delay", - "delay-channels-D-reference", - "delay-channels-D-delay", - "delay-channels-E-reference", - "delay-channels-E-delay", - "delay-channels-F-reference", - "delay-channels-F-delay", - "delay-channels-G-reference", - "delay-channels-G-delay", - "delay-channels-H-reference", - "delay-channels-H-delay", + "delay-channel_A-reference", + "delay-channel_A-delay", + "delay-channel_B-reference", + "delay-channel_B-delay", + "delay-channel_C-reference", + "delay-channel_C-delay", + "delay-channel_D-reference", + "delay-channel_D-delay", + "delay-channel_E-reference", + "delay-channel_E-delay", + "delay-channel_F-reference", + "delay-channel_F-delay", + "delay-channel_G-reference", + "delay-channel_G-delay", + "delay-channel_H-reference", + "delay-channel_H-delay", "delay-device_id", "delay-label", - "delay-outputs-AB-amplitude", - "delay-outputs-AB-offset", - "delay-outputs-AB-polarity", - "delay-outputs-AB-trigger_phase", - "delay-outputs-AB-trigger_prescale", - "delay-outputs-CD-amplitude", - "delay-outputs-CD-offset", - "delay-outputs-CD-polarity", - "delay-outputs-CD-trigger_phase", - "delay-outputs-CD-trigger_prescale", - "delay-outputs-EF-amplitude", - "delay-outputs-EF-offset", - "delay-outputs-EF-polarity", - "delay-outputs-EF-trigger_phase", - "delay-outputs-EF-trigger_prescale", - "delay-outputs-GH-amplitude", - "delay-outputs-GH-offset", - "delay-outputs-GH-polarity", - "delay-outputs-GH-trigger_phase", - "delay-outputs-GH-trigger_prescale", - "delay-outputs-T0-amplitude", - "delay-outputs-T0-offset", - "delay-outputs-T0-polarity", + "delay-output_AB-amplitude", + "delay-output_AB-offset", + "delay-output_AB-polarity", + "delay-output_AB-trigger_phase", + "delay-output_AB-trigger_prescale", + "delay-output_CD-amplitude", + "delay-output_CD-offset", + "delay-output_CD-polarity", + "delay-output_CD-trigger_phase", + "delay-output_CD-trigger_prescale", + "delay-output_EF-amplitude", + "delay-output_EF-offset", + "delay-output_EF-polarity", + "delay-output_EF-trigger_phase", + "delay-output_EF-trigger_prescale", + "delay-output_GH-amplitude", + "delay-output_GH-offset", + "delay-output_GH-polarity", + "delay-output_GH-trigger_phase", + "delay-output_GH-trigger_prescale", + "delay-output_T0-amplitude", + "delay-output_T0-offset", + "delay-output_T0-polarity", "delay-trigger_advanced_mode", "delay-trigger_holdoff", "delay-trigger_inhibit", @@ -81,7 +81,14 @@ async def test_dg645_device(): "delay-burst_delay", "delay-burst_mode", "delay-burst_period", - "delay-channels", + "delay-channel_A", + "delay-channel_B", + "delay-channel_C", + "delay-channel_D", + "delay-channel_E", + "delay-channel_F", + "delay-channel_G", + "delay-channel_H", "delay-clear_error", "delay-device_id", "delay-dhcp_state", @@ -95,7 +102,11 @@ async def test_dg645_device(): "delay-lan_state", "delay-mac_address", "delay-network_mask", - "delay-outputs", + "delay-output_AB", + "delay-output_CD", + "delay-output_EF", + "delay-output_GH", + "delay-output_T0", "delay-reset", "delay-reset_gpib", "delay-reset_lan", diff --git a/src/haven/tests/test_energy_positioner.py b/src/haven/tests/test_energy_positioner.py index e44727f2..fd5c014b 100644 --- a/src/haven/tests/test_energy_positioner.py +++ b/src/haven/tests/test_energy_positioner.py @@ -4,7 +4,6 @@ from ophyd_async.core import set_mock_value from haven.devices.energy_positioner import EnergyPositioner -from haven.devices.xray_source import BusyStatus @pytest.fixture() @@ -15,6 +14,8 @@ async def positioner(): undulator_prefix="S255ID:", ) await positioner.connect(mock=True) + await positioner.setpoint.connect(mock=False) + await positioner.readback.connect(mock=False) return positioner @@ -22,12 +23,9 @@ async def test_set_energy(positioner): # Set up dependent values set_mock_value(positioner.monochromator.id_offset, 150) # Change the energy - status = positioner.set(10000, timeout=3) + await positioner.set(10000, timeout=3, wait=False) # Trick the Undulator into being done - set_mock_value(positioner.undulator.energy.done, BusyStatus.BUSY) - await asyncio.sleep(0.01) # Let the event loop run - set_mock_value(positioner.undulator.energy.done, BusyStatus.DONE) - await status + await asyncio.sleep(0.05) # Let the event loop run # Check that all the sub-components were set properly assert await positioner.monochromator.energy.user_setpoint.get_value() == 10000 assert await positioner.undulator.energy.setpoint.get_value() == 10.150 diff --git a/src/haven/tests/test_instrument.py b/src/haven/tests/test_instrument.py index 93036bfc..cce8bb23 100644 --- a/src/haven/tests/test_instrument.py +++ b/src/haven/tests/test_instrument.py @@ -84,11 +84,13 @@ async def test_connect(instrument): # Are devices disconnected to start with? assert all([d._connect_task is None for d in async_devices]) assert all([not d.connected is None for d in sync_devices]) + for device in async_devices: + device._connector.connect_mock = AsyncMock() # Connect the device await instrument.connect(mock=True) # Are devices connected afterwards? # NB: This doesn't actually test the code for threaded devices - assert all([d._connect_task.done for d in async_devices]) + assert all([d._connector.connect_mock.called for d in async_devices]) async def test_load(monkeypatch): diff --git a/src/haven/tests/test_ion_chamber.py b/src/haven/tests/test_ion_chamber.py index fbf75f52..6fcd9d8f 100644 --- a/src/haven/tests/test_ion_chamber.py +++ b/src/haven/tests/test_ion_chamber.py @@ -197,8 +197,11 @@ async def test_voltmeter_name(ion_chamber): await ion_chamber.connect(mock=True) assert (await ion_chamber.voltmeter_channel.description.get_value()) != "Icake" # Change the ion chamber name, and see if the voltmeter name updates - set_mock_value(ion_chamber.scaler_channel.description, "Icake") + # set_mock_value(ion_chamber.scaler_channel.description, "Icake") + ion_chamber.scaler_channel.description.get_value = AsyncMock(return_value="Icake") + assert (await ion_chamber.scaler_channel.description.get_value()) == "Icake" await ion_chamber.connect(mock=True) + assert (await ion_chamber.scaler_channel.description.get_value()) == "Icake" assert (await ion_chamber.voltmeter_channel.description.get_value()) == "Icake" diff --git a/src/haven/tests/test_labjack.py b/src/haven/tests/test_labjack.py index c4a1847a..4202bf2d 100644 --- a/src/haven/tests/test_labjack.py +++ b/src/haven/tests/test_labjack.py @@ -202,18 +202,17 @@ async def test_digital_words(LabJackDevice, num_dios): """Test analog inputs for different device types.""" device = LabJackDevice(PV_PREFIX, name="labjack_T") await device.connect(mock=True) - assert hasattr(device, "digital_words") # Check that the individual digital word outputs were created - assert "dio" in device.digital_words.keys() - assert "fio" in device.digital_words.keys() - assert "eio" in device.digital_words.keys() - assert "cio" in device.digital_words.keys() - assert "mio" in device.digital_words.keys() + assert hasattr(device, "dio") + assert hasattr(device, "fio") + assert hasattr(device, "eio") + assert hasattr(device, "cio") + assert hasattr(device, "mio") # Check read attrs read_attrs = ["dio", "eio", "fio", "mio", "cio"] description = await device.describe() for attr in read_attrs: - assert f"{device.name}-digital_words-{attr}" in description.keys() + assert f"{device.name}-{attr}" in description.keys() async def test_waveform_digitizer(): diff --git a/src/haven/tests/test_mirrors.py b/src/haven/tests/test_mirrors.py index 1ad38436..c984d31b 100644 --- a/src/haven/tests/test_mirrors.py +++ b/src/haven/tests/test_mirrors.py @@ -14,11 +14,11 @@ async def test_high_heat_load_mirror_PVs(): assert mirror.bender.user_setpoint.source == "mock+ca://255ida:ORM2:m5.VAL" # Check the transform PVs assert ( - mirror.drive_transform.channels["B"].input_pv.source + mirror.drive_transform.channel_B.input_pv.source == "mock+ca://255ida:ORM2:lats:Drive.INPB" ) assert ( - mirror.readback_transform.channels["B"].input_pv.source + mirror.readback_transform.channel_B.input_pv.source == "mock+ca://255ida:ORM2:lats:Readback.INPB" ) @@ -51,10 +51,10 @@ async def test_kb_mirrors_PVs(): assert kb.vert.downstream.user_setpoint.source == "mock+ca://255idcVME:m36.VAL" # Check the transforms assert ( - kb.horiz.drive_transform.channels["B"].input_pv.source + kb.horiz.drive_transform.channel_B.input_pv.source == "mock+ca://255idcVME:LongKB_CdnH:Drive.INPB" ) assert ( - kb.horiz.readback_transform.channels["B"].input_pv.source + kb.horiz.readback_transform.channel_B.input_pv.source == "mock+ca://255idcVME:LongKB_CdnH:Readback.INPB" ) diff --git a/src/haven/tests/test_signal.py b/src/haven/tests/test_signal.py index 2d967f20..b62f1bdf 100644 --- a/src/haven/tests/test_signal.py +++ b/src/haven/tests/test_signal.py @@ -3,9 +3,9 @@ from unittest.mock import MagicMock import pytest -from ophyd_async.core import Device, get_mock_put, DeviceVector +from ophyd_async.core import Device, DeviceVector, get_mock_put from ophyd_async.core._signal import soft_signal_rw -from ophyd_async.epics.core import epics_signal_x, epics_signal_rw +from ophyd_async.epics.core import epics_signal_rw, epics_signal_x from haven.devices.signal import derived_signal_rw, derived_signal_x @@ -178,9 +178,7 @@ async def test_device_vector_parent(): class MyDevice(Device): def __init__(self, name): self.my_signal = soft_signal_rw(float) - self.channels = DeviceVector({ - 0: soft_signal_rw(float) - }) + self.channels = DeviceVector({0: soft_signal_rw(float)}) super().__init__(name=name) my_device = MyDevice(name="my_device") diff --git a/src/haven/tests/test_srs570.py b/src/haven/tests/test_srs570.py index 51e9156e..7903806e 100644 --- a/src/haven/tests/test_srs570.py +++ b/src/haven/tests/test_srs570.py @@ -2,7 +2,7 @@ from unittest import mock import pytest -from ophyd_async.core import DEFAULT_TIMEOUT, get_mock_put +from ophyd_async.core import get_mock_put from haven.devices.srs570 import GainSignal, SRS570PreAmplifier @@ -146,7 +146,8 @@ async def test_preamp_gain_settling(gain_value, gain_unit, gain_mode, mocker, pr await preamp.sensitivity_value.set(gain_value) # Check that the signal's ``set`` was called with correct arguments get_mock_put(preamp.sensitivity_value).assert_called_once_with( - gain_value, wait=True, + gain_value, + wait=True, ) # Check that the settle time was included sleep_mock.assert_called_once_with(settle_time) diff --git a/src/haven/typing.py b/src/haven/typing.py index 6ff8166d..a5a37698 100644 --- a/src/haven/typing.py +++ b/src/haven/typing.py @@ -1,4 +1,3 @@ -from enum import Enum from typing import Sequence, Union from ophyd import Component, Device, Signal @@ -12,9 +11,6 @@ Motor = Union[Device, Component, Signal, str] -class StrEnum(str, Enum): ... - - # ----------------------------------------------------------------------------- # :author: Mark Wolfman # :email: wolfman@anl.gov From a1eeb6606b0ca6ac833d4b807129405b29e473bb Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Wed, 13 Nov 2024 09:50:34 -0600 Subject: [PATCH 06/10] Updated ophyd-async version requirement in pyproject. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 61670b97..25469391 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ "Topic :: System :: Hardware", ] keywords = ["synchrotron", "xray", "bluesky"] -dependencies = ["aioca", "aiokafka", "bluesky", "ophyd", "ophyd-async>0.8.0a4", "databroker", "apsbss", "xraydb", +dependencies = ["aioca", "aiokafka", "bluesky", "ophyd", "ophyd-async>=0.8.0a3", "databroker", "apsbss", "xraydb", "mergedeep", "xrayutilities", "bluesky-queueserver-api", "tomlkit", "apstools", "databroker", "ophyd-registry", "caproto", "pcdsdevices", "strenum", "bluesky-adaptive", "tiled[client]"] From 1aac5e3f3d9bfbf768245e97c0723243e202fd0f Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Wed, 13 Nov 2024 10:15:47 -0600 Subject: [PATCH 07/10] Fixed typing on ophyd-async signals with array types. --- src/haven/devices/labjack.py | 20 ++++++++++---------- src/haven/devices/scaler.py | 6 +++--- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/haven/devices/labjack.py b/src/haven/devices/labjack.py index ca9e42be..9fbfd6a0 100644 --- a/src/haven/devices/labjack.py +++ b/src/haven/devices/labjack.py @@ -38,9 +38,9 @@ def __init__(self, prefix: str, name: str = ""): import numpy as np from bluesky.protocols import Triggerable -from numpy.typing import NDArray from ophyd_async.core import ( DEFAULT_TIMEOUT, + Array1D, AsyncStatus, DeviceVector, StandardReadable, @@ -284,7 +284,7 @@ class Resolution(SubsetEnum): def __init__(self, prefix: str, name: str = "", waveforms=[]): with self.add_children_as_readables(): self.timebase_waveform = epics_signal_rw( - NDArray[np.float64], f"{prefix}WaveDigTimeWF" + Array1D[np.float64], f"{prefix}WaveDigTimeWF" ) self.dwell_actual = epics_signal_rw(float, f"{prefix}WaveDigDwellActual") self.total_time = epics_signal_rw(float, f"{prefix}WaveDigTotalTime") @@ -316,7 +316,7 @@ def __init__(self, prefix: str, name: str = "", waveforms=[]): self.waveforms = DeviceVector( { idx: epics_signal_r( - NDArray[np.float64], f"{prefix}WaveDigVoltWF{idx}" + Array1D[np.float64], f"{prefix}WaveDigVoltWF{idx}" ) for idx in waveforms } @@ -383,7 +383,7 @@ def __init__(self, prefix: str, name: str = ""): # Settings for user-defined waveforms with self.add_children_as_readables(): self.user_time_waveform = epics_signal_rw( - NDArray[np.float64], f"{prefix}WaveGenUserTimeWF" + Array1D[np.float64], f"{prefix}WaveGenUserTimeWF" ) self.user_num_points = epics_signal_rw(int, f"{prefix}WaveGenUserNumPoints") self.user_dwell = epics_signal_rw(float, f"{prefix}WaveGenUserDwell") @@ -392,7 +392,7 @@ def __init__(self, prefix: str, name: str = ""): # Settings for internal waveforms with self.add_children_as_readables(): self.internal_time_waveform = epics_signal_rw( - NDArray[np.float64], f"{prefix}WaveGenIntTimeWF" + Array1D[np.float64], f"{prefix}WaveGenIntTimeWF" ) self.internal_num_points = epics_signal_rw(int, f"{prefix}WaveGenIntNumPoints") self.internal_dwell = epics_signal_rw(float, f"{prefix}WaveGenIntDwell") @@ -401,7 +401,7 @@ def __init__(self, prefix: str, name: str = ""): # Waveform specific settings with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.user_waveform_0 = epics_signal_rw( - NDArray[np.float64], f"{prefix}WaveGenUserWF0" + Array1D[np.float64], f"{prefix}WaveGenUserWF0" ) self.enable_0 = epics_signal_rw(bool, f"{prefix}WaveGenEnable0") self.type_0 = epics_signal_rw(self.WaveType, f"{prefix}WaveGenType0") @@ -409,7 +409,7 @@ def __init__(self, prefix: str, name: str = ""): self.amplitude_0 = epics_signal_rw(float, f"{prefix}WaveGenAmplitude0") self.offset_0 = epics_signal_rw(float, f"{prefix}WaveGenOffset0") self.user_waveform_1 = epics_signal_rw( - NDArray[np.float64], f"{prefix}WaveGenUserWF1" + Array1D[np.float64], f"{prefix}WaveGenUserWF1" ) self.enable_1 = epics_signal_rw(bool, f"{prefix}WaveGenEnable1") self.type_1 = epics_signal_rw(self.WaveType, f"{prefix}WaveGenType1") @@ -417,10 +417,10 @@ def __init__(self, prefix: str, name: str = ""): self.amplitude_1 = epics_signal_rw(float, f"{prefix}WaveGenAmplitude1") self.offset_1 = epics_signal_rw(float, f"{prefix}WaveGenOffset1") self.internal_waveform_0 = epics_signal_rw( - NDArray[np.float64], f"{prefix}WaveGenInternalWF0" + Array1D[np.float64], f"{prefix}WaveGenInternalWF0" ) self.internal_waveform_1 = epics_signal_r( - NDArray[np.float64], f"{prefix}WaveGenInternalWF1" + Array1D[np.float64], f"{prefix}WaveGenInternalWF1" ) super().__init__(name=name) @@ -495,7 +495,7 @@ def __init__( self.ljm_version = epics_signal_r(str, f"{prefix}LJMVersion") self.driver_version = epics_signal_r(str, f"{prefix}DriverVersion") self.last_error_message = epics_signal_r( - NDArray[np.uint8], f"{prefix}LastErrorMessage" + Array1D[np.uint8], f"{prefix}LastErrorMessage" ) self.poll_sleep_ms = epics_signal_rw(float, f"{prefix}PollSleepMS") self.analog_in_settling_time_all = epics_signal_rw( diff --git a/src/haven/devices/scaler.py b/src/haven/devices/scaler.py index 5fd0aa68..cd758828 100644 --- a/src/haven/devices/scaler.py +++ b/src/haven/devices/scaler.py @@ -1,6 +1,6 @@ import numpy as np -from numpy.typing import NDArray from ophyd_async.core import ( + Array1D, DeviceVector, StandardReadable, StandardReadableFormat, @@ -48,8 +48,8 @@ class MCAMode(SubsetEnum): def __init__(self, prefix, name=""): # Signals with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): - self.spectrum = epics_signal_r(NDArray[np.int32], f"{prefix}.VAL") - self.background = epics_signal_r(NDArray[np.int32], f"{prefix}.BG") + self.spectrum = epics_signal_r(Array1D[np.int32], f"{prefix}.VAL") + self.background = epics_signal_r(Array1D[np.int32], f"{prefix}.BG") with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.mode = epics_signal_rw(self.MCAMode, f"{prefix}.MODE") super().__init__(name=name) From 752e678d35936bf5639582d277ac34b028e29861 Mon Sep 17 00:00:00 2001 From: yannachen Date: Wed, 13 Nov 2024 11:22:04 -0600 Subject: [PATCH 08/10] Removed DTYP from common EPICS records since it's not common. Also fixed some enums and partially fixed connecting to derived signals. --- environment.yml | 2 +- src/haven/devices/signal.py | 14 ++++++++------ src/haven/devices/srs570.py | 10 +++++----- src/haven/devices/synApps.py | 19 +++++++++++++------ src/haven/devices/transform.py | 2 -- src/haven/ipython_startup.ipy | 11 +++++++++-- 6 files changed, 36 insertions(+), 22 deletions(-) diff --git a/environment.yml b/environment.yml index 1a5ccd19..d19d2842 100644 --- a/environment.yml +++ b/environment.yml @@ -59,7 +59,7 @@ dependencies: - bluesky >=1.8.1 - ophyd >=1.6.3 # - ophyd-async > 0.8.0a4 - - git+https://github.com/bluesky/ophyd-async.git # switch back to pip once a new release is available + - git+https://github.com/bluesky/ophyd-async.git # switch back to pip once a new release (0.8.0a5) is available - apstools == 1.6.20 # Leave at 1.6.20 until this is fixed: https://github.com/BCDA-APS/apstools/issues/1022 - pcdsdevices # For extra signal types - pydm >=1.18.0 diff --git a/src/haven/devices/signal.py b/src/haven/devices/signal.py index 45e5861c..e9b4c2be 100644 --- a/src/haven/devices/signal.py +++ b/src/haven/devices/signal.py @@ -125,8 +125,15 @@ def source(self, name: str, read: bool): async def connect(self, timeout=DEFAULT_TIMEOUT) -> None: # Connect this signal await super().connect(timeout=timeout) + # Make sure the subordinate signal are connected + sub_signals = self._derived_from.values() + tasks = [] + for sig in sub_signals: + if sig._mock is None and sig._connect_task is not None: + tasks.append(sig._connect_task) + await asyncio.wait_for(asyncio.gather(*tasks), timeout=timeout) # Listen for changes in the derived_from signals - for sig in self._derived_from.values(): + for sig in sub_signals: # Subscribe with a partial in case the signal's name changes if isinstance(sig, Subscribable): sig.subscribe(partial(self.update_readings, signal=sig)) @@ -149,7 +156,6 @@ def update_readings(self, reading, signal): Stashes them for later recall. """ - print("UPDATING") # Stash this reading self._cached_readings.update({signal: reading[signal.name]}) # Update interested parties if we have a full set of readings @@ -203,10 +209,6 @@ async def get_reading(self) -> Reading: return self.combine_readings(readings) -class DerivedSignalRW(SignalRW): - pass - - def derived_signal_rw( datatype: Optional[Type[T]], *, diff --git a/src/haven/devices/srs570.py b/src/haven/devices/srs570.py index 2490525b..a3622d58 100644 --- a/src/haven/devices/srs570.py +++ b/src/haven/devices/srs570.py @@ -257,12 +257,12 @@ class SRS570PreAmplifier(Device): offset_difference = -3 # How many levels higher should the offset be class FilterType(StrictEnum): - NO_FILTER = "No filter" - _6DB_HIGHPASS = "6 dB highpass" + NO_FILTER = " No filter" + _6DB_HIGHPASS = " 6 dB highpass" _12DB_HIGHPASS = "12 dB highpass" - _6DB_BANDPASS = "6 dB bandpass" - _6DB_LOWPASS = "6 dB lowpass" - _12DB_LOWPASS = "6 dB lowpass" + _6DB_BANDPASS = " 6 dB bandpass" + _6DB_LOWPASS = " 6 dB lowpass" + _12DB_LOWPASS = "12 dB lowpass" class FilterLowPass(StrictEnum): _0_03_HZ = " 0.03 Hz" diff --git a/src/haven/devices/synApps.py b/src/haven/devices/synApps.py index e9bd9227..86ec0b0b 100644 --- a/src/haven/devices/synApps.py +++ b/src/haven/devices/synApps.py @@ -52,11 +52,6 @@ class EpicsRecordDeviceCommonAll(StandardReadable): # More valid options are specific to the record type # Subclasses may override this attribute - class DeviceType(SubsetEnum): - SOFT_CHANNEL = "Soft Channel" - RAW_SOFT_CHANNEL = "Raw Soft Channel" - ASYNC_SOFT_CHANNEL = "Async Soft Channel" - class ScanInterval(SubsetEnum): PASSIVE = "Passive" EVENT = "Event" @@ -74,7 +69,6 @@ def __init__(self, prefix: str, name: str = ""): with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.description = epics_signal_rw(str, f"{prefix}.DESC") self.scanning_rate = epics_signal_rw(self.ScanInterval, f"{prefix}.SCAN") - self.device_type = epics_signal_r(self.DeviceType, f"{prefix}.DTYP") # Other signals, not included in read self.disable_value = epics_signal_rw(int, f"{prefix}.DISV") self.scan_disable_input_link_value = epics_signal_rw(int, f"{prefix}.DISA") @@ -112,10 +106,16 @@ class EpicsRecordInputFields(EpicsRecordDeviceCommonAll): """ Some fields common to EPICS input records. """ + class DeviceType(SubsetEnum): + SOFT_CHANNEL = "Soft Channel" + RAW_SOFT_CHANNEL = "Raw Soft Channel" + ASYNC_SOFT_CHANNEL = "Async Soft Channel" + def __init__(self, prefix: str, name: str = ""): with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.input_link = epics_signal_rw(str, f"{prefix}.INP") + self.device_type = epics_signal_r(self.DeviceType, f"{prefix}.DTYP") super().__init__(prefix=prefix, name=name) @@ -124,6 +124,12 @@ class EpicsRecordOutputFields(EpicsRecordDeviceCommonAll): Some fields common to EPICS output records. """ + class DeviceType(SubsetEnum): + SOFT_CHANNEL = "Soft Channel" + RAW_SOFT_CHANNEL = "Raw Soft Channel" + ASYNC_SOFT_CHANNEL = "Async Soft Channel" + + class ModeSelect(SubsetEnum): SUPERVISORY = "supervisory" CLOSED_LOOP = "closed_loop" @@ -133,4 +139,5 @@ def __init__(self, prefix: str, name: str = ""): self.output_link = epics_signal_rw(str, f"{prefix}.OUT") self.desired_output_location = epics_signal_rw(str, f"{prefix}.DOL") self.output_mode_select = epics_signal_rw(self.ModeSelect, f"{prefix}.OMSL") + self.device_type = epics_signal_r(self.DeviceType, f"{prefix}.DTYP") super().__init__(prefix=prefix, name=name) diff --git a/src/haven/devices/transform.py b/src/haven/devices/transform.py index 0e36c53e..be0908b5 100644 --- a/src/haven/devices/transform.py +++ b/src/haven/devices/transform.py @@ -125,8 +125,6 @@ def __init__(self, prefix, name=""): ) super().__init__(prefix=prefix, name=name) - # Remove dtype, it's broken for some reason - del self.device_type async def reset(self): """set all fields to default values""" diff --git a/src/haven/ipython_startup.ipy b/src/haven/ipython_startup.ipy index 7b0beff6..95b15271 100644 --- a/src/haven/ipython_startup.ipy +++ b/src/haven/ipython_startup.ipy @@ -13,6 +13,7 @@ from bluesky.plan_stubs import mv, mvr, rd # noqa: F401 from bluesky.run_engine import RunEngine, call_in_bluesky_event_loop # noqa: F401 from bluesky.simulators import summarize_plan # noqa: F401 from ophyd_async.core import NotConnected +from ophydregistry import ComponentNotFound import matplotlib.pyplot as plt from rich import print from rich.align import Align @@ -61,8 +62,14 @@ print(f"Connected to {num_devices} devices in {time.monotonic() - t0:.2f} second # Save references to some commonly used things in the global namespace registry = haven.beamline.registry -ion_chambers = registry.findall("ion_chambers", allow_none=True) -energy = registry['energy'] +try: + ion_chambers = registry.findall("ion_chambers", allow_none=True) +except ComponentNotFound as exc: + log.exception(exc) +try: + energy = registry['energy'] +except ComponentNotFound as exc: + log.exception(exc) # Print helpful information to the console custom_theme = Theme( From fae6a99f680270a4c492ee22422de14f6c69c7eb Mon Sep 17 00:00:00 2001 From: yannachen Date: Thu, 14 Nov 2024 13:10:00 -0600 Subject: [PATCH 09/10] Derived signals now subscribe to their dependent signals via brute force checking. --- src/haven/devices/signal.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/haven/devices/signal.py b/src/haven/devices/signal.py index e9b4c2be..0e9ac231 100644 --- a/src/haven/devices/signal.py +++ b/src/haven/devices/signal.py @@ -122,21 +122,31 @@ def source(self, name: str, read: bool): args = ",".join(self._derived_from.keys()) return f"{src}({args})" + async def _subscribe_child(self, child_signal): + """Subscribe to a child signal for updating value changes. + + If *child_signal* is not yet connected, keep retrying until + sucessful or the timeout value is reached. + + """ + handler = partial(self.update_readings, signal=child_signal) + while True: + try: + child_signal.subscribe(handler) + except NotImplementedError: + await asyncio.sleep(0.01) + else: + break + async def connect(self, timeout=DEFAULT_TIMEOUT) -> None: - # Connect this signal - await super().connect(timeout=timeout) - # Make sure the subordinate signal are connected - sub_signals = self._derived_from.values() - tasks = [] - for sig in sub_signals: - if sig._mock is None and sig._connect_task is not None: - tasks.append(sig._connect_task) - await asyncio.wait_for(asyncio.gather(*tasks), timeout=timeout) # Listen for changes in the derived_from signals - for sig in sub_signals: - # Subscribe with a partial in case the signal's name changes - if isinstance(sig, Subscribable): - sig.subscribe(partial(self.update_readings, signal=sig)) + sub_signals = self._derived_from.values() + sub_signals = (sig for sig in sub_signals if isinstance(sig, Subscribable)) + subs = ( + asyncio.wait_for(self._subscribe_child(sig), timeout=timeout) + for sig in sub_signals + ) + await asyncio.gather(super().connect(timeout=timeout), *subs) def combine_readings(self, readings): timestamp = max([rd["timestamp"] for rd in readings.values()]) From 8ca4141111fbe8fa21f59339f79561d255c644c1 Mon Sep 17 00:00:00 2001 From: Mark Wolfman Date: Thu, 14 Nov 2024 13:19:18 -0600 Subject: [PATCH 10/10] Black linting. --- src/haven/devices/synApps.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/haven/devices/synApps.py b/src/haven/devices/synApps.py index 86ec0b0b..62071cdc 100644 --- a/src/haven/devices/synApps.py +++ b/src/haven/devices/synApps.py @@ -106,12 +106,12 @@ class EpicsRecordInputFields(EpicsRecordDeviceCommonAll): """ Some fields common to EPICS input records. """ + class DeviceType(SubsetEnum): SOFT_CHANNEL = "Soft Channel" RAW_SOFT_CHANNEL = "Raw Soft Channel" ASYNC_SOFT_CHANNEL = "Async Soft Channel" - def __init__(self, prefix: str, name: str = ""): with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.input_link = epics_signal_rw(str, f"{prefix}.INP") @@ -129,7 +129,6 @@ class DeviceType(SubsetEnum): RAW_SOFT_CHANNEL = "Raw Soft Channel" ASYNC_SOFT_CHANNEL = "Async Soft Channel" - class ModeSelect(SubsetEnum): SUPERVISORY = "supervisory" CLOSED_LOOP = "closed_loop"