-
Notifications
You must be signed in to change notification settings - Fork 8
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
Artemis 262 move arming out of stage #43
Conversation
Codecov Report
@@ Coverage Diff @@
## main #43 +/- ##
==========================================
+ Coverage 87.71% 88.02% +0.30%
==========================================
Files 54 54
Lines 1767 1796 +29
==========================================
+ Hits 1550 1581 +31
+ Misses 217 215 -2
... and 1 file with indirect coverage changes 📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! With the requested changes it looks like a good start that we could at least test on the beamline
src/dodal/devices/eiger.py
Outdated
class ArmingSignal(Signal): | ||
def set(self, value, *, timeout=None, settle_time=None, **kwargs): | ||
return self.parent.stage() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Must: This creates the class but you don't actually have an instance of the signal
src/dodal/devices/eiger.py
Outdated
@@ -85,7 +91,7 @@ def stage(self): | |||
LOGGER.info("Waiting on parameter callbacks") | |||
status.wait(self.STALE_PARAMS_TIMEOUT) | |||
|
|||
self.arm_detector() | |||
return self.arm_detector() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Must: With this now being asynchronous it no longer conforms to the interface for stage
so we should rename it.
src/dodal/devices/eiger.py
Outdated
|
||
def forward_bit_depth_to_filewriter(self): | ||
bit_depth = self.bit_depth.get() | ||
self.odin.file_writer.data_type.put(f"UInt{bit_depth}") | ||
|
||
def arm_detector(self): | ||
def arm_detector(self) -> Status: | ||
statuses = [Status(1), Status(2), Status(3), Status(4)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should: Each function that we pass these to is overwriting the entry so it's a bit confusing having these here. Might be better to start with an empty list then each function can just append to the list.
src/dodal/devices/eiger.py
Outdated
LOGGER.info("Waiting on stale parameters to go low") | ||
self.wait_for_stale_parameters() | ||
self.wait_for_stale_parameters(statuses) # Starts the chain of arming functions | ||
|
||
self.forward_bit_depth_to_filewriter() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Must: This needs to be async too
src/dodal/devices/eiger.py
Outdated
|
||
LOGGER.info("Setting aquire") | ||
self.cam.acquire.set(1).wait(timeout=10) | ||
|
||
self.filewriters_finished = self.odin.create_finished_status() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Must: This also needs to be async
src/dodal/devices/eiger.py
Outdated
@@ -24,6 +25,11 @@ class EigerTriggerNumber(str, Enum): | |||
|
|||
|
|||
class EigerDetector(Device): | |||
class ArmingSignal(Signal): | |||
def set(self, value, *, timeout=None, settle_time=None, **kwargs): | |||
return self.parent.stage() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Must: The function has been renamed so we need t change it here
@callumforrester and @coretl, you might have some opinions on this. Callum, I know you weren't keen on this approach but I think it makes the plan side quite elegant (https://github.com/DiamondLightSource/python-artemis/pull/631/files). I'm particularly interested in:
(I am currently reviewing it for style etc. so if you do have comments around that they may be similar to mine in a minute...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@DominicOram I can see the elegance of the plan side, but you've actually done two things:
- Move arming out of stage
- Remove automatic staging/unstaging from the plan and do it manually
Can we not achieve the same just by doing 2.?
i.e.
yield from bps.stage(eiger)
...
def do_fgs():
try:
yield from bps.wait() # Wait for all moves to complete
yield from bps.kickoff(fgs_motors)
yield from bps.complete(fgs_motors, wait=True)
finally:
yield from bps.unstage(fgs_composite.eiger)
Regardless of that, wrap_and_do_funcs
does look like a useful addition, see my other comment for musings on implementation.
src/dodal/devices/utils.py
Outdated
@@ -12,3 +18,71 @@ def epics_signal_put_wait(pv_name: str, wait: float = 1.0) -> EpicsSignal: | |||
EpicsSignal: An EpicsSignal that will wait for a callback. | |||
""" | |||
return Component(EpicsSignal, pv_name, put_complete=True, write_timeout=wait) | |||
|
|||
|
|||
def wrap_and_do_funcs(unwrapped_funcs, timeout=60): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@DominicOram Have been thinking about a cleaner implementation, how about something like this?
It's not completely right because some of the tests don't like it, but I think recursion may be the way to go:
def wrap_and_do_funcs(
unwrapped_funcs: list[Callable[[], Status]],
timeout: float = 60.0,
previous_status: Optional[Status] = None,
) -> Status:
func, other_funcs = unwrapped_funcs[-1], unwrapped_funcs[1:-1]
def wrapper(previous_status: Optional[Status] = None) -> Status:
if previous_status is not None and previous_status.exception() is not None:
# If there is an exception, return the status immediately, ends recursion
# so no further statuses are evaluated
LOGGER.error(
f"{previous_status} has failed with error {previous_status.exception()}"
)
return previous_status
LOGGER.info(f"Doing {func.__name__}")
next_status = func()
if len(other_funcs) > 0:
# Queue up the next function to run when this status completes.
# Do not queue up more functions if there aren't any, ends recursion
next_status.add_callback(lambda: wrap_and_do_funcs(other_funcs, timeout))
return next_status
return wrapper(previous_status)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I feel like we can do it nicer with recursion. I think that might require a bit more thought so let's do a beamline test first @olliesilvester then we'll try and refactor to use recursion
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really good, should speed things up a lot. A few comments in code but most of these probably came from us chopping and changing the solution a few times. Additionally:
- Can you pull the
10
timeout out into a constant - I think we could do with some separate unit tests on
wrap_and_do_funcs
src/dodal/devices/eiger.py
Outdated
dummy_status = Status() | ||
dummy_status.set_finished() | ||
return dummy_status |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should: I think we can use Status(done=True, success=True)
|
||
from ophyd.status import SubscriptionStatus | ||
|
||
T = TypeVar("T") | ||
|
||
|
||
def await_value(subscribable: Any, expected_value: T) -> SubscriptionStatus: | ||
def await_value( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, thanks
src/dodal/devices/utils.py
Outdated
This function can be used to convert a series of blocking status functions to being asynchronous | ||
by making use of callbacks. It also checks for exceptions on each returned status | ||
For example, instead of blocking on status.wait(), the code will continue running until the status | ||
is marked as finished |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should: I think you need to mention something about them run in sequence, at the moment this could read like all of the functions are run in parallel. I also think the bit after For example
doesn't add much to the explanation
src/dodal/devices/utils.py
Outdated
|
||
# The returned status - marked as finished at the end of the callback chain. If any | ||
# intermediate statuses have an exception, the full_status will timeout. | ||
full_status = Status(timeout=timeout) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could: Might be nice to set the full_status
to have the exception thrown from individual statuses
src/dodal/devices/utils.py
Outdated
# Once the series of functions have finished with successful statuses, the final action is to mark | ||
# the full_status as finished |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Rather than this comment I would create a closing_func=lambda: full_status.set_finished()
then use that. Makes the code a bit more self documenting
src/dodal/devices/eiger.py
Outdated
energy=detector_params.current_energy | ||
), | ||
self.set_cam_pvs, | ||
self.set_odin_pvs_after_file_writer_set, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Must: We're missing the set_odin_pvs
step. I also think we could rename those to be a bit nicer. Probably set_number_of_frame_chunks
and set_odin_pvs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks I missed this - the function is back in and renamed now
src/dodal/devices/eiger.py
Outdated
] | ||
) | ||
|
||
if not self.armed: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should: As discussed it would be better to just check this before doing anything, probably in the stage itself
src/dodal/devices/eiger.py
Outdated
if not self.armed: | ||
unwrapped_funcs.extend( | ||
[ | ||
lambda: await_value(self.stale_params, 0, 60), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should: we can use STALE_PARAMS_TIMEOUT
here
src/dodal/devices/eiger.py
Outdated
|
||
await_value(self.odin.fan.ready, 1).wait(10) | ||
return self.arming_status |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should: I don't think this is the correct status to return, if we want it to return a status we should change the put
to a set
. We should actually also do a wait
on it before moving on with the arming (even though that wasn't in there previously)
src/dodal/devices/eiger.py
Outdated
def _wait_for_cam_acquire(self) -> Status: | ||
LOGGER.info("Setting aquire") | ||
self.cam.acquire.set(1).wait(timeout=10) | ||
this_status = self.cam.acquire.set(1, timeout=10) | ||
return this_status |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should: This can probably just be a lambda now
Can we? I'm not sure if we can if |
In this case, don't we just make stage block on the status? |
Ah, didn't see that you were waiting for a group, nevermind |
As discussed offline with @callumforrester, we should tidy up the recursion then put in a PR to ophyd |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I haven't fully reviewed the tests but have some comments on the rest of the changes
src/dodal/devices/eiger.py
Outdated
# this is no longer used TODO clean up | ||
def enable_roi_mode(self): | ||
self.change_roi_mode(True) | ||
|
||
def disable_roi_mode(self): | ||
self.change_roi_mode(False) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should: Agree, lets remove them in this PR (first do a quick search that they're not used anywhere)
status = self.cam.roi_mode.set( | ||
1 if enable else 0, timeout=self.GENERAL_STATUS_TIMEOUT | ||
) | ||
status &= self.odin.file_writer.image_height.set( | ||
detector_dimensions.height, timeout=self.GENERAL_STATUS_TIMEOUT | ||
) | ||
status &= self.odin.file_writer.image_width.set( | ||
detector_dimensions.width, timeout=self.GENERAL_STATUS_TIMEOUT | ||
) | ||
status &= self.odin.file_writer.num_row_chunks.set( | ||
detector_dimensions.height, timeout=self.GENERAL_STATUS_TIMEOUT | ||
) | ||
status &= self.odin.file_writer.num_col_chunks.set( | ||
detector_dimensions.width, timeout=self.GENERAL_STATUS_TIMEOUT | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could: You're right that these should all have a timeout but it's getting messy with them set so many times here. What about something like:
status = Status(timeout=self.GENERAL_STATUS_TIMEOUT) # Overall status to track timeout for the whole operation
status &= self.cam.roi_mode.set(1 if enable else 0)
...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And we could repeat the pattern elsewhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if we did this then the first part of the status would never automatically be set as finished. Would attaching a timeout to just one of these statuses give the same effect?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As discussed we'll leave this as it is for now
src/dodal/devices/eiger.py
Outdated
|
||
def disarm_detector(self): | ||
self.cam.acquire.put(0) | ||
self.armed = False | ||
|
||
def make_chained_functions(self) -> Status: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should: I think we could do with a better name here. This is not juts making the chain, it's kicking it off. The name is also too generic, we're doing the arming, not just any chain.
src/dodal/devices/eiger.py
Outdated
this_status = await_value(self.odin.fan.ready, 1, self.GENERAL_STATUS_TIMEOUT) | ||
return this_status |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Can do this in one line
src/dodal/devices/eiger.py
Outdated
|
||
await_value(self.odin.fan.ready, 1).wait(10) | ||
unwrapped_funcs.extend( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should: We can just extend it once
@pytest.fixture | ||
def finished_status(): | ||
status = Status() | ||
status.set_finished() | ||
return status |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I wouldn't make this a fixture, it can just be a function you call to get a status when you need one
src/dodal/devices/utils.py
Outdated
|
||
|
||
def wrap_and_do_funcs( | ||
unwrapped_funcs: list[Callable[[], StatusBase]], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: functions_to_chain
rather than unwrapped_funcs
maybe?
src/dodal/devices/eiger.py
Outdated
|
||
self.filewriters_finished = self.odin.create_finished_status() | ||
def make_chained_functions(self) -> Status: | ||
unwrapped_funcs = list() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should: unwrapped_funcs
is too generic, something like functions_to_do_arm
("fake_eiger._finish_arm"), | ||
], | ||
) | ||
def test_check_callback_error(fake_eiger: EigerDetector, func): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should: I think a more useful test here would be to keep the same list as defined in the eiger
but change which one throws an error
|
||
fake_eiger.odin.file_writer.capture.sim_put(1) | ||
fake_eiger.odin.file_writer.num_captured.sim_put(2000) | ||
fake_eiger.odin.check_odin_state = MagicMock(return_value=True) | ||
|
||
fake_eiger.detector_params.trigger_mode = TriggerMode.FREE_RUN | ||
fake_eiger.armed = True |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should: No longer used, remove
Co-authored-by: Dominic Oram <dominic.oram@diamond.ac.uk>
…tage' into artemis_262_move_arming_out_of_stage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Much nicer naming, thank you!
Fixes Artemis issue #262