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

add i18 beamline definition #722

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6b77b83
add i18 beamline definition
stan-dot Aug 1, 2024
6a7a986
add metadata to dcm crystals
stan-dot Aug 1, 2024
c5110bd
adapt the tests
stan-dot Aug 1, 2024
95abf4a
udpate fixtures
stan-dot Aug 2, 2024
407af41
move devices from i18-bluesky
stan-dot Aug 2, 2024
3de5fca
fix some pv values
stan-dot Aug 5, 2024
40543ac
move away from ophyd devices
stan-dot Aug 5, 2024
fb16c14
add a kb mirror device@
stan-dot Aug 19, 2024
e807242
test lookup table
stan-dot Aug 21, 2024
c734a1c
readjust the value param in the set method
stan-dot Aug 21, 2024
a5a80b6
trying to add the sim extra for ophyd-async
stan-dot Aug 30, 2024
e4d5a71
adapt to the ophyd-async update
stan-dot Sep 12, 2024
4fb0e21
remove the wrong panda import
stan-dot Sep 13, 2024
490f5ee
fix epics imports
stan-dot Sep 13, 2024
50ade1e
fix motor
stan-dot Sep 13, 2024
1907492
add a set method
stan-dot Sep 23, 2024
1f96446
add set wrap asyncstatus
stan-dot Sep 23, 2024
eb30b76
remove the Movable interface
stan-dot Sep 23, 2024
0726a08
table error still
stan-dot Sep 30, 2024
b1442b9
devices cleared up
stan-dot Sep 30, 2024
1e52f97
add testing to the i18 devices
stan-dot Oct 2, 2024
c12d3ba
respond to feedback
stan-dot Oct 7, 2024
68bbbbe
add testing for the crystal metadata
stan-dot Oct 7, 2024
2a96e88
ruff fix
stan-dot Oct 7, 2024
4455702
respond to feedback
stan-dot Oct 15, 2024
4927133
cancel the skip_device comment
stan-dot Oct 16, 2024
cc8e5e2
cancel the crystal metadata
stan-dot Oct 16, 2024
8214942
fix imports again
stan-dot Oct 16, 2024
5cb3cbb
fix dcm i18 test
stan-dot Oct 29, 2024
03e1575
fix lint
stan-dot Nov 11, 2024
3a0ff4f
fix import
stan-dot Nov 11, 2024
a16645f
add test to increase covereage
stan-dot Nov 11, 2024
3977039
deps change
stan-dot Nov 14, 2024
14e31c8
update the imports
stan-dot Nov 22, 2024
cace9c9
update ophyd-async version
stan-dot Nov 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ description = "Ophyd devices and other utils that could be used across DLS beaml
dependencies = [
"click",
"ophyd",
"ophyd-async >= 0.8.0a5",
"ophyd-async>=0.8.0a5",
"ophyd-async[sim]",
"bluesky",
"pyepics",
"dataclasses-json",
Expand Down
286 changes: 286 additions & 0 deletions src/dodal/beamlines/i18.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
from pathlib import Path

from ophyd_async.fastcs.panda import HDFPanda

from dodal.common.beamlines.beamline_utils import (
device_instantiation,
get_path_provider,
set_path_provider,
)
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
from dodal.common.beamlines.device_helpers import numbered_slits
from dodal.common.visit import (
LocalDirectoryServiceClient,
StaticVisitPathProvider,
)
from dodal.devices.i18.diode import Diode
from dodal.devices.i18.KBMirror import KBMirror
from dodal.devices.i18.sim_detector import SimDetector
from dodal.devices.i18.table import Table
from dodal.devices.i22.dcm import CrystalMetadata, DoubleCrystalMonochromator
from dodal.devices.slits import Slits
from dodal.devices.synchrotron import Synchrotron
from dodal.devices.tetramm import TetrammDetector
from dodal.devices.undulator import Undulator
from dodal.devices.xspress3.xspress3 import Xspress3
from dodal.log import set_beamline as set_log_beamline
from dodal.utils import BeamlinePrefix, get_beamline_name, skip_device

BL = get_beamline_name("i18")
set_log_beamline(BL)
set_utils_beamline(BL)


# Currently we must hard-code the visit, determining the visit at runtime requires
# infrastructure that is still WIP.
# Communication with GDA is also WIP so for now we determine an arbitrary scan number
# locally and write the commissioning directory. The scan number is not guaranteed to
# be unique and the data is at risk - this configuration is for testing only.
set_path_provider(
StaticVisitPathProvider(
BL,
Path("/dls/i18/data/2024/cm37264-2/bluesky"),
client=LocalDirectoryServiceClient(),
)
)


def synchrotron(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> Synchrotron:
return device_instantiation(
Synchrotron,
"synchrotron",
"",
wait_for_connection,
fake_with_ophyd_sim,
)


# not ready yet
@skip_device()
stan-dot marked this conversation as resolved.
Show resolved Hide resolved
def undulator(
wait_for_connection: bool = True,
fake_with_ophyd_sim: bool = False,
) -> Undulator:
return device_instantiation(
Undulator,
"undulator",
f"{BeamlinePrefix(BL).insertion_prefix}-MO-SERVC-01:",
wait_for_connection,
fake_with_ophyd_sim,
bl_prefix=False,
poles=80,
length=2.0,
Comment on lines +73 to +74
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this metadata checked or copied from i22? If the former 👍 if the latter, I'd rather have something that is obviously wrong than something that looks correct until the analysis comes back nonsense.

)


@skip_device()
def slits_1(
wait_for_connection: bool = True,
fake_with_ophyd_sim: bool = False,
) -> Slits:
return numbered_slits(
1,
wait_for_connection,
fake_with_ophyd_sim,
)


# Must document what PandAs are physically connected to
# See: https://github.com/bluesky/ophyd-async/issues/284
@skip_device()
def panda1(
wait_for_connection: bool = True,
fake_with_ophyd_sim: bool = False,
) -> HDFPanda:
return device_instantiation(
HDFPanda,
"panda1",
"-MO-PANDA-01:",
wait_for_connection,
fake_with_ophyd_sim,
path_provider=get_path_provider(),
)


def old_xspress3(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> Xspress3:
return device_instantiation(
Xspress3,
prefix="-EA-XSP-02:",
name="Xspress3",
num_channels=16,
wait=wait_for_connection,
fake=fake_with_ophyd_sim,
)
Comment on lines +107 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def old_xspress3(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> Xspress3:
return device_instantiation(
Xspress3,
prefix="-EA-XSP-02:",
name="Xspress3",
num_channels=16,
wait=wait_for_connection,
fake=fake_with_ophyd_sim,
)
def xspress3(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> Xspress3:
return device_instantiation(
Xspress3,
prefix="-EA-XSP-02:",
name="xspress3",
num_channels=16,
wait=wait_for_connection,
fake=fake_with_ophyd_sim,
)



# odin detectors are not yet supported.
# There is a controls project in the works,
# not ready anytime soon
@skip_device()
def xspress3_odin(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> Xspress3:
return device_instantiation(
Xspress3,
prefix="-EA-XSP-03:",
name="Xspress3",
num_channels=4,
wait=wait_for_connection,
fake=fake_with_ophyd_sim,
)
Comment on lines +123 to +134
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@skip_device()
def xspress3_odin(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> Xspress3:
return device_instantiation(
Xspress3,
prefix="-EA-XSP-03:",
name="Xspress3",
num_channels=4,
wait=wait_for_connection,
fake=fake_with_ophyd_sim,
)
@skip_device()
def xspress3_odin(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> Xspress3:
return device_instantiation(
Xspress3,
prefix="-EA-XSP-03:",
name="xspress3_odin",
num_channels=4,
wait=wait_for_connection,
fake=fake_with_ophyd_sim,
)



crystal_1_metadata = CrystalMetadata(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: move these inside the dcm method to prevent namespace leaking

usage="Bragg",
type="silicon",
reflection=(1, 1, 1),
d_spacing=(3.13475, "nm"),
)

crystal_2_metadata = CrystalMetadata(
usage="Bragg",
type="silicon",
reflection=(1, 1, 1),
d_spacing=(3.13475, "nm"),
)


def dcm(
wait_for_connection: bool = True,
fake_with_ophyd_sim: bool = False,
) -> DoubleCrystalMonochromator:
return device_instantiation(
DoubleCrystalMonochromator,
"dcm",
f"{BeamlinePrefix(BL).beamline_prefix}-MO-DCM-01:",
wait_for_connection,
fake_with_ophyd_sim,
bl_prefix=False,
temperature_prefix=f"{BeamlinePrefix(BL).beamline_prefix}-DI-DCM-01:",
crystal_1_metadata=crystal_1_metadata,
crystal_2_metadata=crystal_2_metadata,
)


@skip_device()
def i0(
wait_for_connection: bool = True,
fake_with_ophyd_sim: bool = False,
) -> TetrammDetector:
return device_instantiation(
TetrammDetector,
"i0",
"-DI-XBPM-02:",
wait_for_connection,
fake_with_ophyd_sim,
type="Cividec Diamond XBPM",
path_provider=get_path_provider(),
)


@skip_device()
def it(
wait_for_connection: bool = True,
fake_with_ophyd_sim: bool = False,
) -> TetrammDetector:
return device_instantiation(
TetrammDetector,
"it",
"-DI-XBPM-01:",
wait_for_connection,
fake_with_ophyd_sim,
type="Tetramm",
path_provider=get_path_provider(),
)


@skip_device
def vfm(
stan-dot marked this conversation as resolved.
Show resolved Hide resolved
wait_for_connection: bool = True,
fake_with_ophyd_sim: bool = False,
) -> KBMirror:
return device_instantiation(

Check warning on line 206 in src/dodal/beamlines/i18.py

View check run for this annotation

Codecov / codecov/patch

src/dodal/beamlines/i18.py#L206

Added line #L206 was not covered by tests
KBMirror,
"vfm",
"-OP-VFM-01:",
wait_for_connection,
fake_with_ophyd_sim,
)


def hfm(
wait_for_connection: bool = True,
fake_with_ophyd_sim: bool = False,
) -> KBMirror:
return device_instantiation(
KBMirror,
"hfm",
"-OP-HFM-01:",
wait_for_connection,
fake_with_ophyd_sim,
)


def diode(wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False) -> Diode:
return device_instantiation(
Diode,
"diodad7bdiode",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"diodad7bdiode",
"diode",

Should: or rename the device factory method to make it clear which specific diode this is.

"-DI-PHDGN-07:",
wait_for_connection,
fake_with_ophyd_sim,
)


def main_table(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> Table:
return device_instantiation(
Table,
"table",
"-MO-TABLE-01:",
wait_for_connection,
fake_with_ophyd_sim,
)


def thor_labs_table(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> Table:
return device_instantiation(
Table,
"table",
"-MO-TABLE-02:",
wait_for_connection,
fake_with_ophyd_sim,
)
Comment on lines +238 to +259
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're defining 2 Table classes, but then re-using the same one for both of these tables?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??the PVs are different, this is lab-beamline relationship

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok thanks



# SIMULATED DEVICES


# this is a mock table, not sure how does it relate to the real Table, maybe just a fake option in instantiation is needed
@skip_device()
def raster_stage(
wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False
) -> Table:
return device_instantiation(
Table,
"raster_stage",
"-MO-SIM-01:",
wait_for_connection,
fake_with_ophyd_sim,
)


def sim_detector(wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False):
return device_instantiation(

Check warning on line 280 in src/dodal/beamlines/i18.py

View check run for this annotation

Codecov / codecov/patch

src/dodal/beamlines/i18.py#L280

Added line #L280 was not covered by tests
SimDetector,
"sim_detector",
"-MO-SIM-01:",
wait_for_connection,
fake_with_ophyd_sim,
)
DiamondJoseph marked this conversation as resolved.
Show resolved Hide resolved
31 changes: 31 additions & 0 deletions src/dodal/devices/i18/KBMirror.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from bluesky.protocols import Movable
from ophyd_async.core import AsyncStatus, StandardReadable
from ophyd_async.epics.core import epics_signal_rw
from pydantic import BaseModel


class XYPosition(BaseModel):
x: float
y: float


class KBMirror(StandardReadable, Movable):
def __init__(
self,
prefix: str,
name: str = "",
):
self._prefix = prefix
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self._prefix = prefix

nit: not used anywhere

with self.add_children_as_readables():
self.x = epics_signal_rw(float, prefix + "X")
self.y = epics_signal_rw(float, prefix + "Y")
self.bend1 = epics_signal_rw(float, prefix + "BEND1")
self.bend2 = epics_signal_rw(float, prefix + "BEND2")
self.curve = epics_signal_rw(float, prefix + "CURVE")
self.ellip = epics_signal_rw(float, prefix + "ELLIP")
super().__init__(name=name)

@AsyncStatus.wrap
async def set(self, value: XYPosition):
self.x.set(value.x)
self.y.set(value.y)
17 changes: 17 additions & 0 deletions src/dodal/devices/i18/diode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from ophyd_async.core import (
StandardReadable,
)
from ophyd_async.epics.core import epics_signal_r


class Diode(StandardReadable):
def __init__(
self,
prefix: str,
name: str = "",
):
self._prefix = prefix
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
self._prefix = prefix

nit

with self.add_children_as_readables():
self.signal = epics_signal_r(float, prefix + "B:DIODE:I")

super().__init__(name=name)
18 changes: 18 additions & 0 deletions src/dodal/devices/i18/sim_detector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from ophyd_async.epics.motor import Motor
stan-dot marked this conversation as resolved.
Show resolved Hide resolved
from ophyd_async.sim.demo import PatternGenerator


class SimDetector:
def __init__(self, name: str, motor: Motor, motor_field: str):
self.name = name
self.motor = motor
self.motor_field = motor_field
self.pattern_generator = PatternGenerator(
saturation_exposure_time=0.1, detector_height=100, detector_width=100
)
Comment on lines +5 to +12
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason you're redefining a SimDetector here rather than using the one in ophyd-async that uses the PatternGenerator?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The motor field isn't being used at all- is the PatternGenrator meant to read the motor and change its output depending on the value of the motor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure of the details - what do you think @iain-hall ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with PatternGenerator - could be useful. Any simulated detectors would be generally be used for offline testing rather than on the beamline, and we have one already for the diode used in the undulator gap/lookup table generator scan. Maybe we should only have real beamline devices in beamlines/i18.py, and add simulated devices as needed for offline testing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the motor fields could be used in a plan with dot notation

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should lose the internal motor- you don't want to write your plans with the assumption that your Detector will have an internal motor, you want to write them to be generic enough to use StandardDetector: this will allow you to re-use the plans you're writing for your simulated detector. If you're not going to be using the position of the motor as an input to your PatternGenerator, I would remove it. If you are, I would make it an internal/private field.


def read(self):
return {self.name: {"value": self.pattern_generator}}

def describe(self):
return {self.name: {"source": "synthetic", "dtype": "number"}}
28 changes: 28 additions & 0 deletions src/dodal/devices/i18/table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from ophyd_async.core import (
AsyncStatus,
StandardReadable,
)
from ophyd_async.epics.motor import Motor
from pydantic import BaseModel


class Four_D_Position(BaseModel):
x: float
y: float
z: float
theta: float


class Table(StandardReadable):
def __init__(self, prefix: str = "", name: str = "") -> None:
with self.add_children_as_readables():
self.x = Motor(prefix + "X")
self.y = Motor(prefix + "Y")
self.z = Motor(prefix + "Z")
self.theta = Motor(prefix + "THETA")
super().__init__(name=name)

@AsyncStatus.wrap
async def set(self, value: Four_D_Position):
self.x.set(value.x)
self.y.set(value.y)
stan-dot marked this conversation as resolved.
Show resolved Hide resolved
Loading
Loading