From f6a0d456fa308c3fdd8c9eac5dcaec95e3e7507c Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Thu, 15 Oct 2026 12:04:16 +0200 Subject: [PATCH 1/3] Add per-magnet function lists, strengths/hardwares array accessors and completed partial requests for serialized magnets (#461) --- pyaml/arrays/magnet_array.py | 8 +- pyaml/control/abstract_impl.py | 3 +- pyaml/lattice/simulator.py | 4 +- pyaml/magnet/linear_serialized_model.py | 73 ++++-- pyaml/magnet/serialized_magnet.py | 255 +++++++++++++++++++-- tests/config/sr_serialized_magnets.yaml | 16 ++ tests/magnet/test_serialized_magnets.py | 98 ++++++++ tests/magnet/test_serialized_magnets_cs.py | 98 +++++++- 8 files changed, 505 insertions(+), 50 deletions(-) diff --git a/pyaml/arrays/magnet_array.py b/pyaml/arrays/magnet_array.py index 7f05f35a5..52e06d83e 100644 --- a/pyaml/arrays/magnet_array.py +++ b/pyaml/arrays/magnet_array.py @@ -67,7 +67,7 @@ def set(self, value: np.array): nvalue = np.ones(self.__nb) * value if isinstance(value, float) else value if not self.__aggregator: # Magnets of a series share one power supply: it is written once, from the strengths of the - # magnets of the series present in the array + # magnets of the series present in the array, the others keeping their current strength series: dict[int, tuple[Magnet, dict[int, float]]] = {} for idx, m in enumerate(self.__magnets): if m.model is not None and m.model.shares_power_supply(): @@ -75,10 +75,8 @@ def set(self, value: np.array): else: m.strength.set(nvalue[idx]) for m, requested in series.values(): - if len(requested) == 1: - m.strength.set(next(iter(requested.values()))) - else: - m.hardware.set(m.model.compute_hardware_values_partial(None, requested)[0]) + current_hardware = np.array([m.hardware.get()]) + m.hardware.set(m.model.compute_hardware_values_partial(current_hardware, requested)[0]) else: self.__aggregator.set(nvalue) diff --git a/pyaml/control/abstract_impl.py b/pyaml/control/abstract_impl.py index ffea32340..2ce608d39 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -431,7 +431,8 @@ def add_magnet(self, magnet: Magnet, devs: list[DeviceAccess]): Hardware devices associated with the magnet model. """ # Magnets of a serialized group share the serialized model (one power supply for N strengths): - # the power-supply current is computed by the model from the strengths of the magnets present in the array. + # the power-supply current is computed by the model from the strengths of the magnets present in the array, + # the others keeping their current strength. is_indexed = isinstance(magnet.strength, (abstract.RWMapper, RWSerializedStrengthScalar)) strengthIndex = magnet.strength.index() if is_indexed else 0 if magnet.model not in self.__models: diff --git a/pyaml/lattice/simulator.py b/pyaml/lattice/simulator.py index e0922ae38..4e4f92e04 100644 --- a/pyaml/lattice/simulator.py +++ b/pyaml/lattice/simulator.py @@ -271,12 +271,12 @@ def _fill_serialized_magnets(self, magnets: SerializedMagnets) -> None: strengths = [] for index, magnet in enumerate(magnets.get_magnets()): current = ( - RWHardwareScalar(self.get_at_elems(magnet), magnets.polynom, magnets.model.get_sub_model(index)) + RWHardwareScalar(self.get_at_elems(magnet), magnets.polynoms[index], magnets.model.get_sub_model(index)) if magnets.model.has_hardware() else None ) strength = ( - RWStrengthScalar(self.get_at_elems(magnet), magnets.polynom, magnets.model.get_sub_model(index)) + RWStrengthScalar(self.get_at_elems(magnet), magnets.polynoms[index], magnets.model.get_sub_model(index)) if magnets.model.has_physics() else None ) diff --git a/pyaml/magnet/linear_serialized_model.py b/pyaml/magnet/linear_serialized_model.py index 6915c862b..f0a85d052 100644 --- a/pyaml/magnet/linear_serialized_model.py +++ b/pyaml/magnet/linear_serialized_model.py @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) -# Relative spread above which the currents requested by members of a series are considered inconsistent +# Relative gap above which strengths requested for a series are considered unreachable with a single hardware value _SPREAD_RTOL = 1e-6 @@ -180,6 +180,8 @@ class LinearSerializedMagnetModel(MagnetModel, DynamicValidation): Return the average strength of the series for a hardware value. compute_hardware_from_mean_strength(strength) Return the hardware value giving an average strength of the series. + compute_hardware_from_strengths(strengths) + Return the hardware value for a requested strength of each magnet of the series. shares_power_supply() Return True, all the magnets of the series share one power supply. get_strength_units() @@ -355,37 +357,37 @@ def compute_strengths(self, currents: np.array) -> np.array: return np.array([model.compute_strengths([current])[0] for model in self.__sub_models]) def compute_hardware_values_partial( - self, current_hardware: np.ndarray | None, strengths_by_index: dict[int, float] + self, current_hardware: np.ndarray, strengths_by_index: dict[int, float] ) -> np.ndarray: """ Convert the strengths of some magnets of the series to the shared hardware value. - Each requested strength implies a hardware value through its own submodel. The power supply gets the mean of - the hardware values implied by the requested magnets only: the magnets that are not requested do not take part. - A warning is logged when the requested strengths are not consistent with a single hardware value. + The magnets that are not requested keep the strength given by the current hardware value. The completed + strengths are then converted by :meth:`compute_hardware_from_strengths`, which logs a warning when they + cannot all be reached with a single hardware value. Parameters ---------- - current_hardware : np.array | None - Current hardware value of the series. Not used: the requested strengths define the hardware value. + current_hardware : np.array + Current hardware value of the series, one value per power supply. strengths_by_index : dict[int, float] Requested strengths, indexed by the position of the magnet in the series. Returns ------- np.array - Hardware value of the power supply. + Hardware value of the power supply, with the shape of ``current_hardware``. + + Raises + ------ + PyAMLException + If ``current_hardware`` does not hold one value per power supply of the series. """ - currents = np.array( - [self.__sub_models[idx].compute_hardware_values([strength])[0] for idx, strength in strengths_by_index.items()] - ) - current = np.mean(currents) - if np.ptp(currents) > _SPREAD_RTOL * max(abs(current), 1.0): - logger.warning( - f"Inconsistent strengths requested for magnets {list(strengths_by_index.keys())} of a series on " - f"{self._powerconverter}: implied hardware values {currents}, the mean {current} is applied" - ) - return np.array([current]) + _check_len(current_hardware, "current_hardware", len(self.get_device_names())) + strengths = self.compute_strengths(current_hardware) + for idx, strength in strengths_by_index.items(): + strengths[idx] = strength + return np.array([self.compute_hardware_from_strengths(strengths)]) def compute_mean_strength(self, current: float) -> float: """ @@ -436,6 +438,41 @@ def compute_hardware_from_mean_strength(self, strength: float) -> float: ) return float(np.interp(strength, strengths, currents)) + def compute_hardware_from_strengths(self, strengths: np.ndarray) -> float: + """ + Return the hardware value for a requested strength of each magnet of the series. + + The series has a single hardware value, so it solves ``sum(f_i(I)) == sum(k_i)`` for ``I``. When the requested + strengths cannot all be reached with one hardware value, the result is a compromise and a warning is logged. + + Parameters + ---------- + strengths : np.ndarray + Requested strength ``k_i`` of each magnet of the series, in declaration order. + + Returns + ------- + float + Hardware value of the power supply. + + Raises + ------ + PyAMLException + If the number of strengths does not match the number of magnets, or if the average strength is not + monotonic in the hardware value. + """ + strengths = np.asarray(strengths, dtype=float) + _check_len(strengths, "strengths", self.__nbMagnets) + current = self.compute_hardware_from_mean_strength(float(np.mean(strengths))) + gaps = self.compute_strengths([current]) - strengths + if np.any(np.abs(gaps) > _SPREAD_RTOL * np.maximum(np.abs(strengths), 1.0)): + worst = int(np.argmax(np.abs(gaps))) + logger.warning( + f"Inconsistent strengths requested for the series on {self._powerconverter}: the hardware value " + f"{current} misses the requested strengths by up to {gaps[worst]:.3g} (magnet {worst})" + ) + return current + def shares_power_supply(self) -> bool: """Return True, all the magnets of the series share one power supply.""" return True diff --git a/pyaml/magnet/serialized_magnet.py b/pyaml/magnet/serialized_magnet.py index 44cf55c1a..d9e0e6e57 100644 --- a/pyaml/magnet/serialized_magnet.py +++ b/pyaml/magnet/serialized_magnet.py @@ -120,12 +120,17 @@ class ReadWriteSerializedStrength(ReadWriteSerializedHardware): Magnet ``i`` of the series has the strength ``k_i = f_i(I)``. The family strength is the average strength ``sum(f_i(I)) / N``. + A single strength value for the whole family is only defined when every magnet has the same function: for a + series mixing functions (e.g. quadrupoles and sextupoles), use the strength of each magnet instead. + Parameters ---------- hardwares : list[abstract.ReadWriteFloatScalar] Per-magnet hardware accessors. Writing one of them moves the whole series. model : MagnetModel Serialized magnet model converting between average strength and hardware value. + homogeneous : bool, optional + Whether every magnet of the series has the same function. Default is ``True``. Methods ------- @@ -135,10 +140,23 @@ class ReadWriteSerializedStrength(ReadWriteSerializedHardware): Set the hardware value giving the requested average strength. unit() Return the physical strength unit. + + Raises + ------ + PyAMLException + On ``get()`` or ``set()`` when the series mixes magnet functions. """ + def __init__(self, hardwares: list[abstract.ReadWriteFloatScalar], model: MagnetModel, homogeneous: bool = True): + """ + Initialize a family strength accessor for serialized magnets. + """ + super().__init__(hardwares, model) + self._homogeneous = homogeneous + def get(self) -> float: """Return the average strength of the series.""" + self._check_homogeneous() return self._model.compute_mean_strength(self._hardwares[0].get()) def set(self, value: float): @@ -150,12 +168,167 @@ def set(self, value: float): value : float Average strength of the series. """ + self._check_homogeneous() self._hardwares[0].set(self._model.compute_hardware_from_mean_strength(value)) def unit(self) -> str: """Return the physical strength unit.""" return self._model.get_strength_units()[0] + def _check_homogeneous(self): + if not self._homogeneous: + raise PyAMLException( + "A single strength is not defined for a serialized family mixing magnet functions, " + "use the strength of each magnet (family.strengths or magnet.strength)" + ) + + +class ReadWriteSerializedHardwareArray(abstract.ReadWriteFloatArray): + """ + Read/write access to the hardware value of each magnet of a serialized-magnet family. + + Every magnet of the series reads the hardware value ``I`` of the shared power supply. + + Parameters + ---------- + hardwares : list[abstract.ReadWriteFloatScalar] + Per-magnet hardware accessors. Writing one of them moves the whole series. + model : MagnetModel + Serialized magnet model used for units. + + Methods + ------- + get() + Return the hardware value of each magnet. + set(value) + Set the shared hardware value to the average of the requested values. + set_and_wait(value) + Set the hardware values and wait for convergence. + unit() + Return the hardware-value units. + """ + + def __init__(self, hardwares: list[abstract.ReadWriteFloatScalar], model: MagnetModel): + """ + Initialize a per-magnet hardware accessor for serialized magnets. + """ + self._hardwares = hardwares + self._model = model + + def get(self) -> np.ndarray: + """Return the hardware value of each magnet.""" + return np.array([hardware.get() for hardware in self._hardwares]) + + def set(self, value: np.ndarray): + """ + Set the shared hardware value to the average of the requested values. + + Parameters + ---------- + value : np.ndarray + Requested hardware value of each magnet, in declaration order. + """ + self._hardwares[0].set(float(np.mean(value))) + + def set_and_wait(self, value: np.ndarray): + """ + Set the hardware values and wait for convergence. + + Parameters + ---------- + value : np.ndarray + Requested hardware value of each magnet, in declaration order. + + Raises + ------ + NotImplementedError + Waiting for readback convergence is not implemented for this accessor. + """ + raise NotImplementedError("Not implemented yet.") + + def unit(self) -> list[str]: + """Return the hardware-value units.""" + return self._model.get_hardware_units() + + +class ReadWriteSerializedStrengthArray(abstract.ReadWriteFloatArray): + """ + Read/write access to the strength of each magnet of a serialized-magnet family. + + Magnet ``i`` of the series has the strength ``k_i = f_i(I)``. Writing the strengths ``k_i`` sets the hardware + value ``I`` solving ``sum(f_i(I)) == sum(k_i)``, which is exact when the requested strengths are reachable with + one hardware value. Each strength is expressed in the unit of its own magnet, so this works for a series mixing + magnet functions. + + Parameters + ---------- + strengths : list[abstract.ReadWriteFloatScalar] + Per-magnet strength accessors. + hardwares : list[abstract.ReadWriteFloatScalar] + Per-magnet hardware accessors. Writing one of them moves the whole series. + model : MagnetModel + Serialized magnet model converting between strengths and hardware value. + + Methods + ------- + get() + Return the strength of each magnet. + set(value) + Set the hardware value giving the requested strengths. + set_and_wait(value) + Set the strengths and wait for convergence. + unit() + Return the physical strength units. + """ + + def __init__( + self, + strengths: list[abstract.ReadWriteFloatScalar], + hardwares: list[abstract.ReadWriteFloatScalar], + model: MagnetModel, + ): + """ + Initialize a per-magnet strength accessor for serialized magnets. + """ + self._strengths = strengths + self._hardwares = hardwares + self._model = model + + def get(self) -> np.ndarray: + """Return the strength of each magnet.""" + return np.array([strength.get() for strength in self._strengths]) + + def set(self, value: np.ndarray): + """ + Set the hardware value giving the requested strengths. + + Parameters + ---------- + value : np.ndarray + Requested strength of each magnet, in declaration order. + """ + self._hardwares[0].set(self._model.compute_hardware_from_strengths(value)) + + def set_and_wait(self, value: np.ndarray): + """ + Set the strengths and wait for convergence. + + Parameters + ---------- + value : np.ndarray + Requested strength of each magnet, in declaration order. + + Raises + ------ + NotImplementedError + Waiting for readback convergence is not implemented for this accessor. + """ + raise NotImplementedError("Not implemented yet.") + + def unit(self) -> list[str]: + """Return the physical strength units.""" + return self._model.get_strength_units() + @register_schema class SerializedMagnets(Element, DynamicValidation): @@ -171,8 +344,9 @@ class SerializedMagnets(Element, DynamicValidation): ---------- name : str Name of the serialized magnet group. - function : str - Magnet function identifier used to select the concrete virtual magnet type. + function : list[str] | str + Magnet function identifier selecting the concrete virtual magnet type. A single value applies to every + element, a list gives one function per element for a series mixing magnet functions. elements : list[str] | str Names of the individual magnets in the group. model : MagnetModel | None, optional @@ -185,9 +359,14 @@ class SerializedMagnets(Element, DynamicValidation): Attributes ---------- strength - Average strength of the magnets of the series, in physics unit + Average strength of the magnets of the series, in physics unit. Only defined when every magnet has the same + function. hardware Hardware value of the power supply shared by the series + strengths + Strength of each magnet of the series, in physics unit + hardwares + Hardware value of each magnet of the series Methods ------- @@ -195,6 +374,8 @@ class SerializedMagnets(Element, DynamicValidation): Return the number of magnets in the serialized group. get_magnets() Return the group's virtual single-function magnets. + is_homogeneous() + Return True when every magnet of the series has the same function. attach(peer, strengths, hardwares) Attach the group and its virtual magnets to a runtime peer. set_energy(energy) @@ -222,10 +403,15 @@ class SerializedMagnets(Element, DynamicValidation): ====================== =============================== ================================================== ``family.hardware`` ``I`` write ``I = x`` ``family.strength`` average ``sum(f_i(I)) / N`` solve ``sum(f_i(I)) / N = x`` for ``I``, write it + ``family.hardwares`` ``[I, ..., I]`` write ``I = mean(x)`` + ``family.strengths`` ``[f_1(I), ..., f_N(I)]`` solve ``sum(f_i(I)) = sum(x_i)`` for ``I``, write it ``magnet[i].hardware`` ``I`` write ``I = x``, all the magnets follow ``magnet[i].strength`` ``k_i = f_i(I)`` write ``I = f_i^-1(x)``, the others follow ====================== =============================== ================================================== + ``family.strength`` raises a ``PyAMLException`` for a series mixing magnet functions: a single strength value + cannot be both, say, a quadrupole and a sextupole strength. + Configuration ------------- The model can be defined inline or in a separate file. See @@ -245,7 +431,7 @@ class SerializedMagnets(Element, DynamicValidation): def __init__( self, name: str, - function: str, + function: list[str] | str, elements: list[str] | str, model: MagnetModel | None = None, description: str | None = None, @@ -256,25 +442,32 @@ def __init__( """ super().__init__(name, None, description) - self.function = function self.model = model - self.polynom = None + self.polynoms = None self.__strengths = None self.__hardwares = None + self.__strengths_array = None + self.__hardwares_array = None self.__virtuals: list[Magnet] = [] self.__elements = elements if isinstance(elements, list) else [elements] + self.functions: list[str] = function if isinstance(function, list) else [function] * len(self.__elements) + if len(self.functions) != len(self.__elements): + raise PyAMLException( + f"{name}: function must be a single value or one value per element " + f"({len(self.__elements)} elements, {len(self.functions)} functions)" + ) if peer is None: # Configuration part + for func in self.functions: + if func not in function_map: + raise PyAMLException(func + " not implemented for serialized magnet") # The submodels are built once here: the attached copies share them with the per-magnet accessors self.model.set_number_of_magnets(len(self.__elements)) - self.polynom = function_map[self.function].polynom - if self.function not in function_map: - raise PyAMLException(self.function + " not implemented for serialized magnet") - for element in self.__elements: - # Check mapping validity + self.polynoms = [function_map[func].polynom for func in self.functions] + for element, func in zip(self.__elements, self.functions, strict=True): # Create the virtual magnet for the corresponding magnet - vm = self.__create_virtual_magnet(element) + vm = self.__create_virtual_magnet(element, func) self.__virtuals.append(vm) # Register the virtual element in the factory to have a coherent factory and improve error reporting ELEMENT_REGISTRY.register(vm) @@ -285,17 +478,19 @@ def __init__( def _fill_device(self, holder: "ElementHolder") -> None: holder._fill_serialized_magnets(self) - def __create_virtual_magnet(self, name: str) -> Magnet: + def __create_virtual_magnet(self, name: str, function: str) -> Magnet: """ Create a virtual magnet for one serialized element. - The configured function mapping selects the concrete magnet class, and + The function of the element selects the concrete magnet class, and the serialized group's model is shared with the new virtual magnet. Parameters ---------- name : str Name assigned to the virtual magnet. + function : str + Function of the element, selecting the virtual magnet class. Returns ------- @@ -303,10 +498,14 @@ def __create_virtual_magnet(self, name: str) -> Magnet: Newly created virtual magnet linked to the serialized group. """ args = {"name": name, "model": self.model} - virtual: Magnet = function_map[self.function](**args) + virtual: Magnet = function_map[function](**args) virtual.set_model_name(self.get_name()) return virtual + def is_homogeneous(self) -> bool: + """Return True when every magnet of the series has the same function.""" + return len(set(self.functions)) == 1 + def get_nb_magnets(self) -> int: """Return the number of magnets in the serialized group.""" return len(self.__elements) @@ -339,11 +538,13 @@ def attach( Virtual magnets of the group, each bound to ``peer``. """ l = [] - n_ser_mag = SerializedMagnets(self._name, self.function, self.__elements, self.model, self.description, peer) + n_ser_mag = SerializedMagnets(self._name, self.functions, self.__elements, self.model, self.description, peer) if self.model.has_hardware(): n_ser_mag.__hardwares = ReadWriteSerializedHardware(hardwares, self.model) + n_ser_mag.__hardwares_array = ReadWriteSerializedHardwareArray(hardwares, self.model) if self.model.has_physics(): - n_ser_mag.__strengths = ReadWriteSerializedStrength(hardwares, self.model) + n_ser_mag.__strengths = ReadWriteSerializedStrength(hardwares, self.model, self.is_homogeneous()) + n_ser_mag.__strengths_array = ReadWriteSerializedStrengthArray(strengths, hardwares, self.model) l.append(n_ser_mag) # Construct a single magnet for each magnet. sub_magnets: list[Magnet] = [] @@ -375,6 +576,26 @@ def hardware(self) -> abstract.ReadWriteFloatScalar: raise PyAMLException(f"{str(self)} has no model that supports hardware units") return self.__hardwares + @property + def strengths(self) -> abstract.ReadWriteFloatArray: + """ + Strength of each magnet of the series, in physics unit + """ + self.check_peer() + if self.__strengths_array is None: + raise PyAMLException(f"{str(self)} has no model that supports physics units") + return self.__strengths_array + + @property + def hardwares(self) -> abstract.ReadWriteFloatArray: + """ + Hardware value of each magnet of the series, all equal to the shared power-supply value + """ + self.check_peer() + if self.__hardwares_array is None: + raise PyAMLException(f"{str(self)} has no model that supports hardware units") + return self.__hardwares_array + def set_energy(self, energy: float): """ Set beam energy for serialized-magnet strength conversion. diff --git a/tests/config/sr_serialized_magnets.yaml b/tests/config/sr_serialized_magnets.yaml index a8a3c486d..bbb7fb889 100644 --- a/tests/config/sr_serialized_magnets.yaml +++ b/tests/config/sr_serialized_magnets.yaml @@ -37,6 +37,22 @@ devices: type: pyaml.magnet.csvcurve file: sr/magnet_models/serialized_strength.csv powerconverter: srmag/ps-corr-sh1/c01-a-ch1/current + - type: pyaml.magnet.serialized_magnet + name: mixedFunctionSeries + function: [B1, B2] + elements: + - QF6D-C01 + - SD1A-C01 + model: + type: pyaml.magnet.linear_serialized_model + calibration_factors: 1.0 + calibration_offsets: 0.0 + unit: m-1 + hardware_unit: A + curves: + type: pyaml.magnet.csvcurve + file: sr/magnet_models/serialized_strength.csv + powerconverter: srmag/ps-mixed-series/current - type: pyaml.magnet.serialized_magnet name: QF1A function: B1 diff --git a/tests/magnet/test_serialized_magnets.py b/tests/magnet/test_serialized_magnets.py index 564ab6c1d..4e308c66d 100644 --- a/tests/magnet/test_serialized_magnets.py +++ b/tests/magnet/test_serialized_magnets.py @@ -1,8 +1,11 @@ import numpy as np import pytest +from pyaml import PyAMLException from pyaml.accelerator import Accelerator +from pyaml.magnet.quadrupole import Quadrupole from pyaml.magnet.serialized_magnet import SerializedMagnets +from pyaml.magnet.sextupole import Sextupole def check_no_diff(array: list[np.float64]) -> bool: @@ -202,3 +205,98 @@ def test_strength_computation(sr_file): # The family strength is the average strength of the magnets assert abs(np.mean(magnets_from_lattice_strengths) - 0.8) < 1e-6 assert magnets_strengths == magnets_from_lattice_strengths + + +SR_FILE = "tests/config/sr_serialized_magnets.yaml" +# A quadrupole (B1) and a sextupole (B2) on the same power supply +MIXED = "mixedFunctionSeries" + + +def _effective_length(element) -> float: + return element.Length if element.Length else 1.0 + + +def test_partial_strength_request_keeps_one_hardware_value_per_power_supply(): + sr: Accelerator = Accelerator.load(SR_FILE, include_locations=False, ignore_external=True) + family: SerializedMagnets = sr.design.serialized_magnet.get("QF1A") + model = family.model + current = np.array([family.hardware.get()]) + + assert model.compute_hardware_values_partial(current, {1: 0.8}).shape == current.shape + with pytest.raises(PyAMLException, match="current_hardware"): + model.compute_hardware_values_partial(np.array([80.0, 80.0]), {1: 0.8}) + + +def test_series_with_one_function_is_homogeneous(): + sr: Accelerator = Accelerator.load(SR_FILE, include_locations=False, ignore_external=True) + assert sr.design.serialized_magnet.get("QF1A").is_homogeneous() + assert not sr.design.serialized_magnet.get(MIXED).is_homogeneous() + + +def test_mixed_function_series_has_no_single_family_strength(): + sr: Accelerator = Accelerator.load(SR_FILE, include_locations=False, ignore_external=True) + family: SerializedMagnets = sr.design.serialized_magnet.get(MIXED) + with pytest.raises(PyAMLException, match="mixing magnet functions"): + family.strength.get() + with pytest.raises(PyAMLException, match="mixing magnet functions"): + family.strength.set(0.5) + + +def test_mixed_function_series_keeps_hardware_and_per_magnet_access(): + sr: Accelerator = Accelerator.load(SR_FILE, include_locations=False, ignore_external=True) + family: SerializedMagnets = sr.design.serialized_magnet.get(MIXED) + quad, sextu = family.get_magnets() + assert isinstance(quad, Quadrupole) + assert isinstance(sextu, Sextupole) + + family.hardware.set(50.0) + + assert family.hardware.get() == pytest.approx(50.0) + assert np.allclose(family.hardwares.get(), 50.0) + assert np.allclose(family.strengths.get(), [quad.strength.get(), sextu.strength.get()]) + + +def test_mixed_function_series_writes_each_magnet_in_its_own_polynom(): + sr: Accelerator = Accelerator.load(SR_FILE, include_locations=False, ignore_external=True) + family: SerializedMagnets = sr.design.serialized_magnet.get(MIXED) + ring = sr.design.get_lattice() + quad_element = ring.get_elements("QF6D-C01")[0] + sextu_element = ring.get_elements("SD1A-C01")[0] + sextu_b1_before = sextu_element.PolynomB[1] + + family.hardware.set(50.0) + + quad_strength = family.model.get_sub_model(0).compute_strengths([50.0])[0] + sextu_strength = family.model.get_sub_model(1).compute_strengths([50.0])[0] + assert quad_element.PolynomB[1] * _effective_length(quad_element) == pytest.approx(quad_strength) + assert sextu_element.PolynomB[2] * _effective_length(sextu_element) == pytest.approx(sextu_strength) + assert sextu_element.PolynomB[1] == sextu_b1_before + + +def test_mixed_function_series_strengths_converge_for_a_consistent_request(caplog): + sr: Accelerator = Accelerator.load(SR_FILE, include_locations=False, ignore_external=True) + family: SerializedMagnets = sr.design.serialized_magnet.get(MIXED) + family.hardware.set(50.0) + requested = family.strengths.get() + family.hardware.set(30.0) + + with caplog.at_level("WARNING"): + family.strengths.set(requested) + + assert family.hardware.get() == pytest.approx(50.0) + assert np.allclose(family.strengths.get(), requested) + assert "Inconsistent strengths" not in caplog.text + + +def test_mixed_function_series_strengths_warn_for_an_inconsistent_request(caplog): + sr: Accelerator = Accelerator.load(SR_FILE, include_locations=False, ignore_external=True) + family: SerializedMagnets = sr.design.serialized_magnet.get(MIXED) + family.hardware.set(50.0) + requested = family.strengths.get() * np.array([1.0, 1.1]) + + with caplog.at_level("WARNING"): + family.strengths.set(requested) + + # One power supply: the series gets the hardware value solving sum(f_i(I)) == sum(k_i) + assert family.hardware.get() == pytest.approx(family.model.compute_hardware_from_mean_strength(np.mean(requested))) + assert "Inconsistent strengths" in caplog.text diff --git a/tests/magnet/test_serialized_magnets_cs.py b/tests/magnet/test_serialized_magnets_cs.py index 1cc7a3e3d..591bdee7c 100644 --- a/tests/magnet/test_serialized_magnets_cs.py +++ b/tests/magnet/test_serialized_magnets_cs.py @@ -139,17 +139,25 @@ def test_design_and_live_agree(install_test_package): @pytest.mark.parametrize("mode", MODES) @pytest.mark.parametrize("install_test_package", TANGO_DUMMY, indirect=True) -def test_array_with_one_magnet_of_a_series_moves_the_whole_series(install_test_package, mode): +def test_array_with_one_magnet_of_a_series_completes_the_others_with_their_current_strength( + install_test_package, mode, caplog +): from pyaml.arrays.magnet_array import MagnetArray sr: Accelerator = Accelerator.load(CONFIG) family = _family(sr, mode) family.hardware.set(80.0) array = MagnetArray("ONE", [family.get_magnets()[1]]) - array.strengths.set(np.array([0.5])) - current = family.model.get_sub_model(1).compute_hardware_values([0.5])[0] - assert family.hardware.get() == pytest.approx(current) - assert array.strengths.get() == pytest.approx([0.5]) + + with caplog.at_level("WARNING"): + array.strengths.set(np.array([0.5])) + + # The other magnets of the series keep f_i(80), then sum(f_i(I)) == sum(k_i) is solved for I + completed = _sub_strengths(family, 80.0) + completed[1] = 0.5 + expected = family.model.compute_hardware_from_mean_strength(np.mean(completed)) + assert family.hardware.get() == pytest.approx(expected) + assert "Inconsistent strengths" in caplog.text @pytest.mark.parametrize("mode", MODES) @@ -178,8 +186,84 @@ def test_array_with_inconsistent_magnets_of_a_series(install_test_package, mode, strengths[:4] = requested with caplog.at_level("WARNING"): array.strengths.set(strengths) - # The power supply gets the mean of the currents asked by the magnets of the series - expected = np.mean([qf1a.model.get_sub_model(i).compute_hardware_values([s])[0] for i, s in enumerate(requested)]) + # Every magnet of QF1A is in the array: the power supply gets the I solving sum(f_i(I)) / N == mean(k_i) + expected = qf1a.model.compute_hardware_from_mean_strength(np.mean(requested)) assert qf1a.hardware.get() == pytest.approx(expected) assert qd2a.hardware.get() == pytest.approx(20.0) assert "Inconsistent strengths" in caplog.text + + +@pytest.mark.parametrize("mode", MODES) +@pytest.mark.parametrize("install_test_package", TANGO_DUMMY, indirect=True) +def test_array_with_part_of_a_series_completes_the_others_with_their_current_strength(install_test_package, mode): + from pyaml.arrays.magnet_array import MagnetArray + + sr: Accelerator = Accelerator.load(CONFIG) + family = _family(sr, mode) + family.hardware.set(80.0) + magnets = family.get_magnets() + array = MagnetArray("PART", [magnets[1], magnets[2]]) + + array.strengths.set(np.array([0.78, 0.82])) + + # Magnets 0 and 3 are not in the array: they keep f_i(80), then sum(f_i(I)) == sum(k_i) is solved for I + completed = _sub_strengths(family, 80.0) + completed[[1, 2]] = [0.78, 0.82] + expected = family.model.compute_hardware_from_mean_strength(np.mean(completed)) + assert family.hardware.get() == pytest.approx(expected) + + +@pytest.mark.parametrize("mode", MODES) +@pytest.mark.parametrize("install_test_package", TANGO_DUMMY, indirect=True) +def test_family_strengths_and_hardwares_read_every_magnet(install_test_package, mode): + sr: Accelerator = Accelerator.load(CONFIG) + family = _family(sr, mode) + family.hardware.set(80.0) + + # One call reads every magnet of the series: k_i = f_i(I), and the shared current I + assert np.allclose(family.strengths.get(), [m.strength.get() for m in family.get_magnets()]) + assert np.allclose(family.strengths.get(), _sub_strengths(family, 80.0)) + assert np.allclose(family.hardwares.get(), 80.0) + + +@pytest.mark.parametrize("mode", MODES) +@pytest.mark.parametrize("install_test_package", TANGO_DUMMY, indirect=True) +def test_family_strengths_set_solves_the_average_strength(install_test_package, mode, caplog): + sr: Accelerator = Accelerator.load(CONFIG) + family = _family(sr, mode) + family.hardware.set(80.0) + requested = np.array([0.78, 0.79, 0.80, 0.81]) + + with caplog.at_level("WARNING"): + family.strengths.set(requested) + + # One power supply: the series gets the I solving sum(f_i(I)) / N == mean(k_i) + assert family.hardware.get() == pytest.approx(family.model.compute_hardware_from_mean_strength(np.mean(requested))) + assert family.strength.get() == pytest.approx(np.mean(requested)) + # QF1A has a different calibration factor per magnet: these strengths cannot all be reached with one current + assert "Inconsistent strengths" in caplog.text + + +@pytest.mark.parametrize("mode", MODES) +@pytest.mark.parametrize("install_test_package", TANGO_DUMMY, indirect=True) +def test_family_hardwares_set_applies_the_average_current(install_test_package, mode): + sr: Accelerator = Accelerator.load(CONFIG) + family = _family(sr, mode) + + family.hardwares.set(np.array([79.0, 80.0, 81.0, 84.0])) + + assert family.hardware.get() == pytest.approx(81.0) + assert np.allclose(family.hardwares.get(), 81.0) + + +@pytest.mark.parametrize("mode", MODES) +@pytest.mark.parametrize("install_test_package", TANGO_DUMMY, indirect=True) +def test_family_array_set_of_get_does_not_change_the_machine(install_test_package, mode, caplog): + sr: Accelerator = Accelerator.load(CONFIG) + family = _family(sr, mode) + family.hardware.set(80.0) + with caplog.at_level("WARNING"): + for accessor in [family.strengths, family.hardwares]: + accessor.set(accessor.get()) + assert family.hardware.get() == pytest.approx(80.0) + assert "Inconsistent strengths" not in caplog.text From ce9f717a21382d20f140cf41510f496fa3f19a58 Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Fri, 16 Oct 2026 13:21:52 +0200 Subject: [PATCH 2/3] Drive a series from the magnets present in an array only, warn when a strength is clamped to the excitation curve --- pyaml/arrays/magnet_array.py | 2 +- pyaml/control/abstract_impl.py | 2 +- pyaml/magnet/linear_serialized_model.py | 108 +++++++++++++++------ tests/magnet/test_serialized_magnets_cs.py | 89 ++++++++++++++--- 4 files changed, 156 insertions(+), 45 deletions(-) diff --git a/pyaml/arrays/magnet_array.py b/pyaml/arrays/magnet_array.py index 52e06d83e..ec2669ea6 100644 --- a/pyaml/arrays/magnet_array.py +++ b/pyaml/arrays/magnet_array.py @@ -67,7 +67,7 @@ def set(self, value: np.array): nvalue = np.ones(self.__nb) * value if isinstance(value, float) else value if not self.__aggregator: # Magnets of a series share one power supply: it is written once, from the strengths of the - # magnets of the series present in the array, the others keeping their current strength + # magnets of the series present in the array, the others following the resulting hardware value series: dict[int, tuple[Magnet, dict[int, float]]] = {} for idx, m in enumerate(self.__magnets): if m.model is not None and m.model.shares_power_supply(): diff --git a/pyaml/control/abstract_impl.py b/pyaml/control/abstract_impl.py index 2ce608d39..176697ab1 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -432,7 +432,7 @@ def add_magnet(self, magnet: Magnet, devs: list[DeviceAccess]): """ # Magnets of a serialized group share the serialized model (one power supply for N strengths): # the power-supply current is computed by the model from the strengths of the magnets present in the array, - # the others keeping their current strength. + # the others following the resulting current. is_indexed = isinstance(magnet.strength, (abstract.RWMapper, RWSerializedStrengthScalar)) strengthIndex = magnet.strength.index() if is_indexed else 0 if magnet.model not in self.__models: diff --git a/pyaml/magnet/linear_serialized_model.py b/pyaml/magnet/linear_serialized_model.py index f0a85d052..39c852b6d 100644 --- a/pyaml/magnet/linear_serialized_model.py +++ b/pyaml/magnet/linear_serialized_model.py @@ -362,14 +362,14 @@ def compute_hardware_values_partial( """ Convert the strengths of some magnets of the series to the shared hardware value. - The magnets that are not requested keep the strength given by the current hardware value. The completed - strengths are then converted by :meth:`compute_hardware_from_strengths`, which logs a warning when they - cannot all be reached with a single hardware value. + Only the requested magnets define the hardware value: it solves ``sum(f_i(I)) == sum(k_i)`` over them. The + other magnets of the series follow the resulting hardware value. A warning is logged when the requested + strengths cannot all be reached with one hardware value, or when they are outside the excitation curves. Parameters ---------- current_hardware : np.array - Current hardware value of the series, one value per power supply. + Current hardware value of the series, one value per power supply. Only its shape is used. strengths_by_index : dict[int, float] Requested strengths, indexed by the position of the magnet in the series. @@ -381,13 +381,13 @@ def compute_hardware_values_partial( Raises ------ PyAMLException - If ``current_hardware`` does not hold one value per power supply of the series. + If ``current_hardware`` does not hold one value per power supply of the series, or if the average strength + of the requested magnets is not monotonic in the hardware value. """ _check_len(current_hardware, "current_hardware", len(self.get_device_names())) - strengths = self.compute_strengths(current_hardware) - for idx, strength in strengths_by_index.items(): - strengths[idx] = strength - return np.array([self.compute_hardware_from_strengths(strengths)]) + indices = list(strengths_by_index.keys()) + strengths = np.array(list(strengths_by_index.values()), dtype=float) + return np.array([self._compute_hardware_from_strengths(strengths, indices)]) def compute_mean_strength(self, current: float) -> float: """ @@ -410,7 +410,8 @@ def compute_hardware_from_mean_strength(self, strength: float) -> float: Return the hardware value giving an average strength of the series. The submodels are piecewise linear in the hardware value, so the average strength is piecewise linear on the - union of the curve breakpoints and is inverted exactly by interpolation. + union of the curve breakpoints and is inverted exactly by interpolation. A strength outside the range reachable + by the excitation curves is clamped to the end of the curves, and a warning is logged. Parameters ---------- @@ -427,16 +428,7 @@ def compute_hardware_from_mean_strength(self, strength: float) -> float: PyAMLException If the average strength is not monotonic in the hardware value. """ - currents = np.unique(np.concatenate([curve.get_curve()[:, 0] for curve in self.__curves])) - strengths = np.array([self.compute_mean_strength(current) for current in currents]) - steps = np.diff(strengths) - if np.all(steps <= 0): - currents, strengths = currents[::-1], strengths[::-1] - elif not np.all(steps >= 0): - raise PyAMLException( - f"Average strength of the series on {self._powerconverter} is not monotonic, it cannot be inverted" - ) - return float(np.interp(strength, strengths, currents)) + return self._compute_hardware_from_mean_strength(strength, list(range(self.__nbMagnets)))[0] def compute_hardware_from_strengths(self, strengths: np.ndarray) -> float: """ @@ -463,15 +455,77 @@ def compute_hardware_from_strengths(self, strengths: np.ndarray) -> float: """ strengths = np.asarray(strengths, dtype=float) _check_len(strengths, "strengths", self.__nbMagnets) - current = self.compute_hardware_from_mean_strength(float(np.mean(strengths))) - gaps = self.compute_strengths([current]) - strengths - if np.any(np.abs(gaps) > _SPREAD_RTOL * np.maximum(np.abs(strengths), 1.0)): - worst = int(np.argmax(np.abs(gaps))) + return self._compute_hardware_from_strengths(strengths, list(range(self.__nbMagnets))) + + def _compute_hardware_from_strengths(self, strengths: np.ndarray, indices: list[int]) -> float: + """ + Return the hardware value solving ``sum(f_i(I)) == sum(k_i)`` over some magnets of the series. + + Parameters + ---------- + strengths : np.ndarray + Requested strength ``k_i`` of each magnet in ``indices``. + indices : list[int] + Positions of the requested magnets in the series. + + Returns + ------- + float + Hardware value of the power supply. + """ + current, clamped = self._compute_hardware_from_mean_strength(float(np.mean(strengths)), indices) + if not clamped: + reached = np.array([self.__sub_models[idx].compute_strengths([current])[0] for idx in indices]) + gaps = reached - strengths + if np.any(np.abs(gaps) > _SPREAD_RTOL * np.maximum(np.abs(strengths), 1.0)): + worst = int(np.argmax(np.abs(gaps))) + logger.warning( + f"Inconsistent strengths requested for the series on {self._powerconverter}: the hardware value " + f"{current} misses the requested strengths by up to {gaps[worst]:.3g} (magnet {indices[worst]})" + ) + return current + + def _compute_hardware_from_mean_strength(self, strength: float, indices: list[int]) -> tuple[float, bool]: + """ + Return the hardware value giving an average strength of some magnets of the series. + + Parameters + ---------- + strength : float + Requested average strength of the magnets in ``indices``. + indices : list[int] + Positions of the magnets taken into account in the series. + + Returns + ------- + tuple[float, bool] + Hardware value, and whether it was clamped to the end of the excitation curves. + + Raises + ------ + PyAMLException + If the average strength of these magnets is not monotonic in the hardware value. + """ + currents = np.unique(np.concatenate([self.__curves[idx].get_curve()[:, 0] for idx in indices])) + strengths = np.array( + [np.mean([self.__sub_models[idx].compute_strengths([current])[0] for idx in indices]) for current in currents] + ) + steps = np.diff(strengths) + if np.all(steps <= 0): + currents, strengths = currents[::-1], strengths[::-1] + elif not np.all(steps >= 0): + raise PyAMLException( + f"Average strength of the series on {self._powerconverter} is not monotonic, it cannot be inverted" + ) + tolerance = _SPREAD_RTOL * max(abs(strengths[0]), abs(strengths[-1]), 1.0) + clamped = bool(strength < strengths[0] - tolerance or strength > strengths[-1] + tolerance) + if clamped: logger.warning( - f"Inconsistent strengths requested for the series on {self._powerconverter}: the hardware value " - f"{current} misses the requested strengths by up to {gaps[worst]:.3g} (magnet {worst})" + f"Strength {strength:.6g} requested for the series on {self._powerconverter} is outside the range " + f"[{strengths[0]:.6g}, {strengths[-1]:.6g}] reachable by the excitation curves, the hardware value is " + f"clamped to the end of the curves" ) - return current + return float(np.interp(strength, strengths, currents)), clamped def shares_power_supply(self) -> bool: """Return True, all the magnets of the series share one power supply.""" diff --git a/tests/magnet/test_serialized_magnets_cs.py b/tests/magnet/test_serialized_magnets_cs.py index 591bdee7c..1cf84f811 100644 --- a/tests/magnet/test_serialized_magnets_cs.py +++ b/tests/magnet/test_serialized_magnets_cs.py @@ -1,6 +1,7 @@ import numpy as np import pytest +from pyaml import PyAMLException from pyaml.accelerator import Accelerator from pyaml.magnet.serialized_magnet import SerializedMagnets @@ -139,9 +140,7 @@ def test_design_and_live_agree(install_test_package): @pytest.mark.parametrize("mode", MODES) @pytest.mark.parametrize("install_test_package", TANGO_DUMMY, indirect=True) -def test_array_with_one_magnet_of_a_series_completes_the_others_with_their_current_strength( - install_test_package, mode, caplog -): +def test_array_with_one_magnet_of_a_series_drives_the_series_with_its_own_submodel(install_test_package, mode, caplog): from pyaml.arrays.magnet_array import MagnetArray sr: Accelerator = Accelerator.load(CONFIG) @@ -152,12 +151,11 @@ def test_array_with_one_magnet_of_a_series_completes_the_others_with_their_curre with caplog.at_level("WARNING"): array.strengths.set(np.array([0.5])) - # The other magnets of the series keep f_i(80), then sum(f_i(I)) == sum(k_i) is solved for I - completed = _sub_strengths(family, 80.0) - completed[1] = 0.5 - expected = family.model.compute_hardware_from_mean_strength(np.mean(completed)) - assert family.hardware.get() == pytest.approx(expected) - assert "Inconsistent strengths" in caplog.text + # Same as magnets[1].strength.set(0.5): I = f_1^-1(0.5), the other magnets follow as f_j(I) + current = family.model.get_sub_model(1).compute_hardware_values([0.5])[0] + assert family.hardware.get() == pytest.approx(current) + assert np.allclose(family.strengths.get(), _sub_strengths(family, current)) + assert "Inconsistent strengths" not in caplog.text @pytest.mark.parametrize("mode", MODES) @@ -195,7 +193,7 @@ def test_array_with_inconsistent_magnets_of_a_series(install_test_package, mode, @pytest.mark.parametrize("mode", MODES) @pytest.mark.parametrize("install_test_package", TANGO_DUMMY, indirect=True) -def test_array_with_part_of_a_series_completes_the_others_with_their_current_strength(install_test_package, mode): +def test_array_with_part_of_a_series_drives_the_series_from_the_requested_magnets_only(install_test_package, mode): from pyaml.arrays.magnet_array import MagnetArray sr: Accelerator = Accelerator.load(CONFIG) @@ -203,14 +201,32 @@ def test_array_with_part_of_a_series_completes_the_others_with_their_current_str family.hardware.set(80.0) magnets = family.get_magnets() array = MagnetArray("PART", [magnets[1], magnets[2]]) + requested = np.array([0.78, 0.82]) + + array.strengths.set(requested) + + # Only magnets 1 and 2 define I: f_1(I) + f_2(I) == 0.78 + 0.82. Magnets 0 and 3 follow as f_j(I) + current = family.hardware.get() + reached = _sub_strengths(family, current) + assert np.mean(reached[[1, 2]]) == pytest.approx(np.mean(requested)) + assert np.allclose(family.strengths.get(), reached) + + +@pytest.mark.parametrize("mode", MODES) +@pytest.mark.parametrize("install_test_package", TANGO_DUMMY, indirect=True) +def test_strength_outside_the_excitation_curve_is_clamped_with_a_warning(install_test_package, mode, caplog): + sr: Accelerator = Accelerator.load(CONFIG) + family = _family(sr, mode) + family.hardware.set(80.0) + # Highest current of the QF1 excitation curve + curve_end = family.model.get_sub_model(0).compute_hardware_values([1e3])[0] - array.strengths.set(np.array([0.78, 0.82])) + with caplog.at_level("WARNING"): + family.strengths.set(np.full(family.get_nb_magnets(), 1e3)) - # Magnets 0 and 3 are not in the array: they keep f_i(80), then sum(f_i(I)) == sum(k_i) is solved for I - completed = _sub_strengths(family, 80.0) - completed[[1, 2]] = [0.78, 0.82] - expected = family.model.compute_hardware_from_mean_strength(np.mean(completed)) - assert family.hardware.get() == pytest.approx(expected) + assert family.hardware.get() == pytest.approx(curve_end) + assert "outside the range" in caplog.text + assert "Inconsistent strengths" not in caplog.text @pytest.mark.parametrize("mode", MODES) @@ -267,3 +283,44 @@ def test_family_array_set_of_get_does_not_change_the_machine(install_test_packag accessor.set(accessor.get()) assert family.hardware.get() == pytest.approx(80.0) assert "Inconsistent strengths" not in caplog.text + + +# QF1A on a power supply limited to [0, 90] A, with an array holding magnets C06 and C07 only +RANGE_CONFIG = "tests/config/sr_serialized_magnets_range.yaml" + + +@pytest.mark.parametrize("install_test_package", TANGO_DUMMY, indirect=True) +def test_power_supply_range_protects_every_write_to_a_series(install_test_package): + sr: Accelerator = Accelerator.load(RANGE_CONFIG) + family: SerializedMagnets = sr.live.serialized_magnet.get("QF1A") + magnets = family.get_magnets() + family.hardware.set(80.0) + # Strengths that need 100 A, outside the range of the power supply + too_strong = _sub_strengths(family, 100.0) + + writes = [ + lambda: sr.live.magnets.get("QF1A_PART").strengths.set(too_strong[[1, 2]]), + lambda: family.strengths.set(too_strong), + lambda: family.strength.set(np.mean(too_strong)), + lambda: magnets[1].strength.set(too_strong[1]), + lambda: family.hardware.set(100.0), + ] + for write in writes: + with pytest.raises(PyAMLException): + write() + # Nothing is written: the series stays at 80 A + assert family.hardware.get() == pytest.approx(80.0) + + +@pytest.mark.parametrize("install_test_package", TANGO_DUMMY, indirect=True) +def test_array_with_part_of_a_series_within_the_power_supply_range(install_test_package): + sr: Accelerator = Accelerator.load(RANGE_CONFIG) + family: SerializedMagnets = sr.live.serialized_magnet.get("QF1A") + family.hardware.set(80.0) + within = _sub_strengths(family, 70.0) + + sr.live.magnets.get("QF1A_PART").strengths.set(within[[1, 2]]) + + # Magnets C06 and C07 ask for 70 A, magnets C05 and C08 follow + assert family.hardware.get() == pytest.approx(70.0) + assert np.allclose(family.strengths.get(), within) From b2844bf56235235bf7736adebc2b2bd390e2c0d8 Mon Sep 17 00:00:00 2001 From: guillaumepichon Date: Fri, 16 Oct 2026 13:29:21 +0200 Subject: [PATCH 3/3] Drive a series from the magnets present in an array only, warn when a strength is clamped to the excitation curve --- tests/config/sr_serialized_magnets_range.yaml | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/config/sr_serialized_magnets_range.yaml diff --git a/tests/config/sr_serialized_magnets_range.yaml b/tests/config/sr_serialized_magnets_range.yaml new file mode 100644 index 000000000..0e18339f7 --- /dev/null +++ b/tests/config/sr_serialized_magnets_range.yaml @@ -0,0 +1,48 @@ +type: pyaml.accelerator +facility: ESRF +machine: sr +energy: 6e9 +simulators: + - type: pyaml.lattice.simulator + lattice: sr/lattices/ebs.mat + name: design +controls: + - type: tango.pyaml.controlsystem + tango_host: ebs-simu-3:10000 + name: live + catalog: + type: tango.pyaml.static_catalog + entries: + - type: tango.pyaml.static_catalog_entry + key: srmag/ps-qf1a/series/current + device: + type: tango.pyaml.attribute + attribute: srmag/ps-qf1a/series/current + unit: A + range: [0, 90] +data_folder: /data/store +arrays: + - type: pyaml.arrays.magnet + name: QF1A_PART + elements: + - QF1A-C06 + - QF1A-C07 +devices: + - type: pyaml.magnet.serialized_magnet + name: QF1A + function: B1 + elements: + - QF1A-C05 + - QF1A-C06 + - QF1A-C07 + - QF1A-C08 + model: + type: pyaml.magnet.linear_serialized_model + calibration_factors: [1.0, 1.01, 0.99, 1.02] + calibration_offsets: 0.0 + unit: m-1 + hardware_unit: A + curves: + type: pyaml.magnet.csvcurve + file: sr/magnet_models/QF1_strength.csv + powerconverter: srmag/ps-qf1a/series/current