Skip to content

Commit

Permalink
Merge branch 'main' into 76_make_stage_synchronous_if_arming_not_star…
Browse files Browse the repository at this point in the history
…ted_rebased
  • Loading branch information
olliesilvester authored Jun 27, 2023
2 parents 126cf27 + 8af1733 commit bc45b1e
Show file tree
Hide file tree
Showing 12 changed files with 353 additions and 21 deletions.
117 changes: 117 additions & 0 deletions src/dodal/devices/attenuator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from typing import Optional

from ophyd import Component, Device, EpicsSignal, EpicsSignalRO
from ophyd.status import Status, SubscriptionStatus

from dodal.devices.detector import DetectorParams
from dodal.devices.status import await_value
from dodal.log import LOGGER


class AtteunatorFilter(Device):
actual_filter_state: EpicsSignalRO = Component(EpicsSignalRO, ":INLIM")


class Attenuator(Device):
# Sets transmission - range 0-1
def set(self, transmission) -> SubscriptionStatus:
"""Get desired states and calculated states, return a status which is complete once they are equal"""

LOGGER.info("Using current energy")
self.use_current_energy.set(1).wait()
LOGGER.info(f"Setting desired transmission to {transmission}")
self.desired_transmission.set(transmission).wait()
LOGGER.info("Sending change filter command")
self.change.set(1).wait()

status = Status(done=True, success=True)
actual_states = self.get_actual_filter_state_list()
calculated_states = self.get_calculated_filter_state_list()
for i in range(16):
status &= await_value(
actual_states[i], calculated_states[i].get(), timeout=10
)
return status

calulated_filter_state_1: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.B0")
calulated_filter_state_2: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.B1")
calulated_filter_state_3: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.B2")
calulated_filter_state_4: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.B3")
calulated_filter_state_5: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.B4")
calulated_filter_state_6: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.B5")
calulated_filter_state_7: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.B6")
calulated_filter_state_8: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.B7")
calulated_filter_state_9: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.B8")
calulated_filter_state_10: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.B9")
calulated_filter_state_11: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.BA")
calulated_filter_state_12: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.BB")
calulated_filter_state_13: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.BC")
calulated_filter_state_14: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.BD")
calulated_filter_state_15: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.BE")
calulated_filter_state_16: EpicsSignalRO = Component(EpicsSignalRO, "DEC_TO_BIN.BF")

filter_1: AtteunatorFilter = Component(AtteunatorFilter, "FILTER1")
filter_2: AtteunatorFilter = Component(AtteunatorFilter, "FILTER2")
filter_3: AtteunatorFilter = Component(AtteunatorFilter, "FILTER3")
filter_4: AtteunatorFilter = Component(AtteunatorFilter, "FILTER4")
filter_5: AtteunatorFilter = Component(AtteunatorFilter, "FILTER5")
filter_6: AtteunatorFilter = Component(AtteunatorFilter, "FILTER6")
filter_7: AtteunatorFilter = Component(AtteunatorFilter, "FILTER7")
filter_8: AtteunatorFilter = Component(AtteunatorFilter, "FILTER8")
filter_9: AtteunatorFilter = Component(AtteunatorFilter, "FILTER9")
filter_10: AtteunatorFilter = Component(AtteunatorFilter, "FILTER10")
filter_11: AtteunatorFilter = Component(AtteunatorFilter, "FILTER11")
filter_12: AtteunatorFilter = Component(AtteunatorFilter, "FILTER12")
filter_13: AtteunatorFilter = Component(AtteunatorFilter, "FILTER13")
filter_14: AtteunatorFilter = Component(AtteunatorFilter, "FILTER14")
filter_15: AtteunatorFilter = Component(AtteunatorFilter, "FILTER15")
filter_16: AtteunatorFilter = Component(AtteunatorFilter, "FILTER16")

desired_transmission: EpicsSignal = Component(EpicsSignal, "T2A:SETVAL1")
use_current_energy: EpicsSignal = Component(
EpicsSignal, "E2WL:USECURRENTENERY.PROC"
)
change: EpicsSignal = Component(EpicsSignal, "FANOUT")
actual_transmission: EpicsSignal = Component(EpicsSignal, "MATCH")

detector_params: Optional[DetectorParams] = None

def get_calculated_filter_state_list(self) -> list[EpicsSignal]:
return [
self.calulated_filter_state_1,
self.calulated_filter_state_2,
self.calulated_filter_state_3,
self.calulated_filter_state_4,
self.calulated_filter_state_5,
self.calulated_filter_state_6,
self.calulated_filter_state_7,
self.calulated_filter_state_8,
self.calulated_filter_state_9,
self.calulated_filter_state_10,
self.calulated_filter_state_11,
self.calulated_filter_state_12,
self.calulated_filter_state_13,
self.calulated_filter_state_14,
self.calulated_filter_state_15,
self.calulated_filter_state_16,
]

def get_actual_filter_state_list(self) -> list[EpicsSignal]:
return [
self.filter_1.actual_filter_state,
self.filter_2.actual_filter_state,
self.filter_3.actual_filter_state,
self.filter_4.actual_filter_state,
self.filter_5.actual_filter_state,
self.filter_6.actual_filter_state,
self.filter_7.actual_filter_state,
self.filter_8.actual_filter_state,
self.filter_9.actual_filter_state,
self.filter_10.actual_filter_state,
self.filter_11.actual_filter_state,
self.filter_12.actual_filter_state,
self.filter_13.actual_filter_state,
self.filter_14.actual_filter_state,
self.filter_15.actual_filter_state,
self.filter_16.actual_filter_state,
]
2 changes: 1 addition & 1 deletion src/dodal/devices/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class DetectorParams(BaseModel):
"""Holds parameters for the detector. Provides access to a list of Dectris detector
sizes and a converter for distance to beam centre."""

current_energy: float
current_energy_ev: float
exposure_time: float
directory: str
prefix: str
Expand Down
5 changes: 3 additions & 2 deletions src/dodal/devices/eiger.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ def async_stage(self):
status_ok, error_message = self.odin.check_odin_initialised()
if not status_ok:
raise Exception(f"Odin not initialised: {error_message}")

self.arming_status = self.do_arming_chain()
return self.arming_status

Expand Down Expand Up @@ -172,6 +173,7 @@ def set_odin_number_of_frame_chunks(self) -> Status:
return status

def set_odin_pvs(self) -> Status:
assert self.detector_params is not None
file_prefix = self.detector_params.full_filename
status = self.odin.file_writer.file_path.set(
self.detector_params.directory, timeout=self.GENERAL_STATUS_TIMEOUT
Expand Down Expand Up @@ -219,7 +221,6 @@ def set_detector_threshold(self, energy: float, tolerance: float = 0.1) -> Statu
"""

current_energy = self.cam.photon_energy.get()

if abs(current_energy - energy) > tolerance:
return self.cam.photon_energy.set(
energy, timeout=self.GENERAL_STATUS_TIMEOUT
Expand Down Expand Up @@ -296,7 +297,7 @@ def do_arming_chain(self) -> Status:
functions_to_do_arm.extend(
[
lambda: self.set_detector_threshold(
energy=detector_params.current_energy
energy=detector_params.current_energy_ev
),
self.set_cam_pvs,
self.set_odin_number_of_frame_chunks,
Expand Down
13 changes: 13 additions & 0 deletions src/dodal/devices/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,16 @@ def value_is(value, **_):
return value == expected_value

return SubscriptionStatus(subscribable, value_is, timeout=timeout)


# Returns a status which is completed when the subscriptable contains a value within the expected_value list
def await_value_in_list(
subscribable: Any, expected_value: list, timeout: Union[None, int] = None
) -> SubscriptionStatus:
def value_is(value, **_):
return value in expected_value

if type(expected_value) != list:
raise TypeError(f"expected value {expected_value} is not a list")
else:
return SubscriptionStatus(subscribable, value_is, timeout=timeout)
92 changes: 85 additions & 7 deletions src/dodal/devices/xspress3_mini/xspress3_mini.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,101 @@
from ophyd import Component, Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
from enum import Enum

from ophyd import (
Component,
Device,
EpicsSignal,
EpicsSignalRO,
EpicsSignalWithRBV,
Signal,
)
from ophyd.status import Status

from dodal.devices.status import await_value_in_list
from dodal.devices.xspress3_mini.xspress3_mini_channel import Xspress3MiniChannel
from dodal.log import LOGGER


class TriggerMode(Enum):
SOFTWARE = "Software"
HARDWARE = "Hardware"
BURST = "Burst"
TTL_Veto_Only = "TTL_Veto_Only"
IDC = "IDC"
SOTWARE_START_STOP = "Software_Start/Stop"
TTL_BOTH = "TTL_Both"
LVDS_VETO_ONLY = "LVDS_Veto_Only"
LVDS_both = "LVDS_Both"


class UpdateRBV(Enum):
DISABLED = "Disabled"
ENABLED = "Enabled"


class EraseState(Enum):
DONE = "Done"
ERASE = "Erase"


class AcquireState(Enum):
DONE = "Done"
ACQUIRE = "Acquire"


class DetectorState(Enum):
ACQUIRE = "Acquire"
CORRECT = "Correct"
READOUT = "Readout"
ABORTING = "Aborting"

IDLE = "Idle"
SAVING = "Saving"
ERROR = "Error"
INTILTIALIZING = "Initializing"
DISCONNECTED = "Disconnected"
ABORTED = "Aborted"


class Xspress3Mini(Device):
class ArmingSignal(Signal):
def set(self, value, *, timeout=None, settle_time=None, **kwargs):
return self.parent.arm()

do_arm: ArmingSignal = Component(ArmingSignal)

# Assume only one channel for now
channel_1 = Component(Xspress3MiniChannel, "C1_")

erase: EpicsSignal = Component(EpicsSignal, "ERASE")
get_max_num_channels = Component(EpicsSignalRO, "MAX_NUM_CHANNELS_RBV")

acquire: EpicsSignal = Component(EpicsSignal, "Acquire")

get_roi_calc_mini: EpicsSignal = Component(EpicsSignal, "MCA1:Enable_RBV")

NUMBER_ROIS_DEFAULT = 6

trigger_mode_mini: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "TriggerMode")

roi_start_x: EpicsSignal = Component(EpicsSignal, "ROISUM1:MinX")
roi_size_x: EpicsSignal = Component(EpicsSignal, "ROISUM1:SizeX")
acquire_time: EpicsSignal = Component(EpicsSignal, "AcquireTime")
detector_state: EpicsSignalRO = Component(EpicsSignalRO, ":DetectorState_RBV")
NUMBER_ROIS_DEFAULT = 6
acquire_status: Status = None

detector_busy_states = [
DetectorState.ACQUIRE.value,
DetectorState.CORRECT.value,
DetectorState.ABORTING.value,
]

def stage(self):
self.arm().wait(timeout=10)

def do_start(self) -> Status:
self.erase.put(EraseState.ERASE.value)
status = self.channel_1.update_arrays.set(AcquireState.DONE.value)
self.acquire_status = self.acquire.set(AcquireState.ACQUIRE.value)
return status

def arm(self) -> Status:
LOGGER.info("Arming Xspress3Mini detector...")
self.trigger_mode_mini.put(TriggerMode.BURST.value)
self.do_start().wait(timeout=10)
arm_status = await_value_in_list(self.detector_state, self.detector_busy_states)
return arm_status
2 changes: 1 addition & 1 deletion src/dodal/devices/xspress3_mini/xspress3_mini_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@


class Xspress3MiniChannel(Device):
sca5_update_arrays_mini = Component(EpicsSignalRO, "SCAS:TS:TSAcquire")
update_arrays = Component(EpicsSignal, "SCAS:TS:TSAcquire")

roi_high_limit = Component(EpicsSignal, "SCA5_HLM")
roi_llm = Component(EpicsSignal, "SCA5_LLM")
Expand Down
2 changes: 1 addition & 1 deletion tests/devices/system_tests/test_eiger_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@pytest.fixture()
def eiger():
detector_params: DetectorParams = DetectorParams(
current_energy=100,
current_energy_ev=100,
exposure_time=0.1,
directory="/tmp",
prefix="file_name",
Expand Down
40 changes: 40 additions & 0 deletions tests/devices/unit_tests/test_attenuator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from unittest.mock import MagicMock

import pytest
from bluesky import RunEngine
from bluesky import plan_stubs as bps
from ophyd.sim import make_fake_device
from ophyd.status import Status

from dodal.devices.attenuator import Attenuator

CALCULATED_VALUE = range(0, 17)


@pytest.fixture
def fake_attenuator():
FakeAttenuator: Attenuator = make_fake_device(Attenuator)
fake_attenuator: Attenuator = FakeAttenuator(name="attenuator")

def mock_apply_values(val: int):
actual_states = fake_attenuator.get_actual_filter_state_list()
calculated_states = fake_attenuator.get_calculated_filter_state_list()
for i in range(16):
calculated_states[i].sim_put(
CALCULATED_VALUE[i]
) # Ignore the actual calculation as this is EPICS layer
actual_states[i].sim_put(calculated_states[i].get())
return Status(done=True, success=True)

fake_attenuator.change.set = MagicMock(side_effect=mock_apply_values)

return fake_attenuator


def test_set_transmission_success(fake_attenuator: Attenuator):
fake_attenuator.set(1.0).wait(1)


def test_set_transmission_in_run_engine(fake_attenuator: Attenuator):
RE = RunEngine()
RE(bps.abs_set(fake_attenuator, 1, wait=True))
4 changes: 2 additions & 2 deletions tests/devices/unit_tests/test_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

def create_detector_params_with_directory(directory):
return DetectorParams(
current_energy=100,
current_energy_ev=100,
exposure_time=1.0,
directory=directory,
prefix="test",
Expand Down Expand Up @@ -36,7 +36,7 @@ def test_if_trailing_slash_provided_then_not_appended():
)
def test_correct_det_dist_to_beam_converter_path_passed_in(mocked_parse_table):
params = DetectorParams(
current_energy=100,
current_energy_ev=100,
exposure_time=1.0,
directory="directory",
prefix="test",
Expand Down
Loading

0 comments on commit bc45b1e

Please sign in to comment.