44
55Data handling for turn-by-turn measurement files from the ``SPS`` (files in **SDDS** format).
66"""
7+ from __future__ import annotations
8+
79import logging
810from datetime import datetime
911from pathlib import Path
1012import re
11- from typing import Union
13+ from typing import TYPE_CHECKING
1214
1315import numpy as np
1416import pandas as pd
1820from turn_by_turn .ascii import is_ascii_file , read_tbt as read_ascii
1921from turn_by_turn .structures import TbtData , TransverseData
2022
23+ if TYPE_CHECKING :
24+ from collections .abc import Sequence
25+
2126LOGGER = logging .getLogger (__name__ )
2227
28+
2329# IDs
2430N_TURNS : str = "nbOfTurns"
2531TIMESTAMP : str = "timestamp"
2632BPM_NAMES : str = "MonNames"
2733BPM_PLANES : str = "MonPlanes"
2834
2935
30- def read_tbt (file_path : Union [ str , Path ] , remove_trailing_bpm_plane : bool = True ) -> TbtData :
36+ def read_tbt (file_path : str | Path , remove_trailing_bpm_plane : bool = True ) -> TbtData :
3137 """
3238 Reads turn-by-turn data from the ``SPS``'s **SDDS** format file.
3339 Will first determine if it is in ASCII format to figure out which reading method to use.
3440
3541 Args:
36- file_path (Union[ str, Path] ): path to the turn-by-turn measurement file.
42+ file_path (str | Path): path to the turn-by-turn measurement file.
3743 remove_trailing_bpm_plane (bool, optional): if ``True``, will remove the trailing
38- BPM plane ('.H', '.V') from the BPM-names.
44+ BPM plane ('.H', '.V') from the BPM-names.
3945 This makes the measurement data compatible with the madx-models.
4046 Defaults to ``True``.
4147
@@ -51,16 +57,16 @@ def read_tbt(file_path: Union[str, Path], remove_trailing_bpm_plane: bool = True
5157 sdds_file = sdds .read (file_path )
5258
5359 nturns = sdds_file .values [N_TURNS ]
54- date = datetime .fromtimestamp (sdds_file .values [TIMESTAMP ] / 1e9 , tz = tz .tzutc ())
55- bpm_names = np .array (sdds_file .values [BPM_NAMES ])
56- bpm_planes = np .array (sdds_file .values [BPM_PLANES ]).astype (bool )
60+ date = datetime .fromtimestamp (sdds_file .values [TIMESTAMP ] / 1e9 , tz = tz .tzutc ())
5761
58- bpm_names_y = bpm_names [bpm_planes ]
59- bpm_names_x = bpm_names [~ bpm_planes ]
62+ bpm_names_x , bpm_names_y = _split_bpm_names_to_planes (
63+ sdds_file .values [BPM_NAMES ],
64+ sdds_file .values [BPM_PLANES ]
65+ )
6066
6167 tbt_data_x = [sdds_file .values [bpm ] for bpm in bpm_names_x ]
6268 tbt_data_y = [sdds_file .values [bpm ] for bpm in bpm_names_y ]
63-
69+
6470 if remove_trailing_bpm_plane :
6571 pattern = re .compile (r"\.[HV]$" , flags = re .IGNORECASE )
6672 bpm_names_x = [pattern .sub ("" , bpm ) for bpm in bpm_names_x ]
@@ -76,17 +82,62 @@ def read_tbt(file_path: Union[str, Path], remove_trailing_bpm_plane: bool = True
7682 return TbtData (matrices , date , [0 ], nturns )
7783
7884
79- def write_tbt (output_path : Union [str , Path ], tbt_data : TbtData , add_trailing_bpm_plane : bool = True ) -> None :
85+ def _split_bpm_names_to_planes (bpm_names : Sequence [str ], bpm_planes : Sequence [int ] = ()) -> tuple [np .ndarray , np .ndarray ]:
86+ """ Splits BPM names into X and Y planes.
87+
88+ In the past, this was done by using the ``MonPlanes`` array, but in 2025 the SPS output changed from using
89+ `1` for vertical and `0` for horizontal to `3` for vertical and `1` for horizontal.
90+ It is therefore probably better to split based on the naming of the BPMs.
91+
92+ Args:
93+ bpm_names (Sequence[str]): BPM names
94+ bpm_planes (Sequence[int], optional): BPM planes
95+
96+ Returns:
97+ tuple[np.ndarray, np.ndarray]: BPM names for X and Y planes
98+ """
99+ bpm_names = pd .Series (bpm_names )
100+
101+ if bpm_names .str .match (r".+\.[HV]$" , flags = re .IGNORECASE ).all ():
102+ # all names end in .V or .H -> split by name
103+ vertical_bpms = bpm_names .str .endswith (".V" )
104+ else :
105+ LOGGER .warning (
106+ "Could not determine BPM planes from BPM names. "
107+ "Splitting by the 'MonPlanes' array, which might be subject to changes."
108+ )
109+ if 3 in bpm_planes : # 2025 format splitting: 3 for vertical, 1 for horizontal
110+ vertical_bpms = np .array (bpm_planes ) == 3
111+
112+ elif 0 in bpm_planes : # pre-2025 format splitting: 1 for vertical, 0 for horizontal
113+ vertical_bpms = np .array (bpm_planes ).astype (bool )
114+
115+ else :
116+ msg = "Could not determine SPS file format to split BPMs into planes."
117+ raise ValueError (msg )
118+
119+ bpm_names_y = bpm_names [vertical_bpms ].to_numpy ()
120+ bpm_names_x = bpm_names [~ vertical_bpms ].to_numpy ()
121+
122+ return bpm_names_x , bpm_names_y
123+
124+
125+ def write_tbt (output_path : str | Path , tbt_data : TbtData , add_trailing_bpm_plane : bool = True ) -> None :
80126 """
81127 Write a ``TbtData`` object's data to file, in a ``SPS``'s **SDDS** format.
82- The format is reduced to the necessary parameters used by the reader.
128+ The format is reduced to the minimum parameters used by the reader.
129+
130+ WARNING: This writer uses ``0`` for horizontal and ``1`` for vertical BPMs
131+ in the ``MonPlanes`` array, i.e. the pre-2025 format.
83132
84133 Args:
85- output_path (Union[ str, Path] ): path to a the disk location where to write the data.
134+ output_path (str | Path): path to a the disk location where to write the data.
86135 tbt_data (TbtData): the ``TbtData`` object to write to disk.
87136 add_trailing_bpm_plane (bool, optional): if ``True``, will add the trailing
88137 BPM plane ('.H', '.V') to the BPM-names. This assures that all BPM-names are unique,
89138 and that the measurement data is compatible with the sdds files from the FESA-class.
139+ WARNING: If present, these will be used to determine the plane of the BPMs,
140+ otherwise the ``MonPlanes`` array will be used.
90141 Defaults to ``True``.
91142 """
92143 output_path = Path (output_path )
0 commit comments