Skip to content

Update id logic to use standard movable - #2161

Open
oliwenmandiamond wants to merge 14 commits into
mainfrom
update_id_logic_to_use_standard_movable
Open

Update id logic to use standard movable#2161
oliwenmandiamond wants to merge 14 commits into
mainfrom
update_id_logic_to_use_standard_movable

Conversation

@oliwenmandiamond

@oliwenmandiamond oliwenmandiamond commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #2158

This fixes the insertion device logic to be compatible with StandardMovable, specifically the UndulatorGap. This has also restructured the ID logic so that we now these new files:

  • apple2_undulator_phases.py
  • apple2_undulator_gap.py
  • apple2_undulator_base.py
  • apple2_undulaltor_motors.py
  • apple2_undulator.py

Other notable change is MotorStringSetpoint which allows us to still use Motor which have setpoints as str still take floats using a derived signal, allowing the ID logic to not break when using MotorMovableLogic (soon to be MotorFlyableMoveLogic).

This has made the logic much more modular and easier to understand. Please note I have not altered the actual logic or the tests so this hasn't changed the ID behaviour. This was necessary because Motor has changed to use StandardMovable (and in ophyd-async 0.21.1 StandardFlyable) which made the current API conflicting as it was overriding methods it shouldn't of. This will make the migration to ophyd-async 0.21.1 much easier to manage. I've also added more documentation to classes to make it easier for someone else to maintain in the future.

Instructions to reviewer on how to test:

  1. Check refactor makes sense
  2. Tests pass
  3. dodal connect works

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}

@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.55556% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 99.15%. Comparing base (f19cb4a) to head (145b592).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
...l/devices/insertion_device/apple2_undulator_gap.py 98.46% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2161      +/-   ##
==========================================
- Coverage   99.16%   99.15%   -0.01%     
==========================================
  Files         352      356       +4     
  Lines       13824    13871      +47     
==========================================
+ Hits        13708    13754      +46     
- Misses        116      117       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@oliwenmandiamond
oliwenmandiamond marked this pull request as ready for review August 6, 2026 09:59
@oliwenmandiamond
oliwenmandiamond requested a review from a team as a code owner August 6, 2026 09:59

@Relm-Arrowny Relm-Arrowny left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

look good thanks for doing all the work.

Comment on lines +54 to +55
await set_move.set(value=1, timeout=timeout)
await wait_for_value(gate, UndulatorGateStatus.CLOSE, timeout=timeout)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We probably have not test it with the real system enough, should add to avoid racing condition. :

Suggested change
await set_move.set(value=1, timeout=timeout)
await wait_for_value(gate, UndulatorGateStatus.CLOSE, timeout=timeout)
await set_move.set(value=1, timeout=timeout)
await wait_for_value(gate, UndulatorGateStatus.OPEN, timeout=5)
await wait_for_value(gate, UndulatorGateStatus.CLOSE, timeout=timeout)


def __init__(self, prefix: str, name: str = ""):
super().__init__(prefix=prefix, name=name)
self.set_move = epics_signal_rw(int, prefix + "BLGSETP")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: we made set move SignalW through out so I am not sure which we want to change. I probably does not matter in practice.

Suggested change
self.set_move = epics_signal_rw(int, prefix + "BLGSETP")
self.set_move = epics_signal_w(int, prefix + "BLGSETP")

btm_inner: str,
name: str = "",
):
# Gap demand set point and readback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could:Not gap

Suggested change
# Gap demand set point and readback

self.top_outer = UndulatorPhaseMotor(prefix=f"{prefix}BL{top_outer}")
self.btm_inner = UndulatorPhaseMotor(prefix=f"{prefix}BL{btm_inner}")
# Nothing move until this is set to 1 and it will return to 0 when done.
self.set_move = epics_signal_rw(int, f"{prefix}BL{top_outer}" + "MOVE")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit:?

Suggested change
self.set_move = epics_signal_rw(int, f"{prefix}BL{top_outer}" + "MOVE")
self.set_move = epics_signal_w(int, f"{prefix}BL{top_outer}" + "MOVE")

def __init__(self, prefix: str, name: str = ""):
motor_pv = f"{prefix}MTR"
super().__init__(motor_pv, prefix + "SET", name=name)
del self.motor_stop

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should: matching gap and its safer.

Suggested change
del self.motor_stop
self.motor_stop = None

Comment on lines +128 to +170
class UndulatorGap(SafeUndulatorMoverBase[float], Flyable, Preparable):
"""Apple2 undulator gap device.

Wraps an internal :class:`UndulatorGapMotor` and exposes the Apple2
controller interface, where demand positions are written separately from
triggering motion via a dedicated move PV.

Supports standard moves as well as fly scanning.

Args:
prefix: Beamline-specific PV prefix.
name: Device name.
"""

def __init__(self, prefix: str, name: str = ""):
super().__init__(prefix=prefix, name=name)
self.set_move = epics_signal_rw(int, prefix + "BLGSETP")
with self.add_children_as_readables():
self.motor = UndulatorGapMotor(
prefix, self.gate, self.status, self.set_move
)

async def set_demand_positions(self, value: float) -> None:
await self.motor.user_setpoint.set(value)

async def get_timeout(self) -> float:
return await estimate_motor_timeout_from_signals(
self.motor.user_setpoint, self.motor.user_readback, self.motor.velocity
)

@AsyncStatus.wrap
async def prepare(self, value: FlyMotorInfo) -> None:
"""Prepare for a fly scan by moving to the run-up position at max velocity.
Stores fly info for later use in kickoff.
"""
await self.motor.prepare(value)

@AsyncStatus.wrap
async def kickoff(self):
await self.motor.kickoff()

def complete(self) -> WatchableAsyncStatus:
return self.motor.complete()

@Relm-Arrowny Relm-Arrowny Aug 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the safe SafeUndulatorMoverBase is already built in the UndulatorGapMotor
this: #2167

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.

Update insertion device devices to user StandardMovable and StandardFlyer

2 participants