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

Convert mirror voltage devices to use ophyd async #636

Merged
merged 7 commits into from
Jul 11, 2024

Conversation

rtuck99
Copy link
Contributor

@rtuck99 rtuck99 commented Jun 18, 2024

This converts the VFMMirrorVoltages device to use ophyd-async
Fixes #604

See also corresponding hyperion change TBD

Instructions to reviewer on how to test:

  1. Tests pass
  2. mirror voltage devices connect and experiment plans continue to work

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
  • If changing the API for a pre-existing device, ensure that any beamlines using this device have updated their Bluesky plans accordingly
  • Have the connection tests for the relevant beamline(s) been run via dodal connect ${BEAMLINE}

@rtuck99 rtuck99 changed the title 604 ophyd async device for mirror voltages 604 convert mirror voltage devices to use ophyd async Jun 18, 2024
@rtuck99 rtuck99 marked this pull request as ready for review June 18, 2024 14:42
Comment on lines 80 to 99
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
Copy link
Contributor

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

@@ -74,16 +73,10 @@ def pytest_runtest_teardown():

@pytest.fixture
def vfm_mirror_voltages() -> VFMMirrorVoltages:
Copy link
Contributor

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

Copy link

codecov bot commented Jun 19, 2024

Codecov Report

Attention: Patch coverage is 94.11765% with 2 lines in your changes missing coverage. Please review.

Project coverage is 93.43%. Comparing base (76be228) to head (e306c61).
Report is 35 commits behind head on main.

Current head e306c61 differs from pull request most recent head e3af2a7

Please upload reports for the commit e3af2a7 to get more accurate results.

Files Patch % Lines
src/dodal/devices/focusing_mirror.py 94.11% 2 Missing ⚠️
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.
📢 Have feedback on the report? Share it here.

@rtuck99 rtuck99 requested a review from DominicOram June 20, 2024 11:57
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.

Thank you, I think this is a lot cleaner. Some comments in code

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)
Copy link
Contributor

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?

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 think it would be good to have a comment on why we're starting the observe early e.g. the race condition stuff

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:
Copy link
Contributor

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

Comment on lines 93 to 94
while MirrorVoltageDemand.SLEW == (accepted_value := await anext(it_values)):
pass
Copy link
Contributor

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?

Copy link
Collaborator

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

Copy link
Contributor

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
Copy link
Contributor

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(
Copy link
Contributor

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

@rtuck99 rtuck99 requested a review from DominicOram June 27, 2024 09:43
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.

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.

Comment on lines +85 to +91
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)
Copy link
Contributor

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?

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 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.

Copy link
Contributor

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
Copy link
Contributor

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?

@rtuck99 rtuck99 requested a review from DominicOram July 2, 2024 14:56
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! Sorry for the back and forth, took a while to get my head round all the potential races.

Comment on lines +85 to +91
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)
Copy link
Contributor

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.

@DominicOram DominicOram changed the title 604 convert mirror voltage devices to use ophyd async Convert mirror voltage devices to use ophyd async Jul 11, 2024
@DominicOram DominicOram merged commit 555f696 into main Jul 11, 2024
11 checks passed
@DominicOram DominicOram deleted the 604_ophyd_async_device_for_mirror_voltages branch July 11, 2024 16: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.

Ophyd-async device for mirror voltages
3 participants