Skip to content

Beam attenuation#35

Open
Dan Henriksen (dihenriksen) wants to merge 34 commits into
mainfrom
beam-attenuation
Open

Beam attenuation#35
Dan Henriksen (dihenriksen) wants to merge 34 commits into
mainfrom
beam-attenuation

Conversation

@dihenriksen
Copy link
Copy Markdown
Contributor

@dihenriksen Dan Henriksen (dihenriksen) commented May 8, 2026

Mainly, this PR creates the ability to attenuate a beam by giving control over a set of attenuators:

  • Made AttenuatorBank, Attenuator, and other helper classes
  • Implemented movable protocol for those classes to control individual attenuators, and to set total attenuation
  • Tests for attenuation functionality

Also fixed various other issues:

  • Ignore test_eiger_async tests for now since they are very broken
  • Update ophyd-async version to match what is in cdi-profile-collection
  • Fix distribution build step
  • Fix style problems
  • Remove python 3.10 from supported python versions due to compatibility issues with the latest version of ophyd-async

@dihenriksen Dan Henriksen (dihenriksen) marked this pull request as ready for review May 12, 2026 02:55
Copy link
Copy Markdown

@jwlodek Jakub Wlodek (jwlodek) left a comment

Choose a reason for hiding this comment

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

Marked some suggestions. Mainly I'd recommend making both device classes Movables and re-work the current methods for changing states into a set method override.

Comment thread src/cditools/attenuator.py Outdated
Comment thread src/cditools/attenuator.py Outdated
Comment thread src/cditools/attenuator.py Outdated
Comment thread src/cditools/attenuator.py Outdated
Comment thread src/cditools/attenuator.py Outdated
Comment thread .github/workflows/cd.yml
fetch-depth: 0
persist-credentials: false

- uses: hynek/build-and-inspect-python-package@c52c3a4710070b50470d903818a7b25115dcd076 # v2.13.0
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.

Please put the SHA pinning back.

return 1 - self.transmission


THICKNESSES = (16, 24, 66, 124) # microns
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 would make each of these a combination of (material, thickness), the generalization is pretty easy to do up front.

class Attenuator(EpicsDevice, AsyncMovable[AttenuatorStatusEnum]):
filter_material = xu.materials.Al

def __init__(self, prefix: str, num: int, thickness: int):
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 would type this as float.


def __init__(self, prefix: str, num: int, thickness: int):
"""
prefix - the common prefix for the attenuator bank
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.

Please use numpydoc conventions

Comment on lines +126 to +127
i: Attenuator(self.prefix, i, self.thicknesses[i - 1])
for i in range(1, len(self.thicknesses) + 1)
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.

Suggested change
i: Attenuator(self.prefix, i, self.thicknesses[i - 1])
for i in range(1, len(self.thicknesses) + 1)
i: Attenuator(self.prefix, i, thickness)
for i, thickness in enumerate(self.thicknesses, start=1)

Slightly more idiomatic Python


@property
def photon_energy(self):
return self.energy.energy.readback.get()
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.

after a lot of back-and-forth we have avoided wrapping epics I/O in properties. The main arguments against are that it violates the expectation that properties are attributes which are fast (not arbitrarily long) and is not uniform across other devices.

def egu(self):
return self.energy.egu

async def get_status(self): # type: ignore[reportUnknownParameterType]
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 this be async def read ? That way we can toss this in a baseline list and it will "just work".

"""
status = {}
active_attens = []
en = self.photon_energy
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 are already in an async function here so

Suggested change
en = self.photon_energy
en = await self.energy.readback.value()

I may be wrong about the exact name, ophyd-async has a verb for "get me the one true number for this or explode".


def _calculate_total_transmission(self, *attenuators: Attenuator) -> float:
transmissions = [
a.transmission(self.photon_energy, self.egu) for a in attenuators
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.

rather than get the energy here, pass it in. This would both open up a path to having an extra API of "what is the best combination if I changed to a different energy" without having to actually change the energy.

In both set/read you are in an async function so can do the read and push it down.

diff = float("inf")
new_diff = abs(target_transmission - atten)
inc = 1 if target_transmission > atten else -1

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.

best_inx = np.argmax([_.transmission for _ in available_attenuations])

or np.insert_sorted or https://docs.python.org/3/library/bisect.html

As there are physical attenuator banks we are always going to be in the realm of small numbers and my expectation on the relative runtimes is that computing the transmission is >> more expensive that this search.

To some degree, the sort in _calculate_available_transmissions is a false economy as that is at best O(n * log(n)), but if we just need to find the max we can do that with on O(n) scan (which given that n here is < 256 it it all a bit moot!).

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.

3 participants