Skip to content

Simplify ElementHolder API: one accessor per element family (merge magnet/magnets, bpm/bpms, …) #468

Description

@GamelinAl

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 element

Proposal

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:

  1. get(name) always returns one element, and get_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).

  2. [key] is the interactive shortcut: a literal name gives the element, a pattern gives a typed array. One rule everywhere.

  3. Element names and family names live in separate namespaces (get/[] for elements, get_array/attribute for families), so they never clash.

  4. The same get / get_array convention applies at the root of the holder. Today sr.design.get(name) returns an array (ElementHolder.get), while rf.get(name), tool.get(name) and diagnostic.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 name

I think that what is done on the Element/Array level is enough to have a clear difference.

  • BPMArray gains offset / tilt and BPM gains h / 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

  1. added theissue type on Oct 9, 2026
  2. JeanLucPons commented on Oct 9, 2026

    @JeanLucPons
    Member

    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.
    strengths tell, as is the example below, that sextu is an array.

            k = sextu.strengths.get()
            k += self.correct(dchroma)
            sextu.strengths.set(k)

    To follow pyAML style, i would prefer:

      sextu = sr.design.magnet.arrays.get(self.sextu_array_name)
  3. GamelinAl commented on Oct 9, 2026

    @GamelinAl
    MemberAuthor

    3. 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.
    strengths tell, as is the example below, that sextu is an array.
    k = sextu.strengths.get()
    k += self.correct(dchroma)
    sextu.strengths.set(k)

    To follow pyAML style, i would prefer:
      sextu = sr.design.magnet.arrays.get(self.sextu_array_name)
    

    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?

  4. gubaidulinvadim commented on Oct 9, 2026

    @gubaidulinvadim
    Member

    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.bpms you would have sr.design.bpm_array.

  5. JeanLucPons commented on Oct 9, 2026

    @JeanLucPons
    Member

    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 a bpm_array field instead of bpms.
    It is subjective.

  6. TeresiaOlsson commented on Oct 9, 2026

    @TeresiaOlsson
    Member

    I think an element and an array should have the same interface. Having magnet and magnets is very confusing and so is having strength and strengths. 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.

  7. TeresiaOlsson commented on Oct 9, 2026

    @TeresiaOlsson
    Member

    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 just sr.design.magnet.get() or optionally something like sr.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 strengths would 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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions