From ca42f9e3f0940844b87560af59972bf2bc42f0da Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Mon, 8 Sep 2025 11:56:39 +0200 Subject: [PATCH 01/14] Initial implementation of pyAML BPM interfaces for control system and simulator --- pyaml/bpm/bpm.py | 74 ++++++++++++++++++++++ pyaml/bpm/bpm_model.py | 92 +++++++++++++++++++++++++++ pyaml/bpm/bpm_tiltoffset_model.py | 102 ++++++++++++++++++++++++++++++ pyaml/control/abstract_impl.py | 59 ++++++++++++++++- pyaml/control/controlsystem.py | 32 ++++++---- pyaml/lattice/abstract_impl.py | 95 ++++++++++++++++++++++++++++ pyaml/lattice/element_holder.py | 10 ++- pyaml/lattice/simulator.py | 10 ++- 8 files changed, 457 insertions(+), 17 deletions(-) create mode 100644 pyaml/bpm/bpm.py create mode 100644 pyaml/bpm/bpm_model.py create mode 100644 pyaml/bpm/bpm_tiltoffset_model.py diff --git a/pyaml/bpm/bpm.py b/pyaml/bpm/bpm.py new file mode 100644 index 00000000..10abfb08 --- /dev/null +++ b/pyaml/bpm/bpm.py @@ -0,0 +1,74 @@ +from pyaml.lattice.element import Element, ElementConfigModel +from pyaml.pyaml.lattice.abstract_impl import RBpmPositionArray, RWBpmOffsetArray, RBpmTiltScalar +from ..control.deviceaccess import DeviceAccess +from ..control import abstract +from typing import Self + +PYAMLCLASS = "BPM" + +class BPMConfigModel(ElementConfigModel): + """ + Class providing access to BPM configuration parameters + """ + + def __init__(self, hardware_name: str): + """ + Construct a BPM configuration model + + Parameters + ---------- + name : str + Element name + """ + hardware_name: str + +class BPM (Element): + """ + Class providing access to one BPM of a physical or simulated lattice + """ + + def __init__(self, name: str, hardware: DeviceAccess = None, model: + BPMModel = None): + """ + Construct a BPM + + Parameters + ---------- + name : str + Element name + hardware : DeviceAccess + Direct access to a hardware (bypass the BPM model) + model : BPMModel + BPM model in charge of computing beam position + """ + super().__init__(name) + self.__model = model + self.__hardware = hardware + self.__positions = None + self.__offset = None + self.__tilt = None + + if hardware is not None: + # TODO + # Direct access to a BPM device that supports beam position + # computation + raise Exception( + "%s, hardware access not implemented" % + (self.__class__.__name__, name)) + @property + def hardware(self) -> abstract.ReadWriteFloatScalar: + if self.__hardware is None: + raise Exception(f"{str(self)} has no model that supports hardware units") + return self.__hardware + + def attach(self, positions: RBpmPositionArray , offset: RWBpmOffsetArray, tilt: RBpmTiltScalar) -> Self: + # Attach positions, offset and tilt attributes and returns a new + # reference + obj = self.__class__(self._cfg) + obj.__model = self.__model + obj.__hardware = self.__hardware + obj.__positions = positions + obj.__offset = offset + obj.__tilt = tilt + return obj + diff --git a/pyaml/bpm/bpm_model.py b/pyaml/bpm/bpm_model.py new file mode 100644 index 00000000..ca010176 --- /dev/null +++ b/pyaml/bpm/bpm_model.py @@ -0,0 +1,92 @@ +from abc import ABCMeta, abstractmethod +import numpy as np +from numpy.typing import NDArray + +class BPMModel(metaclass=ABCMeta): + """ + Abstract class providing interface to accessing BPM positions, offsets, + tilts. + """ + @abstractmethod + def read_hardware_positions(self) -> NDArray[np.float64]: + """ + Read horizontal and vertical positions from a BPM. + Returns + ------- + NDArray[np.float64] + Array of shape (2,) containing the horizontal and vertical + positions + """ + pass + + @abstractmethod + def read_hardware_tilt_value(self) -> float: + """ + Read the tilt value from a BPM. + Returns + ------- + float + The tilt value of the BPM + """ + pass + + @abstractmethod + def read_hardware_offset_values(self) -> NDArray[np.float64]: + """ + Read the offset values from a BPM. + Returns + ------- + NDArray[np.float64] + Array of shape (2,) containing the horizontal and vertical + offsets + """ + pass + + @abstractmethod + def set_hardware_tilt_value(self, tilt: float): + """ + Set the tilt value of a BPM. + Parameters + ---------- + tilt : float + The tilt value to set for the BPM + Returns + ------- + None + """ + pass + + @abstractmethod + def set_hardware_offset_values(self, offset_values: NDArray[np.float64]): + """ + Set the offset values of a BPM + Parameters + ---------- + offset_values : NDArray[np.float64] + Array of shape (2,) containing the horizontal and vertical + offsets to set for the BPM + """ + pass + + @abstractmethod + def get_hardware_angle_unit(self) -> str: + """ + Get the hardware unit for BPM readings. + Returns + ------- + str + The unit of measurement for BPM hardware values. + """ + pass + + @abstractmethod + def get_hardware_position_units(self) -> list[str]: + """ + Get the hardware units for BPM positions and offsets. + Returns + ------- + list[str] + List of units for horizontal and vertical positions and offsets. + """ + pass + diff --git a/pyaml/bpm/bpm_tiltoffset_model.py b/pyaml/bpm/bpm_tiltoffset_model.py new file mode 100644 index 00000000..32b2c9cd --- /dev/null +++ b/pyaml/bpm/bpm_tiltoffset_model.py @@ -0,0 +1,102 @@ +from pyaml.bpm.bpm_model import BPMModel +from pydantic import BaseModel,ConfigDict +import numpy as np +from ..control.deviceaccess import DeviceAccess + +# Define the main class name for this module +PYAMLCLASS = "BPMTiltOffsetModel" + +class ConfigModel(BaseModel): + + model_config = ConfigDict(arbitrary_types_allowed=True,extra="forbid") + + bpm_device: DeviceAccess + """Power converter device to apply currrent""" + position_unit: str + """Unit of the positions (i.e. mm)""" + tilt_unit: str + """Unit of the tilt (i.e. rad)""" + offset_unit: str + """Unit of the offsets (i.e. mm)""" + +class BPMTiltOffsetModel(BPMModel): + """ + Concrete implementation of BPMModel that simulates a BPM with tilt and + offset values. + """ + def __init__(self, cfg: ConfigModel): + self._cfg = cfg + + self.__position_unit = cfg.position_unit + self.__tilt_unit = cfg.tilt_unit + self.__offset_unit = cfg.offset_unit + self.__position_hardware_unit = cfg.bpm_device.unit() + self.__bpm_device = cfg.bpm_device + + def read_hardware_position_values(self) -> np.ndarray: + """ + Simulate reading the position values from a BPM. + Returns + ------- + np.ndarray + Array of shape (2,) containing the horizontal and vertical + positions + """ + return self.__bpm_device.get_position() + + def set_hardware_position_values(self, position_values: np.ndarray): + """ + Simulate setting the position values of a BPM + Parameters + ---------- + position_values : np.ndarray + Array of shape (2,) containing the horizontal and vertical + positions to set for the BPM + """ + self.__bpm_device.set_position(position_values) + + def read_hardware_tilt_value(self) -> float: + """ + Simulate reading the tilt value from a BPM. + Returns + ------- + float + The tilt value of the BPM + """ + return self.__bpm_device.get_tilt() + + def read_hardware_offset_values(self) -> np.ndarray: + """ + Simulate reading the offset values from a BPM. + Returns + ------- + np.ndarray + Array of shape (2,) containing the horizontal and vertical + offsets + """ + return self.__bpm_device.get_offset() + + def set_hardware_tilt_value(self, tilt: float): + """ + Simulate setting the tilt value of a BPM. + Parameters + ---------- + tilt : float + The tilt value to set for the BPM + Returns + ------- + None + """ + self.__bpm_device.set_tilt(tilt) + + def set_hardware_offset_values(self, offset_values: np.ndarray): + """ + Simulate setting the offset values of a BPM + Parameters + ---------- + offset_values : np.ndarray + Array of shape (2,) containing the horizontal and vertical + offsets to set for the BPM + """ + self.__bpm_device.set_offset(offset_values) + diff --git a/pyaml/control/abstract_impl.py b/pyaml/control/abstract_impl.py index 7cf40212..a6dfa129 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -1,7 +1,8 @@ from pyaml.control import abstract from pyaml.magnet.model import MagnetModel +from pyaml.bpm.bpm_model import BPMModel import numpy as np - +from numpy.typing import NDArray #------------------------------------------------------------------------------ class RWHardwareScalar(abstract.ReadFloatScalar): @@ -102,6 +103,62 @@ def set_and_wait(self, value:np.array): def unit(self) -> list[str]: return self.model.get_strength_units() +#------------------------------------------------------------------------------ + +class RbpmArray(abstract.ReadFloatArray): + """ + Class providing read access to a BPM array of a control system + """ + def __init__(self, model:BPMModel): + self.__model = model + + # Gets the value + def get(self) -> np.array: + return self.__model.read_hardware_positions() + + # Gets the unit of the value + def unit(self) -> list[str]: + return self.__model.get_hardware_position_units() + +#------------------------------------------------------------------------------ + +class RWbpmTiltScalar(abstract.ReadFloatScalar): + """ + Class providing read access to a BPM tilt of a control system + """ + def __init__(self, model:BPMModel): + self.__model = model + + # Gets the value + def get(self) -> float: + return self.__model.read_hardware_tilt_value() + + def set(self, value:float): + self.__model.set_hardware_tilt_value(value) + + # Gets the unit of the value + def unit(self) -> str: + return self.__model.get_hardware_angle_unit() +#------------------------------------------------------------------------------ + +class RWBpmOffsetArray(abstract.ReadWriteFloatArray): + """ + Class providing read write access to a BPM offset of a control system + """ + def __init__(self, model: BPMModel): + self.__model = model + + # Gets the value + def get(self) -> NDArray[np.float64]: + return self.__model.read_hardware_offset_values() + + # Sets the value + def set(self, value: NDArray[np.float64]): + self.__model.set_hardware_offset_values(value) + + # Gets the unit of the value + def unit(self) -> str: + return self.__model.get_hardware_position_units()[0] diff --git a/pyaml/control/controlsystem.py b/pyaml/control/controlsystem.py index da2de346..d2c65ba5 100644 --- a/pyaml/control/controlsystem.py +++ b/pyaml/control/controlsystem.py @@ -45,17 +45,21 @@ def fill_device(self,elements:list[Element]): List of elements coming from the configuration file to attach to this control system """ for e in elements: - if isinstance(e,Magnet): - current = RWHardwareScalar(e.model) - strength = RWStrengthScalar(e.model) - # Create a unique ref for this control system - m = e.attach(strength,current) - self.add_magnet(str(m),m) - elif isinstance(e,CombinedFunctionMagnet): - self.add_magnet(str(e),e) - currents = RWHardwareArray(e.model) - strengths = RWStrengthArray(e.model) - # Create unique refs of each function for this control system - ms = e.attach(strengths,currents) - for m in ms: - self.add_magnet(str(m),m) + if isinstance(e,Magnet): + current = RWHardwareScalar(e.model) + strength = RWStrengthScalar(e.model) + # Create a unique ref for this control system + m = e.attach(strength,current) + self.add_magnet(str(m),m) + elif isinstance(e,CombinedFunctionMagnet): + self.add_magnet(str(e),e) + currents = RWHardwareArray(e.model) + strengths = RWStrengthArray(e.model) + # Create unique refs of each function for this control system + ms = e.attach(strengths,currents) + for m in ms: + self.add_magnet(str(m),m) + elif isinstance(e,BPM): + self.add_bpm(str(e),e) + else: + pass diff --git a/pyaml/lattice/abstract_impl.py b/pyaml/lattice/abstract_impl.py index 6ed52cac..e5b771c4 100644 --- a/pyaml/lattice/abstract_impl.py +++ b/pyaml/lattice/abstract_impl.py @@ -142,3 +142,98 @@ def set_and_wait(self, value:np.array): def unit(self) -> list[str]: return self.unitconv.get_strength_units() +#------------------------------------------------------------------------------ + +class RBpmArray(abstract.ReadFloatArray): + """ + Class providing read access to a BPM position (array) of a simulator. + Position in pyAT is calculated using find_orbit function, which returns the + orbit at a specified index. The position is then extracted from the orbit + array as the first two elements (x, y). + """ + + def __init__(self, element: at.Element, lattice: at.Lattice): + self.element = element + self.lattice = lattice + + + # Gets the value + def get(self) -> np.array: + index = self.lattice.get_refpts(self.element.name) + _, orbit = at.find_orbit(self.lattice, index) + return orbit[0, [0, 2]] + + # Gets the unit of the value + def unit(self) -> list[str]: + return ['mm', 'mm'] + +#------------------------------------------------------------------------------ + +class RWBpmOffsetArray(abstract.ReadWriteFloatArray): + """ + Class providing read write access to a BPM offset (array) of a simulator. + Offset in pyAT is defined in Offset attribute as a 2-element array. + """ + + def __init__(self, element:at.Element): + self.element = element + try: + self.offset = element.__getattribute__('Offset') + except AttributeError: + self.offset = None + + # Gets the value + def get(self) -> np.array: + if self.offset is None: + raise ValueError("Element does not have an Offset attribute.") + return self.offset + + # Sets the value + def set(self, value:np.array): + if self.offset is None: + raise ValueError("Element does not have an Offset attribute.") + if len(value) != 2: + raise ValueError("BPM offset must be a 2-element array.") + self.offset = value + + # Sets the value and wait that the read value reach the setpoint + def set_and_wait(self, value:np.array): + raise NotImplementedError("Not implemented yet.") + + # Gets the unit of the value + def unit(self) -> list[str]: + return ['mm', 'mm'] # Assuming all offsets are in mm + +#------------------------------------------------------------------------------ + +class RWBpmTiltScalar(abstract.ReadWriteFloatScalar): + """ + Class providing read write access to a BPM tilt of a simulator. Tilt in + pyAT is defined in Rotation attribute as a first element. + """ + + def __init__(self, element:at.Element): + self.element = element + try: + self.tilt = element.__getattribute__('Rotation')[0] + except AttributeError: + self.tilt = None + + # Gets the value + def get(self) -> float: + if self.tilt is None: + raise ValueError("Element does not have a Tilt attribute.") + return self.tilt + + # Sets the value + def set(self, value:float, ): + self.tilt = value + self.element.__setattr__('Rotation', [value, None, None]) + + # Sets the value and wait that the read value reach the setpoint + def set_and_wait(self, value:float): + raise NotImplementedError("Not implemented yet.") + + # Gets the unit of the value + def unit(self) -> str: + return 'rad' # Assuming BPM tilts are in rad diff --git a/pyaml/lattice/element_holder.py b/pyaml/lattice/element_holder.py index 33f25f6e..fc57b303 100644 --- a/pyaml/lattice/element_holder.py +++ b/pyaml/lattice/element_holder.py @@ -71,4 +71,12 @@ def get_magnets(self,name:str) -> MagnetArray: if name not in self.__MAGNET_ARRAYS: raise Exception(f"Magnet array {name} not defined") return self.__MAGNET_ARRAYS[name] - \ No newline at end of file + + def get_bpm(self,name:str) -> Element: + if name not in self.__BPMS: + raise Exception(f"BPM {name} not defined") + return self.__BPMS[name] + + def add_bpm(self,name:str,bpm:Element): + self.__BPMS[name] = bpm + diff --git a/pyaml/lattice/simulator.py b/pyaml/lattice/simulator.py index 4056a534..95e9eee2 100644 --- a/pyaml/lattice/simulator.py +++ b/pyaml/lattice/simulator.py @@ -4,11 +4,12 @@ from .element import Element from pathlib import Path from ..magnet.magnet import Magnet +from pyaml.bpm.bpm import BPM from ..magnet.cfm_magnet import CombinedFunctionMagnet from ..lattice.abstract_impl import RWHardwareScalar,RWHardwareArray from ..lattice.abstract_impl import RWStrengthScalar,RWStrengthArray from .element_holder import ElementHolder - +from ..lattice.abstract_impl import RWBpmTiltScalar,RWBpmOffsetArray, RBpmArray # Define the main class name for this module PYAMLCLASS = "Simulator" @@ -67,6 +68,13 @@ def fill_device(self,elements:list[Element]): for m in ms: self.add_magnet(str(m),m) self.add_magnet(str(m),m) + elif isinstance(e,BPM): + # This assumes unique BPM names in the pyAT lattice + tilt = RWBpmTiltScalar(self.get_at_elems(e.name)) + offsets = RWBpmOffsetArray(self.get_at_elems(e.name)) + positions = RBpmArray(self.get_at_elems(e.name),self.ring) + e.attach(positions, offsets, tilt) + self.add_bpm(str(e),e) def get_at_elems(self,elementName:str) -> list[at.Element]: elementList = [e for e in self.ring if e.FamName == elementName] From 457893107e3a6c7c934e516a91eba38903498cbc Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Wed, 10 Sep 2025 17:39:19 +0200 Subject: [PATCH 02/14] Fixed import errors. First runnable test (still fails). --- pyaml/bpm/bpm.py | 8 +++++--- pyaml/lattice/abstract_impl.py | 2 +- pyaml/lattice/simulator.py | 2 +- tests/config/bpms.yaml | 24 ++++++++++++++++++++++++ tests/test_bpm.py | 23 +++++++++++++++++++++++ 5 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 tests/config/bpms.yaml create mode 100644 tests/test_bpm.py diff --git a/pyaml/bpm/bpm.py b/pyaml/bpm/bpm.py index 10abfb08..64ededea 100644 --- a/pyaml/bpm/bpm.py +++ b/pyaml/bpm/bpm.py @@ -1,8 +1,9 @@ from pyaml.lattice.element import Element, ElementConfigModel -from pyaml.pyaml.lattice.abstract_impl import RBpmPositionArray, RWBpmOffsetArray, RBpmTiltScalar +from pyaml.lattice.abstract_impl import RBpmPositionArray, RWBpmOffsetArray, RWBpmTiltScalar from ..control.deviceaccess import DeviceAccess from ..control import abstract from typing import Self +from pyaml.bpm.bpm_model import BPMModel PYAMLCLASS = "BPM" @@ -22,7 +23,7 @@ def __init__(self, hardware_name: str): """ hardware_name: str -class BPM (Element): +class BPM(Element): """ Class providing access to one BPM of a physical or simulated lattice """ @@ -61,7 +62,8 @@ def hardware(self) -> abstract.ReadWriteFloatScalar: raise Exception(f"{str(self)} has no model that supports hardware units") return self.__hardware - def attach(self, positions: RBpmPositionArray , offset: RWBpmOffsetArray, tilt: RBpmTiltScalar) -> Self: + def attach(self, positions: RBpmPositionArray , offset: RWBpmOffsetArray, + tilt: RWBpmTiltScalar) -> Self: # Attach positions, offset and tilt attributes and returns a new # reference obj = self.__class__(self._cfg) diff --git a/pyaml/lattice/abstract_impl.py b/pyaml/lattice/abstract_impl.py index e5b771c4..d99795af 100644 --- a/pyaml/lattice/abstract_impl.py +++ b/pyaml/lattice/abstract_impl.py @@ -144,7 +144,7 @@ def unit(self) -> list[str]: #------------------------------------------------------------------------------ -class RBpmArray(abstract.ReadFloatArray): +class RBpmPositionArray(abstract.ReadFloatArray): """ Class providing read access to a BPM position (array) of a simulator. Position in pyAT is calculated using find_orbit function, which returns the diff --git a/pyaml/lattice/simulator.py b/pyaml/lattice/simulator.py index 95e9eee2..880a55c0 100644 --- a/pyaml/lattice/simulator.py +++ b/pyaml/lattice/simulator.py @@ -9,7 +9,7 @@ from ..lattice.abstract_impl import RWHardwareScalar,RWHardwareArray from ..lattice.abstract_impl import RWStrengthScalar,RWStrengthArray from .element_holder import ElementHolder -from ..lattice.abstract_impl import RWBpmTiltScalar,RWBpmOffsetArray, RBpmArray +from ..lattice.abstract_impl import RWBpmTiltScalar,RWBpmOffsetArray, RBpmPositionArray # Define the main class name for this module PYAMLCLASS = "Simulator" diff --git a/tests/config/bpms.yaml b/tests/config/bpms.yaml new file mode 100644 index 00000000..c85af97b --- /dev/null +++ b/tests/config/bpms.yaml @@ -0,0 +1,24 @@ +type: pyaml.pyaml +instruments: + - type: pyaml.instrument + name: 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 + data_folder: /data/store + devices: + - type: pyaml.bpm.bpm + name: BPM_C03-02 + model: + type: pyaml.bpm.bpm_tiltoffset_model + position_unit: mm + tilt_unit: rad + offset_unit: mm + position_hardware_unit: mm + bpm_device: sr/bpm/bpm-c03-02 diff --git a/tests/test_bpm.py b/tests/test_bpm.py new file mode 100644 index 00000000..4e9d7917 --- /dev/null +++ b/tests/test_bpm.py @@ -0,0 +1,23 @@ + +from pyaml.pyaml import PyAML, pyaml +from pyaml.instrument import Instrument +from pyaml.configuration.factory import Factory +import numpy as np +import at +import pytest + +@pytest.mark.parametrize("install_test_package", [{ + "name": "tango", + "path": "tests/dummy_cs/tango" +}], indirect=True) +def test_bpm(install_test_package): + + ml:PyAML = pyaml("tests/config/bpms.yaml") + sr:Instrument = ml.get('sr') + sr.design.get_lattice().disable_6d() + + bpm = sr.design.get_bpm('BPM_C03-02') + + assert( True ) + + Factory.clear() From 438671f884e26577083c868611dc5ccf670ecccb Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Mon, 20 Oct 2025 17:09:36 +0200 Subject: [PATCH 03/14] Changed to only scalar attributes for DeviceAccess (xpos, ypos, etc) --- pyaml/bpm/bpm.py | 44 ++++++++++----------- pyaml/bpm/bpm_model.py | 46 +++++++++++----------- pyaml/bpm/bpm_tiltoffset_model.py | 65 ++++++++++++++++--------------- pyaml/lattice/abstract_impl.py | 8 ++-- tests/config/bpms.yaml | 27 ++++++++++--- tests/test_bpm.py | 2 +- 6 files changed, 103 insertions(+), 89 deletions(-) diff --git a/pyaml/bpm/bpm.py b/pyaml/bpm/bpm.py index 64ededea..bc4d4bee 100644 --- a/pyaml/bpm/bpm.py +++ b/pyaml/bpm/bpm.py @@ -7,29 +7,31 @@ PYAMLCLASS = "BPM" -class BPMConfigModel(ElementConfigModel): +class ConfigModel(ElementConfigModel): """ Class providing access to BPM configuration parameters """ - def __init__(self, hardware_name: str): + def __init__(self): """ Construct a BPM configuration model Parameters ---------- - name : str - Element name """ - hardware_name: str + hardware: DeviceAccess | None = None + """Direct access to a magnet device that provides strength/current conversion""" + model: BPMModel | None = None + """Object in charge of converting magnet strenghts to power supply values""" + + class BPM(Element): """ Class providing access to one BPM of a physical or simulated lattice """ - def __init__(self, name: str, hardware: DeviceAccess = None, model: - BPMModel = None): + def __init__(self, cfg: ConfigModel): """ Construct a BPM @@ -42,26 +44,22 @@ def __init__(self, name: str, hardware: DeviceAccess = None, model: model : BPMModel BPM model in charge of computing beam position """ - super().__init__(name) - self.__model = model - self.__hardware = hardware - self.__positions = None - self.__offset = None - self.__tilt = None + + super().__init__(cfg.name) - if hardware is not None: - # TODO - # Direct access to a BPM device that supports beam position - # computation - raise Exception( - "%s, hardware access not implemented" % - (self.__class__.__name__, name)) + self.__hardware = cfg.hardware if hasattr(cfg, "hardware") else None + self.__model = cfg.model if hasattr(cfg, "model") else None + self.__cfg = cfg @property def hardware(self) -> abstract.ReadWriteFloatScalar: if self.__hardware is None: raise Exception(f"{str(self)} has no model that supports hardware units") return self.__hardware + @property + def model(self) -> BPMModel: + return self.__model + def attach(self, positions: RBpmPositionArray , offset: RWBpmOffsetArray, tilt: RWBpmTiltScalar) -> Self: # Attach positions, offset and tilt attributes and returns a new @@ -69,8 +67,8 @@ def attach(self, positions: RBpmPositionArray , offset: RWBpmOffsetArray, obj = self.__class__(self._cfg) obj.__model = self.__model obj.__hardware = self.__hardware - obj.__positions = positions - obj.__offset = offset - obj.__tilt = tilt + # obj.__positions = positions + # obj.__offset = offset + # obj.__tilt = tilt return obj diff --git a/pyaml/bpm/bpm_model.py b/pyaml/bpm/bpm_model.py index ca010176..ec394cb4 100644 --- a/pyaml/bpm/bpm_model.py +++ b/pyaml/bpm/bpm_model.py @@ -8,7 +8,7 @@ class BPMModel(metaclass=ABCMeta): tilts. """ @abstractmethod - def read_hardware_positions(self) -> NDArray[np.float64]: + def read_hardware_position_values(self) -> NDArray[np.float64]: """ Read horizontal and vertical positions from a BPM. Returns @@ -31,7 +31,7 @@ def read_hardware_tilt_value(self) -> float: pass @abstractmethod - def read_hardware_offset_values(self) -> NDArray[np.float64]: + def read_hardware_offset_values(self) -> NDArray: """ Read the offset values from a BPM. Returns @@ -68,25 +68,25 @@ def set_hardware_offset_values(self, offset_values: NDArray[np.float64]): """ pass - @abstractmethod - def get_hardware_angle_unit(self) -> str: - """ - Get the hardware unit for BPM readings. - Returns - ------- - str - The unit of measurement for BPM hardware values. - """ - pass - - @abstractmethod - def get_hardware_position_units(self) -> list[str]: - """ - Get the hardware units for BPM positions and offsets. - Returns - ------- - list[str] - List of units for horizontal and vertical positions and offsets. - """ - pass + # @abstractmethod + # def get_hardware_angle_unit(self) -> str: + # """ + # Get the hardware unit for BPM readings. + # Returns + # ------- + # str + # The unit of measurement for BPM hardware values. + # """ + # pass + # + # @abstractmethod + # def get_hardware_position_units(self) -> list[str]: + # """ + # Get the hardware units for BPM positions and offsets. + # Returns + # ------- + # list[str] + # List of units for horizontal and vertical positions and offsets. + # """ + # pass diff --git a/pyaml/bpm/bpm_tiltoffset_model.py b/pyaml/bpm/bpm_tiltoffset_model.py index 32b2c9cd..17530e9b 100644 --- a/pyaml/bpm/bpm_tiltoffset_model.py +++ b/pyaml/bpm/bpm_tiltoffset_model.py @@ -2,7 +2,7 @@ from pydantic import BaseModel,ConfigDict import numpy as np from ..control.deviceaccess import DeviceAccess - +from numpy.typing import NDArray # Define the main class name for this module PYAMLCLASS = "BPMTiltOffsetModel" @@ -10,14 +10,22 @@ class ConfigModel(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True,extra="forbid") - bpm_device: DeviceAccess - """Power converter device to apply currrent""" - position_unit: str - """Unit of the positions (i.e. mm)""" - tilt_unit: str - """Unit of the tilt (i.e. rad)""" - offset_unit: str - """Unit of the offsets (i.e. mm)""" + x_pos: DeviceAccess + """Horizontal position""" + y_pos: DeviceAccess + """Vertical position""" + x_offset: DeviceAccess + """Horizontal BPM offset""" + y_offset: DeviceAccess + """Vertical BPM offset""" + tilt: DeviceAccess + """BPM tilt""" + # position_unit: str + # """Unit of the positions (i.e. mm)""" + # tilt_unit: str + # """Unit of the tilt (i.e. rad)""" + # offset_unit: str + # """Unit of the offsets (i.e. mm)""" class BPMTiltOffsetModel(BPMModel): """ @@ -27,13 +35,16 @@ class BPMTiltOffsetModel(BPMModel): def __init__(self, cfg: ConfigModel): self._cfg = cfg - self.__position_unit = cfg.position_unit - self.__tilt_unit = cfg.tilt_unit - self.__offset_unit = cfg.offset_unit - self.__position_hardware_unit = cfg.bpm_device.unit() - self.__bpm_device = cfg.bpm_device + # self.__position_unit = cfg.position_unit + # self.__tilt_unit = cfg.tilt_unit + # self.__offset_unit = cfg.offset_unit + self.__x_pos = cfg.x_pos + self.__y_pos = cfg.y_pos + self.__x_offset = cfg.x_offset + self.__y_offset = cfg.y_offset + self.__tilt = cfg.tilt - def read_hardware_position_values(self) -> np.ndarray: + def read_hardware_position_values(self) -> NDArray: """ Simulate reading the position values from a BPM. Returns @@ -42,18 +53,7 @@ def read_hardware_position_values(self) -> np.ndarray: Array of shape (2,) containing the horizontal and vertical positions """ - return self.__bpm_device.get_position() - - def set_hardware_position_values(self, position_values: np.ndarray): - """ - Simulate setting the position values of a BPM - Parameters - ---------- - position_values : np.ndarray - Array of shape (2,) containing the horizontal and vertical - positions to set for the BPM - """ - self.__bpm_device.set_position(position_values) + return np.array([self.__x_pos.get(), self.__y_pos.get()]) def read_hardware_tilt_value(self) -> float: """ @@ -63,9 +63,9 @@ def read_hardware_tilt_value(self) -> float: float The tilt value of the BPM """ - return self.__bpm_device.get_tilt() + return self.__tilt.get() - def read_hardware_offset_values(self) -> np.ndarray: + def read_hardware_offset_values(self) -> NDArray: """ Simulate reading the offset values from a BPM. Returns @@ -74,7 +74,7 @@ def read_hardware_offset_values(self) -> np.ndarray: Array of shape (2,) containing the horizontal and vertical offsets """ - return self.__bpm_device.get_offset() + return np.array([self.__x_offset.get(), self.__y_offset.get()]) def set_hardware_tilt_value(self, tilt: float): """ @@ -87,7 +87,7 @@ def set_hardware_tilt_value(self, tilt: float): ------- None """ - self.__bpm_device.set_tilt(tilt) + self.__tilt.set(tilt) def set_hardware_offset_values(self, offset_values: np.ndarray): """ @@ -98,5 +98,6 @@ def set_hardware_offset_values(self, offset_values: np.ndarray): Array of shape (2,) containing the horizontal and vertical offsets to set for the BPM """ - self.__bpm_device.set_offset(offset_values) + self.__x_offset.set(offset_values[0]) + self.__y_offset.set(offset_values[1]) diff --git a/pyaml/lattice/abstract_impl.py b/pyaml/lattice/abstract_impl.py index d99795af..daaad199 100644 --- a/pyaml/lattice/abstract_impl.py +++ b/pyaml/lattice/abstract_impl.py @@ -164,8 +164,8 @@ def get(self) -> np.array: return orbit[0, [0, 2]] # Gets the unit of the value - def unit(self) -> list[str]: - return ['mm', 'mm'] + def unit(self) -> str: + return 'mm' #------------------------------------------------------------------------------ @@ -201,8 +201,8 @@ def set_and_wait(self, value:np.array): raise NotImplementedError("Not implemented yet.") # Gets the unit of the value - def unit(self) -> list[str]: - return ['mm', 'mm'] # Assuming all offsets are in mm + def unit(self) -> str: + return 'mm' # Assuming all offsets are in mm #------------------------------------------------------------------------------ diff --git a/tests/config/bpms.yaml b/tests/config/bpms.yaml index c85af97b..c571c590 100644 --- a/tests/config/bpms.yaml +++ b/tests/config/bpms.yaml @@ -14,11 +14,26 @@ instruments: data_folder: /data/store devices: - type: pyaml.bpm.bpm - name: BPM_C03-02 + name: BPM_C01-01 model: type: pyaml.bpm.bpm_tiltoffset_model - position_unit: mm - tilt_unit: rad - offset_unit: mm - position_hardware_unit: mm - bpm_device: sr/bpm/bpm-c03-02 + x_pos: + type: tango.pyaml.attribute + attribute: srdiag/bpm/c01-01/SA_HPosition + unit: mm + y_pos: + type: tango.pyaml.attribute + attribute: srdiag/bpm/c01-01/SA_VPosition + unit: mm + x_offset: + type: tango.pyaml.attribute + attribute: srdiag/bpm/c01-01/HOffset + unit: mm + y_offset: + type: tango.pyaml.attribute + attribute: srdiag/bpm/c01-01/VOffset + unit: mm + tilt: + type: tango.pyaml.attribute + attribute: srdiag/bpm/c01-01/Tilt_Angle + unit: rad diff --git a/tests/test_bpm.py b/tests/test_bpm.py index 4e9d7917..94e954eb 100644 --- a/tests/test_bpm.py +++ b/tests/test_bpm.py @@ -16,7 +16,7 @@ def test_bpm(install_test_package): sr:Instrument = ml.get('sr') sr.design.get_lattice().disable_6d() - bpm = sr.design.get_bpm('BPM_C03-02') + # bpm = sr.design.get_bpm('BPM_C03-02') assert( True ) From 329f0b04a670af5011967e9c807c2f73fd9d354b Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Mon, 20 Oct 2025 17:41:36 +0200 Subject: [PATCH 04/14] Corrected configuration of BPM. Configuration passes without errors now. --- pyaml/bpm/bpm.py | 16 +++------------- pyaml/control/abstract_impl.py | 4 ++-- pyaml/control/controlsystem.py | 1 + pyaml/lattice/abstract_impl.py | 2 +- pyaml/lattice/simulator.py | 2 +- 5 files changed, 8 insertions(+), 17 deletions(-) diff --git a/pyaml/bpm/bpm.py b/pyaml/bpm/bpm.py index bc4d4bee..0a30769d 100644 --- a/pyaml/bpm/bpm.py +++ b/pyaml/bpm/bpm.py @@ -1,5 +1,5 @@ from pyaml.lattice.element import Element, ElementConfigModel -from pyaml.lattice.abstract_impl import RBpmPositionArray, RWBpmOffsetArray, RWBpmTiltScalar +from pyaml.lattice.abstract_impl import RBpmArray, RWBpmOffsetArray, RWBpmTiltScalar from ..control.deviceaccess import DeviceAccess from ..control import abstract from typing import Self @@ -8,17 +8,7 @@ PYAMLCLASS = "BPM" class ConfigModel(ElementConfigModel): - """ - Class providing access to BPM configuration parameters - """ - - def __init__(self): - """ - Construct a BPM configuration model - Parameters - ---------- - """ hardware: DeviceAccess | None = None """Direct access to a magnet device that provides strength/current conversion""" model: BPMModel | None = None @@ -49,7 +39,7 @@ def __init__(self, cfg: ConfigModel): self.__hardware = cfg.hardware if hasattr(cfg, "hardware") else None self.__model = cfg.model if hasattr(cfg, "model") else None - self.__cfg = cfg + self._cfg = cfg @property def hardware(self) -> abstract.ReadWriteFloatScalar: if self.__hardware is None: @@ -60,7 +50,7 @@ def hardware(self) -> abstract.ReadWriteFloatScalar: def model(self) -> BPMModel: return self.__model - def attach(self, positions: RBpmPositionArray , offset: RWBpmOffsetArray, + def attach(self, positions: RBpmArray , offset: RWBpmOffsetArray, tilt: RWBpmTiltScalar) -> Self: # Attach positions, offset and tilt attributes and returns a new # reference diff --git a/pyaml/control/abstract_impl.py b/pyaml/control/abstract_impl.py index e479776b..07c3f450 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -111,7 +111,7 @@ def unit(self) -> list[str]: #------------------------------------------------------------------------------ -class RbpmArray(abstract.ReadFloatArray): +class RBpmArray(abstract.ReadFloatArray): """ Class providing read access to a BPM array of a control system """ @@ -128,7 +128,7 @@ def unit(self) -> list[str]: #------------------------------------------------------------------------------ -class RWbpmTiltScalar(abstract.ReadFloatScalar): +class RWBpmTiltScalar(abstract.ReadFloatScalar): """ Class providing read access to a BPM tilt of a control system """ diff --git a/pyaml/control/controlsystem.py b/pyaml/control/controlsystem.py index d2c65ba5..409ef4e5 100644 --- a/pyaml/control/controlsystem.py +++ b/pyaml/control/controlsystem.py @@ -4,6 +4,7 @@ from ..control.abstract_impl import RWHardwareScalar,RWHardwareArray,RWStrengthScalar,RWStrengthArray from ..magnet.magnet import Magnet from ..magnet.cfm_magnet import CombinedFunctionMagnet +from ..bpm.bpm import BPM class ControlSystem(ElementHolder,metaclass=ABCMeta): """ diff --git a/pyaml/lattice/abstract_impl.py b/pyaml/lattice/abstract_impl.py index daaad199..039da099 100644 --- a/pyaml/lattice/abstract_impl.py +++ b/pyaml/lattice/abstract_impl.py @@ -144,7 +144,7 @@ def unit(self) -> list[str]: #------------------------------------------------------------------------------ -class RBpmPositionArray(abstract.ReadFloatArray): +class RBpmArray(abstract.ReadFloatArray): """ Class providing read access to a BPM position (array) of a simulator. Position in pyAT is calculated using find_orbit function, which returns the diff --git a/pyaml/lattice/simulator.py b/pyaml/lattice/simulator.py index 880a55c0..95e9eee2 100644 --- a/pyaml/lattice/simulator.py +++ b/pyaml/lattice/simulator.py @@ -9,7 +9,7 @@ from ..lattice.abstract_impl import RWHardwareScalar,RWHardwareArray from ..lattice.abstract_impl import RWStrengthScalar,RWStrengthArray from .element_holder import ElementHolder -from ..lattice.abstract_impl import RWBpmTiltScalar,RWBpmOffsetArray, RBpmPositionArray +from ..lattice.abstract_impl import RWBpmTiltScalar,RWBpmOffsetArray, RBpmArray # Define the main class name for this module PYAMLCLASS = "Simulator" From 44085d63f7fe18f27a0a05b0cd558c6686c38fae Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Mon, 20 Oct 2025 22:53:58 +0200 Subject: [PATCH 05/14] Passing simplest test for BPM in Simulator --- pyaml/bpm/bpm.py | 22 +++++++++++++++++++--- pyaml/bpm/bpm_tiltoffset_model.py | 9 --------- pyaml/control/abstract_impl.py | 4 ++-- pyaml/lattice/abstract_impl.py | 2 +- pyaml/lattice/element_holder.py | 1 + pyaml/lattice/simulator.py | 8 ++++---- tests/test_bpm.py | 14 ++++++++++---- 7 files changed, 37 insertions(+), 23 deletions(-) diff --git a/pyaml/bpm/bpm.py b/pyaml/bpm/bpm.py index 0a30769d..30a414c6 100644 --- a/pyaml/bpm/bpm.py +++ b/pyaml/bpm/bpm.py @@ -50,6 +50,18 @@ def hardware(self) -> abstract.ReadWriteFloatScalar: def model(self) -> BPMModel: return self.__model + @property + def positions(self) -> RBpmArray: + return self.__positions + + @property + def offset(self) -> RWBpmOffsetArray: + return self.__offset + + @property + def tilt(self) -> RWBpmTiltScalar: + return self.__tilt + def attach(self, positions: RBpmArray , offset: RWBpmOffsetArray, tilt: RWBpmTiltScalar) -> Self: # Attach positions, offset and tilt attributes and returns a new @@ -57,8 +69,12 @@ def attach(self, positions: RBpmArray , offset: RWBpmOffsetArray, obj = self.__class__(self._cfg) obj.__model = self.__model obj.__hardware = self.__hardware - # obj.__positions = positions - # obj.__offset = offset - # obj.__tilt = tilt + obj.__positions = positions + obj.__offset = offset + # obj.__x_pos = positions[0] + # obj.__y_pos = positions[1] + # obj.__x_offset = offset[0] + # obj.__y_offset = offset[1] + obj.__tilt = tilt return obj diff --git a/pyaml/bpm/bpm_tiltoffset_model.py b/pyaml/bpm/bpm_tiltoffset_model.py index 17530e9b..9f1331ba 100644 --- a/pyaml/bpm/bpm_tiltoffset_model.py +++ b/pyaml/bpm/bpm_tiltoffset_model.py @@ -20,12 +20,6 @@ class ConfigModel(BaseModel): """Vertical BPM offset""" tilt: DeviceAccess """BPM tilt""" - # position_unit: str - # """Unit of the positions (i.e. mm)""" - # tilt_unit: str - # """Unit of the tilt (i.e. rad)""" - # offset_unit: str - # """Unit of the offsets (i.e. mm)""" class BPMTiltOffsetModel(BPMModel): """ @@ -35,9 +29,6 @@ class BPMTiltOffsetModel(BPMModel): def __init__(self, cfg: ConfigModel): self._cfg = cfg - # self.__position_unit = cfg.position_unit - # self.__tilt_unit = cfg.tilt_unit - # self.__offset_unit = cfg.offset_unit self.__x_pos = cfg.x_pos self.__y_pos = cfg.y_pos self.__x_offset = cfg.x_offset diff --git a/pyaml/control/abstract_impl.py b/pyaml/control/abstract_impl.py index 07c3f450..d4c162bf 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -148,7 +148,7 @@ def unit(self) -> str: #------------------------------------------------------------------------------ -class RWBpmOffsetArray(abstract.ReadWriteFloatArray): +class RWBpmOffset(abstract.ReadWriteFloatArray): """ Class providing read write access to a BPM offset of a control system """ @@ -161,7 +161,7 @@ def get(self) -> NDArray[np.float64]: # Sets the value def set(self, value: NDArray[np.float64]): - self.__model.set_hardware_offset_values(value) + self.__model.set_hardware_offset_values(value) # Gets the unit of the value def unit(self) -> str: diff --git a/pyaml/lattice/abstract_impl.py b/pyaml/lattice/abstract_impl.py index 039da099..b81155e9 100644 --- a/pyaml/lattice/abstract_impl.py +++ b/pyaml/lattice/abstract_impl.py @@ -159,7 +159,7 @@ def __init__(self, element: at.Element, lattice: at.Lattice): # Gets the value def get(self) -> np.array: - index = self.lattice.get_refpts(self.element.name) + index = self.lattice.get_refpts(self.element.FamName) _, orbit = at.find_orbit(self.lattice, index) return orbit[0, [0, 2]] diff --git a/pyaml/lattice/element_holder.py b/pyaml/lattice/element_holder.py index 808b7db5..00e1a577 100644 --- a/pyaml/lattice/element_holder.py +++ b/pyaml/lattice/element_holder.py @@ -74,6 +74,7 @@ def get_magnets(self,name:str) -> MagnetArray: def get_bpm(self,name:str) -> Element: if name not in self.__BPMS: + print(self.__BPMS.keys()) raise Exception(f"BPM {name} not defined") return self.__BPMS[name] diff --git a/pyaml/lattice/simulator.py b/pyaml/lattice/simulator.py index 95e9eee2..8a4c411e 100644 --- a/pyaml/lattice/simulator.py +++ b/pyaml/lattice/simulator.py @@ -70,10 +70,10 @@ def fill_device(self,elements:list[Element]): self.add_magnet(str(m),m) elif isinstance(e,BPM): # This assumes unique BPM names in the pyAT lattice - tilt = RWBpmTiltScalar(self.get_at_elems(e.name)) - offsets = RWBpmOffsetArray(self.get_at_elems(e.name)) - positions = RBpmArray(self.get_at_elems(e.name),self.ring) - e.attach(positions, offsets, tilt) + tilt = RWBpmTiltScalar(self.get_at_elems(e.name)[0]) + offsets = RWBpmOffsetArray(self.get_at_elems(e.name)[0]) + positions = RBpmArray(self.get_at_elems(e.name)[0],self.ring) + e = e.attach(positions, offsets, tilt) self.add_bpm(str(e),e) def get_at_elems(self,elementName:str) -> list[at.Element]: diff --git a/tests/test_bpm.py b/tests/test_bpm.py index 94e954eb..d0942918 100644 --- a/tests/test_bpm.py +++ b/tests/test_bpm.py @@ -15,9 +15,15 @@ def test_bpm(install_test_package): ml:PyAML = pyaml("tests/config/bpms.yaml") sr:Instrument = ml.get('sr') sr.design.get_lattice().disable_6d() - - # bpm = sr.design.get_bpm('BPM_C03-02') - - assert( True ) + bpm = sr.design.get_bpm('BPM(BPM_C01-01)') + # print(bpm.__dict__) + # print(bpm.model.__dict__) + + bpm.tilt.set(0.01) + assert(bpm.tilt.get() == 0.01) + bpm.offset.set( np.array([0.1,0.2]) ) + assert(bpm.offset.get()[0] == 0.1) + assert(bpm.offset.get()[1] == 0.2) + assert( np.allclose( bpm.positions.get(), np.array([0.0,0.0]) ) ) Factory.clear() From 7abdb74d496c5d275202edb47b2b9358bc7a835d Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Tue, 21 Oct 2025 16:16:27 +0200 Subject: [PATCH 06/14] Separated tests for simulator. --- tests/test_bpm.py | 45 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/tests/test_bpm.py b/tests/test_bpm.py index d0942918..5f6f4339 100644 --- a/tests/test_bpm.py +++ b/tests/test_bpm.py @@ -3,27 +3,56 @@ from pyaml.instrument import Instrument from pyaml.configuration.factory import Factory import numpy as np -import at import pytest @pytest.mark.parametrize("install_test_package", [{ "name": "tango", "path": "tests/dummy_cs/tango" }], indirect=True) -def test_bpm(install_test_package): +def test_simulator_bpm_tilt(install_test_package): ml:PyAML = pyaml("tests/config/bpms.yaml") sr:Instrument = ml.get('sr') sr.design.get_lattice().disable_6d() bpm = sr.design.get_bpm('BPM(BPM_C01-01)') - # print(bpm.__dict__) - # print(bpm.model.__dict__) + assert bpm.tilt.get() == 0 bpm.tilt.set(0.01) - assert(bpm.tilt.get() == 0.01) + assert bpm.tilt.get() == 0.01 + + Factory.clear() + +@pytest.mark.parametrize("install_test_package", [{ + "name": "tango", + "path": "tests/dummy_cs/tango" +}], indirect=True) +def test_simulator_bpm_offset(install_test_package): + + ml:PyAML = pyaml("tests/config/bpms.yaml") + sr:Instrument = ml.get('sr') + sr.design.get_lattice().disable_6d() + bpm = sr.design.get_bpm('BPM(BPM_C01-01)') + + assert bpm.offset.get()[0] == 0 + assert bpm.offset.get()[1] == 0 bpm.offset.set( np.array([0.1,0.2]) ) - assert(bpm.offset.get()[0] == 0.1) - assert(bpm.offset.get()[1] == 0.2) - assert( np.allclose( bpm.positions.get(), np.array([0.0,0.0]) ) ) + assert bpm.offset.get()[0] == 0.1 + assert bpm.offset.get()[1] == 0.2 + assert np.allclose( bpm.positions.get(), np.array([0.0,0.0]) ) Factory.clear() + +@pytest.mark.parametrize("install_test_package", [{ + "name": "tango", + "path": "tests/dummy_cs/tango" +}], indirect=True) +def test_simulator_bpm_position(install_test_package): + + ml:PyAML = pyaml("tests/config/bpms.yaml") + sr:Instrument = ml.get('sr') + sr.design.get_lattice().disable_6d() + bpm = sr.design.get_bpm('BPM(BPM_C01-01)') + + assert np.allclose( bpm.positions.get(), np.array([0.0,0.0]) ) + + Factory.clear() From 22ce8e9249d9266956e3ff956f04373c7450c7fa Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Tue, 21 Oct 2025 16:48:32 +0200 Subject: [PATCH 07/14] Identation correction --- pyaml/control/controlsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyaml/control/controlsystem.py b/pyaml/control/controlsystem.py index eeed330a..ddd52ed2 100644 --- a/pyaml/control/controlsystem.py +++ b/pyaml/control/controlsystem.py @@ -60,6 +60,6 @@ def fill_device(self,elements:list[Element]): ms = e.attach(strengths,currents) for m in ms: self.add_magnet(m.get_name(),m) - elif isinstance(e,BPM): + elif isinstance(e,BPM): self.add_bpm(str(e),e) From b7f88ceb892dbc64506b8c79b593a2fa65cac08f Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Tue, 21 Oct 2025 17:11:59 +0200 Subject: [PATCH 08/14] Corrections after merge. Simulator tests pass. --- pyaml/bpm/bpm.py | 4 ---- pyaml/control/abstract_impl.py | 7 +++++-- pyaml/control/controlsystem.py | 6 +++++- pyaml/lattice/simulator.py | 8 ++++---- tests/test_bpm.py | 6 +++--- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/pyaml/bpm/bpm.py b/pyaml/bpm/bpm.py index 30a414c6..c8282698 100644 --- a/pyaml/bpm/bpm.py +++ b/pyaml/bpm/bpm.py @@ -71,10 +71,6 @@ def attach(self, positions: RBpmArray , offset: RWBpmOffsetArray, obj.__hardware = self.__hardware obj.__positions = positions obj.__offset = offset - # obj.__x_pos = positions[0] - # obj.__y_pos = positions[1] - # obj.__x_offset = offset[0] - # obj.__y_offset = offset[1] obj.__tilt = tilt return obj diff --git a/pyaml/control/abstract_impl.py b/pyaml/control/abstract_impl.py index 34b4e876..0acbe61c 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -148,13 +148,15 @@ def get(self) -> float: def set(self, value:float): self.__model.set_hardware_tilt_value(value) + def set_and_wait(self, value: NDArray[np.float64]): + raise NotImplementedError("Not implemented yet.") # Gets the unit of the value def unit(self) -> str: return self.__model.get_hardware_angle_unit() #------------------------------------------------------------------------------ -class RWBpmOffset(abstract.ReadWriteFloatArray): +class RWBpmOffsetArray(abstract.ReadWriteFloatArray): """ Class providing read write access to a BPM offset of a control system """ @@ -168,7 +170,8 @@ def get(self) -> NDArray[np.float64]: # Sets the value def set(self, value: NDArray[np.float64]): self.__model.set_hardware_offset_values(value) - + def set_and_wait(self, value: NDArray[np.float64]): + raise NotImplementedError("Not implemented yet.") # Gets the unit of the value def unit(self) -> str: return self.__model.get_hardware_position_units()[0] diff --git a/pyaml/control/controlsystem.py b/pyaml/control/controlsystem.py index ddd52ed2..4a4b77a5 100644 --- a/pyaml/control/controlsystem.py +++ b/pyaml/control/controlsystem.py @@ -2,6 +2,7 @@ from ..lattice.element_holder import ElementHolder from ..lattice.element import Element from ..control.abstract_impl import RWHardwareScalar,RWHardwareArray,RWStrengthScalar,RWStrengthArray +from ..control.abstract_impl import RWBpmTiltScalar,RWBpmOffsetArray, RBpmArray from ..magnet.magnet import Magnet from ..magnet.cfm_magnet import CombinedFunctionMagnet from ..bpm.bpm import BPM @@ -61,5 +62,8 @@ def fill_device(self,elements:list[Element]): for m in ms: self.add_magnet(m.get_name(),m) elif isinstance(e,BPM): - self.add_bpm(str(e),e) + tilt = RWBpmTiltScalar(e.model) + offsets = RWBpmOffsetArray(e.model) + positions = RBpmArray(e.model) + self.add_bpm(e.get_name(),e) diff --git a/pyaml/lattice/simulator.py b/pyaml/lattice/simulator.py index 35e55090..7beefff6 100644 --- a/pyaml/lattice/simulator.py +++ b/pyaml/lattice/simulator.py @@ -79,11 +79,11 @@ def fill_device(self,elements:list[Element]): self.add_magnet(m.get_name(), m) elif isinstance(e,BPM): # This assumes unique BPM names in the pyAT lattice - tilt = RWBpmTiltScalar(self.get_at_elems(e.name)[0]) - offsets = RWBpmOffsetArray(self.get_at_elems(e.name)[0]) - positions = RBpmArray(self.get_at_elems(e.name)[0],self.ring) + tilt = RWBpmTiltScalar(self.get_at_elems(e)[0]) + offsets = RWBpmOffsetArray(self.get_at_elems(e)[0]) + positions = RBpmArray(self.get_at_elems(e)[0],self.ring) e = e.attach(positions, offsets, tilt) - self.add_bpm(str(e),e) + self.add_bpm(e.get_name(),e) def get_at_elems(self,element:Element) -> list[at.Element]: identifier = self._linker.get_element_identifier(element) diff --git a/tests/test_bpm.py b/tests/test_bpm.py index 5f6f4339..ee256cf1 100644 --- a/tests/test_bpm.py +++ b/tests/test_bpm.py @@ -14,7 +14,7 @@ def test_simulator_bpm_tilt(install_test_package): ml:PyAML = pyaml("tests/config/bpms.yaml") sr:Instrument = ml.get('sr') sr.design.get_lattice().disable_6d() - bpm = sr.design.get_bpm('BPM(BPM_C01-01)') + bpm = sr.design.get_bpm('BPM_C01-01') assert bpm.tilt.get() == 0 bpm.tilt.set(0.01) @@ -31,7 +31,7 @@ def test_simulator_bpm_offset(install_test_package): ml:PyAML = pyaml("tests/config/bpms.yaml") sr:Instrument = ml.get('sr') sr.design.get_lattice().disable_6d() - bpm = sr.design.get_bpm('BPM(BPM_C01-01)') + bpm = sr.design.get_bpm('BPM_C01-01') assert bpm.offset.get()[0] == 0 assert bpm.offset.get()[1] == 0 @@ -51,7 +51,7 @@ def test_simulator_bpm_position(install_test_package): ml:PyAML = pyaml("tests/config/bpms.yaml") sr:Instrument = ml.get('sr') sr.design.get_lattice().disable_6d() - bpm = sr.design.get_bpm('BPM(BPM_C01-01)') + bpm = sr.design.get_bpm('BPM_C01-01') assert np.allclose( bpm.positions.get(), np.array([0.0,0.0]) ) From b4f375a3959e32896fe4e0a299704360f9e0eca3 Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Tue, 21 Oct 2025 22:32:01 +0200 Subject: [PATCH 09/14] Added tests to 'live' mode --- pyaml/bpm/bpm_model.py | 22 ------------- pyaml/control/abstract_impl.py | 2 +- pyaml/control/controlsystem.py | 1 + tests/test_bpm.py | 1 - tests/test_bpm_controlsystem.py | 57 +++++++++++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 24 deletions(-) create mode 100644 tests/test_bpm_controlsystem.py diff --git a/pyaml/bpm/bpm_model.py b/pyaml/bpm/bpm_model.py index ec394cb4..654fe3f6 100644 --- a/pyaml/bpm/bpm_model.py +++ b/pyaml/bpm/bpm_model.py @@ -68,25 +68,3 @@ def set_hardware_offset_values(self, offset_values: NDArray[np.float64]): """ pass - # @abstractmethod - # def get_hardware_angle_unit(self) -> str: - # """ - # Get the hardware unit for BPM readings. - # Returns - # ------- - # str - # The unit of measurement for BPM hardware values. - # """ - # pass - # - # @abstractmethod - # def get_hardware_position_units(self) -> list[str]: - # """ - # Get the hardware units for BPM positions and offsets. - # Returns - # ------- - # list[str] - # List of units for horizontal and vertical positions and offsets. - # """ - # pass - diff --git a/pyaml/control/abstract_impl.py b/pyaml/control/abstract_impl.py index 9e19d1c6..50b8c221 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -127,7 +127,7 @@ def __init__(self, model:BPMModel): # Gets the value def get(self) -> np.array: - return self.__model.read_hardware_positions() + return self.__model.read_hardware_position_values() # Gets the unit of the value def unit(self) -> list[str]: diff --git a/pyaml/control/controlsystem.py b/pyaml/control/controlsystem.py index dd0aff17..bdf3843f 100644 --- a/pyaml/control/controlsystem.py +++ b/pyaml/control/controlsystem.py @@ -69,6 +69,7 @@ def fill_device(self,elements:list[Element]): tilt = RWBpmTiltScalar(e.model) offsets = RWBpmOffsetArray(e.model) positions = RBpmArray(e.model) + e = e.attach(positions, offsets, tilt) self.add_bpm(e.get_name(),e) diff --git a/tests/test_bpm.py b/tests/test_bpm.py index ee256cf1..dda8561d 100644 --- a/tests/test_bpm.py +++ b/tests/test_bpm.py @@ -15,7 +15,6 @@ def test_simulator_bpm_tilt(install_test_package): sr:Instrument = ml.get('sr') sr.design.get_lattice().disable_6d() bpm = sr.design.get_bpm('BPM_C01-01') - assert bpm.tilt.get() == 0 bpm.tilt.set(0.01) assert bpm.tilt.get() == 0.01 diff --git a/tests/test_bpm_controlsystem.py b/tests/test_bpm_controlsystem.py new file mode 100644 index 00000000..cbb96dcd --- /dev/null +++ b/tests/test_bpm_controlsystem.py @@ -0,0 +1,57 @@ + + +from pyaml.pyaml import PyAML, pyaml +from pyaml.instrument import Instrument +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_controlsystem_bpm_tilt(install_test_package): + + ml:PyAML = pyaml("tests/config/bpms.yaml") + sr:Instrument = ml.get('sr') + bpm = sr.live.get_bpm('BPM_C01-01') + print(bpm.tilt.get()) + + assert bpm.tilt.get() == 0 + bpm.tilt.set(0.01) + assert bpm.tilt.get() == 0.01 + + Factory.clear() + +@pytest.mark.parametrize("install_test_package", [{ + "name": "tango", + "path": "tests/dummy_cs/tango" +}], indirect=True) +def test_controlsystem_bpm_offset(install_test_package): + + ml:PyAML = pyaml("tests/config/bpms.yaml") + sr:Instrument = ml.get('sr') + bpm = sr.live.get_bpm('BPM_C01-01') + + assert bpm.offset.get()[0] == 0 + assert bpm.offset.get()[1] == 0 + bpm.offset.set( np.array([0.1,0.2]) ) + assert bpm.offset.get()[0] == 0.1 + assert bpm.offset.get()[1] == 0.2 + assert np.allclose( bpm.positions.get(), np.array([0.0,0.0]) ) + + Factory.clear() + +@pytest.mark.parametrize("install_test_package", [{ + "name": "tango", + "path": "tests/dummy_cs/tango" +}], indirect=True) +def test_controlsystem_bpm_position(install_test_package): + + ml:PyAML = pyaml("tests/config/bpms.yaml") + sr:Instrument = ml.get('sr') + bpm = sr.live.get_bpm('BPM_C01-01') + + assert np.allclose( bpm.positions.get(), np.array([0.0,0.0]) ) + + Factory.clear() From b8dd9ed42d03c07adab8e7a50b26abde4dddd1ba Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Wed, 22 Oct 2025 10:59:37 +0200 Subject: [PATCH 10/14] Added a simple BPM model --- pyaml/bpm/bpm_simple_model.py | 83 ++++++++++++++++++++++++++++++++++ pyaml/control/abstract_impl.py | 6 +-- 2 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 pyaml/bpm/bpm_simple_model.py diff --git a/pyaml/bpm/bpm_simple_model.py b/pyaml/bpm/bpm_simple_model.py new file mode 100644 index 00000000..a470c94c --- /dev/null +++ b/pyaml/bpm/bpm_simple_model.py @@ -0,0 +1,83 @@ +from pyaml.bpm.bpm_model import BPMModel +from pydantic import BaseModel,ConfigDict +import numpy as np +from ..control.deviceaccess import DeviceAccess +from numpy.typing import NDArray +# Define the main class name for this module +PYAMLCLASS = "BPMTiltOffsetModel" + +class ConfigModel(BaseModel): + + model_config = ConfigDict(arbitrary_types_allowed=True,extra="forbid") + + x_pos: DeviceAccess + """Horizontal position""" + y_pos: DeviceAccess + """Vertical position""" + +class BPMSimpleModel(BPMModel): + """ + Concrete implementation of BPMModel that simulates a BPM with tilt and + offset values. + """ + def __init__(self, cfg: ConfigModel): + self._cfg = cfg + + self.__x_pos = cfg.x_pos + self.__y_pos = cfg.y_pos + + def read_hardware_position_values(self) -> NDArray: + """ + Simulate reading the position values from a BPM. + Returns + ------- + np.ndarray + Array of shape (2,) containing the horizontal and vertical + positions + """ + return np.array([self.__x_pos.get(), self.__y_pos.get()]) + + def read_hardware_tilt_value(self) -> float: + """ + Simulate reading the tilt value from a BPM. + Returns + ------- + float + The tilt value of the BPM + """ + raise NotImplementedError("Tilt reading not implemented in this model.") + + def read_hardware_offset_values(self) -> NDArray: + """ + Simulate reading the offset values from a BPM. + Returns + ------- + np.ndarray + Array of shape (2,) containing the horizontal and vertical + offsets + """ + raise NotImplementedError("Offset reading not implemented in this model.") + def set_hardware_tilt_value(self, tilt: float): + """ + Simulate setting the tilt value of a BPM. + Parameters + ---------- + tilt : float + The tilt value to set for the BPM + Returns + ------- + None + """ + raise NotImplementedError("Tilt setting not implemented in this model.") + + def set_hardware_offset_values(self, offset_values: np.ndarray): + """ + Simulate setting the offset values of a BPM + Parameters + ---------- + offset_values : np.ndarray + Array of shape (2,) containing the horizontal and vertical + offsets to set for the BPM + """ + raise NotImplementedError("Offset setting not implemented in this model.") + diff --git a/pyaml/control/abstract_impl.py b/pyaml/control/abstract_impl.py index 50b8c221..c6820615 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -131,7 +131,7 @@ def get(self) -> np.array: # Gets the unit of the value def unit(self) -> list[str]: - return self.__model.get_hardware_position_units() + return [self.__model.__x_pos.unit(), self.__model.__y_pos.unit()] #------------------------------------------------------------------------------ @@ -153,7 +153,7 @@ def set_and_wait(self, value: NDArray[np.float64]): raise NotImplementedError("Not implemented yet.") # Gets the unit of the value def unit(self) -> str: - return self.__model.get_hardware_angle_unit() + return self.__model.__tilt.unit() #------------------------------------------------------------------------------ @@ -175,7 +175,7 @@ def set_and_wait(self, value: NDArray[np.float64]): raise NotImplementedError("Not implemented yet.") # Gets the unit of the value def unit(self) -> str: - return self.__model.get_hardware_position_units()[0] + return self.__model.__x_offset.unit() #------------------------------------------------------------------------------ From 55c7df82ee9e43d32768643a1bed3ccf1e28feb8 Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Wed, 22 Oct 2025 11:39:30 +0200 Subject: [PATCH 11/14] Added SimpleBPM to tests --- pyaml/bpm/bpm_simple_model.py | 2 +- tests/config/bpms.yaml | 12 ++++++++++++ tests/test_bpm.py | 2 ++ tests/test_bpm_controlsystem.py | 2 ++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pyaml/bpm/bpm_simple_model.py b/pyaml/bpm/bpm_simple_model.py index a470c94c..4ad5aee5 100644 --- a/pyaml/bpm/bpm_simple_model.py +++ b/pyaml/bpm/bpm_simple_model.py @@ -4,7 +4,7 @@ from ..control.deviceaccess import DeviceAccess from numpy.typing import NDArray # Define the main class name for this module -PYAMLCLASS = "BPMTiltOffsetModel" +PYAMLCLASS = "BPMSimpleModel" class ConfigModel(BaseModel): diff --git a/tests/config/bpms.yaml b/tests/config/bpms.yaml index c571c590..4da1a2ee 100644 --- a/tests/config/bpms.yaml +++ b/tests/config/bpms.yaml @@ -37,3 +37,15 @@ instruments: type: tango.pyaml.attribute attribute: srdiag/bpm/c01-01/Tilt_Angle unit: rad + - type: pyaml.bpm.bpm + name: BPM_C01-02 + model: + type: pyaml.bpm.bpm_simple_model + x_pos: + type: tango.pyaml.attribute + attribute: srdiag/bpm/c01-02/SA_HPosition + unit: mm + y_pos: + type: tango.pyaml.attribute + attribute: srdiag/bpm/c01-02/SA_VPosition + unit: mm diff --git a/tests/test_bpm.py b/tests/test_bpm.py index dda8561d..54c00259 100644 --- a/tests/test_bpm.py +++ b/tests/test_bpm.py @@ -51,7 +51,9 @@ def test_simulator_bpm_position(install_test_package): sr:Instrument = ml.get('sr') sr.design.get_lattice().disable_6d() bpm = sr.design.get_bpm('BPM_C01-01') + bpm_simple = sr.live.get_bpm('BPM_C01-02') assert np.allclose( bpm.positions.get(), np.array([0.0,0.0]) ) + assert np.allclose( bpm_simple.positions.get(), np.array([0.0,0.0]) ) Factory.clear() diff --git a/tests/test_bpm_controlsystem.py b/tests/test_bpm_controlsystem.py index cbb96dcd..d1d2518f 100644 --- a/tests/test_bpm_controlsystem.py +++ b/tests/test_bpm_controlsystem.py @@ -51,7 +51,9 @@ def test_controlsystem_bpm_position(install_test_package): ml:PyAML = pyaml("tests/config/bpms.yaml") sr:Instrument = ml.get('sr') bpm = sr.live.get_bpm('BPM_C01-01') + bpm_simple = sr.live.get_bpm('BPM_C01-02') assert np.allclose( bpm.positions.get(), np.array([0.0,0.0]) ) + assert np.allclose( bpm_simple.positions.get(), np.array([0.0,0.0]) ) Factory.clear() From 0ae537a1a6ec0a132b8b2df4b7dc4e9bcc7f0ce3 Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Wed, 22 Oct 2025 11:57:26 +0200 Subject: [PATCH 12/14] Inheriting from BPMSimpleModel --- pyaml/bpm/bpm_tiltoffset_model.py | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/pyaml/bpm/bpm_tiltoffset_model.py b/pyaml/bpm/bpm_tiltoffset_model.py index 9f1331ba..1fe67a46 100644 --- a/pyaml/bpm/bpm_tiltoffset_model.py +++ b/pyaml/bpm/bpm_tiltoffset_model.py @@ -1,4 +1,5 @@ from pyaml.bpm.bpm_model import BPMModel +from pyaml.bpm.bpm_simple_model import BPMSimpleModel from pydantic import BaseModel,ConfigDict import numpy as np from ..control.deviceaccess import DeviceAccess @@ -21,31 +22,19 @@ class ConfigModel(BaseModel): tilt: DeviceAccess """BPM tilt""" -class BPMTiltOffsetModel(BPMModel): +class BPMTiltOffsetModel(BPMSimpleModel): """ Concrete implementation of BPMModel that simulates a BPM with tilt and offset values. """ def __init__(self, cfg: ConfigModel): - self._cfg = cfg - + super().__init__(cfg) self.__x_pos = cfg.x_pos self.__y_pos = cfg.y_pos self.__x_offset = cfg.x_offset self.__y_offset = cfg.y_offset self.__tilt = cfg.tilt - def read_hardware_position_values(self) -> NDArray: - """ - Simulate reading the position values from a BPM. - Returns - ------- - np.ndarray - Array of shape (2,) containing the horizontal and vertical - positions - """ - return np.array([self.__x_pos.get(), self.__y_pos.get()]) - def read_hardware_tilt_value(self) -> float: """ Simulate reading the tilt value from a BPM. From 5f51ce4c9c7769a3a9654279a5cb64197383fc71 Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Thu, 23 Oct 2025 13:07:01 +0200 Subject: [PATCH 13/14] Following JLP suggestions. Shorter names for some BPM model functions. --- pyaml/bpm/bpm.py | 28 ++++++++++++++++++---------- pyaml/bpm/bpm_model.py | 10 +++++----- pyaml/bpm/bpm_simple_model.py | 18 +++++++++--------- pyaml/bpm/bpm_tiltoffset_model.py | 16 ++++++++-------- pyaml/control/abstract_impl.py | 10 +++++----- pyaml/lattice/element_holder.py | 1 - 6 files changed, 45 insertions(+), 38 deletions(-) diff --git a/pyaml/bpm/bpm.py b/pyaml/bpm/bpm.py index c8282698..f0195e3e 100644 --- a/pyaml/bpm/bpm.py +++ b/pyaml/bpm/bpm.py @@ -9,10 +9,9 @@ class ConfigModel(ElementConfigModel): - hardware: DeviceAccess | None = None - """Direct access to a magnet device that provides strength/current conversion""" + # hardware: DeviceAccess | None = None model: BPMModel | None = None - """Object in charge of converting magnet strenghts to power supply values""" + """Object in charge of BPM modeling""" @@ -37,14 +36,17 @@ def __init__(self, cfg: ConfigModel): super().__init__(cfg.name) - self.__hardware = cfg.hardware if hasattr(cfg, "hardware") else None + # self.__hardware = cfg.hardware if hasattr(cfg, "hardware") else None self.__model = cfg.model if hasattr(cfg, "model") else None self._cfg = cfg - @property - def hardware(self) -> abstract.ReadWriteFloatScalar: - if self.__hardware is None: - raise Exception(f"{str(self)} has no model that supports hardware units") - return self.__hardware + self.__positions = None + self.__offset = None + self.__tilt = None + # @property + # def hardware(self) -> abstract.ReadWriteFloatScalar: + # if self.__hardware is None: + # raise Exception(f"{str(self)} has no model that supports hardware units") + # return self.__hardware @property def model(self) -> BPMModel: @@ -52,14 +54,20 @@ def model(self) -> BPMModel: @property def positions(self) -> RBpmArray: + if self.__positions is None: + raise Exception(f"{str(self)} has no attached positions") return self.__positions @property def offset(self) -> RWBpmOffsetArray: + if self.__offset is None: + raise Exception(f"{str(self)} has no attached offset") return self.__offset @property def tilt(self) -> RWBpmTiltScalar: + if self.__tilt is None: + raise Exception(f"{str(self)} has no attached tilt") return self.__tilt def attach(self, positions: RBpmArray , offset: RWBpmOffsetArray, @@ -68,7 +76,7 @@ def attach(self, positions: RBpmArray , offset: RWBpmOffsetArray, # reference obj = self.__class__(self._cfg) obj.__model = self.__model - obj.__hardware = self.__hardware + # obj.__hardware = self.__hardware obj.__positions = positions obj.__offset = offset obj.__tilt = tilt diff --git a/pyaml/bpm/bpm_model.py b/pyaml/bpm/bpm_model.py index 654fe3f6..68326c2d 100644 --- a/pyaml/bpm/bpm_model.py +++ b/pyaml/bpm/bpm_model.py @@ -8,7 +8,7 @@ class BPMModel(metaclass=ABCMeta): tilts. """ @abstractmethod - def read_hardware_position_values(self) -> NDArray[np.float64]: + def read_position(self) -> NDArray[np.float64]: """ Read horizontal and vertical positions from a BPM. Returns @@ -20,7 +20,7 @@ def read_hardware_position_values(self) -> NDArray[np.float64]: pass @abstractmethod - def read_hardware_tilt_value(self) -> float: + def read_tilt(self) -> float: """ Read the tilt value from a BPM. Returns @@ -31,7 +31,7 @@ def read_hardware_tilt_value(self) -> float: pass @abstractmethod - def read_hardware_offset_values(self) -> NDArray: + def read_offset(self) -> NDArray: """ Read the offset values from a BPM. Returns @@ -43,7 +43,7 @@ def read_hardware_offset_values(self) -> NDArray: pass @abstractmethod - def set_hardware_tilt_value(self, tilt: float): + def set_tilt(self, tilt: float): """ Set the tilt value of a BPM. Parameters @@ -57,7 +57,7 @@ def set_hardware_tilt_value(self, tilt: float): pass @abstractmethod - def set_hardware_offset_values(self, offset_values: NDArray[np.float64]): + def set_offset(self, offset: NDArray[np.float64]): """ Set the offset values of a BPM Parameters diff --git a/pyaml/bpm/bpm_simple_model.py b/pyaml/bpm/bpm_simple_model.py index 4ad5aee5..d362bcb1 100644 --- a/pyaml/bpm/bpm_simple_model.py +++ b/pyaml/bpm/bpm_simple_model.py @@ -26,7 +26,7 @@ def __init__(self, cfg: ConfigModel): self.__x_pos = cfg.x_pos self.__y_pos = cfg.y_pos - def read_hardware_position_values(self) -> NDArray: + def read_position(self) -> NDArray: """ Simulate reading the position values from a BPM. Returns @@ -36,8 +36,8 @@ def read_hardware_position_values(self) -> NDArray: positions """ return np.array([self.__x_pos.get(), self.__y_pos.get()]) - - def read_hardware_tilt_value(self) -> float: + + def read_tilt(self) -> float: """ Simulate reading the tilt value from a BPM. Returns @@ -46,8 +46,8 @@ def read_hardware_tilt_value(self) -> float: The tilt value of the BPM """ raise NotImplementedError("Tilt reading not implemented in this model.") - - def read_hardware_offset_values(self) -> NDArray: + + def read_offset(self) -> NDArray: """ Simulate reading the offset values from a BPM. Returns @@ -57,7 +57,7 @@ def read_hardware_offset_values(self) -> NDArray: offsets """ raise NotImplementedError("Offset reading not implemented in this model.") - def set_hardware_tilt_value(self, tilt: float): + def set_tilt(self, tilt: float): """ Simulate setting the tilt value of a BPM. Parameters @@ -69,8 +69,8 @@ def set_hardware_tilt_value(self, tilt: float): None """ raise NotImplementedError("Tilt setting not implemented in this model.") - - def set_hardware_offset_values(self, offset_values: np.ndarray): + + def set_offset(self, offset_values: np.ndarray): """ Simulate setting the offset values of a BPM Parameters @@ -80,4 +80,4 @@ def set_hardware_offset_values(self, offset_values: np.ndarray): offsets to set for the BPM """ raise NotImplementedError("Offset setting not implemented in this model.") - + diff --git a/pyaml/bpm/bpm_tiltoffset_model.py b/pyaml/bpm/bpm_tiltoffset_model.py index 1fe67a46..115ebc26 100644 --- a/pyaml/bpm/bpm_tiltoffset_model.py +++ b/pyaml/bpm/bpm_tiltoffset_model.py @@ -35,7 +35,7 @@ def __init__(self, cfg: ConfigModel): self.__y_offset = cfg.y_offset self.__tilt = cfg.tilt - def read_hardware_tilt_value(self) -> float: + def read_tilt(self) -> float: """ Simulate reading the tilt value from a BPM. Returns @@ -44,8 +44,8 @@ def read_hardware_tilt_value(self) -> float: The tilt value of the BPM """ return self.__tilt.get() - - def read_hardware_offset_values(self) -> NDArray: + + def read_offset(self) -> NDArray: """ Simulate reading the offset values from a BPM. Returns @@ -55,8 +55,8 @@ def read_hardware_offset_values(self) -> NDArray: offsets """ return np.array([self.__x_offset.get(), self.__y_offset.get()]) - - def set_hardware_tilt_value(self, tilt: float): + + def set_tilt(self, tilt: float): """ Simulate setting the tilt value of a BPM. Parameters @@ -68,8 +68,8 @@ def set_hardware_tilt_value(self, tilt: float): None """ self.__tilt.set(tilt) - - def set_hardware_offset_values(self, offset_values: np.ndarray): + + def set_offset(self, offset_values: np.ndarray): """ Simulate setting the offset values of a BPM Parameters @@ -80,4 +80,4 @@ def set_hardware_offset_values(self, offset_values: np.ndarray): """ self.__x_offset.set(offset_values[0]) self.__y_offset.set(offset_values[1]) - + diff --git a/pyaml/control/abstract_impl.py b/pyaml/control/abstract_impl.py index c6820615..d42841bc 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -127,7 +127,7 @@ def __init__(self, model:BPMModel): # Gets the value def get(self) -> np.array: - return self.__model.read_hardware_position_values() + return self.__model.read_position() # Gets the unit of the value def unit(self) -> list[str]: @@ -144,10 +144,10 @@ def __init__(self, model:BPMModel): # Gets the value def get(self) -> float: - return self.__model.read_hardware_tilt_value() + return self.__model.read_tilt() def set(self, value:float): - self.__model.set_hardware_tilt_value(value) + self.__model.set_tilt(value) def set_and_wait(self, value: NDArray[np.float64]): raise NotImplementedError("Not implemented yet.") @@ -166,11 +166,11 @@ def __init__(self, model: BPMModel): # Gets the value def get(self) -> NDArray[np.float64]: - return self.__model.read_hardware_offset_values() + return self.__model.read_offset() # Sets the value def set(self, value: NDArray[np.float64]): - self.__model.set_hardware_offset_values(value) + self.__model.set_offset(value) def set_and_wait(self, value: NDArray[np.float64]): raise NotImplementedError("Not implemented yet.") # Gets the unit of the value diff --git a/pyaml/lattice/element_holder.py b/pyaml/lattice/element_holder.py index e5f257e8..57a6646b 100644 --- a/pyaml/lattice/element_holder.py +++ b/pyaml/lattice/element_holder.py @@ -75,7 +75,6 @@ def get_magnets(self,name:str) -> MagnetArray: def get_bpm(self,name:str) -> Element: if name not in self.__BPMS: - print(self.__BPMS.keys()) raise Exception(f"BPM {name} not defined") return self.__BPMS[name] From 37a897fabf41e768d032cea5fe5f73fcf54fcefc Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Thu, 23 Oct 2025 14:07:11 +0200 Subject: [PATCH 14/14] Added tests with nonzero orbit at BPMs (Simulator). Removed __hardware part from BPM. Corrected bug in tests. --- pyaml/bpm/bpm.py | 8 -------- pyaml/lattice/abstract_impl.py | 4 ++-- tests/config/bpms.yaml | 23 +++++++++++++++++++++++ tests/test_bpm.py | 22 ++++++++++++++++++++++ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/pyaml/bpm/bpm.py b/pyaml/bpm/bpm.py index f0195e3e..2841fd24 100644 --- a/pyaml/bpm/bpm.py +++ b/pyaml/bpm/bpm.py @@ -9,7 +9,6 @@ class ConfigModel(ElementConfigModel): - # hardware: DeviceAccess | None = None model: BPMModel | None = None """Object in charge of BPM modeling""" @@ -36,17 +35,11 @@ def __init__(self, cfg: ConfigModel): super().__init__(cfg.name) - # self.__hardware = cfg.hardware if hasattr(cfg, "hardware") else None self.__model = cfg.model if hasattr(cfg, "model") else None self._cfg = cfg self.__positions = None self.__offset = None self.__tilt = None - # @property - # def hardware(self) -> abstract.ReadWriteFloatScalar: - # if self.__hardware is None: - # raise Exception(f"{str(self)} has no model that supports hardware units") - # return self.__hardware @property def model(self) -> BPMModel: @@ -76,7 +69,6 @@ def attach(self, positions: RBpmArray , offset: RWBpmOffsetArray, # reference obj = self.__class__(self._cfg) obj.__model = self.__model - # obj.__hardware = self.__hardware obj.__positions = positions obj.__offset = offset obj.__tilt = tilt diff --git a/pyaml/lattice/abstract_impl.py b/pyaml/lattice/abstract_impl.py index 4a6fb16a..a78f80b4 100644 --- a/pyaml/lattice/abstract_impl.py +++ b/pyaml/lattice/abstract_impl.py @@ -163,8 +163,8 @@ def __init__(self, element: at.Element, lattice: at.Lattice): # Gets the value def get(self) -> np.array: - index = self.lattice.get_refpts(self.element.FamName) - _, orbit = at.find_orbit(self.lattice, index) + index = self.lattice.index(self.element) + _, orbit = at.find_orbit(self.lattice, refpts=index) return orbit[0, [0, 2]] # Gets the unit of the value diff --git a/tests/config/bpms.yaml b/tests/config/bpms.yaml index 4da1a2ee..ec190c7d 100644 --- a/tests/config/bpms.yaml +++ b/tests/config/bpms.yaml @@ -49,3 +49,26 @@ instruments: type: tango.pyaml.attribute attribute: srdiag/bpm/c01-02/SA_VPosition unit: mm + - type: pyaml.bpm.bpm + name: BPM_C01-03 + model: + type: pyaml.bpm.bpm_simple_model + x_pos: + type: tango.pyaml.attribute + attribute: srdiag/bpm/c01-03/SA_HPosition + unit: mm + y_pos: + type: tango.pyaml.attribute + attribute: srdiag/bpm/c01-03/SA_VPosition + unit: mm + - type: pyaml.magnet.cfm_magnet + name: SH1A-C01 #Name of the element in the lattice model + mapping: + # Multipole mapping for usage in families, in this example SH1-C01A-H is not + # a lattice element present in the model, it is just a name to use in + # PyAML families. When this 'virutal' element is set, it then applies + # the corresponding multipole on the parent element. + - [B0, SH1A-C01-H] + - [A0, SH1A-C01-V] + - [A1, SH1A-C01-SQ] + model: sr/magnet_models/SH1AC01.yaml diff --git a/tests/test_bpm.py b/tests/test_bpm.py index 54c00259..4a2411ca 100644 --- a/tests/test_bpm.py +++ b/tests/test_bpm.py @@ -57,3 +57,25 @@ def test_simulator_bpm_position(install_test_package): assert np.allclose( bpm_simple.positions.get(), np.array([0.0,0.0]) ) Factory.clear() + +@pytest.mark.parametrize("install_test_package", [{ + "name": "tango", + "path": "tests/dummy_cs/tango" +}], indirect=True) +def test_simulator_bpm_position_with_bad_corrector_strength(install_test_package): + + ml:PyAML = pyaml("tests/config/bpms.yaml") + sr:Instrument = ml.get('sr') + sr.design.get_lattice().disable_6d() + bpm = sr.design.get_bpm('BPM_C01-01') + bpm_simple = sr.design.get_bpm('BPM_C01-02') + bpm3 = sr.design.get_bpm('BPM_C01-03') + + sr.design.get_magnet("SH1A-C01-H").strength.set(-1e-6) + sr.design.get_magnet("SH1A-C01-V").strength.set(-1e-6) + for bpm in [bpm, bpm_simple, bpm3]: + assert bpm.positions.get()[0] != 0.0 + assert bpm.positions.get()[1] != 0.0 + + Factory.clear() +