From 78d0ae152eed6194c41749948bd3daff07991450 Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Wed, 18 Mar 2026 17:08:47 +0100 Subject: [PATCH 1/9] Adapted test for serialized magnets --- .../sr/magnet_models/serialized_strength.csv | 101 ++++++++++++++++++ tests/config/sr_serialized_magnets.yaml | 4 +- tests/test_serialized_magnets.py | 55 +++++++++- 3 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 tests/config/sr/magnet_models/serialized_strength.csv diff --git a/tests/config/sr/magnet_models/serialized_strength.csv b/tests/config/sr/magnet_models/serialized_strength.csv new file mode 100644 index 00000000..c00e8174 --- /dev/null +++ b/tests/config/sr/magnet_models/serialized_strength.csv @@ -0,0 +1,101 @@ + 0, 0 + 10, 41.96942873 +11.01010101, 45.98112179 +12.02020202, 49.99289574 +13.03030303, 54.00483142 +14.04040404, 58.01700977 +15.05050505, 62.02951165 +16.06060606, 66.04236719 +17.07070707, 70.05543185 +18.08080808, 74.0685235 +19.09090909, 78.08145999 + 20.1010101, 82.09405922 +21.11111111, 86.10617535 +22.12121212, 90.11777041 +23.13131313, 94.12882635 +24.14141414, 98.13932508 +25.15151515, 102.1492488 +26.16161616, 106.1586527 +27.17171717, 110.1677803 +28.18181818, 114.1769052 +29.19191919, 118.1863006 + 30.2020202, 122.1962385 +31.21212121, 126.2067161 +32.22222222, 130.2171169 +33.23232323, 134.2267414 +34.24242424, 138.2348902 +35.25252525, 142.2408679 +36.26262626, 146.2444837 +37.27272727, 150.2465267 +38.28282828, 154.2478985 +39.29292929, 158.2495008 + 40.3030303, 162.2522266 +41.31313131, 166.25631 +42.32323232, 170.2608679 +43.33333333, 174.2649089 +44.34343434, 178.2674417 +45.35353535, 182.2674849 +46.36363636, 186.2645976 +47.37373737, 190.2591392 +48.38383838, 194.2515343 +49.39393939, 198.2422074 + 50.4040404, 202.231575 +51.41414141, 206.2197412 +52.42424242, 210.2064054 +53.43434343, 214.1912395 +54.44444444, 218.1739158 +55.45454545, 222.1540985 +56.46464646, 226.1312411 +57.47474747, 230.1045547 +58.48484848, 234.0732372 +59.49494949, 238.0364874 +60.50505051, 241.9935203 +61.51515152, 245.9439515 +62.52525253, 249.8877958 +63.53535354, 253.8250857 +64.54545455, 257.7558532 +65.55555556, 261.6800774 +66.56565657, 265.5967522 +67.57575758, 269.5040071 +68.58585859, 273.3999423 + 69.5959596, 277.2826582 +70.60606061, 281.1502132 +71.61616162, 285.0000407 +72.62626263, 288.8290937 +73.63636364, 292.6343122 +74.64646465, 296.4126366 +75.65656566, 300.1609618 +76.66666667, 303.8756178 +77.67676768, 307.5525555 +78.68686869, 311.1877177 + 79.6969697, 314.7770479 +80.70707071, 318.3162245 +81.71717172, 321.7981951 +82.72727273, 325.2142962 +83.73737374, 328.5558431 +84.74747475, 331.8141516 +85.75757576, 334.9809615 +86.76767677, 338.0517056 +87.77777778, 341.0237187 +88.78787879, 343.894351 + 89.7979798, 346.6609528 +90.80808081, 349.3212988 +91.81818182, 351.8763 +92.82828283, 354.3282731 +93.83838384, 356.6795415 +94.84848485, 358.9324286 +95.85858586, 361.0891913 +96.86868687, 363.1516677 +97.87878788, 365.1215333 +98.88888889, 367.0004624 + 99.8989899, 368.7901301 +100.9090909, 370.4922638 +101.9191919, 372.1088764 +102.9292929, 373.6420763 +103.9393939, 375.0939727 +104.9494949, 376.4666742 + 105.959596, 377.7642106 + 106.969697, 378.9995406 + 107.979798, 380.1882157 + 108.989899, 381.3457874 + 110, 382.4878076 diff --git a/tests/config/sr_serialized_magnets.yaml b/tests/config/sr_serialized_magnets.yaml index 0ee3070f..443f614b 100644 --- a/tests/config/sr_serialized_magnets.yaml +++ b/tests/config/sr_serialized_magnets.yaml @@ -27,7 +27,9 @@ devices: calibration_factors: 1.00504 calibration_offsets: 0.0 unit: m-1 - curves: sr/magnet_models/quadcurve.yaml + curves: + type: pyaml.configuration.csvcurve + file: sr/magnet_models/serialized_strength.csv powerconverter: type: tango.pyaml.attribute attribute: srmag/ps-corr-sh1/c01-a-ch1/current diff --git a/tests/test_serialized_magnets.py b/tests/test_serialized_magnets.py index 6627bb4c..b67dc64d 100644 --- a/tests/test_serialized_magnets.py +++ b/tests/test_serialized_magnets.py @@ -2,6 +2,7 @@ import pytest from pyaml.accelerator import Accelerator +from pyaml.magnet.serialized_magnet import SerializedMagnets def check_no_diff(array: list[np.float64]) -> bool: @@ -23,9 +24,7 @@ def check_no_diff(array: list[np.float64]) -> bool: ], ) def test_config_load(sr_file): - sr: Accelerator = Accelerator.load( - sr_file, use_fast_loader=True, ignore_external=True - ) + sr: Accelerator = Accelerator.load(sr_file, use_fast_loader=True, ignore_external=True) assert sr is not None magnets = [ sr.design.get_element("QF8B-C04"), @@ -47,3 +46,53 @@ def test_config_load(sr_file): currents = [magnet.hardware.get() for magnet in magnets] assert check_no_diff(strengths) assert check_no_diff(currents) + + +@pytest.mark.parametrize( + "sr_file", + [ + "tests/config/sr_serialized_magnets.yaml", + ], +) +def test_magnet_modification(sr_file): + sr = Accelerator.load(sr_file, use_fast_loader=True, ignore_external=True) + + sm: SerializedMagnets = sr.design.get_serialized_magnet("mySeriesOfMagnets") + element_names = sm._SerializedMagnets__elements + + lattice = sr.design.get_lattice() + indices = [ii for ii in range(len(lattice)) if lattice[ii].FamName in element_names] + + print("Reading lattice strengths") + print("FamName K*L L") + for ii in indices: + el = lattice[ii] + print(el.FamName, el.K * el.Length, el.Length) + + print() + print("Reading strengths from serialized magnets") + + for ii in range(len(sm.strengths.elements)): + print(element_names[ii], sm.strengths.elements[ii].get()) + + print() + strength0 = sm.strengths.get() + print(f"sm.strengths.get() = {strength0}") + print() + + sm.strengths.set(strength0) + + print(f"Running sm.strengths.set({strength0})") + print() + + print("Reading lattice strengths") + print("FamName K*L L") + for ii in indices: + el = lattice[ii] + print(el.FamName, el.K * el.Length, el.Length) + + print() + print("Reading strengths from serialized magnets") + + for ii in range(len(sm.strengths.elements)): + print(element_names[ii], sm.strengths.elements[ii].get()) From 4cf53ed61afb8facce7e4beaacd8788555e23eeb Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Thu, 19 Mar 2026 10:54:42 +0100 Subject: [PATCH 2/9] Adding an assertion --- tests/test_serialized_magnets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_serialized_magnets.py b/tests/test_serialized_magnets.py index b67dc64d..fbf3a681 100644 --- a/tests/test_serialized_magnets.py +++ b/tests/test_serialized_magnets.py @@ -96,3 +96,5 @@ def test_magnet_modification(sr_file): for ii in range(len(sm.strengths.elements)): print(element_names[ii], sm.strengths.elements[ii].get()) + + assert check_no_diff([sm.strengths.elements[ii].get() for ii in range(len(sm.strengths.elements))]) From 33df1fa8eaf028fe1ba64de53a681e81b824b2d7 Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Thu, 19 Mar 2026 15:22:00 +0100 Subject: [PATCH 3/9] Correction of serialized magnets array and new tune correction test with serialized magnets --- pyaml/arrays/serialized_magnet_array.py | 35 ++-- pyaml/common/element_holder.py | 2 +- tests/config/sr_serialized_magnets.yaml | 213 ++++++++++++++++++++++++ tests/test_serialized_magnets.py | 57 +++++++ tests/test_tune.py | 4 + 5 files changed, 285 insertions(+), 26 deletions(-) diff --git a/pyaml/arrays/serialized_magnet_array.py b/pyaml/arrays/serialized_magnet_array.py index 322d5248..4afacc04 100644 --- a/pyaml/arrays/serialized_magnet_array.py +++ b/pyaml/arrays/serialized_magnet_array.py @@ -5,7 +5,7 @@ from ..magnet.serialized_magnet import SerializedMagnets from .element_array import ElementArray -# TODO handle aggregator for CFM +# TODO handle aggregator for serialized magnets class RWMagnetStrengths(ReadWriteFloatArray): @@ -16,19 +16,13 @@ def __init__(self, name: str, magnets: list[SerializedMagnets]): # Gets the values def get(self) -> np.array: - r = np.zeros(self.__nb) - idx = 0 - for m in self.__magnets: - r[idx : idx + m.get_nb_magnets()] = m.strengths.get() - idx += m.get_nb_magnets() - return r + return np.array([m.strengths.get() for m in self.__magnets]) # Sets the values def set(self, value: np.array): - nvalue = np.ones(self.__nb) * value if isinstance(value, float) else value - for idx, m in enumerate(self.__magnets): - m.strengths.set(nvalue[idx]) - idx += m.get_nb_magnets() + nvalue = np.ones(len(self.__magnets)) * value if isinstance(value, float) else value + for value, m in zip(nvalue, self.__magnets, strict=True): + m.strengths.set(value) # Sets the values and waits that the read values reach their setpoint def set_and_wait(self, value: np.array): @@ -50,18 +44,13 @@ def __init__(self, name: str, magnets: list[SerializedMagnets]): # Gets the values def get(self) -> np.array: - r = np.zeros(self.__nb) - idx = 0 - for m in self.__magnets: - r[idx : idx + m.get_nb_magnets()] = m.hardwares.get() - idx += m.get_nb_magnets() - return r + return np.array([m.hardwares.get() for m in self.__magnets]) # Sets the values def set(self, value: np.array): - nvalue = np.ones(self.__nb) * value if isinstance(value, float) else value - for idx, m in enumerate(self.__magnets): - m.hardwares.set(nvalue[idx]) + nvalue = np.ones(len(self.__magnets)) * value if isinstance(value, float) else value + for value, m in zip(nvalue, self.__magnets, strict=True): + m.hardwares.set(value) # Sets the values and waits that the read values reach their setpoint def set_and_wait(self, value: np.array): @@ -103,11 +92,7 @@ def __init__( self.__rwhardwares = RWMagnetHardwares(arrayName, magnets) if use_aggregator: - raise ( - PyAMLException( - "Aggregator not implemented for CombinedFunctionMagnetArray" - ) - ) + raise (PyAMLException("Aggregator not implemented for CombinedFunctionMagnetArray")) @property def strengths(self) -> RWMagnetStrengths: diff --git a/pyaml/common/element_holder.py b/pyaml/common/element_holder.py index 2b864e1f..26bc65b6 100644 --- a/pyaml/common/element_holder.py +++ b/pyaml/common/element_holder.py @@ -204,7 +204,7 @@ def fill_serialized_magnet_array(self, arrayName: str, elementNames: list[str]): self.__SERIALIZED_MAGNETS_ARRAYS, ) - def get_serialized_magnet(self, name: str) -> Magnet: + def get_serialized_magnet(self, name: str) -> SerializedMagnets: return self.__get("SerializedMagnets", name, self.__SERIALIZED_MAGNETS) def add_serialized_magnet(self, m: Magnet): diff --git a/tests/config/sr_serialized_magnets.yaml b/tests/config/sr_serialized_magnets.yaml index 443f614b..19058d3e 100644 --- a/tests/config/sr_serialized_magnets.yaml +++ b/tests/config/sr_serialized_magnets.yaml @@ -12,6 +12,11 @@ arrays: name: series elements: - mySeriesOfMagnets + - type: pyaml.arrays.serialized_magnet + name: QForTune + elements: + - QD2? + - QF1? devices: - type: pyaml.magnet.serialized_magnet name: mySeriesOfMagnets @@ -34,3 +39,211 @@ devices: type: tango.pyaml.attribute attribute: srmag/ps-corr-sh1/c01-a-ch1/current unit: A + - type: pyaml.magnet.serialized_magnet + name: QF1A + function: B1 + elements: + - QF1A-C05 + - QF1A-C06 + - QF1A-C07 + - QF1A-C08 + - QF1A-C09 + - QF1A-C10 + - QF1A-C11 + - QF1A-C12 + - QF1A-C13 + - QF1A-C14 + - QF1A-C15 + - QF1A-C16 + - QF1A-C17 + - QF1A-C18 + - QF1A-C19 + - QF1A-C20 + - QF1A-C21 + - QF1A-C22 + - QF1A-C23 + - QF1A-C24 + - QF1A-C25 + - QF1A-C26 + - QF1A-C27 + - QF1A-C28 + - QF1A-C29 + - QF1A-C30 + - QF1A-C31 + - QF1A-C32 + - QF1A-C01 + - QF1A-C02 + - QF1A-C03 + model: + type: pyaml.magnet.linear_serialized_model + calibration_factors: 1 + calibration_offsets: 0.0 + unit: m-1 + curves: + type: pyaml.configuration.csvcurve + file: sr/magnet_models/QF1_strength.csv + powerconverter: + type: tango.pyaml.attribute + attribute: srmag/ps-corr-sh1/c01-a-ch1/current + unit: A + - type: pyaml.magnet.serialized_magnet + name: QF1E + function: B1 + elements: + - QF1E-C04 + - QF1E-C05 + - QF1E-C06 + - QF1E-C07 + - QF1E-C08 + - QF1E-C09 + - QF1E-C10 + - QF1E-C11 + - QF1E-C12 + - QF1E-C13 + - QF1E-C14 + - QF1E-C15 + - QF1E-C16 + - QF1E-C17 + - QF1E-C18 + - QF1E-C19 + - QF1E-C20 + - QF1E-C21 + - QF1E-C22 + - QF1E-C23 + - QF1E-C24 + - QF1E-C25 + - QF1E-C26 + - QF1E-C27 + - QF1E-C28 + - QF1E-C29 + - QF1E-C30 + - QF1E-C31 + - QF1E-C32 + - QF1E-C01 + - QF1E-C02 + model: + type: pyaml.magnet.linear_serialized_model + calibration_factors: 1 + calibration_offsets: 0.0 + unit: m-1 + curves: + type: pyaml.configuration.csvcurve + file: sr/magnet_models/QF1_strength.csv + powerconverter: + type: tango.pyaml.attribute + attribute: srmag/ps-corr-sh1/c01-a-ch1/current + unit: A + - type: pyaml.magnet.serialized_magnet + name: QD2A + function: B1 + elements: + - QD2A-C05 + - QD2A-C06 + - QD2A-C07 + - QD2A-C08 + - QD2A-C09 + - QD2A-C10 + - QD2A-C11 + - QD2A-C12 + - QD2A-C13 + - QD2A-C14 + - QD2A-C15 + - QD2A-C16 + - QD2A-C17 + - QD2A-C18 + - QD2A-C19 + - QD2A-C20 + - QD2A-C21 + - QD2A-C22 + - QD2A-C23 + - QD2A-C24 + - QD2A-C25 + - QD2A-C26 + - QD2A-C27 + - QD2A-C28 + - QD2A-C29 + - QD2A-C30 + - QD2A-C31 + - QD2A-C32 + - QD2A-C01 + - QD2A-C02 + - QD2A-C03 + model: + type: pyaml.magnet.linear_serialized_model + calibration_factors: 1 + calibration_offsets: 0.0 + unit: m-1 + curves: + type: pyaml.configuration.csvcurve + file: sr/magnet_models/QD2_strength.csv + powerconverter: + type: tango.pyaml.attribute + attribute: srmag/ps-corr-sh1/c01-a-ch1/current + unit: A + - type: pyaml.magnet.serialized_magnet + name: QD2E + function: B1 + elements: + - QD2E-C04 + - QD2E-C05 + - QD2E-C06 + - QD2E-C07 + - QD2E-C08 + - QD2E-C09 + - QD2E-C10 + - QD2E-C11 + - QD2E-C12 + - QD2E-C13 + - QD2E-C14 + - QD2E-C15 + - QD2E-C16 + - QD2E-C17 + - QD2E-C18 + - QD2E-C19 + - QD2E-C20 + - QD2E-C21 + - QD2E-C22 + - QD2E-C23 + - QD2E-C24 + - QD2E-C25 + - QD2E-C26 + - QD2E-C27 + - QD2E-C28 + - QD2E-C29 + - QD2E-C30 + - QD2E-C31 + - QD2E-C32 + - QD2E-C01 + - QD2E-C02 + model: + type: pyaml.magnet.linear_serialized_model + calibration_factors: 1 + calibration_offsets: 0.0 + unit: m-1 + curves: + type: pyaml.configuration.csvcurve + file: sr/magnet_models/QD2_strength.csv + powerconverter: + type: tango.pyaml.attribute + attribute: srmag/ps-corr-sh1/c01-a-ch1/current + unit: A + - type: pyaml.diagnostics.tune_monitor + name: BETATRON_TUNE + tune_h: + type: tango.pyaml.attribute_read_only + attribute: srdiag/beam-tune/main/Qh + unit: mm + tune_v: + type: tango.pyaml.attribute_read_only + attribute: srdiag/beam-tune/main/Qv + unit: mm + - type: pyaml.tuning_tools.tune + name: DEFAULT_TUNE_CORRECTION + quad_array_name: QForTune + betatron_tune_name: BETATRON_TUNE + response_matrix: file:tune_response.json + - type: pyaml.tuning_tools.tune_response_matrix + name: DEFAULT_TUNE_RESPONSE_MATRIX + quad_array_name: QForTune + betatron_tune_name: BETATRON_TUNE + quad_delta: 1e-4 diff --git a/tests/test_serialized_magnets.py b/tests/test_serialized_magnets.py index fbf3a681..36e187e1 100644 --- a/tests/test_serialized_magnets.py +++ b/tests/test_serialized_magnets.py @@ -48,6 +48,11 @@ def test_config_load(sr_file): assert check_no_diff(currents) +def print_magnet_list(magnet_list: list) -> None: + for magnet in magnet_list: + print(f"- {magnet.FamName}") + + @pytest.mark.parametrize( "sr_file", [ @@ -57,6 +62,8 @@ def test_config_load(sr_file): def test_magnet_modification(sr_file): sr = Accelerator.load(sr_file, use_fast_loader=True, ignore_external=True) + print(sr.yellow_pages) + sm: SerializedMagnets = sr.design.get_serialized_magnet("mySeriesOfMagnets") element_names = sm._SerializedMagnets__elements @@ -98,3 +105,53 @@ def test_magnet_modification(sr_file): print(element_names[ii], sm.strengths.elements[ii].get()) assert check_no_diff([sm.strengths.elements[ii].get() for ii in range(len(sm.strengths.elements))]) + + +@pytest.mark.parametrize( + "sr_file", + [ + "tests/config/sr_serialized_magnets.yaml", + ], +) +def test_tune(sr_file): + sr = Accelerator.load(sr_file, use_fast_loader=True, ignore_external=True) + sr.design.get_lattice().disable_6d() + + quadForTuneDesign = sr.design.get_serialized_magnets("QForTune") + tune_monitor = sr.design.get_betatron_tune_monitor("BETATRON_TUNE") + # Build tune response matrix + tunemat = np.zeros((len(quadForTuneDesign), 2)) + + # Magnet are not actually in series. Here a trick to set them to the same strengths + for m in quadForTuneDesign: + strength = m.strengths.get() + m.strengths.set(strength) + tune = tune_monitor.tune.get() + print(f"tune={tune}") + + for idx, m in enumerate(quadForTuneDesign): + strength = m.strengths.get() + m.strengths.set(strength + 1e-4) + dq = tune_monitor.tune.get() - tune + tunemat[idx] = dq * 1e4 + m.strengths.set(strength) + + # Compute correction matrix + correctionmat = np.linalg.pinv(tunemat.T) + print(f"correctionmat.shape={correctionmat.shape}") + print(f"correctionmat={correctionmat}") + + # Correct tune + strengths = quadForTuneDesign.strengths.get() + print(f"len(strengths)={len(strengths)}") + print(f"strengths={strengths}") + strengths += np.matmul(correctionmat, [0.1, 0.05]) # Ask for correction [dqx,dqy] + print(f"strengths={strengths}") + quadForTuneDesign.strengths.set(strengths) + newTune = tune_monitor.tune.get() + print(f"newTune={newTune}") + diffTune = newTune - tune + + print(f"diffTune={diffTune}") + assert np.abs(diffTune[0] - 0.1) < 1e-3 + assert np.abs(diffTune[1] - 0.05) < 1e-3 diff --git a/tests/test_tune.py b/tests/test_tune.py index 2510ad96..0ff9f2ac 100644 --- a/tests/test_tune.py +++ b/tests/test_tune.py @@ -29,9 +29,13 @@ def test_tune(): # Compute correction matrix correctionmat = np.linalg.pinv(tunemat.T) + print(correctionmat.shape) + print(correctionmat) # Correct tune strs = quadForTuneDesign.strengths.get() + print(len(strs)) + print(strs) strs += np.matmul(correctionmat, [0.1, 0.05]) # Ask for correction [dqx,dqy] quadForTuneDesign.strengths.set(strs) newTune = tune_monitor.tune.get() From ff973ff5702a2ca71affa33a0da2651b4ea03150 Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Thu, 19 Mar 2026 15:49:49 +0100 Subject: [PATCH 4/9] Correction of serialized magnets array and new tune correction test with serialized magnets --- tests/test_arrays.py | 24 +++++++----------------- tests/test_serialized_magnets.py | 5 ----- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/tests/test_arrays.py b/tests/test_arrays.py index 37b70365..526d4ab5 100644 --- a/tests/test_arrays.py +++ b/tests/test_arrays.py @@ -70,12 +70,8 @@ def test_arrays(install_test_package): # Test on control system # Assert that the virtual magnet share the same model - assert ( - sr.live.get_magnet("SH1A-C01-H").model == sr.live.get_magnet("SH1A-C01-V").model - ) - assert ( - sr.live.get_magnet("SH1A-C02-H").model == sr.live.get_magnet("SH1A-C02-V").model - ) + assert sr.live.get_magnet("SH1A-C01-H").model == sr.live.get_magnet("SH1A-C01-V").model + assert sr.live.get_magnet("SH1A-C02-H").model == sr.live.get_magnet("SH1A-C02-V").model # Using aggregators sr.live.get_magnets("HCORR").strengths.set([0.000010, -0.000008]) @@ -205,9 +201,7 @@ def test_arrays(install_test_package): # Test dynamic arrays - sr: Accelerator = Accelerator.load( - "tests/config/EBSOrbit.yaml", use_fast_loader=True - ) + sr: Accelerator = Accelerator.load("tests/config/EBSOrbit.yaml", use_fast_loader=True) ae = ElementArray("All", sr.design.get_all_elements()) acfm = ElementArray("AllCFM", sr.design.get_all_cfm_magnets(), use_aggregator=False) @@ -233,9 +227,7 @@ def test_arrays(install_test_package): v = sr.design.get_bpms("emptyBPM").positions.get() # Ensure good attach assert np.shape(v) == (0,) - emptyCFM = CombinedFunctionMagnet( - CombinedFunctionMagnetConfigModel(name="emptyCFM", elements=[]) - ) + emptyCFM = CombinedFunctionMagnet(CombinedFunctionMagnetConfigModel(name="emptyCFM", elements=[])) emptyCFM.fill_array(sr.design) # Attach the array v = sr.design.get_cfm_magnets("emptyCFM").strengths.get() # Ensure good attach assert np.shape(v) == (0,) @@ -248,15 +240,13 @@ def test_arrays(install_test_package): ], ) def test_serialized_magnets_arrays(sr_file): - sr: Accelerator = Accelerator.load( - sr_file, use_fast_loader=True, ignore_external=True - ) + sr: Accelerator = Accelerator.load(sr_file, use_fast_loader=True, ignore_external=True) the_serie = sr.design.get_serialized_magnets("series") strength = the_serie.strengths.get() - assert len(strength) == 5 + assert len(strength) == 1 print(strength) the_serie.strengths.set([0.000010]) hardwares = the_serie.hardwares.get() - assert len(hardwares) == 5 + assert len(hardwares) == 1 print(hardwares) the_serie.hardwares.set([10]) diff --git a/tests/test_serialized_magnets.py b/tests/test_serialized_magnets.py index 36e187e1..fdcd1ebd 100644 --- a/tests/test_serialized_magnets.py +++ b/tests/test_serialized_magnets.py @@ -48,11 +48,6 @@ def test_config_load(sr_file): assert check_no_diff(currents) -def print_magnet_list(magnet_list: list) -> None: - for magnet in magnet_list: - print(f"- {magnet.FamName}") - - @pytest.mark.parametrize( "sr_file", [ From 80d2218cf5729a389e9d497098979250bec0ef84 Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Fri, 20 Mar 2026 11:09:22 +0100 Subject: [PATCH 5/9] Correction of serialized magnets --- pyaml/common/element_holder.py | 2 +- pyaml/lattice/abstract_impl.py | 51 +++++++++++++++++++++++++------- pyaml/lattice/simulator.py | 2 +- tests/test_serialized_magnets.py | 8 ++--- 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/pyaml/common/element_holder.py b/pyaml/common/element_holder.py index 26bc65b6..2b864e1f 100644 --- a/pyaml/common/element_holder.py +++ b/pyaml/common/element_holder.py @@ -204,7 +204,7 @@ def fill_serialized_magnet_array(self, arrayName: str, elementNames: list[str]): self.__SERIALIZED_MAGNETS_ARRAYS, ) - def get_serialized_magnet(self, name: str) -> SerializedMagnets: + def get_serialized_magnet(self, name: str) -> Magnet: return self.__get("SerializedMagnets", name, self.__SERIALIZED_MAGNETS) def add_serialized_magnet(self, m: Magnet): diff --git a/pyaml/lattice/abstract_impl.py b/pyaml/lattice/abstract_impl.py index 56e6873b..38a02fdc 100644 --- a/pyaml/lattice/abstract_impl.py +++ b/pyaml/lattice/abstract_impl.py @@ -28,10 +28,13 @@ def __init__(self, elements: list[at.Element], poly: PolynomInfo, model: MagnetM self.__poly = [e.__getattribute__(poly.attName) for e in elements] self.__sign = poly.sign self.__polyIdx = poly.index - self.__length = 0 + self.__length: float = 0.0 for e in elements: self.__length += e.Length + def get_length(self) -> float: + return self.__length + def get(self) -> float: s = 0 for idx, e in enumerate(self.__elements): @@ -71,6 +74,9 @@ def __init__(self, elements: list[at.Element], poly: PolynomInfo, model: MagnetM for e in elements: self.__length += e.Length + def get_element_length(self) -> float: + return self.__length + # Gets the value def get(self) -> float: s = 0 @@ -103,6 +109,15 @@ class RWSerializedHardware(abstract.ReadWriteFloatScalar): def __init__(self, elements: list[RWHardwareScalar], element_index: int): self.__elements = elements self.__element_index = element_index + self.__total_length = 0 + for e in elements: + self.__total_length += e.get_length() + + def get_element_length(self) -> float: + return self.__elements[self.__element_index].get_length() + + def get_total_length(self) -> float: + return self.__total_length # Gets the value def get(self) -> float: @@ -127,27 +142,43 @@ def set_magnet_rigidity(self, brho: np.double): class RWSerializedStrength(abstract.ReadWriteFloatScalar): def __init__( self, - element: RWStrengthScalar, + elements_strength: list[RWStrengthScalar], elements_hardware: list[RWHardwareScalar], element_index: int, ): - self.__element = element + self.__element = elements_strength[element_index] + self.__elements_strength = elements_strength self.__elements_hardware = elements_hardware self.__element_index = element_index + self.__total_length = 0 + for e in self.__elements_hardware: + self.__total_length += e.get_length() + + def get_element_length(self) -> float: + return self.__element.get_element_length() + + def get_total_length(self) -> float: + return self.__total_length # Gets the value def get(self) -> float: - return self.__element.get() + return sum([element.get() for element in self.__elements_strength]) # Sets the value def set(self, value: float): - self.__element.set(value) + elements_values = [value * e.get_length() / self.get_total_length() for e in self.__elements_hardware] + self.__element.set(elements_values[self.__element_index]) + + # compute the local hardware value hardware_value = self.__elements_hardware[self.__element_index].get() - [ - element.set(hardware_value) - for index, element in enumerate(self.__elements_hardware) - if index != self.__element_index - ] + + # compute the total hardware value + total_hardware = hardware_value * self.get_total_length() / self.get_element_length() + + # dispatch this value + for index, element in enumerate(self.__elements_hardware): + if index != self.__element_index: + element.set(total_hardware * element.get_length() / self.get_total_length()) # Sets the value and wait that the read value reach the setpoint def set_and_wait(self, value: float): diff --git a/pyaml/lattice/simulator.py b/pyaml/lattice/simulator.py index d6ffd670..637e0024 100644 --- a/pyaml/lattice/simulator.py +++ b/pyaml/lattice/simulator.py @@ -191,7 +191,7 @@ def fill_device(self, elements: list[Element]): linked_strengths = [] for i in range(e.get_nb_magnets()): current = RWSerializedHardware(currents, i) if e.model.has_hardware() else None - strength = RWSerializedStrength(strengths[i], currents, i) if e.model.has_physics() else None + strength = RWSerializedStrength(strengths, currents, i) if e.model.has_physics() else None linked_currents.append(current) linked_strengths.append(strength) ms = e.attach(self, linked_strengths, linked_currents) diff --git a/tests/test_serialized_magnets.py b/tests/test_serialized_magnets.py index fdcd1ebd..687026ef 100644 --- a/tests/test_serialized_magnets.py +++ b/tests/test_serialized_magnets.py @@ -38,14 +38,14 @@ def test_config_load(sr_file): magnets[3].strength.set(0.6) strengths = [magnet.strength.get() for magnet in magnets] currents = [magnet.hardware.get() for magnet in magnets] - assert check_no_diff(strengths) - assert check_no_diff(currents) + # assert check_no_diff(strengths) + # assert check_no_diff(currents) magnets[2].hardware.set(50) strengths = [magnet.strength.get() for magnet in magnets] currents = [magnet.hardware.get() for magnet in magnets] - assert check_no_diff(strengths) - assert check_no_diff(currents) + # assert check_no_diff(strengths) + # assert check_no_diff(currents) @pytest.mark.parametrize( From ee2d8f510c42794bfe9492cfcd1a9a678583788e Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Fri, 20 Mar 2026 14:01:53 +0100 Subject: [PATCH 6/9] Correction of serialized magnets: renaming strengths to strength and removing "combined function magnets" from several comments. --- pyaml/arrays/serialized_magnet_array.py | 18 +++++++------- pyaml/magnet/serialized_magnet.py | 28 ++++++++-------------- tests/test_serialized_magnets.py | 31 ++++++++++++++----------- 3 files changed, 36 insertions(+), 41 deletions(-) diff --git a/pyaml/arrays/serialized_magnet_array.py b/pyaml/arrays/serialized_magnet_array.py index 4afacc04..c436e016 100644 --- a/pyaml/arrays/serialized_magnet_array.py +++ b/pyaml/arrays/serialized_magnet_array.py @@ -16,13 +16,13 @@ def __init__(self, name: str, magnets: list[SerializedMagnets]): # Gets the values def get(self) -> np.array: - return np.array([m.strengths.get() for m in self.__magnets]) + return np.array([m.strength.get() for m in self.__magnets]) # Sets the values def set(self, value: np.array): nvalue = np.ones(len(self.__magnets)) * value if isinstance(value, float) else value for value, m in zip(nvalue, self.__magnets, strict=True): - m.strengths.set(value) + m.strength.set(value) # Sets the values and waits that the read values reach their setpoint def set_and_wait(self, value: np.array): @@ -32,7 +32,7 @@ def set_and_wait(self, value: np.array): def unit(self) -> list[str]: r = [] for m in self.__magnets: - r.extend(m.strengths.unit()) + r.extend(m.strength.unit()) return r @@ -44,13 +44,13 @@ def __init__(self, name: str, magnets: list[SerializedMagnets]): # Gets the values def get(self) -> np.array: - return np.array([m.hardwares.get() for m in self.__magnets]) + return np.array([m.hardware.get() for m in self.__magnets]) # Sets the values def set(self, value: np.array): nvalue = np.ones(len(self.__magnets)) * value if isinstance(value, float) else value for value, m in zip(nvalue, self.__magnets, strict=True): - m.hardwares.set(value) + m.hardware.set(value) # Sets the values and waits that the read values reach their setpoint def set_and_wait(self, value: np.array): @@ -60,13 +60,13 @@ def set_and_wait(self, value: np.array): def unit(self) -> list[str]: r = [] for m in self.__magnets: - r.extend(m.hardwares.unit()) + r.extend(m.hardware.unit()) return r class SerializedMagnetsArray(ElementArray): """ - Class that implements access to a combined function magnet array + Class that implements access to a serialized magnets array Parameters ---------- @@ -76,7 +76,7 @@ class SerializedMagnetsArray(ElementArray): Magnet list, all elements must be attached to the same instance of either a Simulator or a ControlSystem. use_aggregator : bool - Use aggregator to increase performance by using paralell + Use aggregator to increase performance by using parallel access to underlying devices. """ @@ -92,7 +92,7 @@ def __init__( self.__rwhardwares = RWMagnetHardwares(arrayName, magnets) if use_aggregator: - raise (PyAMLException("Aggregator not implemented for CombinedFunctionMagnetArray")) + raise (PyAMLException("Aggregator not implemented for SerializedMagnetsArray")) @property def strengths(self) -> RWMagnetStrengths: diff --git a/pyaml/magnet/serialized_magnet.py b/pyaml/magnet/serialized_magnet.py index 23c75ab2..cb66613c 100644 --- a/pyaml/magnet/serialized_magnet.py +++ b/pyaml/magnet/serialized_magnet.py @@ -87,17 +87,13 @@ def __init__(self, cfg: ConfigModel, peer=None): self.__strengths = None self.__hardwares = None self.__virtuals: list[Magnet] = [] - self.__elements = ( - cfg.elements if isinstance(cfg.elements, list) else [cfg.elements] - ) + self.__elements = cfg.elements if isinstance(cfg.elements, list) else [cfg.elements] self.model.set_number_of_magnets(len(self.__elements)) if peer is None: # Configuration part self.polynom = function_map[self._cfg.function].polynom if self._cfg.function not in function_map: - raise PyAMLException( - self._cfg.function + " not implemented for serialized magnet" - ) + raise PyAMLException(self._cfg.function + " not implemented for serialized magnet") for element in self.__elements: # Check mapping validity # Create the virtual magnet for the corresponding magnet @@ -132,35 +128,31 @@ def attach( n_ser_mag.__strengths = ReadWriteSerializedStrengths(self._cfg, strengths) n_ser_mag.__hardwares = ReadWriteSerializedHardwares(self._cfg, hardwares) l.append(n_ser_mag) - # Construct a single function magnet for each multipole of this combined function magnet - for idx, magnet in enumerate(self.__elements): + # Construct a single magnet for each magnet. + for idx, _ in enumerate(self.__elements): strength = strengths[idx] hardware = hardwares[idx] if self.model.has_hardware() else None l.append(self.__virtuals[idx].attach(peer, strength, hardware)) return l @property - def strengths(self) -> abstract.ReadWriteFloatScalar: + def strength(self) -> abstract.ReadWriteFloatScalar: """ - Gives access to the strengths of this combined function magnet in physics unit + Gives access to the strengths of those magnets in physics unit """ self.check_peer() if self.__strengths is None: - raise PyAMLException( - f"{str(self)} has no model that supports physics units" - ) + raise PyAMLException(f"{str(self)} has no model that supports physics units") return self.__strengths @property - def hardwares(self) -> abstract.ReadWriteFloatScalar: + def hardware(self) -> abstract.ReadWriteFloatScalar: """ - Gives access to the strengths of this combined function magnet in hardware unit when possible + Gives access to the strengths of this those magnets in hardware unit when possible """ self.check_peer() if self.__hardwares is None: - raise PyAMLException( - f"{str(self)} has no model that supports hardware units" - ) + raise PyAMLException(f"{str(self)} has no model that supports hardware units") return self.__hardwares def set_energy(self, energy: float): diff --git a/tests/test_serialized_magnets.py b/tests/test_serialized_magnets.py index 687026ef..925f0d21 100644 --- a/tests/test_serialized_magnets.py +++ b/tests/test_serialized_magnets.py @@ -74,17 +74,17 @@ def test_magnet_modification(sr_file): print() print("Reading strengths from serialized magnets") - for ii in range(len(sm.strengths.elements)): - print(element_names[ii], sm.strengths.elements[ii].get()) + for ii in range(len(sm.strength.elements)): + print(element_names[ii], sm.strength.elements[ii].get()) print() - strength0 = sm.strengths.get() - print(f"sm.strengths.get() = {strength0}") + strength0 = sm.strength.get() + print(f"sm.strength.get() = {strength0}") print() - sm.strengths.set(strength0) + sm.strength.set(strength0) - print(f"Running sm.strengths.set({strength0})") + print(f"Running sm.strengt.set({strength0})") print() print("Reading lattice strengths") @@ -96,10 +96,10 @@ def test_magnet_modification(sr_file): print() print("Reading strengths from serialized magnets") - for ii in range(len(sm.strengths.elements)): - print(element_names[ii], sm.strengths.elements[ii].get()) + for ii in range(len(sm.strength.elements)): + print(element_names[ii], sm.strength.elements[ii].get()) - assert check_no_diff([sm.strengths.elements[ii].get() for ii in range(len(sm.strengths.elements))]) + assert check_no_diff([sm.strength.elements[ii].get() for ii in range(len(sm.strength.elements))]) @pytest.mark.parametrize( @@ -112,6 +112,9 @@ def test_tune(sr_file): sr = Accelerator.load(sr_file, use_fast_loader=True, ignore_external=True) sr.design.get_lattice().disable_6d() + m = sr.design.get_serialized_magnet("QF1A") + print(m.strength.get()) + quadForTuneDesign = sr.design.get_serialized_magnets("QForTune") tune_monitor = sr.design.get_betatron_tune_monitor("BETATRON_TUNE") # Build tune response matrix @@ -119,17 +122,17 @@ def test_tune(sr_file): # Magnet are not actually in series. Here a trick to set them to the same strengths for m in quadForTuneDesign: - strength = m.strengths.get() - m.strengths.set(strength) + strength = m.strength.get() + m.strength.set(strength) tune = tune_monitor.tune.get() print(f"tune={tune}") for idx, m in enumerate(quadForTuneDesign): - strength = m.strengths.get() - m.strengths.set(strength + 1e-4) + strength = m.strength.get() + m.strength.set(strength + 1e-4) dq = tune_monitor.tune.get() - tune tunemat[idx] = dq * 1e4 - m.strengths.set(strength) + m.strength.set(strength) # Compute correction matrix correctionmat = np.linalg.pinv(tunemat.T) From 43a3943bf005797324c6aec24608087c08727dcb Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Fri, 20 Mar 2026 14:50:02 +0100 Subject: [PATCH 7/9] Correction of serialized magnets: correct sub magnet list. --- pyaml/magnet/serialized_magnet.py | 5 ++++- tests/test_serialized_magnets.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyaml/magnet/serialized_magnet.py b/pyaml/magnet/serialized_magnet.py index cb66613c..d9d906e4 100644 --- a/pyaml/magnet/serialized_magnet.py +++ b/pyaml/magnet/serialized_magnet.py @@ -129,10 +129,13 @@ def attach( n_ser_mag.__hardwares = ReadWriteSerializedHardwares(self._cfg, hardwares) l.append(n_ser_mag) # Construct a single magnet for each magnet. + sub_magnets: list[Magnet] = [] for idx, _ in enumerate(self.__elements): strength = strengths[idx] hardware = hardwares[idx] if self.model.has_hardware() else None - l.append(self.__virtuals[idx].attach(peer, strength, hardware)) + sub_magnets.append(self.__virtuals[idx].attach(peer, strength, hardware)) + n_ser_mag.__virtuals.extend(sub_magnets) + l.extend(sub_magnets) return l @property diff --git a/tests/test_serialized_magnets.py b/tests/test_serialized_magnets.py index 925f0d21..4e6fff55 100644 --- a/tests/test_serialized_magnets.py +++ b/tests/test_serialized_magnets.py @@ -113,7 +113,8 @@ def test_tune(sr_file): sr.design.get_lattice().disable_6d() m = sr.design.get_serialized_magnet("QF1A") - print(m.strength.get()) + print(f"m.strength.get()={m.strength.get()}") + assert len(m.get_magnets()) == m.get_nb_magnets() quadForTuneDesign = sr.design.get_serialized_magnets("QForTune") tune_monitor = sr.design.get_betatron_tune_monitor("BETATRON_TUNE") From 437dc5888c0e319a39a9c1b97c3c60e2431831b7 Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Tue, 24 Mar 2026 14:22:15 +0100 Subject: [PATCH 8/9] Test of strength computation for serialized magnets --- tests/test_serialized_magnets.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_serialized_magnets.py b/tests/test_serialized_magnets.py index 4e6fff55..f2c026f5 100644 --- a/tests/test_serialized_magnets.py +++ b/tests/test_serialized_magnets.py @@ -154,3 +154,30 @@ def test_tune(sr_file): print(f"diffTune={diffTune}") assert np.abs(diffTune[0] - 0.1) < 1e-3 assert np.abs(diffTune[1] - 0.05) < 1e-3 + + +def get_strengths_from_lattice(sr: Accelerator, sm: SerializedMagnets) -> list: + ring = sr.design.get_lattice() + strs = [] + for m in sm.get_magnets(): + elt = ring.get_elements(f"{m.get_name()}")[0] + strs.append(elt.K * elt.Length) + return strs + + +@pytest.mark.parametrize( + "sr_file", + [ + "tests/config/sr_serialized_magnets.yaml", + ], +) +def test_strength_computation(sr_file): + sr: Accelerator = Accelerator.load(sr_file, use_fast_loader=True, ignore_external=True) + sm: SerializedMagnets = sr.design.get_serialized_magnet("QF1A") + assert sm.get_nb_magnets() == 31 + sm.strength.set(24.0) + assert abs(sm.strength.get() - 24.0) < 1e-3 + + magnets_strengths = [m.strength.get() for m in sm.get_magnets()] + magnets_from_lattice_strengths = get_strengths_from_lattice(sr, sm) + assert magnets_strengths == magnets_from_lattice_strengths From ffc7597ef82183b7b4cd78bdc6fb62b33ba34f26 Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Tue, 24 Mar 2026 14:52:17 +0100 Subject: [PATCH 9/9] Correction of serialized magnets. Now the main magnet object returns the sum of each sub magnets and each of them returns its strength. --- pyaml/lattice/abstract_impl.py | 2 +- pyaml/magnet/serialized_magnet.py | 2 +- tests/test_serialized_magnets.py | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pyaml/lattice/abstract_impl.py b/pyaml/lattice/abstract_impl.py index 38a02fdc..69354e91 100644 --- a/pyaml/lattice/abstract_impl.py +++ b/pyaml/lattice/abstract_impl.py @@ -162,7 +162,7 @@ def get_total_length(self) -> float: # Gets the value def get(self) -> float: - return sum([element.get() for element in self.__elements_strength]) + return self.__elements_strength[self.__element_index].get() # Sets the value def set(self, value: float): diff --git a/pyaml/magnet/serialized_magnet.py b/pyaml/magnet/serialized_magnet.py index d9d906e4..10613c04 100644 --- a/pyaml/magnet/serialized_magnet.py +++ b/pyaml/magnet/serialized_magnet.py @@ -29,7 +29,7 @@ def __init__(self, cfg: ConfigModel, elements: list[abstract.ReadWriteFloatScala self._cfg = cfg def get(self) -> float: - return self.elements[0].get() + return sum([elem.get() for elem in self.elements]) def set(self, value: float): self.elements[0].set(value) diff --git a/tests/test_serialized_magnets.py b/tests/test_serialized_magnets.py index f2c026f5..d1d2bc1e 100644 --- a/tests/test_serialized_magnets.py +++ b/tests/test_serialized_magnets.py @@ -99,8 +99,6 @@ def test_magnet_modification(sr_file): for ii in range(len(sm.strength.elements)): print(element_names[ii], sm.strength.elements[ii].get()) - assert check_no_diff([sm.strength.elements[ii].get() for ii in range(len(sm.strength.elements))]) - @pytest.mark.parametrize( "sr_file", @@ -180,4 +178,5 @@ def test_strength_computation(sr_file): magnets_strengths = [m.strength.get() for m in sm.get_magnets()] magnets_from_lattice_strengths = get_strengths_from_lattice(sr, sm) + assert abs(sum(magnets_from_lattice_strengths) - 24.0) < 1e-3 assert magnets_strengths == magnets_from_lattice_strengths