From b79ee36b45a6abef1866307ec620a2a85ea09ce4 Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Tue, 4 Nov 2025 13:48:00 +0100 Subject: [PATCH 1/3] Added a simple tune_monitor (only a readout, only betatron tunes). Added tests. --- pyaml/common/element_holder.py | 17 ++++++++++- pyaml/control/abstract_impl.py | 20 ++++++++++++- pyaml/control/controlsystem.py | 6 ++++ pyaml/diagnostics/tune_monitor.py | 50 +++++++++++++++++++++++++++++++ pyaml/lattice/abstract_impl.py | 16 ++++++++++ pyaml/lattice/simulator.py | 6 ++++ tests/config/tune_monitor.yaml | 25 ++++++++++++++++ tests/test_tune_monitor.py | 37 +++++++++++++++++++++++ 8 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 pyaml/diagnostics/tune_monitor.py create mode 100644 tests/config/tune_monitor.yaml create mode 100644 tests/test_tune_monitor.py diff --git a/pyaml/common/element_holder.py b/pyaml/common/element_holder.py index 01a732b4..24da1b37 100644 --- a/pyaml/common/element_holder.py +++ b/pyaml/common/element_holder.py @@ -21,6 +21,7 @@ def __init__(self): self.__RFPLANT: dict = {} self.__RFTRANSMITTER: dict = {} self.__OTHERS: dict = {} + self.__DIAG: dict = {} # Array handle self.__MAGNET_ARRAYS: dict = {} @@ -105,4 +106,18 @@ def get_rf_trasnmitter(self,name:str) -> RFTransmitter: - + 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 + + def get_betatron_tune_monitor(self, name:str) -> Element: + if name not in self.__DIAG: + raise Exception(f"Diagnostic devices array does not contain {name}") + return self.__DIAG[name] + + def add_betatron_tune_monitor(self, name:str, tune_monitor:Element): + self.__DIAG[name] = tune_monitor diff --git a/pyaml/control/abstract_impl.py b/pyaml/control/abstract_impl.py index 05e9de89..73aa3c76 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -7,8 +7,8 @@ from ..rf.rf_plant import RFPlant from ..rf.rf_transmitter import RFTransmitter from ..common.abstract_aggregator import ScalarAggregator - from numpy import double +from ..diagnostics.tune_monitor import BetatronTuneMonitor import numpy as np from numpy.typing import NDArray @@ -350,3 +350,21 @@ def set_and_wait(self, value:float): def unit(self) -> str: return self.__rf._cfg.masterclock.unit() +#------------------------------------------------------------------------------ + +class RBetatronTuneArray(abstract.ReadFloatScalar): + """ + Class providing read write access to betatron tune of a control system. + """ + + def __init__(self, tune_monitor): + self.__tune_monitor = tune_monitor + + def get(self) -> list[float]: + # Serialized cavity has the same frequency + return [self.__tune_monitor._cfg.tune_h.get(), + self.__tune_monitor._cfg.tune_v.get()] + + def unit(self) -> str: + return self.__tune_monitor._cfg.tune_v.unit() + diff --git a/pyaml/control/controlsystem.py b/pyaml/control/controlsystem.py index 7d02fce2..49ba2b1d 100644 --- a/pyaml/control/controlsystem.py +++ b/pyaml/control/controlsystem.py @@ -4,10 +4,12 @@ from ..lattice.element import Element from ..control.abstract_impl import RWHardwareScalar,RWHardwareArray,RWStrengthScalar,RWStrengthArray from ..bpm.bpm import BPM +from ..diagnostics.tune_monitor import BetatronTuneMonitor from ..control.abstract_impl import RWBpmTiltScalar,RWBpmOffsetArray, RBpmArray from ..control.abstract_impl import RWRFFrequencyScalar,RWRFVoltageScalar,RWRFPhaseScalar from ..control.abstract_impl import CSScalarAggregator,CSStrengthScalarAggregator from ..common.abstract_aggregator import ScalarAggregator +from ..control.abstract_impl import RBetatronTuneArray from ..magnet.magnet import Magnet from ..magnet.cfm_magnet import CombinedFunctionMagnet from ..rf.rf_plant import RFPlant,RWTotalVoltage @@ -134,3 +136,7 @@ def fill_device(self,elements:list[Element]): voltage = RWTotalVoltage(attachedTrans) ne = e.attach(frequency,voltage) self.add_rf_plant(ne.get_name(),ne) + elif isinstance(e,BetatronTuneMonitor): + betatron_tune = RBetatronTuneArray(e) + e = e.attach(betatron_tune) + self.add_betatron_tune_monitor(e.get_name(), e) diff --git a/pyaml/diagnostics/tune_monitor.py b/pyaml/diagnostics/tune_monitor.py new file mode 100644 index 00000000..ffa2ed4e --- /dev/null +++ b/pyaml/diagnostics/tune_monitor.py @@ -0,0 +1,50 @@ + +from pyaml.lattice.element import Element, ElementConfigModel +from pyaml.lattice.abstract_impl import RBetatronTuneArray +from ..control.deviceaccess import DeviceAccess +from typing import Self +from pydantic import ConfigDict +from ..control.deviceaccess import DeviceAccess + +PYAMLCLASS = "BetatronTuneMonitor" + +class ConfigModel(ElementConfigModel): + + model_config = ConfigDict(arbitrary_types_allowed=True,extra="forbid") + + tune_h: DeviceAccess + """Horizontal betatron tune""" + tune_v: DeviceAccess + """Vertical betatron tune""" + +class BetatronTuneMonitor(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 + model : BetatronTuneMonitorModel + BetatronTuneMonitorModel + """ + + super().__init__(cfg.name) + self._cfg = cfg + self.__tune = None + + @property + def tune(self) -> RBetatronTuneArray: + return self.__tune + + def attach(self, betatron_tune: RBetatronTuneArray) -> Self: + + obj = self.__class__(self._cfg) + obj.__tune = betatron_tune + return obj + diff --git a/pyaml/lattice/abstract_impl.py b/pyaml/lattice/abstract_impl.py index ff396ead..5b649614 100644 --- a/pyaml/lattice/abstract_impl.py +++ b/pyaml/lattice/abstract_impl.py @@ -378,3 +378,19 @@ def set_and_wait(self, value:float): def unit(self) -> str: return self.__rf._cfg.masterclock.unit() +#------------------------------------------------------------------------------ + +class RBetatronTuneArray(abstract.ReadFloatScalar): + """ + Class providing read write access to RF frequency of a simulator. + """ + + def __init__(self, ring: at.Lattice): + self.__ring = ring + + def get(self) -> float: + return self.__ring.get_tune()[:2] + + def unit(self) -> str: + return '1' + diff --git a/pyaml/lattice/simulator.py b/pyaml/lattice/simulator.py index 4cf1ed75..169540b8 100644 --- a/pyaml/lattice/simulator.py +++ b/pyaml/lattice/simulator.py @@ -5,6 +5,7 @@ from pathlib import Path from ..magnet.magnet import Magnet from pyaml.bpm.bpm import BPM +from pyaml.diagnostics.tune_monitor import BetatronTuneMonitor from ..magnet.cfm_magnet import CombinedFunctionMagnet from ..rf.rf_plant import RFPlant,RWTotalVoltage from ..rf.rf_transmitter import RFTransmitter @@ -13,6 +14,7 @@ from ..lattice.abstract_impl import RWRFFrequencyScalar,RWRFVoltageScalar,RWRFPhaseScalar from ..common.element_holder import ElementHolder from ..common.abstract_aggregator import ScalarAggregator +from ..lattice.abstract_impl import RBetatronTuneArray from ..lattice.abstract_impl import RWBpmTiltScalar,RWBpmOffsetArray, RBpmArray from ..lattice.abstract_impl import BPMHScalarAggregator,BPMScalarAggregator,BPMVScalarAggregator from ..common.exception import PyAMLException @@ -140,6 +142,10 @@ def fill_device(self,elements:list[Element]): voltage = RWTotalVoltage(attachedTrans) ne = e.attach(frequency,voltage) self.add_rf_plant(ne.get_name(),ne) + elif isinstance(e, BetatronTuneMonitor): + betatron_tune = RBetatronTuneArray(self.ring) + e = e.attach(betatron_tune) + self.add_betatron_tune_monitor(e.get_name(), e) def get_at_elems(self,element:Element) -> list[at.Element]: diff --git a/tests/config/tune_monitor.yaml b/tests/config/tune_monitor.yaml new file mode 100644 index 00000000..8c01ef7b --- /dev/null +++ b/tests/config/tune_monitor.yaml @@ -0,0 +1,25 @@ +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.diagnostics.tune_monitor + name: BETATRON_TUNE + tune_h: + type: tango.pyaml.attribute_read_only + attribute: srdiag/tune/tune_v + unit: mm + tune_v: + type: tango.pyaml.attribute_read_only + attribute: srdiag/tune/tune_h + unit: mm diff --git a/tests/test_tune_monitor.py b/tests/test_tune_monitor.py new file mode 100644 index 00000000..8029f20d --- /dev/null +++ b/tests/test_tune_monitor.py @@ -0,0 +1,37 @@ + +from pyaml.pyaml import PyAML, pyaml +from pyaml.instrument import Instrument +from pyaml.configuration.factory import Factory +from pyaml.lattice.abstract_impl import RBetatronTuneArray +import numpy as np +import pytest + +@pytest.mark.parametrize("install_test_package", [{ + "name": "tango-pyaml", + "path": "tests/dummy_cs/tango-pyaml" +}], indirect=True) +def test_simulator_tune_monitor(install_test_package): + + ml:PyAML = pyaml("tests/config/tune_monitor.yaml") + sr:Instrument = ml.get('sr') + sr.design.get_lattice().disable_6d() + tune_monitor = sr.design.get_betatron_tune_monitor("BETATRON_TUNE") + assert tune_monitor.tune.get()[0] == sr.design.get_lattice().get_tune()[0] + assert tune_monitor.tune.get()[1] == sr.design.get_lattice().get_tune()[1] + + Factory.clear() + +@pytest.mark.parametrize("install_test_package", [{ + "name": "tango-pyaml", + "path": "tests/dummy_cs/tango-pyaml" +}], indirect=True) +def test_constrolsystem_tune_monitor(install_test_package): + + ml:PyAML = pyaml("tests/config/tune_monitor.yaml") + sr:Instrument = ml.get('sr') + tune_monitor = sr.live.get_betatron_tune_monitor("BETATRON_TUNE") + assert tune_monitor.tune.get()[0] == 0.0 + assert tune_monitor.tune.get()[1] == 0.0 + + Factory.clear() + From 4259572124f38dd22ee5693778a117b93de79c06 Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Tue, 4 Nov 2025 16:24:39 +0100 Subject: [PATCH 2/3] Modified tune-related-tests. Set tune to return a numpy-array (to do algebraic operations on arrays). --- pyaml/control/abstract_impl.py | 6 +++--- tests/config/EBSTune.yaml | 11 +++++++++++ tests/test_tune.py | 9 +++++---- tests/test_tune_hardware.py | 8 +++++--- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/pyaml/control/abstract_impl.py b/pyaml/control/abstract_impl.py index 73aa3c76..767e85eb 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -360,10 +360,10 @@ class RBetatronTuneArray(abstract.ReadFloatScalar): def __init__(self, tune_monitor): self.__tune_monitor = tune_monitor - def get(self) -> list[float]: + def get(self) -> NDArray: # Serialized cavity has the same frequency - return [self.__tune_monitor._cfg.tune_h.get(), - self.__tune_monitor._cfg.tune_v.get()] + return np.array([self.__tune_monitor._cfg.tune_h.get(), + self.__tune_monitor._cfg.tune_v.get()]) def unit(self) -> str: return self.__tune_monitor._cfg.tune_v.unit() diff --git a/tests/config/EBSTune.yaml b/tests/config/EBSTune.yaml index e74c9093..2c6153be 100644 --- a/tests/config/EBSTune.yaml +++ b/tests/config/EBSTune.yaml @@ -1877,3 +1877,14 @@ instruments: type: tango.pyaml.attribute attribute: srmag/vps-qd2/c03-a/current unit: A + - type: pyaml.diagnostics.tune_monitor + name: BETATRON_TUNE + tune_h: + type: tango.pyaml.attribute_read_only + attribute: srdiag/tune/tune_v + unit: mm + tune_v: + type: tango.pyaml.attribute_read_only + attribute: srdiag/tune/tune_h + unit: mm + diff --git a/tests/test_tune.py b/tests/test_tune.py index 57bb7bf5..17620125 100644 --- a/tests/test_tune.py +++ b/tests/test_tune.py @@ -17,16 +17,16 @@ def test_tune(install_test_package): quadForTuneDesign = sr.design.get_magnets("QForTune") quadForTuneLive = sr.live.get_magnets("QForTune") - + tune_monitor = sr.design.get_betatron_tune_monitor("BETATRON_TUNE") # Build tune response matrix - tune = sr.design.get_lattice().get_tune() + tune = tune_monitor.tune.get() print(tune) tunemat = np.zeros((len(quadForTuneDesign),2)) for idx,m in enumerate(quadForTuneDesign): str = m.strength.get() m.strength.set(str+1e-4) - dq = sr.design.get_lattice().get_tune() - tune + dq = tune_monitor.tune.get() - tune tunemat[idx] = dq*1e4 m.strength.set(str) @@ -37,8 +37,9 @@ def test_tune(install_test_package): strs = quadForTuneDesign.strengths.get() strs += np.matmul(correctionmat,[0.1,0.05]) # Ask for correction [dqx,dqy] quadForTuneDesign.strengths.set(strs) - newTune = sr.design.get_lattice().get_tune() + newTune = tune_monitor.tune.get() diffTune = newTune-tune + print(diffTune) assert( np.abs(diffTune[0]-0.1) < 1e-3 ) assert( np.abs(diffTune[1]-0.05) < 1e-3 ) diff --git a/tests/test_tune_hardware.py b/tests/test_tune_hardware.py index 9296b9db..da0239c2 100644 --- a/tests/test_tune_hardware.py +++ b/tests/test_tune_hardware.py @@ -18,7 +18,9 @@ def test_tune(install_test_package): quadForTuneDesign = sr.design.get_magnets("QForTune") # Build tune response matrix (hardware units) - tune = sr.design.get_lattice().get_tune() + + tune_monitor = sr.design.get_betatron_tune_monitor("BETATRON_TUNE") + tune = tune_monitor.tune.get() print(tune) tunemat = np.zeros((len(quadForTuneDesign),2)) @@ -26,7 +28,7 @@ def test_tune(install_test_package): for m in quadForTuneDesign: current = m.hardware.get() m.hardware.set(current+1e-6) - dq = sr.design.get_lattice().get_tune() - tune + dq = tune_monitor.tune.get() - tune tunemat[idx] = dq*1e6 m.hardware.set(current) idx += 1 @@ -38,7 +40,7 @@ def test_tune(install_test_package): currents = quadForTuneDesign.hardwares.get() currents += np.matmul(correctionmat,[0.1,0.05]) # Ask for correction [dqx,dqy] quadForTuneDesign.hardwares.set(currents) - newTune = sr.design.get_lattice().get_tune() + newTune = tune_monitor.tune.get() units = quadForTuneDesign.hardwares.unit() diffTune = newTune - tune assert( np.abs(diffTune[0]-0.1) < 1e-3 ) From b19298f6bfa1dc5d411f85389cb260db153f7a89 Mon Sep 17 00:00:00 2001 From: gubaidulinvadim Date: Tue, 4 Nov 2025 21:25:28 +0100 Subject: [PATCH 3/3] Removed unused imports. Improved docstrings. Switched imports to relative. --- pyaml/control/abstract_impl.py | 3 +-- pyaml/diagnostics/tune_monitor.py | 18 ++++++++---------- pyaml/lattice/abstract_impl.py | 2 +- tests/config/EBSTune.yaml | 4 ++-- tests/config/tune_monitor.yaml | 4 ++-- tests/test_tune.py | 1 - tests/test_tune_hardware.py | 1 - tests/test_tune_monitor.py | 4 +--- 8 files changed, 15 insertions(+), 22 deletions(-) diff --git a/pyaml/control/abstract_impl.py b/pyaml/control/abstract_impl.py index 767e85eb..8902965d 100644 --- a/pyaml/control/abstract_impl.py +++ b/pyaml/control/abstract_impl.py @@ -8,7 +8,6 @@ from ..rf.rf_transmitter import RFTransmitter from ..common.abstract_aggregator import ScalarAggregator from numpy import double -from ..diagnostics.tune_monitor import BetatronTuneMonitor import numpy as np from numpy.typing import NDArray @@ -361,7 +360,7 @@ def __init__(self, tune_monitor): self.__tune_monitor = tune_monitor def get(self) -> NDArray: - # Serialized cavity has the same frequency + # Return horizontal and vertical betatron tunes as a NumPy array return np.array([self.__tune_monitor._cfg.tune_h.get(), self.__tune_monitor._cfg.tune_v.get()]) diff --git a/pyaml/diagnostics/tune_monitor.py b/pyaml/diagnostics/tune_monitor.py index ffa2ed4e..5ff79969 100644 --- a/pyaml/diagnostics/tune_monitor.py +++ b/pyaml/diagnostics/tune_monitor.py @@ -1,10 +1,9 @@ -from pyaml.lattice.element import Element, ElementConfigModel -from pyaml.lattice.abstract_impl import RBetatronTuneArray +from ..lattice.element import Element, ElementConfigModel +from ..lattice.abstract_impl import RBetatronTuneArray from ..control.deviceaccess import DeviceAccess from typing import Self from pydantic import ConfigDict -from ..control.deviceaccess import DeviceAccess PYAMLCLASS = "BetatronTuneMonitor" @@ -19,19 +18,18 @@ class ConfigModel(ElementConfigModel): class BetatronTuneMonitor(Element): """ - Class providing access to one BPM of a physical or simulated lattice + Class providing access to a betatron tune monitor of a physical or simulated lattice. + The monitor provides horizontal and vertical betatron tune measurements. """ def __init__(self, cfg: ConfigModel): """ - Construct a BPM - + Construct a BetatronTuneMonitor. Parameters ---------- - name : str - Element name - model : BetatronTuneMonitorModel - BetatronTuneMonitorModel + cfg : ConfigModel + Configuration for the BetatronTuneMonitor, including + device access for horizontal and vertical tunes. """ super().__init__(cfg.name) diff --git a/pyaml/lattice/abstract_impl.py b/pyaml/lattice/abstract_impl.py index 5b649614..fc47f9a3 100644 --- a/pyaml/lattice/abstract_impl.py +++ b/pyaml/lattice/abstract_impl.py @@ -382,7 +382,7 @@ def unit(self) -> str: class RBetatronTuneArray(abstract.ReadFloatScalar): """ - Class providing read write access to RF frequency of a simulator. + Class providing read-only access to the betatron tune of a ring. """ def __init__(self, ring: at.Lattice): diff --git a/tests/config/EBSTune.yaml b/tests/config/EBSTune.yaml index 2c6153be..ccb1334b 100644 --- a/tests/config/EBSTune.yaml +++ b/tests/config/EBSTune.yaml @@ -1881,10 +1881,10 @@ instruments: name: BETATRON_TUNE tune_h: type: tango.pyaml.attribute_read_only - attribute: srdiag/tune/tune_v + attribute: srdiag/tune/tune_h unit: mm tune_v: type: tango.pyaml.attribute_read_only - attribute: srdiag/tune/tune_h + attribute: srdiag/tune/tune_v unit: mm diff --git a/tests/config/tune_monitor.yaml b/tests/config/tune_monitor.yaml index 8c01ef7b..21135002 100644 --- a/tests/config/tune_monitor.yaml +++ b/tests/config/tune_monitor.yaml @@ -17,9 +17,9 @@ instruments: name: BETATRON_TUNE tune_h: type: tango.pyaml.attribute_read_only - attribute: srdiag/tune/tune_v + attribute: srdiag/tune/tune_h unit: mm tune_v: type: tango.pyaml.attribute_read_only - attribute: srdiag/tune/tune_h + attribute: srdiag/tune/tune_v unit: mm diff --git a/tests/test_tune.py b/tests/test_tune.py index 17620125..69586c6f 100644 --- a/tests/test_tune.py +++ b/tests/test_tune.py @@ -2,7 +2,6 @@ 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", [{ diff --git a/tests/test_tune_hardware.py b/tests/test_tune_hardware.py index da0239c2..fc4a7b09 100644 --- a/tests/test_tune_hardware.py +++ b/tests/test_tune_hardware.py @@ -2,7 +2,6 @@ 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", [{ diff --git a/tests/test_tune_monitor.py b/tests/test_tune_monitor.py index 8029f20d..622d1193 100644 --- a/tests/test_tune_monitor.py +++ b/tests/test_tune_monitor.py @@ -2,8 +2,6 @@ from pyaml.pyaml import PyAML, pyaml from pyaml.instrument import Instrument from pyaml.configuration.factory import Factory -from pyaml.lattice.abstract_impl import RBetatronTuneArray -import numpy as np import pytest @pytest.mark.parametrize("install_test_package", [{ @@ -25,7 +23,7 @@ def test_simulator_tune_monitor(install_test_package): "name": "tango-pyaml", "path": "tests/dummy_cs/tango-pyaml" }], indirect=True) -def test_constrolsystem_tune_monitor(install_test_package): +def test_controlsystem_tune_monitor(install_test_package): ml:PyAML = pyaml("tests/config/tune_monitor.yaml") sr:Instrument = ml.get('sr')