Skip to content

Commit 7efbe56

Browse files
authored
Fix LHC BestKnowledge Model fetcher (#487)
* b2 error check * modified for abs/rel paths, added source
1 parent b1a09ab commit 7efbe56

File tree

6 files changed

+107
-78
lines changed

6 files changed

+107
-78
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# OMC3 Changelog
22

3+
#### 2025-03-06 - v0.22.1 - _jdilly_
4+
5+
- Fixed:
6+
- Error in list b2 error choices for lhc best knowledge models.
7+
38
#### 2025-03-05 - v0.22.0 - _jdilly_
49

510
- Added:

omc3/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
__title__ = "omc3"
1212
__description__ = "An accelerator physics tools package for the OMC team at CERN."
1313
__url__ = "https://github.com/pylhc/omc3"
14-
__version__ = "0.22.0"
14+
__version__ = "0.22.1"
1515
__author__ = "pylhc"
1616
__author_email__ = "[email protected]"
1717
__license__ = "MIT"

omc3/model/accelerators/lhc.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,6 @@ def get_parameters():
144144
type=str,
145145
help="The B2 error table to load for the best knowledge model.",
146146
)
147-
params.add_parameter(
148-
name="list_b2_errors",
149-
action="store_true",
150-
help="Lists all available b2 error tables",
151-
)
152147
return params
153148

154149
def __init__(self, *args, **kwargs):
@@ -158,7 +153,6 @@ def __init__(self, *args, **kwargs):
158153
self.year = opt.year
159154
self.ats = opt.ats
160155
self.b2_errors = opt.b2_errors
161-
self.list_b2_errors = opt.list_b2_errors
162156
self.beam = opt.beam
163157
beam_to_beam_direction = {1: 1, 2: -1}
164158
self.beam_direction = beam_to_beam_direction[self.beam]

omc3/model/model_creators/abstract_model_creator.py

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -289,55 +289,6 @@ def _find_modifier(self, modifier: Path | str) -> Path:
289289
raise FileNotFoundError(msg)
290290

291291

292-
def check_folder_choices(
293-
parent: Path,
294-
msg: str,
295-
selection: str,
296-
list_choices: bool = False,
297-
predicate=iotools.always_true,
298-
) -> Path:
299-
"""
300-
A helper function that scans a selected folder for children, which will then be displayed as possible choices.
301-
This funciton allows the model-creator to get only the file/folder names, check
302-
in the desired folder if the choice is present and return the full path to the selected folder.
303-
304-
Args:
305-
parent (Path): The folder to scan.
306-
msg (str): The message to display, on failure.
307-
selection (str): The current selection.
308-
list_choices (bool): Whether to just list the choices.
309-
In that case `None` is returned, instead of an error
310-
predicate (callable): A function that takes a path and returns True.
311-
if the path results in a valid choice.
312-
313-
Returns:
314-
Path: Full path of the selected choice in `parent`.
315-
316-
Examples:
317-
Let's say we expect a choice for a sequence file in the folder `model_root`.
318-
319-
```
320-
check_folder_choices(model_root, "Expected sequence file", predicate=lambda p: p.suffix == ".seq")
321-
```
322-
323-
Or we want all subfolder of `scenarios`
324-
325-
```
326-
check_folder_choices(scenarios, "Expected scenario folder", predicate=lambda p: p.is_dir())
327-
```
328-
"""
329-
choices = [d.name for d in parent.iterdir() if predicate(d)]
330-
331-
if selection in choices:
332-
return parent / selection
333-
334-
if list_choices:
335-
for choice in choices:
336-
print(choice)
337-
raise AcceleratorDefinitionError(f"{msg}.\nSelected: '{selection}'.\nChoices: [{', '.join(choices)}]")
338-
339-
340-
341292
class SegmentCreator(ModelCreator, ABC):
342293
""" Model creator for Segments, to be used in the Segment-by-Segment algorithm.
343294
These segments propagate the measured values from the beginning of the segment to the end.
@@ -435,3 +386,55 @@ def __init__(self, accel: Accelerator, twiss_out: Path | str, corr_files: Sequen
435386
self.logfile= self.twiss_out.parent.absolute() / f"job.create_{self.twiss_out.stem}.log"
436387
self.corr_files = corr_files
437388
self.update_dpp = update_dpp
389+
390+
391+
# Helper functions -------------------------------------------------------------
392+
393+
def check_folder_choices(
394+
parent: Path,
395+
msg: str,
396+
selection: str,
397+
list_choices: bool = False,
398+
predicate=iotools.always_true,
399+
stem_only: bool = False,
400+
) -> Path | str:
401+
"""
402+
A helper function that scans a selected folder for children, which will then be displayed as possible choices.
403+
This funciton allows the model-creator to get only the file/folder names, check
404+
in the desired folder if the choice is present and return the full path to the selected folder.
405+
406+
Args:
407+
parent (Path): The folder to scan.
408+
msg (str): The message to display, on failure.
409+
selection (str): The current selection.
410+
list_choices (bool): Whether to just list the choices.
411+
In that case `None` is returned, instead of an error
412+
predicate (callable): A function that takes a path and returns True.
413+
if the path results in a valid choice.
414+
stem_only (bool): If True, only the stem of the path is checked.
415+
416+
Returns:
417+
Path: Full path of the selected choice in `parent`.
418+
419+
Examples:
420+
Let's say we expect a choice for a sequence file in the folder `model_root`.
421+
422+
```
423+
check_folder_choices(model_root, "Expected sequence file", predicate=lambda p: p.suffix == ".seq")
424+
```
425+
426+
Or we want all subfolder of `scenarios`
427+
428+
```
429+
check_folder_choices(scenarios, "Expected scenario folder", predicate=lambda p: p.is_dir())
430+
```
431+
"""
432+
choices = [d.stem if stem_only else d.name for d in parent.iterdir() if predicate(d)]
433+
434+
if selection in choices:
435+
return parent / selection
436+
437+
if list_choices:
438+
for choice in choices:
439+
print(choice)
440+
raise AcceleratorDefinitionError(f"{msg}.\nSelected: '{selection}'.\nChoices: [{', '.join(choices)}]")

omc3/model/model_creators/lhc_model_creator.py

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
SegmentCreator,
5252
check_folder_choices,
5353
)
54-
from omc3.utils.iotools import create_dirs, get_check_suffix_func
54+
from omc3.optics_measurements.constants import NAME
55+
from omc3.utils.iotools import create_dirs, get_check_by_regex_func, get_check_suffix_func
5556

5657
if TYPE_CHECKING:
5758
from collections.abc import Sequence
@@ -104,7 +105,7 @@ def prepare_options(self, opt):
104105
return
105106

106107
# list optics choices ---
107-
if opt.list_choices: # assumes we want to list optics. Invoked even if modifiers are given!
108+
if opt.list_choices: # assumes we only want to list optics. Invoked even if modifiers are given!
108109
check_folder_choices(
109110
acc_model_path / OPTICS_SUBDIR,
110111
msg="No modifier given",
@@ -399,16 +400,32 @@ class LhcBestKnowledgeCreator(LhcModelCreator): # -----------------------------
399400
jobfile: str = JOB_MODEL_MADX_BEST_KNOWLEDGE
400401
files_to_check: tuple[str] = (TWISS_BEST_KNOWLEDGE_DAT, TWISS_ELEMENTS_BEST_KNOWLEDGE_DAT)
401402

402-
def check_options(self, opt) -> bool:
403+
def prepare_options(self, opt) -> bool:
403404
accel: Lhc = self.accel
404-
if accel.list_b2_errors:
405-
errors_dir = AFS_B2_ERRORS_ROOT / f"Beam{accel.beam}"
406-
for d in errors_dir.iterdir():
407-
if d.suffix==".errors" and d.name.startswith("MB2022"):
408-
print(d.stem)
409-
return False
410405

411-
return super().check_options(opt)
406+
# Check given B2 errors ---
407+
exists = False
408+
if accel.b2_errors is not None:
409+
# check if user gave absolute path (! with_suffix does not work, due to '.' in name)
410+
exists = Path(f"{accel.b2_errors!s}.errors").is_file()
411+
412+
if not exists:
413+
afs_dir = AFS_B2_ERRORS_ROOT / f"Beam{accel.beam}"
414+
if not AFS_B2_ERRORS_ROOT.is_dir():
415+
raise AcceleratorDefinitionError(
416+
f"Given b2 errors do not exist and neither does the AFS path: '{afs_dir!s}'."
417+
)
418+
419+
accel.b2_errors = check_folder_choices( # returns full path
420+
afs_dir,
421+
msg="No valid b2 errors given.",
422+
selection=accel.b2_errors,
423+
list_choices=opt.list_choices,
424+
predicate=get_check_by_regex_func(r"MB2022.+\.errors$"),
425+
stem_only=True,
426+
) # raises AcceleratorDefinitionError
427+
428+
return super().prepare_options(opt)
412429

413430
def check_accelerator_instance(self):
414431
accel: Lhc = self.accel
@@ -430,24 +447,29 @@ def prepare_run(self):
430447

431448
LOGGER.debug("Copying B2 error tables")
432449

433-
b2_error_path = AFS_B2_ERRORS_ROOT / f"Beam{accel.beam}" / f"{accel.b2_errors}.errors"
434-
b2_madx_path = AFS_B2_ERRORS_ROOT / f"Beam{accel.beam}" / f"{accel.b2_errors}.madx"
435-
shutil.copy(
436-
b2_madx_path,
437-
accel.model_dir / B2_SETTINGS_MADX,
438-
)
439-
b2_table = tfs.read(b2_error_path, index="NAME")
450+
b2_stem_path = Path(f"{accel.b2_errors!s}.fakesuffix") # makes `with_suffix`` work
451+
# hint: if b2_stem_path is absolute, the following AFS parts are ignored
452+
b2_error_path = AFS_B2_ERRORS_ROOT / f"Beam{accel.beam}" / b2_stem_path.with_suffix(".errors")
453+
b2_madx_path = AFS_B2_ERRORS_ROOT / f"Beam{accel.beam}" / b2_stem_path.with_suffix(".madx")
454+
455+
# copy madx ---
456+
output_madx = accel.model_dir / B2_SETTINGS_MADX
457+
content = f"! Source: {b2_madx_path}\n{b2_madx_path.read_text()}"
458+
output_madx.write_text(content)
459+
460+
# copy errors table ---
461+
b2_table = tfs.read(b2_error_path, index=NAME)
440462
gen_df = pd.DataFrame(
441-
data=np.zeros((b2_table.index.size, len(_b2_columns()))),
463+
data=0.,
442464
index=b2_table.index,
443465
columns=_b2_columns(),
444466
)
445467
gen_df["K1L"] = b2_table.loc[:, "K1L"].to_numpy()
446468
tfs.write(
447469
accel.model_dir / B2_ERRORS_TFS,
448470
gen_df,
449-
headers_dict={"NAME": "EFIELD", "TYPE": "EFIELD"},
450-
save_index="NAME",
471+
headers_dict={"NAME": "EFIELD", "TYPE": "EFIELD", "SOURCE": b2_error_path},
472+
save_index=NAME,
451473
)
452474

453475
def get_madx_script(self) -> str:

omc3/utils/iotools.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,13 @@ def always_true(*args, **kwargs) -> bool:
264264

265265

266266
def get_check_suffix_func(suffix: str) -> Callable[[Path],bool]:
267-
""" Returns a function that checks the suffix of a given path agains
268-
the suffix. """
267+
""" Returns a function that checks the suffix of a given path against the suffix. """
269268
def check_suffix(path: Path) -> bool:
270269
return path.suffix == suffix
271270
return check_suffix
271+
272+
def get_check_by_regex_func(pattern: str) -> Callable[[Path],bool]:
273+
""" Returns a function that checks the name of a given path against the pattern. """
274+
def check(path: Path) -> bool:
275+
return re.match(pattern, path.name) is not None
276+
return check

0 commit comments

Comments
 (0)