diff --git a/pyaml/bpm/bpm.py b/pyaml/bpm/bpm.py new file mode 100644 index 00000000..2841fd24 --- /dev/null +++ b/pyaml/bpm/bpm.py @@ -0,0 +1,76 @@ +from pyaml.lattice.element import Element, ElementConfigModel +from pyaml.lattice.abstract_impl import RBpmArray, RWBpmOffsetArray, RWBpmTiltScalar +from ..control.deviceaccess import DeviceAccess +from ..control import abstract +from typing import Self +from pyaml.bpm.bpm_model import BPMModel + +PYAMLCLASS = "BPM" + +class ConfigModel(ElementConfigModel): + + model: BPMModel | None = None + """Object in charge of BPM modeling""" + + + +class BPM(Element): + """ + Class providing access to one BPM of a physical or simulated lattice + """ + + def __init__(self, cfg: ConfigModel): + """ + 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__(cfg.name) + + self.__model = cfg.model if hasattr(cfg, "model") else None + self._cfg = cfg + self.__positions = None + self.__offset = None + self.__tilt = None + + @property + def model(self) -> BPMModel: + return self.__model + + @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, + tilt: RWBpmTiltScalar) -> Self: + # Attach positions, offset and tilt attributes and returns a new + # reference + obj = self.__class__(self._cfg) + obj.__model = self.__model + 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..68326c2d --- /dev/null +++ b/pyaml/bpm/bpm_model.py @@ -0,0 +1,70 @@ +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_position(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_tilt(self) -> float: + """ + Read the tilt value from a BPM. + Returns + ------- + float + The tilt value of the BPM + """ + pass + + @abstractmethod + def read_offset(self) -> NDArray: + """ + 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_tilt(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_offset(self, offset: 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 + diff --git a/pyaml/bpm/bpm_simple_model.py b/pyaml/bpm/bpm_simple_model.py new file mode 100644 index 00000000..d362bcb1 --- /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 = "BPMSimpleModel" + +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_position(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_tilt(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_offset(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_tilt(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_offset(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/bpm/bpm_tiltoffset_model.py b/pyaml/bpm/bpm_tiltoffset_model.py new file mode 100644 index 00000000..115ebc26 --- /dev/null +++ b/pyaml/bpm/bpm_tiltoffset_model.py @@ -0,0 +1,83 @@ +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 +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""" + x_offset: DeviceAccess + """Horizontal BPM offset""" + y_offset: DeviceAccess + """Vertical BPM offset""" + tilt: DeviceAccess + """BPM tilt""" + +class BPMTiltOffsetModel(BPMSimpleModel): + """ + Concrete implementation of BPMModel that simulates a BPM with tilt and + offset values. + """ + def __init__(self, cfg: ConfigModel): + 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_tilt(self) -> float: + """ + Simulate reading the tilt value from a BPM. + Returns + ------- + float + The tilt value of the BPM + """ + return self.__tilt.get() + + def read_offset(self) -> NDArray: + """ + Simulate reading the offset values from a BPM. + Returns + ------- + np.ndarray + Array of shape (2,) containing the horizontal and vertical + offsets + """ + return np.array([self.__x_offset.get(), self.__y_offset.get()]) + + def set_tilt(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.__tilt.set(tilt) + + def set_offset(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.__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 f86e7569..d42841bc 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -1,11 +1,11 @@ from numpy import double -import numpy as np - -from ..control import abstract -from ..magnet.model import MagnetModel +from pyaml.control import abstract +from pyaml.magnet.model import MagnetModel +from pyaml.bpm.bpm_model import BPMModel from ..rf.rf_plant import RFPlant from ..rf.rf_transmitter import RFTransmitter - +import numpy as np +from numpy.typing import NDArray #------------------------------------------------------------------------------ class RWHardwareScalar(abstract.ReadWriteFloatScalar): @@ -116,6 +116,66 @@ 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_position() + + # Gets the unit of the value + def unit(self) -> list[str]: + return [self.__model.__x_pos.unit(), self.__model.__y_pos.unit()] + +#------------------------------------------------------------------------------ + +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_tilt() + + def set(self, value:float): + self.__model.set_tilt(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.__tilt.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_offset() + + # Sets the value + def set(self, value: NDArray[np.float64]): + 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 + def unit(self) -> str: + return self.__model.__x_offset.unit() #------------------------------------------------------------------------------ diff --git a/pyaml/control/controlsystem.py b/pyaml/control/controlsystem.py index ca272318..bdf3843f 100644 --- a/pyaml/control/controlsystem.py +++ b/pyaml/control/controlsystem.py @@ -2,6 +2,8 @@ from ..lattice.element_holder import ElementHolder from ..lattice.element import Element from ..control.abstract_impl import RWHardwareScalar,RWHardwareArray,RWStrengthScalar,RWStrengthArray +from ..bpm.bpm import BPM +from ..control.abstract_impl import RWBpmTiltScalar,RWBpmOffsetArray, RBpmArray from ..control.abstract_impl import RWRFFrequencyScalar,RWRFVoltageScalar,RWRFPhaseScalar from ..magnet.magnet import Magnet from ..magnet.cfm_magnet import CombinedFunctionMagnet @@ -63,6 +65,13 @@ 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): + 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) + elif isinstance(e,RFPlant): self.add_rf_plant(e.get_name(),e) diff --git a/pyaml/lattice/abstract_impl.py b/pyaml/lattice/abstract_impl.py index fb5eac0c..a78f80b4 100644 --- a/pyaml/lattice/abstract_impl.py +++ b/pyaml/lattice/abstract_impl.py @@ -148,6 +148,100 @@ def unit(self) -> list[str]: #------------------------------------------------------------------------------ +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.index(self.element) + _, orbit = at.find_orbit(self.lattice, refpts=index) + return orbit[0, [0, 2]] + + # Gets the unit of the value + def unit(self) -> str: + return '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) -> str: + return '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 + class RWRFVoltageScalar(abstract.ReadWriteFloatScalar): """ Class providing read write access to a cavity voltage of a simulator for a given RF trasnmitter. diff --git a/pyaml/lattice/element_holder.py b/pyaml/lattice/element_holder.py index 1b7113ff..57a6646b 100644 --- a/pyaml/lattice/element_holder.py +++ b/pyaml/lattice/element_holder.py @@ -71,4 +71,13 @@ def get_all_magnets(self) -> dict: 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 + return self.__MAGNET_ARRAYS[name] + + 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 15dd1e9e..8fa858c9 100644 --- a/pyaml/lattice/simulator.py +++ b/pyaml/lattice/simulator.py @@ -7,6 +7,7 @@ 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 ..rf.rf_plant import RFPlant,RWTotalVoltage from ..rf.rf_transmitter import RFTransmitter @@ -14,7 +15,7 @@ from ..lattice.abstract_impl import RWStrengthScalar,RWStrengthArray from ..lattice.abstract_impl import RWRFFrequencyScalar,RWRFVoltageScalar,RWRFPhaseScalar from .element_holder import ElementHolder - +from ..lattice.abstract_impl import RWBpmTiltScalar,RWBpmOffsetArray, RBpmArray # Define the main class name for this module PYAMLCLASS = "Simulator" @@ -78,8 +79,15 @@ def fill_device(self,elements:list[Element]): # Create unique refs of each function for this simulator ms = e.attach(strengths,currents) for m in ms: - self.add_magnet(m.get_name(),m) - self.add_magnet(m.get_name(),m) + self.add_magnet(m.get_name(), m) + 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)[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(e.get_name(),e) elif isinstance(e,RFPlant): self.add_rf_plant(e.get_name(),e) diff --git a/tests/config/bpms.yaml b/tests/config/bpms.yaml new file mode 100644 index 00000000..ec190c7d --- /dev/null +++ b/tests/config/bpms.yaml @@ -0,0 +1,74 @@ +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_C01-01 + model: + type: pyaml.bpm.bpm_tiltoffset_model + 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 + - 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 + - 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 new file mode 100644 index 00000000..4a2411ca --- /dev/null +++ b/tests/test_bpm.py @@ -0,0 +1,81 @@ + +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_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_C01-01') + 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_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_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_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_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() + +@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() + diff --git a/tests/test_bpm_controlsystem.py b/tests/test_bpm_controlsystem.py new file mode 100644 index 00000000..d1d2518f --- /dev/null +++ b/tests/test_bpm_controlsystem.py @@ -0,0 +1,59 @@ + + +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') + 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()