From f0d0daba6dd5f9669e414adc189e16dd44c206f9 Mon Sep 17 00:00:00 2001 From: PONS Date: Wed, 6 Aug 2025 09:21:48 +0200 Subject: [PATCH 01/10] Added DeviceAccessList --- pyaml/control/deviceaccesslist.py | 45 +++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 pyaml/control/deviceaccesslist.py diff --git a/pyaml/control/deviceaccesslist.py b/pyaml/control/deviceaccesslist.py new file mode 100644 index 00000000..5b6aafb9 --- /dev/null +++ b/pyaml/control/deviceaccesslist.py @@ -0,0 +1,45 @@ +from abc import ABCMeta, abstractmethod + +import numpy.typing as npt +import numpy as np +from .readback_value import Value + +class DeviceAccessList(metaclass=ABCMeta): + """ + Abstract class providing access to a list of control system float variable + """ + + @abstractmethod + def name(self) -> list[str]: + """Return the names of the variables""" + pass + + @abstractmethod + def measure_name(self) -> list[str]: + """Return the names of the measures""" + pass + + @abstractmethod + def set(self, value: npt.NDArray[np.float64]): + """Write a list of control system device variable (i.e. a power supply currents)""" + pass + + @abstractmethod + def set_and_wait(self, value: npt.NDArray[np.float64]): + """Write a list control system device variable (i.e. a power supply currents)""" + pass + + @abstractmethod + def get(self) -> npt.NDArray[np.float64]: + """Return a list of setpoints of control system device variables""" + pass + + @abstractmethod + def readback(self) -> npt.NDArray[Value]: + """Return the measured variables""" + pass + + @abstractmethod + def unit(self) -> str: + """Return the variable unit""" + pass From a53ffe795f8ac28e195f4dd62f67775282623cc9 Mon Sep 17 00:00:00 2001 From: PONS Date: Wed, 6 Aug 2025 10:38:38 +0200 Subject: [PATCH 02/10] Added methods --- pyaml/control/deviceaccesslist.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pyaml/control/deviceaccesslist.py b/pyaml/control/deviceaccesslist.py index 5b6aafb9..415210f1 100644 --- a/pyaml/control/deviceaccesslist.py +++ b/pyaml/control/deviceaccesslist.py @@ -10,15 +10,25 @@ class DeviceAccessList(metaclass=ABCMeta): """ @abstractmethod - def name(self) -> list[str]: + def names(self) -> list[str]: """Return the names of the variables""" pass @abstractmethod - def measure_name(self) -> list[str]: + def set_names(self,names:list[str]): + """Set the names of the variables""" + pass + + @abstractmethod + def measure_names(self) -> list[str]: """Return the names of the measures""" pass + @abstractmethod + def set_measuresnames(self,names:list[str]): + """Set the names of the variables""" + pass + @abstractmethod def set(self, value: npt.NDArray[np.float64]): """Write a list of control system device variable (i.e. a power supply currents)""" From 96b6d6597aff305c2e2f41cc4de2ef73392d232a Mon Sep 17 00:00:00 2001 From: PONS Date: Wed, 6 Aug 2025 11:22:14 +0200 Subject: [PATCH 03/10] Updated methods --- pyaml/control/deviceaccesslist.py | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/pyaml/control/deviceaccesslist.py b/pyaml/control/deviceaccesslist.py index 415210f1..96f3456a 100644 --- a/pyaml/control/deviceaccesslist.py +++ b/pyaml/control/deviceaccesslist.py @@ -3,6 +3,7 @@ import numpy.typing as npt import numpy as np from .readback_value import Value +from .deviceaccess import DeviceAccess class DeviceAccessList(metaclass=ABCMeta): """ @@ -10,23 +11,13 @@ class DeviceAccessList(metaclass=ABCMeta): """ @abstractmethod - def names(self) -> list[str]: - """Return the names of the variables""" + def add_devices(devices:DeviceAccess | list[DeviceAccess]): + """Add a DeviceAccess to this list""" pass @abstractmethod - def set_names(self,names:list[str]): - """Set the names of the variables""" - pass - - @abstractmethod - def measure_names(self) -> list[str]: - """Return the names of the measures""" - pass - - @abstractmethod - def set_measuresnames(self,names:list[str]): - """Set the names of the variables""" + def get_devices() -> DeviceAccess | list[DeviceAccess]: + """Get the DeviceAccess list""" pass @abstractmethod @@ -45,7 +36,7 @@ def get(self) -> npt.NDArray[np.float64]: pass @abstractmethod - def readback(self) -> npt.NDArray[Value]: + def readback(self) -> np.array: """Return the measured variables""" pass From 846d19ae3f1b89757976ccb0b99aa12b216f1692 Mon Sep 17 00:00:00 2001 From: PONS Date: Wed, 6 Aug 2025 11:34:29 +0200 Subject: [PATCH 04/10] Updated methods --- pyaml/control/deviceaccesslist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyaml/control/deviceaccesslist.py b/pyaml/control/deviceaccesslist.py index 96f3456a..1aac41f6 100644 --- a/pyaml/control/deviceaccesslist.py +++ b/pyaml/control/deviceaccesslist.py @@ -11,12 +11,12 @@ class DeviceAccessList(metaclass=ABCMeta): """ @abstractmethod - def add_devices(devices:DeviceAccess | list[DeviceAccess]): + def add_devices(self, devices:DeviceAccess | list[DeviceAccess]): """Add a DeviceAccess to this list""" pass @abstractmethod - def get_devices() -> DeviceAccess | list[DeviceAccess]: + def get_devices(self) -> DeviceAccess | list[DeviceAccess]: """Get the DeviceAccess list""" pass From 333077845188b8a74809b00f7c353cba3fde470a Mon Sep 17 00:00:00 2001 From: PONS Date: Wed, 6 Aug 2025 11:35:36 +0200 Subject: [PATCH 05/10] Updated methods --- pyaml/control/deviceaccesslist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyaml/control/deviceaccesslist.py b/pyaml/control/deviceaccesslist.py index 1aac41f6..f3363201 100644 --- a/pyaml/control/deviceaccesslist.py +++ b/pyaml/control/deviceaccesslist.py @@ -5,7 +5,7 @@ from .readback_value import Value from .deviceaccess import DeviceAccess -class DeviceAccessList(metaclass=ABCMeta): +class DeviceAccessList(list[DeviceAccess],metaclass=ABCMeta): """ Abstract class providing access to a list of control system float variable """ From fefbea5dfb366eda8830bb88a15f8e5af99431c8 Mon Sep 17 00:00:00 2001 From: PONS Date: Wed, 6 Aug 2025 15:42:29 +0200 Subject: [PATCH 06/10] Updated array and magnet models --- pyaml/arrays/array.py | 20 ++++++++++-- pyaml/arrays/magnet_array.py | 53 ++++++++++++++++++++++++++++++-- pyaml/control/abstract.py | 4 +++ pyaml/control/abstract_impl.py | 8 ++++- pyaml/magnet/linear_cfm_model.py | 3 ++ pyaml/magnet/linear_model.py | 3 ++ pyaml/magnet/model.py | 13 ++++++++ pyaml/magnet/spline_model.py | 3 ++ 8 files changed, 100 insertions(+), 7 deletions(-) diff --git a/pyaml/arrays/array.py b/pyaml/arrays/array.py index 3ca93dcb..f657a4d4 100644 --- a/pyaml/arrays/array.py +++ b/pyaml/arrays/array.py @@ -5,6 +5,7 @@ import numpy as np from pydantic import BaseModel from ..lattice.element_holder import ElementHolder +from ..control.deviceaccesslist import DeviceAccessList class ArrayModel(BaseModel): @@ -12,6 +13,11 @@ class ArrayModel(BaseModel): """Family name""" elements: list[str] """List of pyaml element names""" + aggregator: DeviceAccessList | None + """ + Aggregator object. If none specified, writings and readings are serialized. + If no device list is specified, it is dynamically constructed. + """ class Array(object): """ @@ -23,6 +29,14 @@ def __init__(self, cfg: ArrayModel): def fill_array(self,holder:ElementHolder): raise "Array.fill_array() is not subclassed" - - - + + def init_agregator(self,holder:ElementHolder): + if(len(self._cfg.aggregator)==0): + # Construct dynamically aggregator + mag = holder.get_magnets(self._cfg.name) + devives_nb = [] + for m in mag: + devs = m.model.get_devices() + self._cfg.aggregator.add_devices(devs) + devives_nb.append(len(devs)) + mag.set_aggregator(self._cfg.aggregator,devives_nb) diff --git a/pyaml/arrays/magnet_array.py b/pyaml/arrays/magnet_array.py index fe2905b9..e65e4f62 100644 --- a/pyaml/arrays/magnet_array.py +++ b/pyaml/arrays/magnet_array.py @@ -1,20 +1,51 @@ from ..control.abstract import ReadWriteFloatArray from ..magnet.magnet import Magnet import numpy as np +from ..control.deviceaccesslist import DeviceAccessList class RWMagnetStrength(ReadWriteFloatArray): def __init__(self, magnets:list[Magnet]): self.__magnets = magnets + self.aggregator:DeviceAccessList = None + self.devices_nb:list[int] = None # Gets the values def get(self) -> np.array: - return np.array([m.strength.get() for m in self.__magnets]) + if not self.aggregator: + return np.array([m.strength.get() for m in self.__magnets]) + else: + allHardwareValues = self.aggregator.get() # Read all hardware setpoints + allStrength = [] + mIdx = 0 + idx = 0 + for m in self.__magnets: + nbDev = self.devices_nb[mIdx] + allStrength.append( m.model.compute_strengths(allHardwareValues[idx:idx+nbDev]) ) + mIdx += 1 + idx += nbDev + return allStrength # Sets the values def set(self, value:np.array): - for idx,m in enumerate(self.__magnets): - m.strength.set(value[idx]) + if not self.aggregator: + for idx,m in enumerate(self.__magnets): + m.strength.set(value[idx]) + else: + allHardwareValues = self.aggregator.get() # Read all hardware setpoints + newHardwareValues = [] + mIdx = 0 + idx = 0 + for m in self.__magnets: + # m is a single function magnet or a mapping to a + # combined function magnet (RWMapper) + nbDev = self.devices_nb[mIdx] + mStrengths = m.model.compute_strengths( allHardwareValues[idx:idx+nbDev] ) + mStrengths[m.index()] = value[mIdx] + newHardwareValues.append( m.model.compute_hardware_values(mStrengths) ) + mIdx += 1 + idx += nbDev + self.aggregator.set(newHardwareValues) # Sets the values and waits that the read values reach their setpoint def set_and_wait(self, value:np.array): @@ -24,10 +55,16 @@ def set_and_wait(self, value:np.array): def unit(self) -> list[str]: return [m.strength.unit() for m in self.__magnets] + # Set the aggregator (Control system only) + def set_aggregator(self,agg:DeviceAccessList,devices_nb:list[int]): + self.aggregator = agg + self.devices_nb = devices_nb + class RWMagnetHardware(ReadWriteFloatArray): def __init__(self, magnets:list[Magnet]): self.__magnets = magnets + self.aggregator:DeviceAccessList = None # Gets the values def get(self) -> np.array: @@ -46,6 +83,11 @@ def set_and_wait(self, value:np.array): def unit(self) -> list[str]: return [m.hardware.unit() for m in self.__magnets] + # Set the aggregator (Control system only) + def set_aggregator(self,agg:DeviceAccessList,devices_nb:list[int]): + self.aggregator = agg + self.devices_nb = devices_nb + class MagnetArray(list[Magnet]): """ Class that implements access to a magnet array @@ -55,6 +97,11 @@ def __init__(self,iterable): self.__rwstrengths = RWMagnetStrength(iterable) self.__rwhardwares = RWMagnetHardware(iterable) + # Set the aggregator (Control system only) + def set_aggregator(self,agg:DeviceAccessList,devices_nb:list[int]): + self.__rwstrengths.set_aggregator(agg,devices_nb) + self.__rwhardwares.set_aggregator(agg,devices_nb) + @property def strengths(self) -> RWMagnetStrength: return self.__rwstrengths diff --git a/pyaml/control/abstract.py b/pyaml/control/abstract.py index d97b0ded..dfe75db8 100644 --- a/pyaml/control/abstract.py +++ b/pyaml/control/abstract.py @@ -89,4 +89,8 @@ def set_and_wait(self, value:float): # Return the unit def unit(self) -> str: return self.bind.unit()[self.idx] + + # Return the mapped index + def index(self) -> int: + return self.idx diff --git a/pyaml/control/abstract_impl.py b/pyaml/control/abstract_impl.py index 7cf40212..237096ba 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -19,7 +19,10 @@ def set(self, value:float): def unit(self) -> str: return self.model.get_hardware_units()[0] - + + def index(self) -> int: + return 0 + #------------------------------------------------------------------------------ class RWStrengthScalar(abstract.ReadWriteFloatScalar): @@ -48,6 +51,9 @@ def set_and_wait(self, value:float): def unit(self) -> str: return self.__model.get_strength_units()[0] + def index(self) -> int: + return 0 + #------------------------------------------------------------------------------ class RWHardwareArray(abstract.ReadWriteFloatArray): diff --git a/pyaml/magnet/linear_cfm_model.py b/pyaml/magnet/linear_cfm_model.py index 48e5d51d..a25a73f1 100644 --- a/pyaml/magnet/linear_cfm_model.py +++ b/pyaml/magnet/linear_cfm_model.py @@ -153,5 +153,8 @@ def send_harware_values(self, currents: np.array): for idx, p in enumerate(self._cfg.powerconverters): p.set(currents[idx]) + def get_devices(self) -> list[DeviceAccess]: + return self._cfg.powerconverters + def set_magnet_rigidity(self, brho: np.double): self._brho = brho diff --git a/pyaml/magnet/linear_model.py b/pyaml/magnet/linear_model.py index 93340007..67a1c45a 100644 --- a/pyaml/magnet/linear_model.py +++ b/pyaml/magnet/linear_model.py @@ -69,6 +69,9 @@ def readback_hardware_values(self) -> np.array: def send_harware_values(self, currents: np.array): self.__ps.set(currents[0]) + def get_devices(self) -> list[DeviceAccess]: + return [self.__ps] + def set_magnet_rigidity(self, brho: np.double): self.__brho = brho diff --git a/pyaml/magnet/model.py b/pyaml/magnet/model.py index 6788f6a6..7c95f529 100644 --- a/pyaml/magnet/model.py +++ b/pyaml/magnet/model.py @@ -1,6 +1,7 @@ from abc import ABCMeta, abstractmethod import numpy as np import numpy.typing as npt +from ..control.deviceaccess import DeviceAccess class MagnetModel(metaclass=ABCMeta): """ @@ -101,6 +102,18 @@ def send_harware_values(self, hardware_values: npt.NDArray[np.float64]): """ pass + @abstractmethod + def get_devices(self) -> list[DeviceAccess]: + """ + Get device handles + + Returns + ------- + list[DeviceAccess] + Array of DeviceAcess + """ + pass + @abstractmethod def set_magnet_rigidity(self, brho: np.double): """ diff --git a/pyaml/magnet/spline_model.py b/pyaml/magnet/spline_model.py index 88abbef8..28ba9b6f 100644 --- a/pyaml/magnet/spline_model.py +++ b/pyaml/magnet/spline_model.py @@ -70,6 +70,9 @@ def readback_hardware_values(self) -> np.array: def send_harware_values(self, currents: np.array): self.__ps.set(currents[0]) + def get_devices(self) -> list[DeviceAccess]: + return [self.__ps] + def set_magnet_rigidity(self, brho: np.double): self.__brho = brho From 8e27dd1d79023b42cdd6afa05ec2cd14e0d82140 Mon Sep 17 00:00:00 2001 From: PONS Date: Wed, 6 Aug 2025 16:11:45 +0200 Subject: [PATCH 07/10] Comments --- pyaml/arrays/array.py | 5 ++--- pyaml/arrays/magnet_array.py | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/pyaml/arrays/array.py b/pyaml/arrays/array.py index f657a4d4..98b2d32d 100644 --- a/pyaml/arrays/array.py +++ b/pyaml/arrays/array.py @@ -21,16 +21,15 @@ class ArrayModel(BaseModel): class Array(object): """ - Class that implements access to arrays (families) + Class that implements configuration for access to arrays (families) """ - def __init__(self, cfg: ArrayModel): self._cfg = cfg def fill_array(self,holder:ElementHolder): raise "Array.fill_array() is not subclassed" - def init_agregator(self,holder:ElementHolder): + def init_aggregator(self,holder:ElementHolder): if(len(self._cfg.aggregator)==0): # Construct dynamically aggregator mag = holder.get_magnets(self._cfg.name) diff --git a/pyaml/arrays/magnet_array.py b/pyaml/arrays/magnet_array.py index e65e4f62..0e0bfc89 100644 --- a/pyaml/arrays/magnet_array.py +++ b/pyaml/arrays/magnet_array.py @@ -32,6 +32,8 @@ def set(self, value:np.array): for idx,m in enumerate(self.__magnets): m.strength.set(value[idx]) else: + # TODO: if the array does not contains some mappings to combined function + # magnets, the algorithm below can be optimized allHardwareValues = self.aggregator.get() # Read all hardware setpoints newHardwareValues = [] mIdx = 0 @@ -92,22 +94,48 @@ class MagnetArray(list[Magnet]): """ Class that implements access to a magnet array """ + def __init__(self,iterable): + """ + Construct a magnet array + + Parameters + ---------- + iterable + Magnet iterator + """ super().__init__(i for i in iterable) self.__rwstrengths = RWMagnetStrength(iterable) self.__rwhardwares = RWMagnetHardware(iterable) - # Set the aggregator (Control system only) def set_aggregator(self,agg:DeviceAccessList,devices_nb:list[int]): + """ + Set an aggregator for this array. + Aggregator allow fast control system access by parallelizing + call to underlying hardware. + + Parameters + ---------- + agg : DeviceAccessList + List of device access + devices_nb : list[int] + Number of devices needed by the magnet #idx. + """ self.__rwstrengths.set_aggregator(agg,devices_nb) self.__rwhardwares.set_aggregator(agg,devices_nb) - @property + @property def strengths(self) -> RWMagnetStrength: + """ + Give access to strength of each magnet of this array + """ return self.__rwstrengths @property def hardwares(self) -> RWMagnetHardware: + """ + Give access to hardware value of each magnet of this array + """ return self.__rwhardwares From ae6f78947ed6c500568b3d75952eb300ba312339 Mon Sep 17 00:00:00 2001 From: PONS Date: Thu, 7 Aug 2025 15:46:53 +0200 Subject: [PATCH 08/10] Various fix and code refactoring --- pyaml/arrays/array.py | 30 +++++++++++++++++---------- pyaml/arrays/hcorrector.py | 13 ++++-------- pyaml/arrays/magnet_array.py | 40 ++++++++++++++++++++---------------- pyaml/arrays/octupole.py | 13 ++++-------- pyaml/arrays/quadrupole.py | 16 +++++---------- pyaml/arrays/sextupole.py | 13 ++++-------- pyaml/arrays/skewoctu.py | 13 ++++-------- pyaml/arrays/skewquad.py | 13 ++++-------- pyaml/arrays/skewsext.py | 13 ++++-------- pyaml/arrays/vcorrector.py | 13 ++++-------- pyaml/instrument.py | 7 ++++--- tests/config/EBSTune.yaml | 5 +++-- 12 files changed, 81 insertions(+), 108 deletions(-) diff --git a/pyaml/arrays/array.py b/pyaml/arrays/array.py index 98b2d32d..42e3daa7 100644 --- a/pyaml/arrays/array.py +++ b/pyaml/arrays/array.py @@ -3,39 +3,47 @@ """ import numpy as np -from pydantic import BaseModel +from pydantic import BaseModel,ConfigDict from ..lattice.element_holder import ElementHolder from ..control.deviceaccesslist import DeviceAccessList -class ArrayModel(BaseModel): +class ArrayConfigModel(BaseModel): + + model_config = ConfigDict(arbitrary_types_allowed=True,extra="forbid") name: str """Family name""" elements: list[str] """List of pyaml element names""" - aggregator: DeviceAccessList | None + aggregator: DeviceAccessList | None = None """ Aggregator object. If none specified, writings and readings are serialized. If no device list is specified, it is dynamically constructed. """ -class Array(object): +class ArrayConfig(object): """ Class that implements configuration for access to arrays (families) """ - def __init__(self, cfg: ArrayModel): + def __init__(self, cfg: ArrayConfigModel): self._cfg = cfg def fill_array(self,holder:ElementHolder): raise "Array.fill_array() is not subclassed" - + def init_aggregator(self,holder:ElementHolder): - if(len(self._cfg.aggregator)==0): - # Construct dynamically aggregator + raise "Array.init_aggregator() is not subclassed" + +class MagnetArrayConfig(ArrayConfig): + + def __init__(self, cfg: ArrayConfigModel): + super().__init__(cfg) + + def init_aggregator(self,holder:ElementHolder): + if self._cfg.aggregator is not None and len(self._cfg.aggregator)==0: + # Construct dynamically aggregator for magnets mag = holder.get_magnets(self._cfg.name) - devives_nb = [] for m in mag: devs = m.model.get_devices() self._cfg.aggregator.add_devices(devs) - devives_nb.append(len(devs)) - mag.set_aggregator(self._cfg.aggregator,devives_nb) + mag.set_aggregator(self._cfg.aggregator) \ No newline at end of file diff --git a/pyaml/arrays/hcorrector.py b/pyaml/arrays/hcorrector.py index 02332bb6..a3d00291 100644 --- a/pyaml/arrays/hcorrector.py +++ b/pyaml/arrays/hcorrector.py @@ -1,18 +1,13 @@ -from .array import ArrayModel -from .array import Array +from .array import ArrayConfigModel +from .array import MagnetArrayConfig from ..lattice.element_holder import ElementHolder,MagnetType # Define the main class name for this module PYAMLCLASS = "HCorrector" -class ConfigModel(ArrayModel):... +class ConfigModel(ArrayConfigModel):... -class HCorrector(Array): - """ - Class that implements access to arrays (families) - """ - def __init__(self, cfg: ArrayModel): - super().__init__(cfg) +class HCorrector(MagnetArrayConfig): def fill_array(self,holder:ElementHolder): holder.fill_magnet_array(MagnetType.HCORRECTOR,self._cfg.name,self._cfg.elements) diff --git a/pyaml/arrays/magnet_array.py b/pyaml/arrays/magnet_array.py index 0e0bfc89..b8e9cd81 100644 --- a/pyaml/arrays/magnet_array.py +++ b/pyaml/arrays/magnet_array.py @@ -14,14 +14,15 @@ def __init__(self, magnets:list[Magnet]): def get(self) -> np.array: if not self.aggregator: return np.array([m.strength.get() for m in self.__magnets]) - else: + else: + print("Read via aggregator") allHardwareValues = self.aggregator.get() # Read all hardware setpoints - allStrength = [] + allStrength = np.zeros(len(self.__magnets)) mIdx = 0 idx = 0 for m in self.__magnets: nbDev = self.devices_nb[mIdx] - allStrength.append( m.model.compute_strengths(allHardwareValues[idx:idx+nbDev]) ) + allStrength[idx] = m.model.compute_strengths(allHardwareValues[idx:idx+nbDev])[m.strength.index()] mIdx += 1 idx += nbDev return allStrength @@ -32,19 +33,20 @@ def set(self, value:np.array): for idx,m in enumerate(self.__magnets): m.strength.set(value[idx]) else: - # TODO: if the array does not contains some mappings to combined function + print("Write via aggregator") + # TODO: if the array does not contains mappings to combined function # magnets, the algorithm below can be optimized allHardwareValues = self.aggregator.get() # Read all hardware setpoints - newHardwareValues = [] + newHardwareValues = np.zeros(len(self.aggregator)) mIdx = 0 idx = 0 - for m in self.__magnets: + for m in self.__magnets: # m is a single function magnet or a mapping to a # combined function magnet (RWMapper) nbDev = self.devices_nb[mIdx] mStrengths = m.model.compute_strengths( allHardwareValues[idx:idx+nbDev] ) - mStrengths[m.index()] = value[mIdx] - newHardwareValues.append( m.model.compute_hardware_values(mStrengths) ) + mStrengths[m.strength.index()] = value[mIdx] + newHardwareValues[idx:idx+nbDev] = m.model.compute_hardware_values(mStrengths) mIdx += 1 idx += nbDev self.aggregator.set(newHardwareValues) @@ -58,9 +60,11 @@ def unit(self) -> list[str]: return [m.strength.unit() for m in self.__magnets] # Set the aggregator (Control system only) - def set_aggregator(self,agg:DeviceAccessList,devices_nb:list[int]): - self.aggregator = agg - self.devices_nb = devices_nb + def set_aggregator(self,agg:DeviceAccessList): + self.aggregator = agg + self.devices_nb = [] + for m in self.__magnets: + self.devices_nb.append(len(m.model.get_devices())) class RWMagnetHardware(ReadWriteFloatArray): @@ -86,9 +90,11 @@ def unit(self) -> list[str]: return [m.hardware.unit() for m in self.__magnets] # Set the aggregator (Control system only) - def set_aggregator(self,agg:DeviceAccessList,devices_nb:list[int]): + def set_aggregator(self,agg:DeviceAccessList): self.aggregator = agg - self.devices_nb = devices_nb + self.devices_nb = [] + for m in self.__magnets: + self.devices_nb.append(len(m.model.get_devices())) class MagnetArray(list[Magnet]): """ @@ -108,7 +114,7 @@ def __init__(self,iterable): self.__rwstrengths = RWMagnetStrength(iterable) self.__rwhardwares = RWMagnetHardware(iterable) - def set_aggregator(self,agg:DeviceAccessList,devices_nb:list[int]): + def set_aggregator(self,agg:DeviceAccessList): """ Set an aggregator for this array. Aggregator allow fast control system access by parallelizing @@ -118,11 +124,9 @@ def set_aggregator(self,agg:DeviceAccessList,devices_nb:list[int]): ---------- agg : DeviceAccessList List of device access - devices_nb : list[int] - Number of devices needed by the magnet #idx. """ - self.__rwstrengths.set_aggregator(agg,devices_nb) - self.__rwhardwares.set_aggregator(agg,devices_nb) + self.__rwstrengths.set_aggregator(agg) + self.__rwhardwares.set_aggregator(agg) @property def strengths(self) -> RWMagnetStrength: diff --git a/pyaml/arrays/octupole.py b/pyaml/arrays/octupole.py index 62069847..a5123ae3 100644 --- a/pyaml/arrays/octupole.py +++ b/pyaml/arrays/octupole.py @@ -1,18 +1,13 @@ -from .array import ArrayModel -from .array import Array +from .array import ArrayConfigModel +from .array import MagnetArrayConfig from ..lattice.element_holder import ElementHolder,MagnetType # Define the main class name for this module PYAMLCLASS = "Octupole" -class ConfigModel(ArrayModel):... +class ConfigModel(ArrayConfigModel):... -class Octupole(Array): - """ - Class that implements access to arrays (families) - """ - def __init__(self, cfg: ArrayModel): - super().__init__(cfg) +class Octupole(MagnetArrayConfig): def fill_array(self,holder:ElementHolder): holder.fill_magnet_array(MagnetType.OCTUPOLE,self._cfg.name,self._cfg.elements) diff --git a/pyaml/arrays/quadrupole.py b/pyaml/arrays/quadrupole.py index 1805935e..dc4f606c 100644 --- a/pyaml/arrays/quadrupole.py +++ b/pyaml/arrays/quadrupole.py @@ -1,19 +1,13 @@ -from .array import ArrayModel -from .array import Array +from .array import ArrayConfigModel +from .array import MagnetArrayConfig from ..lattice.element_holder import ElementHolder,MagnetType # Define the main class name for this module PYAMLCLASS = "Quadrupole" -class ConfigModel(ArrayModel):... - -class Quadrupole(Array): - """ - Class that implements access to arrays (families) - """ - def __init__(self, cfg: ArrayModel): - super().__init__(cfg) +class ConfigModel(ArrayConfigModel):... +class Quadrupole(MagnetArrayConfig): + def fill_array(self,holder:ElementHolder): holder.fill_magnet_array(MagnetType.QUADRUPOLE,self._cfg.name,self._cfg.elements) - diff --git a/pyaml/arrays/sextupole.py b/pyaml/arrays/sextupole.py index bc45aaeb..27910244 100644 --- a/pyaml/arrays/sextupole.py +++ b/pyaml/arrays/sextupole.py @@ -1,18 +1,13 @@ -from .array import ArrayModel -from .array import Array +from .array import ArrayConfigModel +from .array import MagnetArrayConfig from ..lattice.element_holder import ElementHolder,MagnetType # Define the main class name for this module PYAMLCLASS = "Sextupole" -class ConfigModel(ArrayModel):... +class ConfigModel(ArrayConfigModel):... -class Sextupole(Array): - """ - Class that implements access to arrays (families) - """ - def __init__(self, cfg: ArrayModel): - super().__init__(cfg) +class Sextupole(MagnetArrayConfig): def fill_array(self,holder:ElementHolder): holder.fill_magnet_array(MagnetType.SEXTUPOLE,self._cfg.name,self._cfg.elements) diff --git a/pyaml/arrays/skewoctu.py b/pyaml/arrays/skewoctu.py index 8907ccab..09c25d94 100644 --- a/pyaml/arrays/skewoctu.py +++ b/pyaml/arrays/skewoctu.py @@ -1,18 +1,13 @@ -from .array import ArrayModel -from .array import Array +from .array import ArrayConfigModel +from .array import MagnetArrayConfig from ..lattice.element_holder import ElementHolder,MagnetType # Define the main class name for this module PYAMLCLASS = "SkewOctu" -class ConfigModel(ArrayModel):... +class ConfigModel(ArrayConfigModel):... -class SkewOctu(Array): - """ - Class that implements access to arrays (families) - """ - def __init__(self, cfg: ArrayModel): - super().__init__(cfg) +class SkewOctu(MagnetArrayConfig): def fill_array(self,holder:ElementHolder): holder.fill_magnet_array(MagnetType.SKEWOCTU,self._cfg.name,self._cfg.elements) diff --git a/pyaml/arrays/skewquad.py b/pyaml/arrays/skewquad.py index d744e277..cefa96e3 100644 --- a/pyaml/arrays/skewquad.py +++ b/pyaml/arrays/skewquad.py @@ -1,18 +1,13 @@ -from .array import ArrayModel -from .array import Array +from .array import ArrayConfigModel +from .array import MagnetArrayConfig from ..lattice.element_holder import ElementHolder,MagnetType # Define the main class name for this module PYAMLCLASS = "SkewQuad" -class ConfigModel(ArrayModel):... +class ConfigModel(ArrayConfigModel):... -class SkewQuad(Array): - """ - Class that implements access to arrays (families) - """ - def __init__(self, cfg: ArrayModel): - super().__init__(cfg) +class SkewQuad(MagnetArrayConfig): def fill_array(self,holder:ElementHolder): holder.fill_magnet_array(MagnetType.SKEWQUAD,self._cfg.name,self._cfg.elements) diff --git a/pyaml/arrays/skewsext.py b/pyaml/arrays/skewsext.py index 754e21b5..0e372de5 100644 --- a/pyaml/arrays/skewsext.py +++ b/pyaml/arrays/skewsext.py @@ -1,18 +1,13 @@ -from .array import ArrayModel -from .array import Array +from .array import ArrayConfigModel +from .array import MagnetArrayConfig from ..lattice.element_holder import ElementHolder,MagnetType # Define the main class name for this module PYAMLCLASS = "SkewSext" -class ConfigModel(ArrayModel):... +class ConfigModel(ArrayConfigModel):... -class SkewSext(Array): - """ - Class that implements access to arrays (families) - """ - def __init__(self, cfg: ArrayModel): - super().__init__(cfg) +class SkewSext(MagnetArrayConfig): def fill_array(self,holder:ElementHolder): holder.fill_magnet_array(MagnetType.SKEWSEXT,self._cfg.name,self._cfg.elements) diff --git a/pyaml/arrays/vcorrector.py b/pyaml/arrays/vcorrector.py index f82ae425..c3a9a14c 100644 --- a/pyaml/arrays/vcorrector.py +++ b/pyaml/arrays/vcorrector.py @@ -1,18 +1,13 @@ -from .array import ArrayModel -from .array import Array +from .array import ArrayConfigModel +from .array import MagnetArrayConfig from ..lattice.element_holder import ElementHolder,MagnetType # Define the main class name for this module PYAMLCLASS = "VCorrector" -class ConfigModel(ArrayModel):... +class ConfigModel(ArrayConfigModel):... -class VCorrector(Array): - """ - Class that implements access to arrays (families) - """ - def __init__(self, cfg: ArrayModel): - super().__init__(cfg) +class VCorrector(MagnetArrayConfig): def fill_array(self,holder:ElementHolder): holder.fill_magnet_array(MagnetType.VCORRECTOR,self._cfg.name,self._cfg.elements) diff --git a/pyaml/instrument.py b/pyaml/instrument.py index 4ae6c5eb..0cd70185 100644 --- a/pyaml/instrument.py +++ b/pyaml/instrument.py @@ -5,7 +5,7 @@ from .control.controlsystem import ControlSystem from .lattice.element import Element from .lattice.simulator import Simulator -from .arrays.array import Array +from .arrays.array import ArrayConfig from pydantic import BaseModel,ConfigDict # Define the main class name for this module @@ -25,7 +25,7 @@ class ConfigModel(BaseModel): """Simulator list""" data_folder: str """Data folder""" - arrays: list[Array] = None + arrays: list[ArrayConfig] = None """Element family""" devices: list[Element] """Element list""" @@ -43,7 +43,7 @@ def __init__(self, cfg: ConfigModel): for c in cfg.controls: if c.name() == "live": self.__live = c - c.init_cs() + c.init_cs() c.fill_device(cfg.devices) if cfg.simulators is not None: @@ -60,6 +60,7 @@ def __init__(self, cfg: ConfigModel): if cfg.controls is not None: for c in cfg.controls: a.fill_array(c) + a.init_aggregator(c) if cfg.energy is not None: self.set_energy(cfg.energy) diff --git a/tests/config/EBSTune.yaml b/tests/config/EBSTune.yaml index 2dbad2e2..27c0c029 100644 --- a/tests/config/EBSTune.yaml +++ b/tests/config/EBSTune.yaml @@ -8,14 +8,15 @@ instruments: lattice: sr/lattices/ebs.mat name: design controls: - - type: tango.pyaml.tango_controlsystem + - type: tango.pyaml.controlsystem tango_host: ebs-simu-3:10000 name: live - debug_level: 0 data_folder: /data/store arrays: - type: pyaml.arrays.quadrupole name: QForTune + aggregator: + type: tango.pyaml.multi_attribute elements: - QD2E-C04 - QD2A-C05 From 571e837b3e09e95f65a10e7c2f9d8c0ddf3dbff8 Mon Sep 17 00:00:00 2001 From: PONS Date: Mon, 11 Aug 2025 08:23:21 +0200 Subject: [PATCH 09/10] Added test and hardware unit check --- pyaml/arrays/magnet_array.py | 45 +++++++----- pyaml/configuration/curve.py | 2 +- pyaml/lattice/element_holder.py | 2 +- pyaml/magnet/cfm_magnet.py | 6 +- pyaml/magnet/linear_cfm_model.py | 4 ++ pyaml/magnet/model.py | 14 +++- ...ango_controlsystem.py => controlsystem.py} | 3 +- .../tango/tango/pyaml/multi_attribute.py | 71 +++++++++++++++++++ tests/test_aggregator.py | 25 +++++++ 9 files changed, 146 insertions(+), 26 deletions(-) rename tests/dummy_cs/tango/tango/pyaml/{tango_controlsystem.py => controlsystem.py} (95%) create mode 100644 tests/dummy_cs/tango/tango/pyaml/multi_attribute.py create mode 100644 tests/test_aggregator.py diff --git a/pyaml/arrays/magnet_array.py b/pyaml/arrays/magnet_array.py index b8e9cd81..b3fbfc82 100644 --- a/pyaml/arrays/magnet_array.py +++ b/pyaml/arrays/magnet_array.py @@ -5,23 +5,22 @@ class RWMagnetStrength(ReadWriteFloatArray): - def __init__(self, magnets:list[Magnet]): + def __init__(self, name:str, magnets:list[Magnet]): self.__magnets = magnets + self.__name = name self.aggregator:DeviceAccessList = None - self.devices_nb:list[int] = None # Gets the values def get(self) -> np.array: if not self.aggregator: return np.array([m.strength.get() for m in self.__magnets]) else: - print("Read via aggregator") allHardwareValues = self.aggregator.get() # Read all hardware setpoints allStrength = np.zeros(len(self.__magnets)) mIdx = 0 idx = 0 for m in self.__magnets: - nbDev = self.devices_nb[mIdx] + nbDev = len(m.model.get_devices()) allStrength[idx] = m.model.compute_strengths(allHardwareValues[idx:idx+nbDev])[m.strength.index()] mIdx += 1 idx += nbDev @@ -33,7 +32,6 @@ def set(self, value:np.array): for idx,m in enumerate(self.__magnets): m.strength.set(value[idx]) else: - print("Write via aggregator") # TODO: if the array does not contains mappings to combined function # magnets, the algorithm below can be optimized allHardwareValues = self.aggregator.get() # Read all hardware setpoints @@ -43,7 +41,7 @@ def set(self, value:np.array): for m in self.__magnets: # m is a single function magnet or a mapping to a # combined function magnet (RWMapper) - nbDev = self.devices_nb[mIdx] + nbDev = len(m.model.get_devices()) mStrengths = m.model.compute_strengths( allHardwareValues[idx:idx+nbDev] ) mStrengths[m.strength.index()] = value[mIdx] newHardwareValues[idx:idx+nbDev] = m.model.compute_hardware_values(mStrengths) @@ -62,24 +60,35 @@ def unit(self) -> list[str]: # Set the aggregator (Control system only) def set_aggregator(self,agg:DeviceAccessList): self.aggregator = agg - self.devices_nb = [] - for m in self.__magnets: - self.devices_nb.append(len(m.model.get_devices())) class RWMagnetHardware(ReadWriteFloatArray): - def __init__(self, magnets:list[Magnet]): + def __init__(self, name:str, magnets:list[Magnet]): + self.__name = name self.__magnets = magnets self.aggregator:DeviceAccessList = None + self.hasHardwareMapping = True # Gets the values def get(self) -> np.array: - return np.array([m.hardware.get() for m in self.__magnets]) + if not self.aggregator: + return np.array([m.hardware.get() for m in self.__magnets]) + else: + if not self.hasHardwareMapping: + raise Exception(f"Array {self.__name} contains elements that that do not support hardware units") + else: + return self.aggregator.get() # Sets the values def set(self, value:np.array): - for idx,m in enumerate(self.__magnets): - m.hardware.set(value[idx]) + if not self.aggregator: + for idx,m in enumerate(self.__magnets): + m.hardware.set(value[idx]) + else: + if not self.hasHardwareMapping: + raise Exception(f"Array {self.__name} contains elements that that do not support hardware units") + else: + self.aggregator.set(value) # Sets the values and waits that the read values reach their setpoint def set_and_wait(self, value:np.array): @@ -92,16 +101,15 @@ def unit(self) -> list[str]: # Set the aggregator (Control system only) def set_aggregator(self,agg:DeviceAccessList): self.aggregator = agg - self.devices_nb = [] for m in self.__magnets: - self.devices_nb.append(len(m.model.get_devices())) + self.hasHardwareMapping |= m.model.hasHardwareMapping() class MagnetArray(list[Magnet]): """ Class that implements access to a magnet array """ - def __init__(self,iterable): + def __init__(self,arrayName:str,iterable): """ Construct a magnet array @@ -111,8 +119,9 @@ def __init__(self,iterable): Magnet iterator """ super().__init__(i for i in iterable) - self.__rwstrengths = RWMagnetStrength(iterable) - self.__rwhardwares = RWMagnetHardware(iterable) + self.__name = arrayName + self.__rwstrengths = RWMagnetStrength(arrayName,iterable) + self.__rwhardwares = RWMagnetHardware(arrayName,iterable) def set_aggregator(self,agg:DeviceAccessList): """ diff --git a/pyaml/configuration/curve.py b/pyaml/configuration/curve.py index 772d31a5..6f0ba62a 100644 --- a/pyaml/configuration/curve.py +++ b/pyaml/configuration/curve.py @@ -26,5 +26,5 @@ def inverse(cls, curve:np.array) -> np.array: Curve to be inverted """ __curve = curve - __sortedCurve = __curve[__curve[:,1].argsort()[:-1]] + __sortedCurve = __curve[__curve[:,1].argsort()] return __sortedCurve[:,[1,0]] diff --git a/pyaml/lattice/element_holder.py b/pyaml/lattice/element_holder.py index 33f25f6e..0265e1aa 100644 --- a/pyaml/lattice/element_holder.py +++ b/pyaml/lattice/element_holder.py @@ -53,7 +53,7 @@ def fill_magnet_array(self,type:MagnetType,arrayName:str,elementNames:list[str]) a.append(self.get_magnet(type,name)) except Exception as err: raise Exception(f"MagnetArray {arrayName} : {err}") - self.__MAGNET_ARRAYS[arrayName] = MagnetArray(a) + self.__MAGNET_ARRAYS[arrayName] = MagnetArray(arrayName,a) def get_magnet(self,type:MagnetType,name:str) -> Magnet: fName = f"{_mmap[type]}({name})" diff --git a/pyaml/magnet/cfm_magnet.py b/pyaml/magnet/cfm_magnet.py index 5a1f065d..67f325bc 100644 --- a/pyaml/magnet/cfm_magnet.py +++ b/pyaml/magnet/cfm_magnet.py @@ -60,7 +60,7 @@ def __init__(self, cfg: ConfigModel): raise Exception(m[0] + " not found in underlying magnet model") self.polynoms.append(_fmap[m[0]].polynom) - def attach(self, strengths: abstract.ReadWriteFloatArray, currents: abstract.ReadWriteFloatArray) -> list[Magnet]: + def attach(self, strengths: abstract.ReadWriteFloatArray, hardwares: abstract.ReadWriteFloatArray) -> list[Magnet]: # Construct a single function magnet for each multipole of this combined function magnet l = [] @@ -68,8 +68,8 @@ def attach(self, strengths: abstract.ReadWriteFloatArray, currents: abstract.Rea args = {"name":m[1]} mclass:Magnet = _fmap[m[0]](ElementConfigModel(**args)) strength = RWMapper(strengths,idx) - current = RWMapper(currents,idx) - l.append(mclass.attach(strength,current)) + hardware = RWMapper(hardwares,idx) if self.model.hasHardwareMapping() else None + l.append(mclass.attach(strength,hardware)) return l def set_energy(self,E:float): diff --git a/pyaml/magnet/linear_cfm_model.py b/pyaml/magnet/linear_cfm_model.py index a25a73f1..a5fa28be 100644 --- a/pyaml/magnet/linear_cfm_model.py +++ b/pyaml/magnet/linear_cfm_model.py @@ -158,3 +158,7 @@ def get_devices(self) -> list[DeviceAccess]: def set_magnet_rigidity(self, brho: np.double): self._brho = brho + + def hasHardwareMapping(self) -> bool: + return (self.__nbPS == self.__nbFunction) and np.allclose(self.__matrix, np.eye(self.__nbFunction)) + diff --git a/pyaml/magnet/model.py b/pyaml/magnet/model.py index 7c95f529..56323391 100644 --- a/pyaml/magnet/model.py +++ b/pyaml/magnet/model.py @@ -116,7 +116,7 @@ def get_devices(self) -> list[DeviceAccess]: @abstractmethod def set_magnet_rigidity(self, brho: np.double): - """ + """ Set magnet rigidity Parameters @@ -125,3 +125,15 @@ def set_magnet_rigidity(self, brho: np.double): Magnet rigidity used to calculate power supply setpoints """ pass + + + def hasHardwareMapping(self) -> bool: + """ + Tells if the model allows to work in hardware unit. + + Returns + ---------- + bool + True if the model supports hardware unit + """ + return True diff --git a/tests/dummy_cs/tango/tango/pyaml/tango_controlsystem.py b/tests/dummy_cs/tango/tango/pyaml/controlsystem.py similarity index 95% rename from tests/dummy_cs/tango/tango/pyaml/tango_controlsystem.py rename to tests/dummy_cs/tango/tango/pyaml/controlsystem.py index debace5f..7e92e956 100644 --- a/tests/dummy_cs/tango/tango/pyaml/tango_controlsystem.py +++ b/tests/dummy_cs/tango/tango/pyaml/controlsystem.py @@ -11,8 +11,7 @@ class ConfigModel(BaseModel): name: str tango_host: str - debug_level: int - + debug_level: str=None class TangoControlSystem(ControlSystem): def __init__(self, cfg: ConfigModel): diff --git a/tests/dummy_cs/tango/tango/pyaml/multi_attribute.py b/tests/dummy_cs/tango/tango/pyaml/multi_attribute.py new file mode 100644 index 00000000..d2e1310b --- /dev/null +++ b/tests/dummy_cs/tango/tango/pyaml/multi_attribute.py @@ -0,0 +1,71 @@ +import pyaml +from numpy import typing as npt +import numpy as np +from pyaml.control.deviceaccess import DeviceAccess +from pydantic import BaseModel + +from pyaml.control.deviceaccesslist import DeviceAccessList +from .attribute import Attribute +from .attribute import ConfigModel as AttributeConfigModel + +PYAMLCLASS : str = "MultiAttribute" + + +class ConfigModel(BaseModel): + """ + Configuration model for a list of DeviceAccess. + + Attributes + ---------- + attributes : list of str + List of Tango attribute paths. + name : str, optional + Group name. + unit : str, optional + Unit of the attributes. + """ + attributes: list[str] = [] + name: str = "" + +class MultiAttribute(DeviceAccessList): + + def __init__(self, cfg:ConfigModel=None): + super().__init__() + self._cfg = cfg + if cfg.attributes: + for name in cfg.attributes: + self.add_devices(Attribute(AttributeConfigModel(name,cfg.unit))) + + def add_devices(self, devices: DeviceAccess | list[DeviceAccess]): + if isinstance(devices, list): + if any([not isinstance(device, Attribute) for device in devices]): + raise pyaml.PyAMLException("All devices must be instances of Attribute (tango.pyaml.attribute).") + super().extend(devices) + else: + if not isinstance(devices, Attribute): + raise pyaml.PyAMLException("Device must be an instance of Attribute (tango.pyaml.attribute).") + super().append(devices) + + def get_devices(self) -> DeviceAccess | list[DeviceAccess]: + if len(self)==1: + return self[0] + else: + return self + + def set(self, value: npt.NDArray[np.float64]): + print(f"MultiAttribute.set({len(value)} values)") + for idx,a in enumerate(self): + a.set(value[idx]) + + def set_and_wait(self, value: npt.NDArray[np.float64]): + pass + + def get(self) -> npt.NDArray[np.float64]: + print(f"MultiAttribute.get({len(self)} values)") + return [a.get() for a in self] + + def readback(self) -> np.array: + return [] + + def unit(self) -> list[str]: + return [a.unit() for a in self] diff --git a/tests/test_aggregator.py b/tests/test_aggregator.py new file mode 100644 index 00000000..c14e51b6 --- /dev/null +++ b/tests/test_aggregator.py @@ -0,0 +1,25 @@ +from pyaml.pyaml import pyaml,PyAML +from pyaml.instrument import Instrument +from pyaml.lattice.element_holder import MagnetType +from pyaml.arrays.magnet_array import MagnetArray +from pyaml.configuration.factory import Factory +import numpy as np +import pytest + +@pytest.mark.parametrize("install_test_package", [{ + "name": "tango", + "path": "tests/dummy_cs/tango" +}], indirect=True) +def test_tune(install_test_package,capfd): + + ml:PyAML = pyaml("tests/config/EBSTune.yaml") + sr:Instrument = ml.get('sr') + + quadForTuneLive = sr.live.get_magnets("QForTune") + quadForTuneLive.strengths.set(np.zeros(len(quadForTuneLive))) + strs = quadForTuneLive.strengths.get() + out, err = capfd.readouterr() + assert( "MultiAttribute.set(124 values)" in out ) + assert( "MultiAttribute.get(124 values)" in out ) + + Factory.clear() From 7ff48e697db9858ea8d886d71d8f9a7531a062f0 Mon Sep 17 00:00:00 2001 From: PONS Date: Mon, 11 Aug 2025 08:30:57 +0200 Subject: [PATCH 10/10] Updated custom MagnetModel example --- tests/external/pyaml_external/external_magnet_model.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/external/pyaml_external/external_magnet_model.py b/tests/external/pyaml_external/external_magnet_model.py index 73b360cb..2f5aa86f 100644 --- a/tests/external/pyaml_external/external_magnet_model.py +++ b/tests/external/pyaml_external/external_magnet_model.py @@ -70,6 +70,13 @@ def send_harware_values(self,currents:np.array): self._ps.set(currents) pass + def get_devices(self) -> list[DeviceAccess]: + return [self._ps,self.id] + + def hasHardwareMapping(self) -> bool: + # No trivial conversion between strength and hardware unit + return False + # Set magnet rigidity def set_magnet_rigidity(self,brho:np.double): pass