diff --git a/pyaml/common/element_holder.py b/pyaml/common/element_holder.py index 22df8bc1..59c04b1b 100644 --- a/pyaml/common/element_holder.py +++ b/pyaml/common/element_holder.py @@ -26,6 +26,7 @@ if TYPE_CHECKING: from ..accelerator import Accelerator + from ..tuning_tools.bba import BBA from ..tuning_tools.chromaticity import Chromaticity from ..tuning_tools.chromaticity_response_matrix import ChromaticityResponseMatrix from ..tuning_tools.dispersion import Dispersion @@ -343,6 +344,11 @@ def get_orm_tuning(self, name: str) -> "OrbitResponseMatrix": def orm(self) -> "OrbitResponseMatrix": return self.get_orm_tuning("DEFAULT_ORBIT_RESPONSE_MATRIX") + # ---- BBA -------------------------------------------------------- + + def get_bba(self, name: str) -> "BBA": + return self.__get("BBA tool", name, self.__TUNING_TOOLS) + # ---- Dispersive orbit -------------------------------------------- def get_dispersion_tuning(self, name: str) -> "Dispersion": diff --git a/pyaml/external/pySC_interface.py b/pyaml/external/pySC_interface.py index c553fd1e..d6b6fd99 100644 --- a/pyaml/external/pySC_interface.py +++ b/pyaml/external/pySC_interface.py @@ -10,6 +10,7 @@ class pySCInterface: set_wait_time: float = 0 + read_wait_time: float = 0 def __init__( self, @@ -28,7 +29,7 @@ def __init__( self.rf_plant = None def get_orbit(self) -> Tuple[np.array, np.array]: - # we should wait here somehow according to polling rate + time.sleep(self.read_wait_time) positions = self.bpm_array.positions.get() return positions[:, 0], positions[:, 1] diff --git a/pyaml/tuning_tools/bba.py b/pyaml/tuning_tools/bba.py new file mode 100644 index 00000000..de9b1700 --- /dev/null +++ b/pyaml/tuning_tools/bba.py @@ -0,0 +1,188 @@ +import logging +from typing import Callable, Optional + +import numpy as np +import pySC +from pydantic import ConfigDict +from pySC.apps import measure_bba +from pySC.apps.bba import BBAAnalysis +from pySC.apps.codes import BBACode + +from ..common.constants import Action +from ..external.pySC_interface import pySCInterface +from .measurement_tool import MeasurementTool, MeasurementToolConfigModel + +logger = logging.getLogger(__name__) + +PYAMLCLASS = "BBA" + + +class ConfigModel(MeasurementToolConfigModel): + """ + Configuration model for Beam Based Alignment. + BBA finds the magnetic center of a quad (zero crossing). + + Parameters + ---------- + bpm_array_name : str + BPM array name (orbit) + bpm_name : str + BPM to be corrected (close to the quad) + hcorr_name : str + Horizontal corrector used to make a deviation in the quad + vcorr_name : str + Vertical corrector used to make a deviation in the quad + quad_name : str + Quadrupole used to find the center + hcorr_delta : float + Horizontal corrector delta strength + vcorr_delta : float + Vertical corrector delta strength + hquad_delta : float + Quadrupole delta strength (for h search) + vquad_delta : float + Quadrupole delta strength (for v search) + + """ + + model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid") + + bpm_array_name: str + bpm_name: str + hcorr_name: str + vcorr_name: str + quad_name: str + hcorr_delta: float + vcorr_delta: float + hquad_delta: float + vquad_delta: float + + +class BBA(MeasurementTool): + def __init__(self, cfg: ConfigModel): + super().__init__(cfg.name) + self._cfg = cfg + + def measure( + self, + sleep_between_step: Optional[float] = None, + n_avg_meas: Optional[int] = None, + sleep_between_meas: Optional[float] = None, + callback: Optional[Callable] = None, + ): + """ + Measure BBA. + + **Example** + + .. code-block:: python + + sr = Accelerator.load("MyAccelerator.yaml") + TODO + + Parameters + ---------- + sleep_between_step: float + Default time sleep after steerer or quad exitation + Default: from config + n_avg_meas : int, optional + Default number of orbit measurement per step used for averaging + Default from config + sleep_between_meas: float + Default time sleep between two orbit measurment + Default: from config + callback : Callable, optional + example: callback(action:int, callback_data: 'Complicated struct') + callback is executed after each strength setting and after each orbit + reading. + If the callback returns false, then the process is aborted. + """ + nb_meas = n_avg_meas if n_avg_meas is not None else self._cfg.n_avg_meas + sleep_step = sleep_between_step if sleep_between_step is not None else self._cfg.sleep_between_step + sleep_meas = sleep_between_meas if sleep_between_meas is not None else self._cfg.sleep_between_meas + + element_holder = self._peer + interface = pySCInterface( + element_holder=element_holder, + bpm_array_name=self._cfg.bpm_array_name, + ) + interface.set_wait_time = sleep_step + interface.read_wait_time = sleep_meas + + bpms_names = element_holder.get_bpms(self._cfg.bpm_array_name).names() + + bba_pySC_config = { + "number": bpms_names.index(self._cfg.bpm_name), + "QUAD": self._cfg.quad_name, + "HCORR": self._cfg.hcorr_name, + "VCORR": self._cfg.vcorr_name, + "HCORR_delta": self._cfg.hcorr_delta, + "QUAD_dk_H": self._cfg.hquad_delta, + "VCORR_delta": self._cfg.vcorr_delta, + "QUAD_dk_V": self._cfg.vquad_delta, + "QUAD_is_skew": False, + } + + generator = measure_bba( + interface=interface, + bpm_name=self._cfg.bpm_name, + config=bba_pySC_config, + shots_per_orbit=nb_meas, + n_corr_steps=self._cfg.n_step, + bipolar=False, + skip_save=True, + ) + + pySC.disable_pySC_rich() + aborted = False + err = None + idx = 0 + self._latest_measurement = {"HOffset": np.nan, "VOffset": np.nan, "HOffsetError": np.nan, "VOffsetError": np.nan} + + try: + self._register_callback(callback) + self._init_measure() + for code, measurement_object in generator: + # print(f"Got code: {code.name}") + if code == BBACode.HORIZONTAL_DONE: + result = BBAAnalysis.analyze(measurement_object.H_data) + self.latest_measurement["HOffset"] = result.offset + self.latest_measurement["HOffsetError"] = result.offset_error + if code == BBACode.VERTICAL_DONE: + result = BBAAnalysis.analyze(measurement_object.V_data) + self.latest_measurement["VOffset"] = result.offset + self.latest_measurement["VOffsetError"] = result.offset_error + idx += 1 + except Exception as ex: + err = ex + except KeyboardInterrupt as ex: + aborted = True + finally: + # Restore steerer/quad strength + # TODO + self.send_callback( + Action.RESTORE, + {"idx": idx}, + raiseException=False, + ) + + if err is not None: + raise (err) + + if aborted: + logger.warning(f"{self.get_name()} : measurement aborted (settings not restored)") + return False + + return True + + def h_offset(self) -> float: + return self.latest_measurement["HOffset"] + + def h_offset_error(self) -> float: + return self.latest_measurement["HOffsetError"] + + def v_offset(self) -> float: + return self.latest_measurement["VOffset"] + + def v_offset_error(self) -> float: + return self.latest_measurement["VOffsetError"] diff --git a/pyproject.toml b/pyproject.toml index a9ebee41..3c0b602c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ dependencies = [ "accelerator-toolbox>=0.6.1", "PyYAML>=6.0.2", "pydantic>=2.11.7", - "accelerator-commissioning==1.1.0", #pySC + "accelerator-commissioning==1.3.0", #pySC "h5py", "matplotlib" ] diff --git a/tests/config/EBSOrbit.yaml b/tests/config/EBSOrbit.yaml index 9b66318a..522b8135 100644 --- a/tests/config/EBSOrbit.yaml +++ b/tests/config/EBSOrbit.yaml @@ -36,6 +36,18 @@ arrays: elements: - BPM* devices: +- type: pyaml.tuning_tools.bba + name: BBA-BPM_C04-03 + bpm_array_name: BPM + bpm_name : BPM_C04-03 + hcorr_name : SF2E-C02-H + vcorr_name : SD1A-C26-V + quad_name : QF6B-C04 + n_step: 7 + hcorr_delta : 3.829380038425445e-05 + vcorr_delta : 4.198292876029707e-05 + hquad_delta : 0.005762885683993067 + vquad_delta : 0.009329539724510462 - type: pyaml.diagnostics.tune_monitor name: BETATRON_TUNE tune_h: srdiag/beam-tune/main/Qh @@ -85,6 +97,18 @@ devices: harmonic: 1 distribution: 1 voltage: sys/ringsimulator/ebs/RfVoltage +- type: pyaml.magnet.quadrupole + name: QF6B-C04 + model: + type: pyaml.magnet.linear_model + calibration_factor: 0.998960997 + crosstalk: 0.99918 + curve: + type: pyaml.magnet.csvcurve + file: sr/magnet_models/QF6_strength.csv + unit: 1/m + hardware_unit: A + powerconverter: srmag/vps-qf6/c04-b - type: pyaml.magnet.cfm_magnet name: SH1A-C04 mapping: diff --git a/tests/config/catalogs/ebs_catalogs.yaml b/tests/config/catalogs/ebs_catalogs.yaml index 4b92535c..d15349d6 100644 --- a/tests/config/catalogs/ebs_catalogs.yaml +++ b/tests/config/catalogs/ebs_catalogs.yaml @@ -3,6 +3,6 @@ entries: - catalogs/ebs_bpm_catalogs.yaml - catalogs/ebs_rf_catalogs.yaml - catalogs/ebs_diag_catalogs.yaml - - catalogs/ebs_qf1_qd2_catalogs.yaml + - catalogs/ebs_magnets_catalogs.yaml - catalogs/ebs_sh_catalogs.yaml - catalogs/ebs_sext_catalogs.yaml diff --git a/tests/config/catalogs/ebs_qf1_qd2_catalogs.yaml b/tests/config/catalogs/ebs_magnets_catalogs.yaml similarity index 99% rename from tests/config/catalogs/ebs_qf1_qd2_catalogs.yaml rename to tests/config/catalogs/ebs_magnets_catalogs.yaml index f044e645..73471daa 100644 --- a/tests/config/catalogs/ebs_qf1_qd2_catalogs.yaml +++ b/tests/config/catalogs/ebs_magnets_catalogs.yaml @@ -866,3 +866,10 @@ attribute: srmag/vps-qd2/c03-a/current range: [10,109] unit: A +- type: tango.pyaml.static_catalog_entry + key: srmag/vps-qf6/c04-b + device: + type: tango.pyaml.attribute + attribute: srmag/vps-qf6/c04-b + range: [10,109] + unit: A diff --git a/tests/config/sr/magnet_models/QF6_strength.csv b/tests/config/sr/magnet_models/QF6_strength.csv new file mode 100755 index 00000000..6331415e --- /dev/null +++ b/tests/config/sr/magnet_models/QF6_strength.csv @@ -0,0 +1,101 @@ + 0, 0 + 10, 4.8381057 +11.01010101, 5.309790224 +12.02020202, 5.781444181 +13.03030303, 6.253037002 +14.04040404, 6.72453812 +15.05050505, 7.195916967 +16.06060606, 7.667150484 +17.07070707, 8.138241462 +18.08080808, 8.609198255 +19.09090909, 9.080029216 + 20.1010101, 9.5507427 +21.11111111, 10.02134783 +22.12121212, 10.49185598 +23.13131313, 10.96227898 +24.14141414, 11.43262864 +25.15151515, 11.90291672 +26.16161616, 12.37313684 +27.17171717, 12.84323594 +28.18181818, 13.31315356 +29.19191919, 13.78282926 + 30.2020202, 14.25220272 +31.21212121, 14.7212447 +32.22222222, 15.18999532 +33.23232323, 15.65850402 +34.24242424, 16.12682031 +35.25252525, 16.59499334 +36.26262626, 17.06303632 +37.27272727, 17.53089254 +38.28282828, 17.99849728 +39.29292929, 18.46578582 + 40.3030303, 18.93269377 +41.31313131, 19.39918272 +42.32323232, 19.86525832 +43.33333333, 20.33093051 +44.34343434, 20.79620918 +45.35353535, 21.26110358 +46.36363636, 21.72558595 +47.37373737, 22.18957373 +48.38383838, 22.65297993 +49.39393939, 23.11571754 + 50.4040404, 23.57770039 +51.41414141, 24.03887417 +52.42424242, 24.49922597 +53.43434343, 24.95874566 +54.44444444, 25.4174231 +55.45454545, 25.87522826 +56.46464646, 26.33154575 +57.47474747, 26.7850931 +58.48484848, 27.2345516 +59.49494949, 27.6786025 +60.50505051, 28.11592527 +61.51515152, 28.54515768 +62.52525253, 28.96489584 +63.53535354, 29.37373405 +64.54545455, 29.7702666 +65.55555556, 30.15310646 +66.56565657, 30.52120955 +67.57575758, 30.87383285 +68.58585859, 31.21024353 + 69.5959596, 31.52970877 +70.60606061, 31.83155581 +71.61616162, 32.11601021 +72.62626263, 32.38398911 +73.63636364, 32.63642744 +74.64646465, 32.87426012 +75.65656566, 33.09843152 +76.66666667, 33.31000256 +77.67676768, 33.51011278 +78.68686869, 33.69990319 + 79.6969697, 33.8805148 +80.70707071, 34.05306116 +81.71717172, 34.21837218 +82.72727273, 34.37711048 +83.73737374, 34.52993649 +84.74747475, 34.67751066 +85.75757576, 34.82045497 +86.76767677, 34.95905659 +87.77777778, 35.09343031 +88.78787879, 35.22368951 + 89.7979798, 35.34994755 +90.80808081, 35.47232532 +91.81818182, 35.59099919 +92.82828283, 35.70617041 +93.83838384, 35.81804036 +94.84848485, 35.92681041 +95.85858586, 36.03267776 +96.86868687, 36.13581324 +97.87878788, 36.23637744 +98.88888889, 36.33453092 + 99.8989899, 36.43043424 +100.9090909, 36.52421516 +101.9191919, 36.6158239 +102.9292929, 36.70515097 +103.9393939, 36.7920868 +104.9494949, 36.87652186 + 105.959596, 36.95843775 + 106.969697, 37.03823987 + 107.979798, 37.11645666 + 108.989899, 37.19361655 + 110, 37.270248 diff --git a/tests/test_yellow_pages_basic.py b/tests/test_yellow_pages_basic.py index b0d944b3..06e22c0c 100644 --- a/tests/test_yellow_pages_basic.py +++ b/tests/test_yellow_pages_basic.py @@ -20,6 +20,7 @@ . Tools: + BBA-BPM_C04-03 (pyaml.tuning_tools.bba) CHROMATICITY_MONITOR (pyaml.tuning_tools.chromaticity_monitor) DEFAULT_CHROMATICITY_RESPONSE_MATRIX (pyaml.tuning_tools.chromaticity_response_matrix) DEFAULT_CHROMATICITY_CORRECTION (pyaml.tuning_tools.chromaticity) diff --git a/tests/tuning_tools/test_bba.py b/tests/tuning_tools/test_bba.py new file mode 100644 index 00000000..7e508038 --- /dev/null +++ b/tests/tuning_tools/test_bba.py @@ -0,0 +1,22 @@ +import numpy as np +import pytest + +from pyaml.accelerator import Accelerator + + +def callback(action: int, data: dict): + print(f"action:{action}, data:{data}") + return True + + +def test_bba(): + sr = Accelerator.load("tests/config/EBSOrbit.yaml") + SR = sr.design + bba = SR.get_bba("BBA-BPM_C04-03") + + # Add a misalignement + SR.get_bpm("BPM_C04-03").offset.set([20e-6, -15e-6]) + # Measure offsets + bba.measure() + assert np.abs(bba.h_offset() - 20e-6) < 2e-6 + assert np.abs(bba.v_offset() + 15e-6) < 2e-6