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

Port UndulatorDCM to ophyd-async #461

Merged
merged 24 commits into from
May 7, 2024

Conversation

callumforrester
Copy link
Contributor

@callumforrester callumforrester commented Apr 24, 2024

Works towards #389 and #397

Changes:

Instructions to reviewer on how to test:

  1. Check unit tests pass

Checks for reviewer

  • Would the PR title make sense to a scientist on a set of release notes
  • If a new device has been added does it follow the standards

@callumforrester callumforrester force-pushed the 389-and-397-undulator-dcm-ophyd-async branch from 498792a to 6cce80e Compare April 24, 2024 14:11
@callumforrester
Copy link
Contributor Author

This work provoked a discussion about the best way to handle composite device logic like UndulatorDCM with ophyd-async. The basic options were:

  • Don't do it, use a plan stub
  • Don't use a soft signal like UndulatorDCM does, just directly implement set et al.
  • Keep the soft signal and write a custom SignalBackend

So far, this PR is doing the 3rd option, but is otherwise just a naive conversion to ophyd-async, changing as little logic/attributes as possible. I'm tempted to leave it that way and spin out the additional changes we discussed, as well as the required changes for #389 and #397 out into additional issues/PRs, this one is already getting big.

@DominicOram @dperl-dls @olliesilvester would that be okay with you?

Additional Changes Discussed

  • Expose the fields needed for I22 from Undulator and DCM (see Ophyd-async implementation of optical components for i22  #377)
  • Rename class to BeamEnergy so that BeamEnergy.set sets the beam energy
  • Implement the Locatable protocol so that the beam energy can have a setpoint and a readback
  • Make BeamEnergy.read return a reasonable value for the beam energy

The latter two are not required for Hyperion, however a small Hyperion PR is still needed to make this work

def __init__(
self,
prefix: str,
daq_configuration_path: str,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@DominicOram and @rtuck99 how would you feel about moving the configuration file attributes for both the DCM and Undulator into UndulatorDCM? Since it's just a convenient place to store them for Hyperion and means that the devices are as simple as possible for the read-only use case

Copy link
Contributor

Choose a reason for hiding this comment

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

That's fine by me

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, see what you think

raise AccessError(
"Undulator gap access is disabled. Contact Control Room"
)
def __init__(self, prefix: str, undulator: Undulator, dcm: DCM, name: str = ""):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

prefix is unused and only there to satisfy device_instantiation, which assumes every device takes a prefix. I'm not overjoyed with this but unsure if this should be a change to device_instantiation or just not using to instantiate this device.

Copy link
Contributor

Choose a reason for hiding this comment

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

given we plan to redo device instantiation, is it worth worrying about? IMO best to just note that we don't want this to be the case anymore when we do that

@callumforrester callumforrester force-pushed the 389-and-397-undulator-dcm-ophyd-async branch from 40d8fde to e182834 Compare April 25, 2024 09:45
@callumforrester callumforrester marked this pull request as ready for review April 25, 2024 10:45
Copy link
Contributor

@DominicOram DominicOram left a comment

Choose a reason for hiding this comment

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

@DominicOram @dperl-dls @olliesilvester would that be okay with you?

Yep, works for me, thank you!

Some comments in code. Additionally, can you make test_i03_system (with the i03 panda and pin detect commented out due to #261) and test_i04_system still work? That's how I caught the Enum issue

from dodal.beamlines.beamline_parameters import get_beamline_parameters


class DCM(StandardReadable, HasHints):
Copy link
Contributor

Choose a reason for hiding this comment

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

Should: I appreciate this wasn't your doing but while where here... Can we roll -MO-DCM-01: into the prefix? Either by specifying it in the beamline file like we do in other devices or by just doing a prefix = prefix + "-MO-DCM-01" as the first thing in the init?

def __init__(
self,
prefix: str,
daq_configuration_path: str,
Copy link
Contributor

Choose a reason for hiding this comment

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

That's fine by me

) -> None:
self.gap_motor = Motor(prefix + "BLGAPMTR")
self.current_gap = epics_signal_r(float, prefix + "CURRGAPD")
self.gap_access = epics_signal_r(UndulatorGapAccess, prefix + "IDBLENA")
Copy link
Contributor

Choose a reason for hiding this comment

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

Must: The Enum needs to inherit from str too. +1 to bluesky/ophyd-async#261

Comment on lines 25 to 30
async def _get_energy_distance_table(lookup_table_path: str) -> ndarray:
# Slight cheat to make the file IO async, numpy doesn't do any real IO now, just
# decodes the text
async with aiofiles.open(lookup_table_path, "r") as stream:
raw_table = await stream.read()
return loadtxt(StringIO(raw_table), comments=["#", "Units"])
Copy link
Contributor

Choose a reason for hiding this comment

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

Should: Can you move this util function into lookup_tables.py?

Comment on lines +63 to +59
# Attributes are set after super call so they are not renamed to
# <name>-undulator, etc.
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 Nice comment, thanks!

"undulator-gap_access",
],
)
async def test_read_and_describe_includes(
Copy link
Contributor

Choose a reason for hiding this comment

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

Should: Same as above, why does this need to be staged?

Comment on lines 75 to 76
# fake_undulator_dcm.undulator.gap_motor.move = MagicMock()
# fake_undulator_dcm.dcm.energy_in_kev.move = MagicMock()
Copy link
Contributor

Choose a reason for hiding this comment

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

Should: Can we remove these now?

Comment on lines 99 to 100
# fake_undulator_dcm.undulator.gap_motor.move = MagicMock()
# fake_undulator_dcm.dcm.energy_in_kev.move = MagicMock()
Copy link
Contributor

Choose a reason for hiding this comment

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

Should: Can we remove these?

Comment on lines 120 to 121
# fake_undulator_dcm.undulator.gap_motor.move = MagicMock()
# fake_undulator_dcm.dcm.energy_in_kev.move = MagicMock()
Copy link
Contributor

Choose a reason for hiding this comment

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

Should: As above, remove



def test_energy_set_only_complete_when_all_statuses_are_finished(
async def test_energy_set_only_complete_when_all_statuses_are_finished(
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, this is a nice pattern for testing this that I hadn't yet worked out for ophyd_async!

@callumforrester callumforrester mentioned this pull request Apr 25, 2024
2 tasks
@callumforrester
Copy link
Contributor Author

Fix for black CI issues here: #470

@callumforrester callumforrester force-pushed the 389-and-397-undulator-dcm-ophyd-async branch from d5dd286 to 48091cd Compare May 1, 2024 09:50
Copy link
Contributor

@DominicOram DominicOram left a comment

Choose a reason for hiding this comment

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

Thanks, some more changes in code. This will also break Hyperion, I'll make a PR to fix

Comment on lines 64 to 70
"dcm_bragg_in_degrees",
"dcm_roll_in_mrad",
"dcm_offset_in_mm",
"dcm_perp_in_mm",
"dcm_energy_in_kev",
"dcm_pitch_in_mrad",
"dcm_wavelength",
Copy link
Contributor

Choose a reason for hiding this comment

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

Should: Can we return self.bragg_in_degrees.name etc. This adds the potential to catch people when if they rename the signal but miss this string.

@DominicOram
Copy link
Contributor

I did have some comments about Motor signals being readable but I think those are a wider issue outside this PR, made #492 to discuss

Comment on lines 17 to 56
def __init__(
self,
prefix: str,
name: str = "",
) -> None:
self.bragg_in_degrees = Motor(prefix + "BRAGG")
self.roll_in_mrad = Motor(prefix + "ROLL")
self.offset_in_mm = Motor(prefix + "OFFSET")
self.perp_in_mm = Motor(prefix + "PERP")
self.energy_in_kev = Motor(prefix + "ENERGY")
self.pitch_in_mrad = Motor(prefix + "PITCH")
self.wavelength = Motor(prefix + "WAVELENGTH")

# temperatures
self.xtal1_temp = epics_signal_r(float, prefix + "TEMP1")
self.xtal2_temp = epics_signal_r(float, prefix + "TEMP2")
self.xtal1_heater_temp = epics_signal_r(float, prefix + "TEMP3")
self.xtal2_heater_temp = epics_signal_r(float, prefix + "TEMP4")
self.backplate_temp = epics_signal_r(float, prefix + "TEMP5")
self.perp_temp = epics_signal_r(float, prefix + "TEMP6")
self.perp_sub_assembly_temp = epics_signal_r(float, prefix + "TEMP7")

self.set_readable_signals(
read=[
self.bragg_in_degrees, # type: ignore
self.roll_in_mrad, # type: ignore
self.offset_in_mm, # type: ignore
self.perp_in_mm, # type: ignore
self.energy_in_kev, # type: ignore
self.pitch_in_mrad, # type: ignore
self.wavelength, # type: ignore
self.xtal1_temp,
self.xtal2_temp,
self.xtal1_heater_temp,
self.xtal2_heater_temp,
self.backplate_temp,
self.perp_temp,
self.perp_sub_assembly_temp,
]
)
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 __init__(
self,
prefix: str,
name: str = "",
) -> None:
self.bragg_in_degrees = Motor(prefix + "BRAGG")
self.roll_in_mrad = Motor(prefix + "ROLL")
self.offset_in_mm = Motor(prefix + "OFFSET")
self.perp_in_mm = Motor(prefix + "PERP")
self.energy_in_kev = Motor(prefix + "ENERGY")
self.pitch_in_mrad = Motor(prefix + "PITCH")
self.wavelength = Motor(prefix + "WAVELENGTH")
# temperatures
self.xtal1_temp = epics_signal_r(float, prefix + "TEMP1")
self.xtal2_temp = epics_signal_r(float, prefix + "TEMP2")
self.xtal1_heater_temp = epics_signal_r(float, prefix + "TEMP3")
self.xtal2_heater_temp = epics_signal_r(float, prefix + "TEMP4")
self.backplate_temp = epics_signal_r(float, prefix + "TEMP5")
self.perp_temp = epics_signal_r(float, prefix + "TEMP6")
self.perp_sub_assembly_temp = epics_signal_r(float, prefix + "TEMP7")
self.set_readable_signals(
read=[
self.bragg_in_degrees, # type: ignore
self.roll_in_mrad, # type: ignore
self.offset_in_mm, # type: ignore
self.perp_in_mm, # type: ignore
self.energy_in_kev, # type: ignore
self.pitch_in_mrad, # type: ignore
self.wavelength, # type: ignore
self.xtal1_temp,
self.xtal2_temp,
self.xtal1_heater_temp,
self.xtal2_heater_temp,
self.backplate_temp,
self.perp_temp,
self.perp_sub_assembly_temp,
]
)
def __init__(
self,
prefix: str,
name: str = "",
) -> None:
with self.add_children_as_readables():
self.bragg_in_degrees = Motor(prefix + "BRAGG")
self.roll_in_mrad = Motor(prefix + "ROLL")
self.offset_in_mm = Motor(prefix + "OFFSET")
self.perp_in_mm = Motor(prefix + "PERP")
self.energy_in_kev = Motor(prefix + "ENERGY")
self.pitch_in_mrad = Motor(prefix + "PITCH")
self.wavelength = Motor(prefix + "WAVELENGTH")
# temperatures
self.xtal1_temp = epics_signal_r(float, prefix + "TEMP1")
self.xtal2_temp = epics_signal_r(float, prefix + "TEMP2")
self.xtal1_heater_temp = epics_signal_r(float, prefix + "TEMP3")
self.xtal2_heater_temp = epics_signal_r(float, prefix + "TEMP4")
self.backplate_temp = epics_signal_r(float, prefix + "TEMP5")
self.perp_temp = epics_signal_r(float, prefix + "TEMP6")
self.perp_sub_assembly_temp = epics_signal_r(float, prefix + "TEMP7")

Comment on lines 60 to 72
@property
def hints(self) -> Hints:
return {
"fields": [
self.bragg_in_degrees.name,
self.roll_in_mrad.name,
self.offset_in_mm.name,
self.perp_in_mm.name,
self.energy_in_kev.name,
self.pitch_in_mrad.name,
self.wavelength.name,
]
}
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
@property
def hints(self) -> Hints:
return {
"fields": [
self.bragg_in_degrees.name,
self.roll_in_mrad.name,
self.offset_in_mm.name,
self.perp_in_mm.name,
self.energy_in_kev.name,
self.pitch_in_mrad.name,
self.wavelength.name,
]
}

Not required with above change

Copy link
Contributor

Choose a reason for hiding this comment

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

I was literally half way through doing this when I got called to i03 ;)

Copy link
Contributor

Choose a reason for hiding this comment

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

We are of one mind, when my hair is long again we can link up Na'vi style

Copy link
Contributor

@DominicOram DominicOram left a comment

Choose a reason for hiding this comment

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

Great, thank you. Have updated the Hyperion side to match in DiamondLightSource/hyperion#1349 so happy to merge both at the same time when that is approved.

@DominicOram DominicOram requested a review from DiamondJoseph May 5, 2024 18:17
@DominicOram DominicOram merged commit 6864607 into main May 7, 2024
11 checks passed
@DominicOram DominicOram deleted the 389-and-397-undulator-dcm-ophyd-async branch May 7, 2024 08:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants