Skip to content

Serialized magnets: mixed-function families, strengths/hardwares arrays and partial series writes - #485

Open
gupichon-soleil wants to merge 3 commits into
461-serializedmagnetsfrom
473-serialized-magnets-implement-the-agreed-accessor-convention-in-design-and-live_v2
Open

gupichon-soleil wants to merge 3 commits into
461-serializedmagnetsfrom
473-serialized-magnets-implement-the-agreed-accessor-convention-in-design-and-live_v2

Conversation

@gupichon-soleil

@gupichon-soleil gupichon-soleil commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Description

Follow-up to #474 on the serialized-magnet accessors. It adds three things on top of the agreed accessor convention:

  • A serialized family can mix magnet functions (e.g. quadrupoles and sextupoles on one power supply): function accepts one value per element.
  • family.strengths / family.hardwares read or write every magnet of the series in one call, like CombinedFunctionMagnet.strengths / .hardwares.
  • An array write that only covers part of a series drives the power supply from the requested magnets only, and the other magnets of the series follow the resulting current.

A series has a single degree of freedom, the power-supply current I. Magnet i has the strength k_i = f_i(I):

Accessor get() set(x)
family.hardware I write I = x
family.strength sum(f_i(I)) / N solve sum(f_i(I)) / N = x. Raises on a mixed family
family.hardwares [I, ..., I] write I = mean(x)
family.strengths [f_1(I), ..., f_N(I)] solve sum(f_i(I)) = sum(x_i)
magnet[i].hardware I write I = x
magnet[i].strength f_i(I) write I = f_i^-1(x), the others follow
array with some magnets of a series per magnet solve sum(f_i(I)) = sum(k_i) over the requested magnets, the others follow

Known limitations

  • A series has a single degree of freedom. Any write asking for strengths not reachable with one current gives a compromise, and a warning is logged ("Inconsistent strengths ..."). This is the same compromise as magnet[i].strength.set(x), which imposes f_i^-1(x) on the whole series.
  • The magnets of a series that are not in the request follow the resulting current, whatever their previous strength. A write on part of a series can therefore change the other magnets a lot. The protection is the power-supply range in live: if the device has a configured range, a current outside it raises PyAMLException before anything is written, for every write path (array, family.strength(s), family.hardware, magnet[i].strength). In design there is no power-supply range.
  • A requested strength outside the range reachable by the excitation curves is clamped to the end of the curves, with a warning ("... is outside the range ... reachable by the excitation curves ...").
  • If the average strength of the requested magnets is not monotonic in I, the request cannot be inverted and PyAMLException is raised (existing behaviour of compute_hardware_from_mean_strength).
  • A mixed family still has a single unit / hardware_unit in LinearSerializedMagnetModel, so family.strengths.unit() reports the same strength unit for every magnet whatever its function.
  • Not covered: series of combined-function magnets, magnets with several functions on one power supply (MAX IV case), and one knob per power supply in the tuning tools.

Related Issue

Features/issues described there are:

  • new feature: function accepts a list with one function per element, so a series can mix magnet functions on one power supply. polynoms holds one polynom per element and the simulator uses each magnet's own polynom, otherwise the sextupole of a mixed family would write into the quadrupole slot. is_homogeneous() tells whether every magnet shares the same function.
  • new feature: family.strengths / family.hardwares (ReadWriteSerializedStrengthArray / ReadWriteSerializedHardwareArray), to read or write every magnet of a series in one call, as for CombinedFunctionMagnet. strengths.set(k) solves sum(f_i(I)) = sum(k_i) through the new LinearSerializedMagnetModel.compute_hardware_from_strengths(), which reuses the exact piecewise-linear inversion of compute_hardware_from_mean_strength. hardwares.set(x) applies mean(x).
  • new feature: family.strength raises PyAMLException on a family mixing functions, because a single value cannot be both a quadrupole and a sextupole strength. family.strengths, family.hardware(s) and per-magnet access keep working.
  • new feature: a warning is logged when a requested strength is clamped to the end of the excitation curves (it was silent).
  • bugfix: an unknown function raised a KeyError, the key was read before being checked. It now raises PyAMLException.

Changes to existing functionality

  • compute_hardware_values_partial (array writes covering part or all of a series): reimplemented to solve sum(f_i(I)) = sum(k_i) over the requested magnets only, with the same exact piecewise-linear inversion, because the previous rule, the mean of the currents implied by each requested strength (sum(f_i^-1(k_i)) / N), is only exact when every f_i is the same, or affine with the same slope. With a single requested magnet it gives f_i^-1(k_i), as magnet[i].strength.set(). It now checks that current_hardware holds one value per power supply and returns an array of the same shape. The inconsistency warning is shortened to the largest gap and the magnet concerned.
  • RWMagnetStrength.set() (design arrays, no aggregator): passes the current hardware value instead of None to compute_hardware_values_partial, and the single-member shortcut (magnet.strength.set()) is removed, so design and live arrays go through the same code.

Testing

The following tests (compatible with pytest) were added:

  • tests/config/sr_serialized_magnets.yaml: new mixedFunctionSeries family (quadrupole QF6D-C01 and sextupole SD1A-C01 on one power supply)
  • tests/config/sr_serialized_magnets_range.yaml: QF1A on a power supply limited to [0, 90] A in a static Tango catalog, with an array holding two of its four magnets
  • tests/magnet/test_serialized_magnets.py: homogeneity, family.strength refused on the mixed family, hardware and per-magnet access kept, each magnet written in its own polynom, exact convergence of a consistent strengths.set() request, warning on an inconsistent one, shape check of compute_hardware_values_partial
  • tests/magnet/test_serialized_magnets_cs.py, in design and live: strengths / hardwares read and write, set(get()) leaves the machine unchanged, an array with one magnet of a series drives it with that magnet's submodel, an array with two magnets drives it from those two only while the others follow, a strength outside the curve is clamped with a warning
  • tests/magnet/test_serialized_magnets_cs.py, live with a power-supply range: a write needing a current outside the range raises PyAMLException and leaves the series unchanged, through an array, family.strengths, family.strength, magnet.strength and family.hardware, and an in-range array write on part of the series moves the whole series
  • Updated from Implement the agreed serialized-magnet accessor convention #474: test_array_with_inconsistent_magnets_of_a_series now expects the exact solution of the average strength

Verify that your checklist complies with the project

  • New and existing unit tests pass locally (488 passed with -m "not integration"; test_tuning_orm fails on Windows only, temporary file lock, unrelated)
  • Tests were added to prove that all features/changes are effective
  • The code is commented where appropriate
  • Any existing features are not broken (unless there is an explicit change to an existing functionality)

…d completed partial requests for serialized magnets (#461)
@gupichon-soleil gupichon-soleil self-assigned this Oct 15, 2026
@gupichon-soleil gupichon-soleil changed the title Add per-magnet function lists, strengths/hardwares array accessors an… Serialized magnets: mixed-function families, strengths/hardwares arrays and completed partial requests Oct 15, 2026
@GamelinAl

GamelinAl commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

An array that only covers part of a series moves the series by roughly the requested fraction, since the magnets outside the array keep their current strength in the equation. Writing one magnet of a series through an array moves the power supply by about 1/N of the change, not to f_i^-1(x)

I think this drives a difference of behavior when changing the values directly using the array and if a tuning tool with the planned knob implementation in #461 :

  • with your proposal the array containing j/N magnets in series getting an average change of k will actually go k*j/N
  • the same thing in the tuning tool with the knob implementation will just change it to k

@gupichon-soleil

gupichon-soleil commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor Author

I think this drives a difference of behavior when changing the values directly using the array and if a tuning tool with the planned knob implementation in #461 :

* with your proposal the array containing j/N magnets in series getting an average change of k will actually go k*j/N

* the same thing in the tuning tool with the knob implementation will just change it to k

Yes. An intermediate slution would be to use only the referenced magnet to compute the current (like if it was a sub-serie) but it might lead to errors for the others magnets of the serie. I don't see a good solution here, it's a matter of choice in my opinion.

@GamelinAl

Copy link
Copy Markdown
Member

I think this drives a difference of behavior when changing the values directly using the array and if a tuning tool with the planned knob implementation in #461 :

* with your proposal the array containing j/N magnets in series getting an average change of k will actually go k*j/N

* the same thing in the tuning tool with the knob implementation will just change it to k

Yes. An intermediate slution would be to use only the referenced magnet to compute the current (like if it was a sub-serie) but it might lead to errors for the others magnets of the serie. I don't see a good solution here, it's a matter of choice in my opinion.

For me the second behavior is what we want.

@gupichon-soleil

Copy link
Copy Markdown
Contributor Author

Ok, I'll do it tomorrow.

… strength is clamped to the excitation curve
@gupichon-soleil gupichon-soleil changed the title Serialized magnets: mixed-function families, strengths/hardwares arrays and completed partial requests Serialized magnets: mixed-function families, strengths/hardwares arrays and partial series writes Oct 16, 2026
@gupichon-soleil

Copy link
Copy Markdown
Contributor Author

@GamelinAl, It's done

… strength is clamped to the excitation curve
@gupichon-soleil
gupichon-soleil marked this pull request as ready for review October 16, 2026 12:58
@gubaidulinvadim gubaidulinvadim added this to the ESRF Workshop milestone Oct 16, 2026
@GamelinAl

Copy link
Copy Markdown
Member

@gupichon-soleil There are several issues, finally I prefer to discuss it there so we agree on the changes to do.

1.a The solver fails with opposite polarities

Each f_i(I) is monotonic on its own. Their sum is not, as soon as two magnets have slopes of opposite sign (a
negative calibration factor, or magnets wired in reverse on the same supply). The sum can then go up and down, or be
flat:

Case (2 magnets, consistent request reached at I = 40 A) This PR Previous rule (mean of currents)
Opposite polarities (+1 / −1), different saturations raises PyAMLException: ... not monotonic 40 A
Opposite polarities, identical curves returns 0 A silently (the sum is flat at 0) 40 A

Even a perfectly reachable request fails, or silently gives a wrong current.

1.b On a mixed series, the compromise is weighted by unit size

When the requested strengths cannot all be reached with one current, the result is a compromise. Summing raw strengths means the magnet with the largest numbers wins. Example with a "quadrupole" (calibration 1) and a "sextupole" (calibration 60) on the same curve, with an inconsistent request:

Request (quad, sextu) Current, this PR Relative error, this PR Current, previous rule Relative error, previous rule
0.5, 36 59.84 A +19.7 %, −0.3 % 55.0 A +10 %, −8.3 %
0.6, 30 50.16 A −16.4 %, +0.3 % 55.0 A −8.3 %, +10 %

The sextupole almost gets what it asked for and the quadrupole takes nearly all the error. The existing
mixedFunctionSeries test could not catch this: both of its magnets have the same curve and the same calibration, so
their strengths have the same magnitude.

Proposal for 1a and 1b: normalized residual

We keep the idea of the PR (a single equation in I, inverted exactly), but we normalize each term before summing:

g(I) = sum_i s_i * (f_i(I) - k_i) / w_i = 0
  • w_i = |f_i(I_max) - f_i(I_min)| is the strength range of the excitation curve of magnet i. Each term becomes
    dimensionless, so every magnet weighs the same whatever its unit (fixes point 2).
  • s_i is the sign of the slope of f_i. Each term becomes increasing, so g is increasing and always invertible,
    whatever the polarities (fixes point 1). In practice s_i / w_i is just 1 / span_i, where span_i is the signed
    range.

Properties:

  • Consistent requests stay exact: every term is 0 at the right current.
  • Exact inversion: each f_i is piecewise linear in I, so g is piecewise linear on the union of the curve
    breakpoints. The zero is found by linear interpolation on the segment where g changes sign.
  • Plateau: if g is zero on an interval (for example two saturated magnets), the middle of the interval is
    returned instead of an arbitrary end.
  • Clamping: if the zero lies outside the curves, the current is clamped to the end of the curves and the existing
    warning is kept.
  • Inconsistent requests: the "Inconsistent strengths" warning is kept. The gap is now measured relative to w_i.
  • A curve with no strength range (w_i = 0) raises a PyAMLException, because the current cannot be computed from that strength.

2. family.hardwares.set does not check its input length

ReadWriteSerializedHardwareArray.set writes mean(value) without any check. On a 31-magnet series:

  • family.hardwares.set([10, 20]) silently writes 15 A;
  • family.hardwares.set(7.0) silently writes 7 A;
  • a list of 34 values is accepted.

family.strengths.set with a wrong length correctly raises. An indexing mistake by a caller moves the power supply
without any error.

Proposal: ReadWriteSerializedHardwareArray.set raises a PyAMLException if value is not a 1-D sequence with one value per magnet.

3. The model has two different inversion rules

LinearSerializedMagnetModel.compute_hardware_values still uses the old mean-of-currents rule, while
compute_hardware_from_strengths and compute_hardware_values_partial use the new one. With calibration factors 1.0 and 1.2 on a curve with two slopes, the same request [0.8, 1.6] gives:

  • compute_hardware_values → 61.67 A
  • compute_hardware_from_strengths → 59.09 A

Proposal: compute_hardware_values(strengths) now returns [compute_hardware_from_strengths(strengths)].

This branch has not been deployed

No deployments
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.

SerializedMagnets: get/set are not consitent and design/live disagree

3 participants