Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#207] Add VMXm fast grid scan devices #211

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
20 changes: 15 additions & 5 deletions src/dodal/beamlines/beamline_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,23 @@

ACTIVE_DEVICES: Dict[str, AnyDevice] = {}
BL = ""
PREFIX: BeamlinePrefix = None # type: ignore


def set_beamline(beamline: str):
global BL
def set_beamline(
beamline: str,
suffix: Optional[str] = None,
beamline_prefix: Optional[str] = None,
insertion_prefix: Optional[str] = None,
):
global BL, PREFIX
BL = beamline
PREFIX = BeamlinePrefix(
ixx=beamline,
suffix=suffix,
beamline_prefix=beamline_prefix,
insertion_prefix=insertion_prefix,
)


def clear_devices():
Expand Down Expand Up @@ -95,9 +107,7 @@ def device_instantiation(
if already_existing_device is None:
device_instance = device_factory(
name=name,
prefix=f"{(BeamlinePrefix(BL).beamline_prefix)}{prefix}"
if bl_prefix
else prefix,
prefix=f"{PREFIX.beamline_prefix}{prefix}" if bl_prefix else prefix,
**kwargs,
)
ACTIVE_DEVICES[name] = device_instance
Expand Down
114 changes: 114 additions & 0 deletions src/dodal/beamlines/vmxm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from dodal.beamlines.beamline_utils import device_instantiation
from dodal.beamlines.beamline_utils import set_beamline as set_utils_beamline
from dodal.devices.eiger import EigerDetector
from dodal.devices.fast_grid_scan import FastGridScan
from dodal.devices.vmxm.vmxm_attenuator import VmxmAttenuator
from dodal.devices.zebra import Zebra
from dodal.devices.backlight import Backlight
from dodal.devices.synchrotron import Synchrotron
from dodal.log import set_beamline as set_log_beamline
from dodal.utils import get_beamline_name, skip_device

SIM_BEAMLINE_NAME = "S02-1"

BL = get_beamline_name(SIM_BEAMLINE_NAME)
set_log_beamline(BL)
set_utils_beamline(
BL, suffix="J", beamline_prefix="BL02J", insertion_prefix="SR-DI-J02"
)


@skip_device(lambda: BL == SIM_BEAMLINE_NAME)
def eiger(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> EigerDetector:
"""Get the vmxm eiger device, instantiate it if it hasn't already been.
If this is called when already instantiated in vmxm, it will return the existing object.
"""
return device_instantiation(
device_factory=EigerDetector,
name="eiger",
prefix="-EA-EIGER-01:",
wait=wait_for_connection,
fake=fake_with_ophyd_sim,
)


@skip_device(lambda: BL == SIM_BEAMLINE_NAME)
def fast_grid_scan(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> FastGridScan:
"""Get the vmxm fast_grid_scan device, instantiate it if it hasn't already been.
If this is called when already instantiated in vmxm, it will return the existing object.
"""
return device_instantiation(
device_factory=FastGridScan,
name="fast_grid_scan",
prefix="-MO-SAMP-11:FGS:",
wait=wait_for_connection,
fake=fake_with_ophyd_sim,
)


@skip_device(lambda: BL == SIM_BEAMLINE_NAME)
def zebra(wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False) -> Zebra:
"""Get the vmxm zebra device, instantiate it if it hasn't already been.
If this is called when already instantiated in vmxm, it will return the existing object.
"""
return device_instantiation(
Zebra,
"zebra",
"-EA-ZEBRA-01:",
wait_for_connection,
fake_with_ophyd_sim,
)


@skip_device(lambda: BL == SIM_BEAMLINE_NAME)
def attenuator(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> VmxmAttenuator:
"""Get the vmxm attenuator device, instantiate it if it hasn't already been.
If this is called when already instantiated in vmxm, it will return the existing object.
"""
return device_instantiation(
VmxmAttenuator,
"attenuator",
"-OP-ATTN-01:",
wait_for_connection,
fake_with_ophyd_sim,
)



@skip_device(lambda: BL == SIM_BEAMLINE_NAME)
def backlight(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> Backlight:
"""Get the i03 backlight device, instantiate it if it hasn't already been.
If this is called when already instantiated in i03, it will return the existing object.
Tom-Willemsen marked this conversation as resolved.
Show resolved Hide resolved
"""
return device_instantiation(
device_factory=Backlight,
name="backlight",
prefix="",
wait=wait_for_connection,
fake=fake_with_ophyd_sim,
)


@skip_device(lambda: BL == SIM_BEAMLINE_NAME)
def synchrotron(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> Synchrotron:
"""Get the i03 synchrotron device, instantiate it if it hasn't already been.
If this is called when already instantiated in i03, it will return the existing object.
Tom-Willemsen marked this conversation as resolved.
Show resolved Hide resolved
"""
return device_instantiation(
Synchrotron,
"synchrotron",
"",
wait_for_connection,
fake_with_ophyd_sim,
bl_prefix=False,
)
4 changes: 2 additions & 2 deletions src/dodal/devices/backlight.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ class Backlight(Device):
OUT = 0
IN = 1

pos: EpicsSignal = Component(EpicsSignal, "-EA-BL-01:CTRL")
pos: EpicsSignal = Component(EpicsSignal, "-DI-IOC-02:LED:INOUT")
# Toggle to switch it On or Off
toggle: EpicsSignal = Component(EpicsSignal, "-EA-BLIT-01:TOGGLE")
toggle: EpicsSignal = Component(EpicsSignal, "-EA-OAV-01:FZOOM:TOGGLE")
Tom-Willemsen marked this conversation as resolved.
Show resolved Hide resolved

def set(self, position: int) -> StatusBase:
status = self.pos.set(position)
Expand Down
4 changes: 2 additions & 2 deletions src/dodal/devices/detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from pydantic import BaseModel, validator

from dodal.devices.det_dim_constants import (
EIGER2_X_16M_SIZE,
EIGER2_X_9M_SIZE,
DetectorSize,
DetectorSizeConstants,
constants_from_type,
Expand Down Expand Up @@ -40,7 +40,7 @@ class DetectorParams(BaseModel):
use_roi_mode: bool
det_dist_to_beam_converter_path: str
trigger_mode: TriggerMode = TriggerMode.SET_FRAMES
detector_size_constants: DetectorSizeConstants = EIGER2_X_16M_SIZE
detector_size_constants: DetectorSizeConstants = EIGER2_X_9M_SIZE # This looks like it's always using the default and not taken from the json
Tom-Willemsen marked this conversation as resolved.
Show resolved Hide resolved
beam_xy_converter: DetectorDistanceToBeamXYConverter = None

class Config:
Expand Down
20 changes: 15 additions & 5 deletions src/dodal/devices/eiger.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ def with_params(
return det

def set_detector_parameters(self, detector_params: DetectorParams):
LOGGER.info(f"Exposure time {detector_params.exposure_time}")
Tom-Willemsen marked this conversation as resolved.
Show resolved Hide resolved

self.detector_params = detector_params
if self.detector_params is None:
raise Exception("Parameters for scan must be specified")
Expand Down Expand Up @@ -103,7 +105,7 @@ def stage(self):
self.async_stage().wait(timeout=self.ARMING_TIMEOUT)

def stop_odin_when_all_frames_collected(self):
LOGGER.info("Waiting on all frames")
LOGGER.info(f"Waiting on all frames, expected {self.detector_params.full_number_of_images}")
Tom-Willemsen marked this conversation as resolved.
Show resolved Hide resolved
try:
await_value(
self.odin.file_writer.num_captured,
Expand Down Expand Up @@ -151,6 +153,8 @@ def change_roi_mode(self, enable: bool) -> Status:
else self.detector_params.detector_size_constants.det_size_pixels
)

LOGGER.info(f"Setting height and width on odin to {detector_dimensions.height}, {detector_dimensions.width}")
Tom-Willemsen marked this conversation as resolved.
Show resolved Hide resolved

status = self.cam.roi_mode.set(
1 if enable else 0, timeout=self.GENERAL_STATUS_TIMEOUT
)
Expand All @@ -170,6 +174,7 @@ def change_roi_mode(self, enable: bool) -> Status:
return status

def set_cam_pvs(self) -> AndStatus:
LOGGER.info("Setting cam pvs")
assert self.detector_params is not None
status = self.cam.acquire_time.set(
self.detector_params.exposure_time, timeout=self.GENERAL_STATUS_TIMEOUT
Expand All @@ -188,13 +193,15 @@ def set_cam_pvs(self) -> AndStatus:
return status

def set_odin_number_of_frame_chunks(self) -> Status:
LOGGER.info("Setting num frames")
assert self.detector_params is not None
status = self.odin.file_writer.num_frames_chunks.set(
1, timeout=self.GENERAL_STATUS_TIMEOUT
)
return status

def set_odin_pvs(self) -> Status:
LOGGER.info("Setting odin PVs")
assert self.detector_params is not None
file_prefix = self.detector_params.full_filename
status = self.odin.file_writer.file_path.set(
Expand All @@ -212,6 +219,7 @@ def set_odin_pvs(self) -> Status:
return status

def set_mx_settings_pvs(self):
LOGGER.info("Setting mx settings")
assert self.detector_params is not None
beam_x_pixels, beam_y_pixels = self.detector_params.get_beam_position_pixels(
self.detector_params.detector_distance
Expand Down Expand Up @@ -241,7 +249,7 @@ def set_detector_threshold(self, energy: float, tolerance: float = 0.1) -> Statu
tolerance (float, optional): If the energy is already set to within
this tolerance it is not set again. Defaults to 0.1eV.
"""

LOGGER.info("Setting threshold energy")
current_energy = self.cam.photon_energy.get()
if abs(current_energy - energy) > tolerance:
return self.cam.photon_energy.set(
Expand All @@ -257,7 +265,7 @@ def set_num_triggers_and_captures(self) -> Status:
during the datacollection. The number of images is the number of images per
trigger.
"""

LOGGER.info("Num triggers")
assert self.detector_params is not None
status = self.cam.num_images.set(
self.detector_params.num_images_per_trigger,
Expand All @@ -284,6 +292,7 @@ def set_num_triggers_and_captures(self) -> Status:
return status

def _wait_for_odin_status(self) -> Status:
LOGGER.info("Wait for odin status")
self.forward_bit_depth_to_filewriter()
status = self.odin.file_writer.capture.set(
1, timeout=self.GENERAL_STATUS_TIMEOUT
Expand Down Expand Up @@ -313,8 +322,9 @@ def disarm_detector(self):
def do_arming_chain(self) -> Status:
functions_to_do_arm = []
detector_params: DetectorParams = self.detector_params
if detector_params.use_roi_mode:
functions_to_do_arm.append(lambda: self.change_roi_mode(enable=True))
# Default is no ROI on i03 so we only switych one way
#if detector_params.use_roi_mode:
functions_to_do_arm.append(lambda: self.change_roi_mode(detector_params.use_roi_mode))
Tom-Willemsen marked this conversation as resolved.
Show resolved Hide resolved

functions_to_do_arm.extend(
[
Expand Down
43 changes: 24 additions & 19 deletions src/dodal/devices/fast_grid_scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
EpicsSignalRO,
EpicsSignalWithRBV,
Signal,
Kind
)
from ophyd.status import DeviceStatus, StatusBase
from pydantic import BaseModel, validator
Expand Down Expand Up @@ -208,35 +209,38 @@ def clean_up(self):
class FastGridScan(Device):
x_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "X_NUM_STEPS")
y_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y_NUM_STEPS")
z_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_NUM_STEPS")
#z_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_NUM_STEPS")
z_steps: Signal = Component(Signal, kind=Kind.hinted, value=1)
Tom-Willemsen marked this conversation as resolved.
Show resolved Hide resolved

x_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "X_STEP_SIZE")
y_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y_STEP_SIZE")
z_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_STEP_SIZE")
# z_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_STEP_SIZE")
z_step_size: Signal = Component(Signal, kind=Kind.hinted, value=1)

dwell_time: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "DWELL_TIME")
dwell_time: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "EXPOSURE_TIME")

x_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "X_START")
y1_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y_START")
y2_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y2_START")
# y2_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y2_START")
z1_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_START")
z2_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z2_START")
# z2_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z2_START")

position_counter: EpicsSignal = Component(
EpicsSignal, "POS_COUNTER", write_pv="POS_COUNTER_WRITE"
EpicsSignal, "POS_COUNTER_RBV", write_pv="POS_COUNTER"
)
x_counter: EpicsSignalRO = Component(EpicsSignalRO, "X_COUNTER")
y_counter: EpicsSignalRO = Component(EpicsSignalRO, "Y_COUNTER")
scan_invalid: EpicsSignalRO = Component(EpicsSignalRO, "SCAN_INVALID")
# scan_invalid: EpicsSignalRO = Component(EpicsSignalRO, "SCAN_INVALID")
scan_invalid: Signal = Component(Signal, kind=Kind.hinted, value=0)

run_cmd: EpicsSignal = Component(EpicsSignal, "RUN.PROC")
stop_cmd: EpicsSignal = Component(EpicsSignal, "STOP.PROC")
status: EpicsSignalRO = Component(EpicsSignalRO, "SCAN_STATUS")
status: EpicsSignalRO = Component(EpicsSignalRO, "SCAN_STATUS_RBV")

expected_images: Signal = Component(Signal)

# Kickoff timeout in seconds
KICKOFF_TIMEOUT: float = 5.0
KICKOFF_TIMEOUT: float = 20.0
Tom-Willemsen marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand All @@ -262,10 +266,11 @@ def kickoff(self) -> StatusBase:

def scan():
try:
self.log.debug("Running scan")
self.log.info("Running scan")
self.run_cmd.put(1)
self.log.debug("Waiting for scan to start")
self.log.info("Waiting for scan to start")
await_value(self.status, 1).wait()
self.log.info("Kickoff finished")
st.set_finished()
except Exception as e:
st.set_exception(e)
Expand All @@ -289,26 +294,26 @@ def set_fast_grid_scan_params(scan: FastGridScan, params: GridScanParams):
params.x_steps,
scan.y_steps,
params.y_steps,
scan.z_steps,
params.z_steps,
#scan.z_steps,
#params.z_steps,
scan.x_step_size,
params.x_step_size,
scan.y_step_size,
params.y_step_size,
scan.z_step_size,
params.z_step_size,
#scan.z_step_size,
#params.z_step_size,
scan.dwell_time,
params.dwell_time,
scan.x_start,
params.x_start,
scan.y1_start,
params.y1_start,
scan.y2_start,
params.y2_start,
#scan.y2_start,
#params.y2_start,
scan.z1_start,
params.z1_start,
scan.z2_start,
params.z2_start,
#scan.z2_start,
#params.z2_start,
scan.position_counter,
0,
)
2 changes: 1 addition & 1 deletion src/dodal/devices/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Callable, Optional

from ophyd import Component, Device, EpicsSignal
from ophyd.status import Status, StatusBase
from ophyd.status import AndStatus, Status, StatusBase
Tom-Willemsen marked this conversation as resolved.
Show resolved Hide resolved

from dodal.log import LOGGER

Expand Down
Empty file.
Loading
Loading