Skip to content

Commit 515667d

Browse files
committed
updated SPS reader
1 parent a6f71aa commit 515667d

File tree

3 files changed

+94
-19
lines changed

3 files changed

+94
-19
lines changed

tests/test_sps.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,32 +38,56 @@ def test_write_read(tmp_path):
3838
)
3939
tmp_sdds = tmp_path / "sps_fake_data.sdds"
4040
# Normal read/write test
41-
sps.write_tbt(tmp_sdds, original, add_trailing_bpm_plane=False)
41+
sps.write_tbt(tmp_sdds, original, add_trailing_bpm_plane=False)
4242
read_sdds = sps.read_tbt(tmp_sdds, remove_trailing_bpm_plane=False)
4343
compare_tbt(original, read_sdds, no_binary=True)
4444

4545
# Test no name changes when writing and planes already present
46-
sps.write_tbt(tmp_sdds, original, add_trailing_bpm_plane=True)
46+
sps.write_tbt(tmp_sdds, original, add_trailing_bpm_plane=True)
4747
read_sdds = sps.read_tbt(tmp_sdds, remove_trailing_bpm_plane=False)
4848
compare_tbt(original, read_sdds, no_binary=True)
49-
49+
5050
# Test plane removal on reading
5151
read_sdds = sps.read_tbt(tmp_sdds, remove_trailing_bpm_plane=True)
5252
assert not any(read_sdds.matrices[0].X.index.str.endswith(".H"))
5353
assert not any(read_sdds.matrices[0].Y.index.str.endswith(".V"))
54-
54+
5555
# Test planes stay off when writing
5656
sps.write_tbt(tmp_sdds, read_sdds, add_trailing_bpm_plane=False)
5757
read_sdds = sps.read_tbt(tmp_sdds, remove_trailing_bpm_plane=False)
5858
assert not any(read_sdds.matrices[0].X.index.str.endswith(".H"))
5959
assert not any(read_sdds.matrices[0].Y.index.str.endswith(".V"))
6060

6161
# Test adding planes again
62-
sps.write_tbt(tmp_sdds, read_sdds, add_trailing_bpm_plane=True)
62+
sps.write_tbt(tmp_sdds, read_sdds, add_trailing_bpm_plane=True)
6363
read_sdds = sps.read_tbt(tmp_sdds, remove_trailing_bpm_plane=False)
6464
compare_tbt(original, read_sdds, no_binary=True)
6565

6666

67+
def test_split_function():
68+
""" Tests that the splitting function into planes works as expected. """
69+
70+
names_with_planes = ("bpm1.H", "bpm2.V", "bpm3.V", "bpm4.H")
71+
x, y = sps._split_bpm_names_to_planes(names_with_planes)
72+
73+
assert all([bpm in x for bpm in ("bpm1.H", "bpm4.H")])
74+
assert all([bpm in y for bpm in ("bpm2.V", "bpm3.V")])
75+
76+
names_without_planes = ("bpm1", "bpm2", "bpm3", "bpm4")
77+
planes = (0, 1, 1, 0)
78+
x, y = sps._split_bpm_names_to_planes(names_without_planes, planes)
79+
80+
for bpm, plane in zip(names_without_planes, planes):
81+
assert bpm in x if plane == 0 else bpm in y
82+
assert bpm not in x if plane == 1 else bpm not in y
83+
84+
planes = (1, 3, 1, 3)
85+
x, y = sps._split_bpm_names_to_planes(names_without_planes, planes)
86+
87+
for bpm, plane in zip(names_without_planes, planes):
88+
assert bpm in x if plane == 1 else bpm in y
89+
assert bpm not in x if plane == 3 else bpm not in y
90+
6791

6892
@pytest.fixture()
6993
def _sps_file() -> Path:

turn_by_turn/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
__title__ = "turn_by_turn"
77
__description__ = "Read and write turn-by-turn measurement files from different particle accelerator formats."
88
__url__ = "https://github.com/pylhc/turn_by_turn"
9-
__version__ = "0.9.0"
9+
__version__ = "0.9.1"
1010
__author__ = "pylhc"
1111
__author_email__ = "[email protected]"
1212
__license__ = "MIT"

turn_by_turn/sps.py

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
55
Data handling for turn-by-turn measurement files from the ``SPS`` (files in **SDDS** format).
66
"""
7+
from __future__ import annotations
8+
79
import logging
810
from datetime import datetime
911
from pathlib import Path
1012
import re
11-
from typing import Union
13+
from typing import TYPE_CHECKING
1214

1315
import numpy as np
1416
import pandas as pd
@@ -18,24 +20,28 @@
1820
from turn_by_turn.ascii import is_ascii_file, read_tbt as read_ascii
1921
from turn_by_turn.structures import TbtData, TransverseData
2022

23+
if TYPE_CHECKING:
24+
from collections.abc import Sequence
25+
2126
LOGGER = logging.getLogger(__name__)
2227

28+
2329
# IDs
2430
N_TURNS: str = "nbOfTurns"
2531
TIMESTAMP: str = "timestamp"
2632
BPM_NAMES: str = "MonNames"
2733
BPM_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

Comments
 (0)