Skip to content

Switch back to pixi, update ophyd-async version & fix phantom ophyd class, add ophyd classes for monochromater and filters, add change energy plan" - #76

Open
Jakub Wlodek (jwlodek) wants to merge 9 commits into
NSLS2:mainfrom
jwlodek:pyepics-migration

Conversation

@jwlodek

Copy link
Copy Markdown
Contributor
  • Update gitignore
  • Add motors module remove unused files
  • Switch back to pixi from uv. Fix phantom ophyd object with new ophyd-async version. Add classes for Filters and Monochromater. Add plan for changing energies, with auto-tuning of the second crystal pitch

@sligara7 Anthony Sligar (sligara7) left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks Jakub — the change-energy plan is great: the coarse→fine camera-feedback auto-tune of the crystal-2 pitch is exactly the compensation the mono needs for the encoder/rail imperfections, and the approach-from-below for backlash plus waiting on the shutter position status (not the command status) are both real improvements over the pyepics lib. Inline comments below; the two biggest are the ophyd-async version story (0.21 here vs 0.19.4 in hex-ob and the beamline profile env) and making sure the phantom fixes that were just validated against the simulated beamline in hex-ob PR #12 survive the API migration. CI reds understood as the in-flight pixi move.

Comment thread pixi.toml
rich = ">=15.0.0,<16"
pyyaml = ">=6.0.3,<7"
numpy = ">=2.4.6,<3"
ophyd-async = ">=0.21.1,<0.22"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This pin moves hextools to ophyd-async 0.21, but hex-ob and the beamline profile environment are on 0.19.4 — and the phantom device that just merged into hex-ob (PR #12, validated end-to-end against the simulated beamline) speaks the 0.19 API. Merging this as-is forks the phantom across two API generations.

What's the version story — does the beamline env move to 0.21 (and on what timeline relative to the October user-ready date), or should the phantom migration split out of this PR so the mono/filters work isn't blocked on that decision?

self.driver = driver

async def arm(self):
async def start_acquiring(self):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This migration starts from the pre-hex-ob version of the phantom, so two fixes that were found against the simulated beamline (hex-ob PR #12) don't appear here:

  1. the external-trigger deadlock chain fix — busy-record arm handling in the acquire path (without it, an externally-triggered tomo flyscan deadlocks; it reproduced identically in sim and would at the beamline);
  2. the download-watch race fix in the RAM-download flow.

It's possible the 0.21 acquire-logic contract absorbs part of (1) — but that needs to be shown rather than assumed. Suggest porting the hex-ob lib/phantom.py (fixes included) forward to the 0.21 API, rather than migrating the pre-fix version, so the sim-validated behavior is the thing that survives.

await asyncio.gather(*coros)


def change_energy(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

energy: float = 0.0 is a footgun — calling with the default hits np.arcsin(1.977/0.0). Suggest making energy required, and validating energy > 1.977 (arcsin domain for Si(111)) with a clear error before any motor moves.


# Create a PeakStats object to monitor the fluorescence screen camera signal
# and find the position of the crystal 2 pitch that produces a peak.
ps = PeakStats(dclm.xtal2_pitch.name, f"{fs_camera.name}_stats1_mean")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

f"{fs_camera.name}_stats1_mean" is the ophyd-v1 naming convention — ophyd-async data keys are dash-separated (<name>-stats1-mean), so as written PeakStats never sees the signal, ps.com stays None, and the bps.mv(dclm.xtal2_pitch, ps.com) below fails cryptically.

Two asks: derive the key from the device rather than hardcoding the string, and guard the ps.com is None case (no peak found — e.g. no beam, screen out) with a clear error before any move.

await wait_for_value(self.in_position, True, timeout=10)


class Slits(StandardReadable, EpicsDevice):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The slit motors are created after super().__init__ with bare suffix strings ("I}Mtr" etc.) — do they actually inherit the device prefix? If child prefix propagation only happens via PvSuffix annotations, these connect to literal PV names like I}Mtr. A test that connects one axis against the mock backend would settle it either way.

)
self.xtal1_pitch = AsyncEpicsMotor(prefix + "C1P}Mtr")
self.xtal2_pitch = AsyncEpicsMotor(prefix + "C2P}Mtr")
self.flourescence_screen = AsyncEpicsMotor(prefix + "FS}Mtr")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Typo that's about to become public API surface: flourescence_screenfluorescence_screen (also referenced from the plan below). Cheap to fix now, breaking to fix later.

return closest[1].description or closest[0].name.lower().replace("_", " ")

@AsyncStatus.wrap
async def set(self, value: FilterPosition):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

self.positions[value] raises a bare KeyError if someone passes FilterPosition.UPPER_LIMIT/LOWER_LIMIT (or any position not in the dict). Suggest either excluding the limit markers from the settable enum or raising a descriptive error listing the valid positions.

return BeamMode.WHITE
return BeamMode.MONOCHROMATIC

def _get_energy(self, pitch_angle: float) -> float:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The Si(111) constants (35.2544 offset, 1.977 = hc/2d) are duplicated here and in change_energy. Worth hoisting to module-level named constants so the energy readback and the plan can't drift apart.

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.

2 participants