-
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
Convert mirror voltage devices to use ophyd async #636
Conversation
src/dodal/devices/focusing_mirror.py
Outdated
async for accepted_value in observe_value( | ||
demand_accepted, timeout=DEFAULT_SETTLE_TIME_S | ||
): | ||
LOGGER.debug( | ||
f"Current demand accepted = {accepted_value} for {setpoint_v.name}" | ||
) | ||
if not set_applied: | ||
if accepted_value == MirrorVoltageDemand.OK: | ||
await setpoint_v.set(value) | ||
set_applied = True | ||
else: | ||
raise AssertionError("Demand accepted before set attempted") | ||
demand_accepted.unsubscribe(subs_handle) | ||
|
||
demand_accepted_status.set_finished() | ||
# else timeout handled by parent demand_accepted_status | ||
else: | ||
if accepted_value != MirrorVoltageDemand.OK: | ||
LOGGER.debug( | ||
f"Demand not accepted for {setpoint_v.name}, waiting for acceptance..." | ||
) | ||
else: | ||
LOGGER.debug(f"Demand accepted for {setpoint_v.name}") | ||
break |
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.
Why are we doing the set inside the loop? To try and avoid the race condition of missing the update on demand_accepted
where it goes to OK
? If so I think we can probably think of something slightly cleaner
tests/conftest.py
Outdated
@@ -74,16 +73,10 @@ def pytest_runtest_teardown(): | |||
|
|||
@pytest.fixture | |||
def vfm_mirror_voltages() -> VFMMirrorVoltages: |
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: Given this is an ophyd_async
device it needs a RE before being instantiated
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #636 +/- ##
==========================================
+ Coverage 93.03% 93.43% +0.40%
==========================================
Files 103 105 +2
Lines 3903 4083 +180
==========================================
+ Hits 3631 3815 +184
+ Misses 272 268 -4 ☔ View full report in Codecov by Sentry. |
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.
Thank you, I think this is a lot cleaner. Some comments in code
src/dodal/devices/focusing_mirror.py
Outdated
if subs_handle is None: | ||
raise AssertionError("Demand accepted before set attempted") | ||
demand_accepted.unsubscribe(subs_handle) | ||
it_values = observe_value(demand_accepted, timeout=DEFAULT_SETTLE_TIME_S) |
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: I think it_values
could be better named maybe demand_accepted_iterator
?
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 it would be good to have a comment on why we're starting the observe early e.g. the race condition stuff
src/dodal/devices/focusing_mirror.py
Outdated
demand_accepted.unsubscribe(subs_handle) | ||
it_values = observe_value(demand_accepted, timeout=DEFAULT_SETTLE_TIME_S) | ||
accepted_value = await anext(it_values) | ||
if accepted_value == MirrorVoltageDemand.OK: |
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 do this check at the start of this function, I think it's unlikely it will have changed so no need to check again
src/dodal/devices/focusing_mirror.py
Outdated
while MirrorVoltageDemand.SLEW == (accepted_value := await anext(it_values)): | ||
pass |
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: I'm 90% sure it's we won't get multiple of these so just the check above is fine. @coretl does observe_values
guarantee you wont get multiple updates if the value is the same?
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.
It passes the camonitor
updates straight through, so as long as EPICS isn't squashing the updates (via MDEL) then you will get multiple updates
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.
Hmm... Then we will need to always check it in a loop
from unittest.mock import DEFAULT, MagicMock, patch | ||
import asyncio | ||
|
||
# prevent python 3.10 exception doppelganger stupidity |
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: A link to somewhere that explains this issue would be good. It's not clear what this means
) | ||
return DEFAULT | ||
|
||
vfm_mirror_voltages._channel14_voltage_device._setpoint_v.set = MagicMock( | ||
vfm_mirror_voltages.voltage_channels[0]._setpoint_v.set = AsyncMock( |
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: So set
is already backed by a mock. you can do:
from ophyd_async.core import get_mock_put
mock = get_mock_put(vfm_mirror_voltages.voltage_channels[0]._setpoint_v)
mock
will already be a mock that you can play with and it should fix your type hinting later on
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.
Thank you! Apologies, I think there is still a potential race, maybe I'm getting paranoid now but I feel like if this is the first time we do something like this it will be used as a model for other devices so we should be as accurate as possible.
await anext(demand_accepted_iterator) | ||
await setpoint_v.set(value) | ||
|
||
# The set should always change to SLEW regardless of whether we are | ||
# already at the set point, then change back to OK/FAIL depending on | ||
# success | ||
accepted_value = await anext(demand_accepted_iterator) |
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: Sorry, I'm wondering if there is another race condition here where if we get an update straight after the set we may still see an OK? I think a better solution might be not to discard the value before the set but to keep reading until the updates show slew
?
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 did look at the code in ophyd_async before to confirm that we will always get the current value in the first event, so I don't think that there is any possibility of this race condition and we should be safe.
observe_value
's implementation calls signal.subscribe_value(q.put_nowait)
before awaiting new values.
signal.subscribe()
calls signal_notify()
which calls our q.put_nowait()
- this posts the event on to the queue which is then collected by the subsequent await
.
If this implementation ever changes it will probably break calling code as anything that follows this model under this assumption will end up blocking waiting for an event that never comes.
I'm also slightly confused where you think the extra event will come from - we've already validated that the demand will be OK
prior to entering the function and we should be the only ones applying changes. The discard from the first anext()
is before the set, so there is no way that the discarded value can be "SLEW" or a subsequent "OK", even if the current value wasn't pushed by the call to observe
.
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.
Tom's comment #636 (comment) implies that it is possible we get multiple updates with the same value. It depends quite a bit on how the IOC is written, for example it's possible that the PV will "send out" an update on a periodic basis regardless of if the value has changed or not. I think you're right that I'm being overly cautious though, looking at the PV it seems that it should be stable.
@@ -45,27 +56,36 @@ def vfm_mirror_voltages_with_set_accepted_fail( | |||
|
|||
|
|||
def vfm_mirror_voltages_with_set_to_value( | |||
vfm_mirror_voltages, new_value: MirrorVoltageDemand | |||
vfm_mirror_voltages, new_value: MirrorVoltageDemand, spins: int = 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.
Could: spins
isn't that obvious a name. Maybe number_of_slew_updates
?
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.
Great, thank you! Sorry for the back and forth, took a while to get my head round all the potential races.
await anext(demand_accepted_iterator) | ||
await setpoint_v.set(value) | ||
|
||
# The set should always change to SLEW regardless of whether we are | ||
# already at the set point, then change back to OK/FAIL depending on | ||
# success | ||
accepted_value = await anext(demand_accepted_iterator) |
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.
Tom's comment #636 (comment) implies that it is possible we get multiple updates with the same value. It depends quite a bit on how the IOC is written, for example it's possible that the PV will "send out" an update on a periodic basis regardless of if the value has changed or not. I think you're right that I'm being overly cautious though, looking at the PV it seems that it should be stable.
This converts the VFMMirrorVoltages device to use ophyd-async
Fixes #604
See also corresponding hyperion change TBD
Instructions to reviewer on how to test:
Checks for reviewer
dodal connect ${BEAMLINE}