From c69784a09d68c9e37191717448594fd47ffdbf3b Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 9 Dec 2025 13:29:00 +0100 Subject: [PATCH 01/70] bugfix for zero-length girders --- pySC/core/supports.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pySC/core/supports.py b/pySC/core/supports.py index a83e803..dae5c84 100644 --- a/pySC/core/supports.py +++ b/pySC/core/supports.py @@ -248,6 +248,9 @@ def get_support_offset(self, s, support_level_key): dx1, dy1 = self.get_total_offset(supp_index, supp_level, endpoint='start') dx2, dy2 = self.get_total_offset(supp_index, supp_level, endpoint='end') + if support.length == 0.: #ZERO_LENGTH_THRESHOLD here?? + return np.array([dx1, dy1]) + dx = (dx2 - dx1)/(s2 - s1 + corr_s2) * (s - s1 + corr_s) + dx1 dy = (dy2 - dy1)/(s2 - s1 + corr_s2) * (s - s1 + corr_s) + dy1 return np.array([dx, dy]) From a4a088f56b2479c2cd5edb2142aaecb900d9a4cf Mon Sep 17 00:00:00 2001 From: kparasch Date: Fri, 12 Dec 2025 11:07:17 +0100 Subject: [PATCH 02/70] bugfix of tau in atlattice --- pySC/core/lattice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pySC/core/lattice.py b/pySC/core/lattice.py index 3db9ae8..473b97f 100644 --- a/pySC/core/lattice.py +++ b/pySC/core/lattice.py @@ -78,7 +78,7 @@ def omp_num_threads(self, value: int): def track(self, bunch: nparray, indices: Optional[list[int]] = None, n_turns: int = 1, use_design: bool = False, coordinates: Optional[list] = None) -> nparray: new_bunch = bunch.copy() - new_bunch[:,4], new_bunch[:,5] = new_bunch[:,5], new_bunch[:,4] # swap zeta and delta for AT + new_bunch[:,4], new_bunch[:,5] = new_bunch[:,5].copy(), new_bunch[:,4].copy() # swap zeta and delta for AT if use_design: ring = self._design else: From 094076ff63244c736a8089d29c7f65e39b7299a7 Mon Sep 17 00:00:00 2001 From: kparasch Date: Fri, 12 Dec 2025 12:10:44 +0100 Subject: [PATCH 03/70] add configuration of injection in yaml --- pySC/configuration/generation.py | 4 ++ pySC/configuration/injection_conf.py | 57 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 pySC/configuration/injection_conf.py diff --git a/pySC/configuration/generation.py b/pySC/configuration/generation.py index a1aadf5..91a6034 100644 --- a/pySC/configuration/generation.py +++ b/pySC/configuration/generation.py @@ -9,6 +9,7 @@ from .rf_conf import configure_rf from .supports_conf import configure_supports from .tuning_conf import configure_tuning +from .injection_conf import configure_injection from .general import scale_error_table logger = logging.getLogger(__name__) @@ -70,4 +71,7 @@ def generate_SC(yaml_filepath: str, seed: int = 1, scale_errors: Optional[int] = logger.info('Configuring tuning...') configure_tuning(SC) + + logger.info('Configuring injection...') + configure_injection(SC) return SC \ No newline at end of file diff --git a/pySC/configuration/injection_conf.py b/pySC/configuration/injection_conf.py new file mode 100644 index 0000000..d88fb24 --- /dev/null +++ b/pySC/configuration/injection_conf.py @@ -0,0 +1,57 @@ +import logging + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ..core.new_simulated_commissioning import SimulatedCommissioning + +logger = logging.getLogger(__name__) + +def configure_injection(SC: "SimulatedCommissioning") -> None: + injection_conf = dict.get(SC.configuration, 'injection', {}) + + if 'from_design' in injection_conf: + from_design = injection_conf['from_design'] + else: + from_design = True + + if from_design: + logger.info('Setting injection parameters from design twiss at the beginning of the lattice.') + twiss = SC.lattice.twiss + SC.injection.x = twiss['x'][0] + SC.injection.px = twiss['px'][0] + SC.injection.y = twiss['y'][0] + SC.injection.py = twiss['py'][0] + SC.injection.tau = twiss['tau'][0] + SC.injection.delta = twiss['delta'][0] + + SC.injection.betx = twiss['betx'][0] + SC.injection.alfx = twiss['alfx'][0] + SC.injection.bety = twiss['bety'][0] + SC.injection.alfy = twiss['alfy'][0] + + if 'emit_x' in injection_conf: + SC.injection.gemit_x = injection_conf['emit_x'] + else: + logger.warning('emit_x not specified in injection configuration. Using default value of 1.') + if 'emit_y' in injection_conf: + SC.injection.gemit_y = injection_conf['emit_y'] + else: + logger.warning('emit_y not specified in injection configuration. Using default value of 1.') + if 'bunch_length' in injection_conf: + SC.injection.bunch_length = injection_conf['bunch_length'] + else: + logger.warning('bunch_length (in m, r.m.s.) not specified in injection configuration. Using default value of 1.') + if 'energy_spread' in injection_conf: + SC.injection.energy_spread = injection_conf['energy_spread'] + else: + logger.warning('energy_spread not specified in injection configuration. Using default value of 1.') + + for var in ['x', 'px', 'y', 'py', 'tau', 'delta', 'betx', 'alfx', 'bety', 'alfy']: + if var in injection_conf: + setattr(SC.injection, var, injection_conf[var]) + + for var in ['x_error_syst', 'px_error_syst', 'y_error_syst', 'py_error_syst', 'tau_error_syst', 'delta_error_syst', + 'x_error_stat', 'px_error_stat', 'y_error_stat', 'py_error_stat', 'tau_error_stat', 'delta_error_stat']: + if var in injection_conf: + setattr(SC.injection, var, injection_conf[var]) \ No newline at end of file From 11982231468060be789197d9ff505bcc4a7d5ead Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 15 Dec 2025 10:42:50 +0100 Subject: [PATCH 04/70] make warning appear only when there is warning --- pySC/configuration/supports_conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pySC/configuration/supports_conf.py b/pySC/configuration/supports_conf.py index 9c73886..e2e3650 100644 --- a/pySC/configuration/supports_conf.py +++ b/pySC/configuration/supports_conf.py @@ -78,7 +78,8 @@ def configure_supports(SC: SimulatedCommissioning): if 'roll' in level_conf: sigma = get_error(level_conf['roll'], error_table) this_support.roll = SC.rng.normal_trunc(0, sigma) - logger.warning(f'Found {len(zero_length_supports)} zero-length supports in level {level} ({category_name}).') + if len(zero_length_supports): + logger.warning(f'Found {len(zero_length_supports)} zero-length supports in level {level} ({category_name}).') SC.support_system.resolve_graph() SC.support_system.update_all() \ No newline at end of file From 58a1d76c1a0b4ed6a5ea8bbdedff55de401cd06c Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 15 Dec 2025 19:10:32 +0100 Subject: [PATCH 05/70] injection conf vars were not float --- pySC/configuration/injection_conf.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pySC/configuration/injection_conf.py b/pySC/configuration/injection_conf.py index d88fb24..4d15447 100644 --- a/pySC/configuration/injection_conf.py +++ b/pySC/configuration/injection_conf.py @@ -31,27 +31,27 @@ def configure_injection(SC: "SimulatedCommissioning") -> None: SC.injection.alfy = twiss['alfy'][0] if 'emit_x' in injection_conf: - SC.injection.gemit_x = injection_conf['emit_x'] + SC.injection.gemit_x = float(injection_conf['emit_x']) else: logger.warning('emit_x not specified in injection configuration. Using default value of 1.') if 'emit_y' in injection_conf: - SC.injection.gemit_y = injection_conf['emit_y'] + SC.injection.gemit_y = float(injection_conf['emit_y']) else: logger.warning('emit_y not specified in injection configuration. Using default value of 1.') if 'bunch_length' in injection_conf: - SC.injection.bunch_length = injection_conf['bunch_length'] + SC.injection.bunch_length = float(injection_conf['bunch_length']) else: logger.warning('bunch_length (in m, r.m.s.) not specified in injection configuration. Using default value of 1.') if 'energy_spread' in injection_conf: - SC.injection.energy_spread = injection_conf['energy_spread'] + SC.injection.energy_spread = float(injection_conf['energy_spread']) else: logger.warning('energy_spread not specified in injection configuration. Using default value of 1.') for var in ['x', 'px', 'y', 'py', 'tau', 'delta', 'betx', 'alfx', 'bety', 'alfy']: if var in injection_conf: - setattr(SC.injection, var, injection_conf[var]) + setattr(SC.injection, var, float(injection_conf[var])) for var in ['x_error_syst', 'px_error_syst', 'y_error_syst', 'py_error_syst', 'tau_error_syst', 'delta_error_syst', 'x_error_stat', 'px_error_stat', 'y_error_stat', 'py_error_stat', 'tau_error_stat', 'delta_error_stat']: if var in injection_conf: - setattr(SC.injection, var, injection_conf[var]) \ No newline at end of file + setattr(SC.injection, var, float(injection_conf[var])) \ No newline at end of file From a945d6ff187cf92053716df8a60bea1530399d0b Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 16 Dec 2025 11:16:36 +0100 Subject: [PATCH 06/70] avoid division by zero and raise warning --- pySC/tuning/trajectory_bba.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pySC/tuning/trajectory_bba.py b/pySC/tuning/trajectory_bba.py index 858deb4..aea7d06 100644 --- a/pySC/tuning/trajectory_bba.py +++ b/pySC/tuning/trajectory_bba.py @@ -69,7 +69,11 @@ def generate_config(cls, SC: "SimulatedCommissioning", max_dx_at_bpm = 1e-3, if response[imax] > max_H_response: max_H_response = float(response[imax]) the_HCORR_number = int(imax) - hcorr_delta = max_dx_at_bpm/max_H_response + if max_H_response <= 0: + logger.warning(f'WARNING: zero H response for BPM {SC.bpm_system.names[bpm_number]}!') + hcorr_delta = 0 + else: + hcorr_delta = max_dx_at_bpm/max_H_response if the_bba_magnet.split('/')[-1][0] == 'B': temp_RM = HRM[bpm_number:bpm_number+n_downstream_bpms, the_HCORR_number] @@ -93,7 +97,11 @@ def generate_config(cls, SC: "SimulatedCommissioning", max_dx_at_bpm = 1e-3, if response[imax] > max_V_response: max_V_response = float(response[imax]) the_VCORR_number = int(imax) - vcorr_delta = max_dx_at_bpm/max_V_response + if max_V_response <= 0: + logger.warning(f'WARNING: zero V response for BPM {SC.bpm_system.names[bpm_number]}!') + vcorr_delta = 0 + else: + vcorr_delta = max_dx_at_bpm/max_V_response if the_bba_magnet.split('/')[-1][0] == 'B': temp_RM = VRM[bpm_number:bpm_number+n_downstream_bpms, the_VCORR_number] From 266f411f91f659e74b4449076abcf002f44f7656 Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 16 Dec 2025 13:41:29 +0100 Subject: [PATCH 07/70] chromaticity correction (only cheating for now) --- pySC/core/lattice.py | 25 ++++ pySC/core/new_simulated_commissioning.py | 1 + pySC/tuning/chromaticity.py | 140 +++++++++++++++++++++++ pySC/tuning/tuning_core.py | 2 + 4 files changed, 168 insertions(+) create mode 100644 pySC/tuning/chromaticity.py diff --git a/pySC/core/lattice.py b/pySC/core/lattice.py index 473b97f..e106407 100644 --- a/pySC/core/lattice.py +++ b/pySC/core/lattice.py @@ -182,6 +182,31 @@ def get_tune(self, method='6d', use_design=False) -> tuple[float, float]: return qx, qy + def get_chromaticity(self, method='6d', use_design=False) -> tuple[float, float]: + assert method in ['4d', '6d'] + ring = self._design if use_design else self._ring + + if self.no_6d: + logger.warning("Lattice has 6d disabled, using 4d method instead.") + method = '4d' + + if method == '4d' and not self.no_6d: + ring.disable_6d() + + try: + chroms = ring.get_chrom() + dqx = chroms[0] + dqy = chroms[1] + except Exception as e: + logger.error(f"Error computing chromaticity, {type(e)}: {e}") + dqx = np.nan + dqy = np.nan + + if method == '4d' and not self.no_6d: + ring.enable_6d() + + return dqx, dqy + def find_with_regex(self, regex: str) -> list[int]: """ Find elements in the ring that match the given regular expression. diff --git a/pySC/core/new_simulated_commissioning.py b/pySC/core/new_simulated_commissioning.py index cfb2371..1a33b92 100644 --- a/pySC/core/new_simulated_commissioning.py +++ b/pySC/core/new_simulated_commissioning.py @@ -79,6 +79,7 @@ def propagate_parents(self) -> None: self.injection._parent = self self.tuning._parent = self self.tuning.tune._parent = self.tuning + self.tuning.chromaticity._parent = self.tuning self.tuning.rf._parent = self.tuning return diff --git a/pySC/tuning/chromaticity.py b/pySC/tuning/chromaticity.py new file mode 100644 index 0000000..af426be --- /dev/null +++ b/pySC/tuning/chromaticity.py @@ -0,0 +1,140 @@ +from typing import Optional, TYPE_CHECKING +from pydantic import BaseModel, PrivateAttr, ConfigDict +import numpy as np +import logging + +from ..core.numpy_type import NPARRAY + +if TYPE_CHECKING: + from .tuning_core import Tuning + +logger = logging.getLogger(__name__) + +class Chromaticity(BaseModel, extra="forbid"): + controls_1: list[str] = [] + controls_2: list[str] = [] + response_matrix: Optional[NPARRAY] = None + inverse_response_matrix: Optional[NPARRAY] = None + _parent: Optional['Tuning'] = PrivateAttr(default=None) + + model_config = ConfigDict(arbitrary_types_allowed=True) + + @property + def design_dqx(self): + return self._parent._parent.lattice.twiss['dqx'] + + @property + def design_dqy(self): + return self._parent._parent.lattice.twiss['dqy'] + + def chromaticity_response(self, controls: list[str], delta: float = 1e-5): + SC = self._parent._parent + dq1_i, dq2_i = SC.lattice.get_chromaticity(use_design=True) + + ref_data = SC.design_magnet_settings.get_many(controls) + data = {key: ref_data[key] + delta for key in ref_data.keys()} + SC.design_magnet_settings.set_many(data) + + dq1_f, dq2_f = SC.lattice.get_chromaticity(use_design=True) + + SC.design_magnet_settings.set_many(ref_data) + + delta_dq1 = (dq1_f - dq1_i) / delta + delta_dq2 = (dq2_f - dq2_i) / delta + return delta_dq1, delta_dq2 + + def build_response_matrix(self, delta: float = 1e-5) -> None: + if not len(self.controls_1) > 0: + raise Exception('chromaticity.controls_1 is empty. Please set.') + if not len(self.controls_2) > 0: + raise Exception('chromaticity.controls_2 is empty. Please set.') + + RM = np.zeros((2,2)) + RM[:, 0] = self.chromaticity_response(self.controls_1, delta=delta) + RM[:, 1] = self.chromaticity_response(self.controls_2, delta=delta) + iRM = np.linalg.inv(RM) + + self.response_matrix = RM + self.inverse_response_matrix = iRM + return + + def trim(self, delta_dqx: float = 0, delta_dqy: float = 0, use_design: bool = False) -> None: + SC = self._parent._parent + if self.inverse_response_matrix is None: + logger.info('Did not find inverse (chromaticity) response matrix. Building now.') + self.build_response_matrix() + + delta1, delta2 = np.dot(self.inverse_response_matrix, [delta_dqx, delta_dqy]) + ref_data1 = SC.magnet_settings.get_many(self.controls_1, use_design=use_design) + ref_data2 = SC.magnet_settings.get_many(self.controls_2, use_design=use_design) + data1 = {key: ref_data1[key] + delta1 for key in ref_data1.keys()} + data2 = {key: ref_data2[key] + delta2 for key in ref_data2.keys()} + + SC.magnet_settings.set_many(data1, use_design=use_design) + SC.magnet_settings.set_many(data2, use_design=use_design) + return + + def correct(self, target_dqx: Optional[float] = None, target_dqy: Optional[float] = None, + n_iter: int = 1, gain: float = 1, measurement_method: str = 'cheat'): + ''' + Correct the chromaticity to the target values. + Parameters + ---------- + target_dqx : float, optional + Target horizontal chromaticity. If None, use design chromaticity. + target_dqy : float, optional + Target vertical chromaticity. If None, use design chromaticity. + n_iter : int, optional + Number of correction iterations. Default is 1. + gain : float, optional + Gain for the correction. Default is 1. + measurement_method : str, optional + Method to measure the chromaticity. Options are 'cheat', 'cheat4d'. + Default is 'cheat'. + ''' + + if measurement_method not in ['cheat', 'cheat4d']: + raise NotImplementedError(f'{measurement_method=} not implemented yet.') + + if target_dqx is None: + target_dqx = self.design_dqx + + if target_dqy is None: + target_dqy = self.design_dqy + + for _ in range(n_iter): + if measurement_method == 'cheat': + dqx, dqy = self.cheat() + elif measurement_method == 'cheat4d': + dqx, dqy = self.cheat4d() + else: + raise Exception(f'Unknown measurement_method {measurement_method}') + if dqx is None or dqy is None or dqx != dqx or dqy != dqy: + logger.info("Chromaticity measurement failed, skipping correction.") + return + logger.info(f"Measured tune: dqx={dqx:.4f}, dqy={dqy:.4f}") + delta_dqx = dqx - target_dqx + delta_dqy = dqy - target_dqy + logger.info(f"Delta tune: delta_dqx={delta_dqx:.4f}, delta_dqy={delta_dqy:.4f}") + self.trim(delta_dqx=-gain*delta_dqx, delta_dqy=-gain*delta_dqy) + return + + def cheat4d(self) -> tuple[float, float]: + SC = self._parent._parent + dqx, dqy = SC.lattice.get_chromaticity(method='4d') + delta_x = dqx - self.design_dqx + delta_y = dqy - self.design_dqy + logger.info(f"Horizontal chromaticity: dQx = {dqx:.3f} (Δ = {delta_x:.3f})") + logger.info(f"Vertical chromaticity: dQy = {dqy:.3f} (Δ = {delta_y:.3f})") + + return dqx, dqy + + def cheat(self) -> tuple[float, float]: + SC = self._parent._parent + dqx, dqy = SC.lattice.get_chromaticity(method='6d') + delta_x = dqx - self.design_dqx + delta_y = dqy - self.design_dqy + logger.info(f"Horizontal tune: Qx = {dqx:.3f} (Δ = {delta_x:.3f})") + logger.info(f"Vertical tune: Qy = {dqy:.3f} (Δ = {delta_y:.3f})") + + return dqx, dqy \ No newline at end of file diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index 2837b73..83a9767 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -6,6 +6,7 @@ from .orbit_bba import Orbit_BBA_Configuration, orbit_bba from .parallel import parallel_tbba_target, parallel_obba_target, get_listener_and_queue from .tune import Tune +from .chromaticity import Chromaticity from .rf_tuning import RF_tuning import numpy as np @@ -26,6 +27,7 @@ class Tuning(BaseModel, extra="forbid"): multipoles: list[str] = [] tune: Tune = Tune() ## TODO: generate config from yaml file + chromaticity: Chromaticity = Chromaticity() ## TODO: generate config from yaml file rf: RF_tuning = RF_tuning() ## TODO: generate config from yaml file bba_magnets: list[str] = [] From 15b21d17bd4e53383b5997a1c1222686cbe50a45 Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 18 Dec 2025 14:21:14 +0100 Subject: [PATCH 08/70] bugfix in trajectory bba config --- pySC/tuning/trajectory_bba.py | 47 +++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/pySC/tuning/trajectory_bba.py b/pySC/tuning/trajectory_bba.py index aea7d06..d672054 100644 --- a/pySC/tuning/trajectory_bba.py +++ b/pySC/tuning/trajectory_bba.py @@ -32,7 +32,7 @@ def generate_config(cls, SC: "SimulatedCommissioning", max_dx_at_bpm = 1e-3, # max_modulation=600e-6, max_dx_at_bpm=1.5e-3 config = {} - n_turns = 2 + n_turns = 1 RM_name = f'trajectory{n_turns}' SC.tuning.fetch_response_matrix(RM_name, orbit=False, n_turns=n_turns) RM = SC.tuning.response_matrix[RM_name] @@ -40,9 +40,11 @@ def generate_config(cls, SC: "SimulatedCommissioning", max_dx_at_bpm = 1e-3, bba_magnets = SC.tuning.bba_magnets bba_magnets_s = get_mag_s_pos(SC, bba_magnets) - d1, d2 = RM.RM.shape - HRM = RM.RM[:d1//2, :d2//2] - VRM = RM.RM[d1//2:, d2//2:] + #d1, d2 = RM.RM.shape + nh = len(SC.tuning.HCORR) + nbpm = len(SC.bpm_system.indices) + HRM = RM.RM[:nbpm, :nh] + VRM = RM.RM[nbpm:, nh:] for bpm_number in range(len(SC.bpm_system.indices)): bpm_index = SC.bpm_system.indices[bpm_number] @@ -64,11 +66,10 @@ def generate_config(cls, SC: "SimulatedCommissioning", max_dx_at_bpm = 1e-3, max_H_response = -1 the_HCORR_number = -1 for nn in HCORR_numbers: - response = np.abs(HRM[bpm_number]) - imax = np.argmax(response) - if response[imax] > max_H_response: - max_H_response = float(response[imax]) - the_HCORR_number = int(imax) + response = np.abs(HRM[bpm_number, nn]) + if response > max_H_response: + max_H_response = float(response) + the_HCORR_number = int(nn) if max_H_response <= 0: logger.warning(f'WARNING: zero H response for BPM {SC.bpm_system.names[bpm_number]}!') hcorr_delta = 0 @@ -92,11 +93,10 @@ def generate_config(cls, SC: "SimulatedCommissioning", max_dx_at_bpm = 1e-3, max_V_response = -1 the_VCORR_number = -1 for nn in VCORR_numbers: - response = np.abs(VRM[bpm_number]) - imax = np.argmax(response) - if response[imax] > max_V_response: - max_V_response = float(response[imax]) - the_VCORR_number = int(imax) + response = np.abs(VRM[bpm_number, nn]) + if response > max_V_response: + max_V_response = float(response) + the_VCORR_number = int(nn) if max_V_response <= 0: logger.warning(f'WARNING: zero V response for BPM {SC.bpm_system.names[bpm_number]}!') vcorr_delta = 0 @@ -216,13 +216,18 @@ def get_orbit(): settings.set(corr, corr_sp0) settings.set(quad, quad_sp0) - slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, quad_delta) - mask_bpm_outlier = reject_bpm_outlier(orbits) - mask_slopes = reject_slopes(slopes) - mask_center = reject_center_outlier(center) - final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) - - offset, offset_err = get_offset(center, center_err, final_mask) + try: + slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, quad_delta) + mask_bpm_outlier = reject_bpm_outlier(orbits) + mask_slopes = reject_slopes(slopes) + mask_center = reject_center_outlier(center) + final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) + + offset, offset_err = get_offset(center, center_err, final_mask) + except Exception as exc: + print(exc) + logger.warning(f'Failed to compute trajectory BBA for BPM {bpm_name}') + offset, offset_err = np.nan, np.nan return offset, offset_err From 3a299f048c7e298b4f2b1b5edfec5971c5aa0225 Mon Sep 17 00:00:00 2001 From: kparasch Date: Fri, 19 Dec 2025 13:07:34 +0100 Subject: [PATCH 09/70] tuning method to randomly modify last horizontal and vertical corrector during first-turn steering until beam passes through the previous bottleneck. --- pySC/core/rng.py | 3 +++ pySC/tuning/tuning_core.py | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/pySC/core/rng.py b/pySC/core/rng.py index f8e2740..1bcdcb8 100644 --- a/pySC/core/rng.py +++ b/pySC/core/rng.py @@ -43,5 +43,8 @@ def normal_trunc(self, loc: float = 0, scale: float = 1, def normal(self, loc: float = 0, scale: float = 1, size: Optional[int] = None) -> Union[float, np.ndarray]: return self._rng.normal(loc=loc, scale=scale, size=size) + def uniform(self, low: float = 0, high: float = 1, size: Optional[int] = None) -> Union[float, np.ndarray]: + return low + self._rng.random(size=size) * (high - low) + def randomize_rng(self) -> None: self._rng = default_rng() diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index 83a9767..f282ae9 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -89,6 +89,42 @@ def bad_outputs_from_bad_bpms(self, bad_bpms: list[int], n_turns: int = 1) -> li bad_outputs.append(bpm + turn * n_bpms + plane * n_turns * n_bpms) return bad_outputs + def wiggle_last_corrector(self, max_steps: int = 100, max_sp: float = 500e-6) -> None: + SC = self._parent + def first_turn_transmission(SC): + x, _ = SC.bpm_system.capture_injection() + bad_readings = sum(np.isnan(x)) + good_frac = (len(x) - bad_readings) / len(SC.bpm_system.indices) + last_good_bpm = np.where(~np.isnan(x))[0][-1] + last_good_bpm_index = SC.bpm_system.indices[last_good_bpm] + return good_frac, last_good_bpm_index + + initial_transmission, last_good_bpm_index = first_turn_transmission(SC) + if initial_transmission < 1.: + for corr in SC.tuning.HCORR: + hcor_name = corr.split('/')[0] + hcor_index = SC.magnet_settings.magnets[hcor_name].sim_index + if hcor_index < last_good_bpm_index: + last_hcor = corr + for corr in SC.tuning.VCORR: + vcor_name = corr.split('/')[0] + vcor_index = SC.magnet_settings.magnets[vcor_name].sim_index + if vcor_index < last_good_bpm_index: + last_vcor = corr + + for _ in range(max_steps): + SC.magnet_settings.set(last_hcor, SC.rng.uniform(-max_sp, max_sp)) + SC.magnet_settings.set(last_vcor, SC.rng.uniform(-max_sp, max_sp)) + transmission, _ = first_turn_transmission(SC) + if transmission > initial_transmission: + logger.info(f"Wiggling improved first-turn transmission from {initial_transmission} to {transmission}.") + return + logger.info("Wiggling failed. Reached maximum number of steps.") + else: + logger.info("No need to wiggle, full transmission through first-turn.") + + return + def correct_injection(self, n_turns=1, n_reps=1, method='tikhonov', parameter=100, gain=1, correct_to_first_turn=False): RM_name = f'trajectory{n_turns}' self.fetch_response_matrix(RM_name, orbit=False, n_turns=n_turns) From bd3b5f3b1ab3faff798b7f871a6b0e6f88ea2eb1 Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 6 Jan 2026 13:57:38 +0100 Subject: [PATCH 10/70] cheat tune also with integer part --- pySC/tuning/tune.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/pySC/tuning/tune.py b/pySC/tuning/tune.py index a2196fd..74d9175 100644 --- a/pySC/tuning/tune.py +++ b/pySC/tuning/tune.py @@ -24,10 +24,18 @@ class Tune(BaseModel, extra="forbid"): def design_qx(self): return np.mod(self._parent._parent.lattice.twiss['qx'], 1) + @property + def integer_qx(self): + return np.floor(self._parent._parent.lattice.twiss['qx']) + @property def design_qy(self): return np.mod(self._parent._parent.lattice.twiss['qy'], 1) + @property + def integer_qy(self): + return np.floor(self._parent._parent.lattice.twiss['qy']) + def tune_response(self, quads: list[str], dk: float = 1e-5): SC = self._parent._parent twiss = SC.lattice.get_twiss(use_design=True) @@ -117,18 +125,23 @@ def correct(self, target_qx: Optional[float] = None, target_qy: Optional[float] gain : float, optional Gain for the correction. Default is 1. measurement_method : str, optional - Method to measure the tune. Options are 'kick', 'first_turn', 'orbit', 'cheat', 'cheat4d'. + Method to measure the tune. Options are 'kick', 'first_turn', 'orbit', + 'cheat', 'cheat4d', 'cheat_with_integer'. Default is 'kick'. ''' - if measurement_method not in ['kick', 'first_turn', 'orbit', 'cheat', 'cheat4d']: + if measurement_method not in ['kick', 'first_turn', 'orbit', 'cheat', 'cheat4d', 'cheat_with_integer']: raise NotImplementedError(f'{measurement_method=} not implemented yet.') if target_qx is None: target_qx = self.design_qx + if measurement_method == 'cheat_with_integer': + target_qx += self.integer_qx if target_qy is None: target_qy = self.design_qy + if measurement_method == 'cheat_with_integer': + target_qy += self.integer_qy for _ in range(n_iter): if measurement_method == 'kick': @@ -141,6 +154,8 @@ def correct(self, target_qx: Optional[float] = None, target_qy: Optional[float] qx, qy = self.cheat() elif measurement_method == 'cheat4d': qx, qy = self.cheat4d() + elif measurement_method == 'cheat_with_integer': + qx, qy = self.cheat_with_integer() else: raise Exception(f'Unknown measurement_method {measurement_method}') if qx is None or qy is None or qx != qx or qy != qy: @@ -328,4 +343,16 @@ def cheat(self) -> tuple[float, float]: logger.info(f"Horizontal tune: Qx = {qx:.3f} (Δ = {delta_x:.3f})") logger.info(f"Vertical tune: Qy = {qy:.3f} (Δ = {delta_y:.3f})") + return qx, qy + + def cheat_with_integer(self) -> tuple[float, float]: + SC = self._parent._parent + twiss = SC.lattice.get_twiss() + qx = twiss['qx'] + qy = twiss['qy'] + delta_x = qx - self.design_qx - self.integer_qx + delta_y = qy - self.design_qy - self.integer_qy + logger.info(f"Horizontal tune: Qx = {qx:.3f} (Δ = {delta_x:.3f})") + logger.info(f"Vertical tune: Qy = {qy:.3f} (Δ = {delta_y:.3f})") + return qx, qy \ No newline at end of file From c41b63b7ac80dd0c81d9925fd189e63c24d5e625 Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 6 Jan 2026 17:46:03 +0100 Subject: [PATCH 11/70] add knob method in magnet settings and c minus calculation (first tests unsuccessful, probably not correct) --- pySC/__init__.py | 1 + pySC/core/magnetsettings.py | 34 +++++++++++++- pySC/utils/rdt.py | 92 +++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 pySC/utils/rdt.py diff --git a/pySC/__init__.py b/pySC/__init__.py index 10975df..68eae1d 100644 --- a/pySC/__init__.py +++ b/pySC/__init__.py @@ -10,6 +10,7 @@ from .core.new_simulated_commissioning import SimulatedCommissioning from .configuration.generation import generate_SC +from .tuning.response_matrix import ResponseMatrix import logging import sys diff --git a/pySC/core/magnetsettings.py b/pySC/core/magnetsettings.py index 63197d0..d1eb5a1 100644 --- a/pySC/core/magnetsettings.py +++ b/pySC/core/magnetsettings.py @@ -1,7 +1,7 @@ from typing import Dict, Optional, TYPE_CHECKING from pydantic import BaseModel, Field, model_validator, PrivateAttr from .magnet import Magnet, ControlMagnetLink, MAGNET_NAME_TYPE -from .control import Control +from .control import Control, LinearConv if TYPE_CHECKING: from .new_simulated_commissioning import SimulatedCommissioning @@ -154,6 +154,38 @@ def add_individually_powered_magnet(self, ) self.add_link(link) + def add_knob(self, knob_name: str, control_names: list, weights: Optional[list[float]] = None) -> None: + knob = Control(name=knob_name, setpoint=0) + + if weights is None: + weights = [1]*len(control_names) + + if len(control_names) != len(weights): + raise Exception('Control names and weights have unequal lengths.') + + self.add_control(knob) + for control_name, weight in zip(control_names, weights): + control = self.controls[control_name] + for link in control._links: + if type(link.error) is LinearConv: + new_error = LinearConv(factor=weight*link.error.factor, offset=weight*link.error.offset) + else: + raise Exception(f'Unknown error type: {type(link.error).__name__}') + + new_link = ControlMagnetLink( + link_name=f"{knob_name}->{link.link_name}", + magnet_name=link.magnet_name, + control_name=knob_name, + component=link.component, + order=link.order, + error=new_error, + is_integrated=link.is_integrated, + ) + self.add_link(new_link) + + self.connect_links() + + def connect_links(self) -> None: # Clear any previous links for control in self.controls.values(): diff --git a/pySC/utils/rdt.py b/pySC/utils/rdt.py new file mode 100644 index 0000000..ac5ace6 --- /dev/null +++ b/pySC/utils/rdt.py @@ -0,0 +1,92 @@ +import numpy as np +from typing import TYPE_CHECKING, Optional + +if TYPE_CHECKING: + from pySC import SimulatedCommissioning + +## FACTORIAL[n] = n! +FACTORIAL = np.array([ +1, +1, +2, +6, +24, +120, +720, +5040, +40320, +362880, +3628800, +39916800, +479001600, +6227020800, +87178291200, +1307674368000, +20922789888000, +355687428096000, +6402373705728000, +121645100408832000, +2432902008176640000, +]) + +def binomial_coeff(n:int, k: int): + return FACTORIAL[n] / (FACTORIAL[k] * FACTORIAL[n-k]) + +def feeddown(AB: np.ndarray[complex], r0: complex, n: int): + maxN = len(AB) + value = 0j + for k in range(n, maxN): + value += AB[k] * binomial_coeff(k, n) * (r0 ** (k - n)) + return value + +def get_integrated_strengths_with_feeddown(SC: "SimulatedCommissioning", use_design: bool = False): + twiss = SC.lattice.get_twiss(use_design=use_design) + if use_design: + magnet_settings = SC.design_magnet_settings + else: + magnet_settings = SC.magnet_settings + + N = len(twiss['s']) + temp_max_order = 0 + integrated_strengths = {'norm': {0: np.zeros(N)}, + 'skew': {0: np.zeros(N)} + } + + for magnet in magnet_settings.magnets.values(): + ii = magnet.sim_index + x_co = twiss['x'][ii] + y_co = twiss['y'][ii] + if not use_design: + dx, dy = SC.support_system.get_total_offset(ii) + roll, _, _ = SC.support_system.get_total_rotation(ii) + else: + dx = 0 + dy = 0 + roll = 0 + + r0 = dx - x_co + 1.j * (dy - y_co) + + while magnet.max_order > temp_max_order: + temp_max_order += 1 + integrated_strengths['norm'][temp_max_order] = np.zeros(N) + integrated_strengths['skew'][temp_max_order] = np.zeros(N) + + AB = (np.array(magnet.B) + 1.j*np.array(magnet.A)) * np.exp(1.j*roll) * magnet.length + for jj in range(magnet.max_order + 1): + AB_with_feeddown = feeddown(AB, r0, jj) + integrated_strengths['norm'][jj][ii] = AB_with_feeddown.real * FACTORIAL[jj] + integrated_strengths['skew'][jj][ii] = AB_with_feeddown.imag * FACTORIAL[jj] + + return integrated_strengths + +def calculate_c_minus(SC: Optional["SimulatedCommissioning"] = None, use_design: bool = False, integrated_strengths : Optional[dict] = None, twiss: Optional[dict] = None): + if integrated_strengths is None: + assert SC is not None + integrated_strengths = get_integrated_strengths_with_feeddown(SC, use_design=use_design) + ks1l = integrated_strengths['skew'][1] + if twiss is None: + assert SC is not None + twiss = SC.lattice.get_twiss(use_design=use_design) + integrand = ks1l * np.sqrt(twiss['betx']*twiss['bety']) * np.exp(-1.j*(twiss['mux'] - twiss['muy'])) + c_minus = -np.sum(integrand)/2./np.pi + return c_minus \ No newline at end of file From 24a1796851ae2a01fa41ccf1bbf18a1a09e6f4eb Mon Sep 17 00:00:00 2001 From: kparasch Date: Wed, 7 Jan 2026 14:39:18 +0100 Subject: [PATCH 12/70] enable usage of closed orbit guess --- pySC/core/lattice.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/pySC/core/lattice.py b/pySC/core/lattice.py index e106407..d4ee09e 100644 --- a/pySC/core/lattice.py +++ b/pySC/core/lattice.py @@ -51,6 +51,8 @@ class ATLattice(Lattice): # the different machine types at_simulator: None = None use: str = 'RING' + orbit_guess: Optional[list[float]] = None + use_orbit_guess: bool = False _ring: at.Lattice = PrivateAttr(default=None) _design: at.Lattice = PrivateAttr(default=None) @@ -63,6 +65,9 @@ def load_lattice(self): self._ring.enable_6d() self._design.enable_6d() + if self.orbit_guess is None: + self.orbit_guess = [0] * 6 + self._twiss = self.get_twiss(use_design=True) return self @@ -76,6 +81,19 @@ def omp_num_threads(self, value: int): self._omp_num_threads = value at.lattice.DConstant.patpass_poolsize = value + def update_orbit_guess(self, n_turns=1000): + bunch = np.zeros([1,6]) + out = self.track(bunch, n_turns=n_turns, coordinates=['x','px','y','py','delta','tau']) + guess = np.nanmean(out, axis=3).flatten() + logger.info('Found orbit guess:') + logger.info(f' x = {guess[0]}') + logger.info(f' px = {guess[1]}') + logger.info(f' y = {guess[2]}') + logger.info(f' py = {guess[3]}') + logger.info(f' tau = {guess[5]}') + logger.info(f' delta = {guess[4]}') + self.orbit_guess = list(guess) + def track(self, bunch: nparray, indices: Optional[list[int]] = None, n_turns: int = 1, use_design: bool = False, coordinates: Optional[list] = None) -> nparray: new_bunch = bunch.copy() new_bunch[:,4], new_bunch[:,5] = new_bunch[:,5].copy(), new_bunch[:,4].copy() # swap zeta and delta for AT @@ -117,7 +135,12 @@ def get_orbit(self, indices: list[int] = None, use_design=False) -> dict: if indices is None: indices = range(len(self._design)) ring = self._design if use_design else self._ring - _, orbit = at.find_orbit(ring, refpts=indices) + if self.use_orbit_guess: + assert self.no_6d is False, "Using orbit guesses with a 4D lattice is not checked/implemented." + _, orbit = at.find_orbit(ring, refpts=indices, guess=np.array(self.orbit_guess)) + else: + _, orbit = at.find_orbit(ring, refpts=indices) + return orbit[:, [0,2]].T def get_twiss(self, indices: Optional[list[int]] = None, use_design=False) -> dict: @@ -128,7 +151,13 @@ def get_twiss(self, indices: Optional[list[int]] = None, use_design=False) -> di if indices is None: indices = range(len(self._design)) ring = self._design if use_design else self._ring - _, ringdata, elemdata = at.get_optics(ring, refpts=indices, get_chrom=True) + if self.use_orbit_guess: + assert self.no_6d is False, "Using orbit guesses with a 4D lattice is not checked/implemented." + orbit0, _ = at.find_orbit(ring, refpts=indices, guess=np.array(self.orbit_guess)) + else: + orbit0, _ = at.find_orbit(ring, refpts=indices) + + _, ringdata, elemdata = at.get_optics(ring, refpts=indices, get_chrom=True, orbit=orbit0) qs = ringdata['tune'][2] if not self.no_6d else 0 # doesn't exist when ring has 6d disabled From 10f89c963c1f5f2c14a766373597cf2460206dd6 Mon Sep 17 00:00:00 2001 From: kparasch Date: Wed, 7 Jan 2026 14:39:39 +0100 Subject: [PATCH 13/70] rdt calculation --- pySC/utils/rdt.py | 61 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/pySC/utils/rdt.py b/pySC/utils/rdt.py index ac5ace6..b651fae 100644 --- a/pySC/utils/rdt.py +++ b/pySC/utils/rdt.py @@ -39,6 +39,12 @@ def feeddown(AB: np.ndarray[complex], r0: complex, n: int): value += AB[k] * binomial_coeff(k, n) * (r0 ** (k - n)) return value +def omega(x): + if x%2: + return 0 + else: + return 1 + def get_integrated_strengths_with_feeddown(SC: "SimulatedCommissioning", use_design: bool = False): twiss = SC.lattice.get_twiss(use_design=use_design) if use_design: @@ -87,6 +93,55 @@ def calculate_c_minus(SC: Optional["SimulatedCommissioning"] = None, use_design: if twiss is None: assert SC is not None twiss = SC.lattice.get_twiss(use_design=use_design) - integrand = ks1l * np.sqrt(twiss['betx']*twiss['bety']) * np.exp(-1.j*(twiss['mux'] - twiss['muy'])) - c_minus = -np.sum(integrand)/2./np.pi - return c_minus \ No newline at end of file + Delta = twiss['qx'] - twiss['qy'] + integrand = ks1l * np.sqrt(twiss['betx']*twiss['bety']) * np.exp(+1.j*(twiss['mux'] - twiss['muy'] - np.pi*Delta)) + #integrand = ks1l * np.sqrt(twiss['betx']*twiss['bety']) * np.exp(-1.j*(twiss['mux'] - twiss['muy'] - 2*np.pi*Delta*twiss['s']/circumference)) + c_minus = np.sum(integrand)/2./np.pi + return c_minus + +def hjklm(SC: Optional["SimulatedCommissioning"] = None, j: int = 0, k: int = 0, l: int = 0, m: int = 0, use_design: bool = False, + integrated_strengths: Optional[dict] = None, twiss: Optional[dict] = None): + n = j+k+l+m + assert n > 0 + + if integrated_strengths is None: + assert SC is not None + integrated_strengths = get_integrated_strengths_with_feeddown(SC, use_design=use_design) + if twiss is None: + assert SC is not None + twiss = SC.lattice.get_twiss(use_design=use_design) + + K = integrated_strengths['norm'][n-1] + J = integrated_strengths['skew'][n-1] + h = - (K * omega(l+m) + 1j * J * omega(l+m+1))/(FACTORIAL[j] * FACTORIAL[k] * FACTORIAL[l] * FACTORIAL[m] * 2**(n)) * (1.j)**(l+m) * twiss['betx']**((j+k)/2) * twiss['bety']**((l+m)/2) + return h + + +def fjklm(SC: Optional["SimulatedCommissioning"] = None, j: int = 0, k: int = 0, l: int = 0, m: int = 0, + use_design: bool = False, integrated_strengths: Optional[dict] = None, twiss: Optional[dict] = None, normalized: bool = True): + + assert j + k + l + m > 0 + + if twiss is None: + assert SC is not None + twiss = SC.lattice.get_twiss(use_design=use_design) + + qx = twiss['qx'] + qy = twiss['qy'] + denom = 1 - np.exp(1.j * 2 * np.pi * ((j-k) * qx + (l-m) * qy)) + h = hjklm(SC=SC, j=j, k=k, l=l, m=m, use_design=use_design, integrated_strengths=integrated_strengths, twiss=twiss) + mask = h != 0 + hm = h[mask] + mux = twiss['mux'][mask] + muy = twiss['muy'][mask] + ii = 0 + f = np.zeros_like(twiss['s'], dtype=complex) + for ii in range(len(twiss['s'])): + dphix = 2*np.pi*np.abs(twiss['mux'][ii] - mux) + dphiy = 2*np.pi*np.abs(twiss['muy'][ii] - muy) + expo = np.exp(1.j * ( (j-k) * dphix + (l-m) * dphiy)) + f[ii] = np.sum(hm * expo) + if normalized: + return f / denom + else: + return f \ No newline at end of file From cbae9f85b364c8cdc42773c7aad4f3d97aad82e2 Mon Sep 17 00:00:00 2001 From: kparasch Date: Wed, 7 Jan 2026 14:45:22 +0100 Subject: [PATCH 14/70] rename tune.trim_tune to tune.trim --- pySC/tuning/tune.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pySC/tuning/tune.py b/pySC/tuning/tune.py index 74d9175..e5744b0 100644 --- a/pySC/tuning/tune.py +++ b/pySC/tuning/tune.py @@ -70,6 +70,10 @@ def build_tune_response_matrix(self, dk: float = 1e-5) -> None: return def trim_tune(self, dqx: float = 0, dqy: float = 0, use_design: bool = False) -> None: + logger.warning('Deprecation: please use .trim instead of .trim_tune.') + return self.trim(dqx=dqx, dqy=dqy, use_design=use_design) + + def trim(self, dqx: float = 0, dqy: float = 0, use_design: bool = False) -> None: SC = self._parent._parent if self.inverse_tune_response_matrix is None: logger.info('Did not find inverse tune response matrix. Building now.') From 58b7b379ad7c555bc029e191910c2f1f8ae86d1b Mon Sep 17 00:00:00 2001 From: kparasch Date: Wed, 7 Jan 2026 14:49:51 +0100 Subject: [PATCH 15/70] change nan value to 0 --- pySC/tuning/trajectory_bba.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pySC/tuning/trajectory_bba.py b/pySC/tuning/trajectory_bba.py index d672054..f8d2d4b 100644 --- a/pySC/tuning/trajectory_bba.py +++ b/pySC/tuning/trajectory_bba.py @@ -227,7 +227,7 @@ def get_orbit(): except Exception as exc: print(exc) logger.warning(f'Failed to compute trajectory BBA for BPM {bpm_name}') - offset, offset_err = np.nan, np.nan + offset, offset_err = 0, np.nan return offset, offset_err From a5ad71fc5abb9619acebcedfa0240387b6ce7f69 Mon Sep 17 00:00:00 2001 From: kparasch Date: Wed, 7 Jan 2026 16:33:44 +0100 Subject: [PATCH 16/70] changed functions to use tune.trim instead of tune.trim_tune --- pySC/tuning/tune.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pySC/tuning/tune.py b/pySC/tuning/tune.py index e5744b0..06444d4 100644 --- a/pySC/tuning/tune.py +++ b/pySC/tuning/tune.py @@ -169,7 +169,7 @@ def correct(self, target_qx: Optional[float] = None, target_qy: Optional[float] dqx = qx - target_qx dqy = qy - target_qy logger.info(f"Delta tune: delta_q1={dqx:.4f}, delta_q2={dqy:.4f}") - self.trim_tune(dqx=-gain*dqx, dqy=-gain*dqy) + self.trim(dqx=-gain*dqx, dqy=-gain*dqy) return def get_design_corrector_response_injection(self, corr: str, dk0: float = 1e-6): @@ -225,15 +225,15 @@ def get_average_xy(SC, n=10): ### do fit based on knobs def x_chi2(delta): - SC.tuning.tune.trim_tune(delta, 0, use_design=True) + SC.tuning.tune.trim(delta, 0, use_design=True) dx_ideal, _ = self.get_design_corrector_response_injection(hcorr) - SC.tuning.tune.trim_tune(-delta, 0, use_design=True) + SC.tuning.tune.trim(-delta, 0, use_design=True) return np.sum((dx0 - dx_ideal)**2) def y_chi2(delta): - SC.tuning.tune.trim_tune(0, delta, use_design=True) + SC.tuning.tune.trim(0, delta, use_design=True) _, dy_ideal = self.get_design_corrector_response_injection(vcorr) - SC.tuning.tune.trim_tune(0, -delta, use_design=True) + SC.tuning.tune.trim(0, -delta, use_design=True) return np.sum((dy0 - dy_ideal)**2) x_res = scipy.optimize.minimize_scalar(x_chi2, (-0.1, 0.1), method='Brent') @@ -306,15 +306,15 @@ def get_average_xy(SC, n=10): ### do fit based on knobs def x_chi2(delta): - SC.tuning.tune.trim_tune(delta, 0, use_design=True) + SC.tuning.tune.trim(delta, 0, use_design=True) dx_ideal, _ = self.get_design_corrector_response_orbit(hcorr, dk0=dk0) - SC.tuning.tune.trim_tune(-delta, 0, use_design=True) + SC.tuning.tune.trim(-delta, 0, use_design=True) return np.sum((dx0 - dx_ideal)**2) def y_chi2(delta): - SC.tuning.tune.trim_tune(0, delta, use_design=True) + SC.tuning.tune.trim(0, delta, use_design=True) _, dy_ideal = self.get_design_corrector_response_orbit(vcorr, dk0=dk0) - SC.tuning.tune.trim_tune(0, -delta, use_design=True) + SC.tuning.tune.trim(0, -delta, use_design=True) return np.sum((dy0 - dy_ideal)**2) x_res = scipy.optimize.minimize_scalar(x_chi2, (-0.1, 0.1), method='Brent') From 1bd6bfc781dc6939b25e4571ea7bb9cd05c6b5a1 Mon Sep 17 00:00:00 2001 From: kparasch Date: Wed, 7 Jan 2026 16:45:42 +0100 Subject: [PATCH 17/70] attach information to each control according to what it is supposed to be controlling --- pySC/core/control.py | 15 +++++++++++++-- pySC/core/magnetsettings.py | 11 ++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/pySC/core/control.py b/pySC/core/control.py index 89eec52..4a9df68 100644 --- a/pySC/core/control.py +++ b/pySC/core/control.py @@ -1,6 +1,6 @@ from __future__ import annotations -from pydantic import BaseModel, PrivateAttr -from typing import Optional, TYPE_CHECKING +from pydantic import BaseModel, PrivateAttr, PositiveInt +from typing import Optional, Literal, Union, TYPE_CHECKING if TYPE_CHECKING: from .magnet import ControlMagnetLink @@ -12,9 +12,20 @@ class LinearConv(BaseModel, extra="forbid"): def transform(self, value: float) -> float: return value * self.factor + self.offset +class IndivControl(BaseModel, extra="forbid"): + magnet_name: str + component: Literal["A", "B"] + order: PositiveInt + is_integrated: bool + +class KnobControl(BaseModel, extra="forbid"): + control_names: list[str] + weights: Optional[list[float]] = None + class Control(BaseModel, extra="forbid"): name: str setpoint: float + info: Optional[Union[IndivControl, KnobControl]] = None # calibration: LinearConv = LinearConv() # for future use, if needed limits: Optional[tuple[float, float]] = None _links: Optional[list[ControlMagnetLink]] = PrivateAttr(default=[]) diff --git a/pySC/core/magnetsettings.py b/pySC/core/magnetsettings.py index d1eb5a1..5db91ee 100644 --- a/pySC/core/magnetsettings.py +++ b/pySC/core/magnetsettings.py @@ -1,7 +1,7 @@ from typing import Dict, Optional, TYPE_CHECKING from pydantic import BaseModel, Field, model_validator, PrivateAttr from .magnet import Magnet, ControlMagnetLink, MAGNET_NAME_TYPE -from .control import Control, LinearConv +from .control import Control, LinearConv, IndivControl, KnobControl if TYPE_CHECKING: from .new_simulated_commissioning import SimulatedCommissioning @@ -136,6 +136,14 @@ def add_individually_powered_magnet(self, for component in controlled_components: control_name = f"{magnet.name}/{component}" control = Control(name=control_name, setpoint=0.0) + + # attach information which describes what the control is supposed to do + # (to be used by tuning algorithms) + is_integrated = True if component[-1] == 'L' else False + order = int(component[1:-1]) if is_integrated else int(component[1:]) + control.info = IndivControl(magnet_name=magnet.name, component=component[0], + order=order, is_integrated=is_integrated) + self.add_control(control) # Create links for each component @@ -156,6 +164,7 @@ def add_individually_powered_magnet(self, def add_knob(self, knob_name: str, control_names: list, weights: Optional[list[float]] = None) -> None: knob = Control(name=knob_name, setpoint=0) + knob.info = KnobControl(control_names=control_names, weights=weights) if weights is None: weights = [1]*len(control_names) From 1c15e655c9d29109ab7db5dc90dde6b80d38669f Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 8 Jan 2026 14:19:12 +0100 Subject: [PATCH 18/70] linear normal form for the calculation of c-minus --- pySC/core/lattice.py | 16 ++++++ pySC/utils/rdt.py | 126 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 129 insertions(+), 13 deletions(-) diff --git a/pySC/core/lattice.py b/pySC/core/lattice.py index d4ee09e..1664ed9 100644 --- a/pySC/core/lattice.py +++ b/pySC/core/lattice.py @@ -346,3 +346,19 @@ def update_cavity(self, index: int, voltage: float, phase: float, frequency: flo elem.TimeLag = timelag elem.Frequency = frequency return + + def one_turn_matrix(self, use_design=False): + ring = self._design if use_design else self._ring + + if self.use_orbit_guess: + assert self.no_6d is False, "Using orbit guesses with a 4D lattice is not checked/implemented." + orbit0, _ = at.find_orbit(ring, guess=np.array(self.orbit_guess)) + else: + orbit0, _ = at.find_orbit(ring) + + if self.no_6d: + M = ring.find_m44(orbit=orbit0)[0] + else: + M = ring.find_m66(orbit=orbit0)[0] + + return M diff --git a/pySC/utils/rdt.py b/pySC/utils/rdt.py index b651fae..a28a17a 100644 --- a/pySC/utils/rdt.py +++ b/pySC/utils/rdt.py @@ -29,6 +29,11 @@ 2432902008176640000, ]) +S4 = np.array([[0., 1., 0., 0.], + [-1., 0., 0., 0.], + [ 0., 0., 0., 1.], + [ 0., 0.,-1., 0.]]) + def binomial_coeff(n:int, k: int): return FACTORIAL[n] / (FACTORIAL[k] * FACTORIAL[n-k]) @@ -85,18 +90,18 @@ def get_integrated_strengths_with_feeddown(SC: "SimulatedCommissioning", use_des return integrated_strengths -def calculate_c_minus(SC: Optional["SimulatedCommissioning"] = None, use_design: bool = False, integrated_strengths : Optional[dict] = None, twiss: Optional[dict] = None): - if integrated_strengths is None: - assert SC is not None - integrated_strengths = get_integrated_strengths_with_feeddown(SC, use_design=use_design) - ks1l = integrated_strengths['skew'][1] - if twiss is None: - assert SC is not None - twiss = SC.lattice.get_twiss(use_design=use_design) - Delta = twiss['qx'] - twiss['qy'] - integrand = ks1l * np.sqrt(twiss['betx']*twiss['bety']) * np.exp(+1.j*(twiss['mux'] - twiss['muy'] - np.pi*Delta)) - #integrand = ks1l * np.sqrt(twiss['betx']*twiss['bety']) * np.exp(-1.j*(twiss['mux'] - twiss['muy'] - 2*np.pi*Delta*twiss['s']/circumference)) - c_minus = np.sum(integrand)/2./np.pi +def calculate_c_minus(SC: Optional["SimulatedCommissioning"] = None, use_design: bool = False): + + M = SC.lattice.one_turn_matrix(use_design=use_design) + W, _, _, q1, q2 = linear_normal_form(M) + + c_r1 = np.sqrt(W[2,0]**2 + W[2,1]**2) / W[0,0] + c_r2 = np.sqrt(W[0,2]**2 + W[0,3]**2) / W[2,2] + c_phi1 = np.arctan2(W[2,1], W[2,0]) + + cmin_amp = (2 * np.sqrt(c_r1*c_r2) * np.abs(q1 - q2) / (1 + c_r1 * c_r2)) + c_minus = cmin_amp * np.exp(1j * c_phi1) + return c_minus def hjklm(SC: Optional["SimulatedCommissioning"] = None, j: int = 0, k: int = 0, l: int = 0, m: int = 0, use_design: bool = False, @@ -144,4 +149,99 @@ def fjklm(SC: Optional["SimulatedCommissioning"] = None, j: int = 0, k: int = 0, if normalized: return f / denom else: - return f \ No newline at end of file + return f + + +def Rot2D(mu): + return np.array([[ np.cos(mu), np.sin(mu)], + [-np.sin(mu), np.cos(mu)]]) + +def linear_normal_form(M): + w0, v0 = np.linalg.eig(M[:4,:4]) + + a0 = np.real(v0) + b0 = np.imag(v0) + + index_list = [0,1,2,3] + + ##### Sort modes in pairs of conjugate modes ##### + + conj_modes = np.zeros([2,2], dtype=int) + + conj_modes[0,0] = index_list[0] + del index_list[0] + + min_index = 0 + min_diff = abs(np.imag(w0[conj_modes[0,0]] + w0[index_list[min_index]])) + for i in range(1,len(index_list)): + diff = abs(np.imag(w0[conj_modes[0,0]] + w0[index_list[i]])) + if min_diff > diff: + min_diff = diff + min_index = i + + conj_modes[0,1] = index_list[min_index] + del index_list[min_index] + + conj_modes[1,0] = index_list[0] + conj_modes[1,1] = index_list[1] + + ################################################## + #### Select mode from pairs with positive (real @ S @ imag) ##### + + modes = np.empty(2, dtype=int) + for ii,ind in enumerate(conj_modes): + if np.matmul(np.matmul(a0[:,ind[0]], S4), b0[:,ind[0]]) > 0: + modes[ii] = ind[0] + else: + modes[ii] = ind[1] + + ################################################## + #### Sort modes such that (1,2) is close to (x,y) #### + + if abs(v0[:,modes[1]])[2] < abs(v0[:,modes[0]])[2]: + modes[0], modes[1] = modes[1], modes[0] + + ################################################## + #### Rotate eigenvectors to the Courant-Snyder parameterization #### + phase0 = np.log(v0[0,modes[0]]).imag + phase1 = np.log(v0[2,modes[1]]).imag + + v0[:,modes[0]] *= np.exp(-1.j*phase0) + v0[:,modes[1]] *= np.exp(-1.j*phase1) + + ################################################## + #### Construct W ################################# + + a1 = v0[:,modes[0]].real + a2 = v0[:,modes[1]].real + b1 = v0[:,modes[0]].imag + b2 = v0[:,modes[1]].imag + + n1 = 1./np.sqrt(np.matmul(np.matmul(a1, S4), b1)) + n2 = 1./np.sqrt(np.matmul(np.matmul(a2, S4), b2)) + + a1 *= n1 + a2 *= n2 + + b1 *= n1 + b2 *= n2 + + W = np.array([a1,b1,a2,b2]).T + W[abs(W) < 1.e-14] = 0. # Set very small numbers to zero. + invW = np.matmul(np.matmul(S4.T, W.T), S4) + + ################################################## + #### Get tunes and rotation matrix in the normalized coordinates #### + + mu1 = np.log(w0[modes[0]]).imag + mu2 = np.log(w0[modes[1]]).imag + + q1 = mu1/(2.*np.pi) + q2 = mu2/(2.*np.pi) + + R = np.zeros_like(W) + R[0:2,0:2] = Rot2D(mu1) + R[2:4,2:4] = Rot2D(mu2) + ################################################## + + return W, invW, R, q1, q2 \ No newline at end of file From 5ad1f5cdfbd8e0f7b7c199e8933ac141302edc7b Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 8 Jan 2026 16:31:01 +0100 Subject: [PATCH 19/70] knobdata class and cminus tuning tool --- pySC/core/control.py | 3 + pySC/core/new_simulated_commissioning.py | 18 +++- pySC/tuning/c_minus.py | 125 +++++++++++++++++++++++ pySC/tuning/tuning_core.py | 2 + 4 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 pySC/tuning/c_minus.py diff --git a/pySC/core/control.py b/pySC/core/control.py index 4a9df68..a0a5faa 100644 --- a/pySC/core/control.py +++ b/pySC/core/control.py @@ -22,6 +22,9 @@ class KnobControl(BaseModel, extra="forbid"): control_names: list[str] weights: Optional[list[float]] = None +class KnobData(BaseModel, extra="forbid"): + data: dict[str, KnobControl] = {} + class Control(BaseModel, extra="forbid"): name: str setpoint: float diff --git a/pySC/core/new_simulated_commissioning.py b/pySC/core/new_simulated_commissioning.py index 1a33b92..4f5db32 100644 --- a/pySC/core/new_simulated_commissioning.py +++ b/pySC/core/new_simulated_commissioning.py @@ -12,6 +12,7 @@ from .injection import InjectionSettings from .rng import RNG from ..control_system.server import start_server as _start_server +from .control import KnobData class SimulatedCommissioning(BaseModel, extra="forbid"): lattice: ATLattice | XSuiteLattice @@ -80,6 +81,7 @@ def propagate_parents(self) -> None: self.tuning._parent = self self.tuning.tune._parent = self.tuning self.tuning.chromaticity._parent = self.tuning + self.tuning.c_minus._parent = self.tuning self.tuning.rf._parent = self.tuning return @@ -90,4 +92,18 @@ def copy(self) -> "SimulatedCommissioning": """ Create a copy of the SimulatedCommissioning instance. """ - return SimulatedCommissioning.model_validate(self.model_dump()) \ No newline at end of file + return SimulatedCommissioning.model_validate(self.model_dump()) + + def import_knob(self, json_filename: str) -> None: + with open(json_filename, 'r') as fp: + obj = json.load(fp) + knob_data = KnobData.model_validate(obj) + + for knob_name in knob_data.data.keys(): + assert knob_name not in self.magnet_settings.controls.keys(), f"knob with name {knob_name} already exists SC.magnet_settings." + assert knob_name not in self.design_magnet_settings.controls.keys(), f"knob with name {knob_name} already exists SC.design_magnet_settings." + + for knob_name in knob_data.data.keys(): + tdata = knob_data.data[knob_name] + self.magnet_settings.add_knob(knob_name=knob_name, control_names=tdata.control_names, weights=tdata.weights) + self.design_magnet_settings.add_knob(knob_name=knob_name, control_names=tdata.control_names, weights=tdata.weights) \ No newline at end of file diff --git a/pySC/tuning/c_minus.py b/pySC/tuning/c_minus.py new file mode 100644 index 0000000..d87accd --- /dev/null +++ b/pySC/tuning/c_minus.py @@ -0,0 +1,125 @@ +from typing import Optional, TYPE_CHECKING +from pydantic import BaseModel, PrivateAttr, ConfigDict +import numpy as np +import logging + +from ..core.control import KnobControl, KnobData +from ..utils import rdt +from .response_matrix import ResponseMatrix + +if TYPE_CHECKING: + from .tuning_core import Tuning + +logger = logging.getLogger(__name__) + +class CMinus(BaseModel, extra="forbid"): + knob_real: str = 'c_minus_real' + knob_imag: str = 'c_minus_imag' + controls: list[str] = [] + _parent: Optional['Tuning'] = PrivateAttr(default=None) + + model_config = ConfigDict(arbitrary_types_allowed=True) + + def c_minus_response(self, delta: float = 1e-5): + SC = self._parent._parent + + N = len(self.controls) + assert N > 0 + + c_minus_0 = rdt.calculate_c_minus(SC, use_design=True) + + delta_c_minus = np.zeros_like(self.controls, dtype=complex) + for ii, control in enumerate(self.controls): + logger.info(f'[{ii+1:d}/{N:d}] Calculating ideal c_minus response of {control}.') + sp0 = SC.magnet_settings.get(control, use_design=True) + SC.magnet_settings.set(control, delta + sp0, use_design=True) + c_minus = rdt.calculate_c_minus(SC, use_design=True) + SC.magnet_settings.set(control, sp0, use_design=True) + delta_c_minus[ii] = (c_minus - c_minus_0) / delta + + return delta_c_minus + + def create_c_minus_knobs(self, delta: float = 1e-5) -> None: + if not len(self.controls) > 0: + raise Exception('c_minus.controls is empty. Please set.') + + RM = np.zeros((2,len(self.controls))) + delta_c_minus = self.c_minus_response(delta=delta) + RM[0] = delta_c_minus.real + RM[1] = delta_c_minus.imag + + c_minus_response_matrix = ResponseMatrix(RM=RM) + iRM = c_minus_response_matrix.build_pseudoinverse().matrix + c_minus_real_knob = iRM[:, 0] + c_minus_imag_knob = iRM[:, 1] + + knob_data = KnobData(data={ + self.knob_real: KnobControl(control_names=self.controls, weights=list(c_minus_real_knob)), + self.knob_imag: KnobControl(control_names=self.controls, weights=list(c_minus_imag_knob)) + }) + + return knob_data + + def trim(self, real: float = 0, imag: float = 0, use_design: bool = False) -> None: + SC = self._parent._parent + + if use_design: + assert self.knob_real in SC.design_magnet_settings.controls.keys() + assert self.knob_imag in SC.design_magnet_settings.controls.keys() + else: + assert self.knob_real in SC.magnet_settings.controls.keys() + assert self.knob_imag in SC.magnet_settings.controls.keys() + + real0 = SC.magnet_settings.get(self.knob_real, use_design=use_design) + imag0 = SC.magnet_settings.get(self.knob_imag, use_design=use_design) + + SC.magnet_settings.set(self.knob_real, real0 + real, use_design=use_design) + SC.magnet_settings.set(self.knob_imag, imag0 + imag, use_design=use_design) + + return + + def correct(self, target_c_minus_real: float = 0, target_c_minus_imag: float = 0, + n_iter: int = 1, gain: float = 1, measurement_method: str = 'cheat'): + ''' + Correct c_minus to the target values. + Parameters + ---------- + target_c_minus_real : float + Target real c_minus. Default is 0. + target_c_minus_imag : float, optional + Target imaginary c_minus. Default is 0. + n_iter : int, optional + Number of correction iterations. Default is 1. + gain : float, optional + Gain for the correction. Default is 1. + measurement_method : str, optional + Method to measure c_minus. Options are 'cheat'. + Default is 'cheat'. + ''' + + if measurement_method not in ['cheat']: + raise NotImplementedError(f'{measurement_method=} not implemented yet.') + + for _ in range(n_iter): + if measurement_method == 'cheat': + c_minus = self.cheat() + else: + raise Exception(f'Unknown measurement_method {measurement_method}') + if c_minus is None or c_minus != c_minus: + logger.info("C_minus measurement failed, skipping correction.") + return + logger.info(f"Measured c_minus = {c_minus:.4f}") + delta_real = c_minus.real - target_c_minus_real + delta_imag = c_minus.imag - target_c_minus_imag + self.trim(real=-gain*delta_real, imag=-gain*delta_imag) + return + + def cheat(self, use_design: bool = False) -> tuple[float, float]: + SC = self._parent._parent + try: + c_minus = rdt.calculate_c_minus(SC, use_design=use_design) + except Exception as exc: + logger.warning(f"Exception while measuring c_minus: {exc}") + c_minus = np.nan + + return c_minus \ No newline at end of file diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index f282ae9..13562ed 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -7,6 +7,7 @@ from .parallel import parallel_tbba_target, parallel_obba_target, get_listener_and_queue from .tune import Tune from .chromaticity import Chromaticity +from .c_minus import CMinus from .rf_tuning import RF_tuning import numpy as np @@ -28,6 +29,7 @@ class Tuning(BaseModel, extra="forbid"): tune: Tune = Tune() ## TODO: generate config from yaml file chromaticity: Chromaticity = Chromaticity() ## TODO: generate config from yaml file + c_minus: CMinus = CMinus() ## TODO: generate config from yaml file rf: RF_tuning = RF_tuning() ## TODO: generate config from yaml file bba_magnets: list[str] = [] From 9781db6dee1afb617f2685ecbb9150882b22f837 Mon Sep 17 00:00:00 2001 From: kparasch Date: Fri, 9 Jan 2026 09:51:32 +0100 Subject: [PATCH 20/70] sort controls update using the control info --- pySC/configuration/tuning_conf.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/pySC/configuration/tuning_conf.py b/pySC/configuration/tuning_conf.py index 89a53db..a73388a 100644 --- a/pySC/configuration/tuning_conf.py +++ b/pySC/configuration/tuning_conf.py @@ -1,9 +1,17 @@ from ..core.new_simulated_commissioning import SimulatedCommissioning +from ..core.control import IndivControl import numpy as np def sort_controls(SC: SimulatedCommissioning, control_names: list[str]) -> list[str]: - names = [control_name.split('/')[0] for control_name in control_names] - indices = [SC.magnet_settings.magnets[name].sim_index for name in names] + magnet_names = [] + for control_name in control_names: + control = SC.magnet_settings.controls[control_name] + if type(control.info) is IndivControl: + magnet_name = control.info.magnet_name + else: + raise NotImplementedError(f"{control} is of type {type(control.info).__name__} which is not implemented.") + magnet_names.append(magnet_name) + indices = [SC.magnet_settings.magnets[name].sim_index for name in magnet_names] argsort = np.argsort(indices).tolist() sorted_control_names = [control_names[i] for i in argsort] return sorted_control_names From 05365037cb97c2ed462881b44c0119dcf7a49f6f Mon Sep 17 00:00:00 2001 From: kparasch Date: Fri, 9 Jan 2026 10:36:25 +0100 Subject: [PATCH 21/70] config of c_minus --- pySC/configuration/tuning_conf.py | 10 ++++++++-- pySC/tuning/c_minus.py | 4 ++-- pySC/tuning/tuning_core.py | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pySC/configuration/tuning_conf.py b/pySC/configuration/tuning_conf.py index a73388a..6006f7b 100644 --- a/pySC/configuration/tuning_conf.py +++ b/pySC/configuration/tuning_conf.py @@ -39,7 +39,6 @@ def configure_tuning(SC: SimulatedCommissioning) -> None: VCORR = sort_controls(SC, VCORR) SC.tuning.VCORR = VCORR - if 'model_RM_folder' in tuning_conf: SC.tuning.RM_folder = tuning_conf['model_RM_folder'] @@ -51,4 +50,11 @@ def configure_tuning(SC: SimulatedCommissioning) -> None: if 'bba_magnets' in tuning_conf: bba_magnets = configure_family(SC, config_dict=tuning_conf['bba_magnets']) bba_magnets = sort_controls(SC, bba_magnets) - SC.tuning.bba_magnets = bba_magnets \ No newline at end of file + SC.tuning.bba_magnets = bba_magnets + + if 'c_minus' in tuning_conf: + c_minus_conf = tuning_conf['c_minus'] + if 'controls' in c_minus_conf: + c_minus_controls = configure_family(SC, config_dict=c_minus_conf['controls']) + c_minus_controls = sort_controls(SC, c_minus_controls) + SC.tuning.c_minus.controls = c_minus_controls \ No newline at end of file diff --git a/pySC/tuning/c_minus.py b/pySC/tuning/c_minus.py index d87accd..62fbff8 100644 --- a/pySC/tuning/c_minus.py +++ b/pySC/tuning/c_minus.py @@ -79,7 +79,7 @@ def trim(self, real: float = 0, imag: float = 0, use_design: bool = False) -> No return def correct(self, target_c_minus_real: float = 0, target_c_minus_imag: float = 0, - n_iter: int = 1, gain: float = 1, measurement_method: str = 'cheat'): + n_iter: int = 1, gain: float = 1, measurement_method: str = 'cheat') -> None: ''' Correct c_minus to the target values. Parameters @@ -114,7 +114,7 @@ def correct(self, target_c_minus_real: float = 0, target_c_minus_imag: float = 0 self.trim(real=-gain*delta_real, imag=-gain*delta_imag) return - def cheat(self, use_design: bool = False) -> tuple[float, float]: + def cheat(self, use_design: bool = False) -> complex: SC = self._parent._parent try: c_minus = rdt.calculate_c_minus(SC, use_design=use_design) diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index 13562ed..2ec8920 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -29,7 +29,7 @@ class Tuning(BaseModel, extra="forbid"): tune: Tune = Tune() ## TODO: generate config from yaml file chromaticity: Chromaticity = Chromaticity() ## TODO: generate config from yaml file - c_minus: CMinus = CMinus() ## TODO: generate config from yaml file + c_minus: CMinus = CMinus() rf: RF_tuning = RF_tuning() ## TODO: generate config from yaml file bba_magnets: list[str] = [] From 5033161361c08f9102a317577c7a1be15ee6375a Mon Sep 17 00:00:00 2001 From: kparasch Date: Fri, 9 Jan 2026 11:37:44 +0100 Subject: [PATCH 22/70] pydantic BaseModel extension with save_as function --- pySC/core/bpm_system.py | 2 +- pySC/core/control.py | 3 ++- pySC/core/numpy_type.py | 9 --------- pySC/core/types.py | 27 +++++++++++++++++++++++++++ pySC/tuning/c_minus.py | 2 ++ pySC/tuning/chromaticity.py | 2 +- pySC/tuning/response_matrix.py | 2 +- pySC/tuning/rf_tuning.py | 2 +- pySC/tuning/tune.py | 2 +- 9 files changed, 36 insertions(+), 15 deletions(-) delete mode 100644 pySC/core/numpy_type.py create mode 100644 pySC/core/types.py diff --git a/pySC/core/bpm_system.py b/pySC/core/bpm_system.py index f075bc4..e0822be 100644 --- a/pySC/core/bpm_system.py +++ b/pySC/core/bpm_system.py @@ -1,6 +1,6 @@ from pydantic import BaseModel, PrivateAttr, ConfigDict, model_validator from typing import TYPE_CHECKING, Optional, Union -from .numpy_type import NPARRAY +from .types import NPARRAY import numpy as np import warnings diff --git a/pySC/core/control.py b/pySC/core/control.py index a0a5faa..2e66f5a 100644 --- a/pySC/core/control.py +++ b/pySC/core/control.py @@ -1,6 +1,7 @@ from __future__ import annotations from pydantic import BaseModel, PrivateAttr, PositiveInt from typing import Optional, Literal, Union, TYPE_CHECKING +from .types import BaseModelWithSave if TYPE_CHECKING: from .magnet import ControlMagnetLink @@ -22,7 +23,7 @@ class KnobControl(BaseModel, extra="forbid"): control_names: list[str] weights: Optional[list[float]] = None -class KnobData(BaseModel, extra="forbid"): +class KnobData(BaseModelWithSave, extra="forbid"): data: dict[str, KnobControl] = {} class Control(BaseModel, extra="forbid"): diff --git a/pySC/core/numpy_type.py b/pySC/core/numpy_type.py deleted file mode 100644 index 525fcf0..0000000 --- a/pySC/core/numpy_type.py +++ /dev/null @@ -1,9 +0,0 @@ - -from pydantic import BeforeValidator, PlainSerializer -from typing import Annotated -import numpy as np - -NPARRAY = Annotated[np.ndarray, - BeforeValidator(lambda x: np.array(x)), - PlainSerializer(lambda x: x.tolist(), return_type=list) - ] \ No newline at end of file diff --git a/pySC/core/types.py b/pySC/core/types.py new file mode 100644 index 0000000..b3056ac --- /dev/null +++ b/pySC/core/types.py @@ -0,0 +1,27 @@ +from pydantic import BeforeValidator, PlainSerializer, BaseModel +from typing import Annotated, Union, Optional +import numpy as np +from pathlib import Path + +NPARRAY = Annotated[np.ndarray, + BeforeValidator(lambda x: np.array(x)), + PlainSerializer(lambda x: x.tolist(), return_type=list) + ] + +class BaseModelWithSave(BaseModel): + def save_as(self, filename: Union[Path, str], indent: Optional[int] = None) -> None: + if type(filename) is not Path: + filename = Path(filename) + + data = self.model_dump() + suffix = filename.suffix + with open(filename, 'w') as fp: + if suffix in ['', '.json']: + import json + json.dump(data, fp, indent=indent) + elif suffix == '.yaml': + import yaml + yaml.safe_dump(data, fp, indent=indent) + else: + raise Exception(f'Unknown file extension: {suffix}.') + return \ No newline at end of file diff --git a/pySC/tuning/c_minus.py b/pySC/tuning/c_minus.py index 62fbff8..ac01d80 100644 --- a/pySC/tuning/c_minus.py +++ b/pySC/tuning/c_minus.py @@ -58,6 +58,8 @@ def create_c_minus_knobs(self, delta: float = 1e-5) -> None: self.knob_imag: KnobControl(control_names=self.controls, weights=list(c_minus_imag_knob)) }) + logger.info(f"{self.knob_real}: sum(|weights|)={np.sum(np.abs(c_minus_real_knob)):e}") + logger.info(f"{self.knob_imag}: sum(|weights|)={np.sum(np.abs(c_minus_imag_knob)):e}") return knob_data def trim(self, real: float = 0, imag: float = 0, use_design: bool = False) -> None: diff --git a/pySC/tuning/chromaticity.py b/pySC/tuning/chromaticity.py index af426be..d7f63f0 100644 --- a/pySC/tuning/chromaticity.py +++ b/pySC/tuning/chromaticity.py @@ -3,7 +3,7 @@ import numpy as np import logging -from ..core.numpy_type import NPARRAY +from ..core.types import NPARRAY if TYPE_CHECKING: from .tuning_core import Tuning diff --git a/pySC/tuning/response_matrix.py b/pySC/tuning/response_matrix.py index d7638f5..8764540 100644 --- a/pySC/tuning/response_matrix.py +++ b/pySC/tuning/response_matrix.py @@ -1,6 +1,6 @@ from pydantic import BaseModel, PrivateAttr, model_validator, ConfigDict from typing import Optional, Literal -from ..core.numpy_type import NPARRAY +from ..core.types import NPARRAY import numpy as np import logging diff --git a/pySC/tuning/rf_tuning.py b/pySC/tuning/rf_tuning.py index e52d55d..dc051be 100644 --- a/pySC/tuning/rf_tuning.py +++ b/pySC/tuning/rf_tuning.py @@ -3,7 +3,7 @@ import numpy as np import logging -from ..core.numpy_type import NPARRAY +from ..core.types import NPARRAY if TYPE_CHECKING: from .tuning_core import Tuning diff --git a/pySC/tuning/tune.py b/pySC/tuning/tune.py index 06444d4..2786153 100644 --- a/pySC/tuning/tune.py +++ b/pySC/tuning/tune.py @@ -4,7 +4,7 @@ import logging import scipy.optimize -from ..core.numpy_type import NPARRAY +from ..core.types import NPARRAY if TYPE_CHECKING: from .tuning_core import Tuning From e40cb7487d57a66b02ba4230424db4a1a62435c2 Mon Sep 17 00:00:00 2001 From: kparasch Date: Fri, 9 Jan 2026 12:14:42 +0100 Subject: [PATCH 23/70] some info printing about knobs --- pySC/core/new_simulated_commissioning.py | 7 ++++++- pySC/tuning/c_minus.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pySC/core/new_simulated_commissioning.py b/pySC/core/new_simulated_commissioning.py index 4f5db32..3b69cd3 100644 --- a/pySC/core/new_simulated_commissioning.py +++ b/pySC/core/new_simulated_commissioning.py @@ -1,6 +1,8 @@ from pydantic import BaseModel, model_validator, Field from typing import Optional import json +import numpy as np +import logging from .lattice import ATLattice, XSuiteLattice from .magnetsettings import MagnetSettings @@ -14,6 +16,8 @@ from ..control_system.server import start_server as _start_server from .control import KnobData +logger = logging.getLogger(__name__) + class SimulatedCommissioning(BaseModel, extra="forbid"): lattice: ATLattice | XSuiteLattice magnet_settings: MagnetSettings = MagnetSettings() @@ -106,4 +110,5 @@ def import_knob(self, json_filename: str) -> None: for knob_name in knob_data.data.keys(): tdata = knob_data.data[knob_name] self.magnet_settings.add_knob(knob_name=knob_name, control_names=tdata.control_names, weights=tdata.weights) - self.design_magnet_settings.add_knob(knob_name=knob_name, control_names=tdata.control_names, weights=tdata.weights) \ No newline at end of file + self.design_magnet_settings.add_knob(knob_name=knob_name, control_names=tdata.control_names, weights=tdata.weights) + logger.info(f'Imported knob {knob_name} with sum(|weights|) = {np.sum(np.abs(tdata.weights)):.2e}') \ No newline at end of file diff --git a/pySC/tuning/c_minus.py b/pySC/tuning/c_minus.py index ac01d80..ded4515 100644 --- a/pySC/tuning/c_minus.py +++ b/pySC/tuning/c_minus.py @@ -58,8 +58,8 @@ def create_c_minus_knobs(self, delta: float = 1e-5) -> None: self.knob_imag: KnobControl(control_names=self.controls, weights=list(c_minus_imag_knob)) }) - logger.info(f"{self.knob_real}: sum(|weights|)={np.sum(np.abs(c_minus_real_knob)):e}") - logger.info(f"{self.knob_imag}: sum(|weights|)={np.sum(np.abs(c_minus_imag_knob)):e}") + logger.info(f"{self.knob_real}: sum(|weights|)={np.sum(np.abs(c_minus_real_knob)):.2e}") + logger.info(f"{self.knob_imag}: sum(|weights|)={np.sum(np.abs(c_minus_imag_knob)):.2e}") return knob_data def trim(self, real: float = 0, imag: float = 0, use_design: bool = False) -> None: From daed6219cf840d5657972a2e63c3fe338e88005a Mon Sep 17 00:00:00 2001 From: kparasch Date: Fri, 16 Jan 2026 17:52:04 +0100 Subject: [PATCH 24/70] orbit bba changes --- pySC/tuning/orbit_bba.py | 59 +++++++++++++++++++++++++++----------- pySC/tuning/tuning_core.py | 44 ++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 25 deletions(-) diff --git a/pySC/tuning/orbit_bba.py b/pySC/tuning/orbit_bba.py index a1847c8..c55c9b4 100644 --- a/pySC/tuning/orbit_bba.py +++ b/pySC/tuning/orbit_bba.py @@ -16,7 +16,7 @@ def get_mag_s_pos(SC: "SimulatedCommissioning", MAG: list[str]): corr_name = corr.split('/')[0] index = SC.magnet_settings.magnets[corr_name].sim_index s_pos = SC.lattice.twiss['s'][index] - s_list.append(s_pos) + s_list.append(float(s_pos)) return s_list class Orbit_BBA_Configuration(BaseModel, extra="forbid"): @@ -35,16 +35,32 @@ def generate_config(cls, SC: "SimulatedCommissioning", max_dx_at_bpm = 1e-3, bba_magnets = SC.tuning.bba_magnets bba_magnets_s = get_mag_s_pos(SC, bba_magnets) - d1, d2 = RM.RM.shape - HRM = RM.RM[:d1//2, :d2//2] - VRM = RM.RM[d1//2:, d2//2:] - + mask_H = np.array(RM.inputs_plane) == 'H' + mask_V = np.array(RM.inputs_plane) == 'V' + d1, _ = RM.matrix.shape + HRM = RM.matrix[:d1//2, mask_H] + VRM = RM.matrix[d1//2:, mask_V] + + betx = SC.lattice.twiss['betx'] + bety = SC.lattice.twiss['bety'] + qx = SC.lattice.twiss['qx'] + qy = SC.lattice.twiss['qy'] + betx_at_bpms = betx[SC.bpm_system.indices] + bety_at_bpms = bety[SC.bpm_system.indices] for bpm_number in range(len(SC.bpm_system.indices)): bpm_index = SC.bpm_system.indices[bpm_number] bpm_s = SC.lattice.twiss['s'][bpm_index] bba_magnet_number = np.argmin(np.abs(bba_magnets_s - bpm_s)) the_bba_magnet = bba_magnets[bba_magnet_number] + bba_magnet_info = SC.magnet_settings.controls[the_bba_magnet].info + assert type(bba_magnet_info) is IndivControl, f'BBA magnet of unsupported type: {type(bba_magnet_info)}' + bba_magnet_is_integrated = bba_magnet_info.is_integrated + bba_magnet_index = SC.magnet_settings.magnets[bba_magnet_info.magnet_name].sim_index + if bba_magnet_info.component == 'B': + quad_is_skew = False + else: # it is a skew quadrupole component + quad_is_skew = True max_H_response = -1 the_HCORR_number = -1 @@ -55,12 +71,11 @@ def generate_config(cls, SC: "SimulatedCommissioning", max_dx_at_bpm = 1e-3, the_HCORR_number = int(imax) hcorr_delta = max_dx_at_bpm/max_H_response - if the_bba_magnet.split('/')[-1][0] == 'B': - temp_RM = HRM[:, the_HCORR_number] + if not quad_is_skew: + quad_response = np.sqrt(betx_at_bpms * betx[bba_magnet_index]) / (2 * np.abs(np.sin(np.pi*qx))) else: # it is a skew quadrupole component - ## TODO: this is wrong if hcorr and vcorr are not the same magnets!! - temp_RM = VRM[:, the_HCORR_number] - quad_dk_h = (max_modulation/float(np.max(np.abs(temp_RM)))) / max_dx_at_bpm + quad_response = np.sqrt(bety_at_bpms * bety[bba_magnet_index]) / (2 * np.abs(np.sin(np.pi*qy))) + quad_dkl_h = (max_modulation / float(np.max(np.abs(quad_response)))) / max_dx_at_bpm max_V_response = -1 the_VCORR_number = -1 @@ -71,12 +86,19 @@ def generate_config(cls, SC: "SimulatedCommissioning", max_dx_at_bpm = 1e-3, the_VCORR_number = int(imax) vcorr_delta = max_dx_at_bpm/max_V_response - if the_bba_magnet.split('/')[-1][0] == 'B': - temp_RM = VRM[:, the_VCORR_number] + if not quad_is_skew: + quad_response = np.sqrt(bety_at_bpms * bety[bba_magnet_index]) / (2 * np.abs(np.sin(np.pi*qy))) else: # it is a skew quadrupole component - ## TODO: this is wrong if hcorr and vcorr are not the same magnets!! - temp_RM = HRM[:, the_VCORR_number] - quad_dk_v = (max_modulation/float(np.max(np.abs(temp_RM)))) / max_dx_at_bpm + quad_response = np.sqrt(betx_at_bpms * betx[bba_magnet_index]) / (2 * np.abs(np.sin(np.pi*qx))) + quad_dkl_v = (max_modulation / float(np.max(np.abs(quad_response)))) / max_dx_at_bpm + + if not bba_magnet_is_integrated: + bba_magnet_length = SC.magnet_settings.magnets[bba_magnet_info.magnet_name].length + quad_dk_h = quad_dkl_h / bba_magnet_length + quad_dk_v = quad_dkl_v / bba_magnet_length + else: + quad_dk_h = quad_dkl_h + quad_dk_v = quad_dkl_v bpm_name = SC.bpm_system.names[bpm_number] config[bpm_name] = {'index': bpm_index, @@ -165,10 +187,13 @@ def get_orbit(): bpm_pos[i_corr, 0] = orbit_main_down[bpm_number] bpm_pos[i_corr, 1] = orbit_main_up[bpm_number] - if quad.split('/')[-1] == 'B2': + info = SC.magnet_settings.controls[quad].info + assert type(info) is IndivControl + assert info.order == 2 + if info.component == 'B': orbits[i_corr, 0, :] = orbit_main_down - orbit_main_center orbits[i_corr, 1, :] = orbit_main_up - orbit_main_center - elif quad.split('/')[-1] == 'A2': ## skew quad + elif info.component == 'A': orbits[i_corr, 0, :] = orbit_other_down - orbit_other_center orbits[i_corr, 1, :] = orbit_other_up - orbit_other_center else: diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index 2ec8920..7f3b9f9 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -9,6 +9,7 @@ from .chromaticity import Chromaticity from .c_minus import CMinus from .rf_tuning import RF_tuning +from ..core.control import IndivControl import numpy as np from pathlib import Path @@ -66,18 +67,42 @@ def fetch_response_matrix(self, name: str, orbit=True, n_turns=1) -> None: self.calculate_model_trajectory_response_matrix(n_turns=n_turns) return + def get_inputs_plane(self, control_names): + SC = self._parent + inputs_plane = [] + for corr in control_names: + control = SC.magnet_settings.controls[corr] + if type(control.info) is not IndivControl: + raise NotImplementedError(f'Unsupported control type for {corr} of type {type(control.info).__name__}.') + if control.info.component == 'B': + inputs_plane.append('H') + elif control.info.component == 'A': + inputs_plane.append('V') + else: + raise Exception(f'Unknown component: {control.info.component}') + return inputs_plane + def calculate_model_trajectory_response_matrix(self, n_turns=1, dkick=1e-5, save_as: str = None): RM_name = f'trajectory{n_turns}' - RM = measure_TrajectoryResponseMatrix(self._parent, n_turns=n_turns, dkick=dkick, use_design=True) - self.response_matrix[RM_name] = ResponseMatrix(RM=RM) + input_names = SC.tuning.CORR + output_names = SC.bpm_system.names * n_turns * 2 # two: one per plane and per turn + matrix = measure_TrajectoryResponseMatrix(SC, n_turns=n_turns, dkick=dkick, use_design=True) + inputs_plane = self.get_inputs_plane(SC.tuning.CORR) + + self.response_matrix[RM_name] = ResponseMatrix(matrix=matrix, output_names=output_names, + input_names=input_names, inputs_plane=inputs_plane) if save_as is not None: json.dump(self.response_matrix[RM_name].model_dump(), open(save_as, 'w')) return def calculate_model_orbit_response_matrix(self, dkick=1e-5, save_as: str = None): RM_name = 'orbit' - RM = measure_OrbitResponseMatrix(self._parent, dkick=dkick, use_design=True) - self.response_matrix[RM_name] = ResponseMatrix(RM=RM) + input_names = SC.tuning.CORR + output_names = SC.bpm_system.names * 2 # two: one per plane + matrix = measure_OrbitResponseMatrix(SC, dkick=dkick, use_design=True) + inputs_plane = self.get_inputs_plane(SC.tuning.CORR) + self.response_matrix[RM_name] = ResponseMatrix(matrix=matrix, output_names=output_names, + input_names=input_names, inputs_plane=inputs_plane) if save_as is not None: json.dump(self.response_matrix[RM_name].model_dump(), open(save_as, 'w')) return @@ -266,12 +291,15 @@ def bba_to_quad_true_offset(self, bpm_name: str, plane=None) -> Union[float, tup bpm_index = SC.bpm_system.indices[bpm_number] bpm_s = SC.lattice.twiss['s'][bpm_index] - bba_magnets = SC.tuning.bba_magnets - bba_magnets_s = get_mag_s_pos(SC, bba_magnets) + bba_magnet_controls = SC.tuning.bba_magnets + bba_magnets_s = get_mag_s_pos(SC, bba_magnet_controls) bba_magnet_number = np.argmin(np.abs(bba_magnets_s - bpm_s)) - quad = bba_magnets[bba_magnet_number] + quad = bba_magnet_controls[bba_magnet_number] + bba_control_info = SC.magnet_settings.controls[quad].info + assert type(bba_control_info) is IndivControl + bba_magnet_name = bba_control_info.magnet_name - quad_index = SC.magnet_settings.magnets[quad.split('/')[0]].sim_index + quad_index = SC.magnet_settings.magnets[bba_magnet_name].sim_index true_offset2 = SC.support_system.get_total_offset(quad_index) - SC.support_system.get_total_offset(bpm_index) if plane is None: return tuple(true_offset2) From 5d970851b97dc348dc811dc6693b1d20be808c0a Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 22 Jan 2026 09:55:20 +0100 Subject: [PATCH 25/70] missing SC --- pySC/tuning/tuning_core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index 7f3b9f9..52643fc 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -84,6 +84,7 @@ def get_inputs_plane(self, control_names): def calculate_model_trajectory_response_matrix(self, n_turns=1, dkick=1e-5, save_as: str = None): RM_name = f'trajectory{n_turns}' + SC = self._parent input_names = SC.tuning.CORR output_names = SC.bpm_system.names * n_turns * 2 # two: one per plane and per turn matrix = measure_TrajectoryResponseMatrix(SC, n_turns=n_turns, dkick=dkick, use_design=True) @@ -97,6 +98,7 @@ def calculate_model_trajectory_response_matrix(self, n_turns=1, dkick=1e-5, save def calculate_model_orbit_response_matrix(self, dkick=1e-5, save_as: str = None): RM_name = 'orbit' + SC = self._parent input_names = SC.tuning.CORR output_names = SC.bpm_system.names * 2 # two: one per plane matrix = measure_OrbitResponseMatrix(SC, dkick=dkick, use_design=True) From 496dc3b65c665f2257fb7df8818a8d33b68fc769 Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 22 Jan 2026 10:44:18 +0100 Subject: [PATCH 26/70] zersum argument missing --- pySC/tuning/tuning_core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index a4d61f8..55f46e3 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -156,7 +156,7 @@ def first_turn_transmission(SC): return - def correct_injection(self, n_turns=1, n_reps=1, method='tikhonov', parameter=100, gain=1, correct_to_first_turn=False): + def correct_injection(self, n_turns=1, n_reps=1, method='tikhonov', parameter=100, gain=1, correct_to_first_turn=False, zerosum=False): RM_name = f'trajectory{n_turns}' self.fetch_response_matrix(RM_name, orbit=False, n_turns=n_turns) RM = self.response_matrix[RM_name] From 01a838d518f4f5683f6baba9a710a2d334ea5c5f Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 22 Jan 2026 11:13:41 +0100 Subject: [PATCH 27/70] remove actions --- .github/workflows/coverage.yml | 68 ----------------------------- .github/workflows/documentation.yml | 62 -------------------------- .github/workflows/tests.yml | 39 ----------------- 3 files changed, 169 deletions(-) delete mode 100644 .github/workflows/coverage.yml delete mode 100644 .github/workflows/documentation.yml delete mode 100644 .github/workflows/tests.yml diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml deleted file mode 100644 index 0c0d634..0000000 --- a/.github/workflows/coverage.yml +++ /dev/null @@ -1,68 +0,0 @@ -# Runs all tests and pushes coverage report to codeclimate -name: Coverage - -defaults: - run: - shell: bash - -on: # Runs on all push events to master branch and any push related to a pull request - push: - branches: - - master - pull_request: # so that codeclimate gets coverage and reports on the diff - -jobs: - coverage: - name: ${{ matrix.os }} / ${{ matrix.python-version }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-24.04] - python-version: [3.12] - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: '**/pyproject.toml' - - - name: Upgrade pip, setuptools and wheel - run: python -m pip install --upgrade pip setuptools wheel - - - name: Install package - run: python -m pip install '.[test]' - - - name: Set up env for CodeClimate (push) - run: | - echo "GIT_BRANCH=${GITHUB_REF/refs\/heads\//}" >> $GITHUB_ENV - echo "GIT_COMMIT_SHA=$GITHUB_SHA" >> $GITHUB_ENV - if: github.event_name == 'push' - - - name: Set up env for CodeClimate (pull_request) - env: - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: | - echo "GIT_BRANCH=$GITHUB_HEAD_REF" >> $GITHUB_ENV - echo "GIT_COMMIT_SHA=$PR_HEAD_SHA" >> $GITHUB_ENV - if: github.event_name == 'pull_request' - - - name: Prepare CodeClimate binary - env: - CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} - run: | - curl -LSs 'https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64' >./cc-test-reporter; - chmod +x ./cc-test-reporter - ./cc-test-reporter before-build - - - name: Run all tests - run: python -m pytest --cov-report xml --cov=pySC/core --cov=pySC/correction --cov=pySC/lattice_properties --cov=pySC/plotting --cov=pySC/utils - - - name: Push Coverage to CodeClimate - if: ${{ success() }} # only if tests were successful - env: - CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} - run: ./cc-test-reporter after-build \ No newline at end of file diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml deleted file mode 100644 index 20c54b0..0000000 --- a/.github/workflows/documentation.yml +++ /dev/null @@ -1,62 +0,0 @@ -# Build documentation -# The build is uploaded as artifact if the triggering event is a push for a pull request -# The build is published to github pages if the triggering event is a push to the master branch (PR merge) -name: Build and upload documentation - -defaults: - run: - shell: bash - -on: # Runs on any push event in a PR or any push event to master - pull_request: - push: - branches: - - 'master' - -jobs: - documentation: - name: ${{ matrix.os }} / ${{ matrix.python-version }} - runs-on: ${{ matrix.os }} - strategy: - matrix: # only lowest supported Python on latest ubuntu - os: [ubuntu-latest] - python-version: [3.9] - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: '**/pyproject.toml' - - - name: Get full Python version - id: full-python-version - run: echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") - - - name: Upgrade pip, setuptools and wheel - run: python -m pip install --upgrade pip setuptools wheel - - - name: Install package - run: python -m pip install '.[doc]' - - - name: Build documentation - run: python -m sphinx -b html doc ./doc_build -d ./doc_build - - - name: Upload build artifacts # upload artifacts so reviewers can have a quick look without building documentation from the branch locally - uses: actions/upload-artifact@v4 - if: success() && github.event_name == 'pull_request' # only for pushes in PR - with: - name: site-build - path: doc_build - retention-days: 5 - - - name: Upload documentation to gh-pages - if: success() && github.ref == 'refs/heads/master' # only for pushes to master - uses: JamesIves/github-pages-deploy-action@3.6.2 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - BRANCH: gh-pages - FOLDER: doc_build diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index c733602..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,39 +0,0 @@ -# Runs all tests -name: Tests - -defaults: - run: - shell: bash - -on: # Runs on all push events to any branch - push: - -jobs: - tests: - name: ${{ matrix.os }} / ${{ matrix.python-version }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-22.04, ubuntu-24.04, windows-latest, macos-latest] - python-version: [3.9, "3.10", 3.11, 3.12] - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: 'pip' - cache-dependency-path: '**/pyproject.toml' - - - name: Upgrade pip, setuptools and wheel - run: | - python -m pip install --upgrade pip - pip install setuptools wheel - - - name: Install package - run: pip install '.[test]' - - - name: Run tests - run: python -m pytest From d8193eb7accbe184b7f528d921d1eb52acf69584 Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 22 Jan 2026 11:18:32 +0100 Subject: [PATCH 28/70] bump version --- pySC/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pySC/__init__.py b/pySC/__init__.py index 15b8020..3c4150f 100644 --- a/pySC/__init__.py +++ b/pySC/__init__.py @@ -6,7 +6,7 @@ """ -__version__ = "0.4.0" +__version__ = "0.5.0" from .core.new_simulated_commissioning import SimulatedCommissioning from .configuration.generation import generate_SC From 38ec14f3e6bdf00db9c073d9734fb3285bb810ad Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 26 Jan 2026 14:46:37 +0100 Subject: [PATCH 29/70] trajectory bba --- pySC/apps/bba.py | 71 ++++++++++++++++++++++++++++++++++++++++++ pySC/apps/interface.py | 5 +-- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/pySC/apps/bba.py b/pySC/apps/bba.py index 9fa2019..c14cdee 100644 --- a/pySC/apps/bba.py +++ b/pySC/apps/bba.py @@ -296,6 +296,41 @@ class BBAAnalysis(BaseModel): def analyze(cls, data: BBAData): return BBAAnalysis() +def analyze_trajectory_bba_data(data: BBAData, n_downstream: int = 20): + bpm_number = data.bpm_number + orbits = np.full((data.n0, 2, n_downstream), np.nan) + bpm_pos = np.full((data.n0, 2), np.nan) + start = bpm_number + end = bpm_number + n_downstream + for ii in range(data.n0): + if data.plane == 'X': + bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] + bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] + if data.skew_quad: + orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) + orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) + else: + orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) + orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) + + else: + bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] + bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] + if data.skew_quad: + orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) + orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) + else: + orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) + orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) + + slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) + mask_bpm_outlier = reject_bpm_outlier(orbits) + mask_slopes = reject_slopes(slopes) + mask_center = reject_center_outlier(center) + final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) + + offset, offset_err = get_offset(center, center_err, final_mask) + return offset, offset_err def analyze_bba_data(data: BBAData): bpm_number = data.bpm_number @@ -332,6 +367,42 @@ def analyze_bba_data(data: BBAData): offset, offset_err = get_offset(center, center_err, final_mask) return offset, offset_err +def get_trajectory_bba_analysis_data(data: BBAData, n_downstream: int = 20): + bpm_number = data.bpm_number + nbpms = len(data.raw_bpm_x_center[0]) + orbits = np.full((data.n0, 2, n_downstream), np.nan) + bpm_pos = np.full((data.n0, 2), np.nan) + start = bpm_number + end = bpm_number + n_downstream + for ii in range(data.n0): + if data.plane == 'X': + bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] + bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] + if data.skew_quad: + orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) + orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) + else: + orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) + orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) + else: + bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] + bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] + if data.skew_quad: + orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) + orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) + else: + orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) + orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) + + slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) + mask_bpm_outlier = reject_bpm_outlier(orbits) + mask_slopes = reject_slopes(slopes) + mask_center = reject_center_outlier(center) + final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) + + offset, offset_err = get_offset(center, center_err, final_mask) + return bpm_pos, orbits, slopes, center, final_mask, offset + def get_bba_analysis_data(data: BBAData): bpm_number = data.bpm_number nbpms = len(data.raw_bpm_x_center[0]) diff --git a/pySC/apps/interface.py b/pySC/apps/interface.py index aa76a32..a7c15e2 100644 --- a/pySC/apps/interface.py +++ b/pySC/apps/interface.py @@ -89,9 +89,10 @@ class pySCInjectionInterface(pySCOrbitInterface): n_turns: int = 1 def get_orbit(self) -> tuple[np.ndarray, np.ndarray]: - return self.SC.bpm_system.capture_injection(n_turns=self.n_turns) + x,y= self.SC.bpm_system.capture_injection(n_turns=self.n_turns) + return x.flatten(order='F'), y.flatten(order='F') def get_ref_orbit(self) -> tuple[np.ndarray, np.ndarray]: x_ref = np.repeat(self.SC.bpm_system.reference_x[:, np.newaxis], self.n_turns, axis=1) y_ref = np.repeat(self.SC.bpm_system.reference_y[:, np.newaxis], self.n_turns, axis=1) - return x_ref, y_ref + return x_ref.flatten(order='F'), y_ref.flatten(order='F') From 112137422ffeb463c5e36fefa700d6901b04624b Mon Sep 17 00:00:00 2001 From: kparasch Date: Fri, 6 Feb 2026 11:09:43 +0100 Subject: [PATCH 30/70] fix in import error and other unused imports --- pySC/apps/dispersion.py | 2 +- pySC/apps/response.py | 3 +-- pySC/tuning/averaging.py | 2 +- pySC/tuning/rf_tuning.py | 6 ++---- pySC/tuning/tools.py | 1 - pySC/tuning/tune.py | 2 +- pySC/tuning/tuning_core.py | 2 -- 7 files changed, 6 insertions(+), 12 deletions(-) diff --git a/pySC/apps/dispersion.py b/pySC/apps/dispersion.py index f5165ac..33cda27 100644 --- a/pySC/apps/dispersion.py +++ b/pySC/apps/dispersion.py @@ -9,7 +9,7 @@ from ..utils.file_tools import dict_to_h5 from ..tuning.tools import get_average_orbit from .interface import AbstractInterface -from ..core.numpy_type import NPARRAY +from ..core.types import NPARRAY logger = logging.getLogger(__name__) diff --git a/pySC/apps/response.py b/pySC/apps/response.py index 9248fed..16aa10d 100644 --- a/pySC/apps/response.py +++ b/pySC/apps/response.py @@ -3,7 +3,6 @@ import datetime import logging import numpy as np -from enum import IntEnum from pathlib import Path from contextlib import nullcontext @@ -11,7 +10,7 @@ from ..utils.file_tools import dict_to_h5 from ..tuning.tools import get_average_orbit from .interface import AbstractInterface -from ..core.numpy_type import NPARRAY +from ..core.types import NPARRAY DISABLE_RICH = False diff --git a/pySC/tuning/averaging.py b/pySC/tuning/averaging.py index 3f8b8da..ea71733 100644 --- a/pySC/tuning/averaging.py +++ b/pySC/tuning/averaging.py @@ -1,4 +1,4 @@ -from typing import Optional, Union, TYPE_CHECKING +from typing import TYPE_CHECKING import numpy as np if TYPE_CHECKING: diff --git a/pySC/tuning/rf_tuning.py b/pySC/tuning/rf_tuning.py index dc051be..60a8c60 100644 --- a/pySC/tuning/rf_tuning.py +++ b/pySC/tuning/rf_tuning.py @@ -1,10 +1,8 @@ -from typing import Dict, Optional, Union, TYPE_CHECKING -from pydantic import BaseModel, PrivateAttr, ConfigDict +from typing import Optional, TYPE_CHECKING +from pydantic import BaseModel, PrivateAttr import numpy as np import logging -from ..core.types import NPARRAY - if TYPE_CHECKING: from .tuning_core import Tuning diff --git a/pySC/tuning/tools.py b/pySC/tuning/tools.py index 59a6a15..605656e 100644 --- a/pySC/tuning/tools.py +++ b/pySC/tuning/tools.py @@ -1,7 +1,6 @@ import numpy as np import logging from typing import Callable -from enum import IntEnum logger = logging.getLogger(__name__) diff --git a/pySC/tuning/tune.py b/pySC/tuning/tune.py index 2786153..a86d213 100644 --- a/pySC/tuning/tune.py +++ b/pySC/tuning/tune.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional, Union, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from pydantic import BaseModel, PrivateAttr, ConfigDict import numpy as np import logging diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index 55f46e3..ddd8eee 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -10,11 +10,9 @@ from .c_minus import CMinus from .rf_tuning import RF_tuning from ..core.control import IndivControl -from ..core.control import IndivControl import numpy as np from pathlib import Path -import json import logging from multiprocessing import Process, Queue From 6531d05d9b43d5143ed69ea6969eafd91ccc03c7 Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 9 Feb 2026 11:05:50 +0100 Subject: [PATCH 31/70] move response_matrix --- pySC/apps/response_matrix.py | 410 +++++++++++++++++++++++++++++++++++ pySC/tuning/c_minus.py | 10 +- 2 files changed, 415 insertions(+), 5 deletions(-) create mode 100644 pySC/apps/response_matrix.py diff --git a/pySC/apps/response_matrix.py b/pySC/apps/response_matrix.py new file mode 100644 index 0000000..d6c8443 --- /dev/null +++ b/pySC/apps/response_matrix.py @@ -0,0 +1,410 @@ +from pydantic import BaseModel, PrivateAttr, model_validator, ConfigDict +from typing import Optional, Literal +from ..core.types import NPARRAY +import numpy as np +import logging +import json + +PLANE_TYPE = Literal['H', 'V'] + +logger = logging.getLogger(__name__) + +class InverseResponseMatrix(BaseModel, extra="forbid"): + matrix: NPARRAY + method: Literal['tikhonov', 'svd_values', 'svd_cutoff', 'micado'] + parameter: float + zerosum: bool = True + rf: bool = False + + model_config = ConfigDict(arbitrary_types_allowed=True) + + def dot(self, output: np.array) -> np.array: + return np.dot(self.matrix, output) + + @property + def shape(self): + return self.matrix.shape + +class ResponseMatrix(BaseModel): + #inputs -> columns -> axis = 1 + #outputs -> rows -> axis = 0 + # here, good and bad in the names of the variables mean that bad output/input includes inside + # values which are marked to be ignored (e.g. bad bpms are bad_outputs). + matrix: NPARRAY + + input_names: Optional[list[str]] = None + output_names: Optional[list[str]] = None + inputs_plane: Optional[list[PLANE_TYPE]] = None + outputs_plane: Optional[list[PLANE_TYPE]] = None + rf_response: Optional[NPARRAY] = None + rf_weight: float = 1 + + _n_outputs: int = PrivateAttr(default=0) + _n_inputs: int = PrivateAttr(default=0) + _singular_values: Optional[NPARRAY] = PrivateAttr(default=None) + _bad_outputs: list[int] = PrivateAttr(default=[]) + _bad_inputs: list[int] = PrivateAttr(default=[]) + + _output_mask: Optional[NPARRAY] = PrivateAttr(default=None) + _input_mask: Optional[NPARRAY] = PrivateAttr(default=None) + _inverse_RM: Optional[InverseResponseMatrix] = PrivateAttr(default=None) + _inverse_RM_H: Optional[InverseResponseMatrix] = PrivateAttr(default=None) + _inverse_RM_V: Optional[InverseResponseMatrix] = PrivateAttr(default=None) + + model_config = ConfigDict(arbitrary_types_allowed=True) + + @property + def RM(self): + logger.warning('ResponseMatrix.RM is deprecated! Please use ResponseMatrix.matrix instead.') + return self.matrix + + @model_validator(mode='after') + def initialize_and_check(self): + self._n_outputs, self._n_inputs = self.matrix.shape + try: + self._singular_values = np.linalg.svd(self.matrix, compute_uv=False) + except np.linalg.LinAlgError: + logger.warning('SVD of the response matrix failed, correction will be impossible.') + self._singular_values = None + self.make_masks() + + if self.inputs_plane is None: + Nh = self._n_inputs // 2 + if Nh % 2 != 0: + logger.warning('Plane of inputs is undefined and number of inputs in response matrix is not even.' + 'Misinterpretation of the input plane is guaranteed!') + self.inputs_plane = ['H'] * Nh + ['V'] * (self._n_inputs - Nh) + + if self.outputs_plane is None: + Nh = self._n_outputs // 2 + if Nh % 2 != 0: + logger.warning('Plane of outputs is undefined and number of outputs in response matrix is not even.' + 'Misinterpretation of the output plane is guaranteed!') + self.outputs_plane = ['H'] * Nh + ['V'] * (self._n_outputs - Nh) + + if self.rf_response is None: + self.rf_response = np.zeros(self._n_outputs) + else: + if len(self.rf_response) != self._n_outputs: + logger.warning(f'RF response does not have the correct length: {len(self.rf_response)} (should have been {self._n_outputs}).') + logger.warning('RF response will be removed.') + self.rf_response = np.zeros(self._n_outputs) + + return self + + @property + def singular_values(self) -> np.array: + return self._singular_values + + def set_rf_response(self, rf_response: np.array, plane=None) -> None: + assert plane is None or plane in ['H', 'V'], f"Unknown plane: {plane}." + len_rf = len(rf_response) + if plane is None: + assert len_rf == self._n_outputs, f"Incorrect rf_response length: {len_rf} (instead of {self._n_outputs})." + self.rf_response = np.array(rf_response) + else: + output_plane_mask = self.get_output_plane_mask(plane=plane) + n_plane = sum(output_plane_mask) + assert len_rf == n_plane, f"Incorrect rf_response length for plane {plane}: {len_rf} (instead of {n_plane})." + self.rf_response[output_plane_mask] = np.array(rf_response) + return + + def get_matrix_in_plane(self, plane: Optional[PLANE_TYPE] = None) -> np.array: + if plane is None: + return self.matrix + else: + output_plane_mask = self.get_output_plane_mask(plane) + input_plane_mask = self.get_input_plane_mask(plane) + return self.matrix[output_plane_mask, :][:, input_plane_mask] + + def get_input_plane_mask(self, plane: Literal[PLANE_TYPE]) -> np.array: + return np.array(self.inputs_plane) == plane + + def get_output_plane_mask(self, plane: Literal[PLANE_TYPE]) -> np.array: + return np.array(self.outputs_plane) == plane + + @property + def matrix_h(self) -> np.array: + return self.get_matrix_in_plane(plane='H') + + @property + def matrix_v(self) -> np.array: + return self.get_matrix_in_plane(plane='V') + + @property + def bad_inputs(self) -> list[int]: + return self._bad_inputs + + @bad_inputs.setter + def bad_inputs(self, bad_list: list[int]) -> None: + self._bad_inputs = bad_list.copy() + self.make_masks() + + @property + def bad_outputs(self) -> list[int]: + return self._bad_outputs + + @bad_outputs.setter + def bad_outputs(self, bad_list: list[int]) -> None: + self._bad_outputs = bad_list.copy() + self.make_masks() + + def make_masks(self): + self._inverse_RM = None # discard inverse RM, by changing bad inputs/outputs it becomes invalid + self._inverse_RM_H = None # discard inverse RM, by changing bad inputs/outputs it becomes invalid + self._inverse_RM_V = None # discard inverse RM, by changing bad inputs/outputs it becomes invalid + self._output_mask = np.ones(self._n_outputs, dtype=bool) + self._output_mask[self._bad_outputs] = False + self._input_mask = np.ones(self._n_inputs, dtype=bool) + self._input_mask[self._bad_inputs] = False + + def disable_inputs(self, inputs: list[str]): + assert self.input_names is not None, "ResponseMatrix.input_names are not defined" + for _input in inputs: + assert _input in self.input_names, f"{_input} not found in ResponseMatrix.input_names" + + bad_inputs = self.bad_inputs + self.bad_inputs = [i for i, x in enumerate(self.input_names) if (x in inputs or x in bad_inputs)] + + def enable_inputs(self, inputs: list[str]): + assert self.input_names is not None, "ResponseMatrix.input_names are not defined" + for _input in inputs: + assert _input in self.input_names, f"{_input} not found in ResponseMatrix.input_names" + + bad_inputs = self.bad_inputs + new_bad_inputs = [] + for bad_input in bad_inputs: + if self.input_names[bad_input] not in inputs: + new_bad_inputs.append(bad_input) + self.bad_inputs = new_bad_inputs + + def disable_all_inputs(self): + self.disable_inputs(self.input_names) + + def disable_all_inputs_but(self, inputs: list[str]): + self.disable_all_inputs() + self.enable_inputs(inputs) + + def enable_all_inputs(self): + self.bad_inputs = [] + + def disable_outputs(self, outputs: list[str]): + assert self.output_names is not None, "ResponseMatrix.output_names are not defined" + for _output in outputs: + assert _output in self.output_names, f"{_output} not found in ResponseMatrix.output_names" + + bad_outputs = self.bad_outputs + self.bad_outputs = [i for i, x in enumerate(self.output_names) if (x in outputs or x in bad_outputs)] + + def enable_outputs(self, outputs: list[str]): + assert self.output_names is not None, "ResponseMatrix.output_names are not defined" + for _output in outputs: + assert _output in self.output_names, f"{_output} not found in ResponseMatrix.output_names" + + bad_outputs = self.bad_outputs + new_bad_outputs = [] + for bad_output in bad_outputs: + if self.output_names[bad_output] not in outputs: + new_bad_outputs.append(bad_output) + self.bad_outputs = new_bad_outputs + + def disable_all_outputs(self): + self.disable_outputs(self.output_names) + + def disable_all_outputs_but(self, outputs: list[str]): + self.disable_all_inputs() + self.enable_inputs(outputs) + + def enable_all_outputs(self): + self.bad_outputs = [] + + def build_pseudoinverse(self, method='svd_cutoff', parameter: float = 0., zerosum: bool = False, + rf: bool = False, plane: Optional[PLANE_TYPE] = None): + logger.info(f'(Re-)Building pseudoinverse RM with {method=} and {parameter=} with {zerosum=}.') + assert plane is None or plane in ['H', 'V'], f'Unknown plane: {plane}.' + if plane is None: + matrix = self.matrix[self._output_mask, :][:, self._input_mask] + elif plane in ['H', 'V']: + output_plane_mask = self.get_output_plane_mask(plane) + input_plane_mask = self.get_input_plane_mask(plane) + tot_output_mask = np.logical_and(self._output_mask, output_plane_mask) + tot_input_mask = np.logical_and(self._input_mask, input_plane_mask) + matrix = self.matrix[tot_output_mask, :][:, tot_input_mask] + + if zerosum or rf: + rows, cols = matrix.shape + if zerosum: + rows += 1 + if rf: + cols += 1 + matrix_to_invert = np.zeros((rows, cols), dtype=float) + matrix_to_invert[:matrix.shape[0], :matrix.shape[1]] = matrix + if zerosum: + matrix_to_invert[-1, :matrix.shape[1]] + if rf: + rf_response = self.rf_response + if plane is not None: + rf_response = rf_response[tot_output_mask] # tot_output_mask will have been defined earlier always. + + matrix_to_invert[:matrix.shape[0], -1] = self.rf_weight*rf_response + else: + matrix_to_invert = matrix + + U, s_mat, Vh = np.linalg.svd(matrix_to_invert, full_matrices=False) + if method == 'svd_cutoff': + cutoff = parameter + s0 = s_mat[0] + keep = np.sum(s_mat > cutoff * s0) + d_mat = 1. / s_mat[:keep] + elif method == 'svd_values': + number_of_values_to_keep = parameter + s_mat[int(number_of_values_to_keep):] = 0 + keep = number_of_values_to_keep + d_mat = 1. / s_mat[:keep] + elif method == 'tikhonov': + alpha = parameter + d_mat = s_mat / (np.square(s_mat) + alpha**2) + keep = len(d_mat) + + #matrix_inv = np.dot(np.dot(np.transpose(Vh), np.diag(d_mat)), np.transpose(U)) + matrix_inv = np.dot(np.dot(np.transpose(Vh[:keep,:]), np.diag(d_mat)), np.transpose(U[:, :keep])) + + return InverseResponseMatrix(matrix=matrix_inv, method=method, parameter=parameter, zerosum=zerosum) + + def solve(self, output: np.array, method: str = 'svd_cutoff', parameter: float = 0., + zerosum: bool = False, rf: bool = False, plane: Optional[Literal['H', 'V']] = None) -> np.ndarray: + assert len(self.bad_outputs) != self.matrix.shape[0], 'All outputs are disabled!' + assert len(self.bad_inputs) != self.matrix.shape[1], 'All inputs are disabled!' + assert plane is None or plane in ['H', 'V'], f'Unknown plane: {plane}.' + if plane is None: + expected_shape = (self._n_inputs - len(self._bad_inputs), self._n_outputs - len(self._bad_outputs)) + else: + output_plane_mask = np.array(self.outputs_plane) == plane + input_plane_mask = np.array(self.inputs_plane) == plane + tot_output_mask = np.logical_and(self._output_mask, output_plane_mask) + tot_input_mask = np.logical_and(self._input_mask, input_plane_mask) + expected_shape = (sum(tot_input_mask), sum(tot_output_mask)) + + if zerosum: + expected_shape = (expected_shape[0], expected_shape[1] + 1) + + if rf: + expected_shape = (expected_shape[0] + 1, expected_shape[1]) + + + if method != 'micado': + if plane is None: + active_inverse_RM = self._inverse_RM + elif plane == 'H': + active_inverse_RM = self._inverse_RM_H + elif plane == 'V': + active_inverse_RM = self._inverse_RM_V + + if active_inverse_RM is None: + active_inverse_RM = self.build_pseudoinverse(method=method, parameter=parameter, zerosum=zerosum, plane=plane, rf=rf) + else: + if (active_inverse_RM.method != method or active_inverse_RM.parameter != parameter or + active_inverse_RM.zerosum != zerosum or active_inverse_RM.shape != expected_shape): + active_inverse_RM = self.build_pseudoinverse(method=method, parameter=parameter, zerosum=zerosum, plane=plane, rf=rf) + + # cache it + if plane is None: + self._inverse_RM = active_inverse_RM + elif plane == 'H': + self._inverse_RM_H = active_inverse_RM + elif plane == 'V': + self._inverse_RM_V = active_inverse_RM + + + output_plane_mask = np.ones_like(self._output_mask, dtype=bool) + if plane in ['H', 'V']: + output_plane_mask = np.array(self.outputs_plane) == plane + + bad_output = output.copy() + bad_output[np.isnan(bad_output)] = 0 + good_output = bad_output[np.logical_and(self._output_mask, output_plane_mask)] + + + if method == 'micado': + bad_input = self.micado(good_output, int(parameter), plane=plane) + if zerosum: + logger.warning('Zerosum option is incompatible with the micado method and will be ignored.') + if rf: + logger.warning('Rf option is incompatible with the micado method and will be ignored.') + else: + if active_inverse_RM.shape != expected_shape: + raise Exception('Error: shapes of Response matrix, excluding bad inputs and outputs do not match: \n' + + f'inverse RM shape = {active_inverse_RM.shape},\n' + + f'expected inputs = {expected_shape[0]},\n' + + f'expected outputs = {expected_shape[1]},') + + if zerosum: + zerosum_good_output = np.zeros(len(good_output) + 1) + zerosum_good_output[:-1] = good_output + good_input = active_inverse_RM.dot(zerosum_good_output) + else: + good_input = active_inverse_RM.dot(good_output) + + if rf: # split rf from the other inputs + rf_input = good_input[-1] + good_input = good_input[:-1] + + final_input_length = self._n_inputs + if rf: + final_input_length += 1 + + bad_input = np.zeros(final_input_length, dtype=float) + input_plane_mask = np.ones_like(self._input_mask, dtype=bool) + if plane in ['H', 'V']: + input_plane_mask = np.array(self.inputs_plane) == plane + bad_input[:self._n_inputs][np.logical_and(self._input_mask, input_plane_mask)] = good_input + + if rf: + bad_input[-1] = rf_input + return bad_input + + def micado(self, good_output: np.array, n: int, plane: Optional[PLANE_TYPE] = None) -> np.ndarray: + all_inputs = list(range(self._n_inputs)) + bad_input = np.zeros(self._n_inputs, dtype=float) + already_used_inputs = [] + if plane is None: + tot_output_mask = self._output_mask + else: + output_plane_mask = self.get_output_plane_mask(plane) + tot_output_mask = np.logical_and(self._output_mask, output_plane_mask) + input_plane_mask = self.get_input_plane_mask(plane) + all_inputs = np.array(all_inputs, dtype=int)[input_plane_mask] + good_matrix = self.matrix[tot_output_mask] + + residual = good_output.copy() + + for _ in range(n): + best_chi2 = np.inf + for ii in all_inputs: + if ii in already_used_inputs or ii in self._bad_inputs: + continue + response = good_matrix[:, ii] + trim = np.dot(response, residual) / np.dot(response, response) + chi2 = np.sum(np.square(residual - trim * response)) + if chi2 < best_chi2: + best_chi2 = chi2 + best_input = ii + best_trim = trim + already_used_inputs.append(best_input) + bad_input[best_input] = best_trim + residual -= best_trim * good_matrix[:, best_input] + + return bad_input + + @classmethod + def from_json(cls, json_filename: str) -> "ResponseMatrix": + with open(json_filename, 'r') as fp: + obj = json.load(fp) + if 'RM' in obj: ## for backwards compatibility, to be removed when RM is completely phased out + obj['matrix'] = obj['RM'] + del obj['RM'] + return cls.model_validate(obj) + + def to_json(self, json_filename: str) -> None: + with open(json_filename, 'w') as fp: + json.dump(self.model_dump(), fp) diff --git a/pySC/tuning/c_minus.py b/pySC/tuning/c_minus.py index ded4515..25993a2 100644 --- a/pySC/tuning/c_minus.py +++ b/pySC/tuning/c_minus.py @@ -5,7 +5,7 @@ from ..core.control import KnobControl, KnobData from ..utils import rdt -from .response_matrix import ResponseMatrix +from ..apps.response_matrix import ResponseMatrix if TYPE_CHECKING: from .tuning_core import Tuning @@ -43,12 +43,12 @@ def create_c_minus_knobs(self, delta: float = 1e-5) -> None: if not len(self.controls) > 0: raise Exception('c_minus.controls is empty. Please set.') - RM = np.zeros((2,len(self.controls))) + matrix = np.zeros((2,len(self.controls))) delta_c_minus = self.c_minus_response(delta=delta) - RM[0] = delta_c_minus.real - RM[1] = delta_c_minus.imag + matrix[0] = delta_c_minus.real + matrix[1] = delta_c_minus.imag - c_minus_response_matrix = ResponseMatrix(RM=RM) + c_minus_response_matrix = ResponseMatrix(matrix=matrix) iRM = c_minus_response_matrix.build_pseudoinverse().matrix c_minus_real_knob = iRM[:, 0] c_minus_imag_knob = iRM[:, 1] From e6223794b7d498792ab042b128d6392724288b32 Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 9 Feb 2026 11:24:12 +0100 Subject: [PATCH 32/70] adapt rm for coupling --- pySC/apps/response_matrix.py | 2 +- pySC/tuning/c_minus.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/pySC/apps/response_matrix.py b/pySC/apps/response_matrix.py index d6c8443..6a8f1af 100644 --- a/pySC/apps/response_matrix.py +++ b/pySC/apps/response_matrix.py @@ -5,7 +5,7 @@ import logging import json -PLANE_TYPE = Literal['H', 'V'] +PLANE_TYPE = Literal['H', 'V', 'Q', 'SQ'] logger = logging.getLogger(__name__) diff --git a/pySC/tuning/c_minus.py b/pySC/tuning/c_minus.py index 25993a2..7532378 100644 --- a/pySC/tuning/c_minus.py +++ b/pySC/tuning/c_minus.py @@ -48,10 +48,12 @@ def create_c_minus_knobs(self, delta: float = 1e-5) -> None: matrix[0] = delta_c_minus.real matrix[1] = delta_c_minus.imag - c_minus_response_matrix = ResponseMatrix(matrix=matrix) - iRM = c_minus_response_matrix.build_pseudoinverse().matrix - c_minus_real_knob = iRM[:, 0] - c_minus_imag_knob = iRM[:, 1] + c_minus_response_matrix = ResponseMatrix(matrix=matrix, + outputs_plane=['SQ'] * len(self.controls) + ) + inverse_matrix = c_minus_response_matrix.build_pseudoinverse().matrix + c_minus_real_knob = inverse_matrix[:, 0] + c_minus_imag_knob = inverse_matrix[:, 1] knob_data = KnobData(data={ self.knob_real: KnobControl(control_names=self.controls, weights=list(c_minus_real_knob)), From 2f36a3956178119e52b40395c5bab59a72590708 Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 9 Feb 2026 11:24:32 +0100 Subject: [PATCH 33/70] split interfaces --- pySC/apps/interface.py | 46 +-------------------------------- pySC/tuning/pySC_interface.py | 48 +++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 45 deletions(-) create mode 100644 pySC/tuning/pySC_interface.py diff --git a/pySC/apps/interface.py b/pySC/apps/interface.py index a7c15e2..e993b94 100644 --- a/pySC/apps/interface.py +++ b/pySC/apps/interface.py @@ -1,8 +1,7 @@ from abc import ABC -from pydantic import BaseModel, Field +from pydantic import BaseModel import numpy as np -from ..core.new_simulated_commissioning import SimulatedCommissioning def function_is_overriden(func): obj = func.__self__ @@ -53,46 +52,3 @@ def set_many(self, data: dict[str, float]): waiting time to make sure power supply is settled and eddy currents are decayed to be handled also here. ''' raise NotImplementedError - -class pySCOrbitInterface(AbstractInterface): - SC: SimulatedCommissioning = Field(repr=False) - - def get_orbit(self) -> tuple[np.ndarray, np.ndarray]: - return self.SC.bpm_system.capture_orbit() - - def get_ref_orbit(self) -> tuple[np.ndarray, np.ndarray]: - return self.SC.bpm_system.reference_x, self.SC.bpm_system.reference_y - - def get(self, name: str) -> float: - return self.SC.magnet_settings.get(name) - - def set(self, name: str, value: float): - self.SC.magnet_settings.set(name, value) - return - - def get_many(self, names: list) -> dict[str, float]: - return self.SC.magnet_settings.get_many(names) - - def set_many(self, data: dict[str, float]): - self.SC.magnet_settings.set_many(data) - return - - def get_rf_main_frequency(self) -> float: - return self.SC.rf_settings.main.frequency - - def set_rf_main_frequency(self, frequency: float): - self.SC.rf_settings.main.set_frequency(frequency) - return - -class pySCInjectionInterface(pySCOrbitInterface): - SC: SimulatedCommissioning = Field(repr=False) - n_turns: int = 1 - - def get_orbit(self) -> tuple[np.ndarray, np.ndarray]: - x,y= self.SC.bpm_system.capture_injection(n_turns=self.n_turns) - return x.flatten(order='F'), y.flatten(order='F') - - def get_ref_orbit(self) -> tuple[np.ndarray, np.ndarray]: - x_ref = np.repeat(self.SC.bpm_system.reference_x[:, np.newaxis], self.n_turns, axis=1) - y_ref = np.repeat(self.SC.bpm_system.reference_y[:, np.newaxis], self.n_turns, axis=1) - return x_ref.flatten(order='F'), y_ref.flatten(order='F') diff --git a/pySC/tuning/pySC_interface.py b/pySC/tuning/pySC_interface.py new file mode 100644 index 0000000..9e8c89f --- /dev/null +++ b/pySC/tuning/pySC_interface.py @@ -0,0 +1,48 @@ +from pydantic import Field +import numpy as np + +from ..apps.interface import AbstractInterface +from ..core.new_simulated_commissioning import SimulatedCommissioning + +class pySCOrbitInterface(AbstractInterface): + SC: SimulatedCommissioning = Field(repr=False) + + def get_orbit(self) -> tuple[np.ndarray, np.ndarray]: + return self.SC.bpm_system.capture_orbit() + + def get_ref_orbit(self) -> tuple[np.ndarray, np.ndarray]: + return self.SC.bpm_system.reference_x, self.SC.bpm_system.reference_y + + def get(self, name: str) -> float: + return self.SC.magnet_settings.get(name) + + def set(self, name: str, value: float): + self.SC.magnet_settings.set(name, value) + return + + def get_many(self, names: list) -> dict[str, float]: + return self.SC.magnet_settings.get_many(names) + + def set_many(self, data: dict[str, float]): + self.SC.magnet_settings.set_many(data) + return + + def get_rf_main_frequency(self) -> float: + return self.SC.rf_settings.main.frequency + + def set_rf_main_frequency(self, frequency: float): + self.SC.rf_settings.main.set_frequency(frequency) + return + +class pySCInjectionInterface(pySCOrbitInterface): + SC: SimulatedCommissioning = Field(repr=False) + n_turns: int = 1 + + def get_orbit(self) -> tuple[np.ndarray, np.ndarray]: + x,y= self.SC.bpm_system.capture_injection(n_turns=self.n_turns) + return x.flatten(order='F'), y.flatten(order='F') + + def get_ref_orbit(self) -> tuple[np.ndarray, np.ndarray]: + x_ref = np.repeat(self.SC.bpm_system.reference_x[:, np.newaxis], self.n_turns, axis=1) + y_ref = np.repeat(self.SC.bpm_system.reference_y[:, np.newaxis], self.n_turns, axis=1) + return x_ref.flatten(order='F'), y_ref.flatten(order='F') From 61c6ca833b8c818a10aa957f4a3425295b1f1e32 Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 9 Feb 2026 11:41:37 +0100 Subject: [PATCH 34/70] move imports of response matrix --- pySC/__init__.py | 2 +- pySC/apps/measurements.py | 2 +- pySC/tuning/response_matrix.py | 410 --------------------------------- pySC/tuning/tuning_core.py | 2 +- 4 files changed, 3 insertions(+), 413 deletions(-) delete mode 100644 pySC/tuning/response_matrix.py diff --git a/pySC/__init__.py b/pySC/__init__.py index 3c4150f..2568c00 100644 --- a/pySC/__init__.py +++ b/pySC/__init__.py @@ -10,7 +10,7 @@ from .core.new_simulated_commissioning import SimulatedCommissioning from .configuration.generation import generate_SC -from .tuning.response_matrix import ResponseMatrix +from .apps.response_matrix import ResponseMatrix import logging import sys diff --git a/pySC/apps/measurements.py b/pySC/apps/measurements.py index 6193b7b..ea91cf0 100644 --- a/pySC/apps/measurements.py +++ b/pySC/apps/measurements.py @@ -3,7 +3,7 @@ from typing import Optional, Generator, Union, Literal from pathlib import Path -from ..tuning.response_matrix import ResponseMatrix +from ..apps.response_matrix import ResponseMatrix from .bba import BBA_Measurement, BBACode from .response import ResponseMeasurement, ResponseCode from .dispersion import DispersionMeasurement, DispersionCode diff --git a/pySC/tuning/response_matrix.py b/pySC/tuning/response_matrix.py deleted file mode 100644 index d6c8443..0000000 --- a/pySC/tuning/response_matrix.py +++ /dev/null @@ -1,410 +0,0 @@ -from pydantic import BaseModel, PrivateAttr, model_validator, ConfigDict -from typing import Optional, Literal -from ..core.types import NPARRAY -import numpy as np -import logging -import json - -PLANE_TYPE = Literal['H', 'V'] - -logger = logging.getLogger(__name__) - -class InverseResponseMatrix(BaseModel, extra="forbid"): - matrix: NPARRAY - method: Literal['tikhonov', 'svd_values', 'svd_cutoff', 'micado'] - parameter: float - zerosum: bool = True - rf: bool = False - - model_config = ConfigDict(arbitrary_types_allowed=True) - - def dot(self, output: np.array) -> np.array: - return np.dot(self.matrix, output) - - @property - def shape(self): - return self.matrix.shape - -class ResponseMatrix(BaseModel): - #inputs -> columns -> axis = 1 - #outputs -> rows -> axis = 0 - # here, good and bad in the names of the variables mean that bad output/input includes inside - # values which are marked to be ignored (e.g. bad bpms are bad_outputs). - matrix: NPARRAY - - input_names: Optional[list[str]] = None - output_names: Optional[list[str]] = None - inputs_plane: Optional[list[PLANE_TYPE]] = None - outputs_plane: Optional[list[PLANE_TYPE]] = None - rf_response: Optional[NPARRAY] = None - rf_weight: float = 1 - - _n_outputs: int = PrivateAttr(default=0) - _n_inputs: int = PrivateAttr(default=0) - _singular_values: Optional[NPARRAY] = PrivateAttr(default=None) - _bad_outputs: list[int] = PrivateAttr(default=[]) - _bad_inputs: list[int] = PrivateAttr(default=[]) - - _output_mask: Optional[NPARRAY] = PrivateAttr(default=None) - _input_mask: Optional[NPARRAY] = PrivateAttr(default=None) - _inverse_RM: Optional[InverseResponseMatrix] = PrivateAttr(default=None) - _inverse_RM_H: Optional[InverseResponseMatrix] = PrivateAttr(default=None) - _inverse_RM_V: Optional[InverseResponseMatrix] = PrivateAttr(default=None) - - model_config = ConfigDict(arbitrary_types_allowed=True) - - @property - def RM(self): - logger.warning('ResponseMatrix.RM is deprecated! Please use ResponseMatrix.matrix instead.') - return self.matrix - - @model_validator(mode='after') - def initialize_and_check(self): - self._n_outputs, self._n_inputs = self.matrix.shape - try: - self._singular_values = np.linalg.svd(self.matrix, compute_uv=False) - except np.linalg.LinAlgError: - logger.warning('SVD of the response matrix failed, correction will be impossible.') - self._singular_values = None - self.make_masks() - - if self.inputs_plane is None: - Nh = self._n_inputs // 2 - if Nh % 2 != 0: - logger.warning('Plane of inputs is undefined and number of inputs in response matrix is not even.' - 'Misinterpretation of the input plane is guaranteed!') - self.inputs_plane = ['H'] * Nh + ['V'] * (self._n_inputs - Nh) - - if self.outputs_plane is None: - Nh = self._n_outputs // 2 - if Nh % 2 != 0: - logger.warning('Plane of outputs is undefined and number of outputs in response matrix is not even.' - 'Misinterpretation of the output plane is guaranteed!') - self.outputs_plane = ['H'] * Nh + ['V'] * (self._n_outputs - Nh) - - if self.rf_response is None: - self.rf_response = np.zeros(self._n_outputs) - else: - if len(self.rf_response) != self._n_outputs: - logger.warning(f'RF response does not have the correct length: {len(self.rf_response)} (should have been {self._n_outputs}).') - logger.warning('RF response will be removed.') - self.rf_response = np.zeros(self._n_outputs) - - return self - - @property - def singular_values(self) -> np.array: - return self._singular_values - - def set_rf_response(self, rf_response: np.array, plane=None) -> None: - assert plane is None or plane in ['H', 'V'], f"Unknown plane: {plane}." - len_rf = len(rf_response) - if plane is None: - assert len_rf == self._n_outputs, f"Incorrect rf_response length: {len_rf} (instead of {self._n_outputs})." - self.rf_response = np.array(rf_response) - else: - output_plane_mask = self.get_output_plane_mask(plane=plane) - n_plane = sum(output_plane_mask) - assert len_rf == n_plane, f"Incorrect rf_response length for plane {plane}: {len_rf} (instead of {n_plane})." - self.rf_response[output_plane_mask] = np.array(rf_response) - return - - def get_matrix_in_plane(self, plane: Optional[PLANE_TYPE] = None) -> np.array: - if plane is None: - return self.matrix - else: - output_plane_mask = self.get_output_plane_mask(plane) - input_plane_mask = self.get_input_plane_mask(plane) - return self.matrix[output_plane_mask, :][:, input_plane_mask] - - def get_input_plane_mask(self, plane: Literal[PLANE_TYPE]) -> np.array: - return np.array(self.inputs_plane) == plane - - def get_output_plane_mask(self, plane: Literal[PLANE_TYPE]) -> np.array: - return np.array(self.outputs_plane) == plane - - @property - def matrix_h(self) -> np.array: - return self.get_matrix_in_plane(plane='H') - - @property - def matrix_v(self) -> np.array: - return self.get_matrix_in_plane(plane='V') - - @property - def bad_inputs(self) -> list[int]: - return self._bad_inputs - - @bad_inputs.setter - def bad_inputs(self, bad_list: list[int]) -> None: - self._bad_inputs = bad_list.copy() - self.make_masks() - - @property - def bad_outputs(self) -> list[int]: - return self._bad_outputs - - @bad_outputs.setter - def bad_outputs(self, bad_list: list[int]) -> None: - self._bad_outputs = bad_list.copy() - self.make_masks() - - def make_masks(self): - self._inverse_RM = None # discard inverse RM, by changing bad inputs/outputs it becomes invalid - self._inverse_RM_H = None # discard inverse RM, by changing bad inputs/outputs it becomes invalid - self._inverse_RM_V = None # discard inverse RM, by changing bad inputs/outputs it becomes invalid - self._output_mask = np.ones(self._n_outputs, dtype=bool) - self._output_mask[self._bad_outputs] = False - self._input_mask = np.ones(self._n_inputs, dtype=bool) - self._input_mask[self._bad_inputs] = False - - def disable_inputs(self, inputs: list[str]): - assert self.input_names is not None, "ResponseMatrix.input_names are not defined" - for _input in inputs: - assert _input in self.input_names, f"{_input} not found in ResponseMatrix.input_names" - - bad_inputs = self.bad_inputs - self.bad_inputs = [i for i, x in enumerate(self.input_names) if (x in inputs or x in bad_inputs)] - - def enable_inputs(self, inputs: list[str]): - assert self.input_names is not None, "ResponseMatrix.input_names are not defined" - for _input in inputs: - assert _input in self.input_names, f"{_input} not found in ResponseMatrix.input_names" - - bad_inputs = self.bad_inputs - new_bad_inputs = [] - for bad_input in bad_inputs: - if self.input_names[bad_input] not in inputs: - new_bad_inputs.append(bad_input) - self.bad_inputs = new_bad_inputs - - def disable_all_inputs(self): - self.disable_inputs(self.input_names) - - def disable_all_inputs_but(self, inputs: list[str]): - self.disable_all_inputs() - self.enable_inputs(inputs) - - def enable_all_inputs(self): - self.bad_inputs = [] - - def disable_outputs(self, outputs: list[str]): - assert self.output_names is not None, "ResponseMatrix.output_names are not defined" - for _output in outputs: - assert _output in self.output_names, f"{_output} not found in ResponseMatrix.output_names" - - bad_outputs = self.bad_outputs - self.bad_outputs = [i for i, x in enumerate(self.output_names) if (x in outputs or x in bad_outputs)] - - def enable_outputs(self, outputs: list[str]): - assert self.output_names is not None, "ResponseMatrix.output_names are not defined" - for _output in outputs: - assert _output in self.output_names, f"{_output} not found in ResponseMatrix.output_names" - - bad_outputs = self.bad_outputs - new_bad_outputs = [] - for bad_output in bad_outputs: - if self.output_names[bad_output] not in outputs: - new_bad_outputs.append(bad_output) - self.bad_outputs = new_bad_outputs - - def disable_all_outputs(self): - self.disable_outputs(self.output_names) - - def disable_all_outputs_but(self, outputs: list[str]): - self.disable_all_inputs() - self.enable_inputs(outputs) - - def enable_all_outputs(self): - self.bad_outputs = [] - - def build_pseudoinverse(self, method='svd_cutoff', parameter: float = 0., zerosum: bool = False, - rf: bool = False, plane: Optional[PLANE_TYPE] = None): - logger.info(f'(Re-)Building pseudoinverse RM with {method=} and {parameter=} with {zerosum=}.') - assert plane is None or plane in ['H', 'V'], f'Unknown plane: {plane}.' - if plane is None: - matrix = self.matrix[self._output_mask, :][:, self._input_mask] - elif plane in ['H', 'V']: - output_plane_mask = self.get_output_plane_mask(plane) - input_plane_mask = self.get_input_plane_mask(plane) - tot_output_mask = np.logical_and(self._output_mask, output_plane_mask) - tot_input_mask = np.logical_and(self._input_mask, input_plane_mask) - matrix = self.matrix[tot_output_mask, :][:, tot_input_mask] - - if zerosum or rf: - rows, cols = matrix.shape - if zerosum: - rows += 1 - if rf: - cols += 1 - matrix_to_invert = np.zeros((rows, cols), dtype=float) - matrix_to_invert[:matrix.shape[0], :matrix.shape[1]] = matrix - if zerosum: - matrix_to_invert[-1, :matrix.shape[1]] - if rf: - rf_response = self.rf_response - if plane is not None: - rf_response = rf_response[tot_output_mask] # tot_output_mask will have been defined earlier always. - - matrix_to_invert[:matrix.shape[0], -1] = self.rf_weight*rf_response - else: - matrix_to_invert = matrix - - U, s_mat, Vh = np.linalg.svd(matrix_to_invert, full_matrices=False) - if method == 'svd_cutoff': - cutoff = parameter - s0 = s_mat[0] - keep = np.sum(s_mat > cutoff * s0) - d_mat = 1. / s_mat[:keep] - elif method == 'svd_values': - number_of_values_to_keep = parameter - s_mat[int(number_of_values_to_keep):] = 0 - keep = number_of_values_to_keep - d_mat = 1. / s_mat[:keep] - elif method == 'tikhonov': - alpha = parameter - d_mat = s_mat / (np.square(s_mat) + alpha**2) - keep = len(d_mat) - - #matrix_inv = np.dot(np.dot(np.transpose(Vh), np.diag(d_mat)), np.transpose(U)) - matrix_inv = np.dot(np.dot(np.transpose(Vh[:keep,:]), np.diag(d_mat)), np.transpose(U[:, :keep])) - - return InverseResponseMatrix(matrix=matrix_inv, method=method, parameter=parameter, zerosum=zerosum) - - def solve(self, output: np.array, method: str = 'svd_cutoff', parameter: float = 0., - zerosum: bool = False, rf: bool = False, plane: Optional[Literal['H', 'V']] = None) -> np.ndarray: - assert len(self.bad_outputs) != self.matrix.shape[0], 'All outputs are disabled!' - assert len(self.bad_inputs) != self.matrix.shape[1], 'All inputs are disabled!' - assert plane is None or plane in ['H', 'V'], f'Unknown plane: {plane}.' - if plane is None: - expected_shape = (self._n_inputs - len(self._bad_inputs), self._n_outputs - len(self._bad_outputs)) - else: - output_plane_mask = np.array(self.outputs_plane) == plane - input_plane_mask = np.array(self.inputs_plane) == plane - tot_output_mask = np.logical_and(self._output_mask, output_plane_mask) - tot_input_mask = np.logical_and(self._input_mask, input_plane_mask) - expected_shape = (sum(tot_input_mask), sum(tot_output_mask)) - - if zerosum: - expected_shape = (expected_shape[0], expected_shape[1] + 1) - - if rf: - expected_shape = (expected_shape[0] + 1, expected_shape[1]) - - - if method != 'micado': - if plane is None: - active_inverse_RM = self._inverse_RM - elif plane == 'H': - active_inverse_RM = self._inverse_RM_H - elif plane == 'V': - active_inverse_RM = self._inverse_RM_V - - if active_inverse_RM is None: - active_inverse_RM = self.build_pseudoinverse(method=method, parameter=parameter, zerosum=zerosum, plane=plane, rf=rf) - else: - if (active_inverse_RM.method != method or active_inverse_RM.parameter != parameter or - active_inverse_RM.zerosum != zerosum or active_inverse_RM.shape != expected_shape): - active_inverse_RM = self.build_pseudoinverse(method=method, parameter=parameter, zerosum=zerosum, plane=plane, rf=rf) - - # cache it - if plane is None: - self._inverse_RM = active_inverse_RM - elif plane == 'H': - self._inverse_RM_H = active_inverse_RM - elif plane == 'V': - self._inverse_RM_V = active_inverse_RM - - - output_plane_mask = np.ones_like(self._output_mask, dtype=bool) - if plane in ['H', 'V']: - output_plane_mask = np.array(self.outputs_plane) == plane - - bad_output = output.copy() - bad_output[np.isnan(bad_output)] = 0 - good_output = bad_output[np.logical_and(self._output_mask, output_plane_mask)] - - - if method == 'micado': - bad_input = self.micado(good_output, int(parameter), plane=plane) - if zerosum: - logger.warning('Zerosum option is incompatible with the micado method and will be ignored.') - if rf: - logger.warning('Rf option is incompatible with the micado method and will be ignored.') - else: - if active_inverse_RM.shape != expected_shape: - raise Exception('Error: shapes of Response matrix, excluding bad inputs and outputs do not match: \n' - + f'inverse RM shape = {active_inverse_RM.shape},\n' - + f'expected inputs = {expected_shape[0]},\n' - + f'expected outputs = {expected_shape[1]},') - - if zerosum: - zerosum_good_output = np.zeros(len(good_output) + 1) - zerosum_good_output[:-1] = good_output - good_input = active_inverse_RM.dot(zerosum_good_output) - else: - good_input = active_inverse_RM.dot(good_output) - - if rf: # split rf from the other inputs - rf_input = good_input[-1] - good_input = good_input[:-1] - - final_input_length = self._n_inputs - if rf: - final_input_length += 1 - - bad_input = np.zeros(final_input_length, dtype=float) - input_plane_mask = np.ones_like(self._input_mask, dtype=bool) - if plane in ['H', 'V']: - input_plane_mask = np.array(self.inputs_plane) == plane - bad_input[:self._n_inputs][np.logical_and(self._input_mask, input_plane_mask)] = good_input - - if rf: - bad_input[-1] = rf_input - return bad_input - - def micado(self, good_output: np.array, n: int, plane: Optional[PLANE_TYPE] = None) -> np.ndarray: - all_inputs = list(range(self._n_inputs)) - bad_input = np.zeros(self._n_inputs, dtype=float) - already_used_inputs = [] - if plane is None: - tot_output_mask = self._output_mask - else: - output_plane_mask = self.get_output_plane_mask(plane) - tot_output_mask = np.logical_and(self._output_mask, output_plane_mask) - input_plane_mask = self.get_input_plane_mask(plane) - all_inputs = np.array(all_inputs, dtype=int)[input_plane_mask] - good_matrix = self.matrix[tot_output_mask] - - residual = good_output.copy() - - for _ in range(n): - best_chi2 = np.inf - for ii in all_inputs: - if ii in already_used_inputs or ii in self._bad_inputs: - continue - response = good_matrix[:, ii] - trim = np.dot(response, residual) / np.dot(response, response) - chi2 = np.sum(np.square(residual - trim * response)) - if chi2 < best_chi2: - best_chi2 = chi2 - best_input = ii - best_trim = trim - already_used_inputs.append(best_input) - bad_input[best_input] = best_trim - residual -= best_trim * good_matrix[:, best_input] - - return bad_input - - @classmethod - def from_json(cls, json_filename: str) -> "ResponseMatrix": - with open(json_filename, 'r') as fp: - obj = json.load(fp) - if 'RM' in obj: ## for backwards compatibility, to be removed when RM is completely phased out - obj['matrix'] = obj['RM'] - del obj['RM'] - return cls.model_validate(obj) - - def to_json(self, json_filename: str) -> None: - with open(json_filename, 'w') as fp: - json.dump(self.model_dump(), fp) diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index ddd8eee..8fda871 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -1,6 +1,6 @@ from pydantic import BaseModel, PrivateAttr from typing import Optional, Union, TYPE_CHECKING -from .response_matrix import ResponseMatrix +from ..apps.response_matrix import ResponseMatrix from .response_measurements import measure_TrajectoryResponseMatrix, measure_OrbitResponseMatrix, measure_RFFrequencyOrbitResponse from .trajectory_bba import Trajectory_BBA_Configuration, trajectory_bba, get_mag_s_pos from .orbit_bba import Orbit_BBA_Configuration, orbit_bba From e29394a74e75f5a0411cf7f4e3336490888348bf Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 9 Feb 2026 14:08:57 +0100 Subject: [PATCH 35/70] trajectory response matrix measurement with app --- pySC/__init__.py | 17 +++++++++++- pySC/core/new_simulated_commissioning.py | 2 +- pySC/tuning/pySC_interface.py | 35 ++++++++++++++++-------- pySC/tuning/response_measurements.py | 34 ++++++++++------------- 4 files changed, 56 insertions(+), 32 deletions(-) diff --git a/pySC/__init__.py b/pySC/__init__.py index 2568c00..b140b2a 100644 --- a/pySC/__init__.py +++ b/pySC/__init__.py @@ -11,7 +11,7 @@ from .core.new_simulated_commissioning import SimulatedCommissioning from .configuration.generation import generate_SC from .apps.response_matrix import ResponseMatrix - +from .tuning.pySC_interface import pySCInjectionInterface, pySCOrbitInterface import logging import sys @@ -29,3 +29,18 @@ def disable_pySC_rich(): from .apps import response response_measurements.DISABLE_RICH = True response.DISABLE_RICH = True + +# This is needed to avoid circular imports. +# Firstly the type of SC is hinted to avoid importing SimulatedCommissioning: +# class pySCOrbitInterface(AbstractInterface): +# SC: "SimulatedCommissioning" = Field(repr=False) +# +# Then, the model_rebuild is triggered here to complete the pydantic model, +# and allow validation. +# for this to be triggered, one needs to import pySC or to import from pySC +# (i.e. from pySC import ...) +# to validate a pySCInjectionInterface/pySCOrbitInterface object, one should +# already have a SimulatedCommissioning object. To acquire the SimulatedCommissioning, +# the model_rebuild is "almost certainly"? triggered. +pySCInjectionInterface.model_rebuild() +pySCOrbitInterface.model_rebuild() diff --git a/pySC/core/new_simulated_commissioning.py b/pySC/core/new_simulated_commissioning.py index 3b69cd3..8d53ca5 100644 --- a/pySC/core/new_simulated_commissioning.py +++ b/pySC/core/new_simulated_commissioning.py @@ -111,4 +111,4 @@ def import_knob(self, json_filename: str) -> None: tdata = knob_data.data[knob_name] self.magnet_settings.add_knob(knob_name=knob_name, control_names=tdata.control_names, weights=tdata.weights) self.design_magnet_settings.add_knob(knob_name=knob_name, control_names=tdata.control_names, weights=tdata.weights) - logger.info(f'Imported knob {knob_name} with sum(|weights|) = {np.sum(np.abs(tdata.weights)):.2e}') \ No newline at end of file + logger.info(f'Imported knob {knob_name} with sum(|weights|) = {np.sum(np.abs(tdata.weights)):.2e}') diff --git a/pySC/tuning/pySC_interface.py b/pySC/tuning/pySC_interface.py index 9e8c89f..3d74b21 100644 --- a/pySC/tuning/pySC_interface.py +++ b/pySC/tuning/pySC_interface.py @@ -1,45 +1,58 @@ from pydantic import Field import numpy as np +from typing import TYPE_CHECKING from ..apps.interface import AbstractInterface -from ..core.new_simulated_commissioning import SimulatedCommissioning +if TYPE_CHECKING: + from ..core.new_simulated_commissioning import SimulatedCommissioning class pySCOrbitInterface(AbstractInterface): - SC: SimulatedCommissioning = Field(repr=False) + SC: "SimulatedCommissioning" = Field(repr=False) + use_design: bool = False def get_orbit(self) -> tuple[np.ndarray, np.ndarray]: - return self.SC.bpm_system.capture_orbit() + return self.SC.bpm_system.capture_orbit(use_design=self.use_design) def get_ref_orbit(self) -> tuple[np.ndarray, np.ndarray]: return self.SC.bpm_system.reference_x, self.SC.bpm_system.reference_y def get(self, name: str) -> float: - return self.SC.magnet_settings.get(name) + return self.SC.magnet_settings.get(name, use_design=self.use_design) def set(self, name: str, value: float): - self.SC.magnet_settings.set(name, value) + self.SC.magnet_settings.set(name, value, use_design=self.use_design) return def get_many(self, names: list) -> dict[str, float]: - return self.SC.magnet_settings.get_many(names) + return self.SC.magnet_settings.get_many(names, use_design=self.use_design) def set_many(self, data: dict[str, float]): - self.SC.magnet_settings.set_many(data) + self.SC.magnet_settings.set_many(data, use_design=self.use_design) return def get_rf_main_frequency(self) -> float: - return self.SC.rf_settings.main.frequency + if self.use_design: + rf_settings = self.SC.design_rf_settings + else: + rf_settings = self.SC.rf_settings + + return rf_settings.main.frequency def set_rf_main_frequency(self, frequency: float): - self.SC.rf_settings.main.set_frequency(frequency) + if self.use_design: + rf_settings = self.SC.design_rf_settings + else: + rf_settings = self.SC.rf_settings + + rf_settings.main.set_frequency(frequency) return class pySCInjectionInterface(pySCOrbitInterface): - SC: SimulatedCommissioning = Field(repr=False) + SC: "SimulatedCommissioning" = Field(repr=False) n_turns: int = 1 def get_orbit(self) -> tuple[np.ndarray, np.ndarray]: - x,y= self.SC.bpm_system.capture_injection(n_turns=self.n_turns) + x,y= self.SC.bpm_system.capture_injection(n_turns=self.n_turns, use_design=self.use_design) return x.flatten(order='F'), y.flatten(order='F') def get_ref_orbit(self) -> tuple[np.ndarray, np.ndarray]: diff --git a/pySC/tuning/response_measurements.py b/pySC/tuning/response_measurements.py index f573fee..c3aefdc 100644 --- a/pySC/tuning/response_measurements.py +++ b/pySC/tuning/response_measurements.py @@ -1,5 +1,7 @@ from typing import Union, Optional, TYPE_CHECKING import numpy as np +from .pySC_interface import pySCInjectionInterface +from ..apps import measure_ORM if TYPE_CHECKING: from ..core.new_simulated_commissioning import SimulatedCommissioning @@ -75,30 +77,24 @@ def measure_TrajectoryResponseMatrix(SC: "SimulatedCommissioning", n_turns: int ### set inputs HCORR = SC.tuning.HCORR VCORR = SC.tuning.VCORR - CORR = HCORR + VCORR + corrector_names = HCORR + VCORR - n_CORR = len(CORR) + interface = pySCInjectionInterface(SC=SC, n_turns=n_turns) + interface.use_design = use_design - if type(dkick) is float: - kicks = np.ones(n_CORR, dtype=float) * dkick - else: - assert len(dkick) == n_CORR, f'ERROR: wrong length of dkick array provided. expected {n_CORR}, got {len(dkick)}' - kicks = np.array(dkick) - - ### set function that gathers outputs - def get_orbit(): - x,y = SC.bpm_system.capture_injection(n_turns=n_turns, bba=False, subtract_reference=False, use_design=use_design) - return np.concat((x.flatten(order='F'), y.flatten(order='F'))) + generator = measure_ORM(interface=interface, corrector_names=corrector_names, + delta=dkick, bipolar=bipolar, skip_save=True) - ### specify to use "real" lattice or design - magnet_settings = SC.design_magnet_settings if use_design else SC.magnet_settings - - ### measure the response matrix - generator = response_loop(inputs=CORR, inputs_delta=kicks, get_output=get_orbit, settings=magnet_settings, normalize=normalize, bipolar=bipolar) - for RM in generator: + for code, measurement in generator: pass - return RM + data = measurement.response_data + if normalize: + matrix = data.matrix + else: + matrix = data.not_normalized_response_matrix + + return matrix def measure_OrbitResponseMatrix(SC: "SimulatedCommissioning", HCORR: Optional[list] = None, VCORR: Optional[list] = None, dkick: Union[float, list] = 1e-5, use_design: bool = False, normalize: bool = True, bipolar: bool = True): print('Calculating response matrix') From 6d44ad7624377a7089cac28d4b913d12171d1359 Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 9 Feb 2026 14:39:30 +0100 Subject: [PATCH 36/70] use app in measure orbit response matrix --- pySC/tuning/response_measurements.py | 34 ++++++++++++---------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/pySC/tuning/response_measurements.py b/pySC/tuning/response_measurements.py index c3aefdc..e962174 100644 --- a/pySC/tuning/response_measurements.py +++ b/pySC/tuning/response_measurements.py @@ -1,6 +1,6 @@ from typing import Union, Optional, TYPE_CHECKING import numpy as np -from .pySC_interface import pySCInjectionInterface +from .pySC_interface import pySCInjectionInterface, pySCOrbitInterface from ..apps import measure_ORM if TYPE_CHECKING: @@ -104,30 +104,24 @@ def measure_OrbitResponseMatrix(SC: "SimulatedCommissioning", HCORR: Optional[li HCORR = SC.tuning.HCORR if VCORR is None: VCORR = SC.tuning.VCORR - CORR = HCORR + VCORR - - n_CORR = len(CORR) - - if type(dkick) is float: - kicks = np.ones(n_CORR, dtype=float) * dkick - else: - assert len(dkick) == n_CORR, f'ERROR: wrong length of dkick array provided. expected {n_CORR}, got {len(dkick)}' - kicks = np.array(dkick) + corrector_names = HCORR + VCORR - ### set function that gathers outputs - def get_orbit(): - x,y = SC.bpm_system.capture_orbit(bba=False, subtract_reference=False, use_design=use_design) - return np.concat((x.flatten(order='F'), y.flatten(order='F'))) + interface = pySCOrbitInterface(SC=SC) + interface.use_design = use_design - ### specify to use "real" lattice or design - magnet_settings = SC.design_magnet_settings if use_design else SC.magnet_settings + generator = measure_ORM(interface=interface, corrector_names=corrector_names, + delta=dkick, bipolar=bipolar, skip_save=True) - ### measure the response matrix - generator = response_loop(inputs=CORR, inputs_delta=kicks, get_output=get_orbit, settings=magnet_settings, normalize=normalize, bipolar=bipolar) - for RM in generator: + for code, measurement in generator: pass - return RM + data = measurement.response_data + if normalize: + matrix = data.matrix + else: + matrix = data.not_normalized_response_matrix + + return matrix def measure_RFFrequencyOrbitResponse(SC: "SimulatedCommissioning", delta_frf : float = 20, rf_system_name: str = 'main', use_design: bool = False, normalize: bool = True, bipolar: bool = False): From f3a6c4463175df28a0fab28faa8c295ffdb4edf2 Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 9 Feb 2026 14:41:08 +0100 Subject: [PATCH 37/70] trim down --- pySC/tuning/response_measurements.py | 67 ---------------------------- 1 file changed, 67 deletions(-) diff --git a/pySC/tuning/response_measurements.py b/pySC/tuning/response_measurements.py index e962174..85545a9 100644 --- a/pySC/tuning/response_measurements.py +++ b/pySC/tuning/response_measurements.py @@ -6,71 +6,6 @@ if TYPE_CHECKING: from ..core.new_simulated_commissioning import SimulatedCommissioning - -DISABLE_RICH = False - -def no_rich_progress(): - from contextlib import nullcontext - progress = nullcontext() - progress.add_task = lambda *args, **kwargs: 1 - progress.update = lambda *args, **kwargs: None - progress.remove_task = lambda *args, **kwargs: None - return progress - -try: - from rich.progress import Progress, BarColumn, TextColumn, MofNCompleteColumn, TimeRemainingColumn - rich_progress = Progress( - TextColumn("[progress.description]{task.description}"), - BarColumn(), - MofNCompleteColumn(), - TimeRemainingColumn(), - ) -except ModuleNotFoundError: - rich_progress = no_rich_progress() - DISABLE_RICH = True - - -def response_loop(inputs, inputs_delta, get_output, settings, normalize=True, bipolar=False): - n_inputs = len(inputs) - - if DISABLE_RICH: - progress = no_rich_progress() - else: - progress = rich_progress - - with progress: - task_id = progress.add_task("Measuring RM", start=True, total=n_inputs) - progress.update(task_id, total=n_inputs) - - reference = get_output() - if np.any(np.isnan(reference)): - raise ValueError('Initial output is NaN. Aborting. ') - - RM = np.full((len(reference), n_inputs), np.nan) - - for i, control in enumerate(inputs): - ref_setpoint = settings.get(control) - delta = inputs_delta[i] - if bipolar: - step = delta/2 - settings.set(control, ref_setpoint - step) - reference = get_output() - else: - step = delta - settings.set(control, ref_setpoint + step) - output = get_output() - - RM[:, i] = (output - reference) - if normalize: - RM[:, i] /= delta - settings.set(control, ref_setpoint) - yield RM - progress.update(task_id, completed=i+1, description=f'Measuring response of {control}...') - progress.update(task_id, completed=n_inputs, description='Response measured.') - progress.remove_task(task_id) - - yield RM - def measure_TrajectoryResponseMatrix(SC: "SimulatedCommissioning", n_turns: int = 1, dkick: Union[float, list] = 1e-5, use_design: bool = False, normalize: bool = True, bipolar: bool = False): print('Calculating response matrix') @@ -149,5 +84,3 @@ def get_orbit(): response /= delta_frf return response - - From c9cbc51f4bf270ee2eadc137ca1ad38c0ce19699 Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 9 Feb 2026 14:52:38 +0100 Subject: [PATCH 38/70] use dispersion app --- pySC/tuning/response_measurements.py | 34 +++++++++++----------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/pySC/tuning/response_measurements.py b/pySC/tuning/response_measurements.py index 85545a9..e6172a5 100644 --- a/pySC/tuning/response_measurements.py +++ b/pySC/tuning/response_measurements.py @@ -1,7 +1,7 @@ from typing import Union, Optional, TYPE_CHECKING import numpy as np from .pySC_interface import pySCInjectionInterface, pySCOrbitInterface -from ..apps import measure_ORM +from ..apps import measure_ORM, measure_dispersion if TYPE_CHECKING: from ..core.new_simulated_commissioning import SimulatedCommissioning @@ -58,29 +58,21 @@ def measure_OrbitResponseMatrix(SC: "SimulatedCommissioning", HCORR: Optional[li return matrix -def measure_RFFrequencyOrbitResponse(SC: "SimulatedCommissioning", delta_frf : float = 20, rf_system_name: str = 'main', use_design: bool = False, normalize: bool = True, bipolar: bool = False): +def measure_RFFrequencyOrbitResponse(SC: "SimulatedCommissioning", delta_frf : float = 20, rf_system_name: str = 'main', + use_design: bool = False, normalize: bool = True, bipolar: bool = False): - rf_settings = SC.design_rf_settings if use_design else SC.rf_settings - rf_system = rf_settings.systems[rf_system_name] + interface = pySCOrbitInterface(SC=SC) + interface.use_design = use_design - ### function that gathers outputs - def get_orbit(): - x,y = SC.bpm_system.capture_orbit(bba=False, subtract_reference=False, use_design=use_design) - return np.concat((x.flatten(order='F'), y.flatten(order='F'))) + generator = measure_dispersion(interface=interface, delta=delta_frf, bipolar=bipolar, skip_save=True) - frf = rf_system.frequency - if bipolar: - step = delta_frf / 2 - rf_system.set_frequency(frf - step) - xy0 = get_orbit() - else: - step = delta_frf - xy0 = get_orbit() - rf_system.set_frequency(frf + step) - xy1 = get_orbit() - rf_system.set_frequency(frf) - response = (xy1 - xy0) + for code, measurement in generator: + pass + + data = measurement.dispersion_data if normalize: - response /= delta_frf + response = np.concatenate(data.frequency_response) + else: + response = np.concatenate(data.not_normalized_frequency_response) return response From 99afcd6c5c762a6cc564d47acd6724333bc9aac2 Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 9 Feb 2026 15:23:22 +0100 Subject: [PATCH 39/70] code formatting --- pySC/tuning/response_measurements.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pySC/tuning/response_measurements.py b/pySC/tuning/response_measurements.py index e6172a5..955ea7e 100644 --- a/pySC/tuning/response_measurements.py +++ b/pySC/tuning/response_measurements.py @@ -6,7 +6,9 @@ if TYPE_CHECKING: from ..core.new_simulated_commissioning import SimulatedCommissioning -def measure_TrajectoryResponseMatrix(SC: "SimulatedCommissioning", n_turns: int = 1, dkick: Union[float, list] = 1e-5, use_design: bool = False, normalize: bool = True, bipolar: bool = False): +def measure_TrajectoryResponseMatrix(SC: "SimulatedCommissioning", n_turns: int = 1, + dkick: Union[float, list] = 1e-5, use_design: bool = False, + normalize: bool = True, bipolar: bool = False) -> np.ndarray: print('Calculating response matrix') ### set inputs @@ -31,7 +33,9 @@ def measure_TrajectoryResponseMatrix(SC: "SimulatedCommissioning", n_turns: int return matrix -def measure_OrbitResponseMatrix(SC: "SimulatedCommissioning", HCORR: Optional[list] = None, VCORR: Optional[list] = None, dkick: Union[float, list] = 1e-5, use_design: bool = False, normalize: bool = True, bipolar: bool = True): +def measure_OrbitResponseMatrix(SC: "SimulatedCommissioning", HCORR: Optional[list] = None, + VCORR: Optional[list] = None, dkick: Union[float, list] = 1e-5, + use_design: bool = False, normalize: bool = True, bipolar: bool = True) -> np.ndarray: print('Calculating response matrix') ### set inputs @@ -58,8 +62,8 @@ def measure_OrbitResponseMatrix(SC: "SimulatedCommissioning", HCORR: Optional[li return matrix -def measure_RFFrequencyOrbitResponse(SC: "SimulatedCommissioning", delta_frf : float = 20, rf_system_name: str = 'main', - use_design: bool = False, normalize: bool = True, bipolar: bool = False): +def measure_RFFrequencyOrbitResponse(SC: "SimulatedCommissioning", delta_frf : float = 20, use_design: bool = False, + normalize: bool = True, bipolar: bool = False) -> np.ndarray: interface = pySCOrbitInterface(SC=SC) interface.use_design = use_design From 35a42d5bdd1fc466b67c4ee971ff9c43524ff910 Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 9 Feb 2026 17:34:24 +0100 Subject: [PATCH 40/70] use logger instead of print --- pySC/tuning/response_measurements.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pySC/tuning/response_measurements.py b/pySC/tuning/response_measurements.py index 955ea7e..20c6a22 100644 --- a/pySC/tuning/response_measurements.py +++ b/pySC/tuning/response_measurements.py @@ -1,15 +1,19 @@ from typing import Union, Optional, TYPE_CHECKING import numpy as np +import logging + from .pySC_interface import pySCInjectionInterface, pySCOrbitInterface from ..apps import measure_ORM, measure_dispersion if TYPE_CHECKING: from ..core.new_simulated_commissioning import SimulatedCommissioning +logger = logging.getLogger(__name__) + def measure_TrajectoryResponseMatrix(SC: "SimulatedCommissioning", n_turns: int = 1, dkick: Union[float, list] = 1e-5, use_design: bool = False, normalize: bool = True, bipolar: bool = False) -> np.ndarray: - print('Calculating response matrix') + logger.info(f'Measuring trajectory response matrix with {n_turns=}.') ### set inputs HCORR = SC.tuning.HCORR @@ -36,7 +40,7 @@ def measure_TrajectoryResponseMatrix(SC: "SimulatedCommissioning", n_turns: int def measure_OrbitResponseMatrix(SC: "SimulatedCommissioning", HCORR: Optional[list] = None, VCORR: Optional[list] = None, dkick: Union[float, list] = 1e-5, use_design: bool = False, normalize: bool = True, bipolar: bool = True) -> np.ndarray: - print('Calculating response matrix') + logger.info('Measuring orbit response matrix.') ### set inputs if HCORR is None: @@ -64,7 +68,7 @@ def measure_OrbitResponseMatrix(SC: "SimulatedCommissioning", HCORR: Optional[li def measure_RFFrequencyOrbitResponse(SC: "SimulatedCommissioning", delta_frf : float = 20, use_design: bool = False, normalize: bool = True, bipolar: bool = False) -> np.ndarray: - + logger.info('Measuring orbit response to RF frequency (dispersion).') interface = pySCOrbitInterface(SC=SC) interface.use_design = use_design From 27e627b8753544f20d066b040681c07b27fb09f6 Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 10 Feb 2026 11:13:47 +0100 Subject: [PATCH 41/70] bugfix on responsematrix warning --- pySC/apps/response_matrix.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pySC/apps/response_matrix.py b/pySC/apps/response_matrix.py index 6a8f1af..0e30e30 100644 --- a/pySC/apps/response_matrix.py +++ b/pySC/apps/response_matrix.py @@ -70,16 +70,16 @@ def initialize_and_check(self): if self.inputs_plane is None: Nh = self._n_inputs // 2 - if Nh % 2 != 0: - logger.warning('Plane of inputs is undefined and number of inputs in response matrix is not even.' - 'Misinterpretation of the input plane is guaranteed!') + if self._n_inputs % 2 != 0: + logger.warning('Plane of inputs is undefined and number of inputs in response matrix is not even. ' + 'Misinterpretation of the input plane is guaranteed!') self.inputs_plane = ['H'] * Nh + ['V'] * (self._n_inputs - Nh) if self.outputs_plane is None: Nh = self._n_outputs // 2 - if Nh % 2 != 0: - logger.warning('Plane of outputs is undefined and number of outputs in response matrix is not even.' - 'Misinterpretation of the output plane is guaranteed!') + if self._n_outputs % 2 != 0: + logger.warning('Plane of outputs is undefined and number of outputs in response matrix is not even. ' + 'Misinterpretation of the output plane is guaranteed!') self.outputs_plane = ['H'] * Nh + ['V'] * (self._n_outputs - Nh) if self.rf_response is None: From c1c2c39f55e6e655e2cdee875b4e72cc6ed9aa86 Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 10 Feb 2026 11:14:21 +0100 Subject: [PATCH 42/70] configuration on tune from yaml file and control of tune with knobs --- pySC/configuration/tuning_conf.py | 30 +++++++++++- pySC/tuning/tune.py | 78 ++++++++++++++++++++++--------- 2 files changed, 86 insertions(+), 22 deletions(-) diff --git a/pySC/configuration/tuning_conf.py b/pySC/configuration/tuning_conf.py index 01a0fd7..0c30310 100644 --- a/pySC/configuration/tuning_conf.py +++ b/pySC/configuration/tuning_conf.py @@ -1,5 +1,6 @@ from ..core.new_simulated_commissioning import SimulatedCommissioning from ..core.control import IndivControl +from .general import get_indices_and_names import numpy as np def sort_controls(SC: SimulatedCommissioning, control_names: list[str]) -> list[str]: @@ -37,7 +38,6 @@ def configure_tuning(SC: SimulatedCommissioning) -> None: HCORR = sort_controls(SC, HCORR) SC.tuning.HCORR = HCORR - # if 'sort_correctors' in tuning_conf and tuning_conf['sort_correctors']: if 'VCORR' in tuning_conf: VCORR = configure_family(SC, config_dict=tuning_conf['VCORR']) VCORR = sort_controls(SC, VCORR) @@ -56,6 +56,34 @@ def configure_tuning(SC: SimulatedCommissioning) -> None: bba_magnets = sort_controls(SC, bba_magnets) SC.tuning.bba_magnets = bba_magnets + if 'tune' in tuning_conf: + tune_conf = tuning_conf['tune'] + assert 'controls_1' in tune_conf, 'controls_1 missing from tune configuration.' + assert 'controls_2' in tune_conf, 'controls_2 missing from tune configuration.' + + control_1_conf = tune_conf['controls_1'] + assert 'regex' in control_1_conf, 'regex is missing from controls_1 in tune configuration.' + assert 'component' in control_1_conf, 'component is missing from controls_1 in tune configuration.' + component = control_1_conf['component'] + _, names = get_indices_and_names(SC, 'tune/controls_1', control_1_conf) + controls_1 = [name + '/' + component for name in names] + + if not set(controls_1).issubset(SC.magnet_settings.controls.keys()): + raise Exception(f'At least one of tune/controls_1 was not declared! ({component=})') + + control_2_conf = tune_conf['controls_2'] + assert 'component' in control_2_conf, 'component is missing from controls_2 in tune configuration.' + assert 'regex' in control_2_conf, 'regex is missing from controls_1 in tune configuration.' + component = control_2_conf['component'] + _, names = get_indices_and_names(SC, 'tune/controls_2', control_2_conf) + controls_2 = [name + '/' + component for name in names] + + if not set(controls_2).issubset(SC.magnet_settings.controls.keys()): + raise Exception(f'At least one of tune/controls_2 was not declared! ({component=})') + + SC.tuning.tune.controls_1 = controls_1 + SC.tuning.tune.controls_2 = controls_2 + if 'c_minus' in tuning_conf: c_minus_conf = tuning_conf['c_minus'] if 'controls' in c_minus_conf: diff --git a/pySC/tuning/tune.py b/pySC/tuning/tune.py index a86d213..9162d1d 100644 --- a/pySC/tuning/tune.py +++ b/pySC/tuning/tune.py @@ -4,7 +4,9 @@ import logging import scipy.optimize +from ..core.control import KnobControl, KnobData from ..core.types import NPARRAY +from ..apps.response_matrix import ResponseMatrix if TYPE_CHECKING: from .tuning_core import Tuning @@ -12,14 +14,20 @@ logger = logging.getLogger(__name__) class Tune(BaseModel, extra="forbid"): - tune_quad_controls_1: list[str] = [] - tune_quad_controls_2: list[str] = [] + knob_qx: str = 'qx_trim' + knob_qy: str = 'qy_trim' + controls_1: list[str] = [] + controls_2: list[str] = [] tune_response_matrix: Optional[NPARRAY] = None inverse_tune_response_matrix: Optional[NPARRAY] = None _parent: Optional['Tuning'] = PrivateAttr(default=None) model_config = ConfigDict(arbitrary_types_allowed=True) + @property + def controls(self) -> list[str]: + return self.controls_1 + self.controls_2 + @property def design_qx(self): return np.mod(self._parent._parent.lattice.twiss['qx'], 1) @@ -55,19 +63,45 @@ def tune_response(self, quads: list[str], dk: float = 1e-5): return dq1, dq2 def build_tune_response_matrix(self, dk: float = 1e-5) -> None: - if not len(self.tune_quad_controls_1) > 0: + if not len(self.controls_1) > 0: raise Exception('tune_quad_controls_1 is empty. Please set.') - if not len(self.tune_quad_controls_2) > 0: + if not len(self.controls_2) > 0: raise Exception('tune_quad_controls_2 is empty. Please set.') TRM = np.zeros((2,2)) - TRM[:, 0] = self.tune_response(self.tune_quad_controls_1, dk=dk) - TRM[:, 1] = self.tune_response(self.tune_quad_controls_2, dk=dk) + TRM[:, 0] = self.tune_response(self.controls_1, dk=dk) + TRM[:, 1] = self.tune_response(self.controls_2, dk=dk) iTRM = np.linalg.inv(TRM) - self.tune_response_matrix = TRM - self.inverse_tune_response_matrix = iTRM - return + #self.tune_response_matrix = TRM + #self.inverse_tune_response_matrix = iTRM + return TRM + + def create_tune_knobs(self, delta: float = 1e-5) -> None: + if not len(self.controls) > 0: + raise Exception('tune.controls_1/tune.controls_2 are empty. Please set.') + + matrix = self.build_tune_response_matrix(dk=delta) + + tune_response_matrix = ResponseMatrix(matrix=matrix) + inverse_matrix = tune_response_matrix.build_pseudoinverse().matrix + + dk1_qx, dk2_qx = inverse_matrix[:,0] + dk1_qy, dk2_qy = inverse_matrix[:,1] + + n1 = len(self.controls_1) + n2 = len(self.controls_2) + qx_weights = [float(dk1_qx)] * n1 + [float(dk2_qx)] * n2 + qy_weights = [float(dk1_qy)] * n1 + [float(dk2_qy)] * n2 + + knob_data = KnobData(data={ + self.knob_qx: KnobControl(control_names=self.controls, weights=qx_weights), + self.knob_qy: KnobControl(control_names=self.controls, weights=qy_weights) + }) + + logger.info(f"{self.knob_qx}: sum(|weights|)={np.sum(np.abs(qx_weights)):.2e}") + logger.info(f"{self.knob_qy}: sum(|weights|)={np.sum(np.abs(qy_weights)):.2e}") + return knob_data def trim_tune(self, dqx: float = 0, dqy: float = 0, use_design: bool = False) -> None: logger.warning('Deprecation: please use .trim instead of .trim_tune.') @@ -75,18 +109,20 @@ def trim_tune(self, dqx: float = 0, dqy: float = 0, use_design: bool = False) -> def trim(self, dqx: float = 0, dqy: float = 0, use_design: bool = False) -> None: SC = self._parent._parent - if self.inverse_tune_response_matrix is None: - logger.info('Did not find inverse tune response matrix. Building now.') - self.build_tune_response_matrix() - - dk1, dk2 = np.dot(self.inverse_tune_response_matrix, [dqx, dqy]) - ref_data1 = SC.magnet_settings.get_many(self.tune_quad_controls_1, use_design=use_design) - ref_data2 = SC.magnet_settings.get_many(self.tune_quad_controls_2, use_design=use_design) - data1 = {key: ref_data1[key] + dk1 for key in ref_data1.keys()} - data2 = {key: ref_data2[key] + dk2 for key in ref_data2.keys()} - - SC.magnet_settings.set_many(data1, use_design=use_design) - SC.magnet_settings.set_many(data2, use_design=use_design) + + if use_design: + assert self.knob_qx in SC.design_magnet_settings.controls.keys() + assert self.knob_qy in SC.design_magnet_settings.controls.keys() + else: + assert self.knob_qx in SC.magnet_settings.controls.keys() + assert self.knob_qy in SC.magnet_settings.controls.keys() + + dqx0 = SC.magnet_settings.get(self.knob_qx, use_design=use_design) + dqy0 = SC.magnet_settings.get(self.knob_qy, use_design=use_design) + + SC.magnet_settings.set(self.knob_qx, dqx0 + dqx, use_design=use_design) + SC.magnet_settings.set(self.knob_qy, dqy0 + dqy, use_design=use_design) + return def measure_with_kick(self, kick_px=10e-6, kick_py=10e-6, n_turns=100): From e359cb10a9bb60b81391263250ddb3e2d090f7b0 Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 10 Feb 2026 12:56:55 +0100 Subject: [PATCH 43/70] checks to not discard inverse matrix cache unnecessarily --- pySC/apps/response_matrix.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pySC/apps/response_matrix.py b/pySC/apps/response_matrix.py index 0e30e30..afedc3c 100644 --- a/pySC/apps/response_matrix.py +++ b/pySC/apps/response_matrix.py @@ -137,8 +137,9 @@ def bad_inputs(self) -> list[int]: @bad_inputs.setter def bad_inputs(self, bad_list: list[int]) -> None: - self._bad_inputs = bad_list.copy() - self.make_masks() + if self._bad_inputs != bad_list: + self._bad_inputs = bad_list.copy() + self.make_masks() @property def bad_outputs(self) -> list[int]: @@ -146,8 +147,9 @@ def bad_outputs(self) -> list[int]: @bad_outputs.setter def bad_outputs(self, bad_list: list[int]) -> None: - self._bad_outputs = bad_list.copy() - self.make_masks() + if self._bad_outputs != bad_list: + self._bad_outputs = bad_list.copy() + self.make_masks() def make_masks(self): self._inverse_RM = None # discard inverse RM, by changing bad inputs/outputs it becomes invalid From 1c7960bbbf22467300799b3745c146f22d8f4481 Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 10 Feb 2026 13:01:13 +0100 Subject: [PATCH 44/70] app for correct_injection --- pySC/tuning/tuning_core.py | 32 +++++++++++--------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index 8fda871..aedf6a1 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -10,6 +10,8 @@ from .c_minus import CMinus from .rf_tuning import RF_tuning from ..core.control import IndivControl +from .pySC_interface import pySCInjectionInterface +from ..apps import orbit_correction import numpy as np from pathlib import Path @@ -157,35 +159,23 @@ def first_turn_transmission(SC): def correct_injection(self, n_turns=1, n_reps=1, method='tikhonov', parameter=100, gain=1, correct_to_first_turn=False, zerosum=False): RM_name = f'trajectory{n_turns}' self.fetch_response_matrix(RM_name, orbit=False, n_turns=n_turns) - RM = self.response_matrix[RM_name] - RM.bad_outputs = self.bad_outputs_from_bad_bpms(self.bad_bpms, n_turns=n_turns) - - for _ in range(n_reps): - trajectory_x, trajectory_y = self._parent.bpm_system.capture_injection(n_turns=n_turns) - trajectory = np.concat((trajectory_x.flatten(order='F'), trajectory_y.flatten(order='F'))) + response_matrix = self.response_matrix[RM_name] + response_matrix.bad_outputs = self.bad_outputs_from_bad_bpms(self.bad_bpms, n_turns=n_turns) - if correct_to_first_turn: - reference = np.zeros_like(trajectory) - n_per_turn = len(trajectory) // n_turns - for iturn in range(1, n_turns): - reference[iturn*n_per_turn:(iturn+1)*n_per_turn] = trajectory[0:n_per_turn] - else: - reference = np.zeros_like(trajectory) - - trims = RM.solve(trajectory - reference, method=method, parameter=parameter, zerosum=zerosum) + SC = self._parent + interface = pySCInjectionInterface(SC=SC, n_turns=n_turns) - settings = self._parent.magnet_settings - for control_name, trim in zip(self.CORR, trims): - setpoint = settings.get(control_name=control_name) - gain * trim - settings.set(control_name=control_name, setpoint=setpoint) + for _ in range(n_reps): + trims = orbit_correction(interface=interface, response_matrix=response_matrix, reference=None, + method=method, parameter=parameter, apply=True) - trajectory_x, trajectory_y = self._parent.bpm_system.capture_injection(n_turns=n_turns) + trajectory_x, trajectory_y = SC.bpm_system.capture_injection(n_turns=n_turns) trajectory_x = trajectory_x.flatten('F') trajectory_y = trajectory_y.flatten('F') rms_x = np.nanstd(trajectory_x) * 1e6 rms_y = np.nanstd(trajectory_y) * 1e6 bad_readings = sum(np.isnan(trajectory_x)) - good_turns = (len(trajectory_x) - bad_readings) / len(self._parent.bpm_system.indices) + good_turns = (len(trajectory_x) - bad_readings) / len(SC.bpm_system.indices) logger.info(f'Corrected injection: transmission through {good_turns:.2f}/{n_turns} turns, {rms_x=:.1f} um, {rms_y=:.1f} um.') return From e72b43f180362021699db38af710c5d53d84d16a Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 10 Feb 2026 15:43:35 +0100 Subject: [PATCH 45/70] option for single plane BBA --- pySC/apps/bba.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/pySC/apps/bba.py b/pySC/apps/bba.py index c14cdee..ece318d 100644 --- a/pySC/apps/bba.py +++ b/pySC/apps/bba.py @@ -101,6 +101,7 @@ class BBA_Measurement(BaseModel): shots_per_orbit: int = 2 bipolar: bool = True quad_is_skew: bool = False + plane: str = None initial_h_k0l: Optional[float] = None initial_v_k0l: Optional[float] = None @@ -115,12 +116,12 @@ def __init__(self, **kwargs): super().__init__(**kwargs) # Initialize BBAData instances for horizontal and vertical procedures if self.h_corrector is not None: - self.H_data = BBAData(plane='X', bpm=self.bpm, quadrupole=self.quadrupole, bpm_number=self.bpm_number, + self.H_data = BBAData(plane='H', bpm=self.bpm, quadrupole=self.quadrupole, bpm_number=self.bpm_number, corrector=self.h_corrector, dk0l=self.dk0l_x, dk1l=self.dk1l_x, n0=self.n0, shots_per_orbit=self.shots_per_orbit, bipolar=self.bipolar, skew_quad=self.quad_is_skew) if self.v_corrector is not None: - self.V_data = BBAData(plane='Y', bpm=self.bpm, quadrupole=self.quadrupole, bpm_number=self.bpm_number, + self.V_data = BBAData(plane='V', bpm=self.bpm, quadrupole=self.quadrupole, bpm_number=self.bpm_number, corrector=self.v_corrector, dk0l=self.dk0l_y, dk1l=self.dk1l_y, n0=self.n0, shots_per_orbit=self.shots_per_orbit, bipolar=self.bipolar, skew_quad=self.quad_is_skew) @@ -228,7 +229,7 @@ def one_plane_loop(self, plane: str): #save data yield code_done - def generate(self, interface: AbstractInterface): + def generate(self, interface: AbstractInterface, plane: Optional[str] = None, skip_cycle: bool = False): """ step through the measurement. """ @@ -258,24 +259,25 @@ def generate(self, interface: AbstractInterface): else: dk1 = max(self.H_data.dk1l, self.V_data.dk1l) - for code in hysteresis_loop(self.quadrupole, interface, dk1, n_cycles=2, bipolar=self.bipolar): - yield code + if not skip_cycle: + for code in hysteresis_loop(self.quadrupole, interface, dk1, n_cycles=2, bipolar=self.bipolar): + yield code - if self.h_corrector is not None: + if (plane is None or plane == 'H') and self.h_corrector is not None: for code in self.one_plane_loop('H'): yield code - if self.v_corrector is not None: + if (plane is None or plane == 'V') and self.v_corrector is not None: for code in self.one_plane_loop('V'): yield code yield BBACode.DONE - def run(self, generator=None): - if generator is None: - generator = self.generate() - for code in generator: - logger.debug(f' Got code: {code}') + # def run(self, generator=None): + # if generator is None: + # generator = self.generate() + # for code in generator: + # logger.debug(f' Got code: {code}') class BBAAnalysis(BaseModel): @@ -303,7 +305,7 @@ def analyze_trajectory_bba_data(data: BBAData, n_downstream: int = 20): start = bpm_number end = bpm_number + n_downstream for ii in range(data.n0): - if data.plane == 'X': + if data.plane == 'H': bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] if data.skew_quad: @@ -338,7 +340,7 @@ def analyze_bba_data(data: BBAData): orbits = np.full((data.n0, 2, nbpms), np.nan) bpm_pos = np.full((data.n0, 2), np.nan) for ii in range(data.n0): - if data.plane == 'X': + if data.plane == 'H': bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] if data.skew_quad: @@ -375,7 +377,7 @@ def get_trajectory_bba_analysis_data(data: BBAData, n_downstream: int = 20): start = bpm_number end = bpm_number + n_downstream for ii in range(data.n0): - if data.plane == 'X': + if data.plane == 'H': bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] if data.skew_quad: @@ -409,7 +411,7 @@ def get_bba_analysis_data(data: BBAData): orbits = np.full((data.n0, 2, nbpms), np.nan) bpm_pos = np.full((data.n0, 2), np.nan) for ii in range(data.n0): - if data.plane == 'X': + if data.plane == 'H': bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] if data.skew_quad: From f3b5e902e626785f96801e6faac6684b6b5b98aa Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 10 Feb 2026 15:44:02 +0100 Subject: [PATCH 46/70] options to get raw orbit data (for bba) --- pySC/tuning/pySC_interface.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pySC/tuning/pySC_interface.py b/pySC/tuning/pySC_interface.py index 3d74b21..3620782 100644 --- a/pySC/tuning/pySC_interface.py +++ b/pySC/tuning/pySC_interface.py @@ -50,9 +50,12 @@ def set_rf_main_frequency(self, frequency: float): class pySCInjectionInterface(pySCOrbitInterface): SC: "SimulatedCommissioning" = Field(repr=False) n_turns: int = 1 + bba: bool = True + subtract_reference: bool = True def get_orbit(self) -> tuple[np.ndarray, np.ndarray]: - x,y= self.SC.bpm_system.capture_injection(n_turns=self.n_turns, use_design=self.use_design) + x,y= self.SC.bpm_system.capture_injection(n_turns=self.n_turns, use_design=self.use_design, + bba=self.bba, subtract_reference=self.subtract_reference) return x.flatten(order='F'), y.flatten(order='F') def get_ref_orbit(self) -> tuple[np.ndarray, np.ndarray]: From c8e0af20fff30061945b550b2cb351bffd3438ec Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 10 Feb 2026 15:44:23 +0100 Subject: [PATCH 47/70] single plane bba --- pySC/apps/measurements.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pySC/apps/measurements.py b/pySC/apps/measurements.py index ea91cf0..dcdd1a8 100644 --- a/pySC/apps/measurements.py +++ b/pySC/apps/measurements.py @@ -48,7 +48,9 @@ def orbit_correction(interface: AbstractInterface, response_matrix: ResponseMatr return trims def measure_bba(interface: AbstractInterface, bpm_name, config: dict, shots_per_orbit: int = 1, - n_corr_steps: int = 7, bipolar: bool = True, skip_save: bool = False, folder_to_save: Optional[Path] = None) -> Generator: + n_corr_steps: int = 7, bipolar: bool = True, skip_save: bool = False, + folder_to_save: Optional[Path] = None, plane: Optional[str] = None, + skip_cycle: bool = False) -> Generator: if folder_to_save is None: folder_to_save = Path('data') @@ -73,10 +75,10 @@ def measure_bba(interface: AbstractInterface, bpm_name, config: dict, shots_per_ n0=n_corr_steps, bpm_number=config['number'], shots_per_orbit=shots_per_orbit, - bipolar=bipolar, + bipolar=bipolar ) - generator = measurement.generate(interface=interface) + generator = measurement.generate(interface=interface, plane=plane) # run measurement loop for code in generator: From c140ddde62f9f3e0eb9479c75c65a3b30892ef7c Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 10 Feb 2026 15:44:45 +0100 Subject: [PATCH 48/70] use app for trajectory bba --- pySC/tuning/trajectory_bba.py | 96 +++++++---------------------------- 1 file changed, 17 insertions(+), 79 deletions(-) diff --git a/pySC/tuning/trajectory_bba.py b/pySC/tuning/trajectory_bba.py index 881cae5..a0ac489 100644 --- a/pySC/tuning/trajectory_bba.py +++ b/pySC/tuning/trajectory_bba.py @@ -3,6 +3,10 @@ import numpy as np import logging from ..core.control import IndivControl +from ..apps import measure_bba +from ..apps.bba import analyze_trajectory_bba_data + +from .pySC_interface import pySCInjectionInterface logger = logging.getLogger(__name__) @@ -86,7 +90,7 @@ def generate_config(cls, SC: "SimulatedCommissioning", max_dx_at_bpm = 1e-3, else: # it is a skew quadrupole component ## TODO: this is wrong if hcorr and vcorr are not the same magnets!! temp_RM = VRM[bpm_number:bpm_number+n_downstream_bpms, the_HCORR_number] - + max_response = float(np.max(np.abs(temp_RM))) if max_response < 1e-10: logger.warning(f'WARNING: very small response for BPM {SC.bpm_system.names[bpm_number]} from magnet {the_bba_magnet} and HCORR {SC.tuning.HCORR[the_HCORR_number]}') @@ -151,92 +155,26 @@ def trajectory_bba(SC: "SimulatedCommissioning", bpm_name: str, n_corr_steps: in SC.tuning.generate_trajectory_bba_config() config = SC.tuning.trajectory_bba_config.config[bpm_name] - corr = config[f'{plane}CORR'] - quad = config['QUAD'] - bpm_number = config['number'] - corr_delta_sp = config[f'{plane}CORR_delta'] - quad_delta = config[f'QUAD_dk_{plane}'] - - n1 = bpm_number + 1 - n2 = bpm_number + 1 + n_downstream_bpms - - ## define get_orbit - def get_orbit(): - x, y = SC.bpm_system.capture_injection(n_turns=2, bba=False, subtract_reference=False, use_design=False) - x = x / shots_per_trajectory - y = y / shots_per_trajectory - for i in range(shots_per_trajectory-1): - x_tmp, y_tmp = SC.bpm_system.capture_injection(n_turns=2, bba=False, subtract_reference=False, use_design=False) - x = x + x_tmp / shots_per_trajectory - y = y + y_tmp / shots_per_trajectory - - return (x.flatten(order='F'), y.flatten(order='F')) - - - ## define settings to get/set - settings = SC.magnet_settings - - bpm_pos = np.zeros([n_corr_steps, 2]) - orbits = np.zeros([n_corr_steps, 2, n_downstream_bpms]) - - corr_sp0 = settings.get(corr) - quad_sp0 = settings.get(quad) + interface = pySCInjectionInterface(SC=SC, n_turns=2, bba=False, subtract_reference=False) + generator = measure_bba(interface=interface, bpm_name=bpm_name, config=config, + shots_per_orbit=shots_per_trajectory, n_corr_steps=n_corr_steps, + bipolar=True, skip_save=True, plane=plane, skip_cycle=True) - corr_sp_array = np.linspace(-corr_delta_sp, corr_delta_sp, n_corr_steps) + corr_sp0 - for i_corr, corr_sp in enumerate(corr_sp_array): - settings.set(corr, corr_sp) - trajectory_x_center, trajectory_y_center = get_orbit() + for _, measurement in generator: + pass - settings.set(quad, quad_sp0 + quad_delta) - trajectory_x_up, trajectory_y_up = get_orbit() - - settings.set(quad, quad_sp0 - quad_delta) - trajectory_x_down, trajectory_y_down = get_orbit() - - settings.set(quad, quad_sp0) - - if plane == 'H': - trajectory_main_down = trajectory_x_down - trajectory_main_up = trajectory_x_up - trajectory_main_center = trajectory_x_center - trajectory_other_down = trajectory_y_down - trajectory_other_up = trajectory_y_up - trajectory_other_center = trajectory_y_center - else: - trajectory_main_down = trajectory_y_down - trajectory_main_up = trajectory_y_up - trajectory_main_center = trajectory_y_center - trajectory_other_down = trajectory_x_down - trajectory_other_up = trajectory_x_up - trajectory_other_center = trajectory_x_center - - bpm_pos[i_corr, 0] = trajectory_main_down[bpm_number] - bpm_pos[i_corr, 1] = trajectory_main_up[bpm_number] - if quad.split('/')[-1] == 'B2': - orbits[i_corr, 0, :] = trajectory_main_down[n1:n2] - trajectory_main_center[n1:n2] - orbits[i_corr, 1, :] = trajectory_main_up[n1:n2] - trajectory_main_center[n1:n2] - elif quad.split('/')[-1] == 'A2': ## skew quad - orbits[i_corr, 0, :] = trajectory_other_down[n1:n2] - trajectory_other_center[n1:n2] - orbits[i_corr, 1, :] = trajectory_other_up[n1:n2] - trajectory_other_center[n1:n2] - else: - raise Exception(f'Invalid magnet for BBA: {quad}') - - settings.set(corr, corr_sp0) - settings.set(quad, quad_sp0) + if plane == 'H': + data = measurement.H_data + else: + data = measurement.V_data try: - slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, quad_delta) - mask_bpm_outlier = reject_bpm_outlier(orbits) - mask_slopes = reject_slopes(slopes) - mask_center = reject_center_outlier(center) - final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) - - offset, offset_err = get_offset(center, center_err, final_mask) + offset, offset_err = analyze_trajectory_bba_data(data, n_downstream=n_downstream_bpms) except Exception as exc: print(exc) logger.warning(f'Failed to compute trajectory BBA for BPM {bpm_name}') offset, offset_err = 0, np.nan - + return offset, offset_err def reject_bpm_outlier(orbits): From 89ce44867f49a6b49de6ea6a8fbb5aa1a3afde2f Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 10 Feb 2026 17:42:31 +0100 Subject: [PATCH 49/70] app orbit correction --- pySC/tuning/tuning_core.py | 82 ++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 43 deletions(-) diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index aedf6a1..900ee03 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -10,7 +10,7 @@ from .c_minus import CMinus from .rf_tuning import RF_tuning from ..core.control import IndivControl -from .pySC_interface import pySCInjectionInterface +from .pySC_interface import pySCInjectionInterface, pySCOrbitInterface from ..apps import orbit_correction import numpy as np @@ -180,59 +180,55 @@ def correct_injection(self, n_turns=1, n_reps=1, method='tikhonov', parameter=10 return - def correct_pseudo_orbit_at_injection(self, n_turns=1, n_reps=1, method='tikhonov', parameter=100, gain=1, zerosum=False): - RM_name = 'orbit' - self.fetch_response_matrix(RM_name, orbit=True) - RM = self.response_matrix[RM_name] - RM.bad_outputs = self.bad_outputs_from_bad_bpms(self.bad_bpms) - - for _ in range(n_reps): - trajectory_x, trajectory_y = self._parent.bpm_system.capture_injection(n_turns=n_turns) - pseudo_orbit_x = np.nanmean(trajectory_x, axis=1) - pseudo_orbit_y = np.nanmean(trajectory_y, axis=1) - pseudo_orbit = np.concat((pseudo_orbit_x, pseudo_orbit_y)) - - trims = RM.solve(pseudo_orbit, method=method, parameter=parameter, zerosum=zerosum) - - settings = self._parent.magnet_settings - for control_name, trim in zip(self.CORR, trims): - setpoint = settings.get(control_name=control_name) - gain * trim - settings.set(control_name=control_name, setpoint=setpoint) - - trajectory_x, trajectory_y = self._parent.bpm_system.capture_injection(n_turns=n_turns) - trajectory_x = trajectory_x.flatten('F') - trajectory_y = trajectory_y.flatten('F') - rms_x = np.nanstd(trajectory_x) * 1e6 - rms_y = np.nanstd(trajectory_y) * 1e6 - bad_readings = sum(np.isnan(trajectory_x)) - good_turns = (len(trajectory_x) - bad_readings) / len(self._parent.bpm_system.indices) - logger.info(f'Corrected injection: transmission through {good_turns:.2f}/{n_turns} turns, {rms_x=:.1f} um, {rms_y=:.1f} um.') - - return - def correct_orbit(self, n_reps=1, method='tikhonov', parameter=100, gain=1, zerosum=False): RM_name = 'orbit' self.fetch_response_matrix(RM_name, orbit=True) - RM = self.response_matrix[RM_name] - RM.bad_outputs = self.bad_outputs_from_bad_bpms(self.bad_bpms) - - for _ in range(n_reps): - orbit_x, orbit_y = self._parent.bpm_system.capture_orbit() - orbit = np.concat((orbit_x.flatten(order='F'), orbit_y.flatten(order='F'))) + response_matrix = self.response_matrix[RM_name] + response_matrix.bad_outputs = self.bad_outputs_from_bad_bpms(self.bad_bpms) - trims = RM.solve(orbit, method=method, parameter=parameter, zerosum=zerosum) + SC = self._parent + interface = pySCOrbitInterface(SC=SC) - settings = self._parent.magnet_settings - for control_name, trim in zip(self.CORR, trims): - setpoint = settings.get(control_name=control_name) - gain * trim - settings.set(control_name=control_name, setpoint=setpoint) + for _ in range(n_reps): + trims = orbit_correction(interface=interface, response_matrix=response_matrix, reference=None, + method=method, parameter=parameter, zerosum=zerosum, apply=True) - orbit_x, orbit_y = self._parent.bpm_system.capture_orbit() + orbit_x, orbit_y = SC.bpm_system.capture_orbit() rms_x = np.nanstd(orbit_x) * 1e6 rms_y = np.nanstd(orbit_y) * 1e6 logger.info(f'Corrected orbit: {rms_x=:.1f} um, {rms_y=:.1f} um.') return + # def correct_pseudo_orbit_at_injection(self, n_turns=1, n_reps=1, method='tikhonov', parameter=100, gain=1, zerosum=False): + # RM_name = 'orbit' + # self.fetch_response_matrix(RM_name, orbit=True) + # RM = self.response_matrix[RM_name] + # RM.bad_outputs = self.bad_outputs_from_bad_bpms(self.bad_bpms) + + # for _ in range(n_reps): + # trajectory_x, trajectory_y = self._parent.bpm_system.capture_injection(n_turns=n_turns) + # pseudo_orbit_x = np.nanmean(trajectory_x, axis=1) + # pseudo_orbit_y = np.nanmean(trajectory_y, axis=1) + # pseudo_orbit = np.concat((pseudo_orbit_x, pseudo_orbit_y)) + + # trims = RM.solve(pseudo_orbit, method=method, parameter=parameter, zerosum=zerosum) + + # settings = self._parent.magnet_settings + # for control_name, trim in zip(self.CORR, trims): + # setpoint = settings.get(control_name=control_name) - gain * trim + # settings.set(control_name=control_name, setpoint=setpoint) + + # trajectory_x, trajectory_y = self._parent.bpm_system.capture_injection(n_turns=n_turns) + # trajectory_x = trajectory_x.flatten('F') + # trajectory_y = trajectory_y.flatten('F') + # rms_x = np.nanstd(trajectory_x) * 1e6 + # rms_y = np.nanstd(trajectory_y) * 1e6 + # bad_readings = sum(np.isnan(trajectory_x)) + # good_turns = (len(trajectory_x) - bad_readings) / len(self._parent.bpm_system.indices) + # logger.info(f'Corrected injection: transmission through {good_turns:.2f}/{n_turns} turns, {rms_x=:.1f} um, {rms_y=:.1f} um.') + + # return + def fit_dispersive_orbit(self): SC = self._parent response = measure_RFFrequencyOrbitResponse(SC=SC, use_design=True) From 9a6e7984459190fb7e4a832d0caeb050d577d2c6 Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 10 Feb 2026 17:47:15 +0100 Subject: [PATCH 50/70] bba and subtract_reference also in orbit interface --- pySC/tuning/pySC_interface.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pySC/tuning/pySC_interface.py b/pySC/tuning/pySC_interface.py index 3620782..2a6d6b1 100644 --- a/pySC/tuning/pySC_interface.py +++ b/pySC/tuning/pySC_interface.py @@ -9,9 +9,12 @@ class pySCOrbitInterface(AbstractInterface): SC: "SimulatedCommissioning" = Field(repr=False) use_design: bool = False + bba: bool = True + subtract_reference: bool = True def get_orbit(self) -> tuple[np.ndarray, np.ndarray]: - return self.SC.bpm_system.capture_orbit(use_design=self.use_design) + return self.SC.bpm_system.capture_orbit(use_design=self.use_design, bba=self.bba, + subtract_reference=self.subtract_reference) def get_ref_orbit(self) -> tuple[np.ndarray, np.ndarray]: return self.SC.bpm_system.reference_x, self.SC.bpm_system.reference_y @@ -50,8 +53,6 @@ def set_rf_main_frequency(self, frequency: float): class pySCInjectionInterface(pySCOrbitInterface): SC: "SimulatedCommissioning" = Field(repr=False) n_turns: int = 1 - bba: bool = True - subtract_reference: bool = True def get_orbit(self) -> tuple[np.ndarray, np.ndarray]: x,y= self.SC.bpm_system.capture_injection(n_turns=self.n_turns, use_design=self.use_design, From 2be31f24f060d1ddadf11977619cfae246a7fa87 Mon Sep 17 00:00:00 2001 From: kparasch Date: Tue, 10 Feb 2026 17:58:56 +0100 Subject: [PATCH 51/70] orbit bba to use app, and moved analysis function in apps. to tidy up analysis still --- pySC/apps/bba.py | 95 +++++++++++++++++++- pySC/tuning/orbit_bba.py | 189 +++++---------------------------------- 2 files changed, 116 insertions(+), 168 deletions(-) diff --git a/pySC/apps/bba.py b/pySC/apps/bba.py index ece318d..5db54ff 100644 --- a/pySC/apps/bba.py +++ b/pySC/apps/bba.py @@ -10,7 +10,7 @@ from ..tuning.tools import get_average_orbit from .interface import AbstractInterface -from ..tuning.orbit_bba import reject_bpm_outlier, reject_center_outlier, reject_slopes, get_slopes_center, get_offset +# from ..tuning.orbit_bba import reject_bpm_outlier, reject_center_outlier, reject_slopes, get_slopes_center, get_offset logger = logging.getLogger(__name__) @@ -298,6 +298,99 @@ class BBAAnalysis(BaseModel): def analyze(cls, data: BBAData): return BBAAnalysis() +BPM_OUTLIER = 6 # number of sigma +SLOPE_FACTOR = 0.10 # of max slope +CENTER_OUTLIER = 1 # number of sigma + +def reject_bpm_outlier(orbits): + n_k1 = orbits.shape[1] + n_bpms = orbits.shape[2] + mask = np.ones(n_bpms, dtype=bool) + for k1_step in range(n_k1): + for bpm in range(n_bpms): + data = orbits[:, k1_step, bpm] + if np.any(data - np.mean(data) > BPM_OUTLIER * np.std(data)): + mask[bpm] = False + + # n_rejections = n_bpms - np.sum(mask) + # print(f"Rejected {n_rejections}/{n_bpms} bpms for bpm outliers ( > {BPM_OUTLIER} r.m.s. )") + return mask + +def reject_slopes(slopes): + max_slope = np.nanmax(np.abs(slopes)) + mask = np.abs(slopes) > SLOPE_FACTOR * max_slope + + # n_rejections = len(slopes) - np.sum(mask) + # print(f"Rejected {n_rejections}/{len(slopes)} bpms for small slope ( < {SLOPE_FACTOR} * max(slope) )") + return mask + +def reject_center_outlier(center): + mean = np.nanmean(center) + std = np.nanstd(center) + mask = abs(center - mean) < CENTER_OUTLIER * std + + # n_rejections = len(center) - np.sum(mask) + # print(f"Rejected {n_rejections}/{len(center)} bpms for center away from mean ( > {CENTER_OUTLIER} r.m.s. )") + return mask + +def get_slopes_center(bpm_pos, orbits, dk1): + mag_vec = np.array([dk1, -dk1]) + num_downstream_bpms = orbits.shape[2] + fit_order = 1 + x = np.mean(bpm_pos, axis=1) + x_mask = ~np.isnan(x) + err = np.mean(np.std(bpm_pos[x_mask, :], axis=1)) + x = x[x_mask] + new_tmp_tra = orbits[x_mask, :, :] + + tmp_slope = np.full((new_tmp_tra.shape[0], new_tmp_tra.shape[2]), np.nan) + tmp_slope_err = np.full((new_tmp_tra.shape[0], new_tmp_tra.shape[2]), np.nan) + center = np.full((new_tmp_tra.shape[2]), np.nan) + center_err = np.full((new_tmp_tra.shape[2]), np.nan) + for i in range(new_tmp_tra.shape[0]): + for j in range(new_tmp_tra.shape[2]): + y = new_tmp_tra[i, :, j] + y_mask = ~np.isnan(y) + if np.sum(y_mask) < min(len(mag_vec), 3): + continue + # TODO once the position errors are calculated and propagated, should be used + p, pcov = np.polyfit(mag_vec[y_mask], y[y_mask], 1, w=np.ones(int(np.sum(y_mask))) / err, cov='unscaled') + tmp_slope[i, j], tmp_slope_err[i, j] = p[0], pcov[0, 0] + + slopes = np.full((new_tmp_tra.shape[2]), np.nan) + slopes_err = np.full((new_tmp_tra.shape[2]), np.nan) + for j in range(min(new_tmp_tra.shape[2], num_downstream_bpms)): + y = tmp_slope[:, j] + y_err = tmp_slope_err[:, j] + y_mask = ~np.isnan(y) + if np.sum(y_mask) <= fit_order + 1: + continue + # TODO here do odr as the x values have also measurement errors + p, pcov = np.polyfit(x[y_mask], y[y_mask], fit_order, w=1 / y_err[y_mask], cov='unscaled') + if np.abs(p[0]) < 2 * np.sqrt(pcov[0, 0]): + continue + center[j] = -p[1] / (fit_order * p[0]) # zero-crossing if linear, minimum is quadratic + center_err[j] = np.sqrt(center[j] ** 2 * (pcov[0,0]/p[0]**2 + pcov[1,1]/p[1]**2 - 2 * pcov[0, 1] / p[0] / p[1])) + slopes[j] = p[0] + slopes_err[j] = np.sqrt(pcov[0,0]) + + return slopes, slopes_err, center, center_err + +def get_offset(center, center_err, mask): + from pySC.utils import stats + try: + offset_change = stats.weighted_mean(center[mask], center_err[mask]) + offset_change_error = stats.weighted_error(center[mask]-offset_change, center_err[mask]) / np.sqrt(stats.effective_sample_size(center[mask], stats.weights_from_errors(center_err[mask]))) + except ZeroDivisionError as exc: + print(exc) + print('Failed to estimate offset!!') + print(f'Debug info: {center=}, {center_err=}, {mask=}') + print(f'Debug info: {center[mask]=}, {center_err[mask]=}') + offset_change = 0 + offset_change_error = np.nan + + return offset_change, offset_change_error + def analyze_trajectory_bba_data(data: BBAData, n_downstream: int = 20): bpm_number = data.bpm_number orbits = np.full((data.n0, 2, n_downstream), np.nan) diff --git a/pySC/tuning/orbit_bba.py b/pySC/tuning/orbit_bba.py index 8401720..2d4463d 100644 --- a/pySC/tuning/orbit_bba.py +++ b/pySC/tuning/orbit_bba.py @@ -1,7 +1,14 @@ from pydantic import BaseModel from typing import TYPE_CHECKING, Dict, Literal import numpy as np +import logging + from ..core.control import IndivControl +from .pySC_interface import pySCOrbitInterface +from ..apps import measure_bba +from ..apps.bba import analyze_bba_data + +logger = logging.getLogger(__name__) if TYPE_CHECKING: from ..core.new_simulated_commissioning import SimulatedCommissioning @@ -133,176 +140,24 @@ def orbit_bba(SC: "SimulatedCommissioning", bpm_name: str, n_corr_steps: int = 7 SC.tuning.generate_orbit_bba_config() config = SC.tuning.orbit_bba_config.config[bpm_name] - corr = config[f'{plane}CORR'] - quad = config['QUAD'] - bpm_number = config['number'] - corr_delta_sp = config[f'{plane}CORR_delta'] - quad_delta = config[f'QUAD_dk_{plane}'] - - n_bpms = len(SC.bpm_system.indices) - - ## define get_orbit - def get_orbit(): - x, y = SC.bpm_system.capture_orbit(bba=False, subtract_reference=False, use_design=False) - x = x / shots_per_orbit - y = y / shots_per_orbit - for i in range(shots_per_orbit-1): - x_tmp, y_tmp = SC.bpm_system.capture_orbit(bba=False, subtract_reference=False, use_design=False) - x = x + x_tmp / shots_per_orbit - y = y + y_tmp / shots_per_orbit - - return (x.flatten(order='F'), y.flatten(order='F')) - - - ## define settings to get/set - settings = SC.magnet_settings - - bpm_pos = np.zeros([n_corr_steps, 2]) - orbits = np.zeros([n_corr_steps, 2, n_bpms]) + interface = pySCOrbitInterface(SC=SC, n_turns=2, bba=False, subtract_reference=False) + generator = measure_bba(interface=interface, bpm_name=bpm_name, config=config, + shots_per_orbit=shots_per_orbit, n_corr_steps=n_corr_steps, + bipolar=True, skip_save=True, plane=plane, skip_cycle=True) - corr_sp0 = settings.get(corr) - quad_sp0 = settings.get(quad) + for _, measurement in generator: + pass - corr_sp_array = np.linspace(-corr_delta_sp, corr_delta_sp, n_corr_steps) + corr_sp0 - for i_corr, corr_sp in enumerate(corr_sp_array): - settings.set(corr, corr_sp) - orbit_x_center, orbit_y_center = get_orbit() - - settings.set(quad, quad_sp0 + quad_delta) - orbit_x_up, orbit_y_up = get_orbit() - - settings.set(quad, quad_sp0 - quad_delta) - orbit_x_down, orbit_y_down = get_orbit() - - settings.set(quad, quad_sp0) - - if plane == 'H': - orbit_main_down = orbit_x_down - orbit_main_up = orbit_x_up - orbit_main_center = orbit_x_center - orbit_other_down = orbit_y_down - orbit_other_up = orbit_y_up - orbit_other_center = orbit_y_center - else: - orbit_main_down = orbit_y_down - orbit_main_up = orbit_y_up - orbit_main_center = orbit_y_center - orbit_other_down = orbit_x_down - orbit_other_up = orbit_x_up - orbit_other_center = orbit_x_center - - bpm_pos[i_corr, 0] = orbit_main_down[bpm_number] - bpm_pos[i_corr, 1] = orbit_main_up[bpm_number] - info = SC.magnet_settings.controls[quad].info - assert type(info) is IndivControl - assert info.order == 2 - if info.component == 'B': - orbits[i_corr, 0, :] = orbit_main_down - orbit_main_center - orbits[i_corr, 1, :] = orbit_main_up - orbit_main_center - elif info.component == 'A': - orbits[i_corr, 0, :] = orbit_other_down - orbit_other_center - orbits[i_corr, 1, :] = orbit_other_up - orbit_other_center - else: - raise Exception(f'Invalid magnet for BBA: {quad}') + if plane == 'H': + data = measurement.H_data + else: + data = measurement.V_data - settings.set(corr, corr_sp0) - settings.set(quad, quad_sp0) - - slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, quad_delta) - mask_bpm_outlier = reject_bpm_outlier(orbits) - mask_slopes = reject_slopes(slopes) - mask_center = reject_center_outlier(center) - final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) - - offset, offset_err = get_offset(center, center_err, final_mask) - - return offset, offset_err - -def reject_bpm_outlier(orbits): - n_k1 = orbits.shape[1] - n_bpms = orbits.shape[2] - mask = np.ones(n_bpms, dtype=bool) - for k1_step in range(n_k1): - for bpm in range(n_bpms): - data = orbits[:, k1_step, bpm] - if np.any(data - np.mean(data) > BPM_OUTLIER * np.std(data)): - mask[bpm] = False - - # n_rejections = n_bpms - np.sum(mask) - # print(f"Rejected {n_rejections}/{n_bpms} bpms for bpm outliers ( > {BPM_OUTLIER} r.m.s. )") - return mask - -def reject_slopes(slopes): - max_slope = np.nanmax(np.abs(slopes)) - mask = np.abs(slopes) > SLOPE_FACTOR * max_slope - - # n_rejections = len(slopes) - np.sum(mask) - # print(f"Rejected {n_rejections}/{len(slopes)} bpms for small slope ( < {SLOPE_FACTOR} * max(slope) )") - return mask - -def reject_center_outlier(center): - mean = np.nanmean(center) - std = np.nanstd(center) - mask = abs(center - mean) < CENTER_OUTLIER * std - - # n_rejections = len(center) - np.sum(mask) - # print(f"Rejected {n_rejections}/{len(center)} bpms for center away from mean ( > {CENTER_OUTLIER} r.m.s. )") - return mask - -def get_slopes_center(bpm_pos, orbits, dk1): - mag_vec = np.array([dk1, -dk1]) - num_downstream_bpms = orbits.shape[2] - fit_order = 1 - x = np.mean(bpm_pos, axis=1) - x_mask = ~np.isnan(x) - err = np.mean(np.std(bpm_pos[x_mask, :], axis=1)) - x = x[x_mask] - new_tmp_tra = orbits[x_mask, :, :] - - tmp_slope = np.full((new_tmp_tra.shape[0], new_tmp_tra.shape[2]), np.nan) - tmp_slope_err = np.full((new_tmp_tra.shape[0], new_tmp_tra.shape[2]), np.nan) - center = np.full((new_tmp_tra.shape[2]), np.nan) - center_err = np.full((new_tmp_tra.shape[2]), np.nan) - for i in range(new_tmp_tra.shape[0]): - for j in range(new_tmp_tra.shape[2]): - y = new_tmp_tra[i, :, j] - y_mask = ~np.isnan(y) - if np.sum(y_mask) < min(len(mag_vec), 3): - continue - # TODO once the position errors are calculated and propagated, should be used - p, pcov = np.polyfit(mag_vec[y_mask], y[y_mask], 1, w=np.ones(int(np.sum(y_mask))) / err, cov='unscaled') - tmp_slope[i, j], tmp_slope_err[i, j] = p[0], pcov[0, 0] - - slopes = np.full((new_tmp_tra.shape[2]), np.nan) - slopes_err = np.full((new_tmp_tra.shape[2]), np.nan) - for j in range(min(new_tmp_tra.shape[2], num_downstream_bpms)): - y = tmp_slope[:, j] - y_err = tmp_slope_err[:, j] - y_mask = ~np.isnan(y) - if np.sum(y_mask) <= fit_order + 1: - continue - # TODO here do odr as the x values have also measurement errors - p, pcov = np.polyfit(x[y_mask], y[y_mask], fit_order, w=1 / y_err[y_mask], cov='unscaled') - if np.abs(p[0]) < 2 * np.sqrt(pcov[0, 0]): - continue - center[j] = -p[1] / (fit_order * p[0]) # zero-crossing if linear, minimum is quadratic - center_err[j] = np.sqrt(center[j] ** 2 * (pcov[0,0]/p[0]**2 + pcov[1,1]/p[1]**2 - 2 * pcov[0, 1] / p[0] / p[1])) - slopes[j] = p[0] - slopes_err[j] = np.sqrt(pcov[0,0]) - - return slopes, slopes_err, center, center_err - -def get_offset(center, center_err, mask): - from pySC.utils import stats try: - offset_change = stats.weighted_mean(center[mask], center_err[mask]) - offset_change_error = stats.weighted_error(center[mask]-offset_change, center_err[mask]) / np.sqrt(stats.effective_sample_size(center[mask], stats.weights_from_errors(center_err[mask]))) - except ZeroDivisionError as exc: + offset, offset_err = analyze_bba_data(data) + except Exception as exc: print(exc) - print('Failed to estimate offset!!') - print(f'Debug info: {center=}, {center_err=}, {mask=}') - print(f'Debug info: {center[mask]=}, {center_err[mask]=}') - offset_change = 0 - offset_change_error = np.nan + logger.warning(f'Failed to compute trajectory BBA for BPM {bpm_name}') + offset, offset_err = 0, np.nan - return offset_change, offset_change_error + return offset, offset_err From 262d6c8346b87cd9eedfa77bfd46fd7e9f766085 Mon Sep 17 00:00:00 2001 From: kparasch Date: Wed, 11 Feb 2026 10:42:17 +0100 Subject: [PATCH 52/70] moved pySC/tuning/tools.py to pySC/apps --- pySC/apps/bba.py | 2 +- pySC/apps/dispersion.py | 2 +- pySC/apps/response.py | 2 +- pySC/apps/tools.py | 21 +++++++++++++++++++++ pySC/tuning/tools.py | 19 ++++--------------- 5 files changed, 28 insertions(+), 18 deletions(-) create mode 100644 pySC/apps/tools.py diff --git a/pySC/apps/bba.py b/pySC/apps/bba.py index 5db54ff..b75191e 100644 --- a/pySC/apps/bba.py +++ b/pySC/apps/bba.py @@ -7,7 +7,7 @@ from .codes import BBACode from ..utils.file_tools import dict_to_h5 -from ..tuning.tools import get_average_orbit +from .tools import get_average_orbit from .interface import AbstractInterface # from ..tuning.orbit_bba import reject_bpm_outlier, reject_center_outlier, reject_slopes, get_slopes_center, get_offset diff --git a/pySC/apps/dispersion.py b/pySC/apps/dispersion.py index 33cda27..5f92ab8 100644 --- a/pySC/apps/dispersion.py +++ b/pySC/apps/dispersion.py @@ -7,7 +7,7 @@ from .codes import DispersionCode from ..utils.file_tools import dict_to_h5 -from ..tuning.tools import get_average_orbit +from .tools import get_average_orbit from .interface import AbstractInterface from ..core.types import NPARRAY diff --git a/pySC/apps/response.py b/pySC/apps/response.py index 16aa10d..646ac8e 100644 --- a/pySC/apps/response.py +++ b/pySC/apps/response.py @@ -8,7 +8,7 @@ from .codes import ResponseCode from ..utils.file_tools import dict_to_h5 -from ..tuning.tools import get_average_orbit +from .tools import get_average_orbit from .interface import AbstractInterface from ..core.types import NPARRAY diff --git a/pySC/apps/tools.py b/pySC/apps/tools.py new file mode 100644 index 0000000..605656e --- /dev/null +++ b/pySC/apps/tools.py @@ -0,0 +1,21 @@ +import numpy as np +import logging +from typing import Callable + +logger = logging.getLogger(__name__) + +def get_average_orbit(get_orbit: Callable, n_orbits: int = 10): + orbit_x, orbit_y = get_orbit() + all_orbit_x = np.zeros((len(orbit_x), n_orbits)) + all_orbit_y = np.zeros((len(orbit_y), n_orbits)) + + all_orbit_x[:,0] = orbit_x + all_orbit_y[:,0] = orbit_y + for ii in range(1, n_orbits): + all_orbit_x[:, ii], all_orbit_y[:, ii] = get_orbit() + + mean_orbit_x = np.mean(all_orbit_x, axis=1) + mean_orbit_y = np.mean(all_orbit_y, axis=1) + std_orbit_x = np.std(all_orbit_x, axis=1) + std_orbit_y = np.std(all_orbit_y, axis=1) + return mean_orbit_x, mean_orbit_y, std_orbit_x, std_orbit_y diff --git a/pySC/tuning/tools.py b/pySC/tuning/tools.py index 605656e..671b21e 100644 --- a/pySC/tuning/tools.py +++ b/pySC/tuning/tools.py @@ -1,21 +1,10 @@ import numpy as np import logging from typing import Callable - +from ..apps.tools import get_average_orbit as app_get_average_orbit logger = logging.getLogger(__name__) def get_average_orbit(get_orbit: Callable, n_orbits: int = 10): - orbit_x, orbit_y = get_orbit() - all_orbit_x = np.zeros((len(orbit_x), n_orbits)) - all_orbit_y = np.zeros((len(orbit_y), n_orbits)) - - all_orbit_x[:,0] = orbit_x - all_orbit_y[:,0] = orbit_y - for ii in range(1, n_orbits): - all_orbit_x[:, ii], all_orbit_y[:, ii] = get_orbit() - - mean_orbit_x = np.mean(all_orbit_x, axis=1) - mean_orbit_y = np.mean(all_orbit_y, axis=1) - std_orbit_x = np.std(all_orbit_x, axis=1) - std_orbit_y = np.std(all_orbit_y, axis=1) - return mean_orbit_x, mean_orbit_y, std_orbit_x, std_orbit_y + logger.warning('Please stop using "get_average_orbit" from pySC.tuning.tools. ' + 'Import it from pySC.apps.tools instead!') + return app_get_average_orbit(get_orbit=get_orbit, n_orbits=n_orbits) \ No newline at end of file From 26ca3d919608cadeda8e3aeba75d582e7b56a39c Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 12 Feb 2026 09:54:43 +0100 Subject: [PATCH 53/70] trim down a bit the defaults --- pySC/tuning/tuning_core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index 900ee03..53b5151 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -347,7 +347,7 @@ def do_trajectory_bba(self, bpm_names: Optional[list[str]] = None, shots_per_tra SC.bpm_system.bba_offsets_y[bpm_number] = offsets_y[ii] return offsets_x, offsets_y - def do_orbit_bba(self, bpm_names: Optional[list[str]] = None, shots_per_orbit: int = 1, skip_summary: bool = False, n_corr_steps: int = 7): + def do_orbit_bba(self, bpm_names: Optional[list[str]] = None, shots_per_orbit: int = 1, skip_summary: bool = False, n_corr_steps: int = 5): SC = self._parent if bpm_names is None: bpm_names = SC.bpm_system.names @@ -440,7 +440,7 @@ def do_parallel_trajectory_bba(self, bpm_names: Optional[list[str]] = None, shot SC.bpm_system.bba_offsets_y[bpm_number] = offsets_y[ii] return offsets_x, offsets_y - def do_parallel_orbit_bba(self, bpm_names: Optional[list[str]] = None, shots_per_orbit: int = 1, omp_num_threads: int = 2, n_corr_steps: int = 7): + def do_parallel_orbit_bba(self, bpm_names: Optional[list[str]] = None, shots_per_orbit: int = 1, omp_num_threads: int = 2, n_corr_steps: int = 5): SC = self._parent if bpm_names is None: bpm_names = SC.bpm_system.names From e3aa5f4589eaa385c1162d19c8f6667be96eefec Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 12 Feb 2026 09:55:11 +0100 Subject: [PATCH 54/70] centralized analysis for bba --- pySC/apps/bba.py | 637 +++++++++++++++++++++------------- pySC/tuning/orbit_bba.py | 16 +- pySC/tuning/trajectory_bba.py | 105 +----- 3 files changed, 416 insertions(+), 342 deletions(-) diff --git a/pySC/apps/bba.py b/pySC/apps/bba.py index b75191e..bc73c71 100644 --- a/pySC/apps/bba.py +++ b/pySC/apps/bba.py @@ -1,5 +1,5 @@ -from pydantic import BaseModel, PrivateAttr -from typing import Optional +from pydantic import BaseModel, PrivateAttr, ConfigDict +from typing import Optional, ClassVar import datetime import logging import numpy as np @@ -9,7 +9,7 @@ from ..utils.file_tools import dict_to_h5 from .tools import get_average_orbit from .interface import AbstractInterface - +from ..core.types import NPARRAY # from ..tuning.orbit_bba import reject_bpm_outlier, reject_center_outlier, reject_slopes, get_slopes_center, get_offset logger = logging.getLogger(__name__) @@ -279,255 +279,426 @@ def generate(self, interface: AbstractInterface, plane: Optional[str] = None, sk # for code in generator: # logger.debug(f' Got code: {code}') +def prep_ios(data: BBAData, n_downstream: Optional[int] = None): + bpm_number = data.bpm_number + bpm_position = np.full((data.n0), np.nan) + + if n_downstream is not None: + induced_orbit_shift = np.full((data.n0, n_downstream), np.nan) + start = bpm_number + end = bpm_number + n_downstream + else: + n_bpm = len(data.raw_bpm_x_center[0]) + induced_orbit_shift = np.full((data.n0, n_bpm), np.nan) + start = 0 + end = n_bpm + + x_up = np.array(data.raw_bpm_x_up) + y_up = np.array(data.raw_bpm_y_up) + x_center = np.array(data.raw_bpm_x_center) + y_center = np.array(data.raw_bpm_y_center) + if data.bipolar: + k1_arr = [-data.dk1l, 0, data.dk1l] + x_down = np.array(data.raw_bpm_x_down) + y_down = np.array(data.raw_bpm_y_down) + all_x = np.array([x_down[:, start:end], x_center[:,start:end], x_up[:, start:end]]) + all_y = np.array([y_down[:, start:end], y_center[:,start:end], y_up[:, start:end]]) + else: + k1_arr = [0, data.dk1l] + all_x = np.array([x_center[:,start:end], x_up[:, start:end]]) + all_y = np.array([y_center[:,start:end], y_up[:, start:end]]) -class BBAAnalysis(BaseModel): - offset: float - offset_error: float - - slopes: list[float] - centers: list[float] - - modulation: list[list[float]] - position: list[list[float]] - - rejected_outliers: int - rejected_slopes: int - rejected_centers: int - - @classmethod - def analyze(cls, data: BBAData): - return BBAAnalysis() - -BPM_OUTLIER = 6 # number of sigma -SLOPE_FACTOR = 0.10 # of max slope -CENTER_OUTLIER = 1 # number of sigma + for ii in range(data.n0): + if data.plane == 'H': + bpm_position[ii] = np.mean(all_x[:, ii, bpm_number - start]) + if data.skew_quad: + induced_orbit_shift[ii] = np.polyfit(k1_arr, all_y[:,ii], 1)[0] * data.dk1l + else: + induced_orbit_shift[ii] = np.polyfit(k1_arr, all_x[:,ii], 1)[0] * data.dk1l + else: + bpm_position[ii] = np.mean(all_y[:, ii, bpm_number - start]) + if data.skew_quad: + induced_orbit_shift[ii] = np.polyfit(k1_arr, all_x[:,ii], 1)[0] * data.dk1l + else: + induced_orbit_shift[ii] = np.polyfit(k1_arr, all_y[:,ii], 1)[0] * data.dk1l + return bpm_position, induced_orbit_shift -def reject_bpm_outlier(orbits): - n_k1 = orbits.shape[1] - n_bpms = orbits.shape[2] +def reject_bpm_outlier(induced_orbit_shift: np.ndarray, bpm_outlier_sigma: float) -> np.ndarray[bool]: + n_bpms = induced_orbit_shift.shape[1] mask = np.ones(n_bpms, dtype=bool) - for k1_step in range(n_k1): - for bpm in range(n_bpms): - data = orbits[:, k1_step, bpm] - if np.any(data - np.mean(data) > BPM_OUTLIER * np.std(data)): - mask[bpm] = False - - # n_rejections = n_bpms - np.sum(mask) - # print(f"Rejected {n_rejections}/{n_bpms} bpms for bpm outliers ( > {BPM_OUTLIER} r.m.s. )") + for bpm in range(n_bpms): + data = induced_orbit_shift[:, bpm] + if np.any(data - np.mean(data) > bpm_outlier_sigma * np.std(data)): + mask[bpm] = False return mask -def reject_slopes(slopes): +def reject_slopes(slopes: np.ndarray, slope_cutoff: float) -> np.ndarray[bool]: max_slope = np.nanmax(np.abs(slopes)) - mask = np.abs(slopes) > SLOPE_FACTOR * max_slope - - # n_rejections = len(slopes) - np.sum(mask) - # print(f"Rejected {n_rejections}/{len(slopes)} bpms for small slope ( < {SLOPE_FACTOR} * max(slope) )") + mask = np.abs(slopes) > slope_cutoff * max_slope return mask -def reject_center_outlier(center): +def reject_center_outlier(center: np.ndarray, center_cutoff: float) -> np.ndarray[bool]: mean = np.nanmean(center) std = np.nanstd(center) - mask = abs(center - mean) < CENTER_OUTLIER * std - - # n_rejections = len(center) - np.sum(mask) - # print(f"Rejected {n_rejections}/{len(center)} bpms for center away from mean ( > {CENTER_OUTLIER} r.m.s. )") + mask = abs(center - mean) < center_cutoff * std return mask -def get_slopes_center(bpm_pos, orbits, dk1): - mag_vec = np.array([dk1, -dk1]) - num_downstream_bpms = orbits.shape[2] - fit_order = 1 - x = np.mean(bpm_pos, axis=1) - x_mask = ~np.isnan(x) - err = np.mean(np.std(bpm_pos[x_mask, :], axis=1)) - x = x[x_mask] - new_tmp_tra = orbits[x_mask, :, :] - - tmp_slope = np.full((new_tmp_tra.shape[0], new_tmp_tra.shape[2]), np.nan) - tmp_slope_err = np.full((new_tmp_tra.shape[0], new_tmp_tra.shape[2]), np.nan) - center = np.full((new_tmp_tra.shape[2]), np.nan) - center_err = np.full((new_tmp_tra.shape[2]), np.nan) - for i in range(new_tmp_tra.shape[0]): - for j in range(new_tmp_tra.shape[2]): - y = new_tmp_tra[i, :, j] - y_mask = ~np.isnan(y) - if np.sum(y_mask) < min(len(mag_vec), 3): - continue - # TODO once the position errors are calculated and propagated, should be used - p, pcov = np.polyfit(mag_vec[y_mask], y[y_mask], 1, w=np.ones(int(np.sum(y_mask))) / err, cov='unscaled') - tmp_slope[i, j], tmp_slope_err[i, j] = p[0], pcov[0, 0] - - slopes = np.full((new_tmp_tra.shape[2]), np.nan) - slopes_err = np.full((new_tmp_tra.shape[2]), np.nan) - for j in range(min(new_tmp_tra.shape[2], num_downstream_bpms)): - y = tmp_slope[:, j] - y_err = tmp_slope_err[:, j] - y_mask = ~np.isnan(y) - if np.sum(y_mask) <= fit_order + 1: - continue - # TODO here do odr as the x values have also measurement errors - p, pcov = np.polyfit(x[y_mask], y[y_mask], fit_order, w=1 / y_err[y_mask], cov='unscaled') - if np.abs(p[0]) < 2 * np.sqrt(pcov[0, 0]): - continue - center[j] = -p[1] / (fit_order * p[0]) # zero-crossing if linear, minimum is quadratic - center_err[j] = np.sqrt(center[j] ** 2 * (pcov[0,0]/p[0]**2 + pcov[1,1]/p[1]**2 - 2 * pcov[0, 1] / p[0] / p[1])) - slopes[j] = p[0] - slopes_err[j] = np.sqrt(pcov[0,0]) - - return slopes, slopes_err, center, center_err - -def get_offset(center, center_err, mask): - from pySC.utils import stats - try: - offset_change = stats.weighted_mean(center[mask], center_err[mask]) - offset_change_error = stats.weighted_error(center[mask]-offset_change, center_err[mask]) / np.sqrt(stats.effective_sample_size(center[mask], stats.weights_from_errors(center_err[mask]))) - except ZeroDivisionError as exc: - print(exc) - print('Failed to estimate offset!!') - print(f'Debug info: {center=}, {center_err=}, {mask=}') - print(f'Debug info: {center[mask]=}, {center_err[mask]=}') - offset_change = 0 - offset_change_error = np.nan - - return offset_change, offset_change_error - -def analyze_trajectory_bba_data(data: BBAData, n_downstream: int = 20): - bpm_number = data.bpm_number - orbits = np.full((data.n0, 2, n_downstream), np.nan) - bpm_pos = np.full((data.n0, 2), np.nan) - start = bpm_number - end = bpm_number + n_downstream - for ii in range(data.n0): - if data.plane == 'H': - bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] - bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] - if data.skew_quad: - orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) - orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) - else: - orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) - orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) - - else: - bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] - bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] - if data.skew_quad: - orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) - orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) - else: - orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) - orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) - - slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) - mask_bpm_outlier = reject_bpm_outlier(orbits) - mask_slopes = reject_slopes(slopes) - mask_center = reject_center_outlier(center) - final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) - - offset, offset_err = get_offset(center, center_err, final_mask) - return offset, offset_err +class BBAAnalysis(BaseModel): + offset: float + offset_error: float -def analyze_bba_data(data: BBAData): - bpm_number = data.bpm_number - nbpms = len(data.raw_bpm_x_center[0]) - orbits = np.full((data.n0, 2, nbpms), np.nan) - bpm_pos = np.full((data.n0, 2), np.nan) - for ii in range(data.n0): - if data.plane == 'H': - bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] - bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] - if data.skew_quad: - orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii]) - np.array(data.raw_bpm_y_center[ii]) - orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii]) - np.array(data.raw_bpm_y_center[ii]) - else: - orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii]) - np.array(data.raw_bpm_x_center[ii]) - orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii]) - np.array(data.raw_bpm_x_center[ii]) + slopes: NPARRAY + centers: NPARRAY - else: - bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] - bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] - if data.skew_quad: - orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii]) - np.array(data.raw_bpm_x_center[ii]) - orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii]) - np.array(data.raw_bpm_x_center[ii]) - else: - orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii]) - np.array(data.raw_bpm_y_center[ii]) - orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii]) - np.array(data.raw_bpm_y_center[ii]) + slopes_err: NPARRAY + centers_err: NPARRAY - slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) - mask_bpm_outlier = reject_bpm_outlier(orbits) - mask_slopes = reject_slopes(slopes) - mask_center = reject_center_outlier(center) - final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) + induced_orbit_shift: NPARRAY + bpm_position: NPARRAY - offset, offset_err = get_offset(center, center_err, final_mask) - return offset, offset_err + mask_accepted: NPARRAY -def get_trajectory_bba_analysis_data(data: BBAData, n_downstream: int = 20): - bpm_number = data.bpm_number - nbpms = len(data.raw_bpm_x_center[0]) - orbits = np.full((data.n0, 2, n_downstream), np.nan) - bpm_pos = np.full((data.n0, 2), np.nan) - start = bpm_number - end = bpm_number + n_downstream - for ii in range(data.n0): - if data.plane == 'H': - bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] - bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] - if data.skew_quad: - orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) - orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) - else: - orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) - orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) - else: - bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] - bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] - if data.skew_quad: - orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) - orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) - else: - orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) - orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) + n_downstream: Optional[int] - slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) - mask_bpm_outlier = reject_bpm_outlier(orbits) - mask_slopes = reject_slopes(slopes) - mask_center = reject_center_outlier(center) - final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) + rejected_outliers: int + rejected_slopes: int + rejected_centers: int - offset, offset_err = get_offset(center, center_err, final_mask) - return bpm_pos, orbits, slopes, center, final_mask, offset + bpm_outlier_sigma: float + slope_cutoff: float + center_cutoff: float -def get_bba_analysis_data(data: BBAData): - bpm_number = data.bpm_number - nbpms = len(data.raw_bpm_x_center[0]) - orbits = np.full((data.n0, 2, nbpms), np.nan) - bpm_pos = np.full((data.n0, 2), np.nan) - for ii in range(data.n0): - if data.plane == 'H': - bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] - bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] - if data.skew_quad: - orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii]) - np.array(data.raw_bpm_y_center[ii]) - orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii]) - np.array(data.raw_bpm_y_center[ii]) - else: - orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii]) - np.array(data.raw_bpm_x_center[ii]) - orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii]) - np.array(data.raw_bpm_x_center[ii]) - else: - bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] - bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] - if data.skew_quad: - orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii]) - np.array(data.raw_bpm_x_center[ii]) - orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii]) - np.array(data.raw_bpm_x_center[ii]) - else: - orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii]) - np.array(data.raw_bpm_y_center[ii]) - orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii]) - np.array(data.raw_bpm_y_center[ii]) + default_bpm_outlier_sigma: ClassVar[float] = 6 # number of sigma + default_slope_cutoff: ClassVar[float] = 0.10 # of max slope + default_center_cutoff: ClassVar[int] = 1 # number of sigma - slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) - mask_bpm_outlier = reject_bpm_outlier(orbits) - mask_slopes = reject_slopes(slopes) - mask_center = reject_center_outlier(center) - final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) + model_config = ConfigDict(arbitrary_types_allowed=True) - offset, offset_err = get_offset(center, center_err, final_mask) - return bpm_pos, orbits, slopes, center, final_mask, offset \ No newline at end of file + @classmethod + def analyze(cls, data: BBAData, n_downstream: Optional[int] = None, bpm_outlier_sigma: Optional[float] = None, + slope_cutoff: Optional[float] = None, center_cutoff: Optional[float] = None): + + if bpm_outlier_sigma is None: + bpm_outlier_sigma = cls.default_bpm_outlier_sigma + + if slope_cutoff is None: + slope_cutoff = cls.default_slope_cutoff + + if center_cutoff is None: + center_cutoff = cls.default_center_cutoff + + bpm_position, induced_orbit_shift = prep_ios(data=data, n_downstream=n_downstream) + + p, pcov = np.polyfit(bpm_position, induced_orbit_shift, 1, cov=True) + slopes = p[0] + centers = - p[1] / p[0] + slopes_err = np.sqrt(pcov[0,0]) + centers_err = np.sqrt(centers ** 2 * (pcov[0,0] / p[0]**2 + pcov[1,1] / p[1] ** 2 - 2 * pcov[0, 1] / p[0] / p[1])) + + mask_bpm_outlier = reject_bpm_outlier(induced_orbit_shift, bpm_outlier_sigma) + mask_slopes = reject_slopes(slopes, slope_cutoff) + mask_center = reject_center_outlier(centers, center_cutoff) + mask_accepted = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) + + # calculate offset as a weighted average of the centers, with weights equal to the absolute slope + cc = centers[mask_accepted] + ww = np.abs(slopes[mask_accepted]) + cc_err = centers_err[mask_accepted] + ww_err = slopes_err[mask_accepted] + + CS = np.sum(ww * cc) + S = np.sum(ww) + VS = np.sum(ww_err**2) # variance of S + VCS = np.sum(cc**2 * ww_err**2 + ww**2 * cc_err**2) # variance of CS + VO = ( VCS / CS**2 + VS / S**2) # (variance of offset) / offset**2 + + offset = CS / S # offset = CS / S, average of centers with abs(slopes) as weights + offset_error = offset * np.sqrt(VO) + + result = BBAAnalysis( + offset=offset, + offset_error=offset_error, + slopes=slopes, + centers=centers, + slopes_err=slopes_err, + centers_err=centers_err, + induced_orbit_shift=induced_orbit_shift, + bpm_position=bpm_position, + mask_accepted=mask_accepted, + n_downstream=n_downstream, + rejected_outliers=sum(~mask_bpm_outlier), + rejected_slopes=sum(~mask_slopes), + rejected_centers=sum(~mask_center), + total_rejections=sum(~mask_accepted), + bpm_outlier_sigma=bpm_outlier_sigma, + slope_cutoff=slope_cutoff, + center_cutoff=center_cutoff, + ) + return result + +# BPM_OUTLIER = 6 # number of sigma +# SLOPE_FACTOR = 0.10 # of max slope +# CENTER_OUTLIER = 1 # number of sigma +# +# +# def get_slopes_center(bpm_pos, orbits, dk1): +# mag_vec = np.array([dk1, -dk1]) +# num_downstream_bpms = orbits.shape[2] +# fit_order = 1 +# x = np.mean(bpm_pos, axis=1) +# x_mask = ~np.isnan(x) +# err = np.mean(np.std(bpm_pos[x_mask, :], axis=1)) +# x = x[x_mask] +# new_tmp_tra = orbits[x_mask, :, :] +# +# tmp_slope = np.full((new_tmp_tra.shape[0], new_tmp_tra.shape[2]), np.nan) +# tmp_slope_err = np.full((new_tmp_tra.shape[0], new_tmp_tra.shape[2]), np.nan) +# center = np.full((new_tmp_tra.shape[2]), np.nan) +# center_err = np.full((new_tmp_tra.shape[2]), np.nan) +# for i in range(new_tmp_tra.shape[0]): +# for j in range(new_tmp_tra.shape[2]): +# y = new_tmp_tra[i, :, j] +# y_mask = ~np.isnan(y) +# if np.sum(y_mask) < min(len(mag_vec), 3): +# continue +# # TODO once the position errors are calculated and propagated, should be used +# p, pcov = np.polyfit(mag_vec[y_mask], y[y_mask], 1, w=np.ones(int(np.sum(y_mask))) / err, cov='unscaled') +# tmp_slope[i, j], tmp_slope_err[i, j] = p[0], pcov[0, 0] +# +# slopes = np.full((new_tmp_tra.shape[2]), np.nan) +# slopes_err = np.full((new_tmp_tra.shape[2]), np.nan) +# for j in range(min(new_tmp_tra.shape[2], num_downstream_bpms)): +# y = tmp_slope[:, j] +# y_err = tmp_slope_err[:, j] +# y_mask = ~np.isnan(y) +# if np.sum(y_mask) <= fit_order + 1: +# continue +# # TODO here do odr as the x values have also measurement errors +# p, pcov = np.polyfit(x[y_mask], y[y_mask], fit_order, w=1 / y_err[y_mask], cov='unscaled') +# if np.abs(p[0]) < 2 * np.sqrt(pcov[0, 0]): +# continue +# center[j] = -p[1] / (fit_order * p[0]) # zero-crossing if linear, minimum is quadratic +# center_err[j] = np.sqrt(center[j] ** 2 * (pcov[0,0]/p[0]**2 + pcov[1,1]/p[1]**2 - 2 * pcov[0, 1] / p[0] / p[1])) +# slopes[j] = p[0] +# slopes_err[j] = np.sqrt(pcov[0,0]) +# +# return slopes, slopes_err, center, center_err +# +# def get_offset(center, center_err, mask): +# from pySC.utils import stats +# try: +# offset_change = stats.weighted_mean(center[mask], center_err[mask]) +# offset_change_error = stats.weighted_error(center[mask]-offset_change, center_err[mask]) / np.sqrt(stats.effective_sample_size(center[mask], stats.weights_from_errors(center_err[mask]))) +# except ZeroDivisionError as exc: +# print(exc) +# print('Failed to estimate offset!!') +# print(f'Debug info: {center=}, {center_err=}, {mask=}') +# print(f'Debug info: {center[mask]=}, {center_err[mask]=}') +# offset_change = 0 +# offset_change_error = np.nan +# +# return offset_change, offset_change_error +# +# def old_analyze_trajectory_bba_data(data: BBAData, n_downstream: int = 20): +# bpm_number = data.bpm_number +# orbits = np.full((data.n0, 2, n_downstream), np.nan) +# bpm_pos = np.full((data.n0, 2), np.nan) +# start = bpm_number +# end = bpm_number + n_downstream +# for ii in range(data.n0): +# if data.plane == 'H': +# bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] +# bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] +# if data.skew_quad: +# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) +# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) +# else: +# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) +# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) +# +# else: +# bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] +# bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] +# if data.skew_quad: +# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) +# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) +# else: +# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) +# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) +# +# slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) +# mask_bpm_outlier = reject_bpm_outlier(orbits) +# mask_slopes = reject_slopes(slopes) +# mask_center = reject_center_outlier(center) +# final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) +# +# offset, offset_err = get_offset(center, center_err, final_mask) +# return offset, offset_err +# +# def analyze_trajectory_bba_data(data: BBAData, n_downstream: int = 20): +# bpm_number = data.bpm_number +# +# induced_orbit_shift = np.full((data.n0, n_downstream), np.nan) +# bpm_pos = np.full((data.n0), np.nan) +# start = bpm_number +# end = bpm_number + n_downstream +# +# x_up = np.array(data.raw_bpm_x_up) +# y_up = np.array(data.raw_bpm_y_up) +# x_center = np.array(data.raw_bpm_x_center) +# y_center = np.array(data.raw_bpm_y_center) +# if data.bipolar: +# k1_arr = [-data.dk1l, 0, data.dk1l] +# x_down = np.array(data.raw_bpm_x_down) +# y_down = np.array(data.raw_bpm_y_down) +# all_x = np.array([x_down[:, start:end], x_center[:,start:end], x_up[:, start:end]]) +# all_y = np.array([y_down[:, start:end], y_center[:,start:end], y_up[:, start:end]]) +# else: +# k1_arr = [0, data.dk1l] +# all_x = np.array([x_center[:,start:end], x_up[:, start:end]]) +# all_y = np.array([y_center[:,start:end], y_up[:, start:end]]) +# +# for ii in range(data.n0): +# if data.plane == 'H': +# bpm_pos[ii] = np.mean(all_x[:, ii, bpm_number - start]) +# if data.skew_quad: +# induced_orbit_shift[ii] = np.polyfit(k1_arr, all_y[:,ii], 1)[0] * data.dk1l +# else: +# induced_orbit_shift[ii] = np.polyfit(k1_arr, all_x[:,ii], 1)[0] * data.dk1l +# else: +# bpm_pos[ii] = np.mean(all_y[:, ii, bpm_number - start]) +# if data.skew_quad: +# induced_orbit_shift[ii] = np.polyfit(k1_arr, all_x[:,ii], 1)[0] * data.dk1l +# else: +# induced_orbit_shift[ii] = np.polyfit(k1_arr, all_y[:,ii], 1)[0] * data.dk1l +# +# p, pcov = np.polyfit(bpm_pos, induced_orbit_shift, 1, cov=True) +# slopes = p[0] +# center = - p[1] / p[0] +# slopes_err = np.sqrt(pcov[0,0]) +# center_err = np.sqrt(center ** 2 * (pcov[0,0] / p[0]**2 + pcov[1,1] / p[1] ** 2 - 2 * pcov[0, 1] / p[0] / p[1])) +# +# mask_bpm_outlier = reject_bpm_outlier(induced_orbit_shift) +# mask_slopes = reject_slopes(slopes) +# mask_center = reject_center_outlier(center) +# final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) +# +# cc = center[final_mask] +# ww = np.abs(slopes[final_mask]) +# cc_err = center_err[final_mask] +# ww_err = slopes_err[final_mask] +# +# CS = np.sum(ww * cc) +# S = np.sum(ww) +# offset = CS / S # offset = CS / S, average of centers with abs(slopes) as weights +# VS = np.sum(ww_err**2) # variance of S +# VCS = np.sum(cc**2 * ww_err**2 + ww**2 * cc_err**2) # variance of CS +# VO = ( VCS / CS**2 + VS / S**2) # (variance of offset) / offset**2 +# offset_err = offset * np.sqrt(VO) +# +# return offset, offset_err +# +# def analyze_bba_data(data: BBAData): +# bpm_number = data.bpm_number +# nbpms = len(data.raw_bpm_x_center[0]) +# orbits = np.full((data.n0, 2, nbpms), np.nan) +# bpm_pos = np.full((data.n0, 2), np.nan) +# for ii in range(data.n0): +# if data.plane == 'H': +# bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] +# bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] +# if data.skew_quad: +# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii]) - np.array(data.raw_bpm_y_center[ii]) +# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii]) - np.array(data.raw_bpm_y_center[ii]) +# else: +# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii]) - np.array(data.raw_bpm_x_center[ii]) +# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii]) - np.array(data.raw_bpm_x_center[ii]) +# +# else: +# bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] +# bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] +# if data.skew_quad: +# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii]) - np.array(data.raw_bpm_x_center[ii]) +# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii]) - np.array(data.raw_bpm_x_center[ii]) +# else: +# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii]) - np.array(data.raw_bpm_y_center[ii]) +# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii]) - np.array(data.raw_bpm_y_center[ii]) +# +# slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) +# mask_bpm_outlier = reject_bpm_outlier(orbits) +# mask_slopes = reject_slopes(slopes) +# mask_center = reject_center_outlier(center) +# final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) +# +# offset, offset_err = get_offset(center, center_err, final_mask) +# return offset, offset_err +# +# def get_trajectory_bba_analysis_data(data: BBAData, n_downstream: int = 20): +# bpm_number = data.bpm_number +# nbpms = len(data.raw_bpm_x_center[0]) +# orbits = np.full((data.n0, 2, n_downstream), np.nan) +# bpm_pos = np.full((data.n0, 2), np.nan) +# start = bpm_number +# end = bpm_number + n_downstream +# for ii in range(data.n0): +# if data.plane == 'H': +# bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] +# bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] +# if data.skew_quad: +# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) +# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) +# else: +# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) +# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) +# else: +# bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] +# bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] +# if data.skew_quad: +# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) +# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) +# else: +# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) +# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) +# +# slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) +# mask_bpm_outlier = reject_bpm_outlier(orbits) +# mask_slopes = reject_slopes(slopes) +# mask_center = reject_center_outlier(center) +# final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) +# +# offset, offset_err = get_offset(center, center_err, final_mask) +# return bpm_pos, orbits, slopes, center, final_mask, offset +# +# def get_bba_analysis_data(data: BBAData): +# bpm_number = data.bpm_number +# nbpms = len(data.raw_bpm_x_center[0]) +# orbits = np.full((data.n0, 2, nbpms), np.nan) +# bpm_pos = np.full((data.n0, 2), np.nan) +# for ii in range(data.n0): +# if data.plane == 'H': +# bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] +# bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] +# if data.skew_quad: +# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii]) - np.array(data.raw_bpm_y_center[ii]) +# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii]) - np.array(data.raw_bpm_y_center[ii]) +# else: +# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii]) - np.array(data.raw_bpm_x_center[ii]) +# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii]) - np.array(data.raw_bpm_x_center[ii]) +# else: +# bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] +# bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] +# if data.skew_quad: +# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii]) - np.array(data.raw_bpm_x_center[ii]) +# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii]) - np.array(data.raw_bpm_x_center[ii]) +# else: +# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii]) - np.array(data.raw_bpm_y_center[ii]) +# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii]) - np.array(data.raw_bpm_y_center[ii]) +# +# slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) +# mask_bpm_outlier = reject_bpm_outlier(orbits) +# mask_slopes = reject_slopes(slopes) +# mask_center = reject_center_outlier(center) +# final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) +# +# offset, offset_err = get_offset(center, center_err, final_mask) +# return bpm_pos, orbits, slopes, center, final_mask, offset \ No newline at end of file diff --git a/pySC/tuning/orbit_bba.py b/pySC/tuning/orbit_bba.py index 2d4463d..5101bbf 100644 --- a/pySC/tuning/orbit_bba.py +++ b/pySC/tuning/orbit_bba.py @@ -6,18 +6,13 @@ from ..core.control import IndivControl from .pySC_interface import pySCOrbitInterface from ..apps import measure_bba -from ..apps.bba import analyze_bba_data +from ..apps.bba import BBAAnalysis logger = logging.getLogger(__name__) if TYPE_CHECKING: from ..core.new_simulated_commissioning import SimulatedCommissioning -BPM_OUTLIER = 6 # number of sigma -SLOPE_FACTOR = 0.10 # of max slope -CENTER_OUTLIER = 1 # number of sigma - - def get_mag_s_pos(SC: "SimulatedCommissioning", MAG: list[str]): s_list = [] for control_name in MAG: @@ -37,7 +32,6 @@ class Orbit_BBA_Configuration(BaseModel, extra="forbid"): @classmethod def generate_config(cls, SC: "SimulatedCommissioning", max_dx_at_bpm = 1e-3, max_modulation=20e-6): - # max_modulation=600e-6, max_dx_at_bpm=1.5e-3 config = {} RM_name = 'orbit' @@ -154,10 +148,12 @@ def orbit_bba(SC: "SimulatedCommissioning", bpm_name: str, n_corr_steps: int = 7 data = measurement.V_data try: - offset, offset_err = analyze_bba_data(data) + analysis_result = BBAAnalysis.analyze(data) + offset = analysis_result.offset + offset_error = analysis_result.offset_error except Exception as exc: print(exc) logger.warning(f'Failed to compute trajectory BBA for BPM {bpm_name}') - offset, offset_err = 0, np.nan + offset, offset_error = 0, np.nan - return offset, offset_err + return offset, offset_error diff --git a/pySC/tuning/trajectory_bba.py b/pySC/tuning/trajectory_bba.py index a0ac489..6b7d999 100644 --- a/pySC/tuning/trajectory_bba.py +++ b/pySC/tuning/trajectory_bba.py @@ -4,8 +4,7 @@ import logging from ..core.control import IndivControl from ..apps import measure_bba -from ..apps.bba import analyze_trajectory_bba_data - +from ..apps.bba import BBAAnalysis from .pySC_interface import pySCInjectionInterface logger = logging.getLogger(__name__) @@ -13,11 +12,6 @@ if TYPE_CHECKING: from ..core.new_simulated_commissioning import SimulatedCommissioning -BPM_OUTLIER = 6 # number of sigma -SLOPE_FACTOR = 0.10 # of max slope -CENTER_OUTLIER = 1 # number of sigma - - def get_mag_s_pos(SC: "SimulatedCommissioning", MAG: list[str]): s_list = [] for control_name in MAG: @@ -169,99 +163,12 @@ def trajectory_bba(SC: "SimulatedCommissioning", bpm_name: str, n_corr_steps: in data = measurement.V_data try: - offset, offset_err = analyze_trajectory_bba_data(data, n_downstream=n_downstream_bpms) + analysis_result = BBAAnalysis.analyze(data, n_downstream=n_downstream_bpms) + offset = analysis_result.offset + offset_error = analysis_result.offset_error except Exception as exc: print(exc) logger.warning(f'Failed to compute trajectory BBA for BPM {bpm_name}') - offset, offset_err = 0, np.nan - - return offset, offset_err - -def reject_bpm_outlier(orbits): - n_k1 = orbits.shape[1] - n_bpms = orbits.shape[2] - mask = np.ones(n_bpms, dtype=bool) - for k1_step in range(n_k1): - for bpm in range(n_bpms): - data = orbits[:, k1_step, bpm] - if np.any(data - np.mean(data) > BPM_OUTLIER * np.std(data)): - mask[bpm] = False - - # n_rejections = n_bpms - np.sum(mask) - # print(f"Rejected {n_rejections}/{n_bpms} bpms for bpm outliers ( > {BPM_OUTLIER} r.m.s. )") - return mask - -def reject_slopes(slopes): - max_slope = np.nanmax(np.abs(slopes)) - mask = np.abs(slopes) > SLOPE_FACTOR * max_slope - - # n_rejections = len(slopes) - np.sum(mask) - # print(f"Rejected {n_rejections}/{len(slopes)} bpms for small slope ( < {SLOPE_FACTOR} * max(slope) )") - return mask - -def reject_center_outlier(center): - mean = np.nanmean(center) - std = np.nanstd(center) - mask = abs(center - mean) < CENTER_OUTLIER * std - - # n_rejections = len(center) - np.sum(mask) - # print(f"Rejected {n_rejections}/{len(center)} bpms for center away from mean ( > {CENTER_OUTLIER} r.m.s. )") - return mask - -def get_slopes_center(bpm_pos, orbits, dk1): - mag_vec = np.array([dk1, -dk1]) - num_downstream_bpms = orbits.shape[2] - fit_order = 1 - x = np.mean(bpm_pos, axis=1) - x_mask = ~np.isnan(x) - err = np.mean(np.std(bpm_pos[x_mask, :], axis=1)) - x = x[x_mask] - new_tmp_tra = orbits[x_mask, :, :] - - tmp_slope = np.full((new_tmp_tra.shape[0], new_tmp_tra.shape[2]), np.nan) - tmp_slope_err = np.full((new_tmp_tra.shape[0], new_tmp_tra.shape[2]), np.nan) - center = np.full((new_tmp_tra.shape[2]), np.nan) - center_err = np.full((new_tmp_tra.shape[2]), np.nan) - for i in range(new_tmp_tra.shape[0]): - for j in range(new_tmp_tra.shape[2]): - y = new_tmp_tra[i, :, j] - y_mask = ~np.isnan(y) - if np.sum(y_mask) < min(len(mag_vec), 3): - continue - # TODO once the position errors are calculated and propagated, should be used - p, pcov = np.polyfit(mag_vec[y_mask], y[y_mask], 1, w=np.ones(int(np.sum(y_mask))) / err, cov='unscaled') - tmp_slope[i, j], tmp_slope_err[i, j] = p[0], pcov[0, 0] - - slopes = np.full((new_tmp_tra.shape[2]), np.nan) - slopes_err = np.full((new_tmp_tra.shape[2]), np.nan) - for j in range(min(new_tmp_tra.shape[2], num_downstream_bpms)): - y = tmp_slope[:, j] - y_err = tmp_slope_err[:, j] - y_mask = ~np.isnan(y) - if np.sum(y_mask) <= fit_order + 1: - continue - # TODO here do odr as the x values have also measurement errors - p, pcov = np.polyfit(x[y_mask], y[y_mask], fit_order, w=1 / y_err[y_mask], cov='unscaled') - if np.abs(p[0]) < 2 * np.sqrt(pcov[0, 0]): - continue - center[j] = -p[1] / (fit_order * p[0]) # zero-crossing if linear, minimum is quadratic - center_err[j] = np.sqrt(center[j] ** 2 * (pcov[0,0]/p[0]**2 + pcov[1,1]/p[1]**2 - 2 * pcov[0, 1] / p[0] / p[1])) - slopes[j] = p[0] - slopes_err[j] = np.sqrt(pcov[0,0]) - - return slopes, slopes_err, center, center_err - -def get_offset(center, center_err, mask): - from pySC.utils import stats - try: - offset_change = stats.weighted_mean(center[mask], center_err[mask]) - offset_change_error = stats.weighted_error(center[mask]-offset_change, center_err[mask]) / np.sqrt(stats.effective_sample_size(center[mask], stats.weights_from_errors(center_err[mask]))) - except ZeroDivisionError as exc: - print(exc) - print('Failed to estimate offset!!') - print(f'Debug info: {center=}, {center_err=}, {mask=}') - print(f'Debug info: {center[mask]=}, {center_err[mask]=}') - offset_change = 0 - offset_change_error = np.nan + offset, offset_error = 0, np.nan - return offset_change, offset_change_error + return offset, offset_error From d0db9d64bf42c786bd1bef8fb0ee333bcc218178 Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 12 Feb 2026 10:39:33 +0100 Subject: [PATCH 55/70] remove unused things --- pySC/tuning/tools.py | 1 - pySC/tuning/tune.py | 4 ---- pySC/tuning/tuning_core.py | 4 ++-- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/pySC/tuning/tools.py b/pySC/tuning/tools.py index 671b21e..c7aa3f7 100644 --- a/pySC/tuning/tools.py +++ b/pySC/tuning/tools.py @@ -1,4 +1,3 @@ -import numpy as np import logging from typing import Callable from ..apps.tools import get_average_orbit as app_get_average_orbit diff --git a/pySC/tuning/tune.py b/pySC/tuning/tune.py index 9162d1d..ecc78b9 100644 --- a/pySC/tuning/tune.py +++ b/pySC/tuning/tune.py @@ -71,10 +71,6 @@ def build_tune_response_matrix(self, dk: float = 1e-5) -> None: TRM = np.zeros((2,2)) TRM[:, 0] = self.tune_response(self.controls_1, dk=dk) TRM[:, 1] = self.tune_response(self.controls_2, dk=dk) - iTRM = np.linalg.inv(TRM) - - #self.tune_response_matrix = TRM - #self.inverse_tune_response_matrix = iTRM return TRM def create_tune_knobs(self, delta: float = 1e-5) -> None: diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index 53b5151..250755c 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -166,7 +166,7 @@ def correct_injection(self, n_turns=1, n_reps=1, method='tikhonov', parameter=10 interface = pySCInjectionInterface(SC=SC, n_turns=n_turns) for _ in range(n_reps): - trims = orbit_correction(interface=interface, response_matrix=response_matrix, reference=None, + _ = orbit_correction(interface=interface, response_matrix=response_matrix, reference=None, method=method, parameter=parameter, apply=True) trajectory_x, trajectory_y = SC.bpm_system.capture_injection(n_turns=n_turns) @@ -190,7 +190,7 @@ def correct_orbit(self, n_reps=1, method='tikhonov', parameter=100, gain=1, zero interface = pySCOrbitInterface(SC=SC) for _ in range(n_reps): - trims = orbit_correction(interface=interface, response_matrix=response_matrix, reference=None, + _ = orbit_correction(interface=interface, response_matrix=response_matrix, reference=None, method=method, parameter=parameter, zerosum=zerosum, apply=True) orbit_x, orbit_y = SC.bpm_system.capture_orbit() From eb56c10c4d83fa669663c4abb532d8904601129b Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 12 Feb 2026 10:43:44 +0100 Subject: [PATCH 56/70] this was not fully removed before --- pySC/apps/bba.py | 267 ----------------------------------------------- 1 file changed, 267 deletions(-) diff --git a/pySC/apps/bba.py b/pySC/apps/bba.py index bc73c71..7dad90c 100644 --- a/pySC/apps/bba.py +++ b/pySC/apps/bba.py @@ -435,270 +435,3 @@ def analyze(cls, data: BBAData, n_downstream: Optional[int] = None, bpm_outlier_ center_cutoff=center_cutoff, ) return result - -# BPM_OUTLIER = 6 # number of sigma -# SLOPE_FACTOR = 0.10 # of max slope -# CENTER_OUTLIER = 1 # number of sigma -# -# -# def get_slopes_center(bpm_pos, orbits, dk1): -# mag_vec = np.array([dk1, -dk1]) -# num_downstream_bpms = orbits.shape[2] -# fit_order = 1 -# x = np.mean(bpm_pos, axis=1) -# x_mask = ~np.isnan(x) -# err = np.mean(np.std(bpm_pos[x_mask, :], axis=1)) -# x = x[x_mask] -# new_tmp_tra = orbits[x_mask, :, :] -# -# tmp_slope = np.full((new_tmp_tra.shape[0], new_tmp_tra.shape[2]), np.nan) -# tmp_slope_err = np.full((new_tmp_tra.shape[0], new_tmp_tra.shape[2]), np.nan) -# center = np.full((new_tmp_tra.shape[2]), np.nan) -# center_err = np.full((new_tmp_tra.shape[2]), np.nan) -# for i in range(new_tmp_tra.shape[0]): -# for j in range(new_tmp_tra.shape[2]): -# y = new_tmp_tra[i, :, j] -# y_mask = ~np.isnan(y) -# if np.sum(y_mask) < min(len(mag_vec), 3): -# continue -# # TODO once the position errors are calculated and propagated, should be used -# p, pcov = np.polyfit(mag_vec[y_mask], y[y_mask], 1, w=np.ones(int(np.sum(y_mask))) / err, cov='unscaled') -# tmp_slope[i, j], tmp_slope_err[i, j] = p[0], pcov[0, 0] -# -# slopes = np.full((new_tmp_tra.shape[2]), np.nan) -# slopes_err = np.full((new_tmp_tra.shape[2]), np.nan) -# for j in range(min(new_tmp_tra.shape[2], num_downstream_bpms)): -# y = tmp_slope[:, j] -# y_err = tmp_slope_err[:, j] -# y_mask = ~np.isnan(y) -# if np.sum(y_mask) <= fit_order + 1: -# continue -# # TODO here do odr as the x values have also measurement errors -# p, pcov = np.polyfit(x[y_mask], y[y_mask], fit_order, w=1 / y_err[y_mask], cov='unscaled') -# if np.abs(p[0]) < 2 * np.sqrt(pcov[0, 0]): -# continue -# center[j] = -p[1] / (fit_order * p[0]) # zero-crossing if linear, minimum is quadratic -# center_err[j] = np.sqrt(center[j] ** 2 * (pcov[0,0]/p[0]**2 + pcov[1,1]/p[1]**2 - 2 * pcov[0, 1] / p[0] / p[1])) -# slopes[j] = p[0] -# slopes_err[j] = np.sqrt(pcov[0,0]) -# -# return slopes, slopes_err, center, center_err -# -# def get_offset(center, center_err, mask): -# from pySC.utils import stats -# try: -# offset_change = stats.weighted_mean(center[mask], center_err[mask]) -# offset_change_error = stats.weighted_error(center[mask]-offset_change, center_err[mask]) / np.sqrt(stats.effective_sample_size(center[mask], stats.weights_from_errors(center_err[mask]))) -# except ZeroDivisionError as exc: -# print(exc) -# print('Failed to estimate offset!!') -# print(f'Debug info: {center=}, {center_err=}, {mask=}') -# print(f'Debug info: {center[mask]=}, {center_err[mask]=}') -# offset_change = 0 -# offset_change_error = np.nan -# -# return offset_change, offset_change_error -# -# def old_analyze_trajectory_bba_data(data: BBAData, n_downstream: int = 20): -# bpm_number = data.bpm_number -# orbits = np.full((data.n0, 2, n_downstream), np.nan) -# bpm_pos = np.full((data.n0, 2), np.nan) -# start = bpm_number -# end = bpm_number + n_downstream -# for ii in range(data.n0): -# if data.plane == 'H': -# bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] -# bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] -# if data.skew_quad: -# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) -# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) -# else: -# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) -# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) -# -# else: -# bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] -# bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] -# if data.skew_quad: -# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) -# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) -# else: -# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) -# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) -# -# slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) -# mask_bpm_outlier = reject_bpm_outlier(orbits) -# mask_slopes = reject_slopes(slopes) -# mask_center = reject_center_outlier(center) -# final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) -# -# offset, offset_err = get_offset(center, center_err, final_mask) -# return offset, offset_err -# -# def analyze_trajectory_bba_data(data: BBAData, n_downstream: int = 20): -# bpm_number = data.bpm_number -# -# induced_orbit_shift = np.full((data.n0, n_downstream), np.nan) -# bpm_pos = np.full((data.n0), np.nan) -# start = bpm_number -# end = bpm_number + n_downstream -# -# x_up = np.array(data.raw_bpm_x_up) -# y_up = np.array(data.raw_bpm_y_up) -# x_center = np.array(data.raw_bpm_x_center) -# y_center = np.array(data.raw_bpm_y_center) -# if data.bipolar: -# k1_arr = [-data.dk1l, 0, data.dk1l] -# x_down = np.array(data.raw_bpm_x_down) -# y_down = np.array(data.raw_bpm_y_down) -# all_x = np.array([x_down[:, start:end], x_center[:,start:end], x_up[:, start:end]]) -# all_y = np.array([y_down[:, start:end], y_center[:,start:end], y_up[:, start:end]]) -# else: -# k1_arr = [0, data.dk1l] -# all_x = np.array([x_center[:,start:end], x_up[:, start:end]]) -# all_y = np.array([y_center[:,start:end], y_up[:, start:end]]) -# -# for ii in range(data.n0): -# if data.plane == 'H': -# bpm_pos[ii] = np.mean(all_x[:, ii, bpm_number - start]) -# if data.skew_quad: -# induced_orbit_shift[ii] = np.polyfit(k1_arr, all_y[:,ii], 1)[0] * data.dk1l -# else: -# induced_orbit_shift[ii] = np.polyfit(k1_arr, all_x[:,ii], 1)[0] * data.dk1l -# else: -# bpm_pos[ii] = np.mean(all_y[:, ii, bpm_number - start]) -# if data.skew_quad: -# induced_orbit_shift[ii] = np.polyfit(k1_arr, all_x[:,ii], 1)[0] * data.dk1l -# else: -# induced_orbit_shift[ii] = np.polyfit(k1_arr, all_y[:,ii], 1)[0] * data.dk1l -# -# p, pcov = np.polyfit(bpm_pos, induced_orbit_shift, 1, cov=True) -# slopes = p[0] -# center = - p[1] / p[0] -# slopes_err = np.sqrt(pcov[0,0]) -# center_err = np.sqrt(center ** 2 * (pcov[0,0] / p[0]**2 + pcov[1,1] / p[1] ** 2 - 2 * pcov[0, 1] / p[0] / p[1])) -# -# mask_bpm_outlier = reject_bpm_outlier(induced_orbit_shift) -# mask_slopes = reject_slopes(slopes) -# mask_center = reject_center_outlier(center) -# final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) -# -# cc = center[final_mask] -# ww = np.abs(slopes[final_mask]) -# cc_err = center_err[final_mask] -# ww_err = slopes_err[final_mask] -# -# CS = np.sum(ww * cc) -# S = np.sum(ww) -# offset = CS / S # offset = CS / S, average of centers with abs(slopes) as weights -# VS = np.sum(ww_err**2) # variance of S -# VCS = np.sum(cc**2 * ww_err**2 + ww**2 * cc_err**2) # variance of CS -# VO = ( VCS / CS**2 + VS / S**2) # (variance of offset) / offset**2 -# offset_err = offset * np.sqrt(VO) -# -# return offset, offset_err -# -# def analyze_bba_data(data: BBAData): -# bpm_number = data.bpm_number -# nbpms = len(data.raw_bpm_x_center[0]) -# orbits = np.full((data.n0, 2, nbpms), np.nan) -# bpm_pos = np.full((data.n0, 2), np.nan) -# for ii in range(data.n0): -# if data.plane == 'H': -# bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] -# bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] -# if data.skew_quad: -# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii]) - np.array(data.raw_bpm_y_center[ii]) -# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii]) - np.array(data.raw_bpm_y_center[ii]) -# else: -# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii]) - np.array(data.raw_bpm_x_center[ii]) -# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii]) - np.array(data.raw_bpm_x_center[ii]) -# -# else: -# bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] -# bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] -# if data.skew_quad: -# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii]) - np.array(data.raw_bpm_x_center[ii]) -# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii]) - np.array(data.raw_bpm_x_center[ii]) -# else: -# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii]) - np.array(data.raw_bpm_y_center[ii]) -# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii]) - np.array(data.raw_bpm_y_center[ii]) -# -# slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) -# mask_bpm_outlier = reject_bpm_outlier(orbits) -# mask_slopes = reject_slopes(slopes) -# mask_center = reject_center_outlier(center) -# final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) -# -# offset, offset_err = get_offset(center, center_err, final_mask) -# return offset, offset_err -# -# def get_trajectory_bba_analysis_data(data: BBAData, n_downstream: int = 20): -# bpm_number = data.bpm_number -# nbpms = len(data.raw_bpm_x_center[0]) -# orbits = np.full((data.n0, 2, n_downstream), np.nan) -# bpm_pos = np.full((data.n0, 2), np.nan) -# start = bpm_number -# end = bpm_number + n_downstream -# for ii in range(data.n0): -# if data.plane == 'H': -# bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] -# bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] -# if data.skew_quad: -# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) -# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) -# else: -# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) -# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) -# else: -# bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] -# bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] -# if data.skew_quad: -# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) -# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii][start:end]) - np.array(data.raw_bpm_x_center[ii][start:end]) -# else: -# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) -# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii][start:end]) - np.array(data.raw_bpm_y_center[ii][start:end]) -# -# slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) -# mask_bpm_outlier = reject_bpm_outlier(orbits) -# mask_slopes = reject_slopes(slopes) -# mask_center = reject_center_outlier(center) -# final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) -# -# offset, offset_err = get_offset(center, center_err, final_mask) -# return bpm_pos, orbits, slopes, center, final_mask, offset -# -# def get_bba_analysis_data(data: BBAData): -# bpm_number = data.bpm_number -# nbpms = len(data.raw_bpm_x_center[0]) -# orbits = np.full((data.n0, 2, nbpms), np.nan) -# bpm_pos = np.full((data.n0, 2), np.nan) -# for ii in range(data.n0): -# if data.plane == 'H': -# bpm_pos[ii, 0] = data.raw_bpm_x_up[ii][bpm_number] -# bpm_pos[ii, 1] = data.raw_bpm_x_down[ii][bpm_number] -# if data.skew_quad: -# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii]) - np.array(data.raw_bpm_y_center[ii]) -# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii]) - np.array(data.raw_bpm_y_center[ii]) -# else: -# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii]) - np.array(data.raw_bpm_x_center[ii]) -# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii]) - np.array(data.raw_bpm_x_center[ii]) -# else: -# bpm_pos[ii, 0] = data.raw_bpm_y_up[ii][bpm_number] -# bpm_pos[ii, 1] = data.raw_bpm_y_down[ii][bpm_number] -# if data.skew_quad: -# orbits[ii, 0] = np.array(data.raw_bpm_x_up[ii]) - np.array(data.raw_bpm_x_center[ii]) -# orbits[ii, 1] = np.array(data.raw_bpm_x_down[ii]) - np.array(data.raw_bpm_x_center[ii]) -# else: -# orbits[ii, 0] = np.array(data.raw_bpm_y_up[ii]) - np.array(data.raw_bpm_y_center[ii]) -# orbits[ii, 1] = np.array(data.raw_bpm_y_down[ii]) - np.array(data.raw_bpm_y_center[ii]) -# -# slopes, slopes_err, center, center_err = get_slopes_center(bpm_pos, orbits, data.dk1l) -# mask_bpm_outlier = reject_bpm_outlier(orbits) -# mask_slopes = reject_slopes(slopes) -# mask_center = reject_center_outlier(center) -# final_mask = np.logical_and(np.logical_and(mask_bpm_outlier, mask_slopes), mask_center) -# -# offset, offset_err = get_offset(center, center_err, final_mask) -# return bpm_pos, orbits, slopes, center, final_mask, offset \ No newline at end of file From f4680928748ab6f43bf3838292b556be0aa396c4 Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 12 Feb 2026 10:46:08 +0100 Subject: [PATCH 57/70] use ResponseMatrix.matrix instead of ResponseMatrix.RM --- pySC/tuning/trajectory_bba.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pySC/tuning/trajectory_bba.py b/pySC/tuning/trajectory_bba.py index 6b7d999..d28eb21 100644 --- a/pySC/tuning/trajectory_bba.py +++ b/pySC/tuning/trajectory_bba.py @@ -46,8 +46,8 @@ def generate_config(cls, SC: "SimulatedCommissioning", max_dx_at_bpm = 1e-3, #d1, d2 = RM.RM.shape nh = len(SC.tuning.HCORR) nbpm = len(SC.bpm_system.indices) - HRM = RM.RM[:nbpm, :nh] - VRM = RM.RM[nbpm:, nh:] + HRM = RM.matrix[:nbpm, :nh] + VRM = RM.matrix[nbpm:, nh:] for bpm_number in range(len(SC.bpm_system.indices)): bpm_index = SC.bpm_system.indices[bpm_number] From f3ba56e3e6d67d968baea0837d79620fea44e1a1 Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 12 Feb 2026 10:54:41 +0100 Subject: [PATCH 58/70] remove some more legacy code --- pySC/utils/stats.py | 345 -------------------------------------------- 1 file changed, 345 deletions(-) delete mode 100644 pySC/utils/stats.py diff --git a/pySC/utils/stats.py b/pySC/utils/stats.py deleted file mode 100644 index 4f3d713..0000000 --- a/pySC/utils/stats.py +++ /dev/null @@ -1,345 +0,0 @@ -""" -Stats ------ - -Helper module providing statistical methods to compute various weighted averages along specified -axis and their errors as well as unbiased error estimator of infinite normal distribution from -finite-sized sample. -""" -import numpy as np -from scipy.special import erf -from scipy.stats import t - -import logging -LOGGER = logging.getLogger(__name__) - -CONFIDENCE_LEVEL = (1 + erf(1 / np.sqrt(2))) / 2 -PI2: float = 2 * np.pi -PI2I: complex = 2j * np.pi - - -def circular_mean(data, period=PI2, errors=None, axis=None): - """ - Computes weighted circular average along the specified axis. - - Parameters: - data: array-like - Contains the data to be averaged - period: scalar, optional, default (2 * np.pi) - Periodicity period of data - errors: array-like, optional - Contains errors associated with the values in data, it is used to calculated weights - axis: int or tuple of ints, optional - Axis or axes along which to average data - - Returns: - Returns the weighted circular average along the specified axis. - """ - phases = data * PI2I / period - weights = weights_from_errors(errors, period=period) - - return np.angle(np.average(np.exp(phases), axis=axis, weights=weights)) * period / PI2 - - -def circular_nanmean(data, period=PI2, errors=None, axis=None): - """"Wrapper around circular_mean with added nan handling""" - return circular_mean(data=np.ma.array(data, mask=np.isnan(data)), - period=period, - errors= None if errors is None else np.ma.array(errors, mask=np.isnan(data)), - axis=axis) - - -def circular_error(data, period=PI2, errors=None, axis=None, t_value_corr=True): - """ - Computes error of weighted circular average along the specified axis. - - Parameters: - data: array-like - Contains the data to be averaged - period: scalar, optional - Periodicity period of data, default is (2 * np.pi) - errors: array-like, optional - Contains errors associated with the values in data, it is used to calculated weights - axis: int or tuple of ints, optional - Axis or axes along which to average data - t_value_corr: bool, optional - Species if the error is corrected for small sample size, default True - - Returns: - Returns the error of weighted circular average along the specified axis. - """ - phases = data * PI2I / period - weights = weights_from_errors(errors, period=period) - complex_phases = np.exp(phases) - complex_average = np.average(complex_phases, axis=axis, weights=weights) - - (sample_variance, sum_of_weights) = np.average( - np.square(np.abs(complex_phases - complex_average.reshape(_get_shape( - complex_phases.shape, axis)))), weights=weights, axis=axis, returned=True) - if weights is not None: - sample_variance = sample_variance + 1. / sum_of_weights - error_of_complex_average = np.sqrt(sample_variance * unbias_variance(data, weights, axis=axis)) - phase_error = np.nan_to_num(error_of_complex_average / np.abs(complex_average)) - if t_value_corr: - phase_error = phase_error * t_value_correction(effective_sample_size(data, weights, axis=axis)) - return np.where(phase_error > 0.25 * PI2, 0.3 * period, phase_error * period / PI2) - - -def circular_nanerror(data, period=PI2, errors=None, axis=None, t_value_corr=True): - """"Wrapper around circular_error with added nan handling""" - return circular_error(data=np.ma.array(data, mask=np.isnan(data)), - period=period, - errors=None if errors is None else np.ma.array(errors, mask=np.isnan(data)), - axis=axis, - t_value_corr=t_value_corr) - - -def weighted_mean(data, errors=None, axis=None): - """ - Computes weighted average along the specified axis. - - Parameters: - data: array-like - Contains the data to be averaged - errors: array-like, optional - Contains errors associated with the values in data, it is used to calculated weights - axis: int or tuple of ints, optional - Axis or axes along which to average data - - Returns: - Returns the weighted average along the specified axis. - """ - weights = weights_from_errors(errors) - return np.average(data, axis=axis, weights=weights) - - -def weighted_nanmean(data, errors=None, axis=None): - """"Wrapper around weighted_mean with added nan handling""" - return weighted_mean(data=np.ma.array(data, mask=np.isnan(data)), - errors=None if errors is None else np.ma.array(errors, mask=np.isnan(data)), - axis=axis) - - -def _get_shape(orig_shape, axis): - new_shape = np.array(orig_shape) - if axis is None: - new_shape[:] = 1 - else: - new_shape[np.array(axis)] = 1 - return tuple(new_shape) - - -def weighted_error(data, errors=None, axis=None, t_value_corr=True): - """ - Computes error of weighted average along the specified axis. - This is similar to calculating the standard deviation on the data, - but with both, the average to which the deviation is calculated, - as well as then the averaging over the deviations weighted by - weights based on the errors. - - In addition, the weighted variance is unbiased by an unbias-factor - n / (n-1), where n is the :meth:`omc3.utils.stats.effective_sample_size` . - Additionally, a (student) t-value correction can be performed (done by default) - which corrects the estimate for small data sets. - - Parameters: - data: array-like - Contains the data on which the weighted error on the average is calculated. - errors: array-like, optional - Contains errors associated with the values in data, it is used to calculated weights - axis: int or tuple of ints, optional - Axis or axes along which to average data - t_value_corr: bool, optional - Species if the error is corrected for small sample size, default True - - Returns: - Returns the error of weighted average along the specified axis. - """ - weights = weights_from_errors(errors) - weighted_average = np.average(data, axis=axis, weights=weights) - weighted_average = weighted_average.reshape(_get_shape(data.shape, axis)) - (sample_variance, sum_of_weights) = np.ma.average( - np.square(np.abs(data - weighted_average)), - weights=weights, axis=axis, returned=True - ) - if weights is not None: - sample_variance = sample_variance + 1 / sum_of_weights - error = np.nan_to_num(np.sqrt(sample_variance * unbias_variance(data, weights, axis=axis))) - if t_value_corr: - error = error * t_value_correction(effective_sample_size(data, weights, axis=axis)) - return error - - -def circular_rms(data, period=PI2, axis=None): - """ - Computes the circular root mean square along the specified axis. - - Parameters: - data: array-like - Contains the data to be averaged - period: scalar, optional - Periodicity period of data, default is (2 * np.pi) - axis: int or tuple of ints, optional - Axis or axes along which to average data - - Returns: - Returns the circular root mean square along the specified axis. - """ - return np.sqrt(circular_mean(np.square(data / period), period=1, axis=axis) * period) - - -def rms(data, axis=None): - """ - Computes the root mean square along the specified axis. - - Parameters: - data: array-like - Contains the data to be averaged - axis: int or tuple of ints, optional - Axis or axes along which to average data - - Returns: - Returns the root mean square along the specified axis. - """ - return weighted_rms(data, axis=axis) - - -def weighted_rms(data, errors=None, axis=None): - """ - Computes weighted root mean square along the specified axis. - - Parameters: - data: array-like - Contains the data to be averaged - errors: array-like, optional - Contains errors associated with the values in data, it is used to calculated weights - axis: int or tuple of ints, optional - Axis or axes along which to average data - - Returns: - Returns weighted root mean square along the specified axis. - """ - weights = weights_from_errors(errors) - return np.sqrt(np.average(np.square(data), weights=weights, axis=axis)) - - -def weighted_nanrms(data, errors=None, axis=None): - """"Wrapper around weigthed_rms with added nan handling""" - return weighted_rms(data=np.ma.array(data, mask=np.isnan(data)), - errors=None if errors is None else np.ma.array(errors, mask=np.isnan(data)), - axis=axis) - - -def weights_from_errors(errors, period=PI2): - """ - Computes weights from measurement errors, weights are not output if errors contain zeros or nans - - Parameters: - errors: array-like - Contains errors which are used to calculated weights - period: scalar, optional - Periodicity period of data, default is (2 * np.pi) - - Returns: - Returns the error of weighted circular average along the specified axis. - """ - if errors is None: - return None - if np.any(np.isnan(errors)): - LOGGER.warning("NaNs found, weights are not used.") - return None - if np.any(np.logical_not(errors)): - LOGGER.warning("Zeros found, weights are not used.") - return None - return 1 / np.square(errors * PI2 / period) - - -def effective_sample_size(data, weights, axis=None): - r""" - Computes effective sample size of weighted data along specified axis, - the minimum value returned is 2 to avoid non-reasonable error blow-up. - - It is calculated via Kish's approximate formula - from the (not necessarily normalized) weights :math:`w_i` (see wikipedia): - - .. math:: - - n_\mathrm{eff} = \frac{\left(\sum_i w_i\right)^2}{\sum_i w_i^2} - - What it represents: - "In most instances, weighting causes a decrease in the statistical significance of results. - The effective sample size is a measure of the precision of the survey - (e.g., even if you have a sample of 1,000 people, an effective sample size of 100 would indicate - that the weighted sample is no more robust than a well-executed un-weighted - simple random sample of 100 people)." - - https://wiki.q-researchsoftware.com/wiki/Weights,_Effective_Sample_Size_and_Design_Effects - - Parameters: - data: array-like - weights: array-like - Contains weights associated with the values in data - axis: int or tuple of ints, optional - Axis or axes along which the effective sample size is computed - - Returns: - Returns the error of weighted circular average along the specified axis. - """ - if weights is None: - sample_size = np.sum(np.ones(data.shape), axis=axis) - else: - sample_size = np.square(np.sum(weights, axis=axis)) / np.sum(np.square(weights), axis=axis) - return np.where(sample_size > 2, sample_size, 2) - - -def unbias_variance(data, weights, axis=None): - r""" - Computes a correction factor to unbias variance of weighted average of data along specified axis, - e.g. transform the standard deviation 1 - - .. math:: - - \sigma^2 = \frac{1}{N} \sum_{i=1}^N (x_i - x_\mathrm{mean})^2 - - into an un-biased estimator - - .. math:: - - \sigma^2 = \frac{1}{N-1} \sum_{i=1}^N (x_i - x_\mathrm{mean})^2 - - Parameters: - data: array-like - weights: array-like - Contains weights associated with the values in data - axis: int or tuple of ints, optional - Axis or axes along which the effective sample size is computed - - Returns: - Returns the error of weighted circular average along the specified axis. - """ - sample_size = effective_sample_size(data, weights, axis=axis) - try: - return sample_size / (sample_size - 1) - except ZeroDivisionError or RuntimeWarning: - return np.zeros(sample_size.shape) - - -def t_value_correction(sample_size): - """ - Calculates the multiplicative correction factor to determine standard deviation of - a normally distributed quantity from standard deviation of its finite-sized sample. - The minimum allowed sample size is 2 to avoid non-reasonable error blow-up - for smaller sample sizes 2 is used instead. - - Note (jdilly): In other words, this transforms the area of 1 sigma under - the given student t distribution to the 1 sigma area of a normal distribution - (this transformation is where the ``CONFIDENCE LEVEL`` comes in). - I hope that makes the intention more clear. - - Args: - sample_size: array-like - - Returns: - multiplicative correction factor(s) of same shape as sample_size. - Can contain nans. - """ - return t.ppf(CONFIDENCE_LEVEL, np.where(sample_size > 2, sample_size, 2) - 1) From 670b9d0c628a91384f42289e2230009fd7630fe9 Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 12 Feb 2026 11:02:49 +0100 Subject: [PATCH 59/70] rename new_simulated_commissioning to simulated_commissioning --- pySC/__init__.py | 2 +- pySC/configuration/bpm_system_conf.py | 2 +- pySC/configuration/general.py | 2 +- pySC/configuration/generation.py | 2 +- pySC/configuration/injection_conf.py | 2 +- pySC/configuration/magnets_conf.py | 2 +- pySC/configuration/rf_conf.py | 2 +- pySC/configuration/supports_conf.py | 2 +- pySC/configuration/tuning_conf.py | 2 +- pySC/control_system/server.py | 2 +- pySC/core/bpm_system.py | 2 +- pySC/core/injection.py | 2 +- pySC/core/rfsettings.py | 2 +- ...ew_simulated_commissioning.py => simulated_commissioning.py} | 0 pySC/core/supports.py | 2 +- pySC/tuning/parallel.py | 2 +- pySC/tuning/pySC_interface.py | 2 +- pySC/tuning/response_measurements.py | 2 +- pySC/tuning/trajectory_bba.py | 2 +- pySC/tuning/tuning_core.py | 2 +- 20 files changed, 19 insertions(+), 19 deletions(-) rename pySC/core/{new_simulated_commissioning.py => simulated_commissioning.py} (100%) diff --git a/pySC/__init__.py b/pySC/__init__.py index b140b2a..9c3a505 100644 --- a/pySC/__init__.py +++ b/pySC/__init__.py @@ -8,7 +8,7 @@ __version__ = "0.5.0" -from .core.new_simulated_commissioning import SimulatedCommissioning +from .core.simulated_commissioning import SimulatedCommissioning from .configuration.generation import generate_SC from .apps.response_matrix import ResponseMatrix from .tuning.pySC_interface import pySCInjectionInterface, pySCOrbitInterface diff --git a/pySC/configuration/bpm_system_conf.py b/pySC/configuration/bpm_system_conf.py index 245441e..2cef049 100644 --- a/pySC/configuration/bpm_system_conf.py +++ b/pySC/configuration/bpm_system_conf.py @@ -1,7 +1,7 @@ import numpy as np import logging -from ..core.new_simulated_commissioning import SimulatedCommissioning +from ..core.simulated_commissioning import SimulatedCommissioning from ..core.bpm_system import BPM_FIELDS_TO_INITIALISE from .general import get_error, get_indices_and_names from .supports_conf import generate_element_misalignments diff --git a/pySC/configuration/general.py b/pySC/configuration/general.py index 3b2da1e..253cd54 100644 --- a/pySC/configuration/general.py +++ b/pySC/configuration/general.py @@ -2,7 +2,7 @@ import logging from .load_config import load_yaml -from ..core.new_simulated_commissioning import SimulatedCommissioning +from ..core.simulated_commissioning import SimulatedCommissioning from ..core.magnet import MAGNET_NAME_TYPE logger = logging.getLogger(__name__) diff --git a/pySC/configuration/generation.py b/pySC/configuration/generation.py index 2637f80..9bb90b3 100644 --- a/pySC/configuration/generation.py +++ b/pySC/configuration/generation.py @@ -2,7 +2,7 @@ import logging from ..core.lattice import ATLattice -from ..core.new_simulated_commissioning import SimulatedCommissioning +from ..core.simulated_commissioning import SimulatedCommissioning from .load_config import load_yaml from .magnets_conf import configure_magnets from .bpm_system_conf import configure_bpms diff --git a/pySC/configuration/injection_conf.py b/pySC/configuration/injection_conf.py index 4d15447..3754b06 100644 --- a/pySC/configuration/injection_conf.py +++ b/pySC/configuration/injection_conf.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from ..core.new_simulated_commissioning import SimulatedCommissioning + from ..core.simulated_commissioning import SimulatedCommissioning logger = logging.getLogger(__name__) diff --git a/pySC/configuration/magnets_conf.py b/pySC/configuration/magnets_conf.py index 0ee66bb..1335175 100644 --- a/pySC/configuration/magnets_conf.py +++ b/pySC/configuration/magnets_conf.py @@ -1,5 +1,5 @@ from typing import Any -from ..core.new_simulated_commissioning import SimulatedCommissioning +from ..core.simulated_commissioning import SimulatedCommissioning from ..core.magnet import MAGNET_NAME_TYPE from .general import get_error, get_indices_and_names from .supports_conf import generate_element_misalignments diff --git a/pySC/configuration/rf_conf.py b/pySC/configuration/rf_conf.py index c85e9cf..6e2fb4e 100644 --- a/pySC/configuration/rf_conf.py +++ b/pySC/configuration/rf_conf.py @@ -1,6 +1,6 @@ import logging -from ..core.new_simulated_commissioning import SimulatedCommissioning +from ..core.simulated_commissioning import SimulatedCommissioning from ..core.rfsettings import RFCavity, RFSystem from .general import get_error, get_indices_and_names diff --git a/pySC/configuration/supports_conf.py b/pySC/configuration/supports_conf.py index e2e3650..c385fc4 100644 --- a/pySC/configuration/supports_conf.py +++ b/pySC/configuration/supports_conf.py @@ -1,6 +1,6 @@ from typing import Any -from ..core.new_simulated_commissioning import SimulatedCommissioning +from ..core.simulated_commissioning import SimulatedCommissioning from .general import get_error, get_indices_and_names import logging diff --git a/pySC/configuration/tuning_conf.py b/pySC/configuration/tuning_conf.py index 0c30310..78640d7 100644 --- a/pySC/configuration/tuning_conf.py +++ b/pySC/configuration/tuning_conf.py @@ -1,4 +1,4 @@ -from ..core.new_simulated_commissioning import SimulatedCommissioning +from ..core.simulated_commissioning import SimulatedCommissioning from ..core.control import IndivControl from .general import get_indices_and_names import numpy as np diff --git a/pySC/control_system/server.py b/pySC/control_system/server.py index 784fce6..2ff40b5 100644 --- a/pySC/control_system/server.py +++ b/pySC/control_system/server.py @@ -9,7 +9,7 @@ from .rf_server import rf_server if TYPE_CHECKING: - from ..core.new_simulated_commissioning import SimulatedCommissioning + from ..core.simulated_commissioning import SimulatedCommissioning HOST = "127.0.0.1" # Standard loopback interface address (localhost) # PORT = 13131 # Port to listen on (non-privileged ports are > 1023) diff --git a/pySC/core/bpm_system.py b/pySC/core/bpm_system.py index e0822be..d02f20c 100644 --- a/pySC/core/bpm_system.py +++ b/pySC/core/bpm_system.py @@ -5,7 +5,7 @@ import warnings if TYPE_CHECKING: - from .new_simulated_commissioning import SimulatedCommissioning + from .simulated_commissioning import SimulatedCommissioning BPM_NAME_TYPE = Union[str, int] diff --git a/pySC/core/injection.py b/pySC/core/injection.py index 15fea04..907f134 100644 --- a/pySC/core/injection.py +++ b/pySC/core/injection.py @@ -3,7 +3,7 @@ import numpy as np if TYPE_CHECKING: - from .new_simulated_commissioning import SimulatedCommissioning + from .simulated_commissioning import SimulatedCommissioning CAVITY_NAME_TYPE = Union[str, int] diff --git a/pySC/core/rfsettings.py b/pySC/core/rfsettings.py index 1fd8973..28cf3ae 100644 --- a/pySC/core/rfsettings.py +++ b/pySC/core/rfsettings.py @@ -3,7 +3,7 @@ from pydantic import BaseModel, Field, PrivateAttr if TYPE_CHECKING: - from .new_simulated_commissioning import SimulatedCommissioning + from .simulated_commissioning import SimulatedCommissioning CAVITY_NAME_TYPE = Union[str, int] diff --git a/pySC/core/new_simulated_commissioning.py b/pySC/core/simulated_commissioning.py similarity index 100% rename from pySC/core/new_simulated_commissioning.py rename to pySC/core/simulated_commissioning.py diff --git a/pySC/core/supports.py b/pySC/core/supports.py index dae5c84..5c8d482 100644 --- a/pySC/core/supports.py +++ b/pySC/core/supports.py @@ -10,7 +10,7 @@ from ..utils.sc_tools import update_transformation if TYPE_CHECKING: - from .new_simulated_commissioning import SimulatedCommissioning + from .simulated_commissioning import SimulatedCommissioning logger = logging.getLogger(__name__) diff --git a/pySC/tuning/parallel.py b/pySC/tuning/parallel.py index cac62df..bfe0cd0 100644 --- a/pySC/tuning/parallel.py +++ b/pySC/tuning/parallel.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: - from ..core.new_simulated_commissioning import SimulatedCommissioning + from ..core.simulated_commissioning import SimulatedCommissioning def get_listener_and_queue(logger): log_queue = Queue() diff --git a/pySC/tuning/pySC_interface.py b/pySC/tuning/pySC_interface.py index 2a6d6b1..9df4b15 100644 --- a/pySC/tuning/pySC_interface.py +++ b/pySC/tuning/pySC_interface.py @@ -4,7 +4,7 @@ from ..apps.interface import AbstractInterface if TYPE_CHECKING: - from ..core.new_simulated_commissioning import SimulatedCommissioning + from ..core.simulated_commissioning import SimulatedCommissioning class pySCOrbitInterface(AbstractInterface): SC: "SimulatedCommissioning" = Field(repr=False) diff --git a/pySC/tuning/response_measurements.py b/pySC/tuning/response_measurements.py index 20c6a22..697cee4 100644 --- a/pySC/tuning/response_measurements.py +++ b/pySC/tuning/response_measurements.py @@ -6,7 +6,7 @@ from ..apps import measure_ORM, measure_dispersion if TYPE_CHECKING: - from ..core.new_simulated_commissioning import SimulatedCommissioning + from ..core.simulated_commissioning import SimulatedCommissioning logger = logging.getLogger(__name__) diff --git a/pySC/tuning/trajectory_bba.py b/pySC/tuning/trajectory_bba.py index d28eb21..f55c4f6 100644 --- a/pySC/tuning/trajectory_bba.py +++ b/pySC/tuning/trajectory_bba.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: - from ..core.new_simulated_commissioning import SimulatedCommissioning + from ..core.simulated_commissioning import SimulatedCommissioning def get_mag_s_pos(SC: "SimulatedCommissioning", MAG: list[str]): s_list = [] diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index 250755c..51b9766 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -19,7 +19,7 @@ from multiprocessing import Process, Queue if TYPE_CHECKING: - from ..core.new_simulated_commissioning import SimulatedCommissioning + from ..core.simulated_commissioning import SimulatedCommissioning logger = logging.getLogger(__name__) From ee1774691156fd3b471e71de6a1ef76c456d84e4 Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 12 Feb 2026 11:40:49 +0100 Subject: [PATCH 60/70] first major version! --- pySC/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pySC/__init__.py b/pySC/__init__.py index 9c3a505..bde0ba3 100644 --- a/pySC/__init__.py +++ b/pySC/__init__.py @@ -6,7 +6,7 @@ """ -__version__ = "0.5.0" +__version__ = "1.0.0" from .core.simulated_commissioning import SimulatedCommissioning from .configuration.generation import generate_SC From e9943f79fd810bc79ca34dc75512fd8d7c80ec67 Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 12 Feb 2026 14:00:36 +0100 Subject: [PATCH 61/70] import apps also in pySC --- pySC/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pySC/__init__.py b/pySC/__init__.py index bde0ba3..a7a6a6c 100644 --- a/pySC/__init__.py +++ b/pySC/__init__.py @@ -11,6 +11,10 @@ from .core.simulated_commissioning import SimulatedCommissioning from .configuration.generation import generate_SC from .apps.response_matrix import ResponseMatrix +from .apps.measurements import orbit_correction +from .apps.measurements import measure_bba +from .apps.measurements import measure_ORM +from .apps.measurements import measure_dispersion from .tuning.pySC_interface import pySCInjectionInterface, pySCOrbitInterface import logging import sys From b49eeb55b38c3f8fee0dac6a9fb60711b33dd825 Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 12 Feb 2026 14:26:31 +0100 Subject: [PATCH 62/70] update readme --- README.md | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 637c556..1c3b4a8 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,37 @@ # pySC -Python Simulated Commissioning toolkit for synchrotrons, inspired by [SC](https://github.com/ThorstenHellert/SC) which is written in Matlab. + +Python Simulated Commissioning toolkit for synchrotrons. ## Installing ```bash -git clone https://github.com/kparasch/pySC -cd pySC -pip install -e . +pip install accelerator-commissioning ``` + +## Importing specific modules + +Intended way of importing a pySC functionality: + +``` +from pySC import SimulatedCommissioning +from pySC import generate_SC + +from pySC import ResponseMatrix + +from pySC import orbit_correction +from pySC import measure_bba +from pySC import measure_ORM +from pySC import measure_dispersion + +from pySC import pySCInjectionInterface +from pySC import pySCOrbitInterface + +# the following disables rich progress bars (doesn't work well with ) +from pySC import disable_pySC_rich +disable_pySC_rich() +``` + + +## Acknowledgements + +This toolkit was inspired by [SC](https://github.com/ThorstenHellert/SC) which is written in Matlab. From 2d8b79318f976f52d9e6529cf85ea9fe73a8607b Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 12 Feb 2026 14:30:36 +0100 Subject: [PATCH 63/70] logging format fix and remove unused variable --- pySC/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pySC/__init__.py b/pySC/__init__.py index a7a6a6c..6c8f043 100644 --- a/pySC/__init__.py +++ b/pySC/__init__.py @@ -22,16 +22,14 @@ logging.basicConfig( #format='%(asctime)s.%(msecs)03d:%(levelname)s:%(name)s:\t%(message)s', format="{asctime} | {levelname} | {message}", - datefmt="%d %b% %Y, %H:%M:%S", + datefmt="%d %b %Y, %H:%M:%S", level=logging.INFO, style='{', stream=sys.stdout ) def disable_pySC_rich(): - from .tuning import response_measurements from .apps import response - response_measurements.DISABLE_RICH = True response.DISABLE_RICH = True # This is needed to avoid circular imports. From 59279c9f8f77dd92a7d85d7bbf4fd5c01db1fac2 Mon Sep 17 00:00:00 2001 From: oscarxblanco Date: Mon, 2 Feb 2026 22:49:11 +0100 Subject: [PATCH 64/70] np.concatenate --- pySC/apps/measurements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pySC/apps/measurements.py b/pySC/apps/measurements.py index dcdd1a8..55027af 100644 --- a/pySC/apps/measurements.py +++ b/pySC/apps/measurements.py @@ -23,7 +23,7 @@ def orbit_correction(interface: AbstractInterface, response_matrix: ResponseMatr logger.warning("Gain is set but apply is False, gain will have no effect.") orbit_x, orbit_y = interface.get_orbit() - orbit = np.concat((orbit_x.flatten(order='F'), orbit_y.flatten(order='F'))) + orbit = np.concatenate((orbit_x.flatten(order='F'), orbit_y.flatten(order='F'))) if reference is not None: assert len(reference) == len(orbit), "Reference orbit has wrong length" From 8ccc6ffc48d749081cbda66be74c0e7ab42b4fd6 Mon Sep 17 00:00:00 2001 From: kparasch Date: Thu, 12 Feb 2026 14:45:05 +0100 Subject: [PATCH 65/70] remove comment --- pySC/apps/bba.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pySC/apps/bba.py b/pySC/apps/bba.py index 7dad90c..60208a2 100644 --- a/pySC/apps/bba.py +++ b/pySC/apps/bba.py @@ -10,7 +10,6 @@ from .tools import get_average_orbit from .interface import AbstractInterface from ..core.types import NPARRAY -# from ..tuning.orbit_bba import reject_bpm_outlier, reject_center_outlier, reject_slopes, get_slopes_center, get_offset logger = logging.getLogger(__name__) From df9d4b6affb6f2e0cfb803ae6352a91c9b7d8ce9 Mon Sep 17 00:00:00 2001 From: kparasch Date: Fri, 13 Feb 2026 09:29:58 +0100 Subject: [PATCH 66/70] file was forgotten unsaved --- pySC/tuning/orbit_bba.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pySC/tuning/orbit_bba.py b/pySC/tuning/orbit_bba.py index 5101bbf..d80352f 100644 --- a/pySC/tuning/orbit_bba.py +++ b/pySC/tuning/orbit_bba.py @@ -11,7 +11,7 @@ logger = logging.getLogger(__name__) if TYPE_CHECKING: - from ..core.new_simulated_commissioning import SimulatedCommissioning + from ..core.simulated_commissioning import SimulatedCommissioning def get_mag_s_pos(SC: "SimulatedCommissioning", MAG: list[str]): s_list = [] From 2676411f5c03e2dd72e3372effe4f1544f306baa Mon Sep 17 00:00:00 2001 From: kparasch Date: Fri, 13 Feb 2026 11:10:56 +0100 Subject: [PATCH 67/70] gain missing ! --- pySC/tuning/tuning_core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pySC/tuning/tuning_core.py b/pySC/tuning/tuning_core.py index 51b9766..89b6452 100644 --- a/pySC/tuning/tuning_core.py +++ b/pySC/tuning/tuning_core.py @@ -167,7 +167,7 @@ def correct_injection(self, n_turns=1, n_reps=1, method='tikhonov', parameter=10 for _ in range(n_reps): _ = orbit_correction(interface=interface, response_matrix=response_matrix, reference=None, - method=method, parameter=parameter, apply=True) + method=method, parameter=parameter, gain=gain, apply=True) trajectory_x, trajectory_y = SC.bpm_system.capture_injection(n_turns=n_turns) trajectory_x = trajectory_x.flatten('F') @@ -191,7 +191,7 @@ def correct_orbit(self, n_reps=1, method='tikhonov', parameter=100, gain=1, zero for _ in range(n_reps): _ = orbit_correction(interface=interface, response_matrix=response_matrix, reference=None, - method=method, parameter=parameter, zerosum=zerosum, apply=True) + method=method, parameter=parameter, zerosum=zerosum, gain=gain, apply=True) orbit_x, orbit_y = SC.bpm_system.capture_orbit() rms_x = np.nanstd(orbit_x) * 1e6 From 359389b7b9cb4b539c6bc53b5e25a77e69562219 Mon Sep 17 00:00:00 2001 From: kparasch Date: Fri, 13 Feb 2026 11:11:40 +0100 Subject: [PATCH 68/70] small simplifcation --- pySC/apps/measurements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pySC/apps/measurements.py b/pySC/apps/measurements.py index 55027af..c014613 100644 --- a/pySC/apps/measurements.py +++ b/pySC/apps/measurements.py @@ -38,8 +38,8 @@ def orbit_correction(interface: AbstractInterface, response_matrix: ResponseMatr if apply: data = interface.get_many(correctors) - for i, corr in enumerate(correctors): - data[corr] += trim_list[i] * gain + for corr in correctors: + data[corr] += trims[corr] * gain interface.set_many(data) if rf and trims['rf'] != 0: f_rf = interface.get_rf_main_frequency() From 5eb16d063abce4473a928c4c9cfe306e39badba1 Mon Sep 17 00:00:00 2001 From: kparasch Date: Fri, 13 Feb 2026 11:19:17 +0100 Subject: [PATCH 69/70] optimization, do not initialize at each SC validation --- pySC/core/simulated_commissioning.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/pySC/core/simulated_commissioning.py b/pySC/core/simulated_commissioning.py index 8d53ca5..72608b6 100644 --- a/pySC/core/simulated_commissioning.py +++ b/pySC/core/simulated_commissioning.py @@ -1,4 +1,4 @@ -from pydantic import BaseModel, model_validator, Field +from pydantic import BaseModel, model_validator, Field, PrivateAttr from typing import Optional import json import numpy as np @@ -35,17 +35,22 @@ class SimulatedCommissioning(BaseModel, extra="forbid"): seed: int = Field(default=1, frozen=True) rng: Optional[RNG] = None + _initialized: bool = PrivateAttr(default=False) + @model_validator(mode="after") def initialize(self): - self.propagate_parents() - if self.rng is None: - self.rng = RNG(seed=self.seed) - self.support_system.update_all() - self.design_magnet_settings.sendall() - self.magnet_settings.sendall() - for rf_settings in [self.rf_settings, self.design_rf_settings]: - for system_name in rf_settings.systems: - rf_settings.systems[system_name].trigger_update() + if not self._initialized: + self._initialized = True + + self.propagate_parents() + if self.rng is None: + self.rng = RNG(seed=self.seed) + self.support_system.update_all() + self.design_magnet_settings.sendall() + self.magnet_settings.sendall() + for rf_settings in [self.rf_settings, self.design_rf_settings]: + for system_name in rf_settings.systems: + rf_settings.systems[system_name].trigger_update() return self @classmethod From a620cd24d79cfce4cac92ebff670636ae7fd7fda Mon Sep 17 00:00:00 2001 From: kparasch Date: Mon, 16 Feb 2026 13:27:25 +0100 Subject: [PATCH 70/70] small bugfix --- pySC/apps/measurements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pySC/apps/measurements.py b/pySC/apps/measurements.py index c014613..0ff15fc 100644 --- a/pySC/apps/measurements.py +++ b/pySC/apps/measurements.py @@ -38,7 +38,7 @@ def orbit_correction(interface: AbstractInterface, response_matrix: ResponseMatr if apply: data = interface.get_many(correctors) - for corr in correctors: + for corr in trims.keys(): data[corr] += trims[corr] * gain interface.set_many(data) if rf and trims['rf'] != 0: