Skip to content

Conversation

@jacob720
Copy link
Contributor

@jacob720 jacob720 commented Nov 12, 2025

Fixes DiamondLightSource/mx-bluesky#1424

Required by DiamondLightSource/mx-bluesky#1445

Adds beamsize devices for i03 and i04 so that the beamsize be read correctly in both cases, using the transfocator for i04 and the aperture for i03.

Instructions to reviewer on how to test:

  1. Do thing x
  2. Confirm thing y happens

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}

@jacob720 jacob720 requested a review from a team as a code owner November 12, 2025 14:19
@jacob720 jacob720 force-pushed the mx_bluesky_1424_put_correct_beamsize branch from a645d65 to 76f53b0 Compare November 12, 2025 14:24
@codecov
Copy link

codecov bot commented Nov 12, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.08%. Comparing base (c719bfc) to head (a58c05d).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff            @@
##             main    #1704    +/-   ##
========================================
  Coverage   99.07%   99.08%            
========================================
  Files         271      278     +7     
  Lines        9971    10140   +169     
========================================
+ Hits         9879    10047   +168     
- Misses         92       93     +1     

☔ View full report in Codecov by Sentry.
📢 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.



class Beamsize(BeamsizeBase):
def __init__(self, aperture_scatterguard: ApertureScatterguard, name="beamsize"):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is it better to add a name when you instantiate the device and leave this as name=""?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I think leave it as "" default

@jacob720 jacob720 force-pushed the mx_bluesky_1424_put_correct_beamsize branch 2 times, most recently from 9b06c0e to 28f718c Compare November 13, 2025 10:47
@jacob720 jacob720 force-pushed the mx_bluesky_1424_put_correct_beamsize branch from 28f718c to 2a57c7b Compare November 13, 2025 10:51
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, thanks. This works (mostly) but I wonder if we could make something more generic where the Beamsize takes a list of signals/constants and just finds the minimum e.g. something like:

class Beamsize(BeamsizeBase):
    def __init__(
        self,
        x_signals:list[SignalR|float],
        y_signals:list[SignalR|float],
    ):
        super().__init__(name=name)

        self.x_um = derived_signal_r(
            self._get_beamsize_x,
            x_signals=x_signals,
            derived_units="µm",
        )
        self.y_um = derived_signal_r(
            self._get_beamsize_y,
            y_signals=y_signals,
            derived_units="µm",
        )

    def _get_beamsize_x(
        self,
        x_signals: list[float],
    ) -> float:
        return min(*x_signals)

    def _get_beamsize_y(
        self,
        y_signals: list[float],
    ) -> float:
        return min(*y_signals)

This won't work exactly like that because derived_signal won't play nice with lists of signals, especially where one of those things might not be a signal but a constant. We could maybe get round the constant thing by making a dummy soft signal that just has the constant value though. I think it's worth playing with this though and if it isn't obvious maybe asking in #ophy-async how it would be done


@device_factory()
def beamsize() -> Beamsize:
"""Get the i03 beamstop device, instantiate it if it hasn't already been.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should:

Suggested change
"""Get the i03 beamstop device, instantiate it if it hasn't already been.
"""Get the i03 beamsize device, instantiate it if it hasn't already been.

These comments are kind of rubbish anyway but let's make them correct rubbish


@device_factory()
def beamsize() -> Beamsize:
"""Get the i03 beamstop device, instantiate it if it hasn't already been.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should: As above

Suggested change
"""Get the i03 beamstop device, instantiate it if it hasn't already been.
"""Get the i03 beamsize device, instantiate it if it hasn't already been.


@device_factory()
def beamsize() -> Beamsize:
"""Get the i04 beamstop device, instantiate it if it hasn't already been.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should: As above

Suggested change
"""Get the i04 beamstop device, instantiate it if it hasn't already been.
"""Get the i04 beamsize device, instantiate it if it hasn't already been.



class Beamsize(BeamsizeBase):
def __init__(self, aperture_scatterguard: ApertureScatterguard, name="beamsize"):
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I think leave it as "" default

Comment on lines 8 to 9
def __init__(self, name="beamsize"):
super().__init__(name=name)
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 we can just remove the constructor all together and it will pass the value through.


self.x_um = derived_signal_r(
self._get_beamsize_x,
aperture_radius=self._aperture_scatterguard_ref().radius,
Copy link
Contributor

Choose a reason for hiding this comment

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

It's only in reading this that I've realised that this isn't the radius at all, it's the diameter. Would you be able to write an issue to fix it in the ApertureScattergaurd and downstream?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah that completely went over my head too lol

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(ApertureValue.PARKED, (0, 0, 0), (0.0, 0.0)),
],
)
async def test_beamsize_gives_min_of_aperture_and_beam_width_and_height(
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 this is testing more of the internal logic of the aperture than you need. You should be able to just do set_mock_value(aperture.radius, 10) then test that value is used. Tests that selecting specific apertures changes the radius should already exist in other places. If you can't do this because you can't do set_mock_value on a derived signal then we should make an ophyd_async issue and instead do ap_sg.aperture._get_current_radius = MagicMock(return_value=10)

Copy link
Contributor Author

@jacob720 jacob720 Nov 13, 2025

Choose a reason for hiding this comment

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

Yeah I tried that initially but it doesn't work with derived signals. I'll raise an issue and mock _get_current_radius for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(ApertureValue.PARKED, (0, 0, 0), (120.0, 120.0), (0.0, 0.0)),
],
)
async def test_beamsize_gives_min_of_aperture_and_transfocator_width_and_height(
Copy link
Contributor

Choose a reason for hiding this comment

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

Should: As on the other test re internal device logic

Comment on lines 19 to 20
(ApertureValue.OUT_OF_BEAM, (0, 0, 0), (0.0, 0.0)),
(ApertureValue.PARKED, (0, 0, 0), (0.0, 0.0)),
Copy link
Contributor

Choose a reason for hiding this comment

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

Must: I think the logic here is actually wrong. When we're out of the beam there is no aperture so the beamsize should be the default value. I think a good solution to this is to change the aperture device so that it's radius at these positions is infinite

Comment on lines 5 to 6
x_um: SignalR[float]
y_um: SignalR[float]
Copy link
Contributor

Choose a reason for hiding this comment

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

Should: These should be hinted as Format.HINTED_SIGNAL. This will mean that they will get read when you read the whole device.

@DominicOram
Copy link
Contributor

This won't work exactly like that because derived_signal won't play nice with lists of signals

https://blueskyproject.io/ophyd-async/main/how-to/derive-one-signal-from-others.html#multi-derived-signal might let you do it. Needs some investigation

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.

I04 XRC: Put beamsize in ispyb correctly

3 participants