Repository navigation
Simplify ElementHolder API: one accessor per element family (merge magnet/magnets, bpm/bpms, …) #468
Description
Here is my proposal to simplify further the API to access elements after #430
@JeanLucPons @TeresiaOlsson @gupichon-soleil @gubaidulinvadim @kparasch
Context
Follow-up of #430 / #199. After #430 every element family has two accessors:
| family | single | array |
|---|---|---|
| magnets | sr.design.magnet |
sr.design.magnets |
| combined-function | combined_function_magnet |
combined_function_magnets |
| serialized | serialized_magnet |
serialized_magnets |
| BPMs | diagnostic.bpm |
diagnostic.bpms |
Before #430 the split made sense: the singular accessor looked up one element and the plural one looked up families. Since #430, the plural accessor can do almost everything the singular one does, so having both is redundant. It is also confusing: users have to remember singular vs plural, and some first thought one of them was a typo.
Results are also inconsistent:
sr.design["QF1E-C04"] # Quadrupole
sr.design.magnet["QF1E-C04"] # Quadrupole
sr.design.magnets["QF1E-C04"] # MagnetArray with 1 elementProposal
Keep one accessor per family, named after the type, in the singular: magnet, combined_function_magnet,
serialized_magnet, diagnostic.bpm. Remove the plural ones.
The accessor has a small, explicit set of methods. The method you call decides what comes back, not whether
the name is singular or plural:
m = sr.design.magnet.get("QF1E-C04") # -> Magnet: exactly one element, raises if unknown
fam = sr.design.magnet.get_array("QuadForTune") # -> MagnetArray: configured family, raises if unknown
fam = sr.design.magnet.QuadForTune # -> MagnetArray: same, attribute + tab completion
all = sr.design.magnet.get_array() # -> MagnetArray of every magnet (replaces .all())
sr.design.magnet["QF1E-C04"] # -> Magnet (literal name, same as sr.design[...])
sr.design.magnet["QF1*"] # -> MagnetArray (wildcard / re: / list / ~)Rules:
-
get(name)always returns one element, andget_array(name)/ attribute access always return a configured array. This is the way we access element/arrays in pyAML code internally. Each has one precise return type, so static typing and IDE autocompletion stay accurate (this answers
@JeanLucPons's concern about generic signatures). -
[key]is the interactive shortcut: a literal name gives the element, a pattern gives a typed array. One rule everywhere. -
Element names and family names live in separate namespaces (
get/[]for elements,get_array/attribute for families), so they never clash. -
The same
get/get_arrayconvention applies at the root of the holder. Todaysr.design.get(name)returns an array (ElementHolder.get), whilerf.get(name),tool.get(name)anddiagnostic.get(name)already return a single element. The root is the odd one out, so we align it:sr.design.get("QF1E-C04") # -> Element: exactly one element, raises if unknown sr.design.get_array("CELL08") # -> ElementArray (or the most specific array type) by array name sr.design.get_array() # -> ElementArray of every element (today: sr.design.get())
After this, get means "one element" and get_array means "an array" everywhere in the API.
The alternative proposal is to just use ["key"] for everything but I think the concern of @JeanLucPons are justified. At least internally, in the pyAML codebase, it's very good to have exact ways to get element/array separated.
Same problem one level down: strength vs strengths
The value accessors have the same singular/plural problem, and the plural is not used consistently:
| object | accessors | get() returns |
|---|---|---|
Magnet |
strength, hardware |
scalar |
CombinedFunctionMagnet (one element) |
strengths, hardwares |
vector (one per multipole) |
SerializedMagnets (group of N magnets) |
strength, hardware |
family value (see #461) |
MagnetArray, CombinedFunctionMagnetArray, SerializedMagnetsArray |
strengths, hardwares |
vector |
BPM |
positions, offset, tilt |
[x, y], [x, y], scalar |
BPMArray |
positions, h, v (no offset / tilt) |
vectors |
A single CFM uses the plural but a serialized group uses the singular, and offset is singular but positions
is plural.
Proposal: use the same attribute name on an element and on an array. Whether get() returns a scalar or a vector depends on the object, not on the attribute name:
| object | accessors | get() returns |
|---|---|---|
Magnet |
strength, hardware |
scalar |
CombinedFunctionMagnet |
strength, hardware |
vector (one per multipole) |
SerializedMagnets |
strength, hardware |
to be decided in #461 |
| any magnet array | strength, hardware |
vector |
BPM / BPMArray |
position, offset, tilt, plus h / v on both |
vector |
sr.design.magnet["QF1E-C04"].strength.get() # scalar
sr.design.magnet["QF1*"].strength.get() # vector, same attribute nameI think that what is done on the Element/Array level is enough to have a clear difference.
BPMArraygainsoffset/tiltandBPMgainsh/v, so the same code works on one BPM or on an array.
Implementation
These two topics can be covered in two sub-issues and their related PR.
We could merge GenericElementHolder and GenericArrayHolder (pyaml/common/holders/) into a single GenericHolder, and merge each XHolder/XsHolder pair in sub_holders.py into one class. Then update the internal callers (tuning tools: peer.magnets.get(...) → peer.magnet.get_array(...)), the tests, the examples/notebooks and the docs.
At the root, ElementHolder.get(name) returns self._get_element(name), and _get_array becomes the public get_array(name=None).
Activity
JeanLucPons commentedon Oct 9, 2026
Personally I would prefer to keep the 's' and update bpm interface using same rules.
For instance we will always work with variables that won't tell you if you work with a vector or not.
strengthstell, as is the example below, that sextu is an array.To follow pyAML style, i would prefer:
GamelinAl commentedon Oct 9, 2026
For me it's not great but I can live with it if you think it's neccessary.
Do you agree on the 1st part of the issue?
gubaidulinvadim commentedon Oct 9, 2026
I think it's OK to have both ways to access things; it's more of a question of taste. So either one is fine for me.
If 's' is too confusing and too typo-friendly, maybe it can be renamed to array? So, instead of
sr.design.bpmsyou would havesr.design.bpm_array.JeanLucPons commentedon Oct 9, 2026
It is just my opinion. If you want to have strength without the 's' for array it is ok. To me having 's' make the code more readable as you immediately see that it is a family.
Concerning point 1, I would just like to avoid the
get_...()but why not keeping it as you propose or as Vadim using abpm_arrayfield instead of bpms.It is subjective.
TeresiaOlsson commentedon Oct 9, 2026
I think an element and an array should have the same interface. Having
magnetandmagnetsis very confusing and so is havingstrengthandstrengths. I don't think that is a good user interface. I think a user shouldn't need to know if something is a single device or an array because that is to some extent a configuration choice. They should be able to use them in the same way.TeresiaOlsson commentedon Oct 9, 2026
But I think it also relates to the interface we are still missing to be able to get/set independently of unit. The users are not supposed to do
sr.design.magnet.strength.get()but justsr.design.magnet.get()or optionally something likesr.design.magnet.get('physics')in case they want to specify another view than the one that has been set as default. Without that interface it's not possible to write applications that can be used by all machines.If that's the interface I'm not sure why
strengthswould be needed? Then an array only need to call the underlying get on the elements in the array and I don't think there is any need for it to define it's own strength attribute?GamelinAl commentedon Oct 12, 2026
I don't really like this, IMO it does not resolve the issue raised here which is to have two accessors to nearly do the same job.
I agree it's kind of subjective but we still need to agree on something to be able to move on :) ...
I agree.
That's another discussion I would say.
JeanLucPons commentedon Oct 12, 2026
An array and a single element do not have the same interface.
If you make to make all in one accessor (possible in python), you will loose informations and make user code less readable.
I agree to have
.get_array()or.array.get()or.array()or .... to keep correct typing instead of.magnets. This will a bit longer to write but correct typing will be preserved.TeresiaOlsson commentedon Oct 12, 2026
I thought it was part of the specification that they should have the same interface. But maybe that requirement was lost.
JeanLucPons commentedon Oct 12, 2026
This is not possible. For instance you have the possibility to access a subgroup of an array, which do not make sense for a single element.
However it is possible to call both method
.strenght()for array and and single element if they have different accessor. This will not loose the typing but here it is a bit subjective. To me having thesmakes the code more readable.TeresiaOlsson commentedon Oct 12, 2026
What I meant is just that the
get,setetc interface should be the same independently of it's a single element or an array. Then the user doesn't have to know if something is an array or a single element when reading or setting it because the syntax is the same.JeanLucPons commentedon Oct 12, 2026
Yes I understand your point and it differs from mine, in the sense of code readability.
If i get a simple example in Tango we have:
DeviceProxy.readd_attribute(...) to read one single attribute and DeviceProxy.read_attributes(...) to read a set of attributes.
We could have chosen to use the same name for both methods with a difference in signature (and in exception handling).
TeresiaOlsson commentedon Oct 12, 2026
Okay. Then it maybe depends on what the users are used to. Since our users use MML, pyepics and ophyd-async they are used to having the same interface to read and set but that there might be different data formats.