From c67b9c97263525a780cbc59861de9818d6905301 Mon Sep 17 00:00:00 2001 From: Axel Huebl Date: Wed, 5 Aug 2026 23:45:15 -0700 Subject: [PATCH] Model the set and sets Commands A `facility` entry keyed by a reserved node keyword is a command, not a lattice element. `sets` carries a sequence, so it never got past the `{name: properties}` unpacker; `set` did get past it and was read as whichever element of the union happened to accept it (a BeamBeam). Add a `commands` package holding SetCommand and SetsCommand, per the standard's Setting Parameters section, and recognize their keywords in the facility list only. Values are recorded verbatim, whether a number or an expression string: this is the exact representation, so nothing is evaluated. Clears unit_tests/sets/sets_compact.pals.yaml from the known-failures list, and makes set_pattern.pals.yaml and set_single_definition.pals.yaml pass for the right reason. Since a bare PASS turned out to prove so little, the corpus validator grows a spot-check table. Co-Authored-By: Claude Opus 5 (1M context) --- src/pals/PALS.py | 8 +- src/pals/__init__.py | 1 + src/pals/commands/FacilityCommand.py | 19 ++ src/pals/commands/SetCommand.py | 53 ++++++ src/pals/commands/SetsCommand.py | 32 ++++ src/pals/commands/__init__.py | 7 + src/pals/commands/all_commands.py | 76 ++++++++ src/pals/kinds/mixin/all_element_mixin.py | 24 ++- tests/pals_files/sets/bad_set_key.pals.yaml | 8 + .../sets/bad_set_sequence.pals.yaml | 5 + .../pals_files/sets/bad_sets_entry.pals.yaml | 6 + .../sets/bad_sets_mapping.pals.yaml | 5 + tests/pals_files/sets/set_command.pals.yaml | 20 +++ tests/pals_files/sets/sets_compact.pals.yaml | 14 ++ tests/standard_examples_known_failures.txt | 5 +- tests/test_commands.py | 165 ++++++++++++++++++ tests/validate_standard_examples.py | 60 +++++-- 17 files changed, 486 insertions(+), 22 deletions(-) create mode 100644 src/pals/commands/FacilityCommand.py create mode 100644 src/pals/commands/SetCommand.py create mode 100644 src/pals/commands/SetsCommand.py create mode 100644 src/pals/commands/__init__.py create mode 100644 src/pals/commands/all_commands.py create mode 100644 tests/pals_files/sets/bad_set_key.pals.yaml create mode 100644 tests/pals_files/sets/bad_set_sequence.pals.yaml create mode 100644 tests/pals_files/sets/bad_sets_entry.pals.yaml create mode 100644 tests/pals_files/sets/bad_sets_mapping.pals.yaml create mode 100644 tests/pals_files/sets/set_command.pals.yaml create mode 100644 tests/pals_files/sets/sets_compact.pals.yaml create mode 100644 tests/test_commands.py diff --git a/src/pals/PALS.py b/src/pals/PALS.py index 0bc4a13..14bb087 100644 --- a/src/pals/PALS.py +++ b/src/pals/PALS.py @@ -3,12 +3,14 @@ from pydantic import model_validator from typing import Self +from .commands.all_commands import get_all_command_types from .kinds import Lattice from .kinds.all_elements import get_all_elements_as_annotation from .functions import load_file_to_dict, store_dict_to_file -Facility = list[get_all_elements_as_annotation()] +# Unlike an element list, the facility list also holds commands. +Facility = list[get_all_elements_as_annotation(extra_types=get_all_command_types())] class Author(BaseModel): @@ -95,7 +97,9 @@ def unpack_json_structure(cls, data): if data.get("facility") is not None: if not isinstance(data["facility"], list): raise TypeError("'facility' must be a list") - data["facility"] = unpack_element_items(data["facility"], "facility") + data["facility"] = unpack_element_items( + data["facility"], "facility", allow_commands=True + ) return data diff --git a/src/pals/__init__.py b/src/pals/__init__.py index 30548f9..5688d12 100644 --- a/src/pals/__init__.py +++ b/src/pals/__init__.py @@ -4,6 +4,7 @@ simpler import statements like `from pals import Drift`. """ +from .commands import * # noqa from .kinds import * # noqa from .parameters import * # noqa from .PALS import Author, ExtensionLabels, PALSroot, load, store # noqa diff --git a/src/pals/commands/FacilityCommand.py b/src/pals/commands/FacilityCommand.py new file mode 100644 index 0000000..ced58c7 --- /dev/null +++ b/src/pals/commands/FacilityCommand.py @@ -0,0 +1,19 @@ +from pydantic import BaseModel + + +# A command value is either a number or an expression written as a string, per +# the standard's Expression type. Expressions are recorded verbatim: this +# implementation builds the exact representation of a PALS file and does not +# evaluate them. +CommandValue = bool | int | float | str | None + + +class FacilityCommand(BaseModel, extra="forbid"): + """A `facility` entry that is a command rather than a lattice element. + + Element entries are one-key mappings whose key is the element's name; + command entries are one-key mappings whose key is a reserved node keyword + (`set`, `sets`, ...). Commands keep their place in the ordered `facility` + list, because the standard's lattice expansion only lets a command act on + what was defined before it. + """ diff --git a/src/pals/commands/SetCommand.py b/src/pals/commands/SetCommand.py new file mode 100644 index 0000000..e0de680 --- /dev/null +++ b/src/pals/commands/SetCommand.py @@ -0,0 +1,53 @@ +from pydantic import model_serializer + +from .FacilityCommand import CommandValue, FacilityCommand + + +class SetCommand(FacilityCommand): + """The `set` command, which writes a parameter of the elements it matches. + + See the standard's Setting Parameters section. `parameter` is a pattern, so + one command can write several elements. In the `value` expression, + `PARAMETER` is the current value of the parameter being written and `SELF` + is the element it belongs to. + + If both `absolute_error` and `relative_error` are given, the true error is + `absolute_error + relative_error * |value|`. The standard documents both as + defaulting to zero; they default to `None` here so that a value the file did + not write stays out of the serialized output. + + Note that `set` is a reserved key of the `facility` list: an element cannot + be named `set`. + + Example: + >>> SetCommand(parameter="B1.*>BendP.e1", value="2*PARAMETER") + SetCommand(parameter='B1.*>BendP.e1', value='2*PARAMETER', ...) + """ + + parameter: str + value: CommandValue = None + absolute_error: float | None = None + relative_error: float | None = None + + @model_serializer(mode="plain") + def _serialize_as_command(self) -> dict: + """Serialize back into the one-key `set:` form the standard writes. + + A plain serializer (rather than a `model_dump` override, as the + elements use) also covers the case where the enclosing model serializes + the whole `facility` union at once. + """ + return {"set": self.command_body()} + + def command_body(self) -> dict: + """The command's properties, without the ones the file did not set. + + Unset properties are dropped, the same way `exclude_none` drops them + for elements and for the root node. + """ + body = {"parameter": self.parameter} + for key in ("value", "absolute_error", "relative_error"): + value = getattr(self, key) + if value is not None: + body[key] = value + return body diff --git a/src/pals/commands/SetsCommand.py b/src/pals/commands/SetsCommand.py new file mode 100644 index 0000000..3def292 --- /dev/null +++ b/src/pals/commands/SetsCommand.py @@ -0,0 +1,32 @@ +from pydantic import model_serializer + +from .FacilityCommand import FacilityCommand +from .SetCommand import SetCommand + + +class SetsCommand(FacilityCommand): + """The compact `sets` command: a sequence of parameter/value pairs. + + See the standard's Setting Parameters section, which offers this form for + sets that only have a value: + + ```yaml + - sets: + - param1: value1 + - param2: value2 + ``` + + Each pair is held as a `SetCommand`, so a reader can treat the compact form + and the `set` form alike; the compact form is what gets written back out. + Being a sequence, it keeps its order and may name the same parameter twice. + + Note that `sets` is a reserved key of the `facility` list: an element cannot + be named `sets`. + """ + + sets: list[SetCommand] + + @model_serializer(mode="plain") + def _serialize_as_command(self) -> dict: + """Serialize back into the one-key `sets:` form the standard writes.""" + return {"sets": [{pair.parameter: pair.value} for pair in self.sets]} diff --git a/src/pals/commands/__init__.py b/src/pals/commands/__init__.py new file mode 100644 index 0000000..34bb965 --- /dev/null +++ b/src/pals/commands/__init__.py @@ -0,0 +1,7 @@ +"""Re-export commonly used classes from submodules so callers can use +simpler import statements like `from pals import SetCommand`. +""" + +from .FacilityCommand import FacilityCommand # noqa: F401 +from .SetCommand import SetCommand # noqa: F401 +from .SetsCommand import SetsCommand # noqa: F401 diff --git a/src/pals/commands/all_commands.py b/src/pals/commands/all_commands.py new file mode 100644 index 0000000..02ac33d --- /dev/null +++ b/src/pals/commands/all_commands.py @@ -0,0 +1,76 @@ +"""Helper module that knows every command the `facility` list can hold. + +This is the one place that maps a reserved node keyword (`set`, `sets`, ...) to +the model that holds it, so adding a command means touching this module and +nothing else. +""" + +from .FacilityCommand import FacilityCommand +from .SetCommand import SetCommand +from .SetsCommand import SetsCommand + + +def _build_set(body) -> SetCommand: + """Build a `set` command from its properties mapping.""" + if not isinstance(body, dict): + raise TypeError( + f"Value for the 'set' command must be a dict (the command's properties), " + f"but we got {body!r}" + ) + return SetCommand(**body) + + +def _build_sets(body) -> SetsCommand: + """Build a `sets` command from its sequence of parameter/value pairs.""" + if not isinstance(body, list): + raise TypeError( + f"Value for the 'sets' command must be a list of parameter/value pairs, " + f"but we got {body!r}" + ) + + pairs = [] + for entry in body: + if not isinstance(entry, dict) or len(entry) != 1: + raise ValueError( + f"Each 'sets' entry must be a dict with exactly one key (the " + f"parameter to set), but we got {entry!r}" + ) + parameter, value = next(iter(entry.items())) + pairs.append(SetCommand(parameter=parameter, value=value)) + + return SetsCommand(sets=pairs) + + +# Reserved keys of the `facility` list, and how to build what they hold. A +# facility entry keyed by one of these is a command, not an element, so an +# element cannot carry one of these names. +_COMMAND_BUILDERS = { + "set": _build_set, + "sets": _build_sets, +} + + +def get_all_command_types() -> tuple: + """Return a tuple of all command types that can appear in a facility.""" + return (SetCommand, SetsCommand) + + +def build_facility_command(item) -> FacilityCommand | None: + """Build the command a raw facility entry holds. + + Args: + item: One raw entry of the `facility` list + + Returns: + The command, or None if the entry is not a command and should be + unpacked as a lattice element instead + """ + if not isinstance(item, dict) or len(item) != 1: + return None + + keyword, body = next(iter(item.items())) + builder = _COMMAND_BUILDERS.get(keyword) + if builder is None: + return None + + return builder(body) diff --git a/src/pals/kinds/mixin/all_element_mixin.py b/src/pals/kinds/mixin/all_element_mixin.py index 3b40795..550a63c 100644 --- a/src/pals/kinds/mixin/all_element_mixin.py +++ b/src/pals/kinds/mixin/all_element_mixin.py @@ -8,7 +8,9 @@ from ..PlaceholderName import PlaceholderName -def unpack_element_items(items: list, container_type: str) -> list: +def unpack_element_items( + items: list, container_type: str, allow_commands: bool = False +) -> list: """Deserialize the JSON/YAML/...-like items of an element list. Each item can be a reference string, a `use:` reference, a one-key dict @@ -17,13 +19,31 @@ def unpack_element_items(items: list, container_type: str) -> list: Args: items: The list of raw element entries container_type: Type of container for error messages (e.g., "line" or "union") + allow_commands: Whether entries keyed by a reserved command keyword + (`set`, `sets`, ...) are commands rather than elements. Only the + `facility` list holds commands; element lists do not. Returns: - A new list with each entry unpacked to a dict, PlaceholderName, or element + A new list with each entry unpacked to a dict, PlaceholderName, command, + or element """ + from pals.commands.all_commands import build_facility_command + from pals.commands.FacilityCommand import FacilityCommand + new_list = [] # Loop over all elements in the list for item in items: + # A facility entry keyed by a reserved keyword is a command, which is + # checked before the `{name: properties}` element form below. + if allow_commands: + if isinstance(item, FacilityCommand): + new_list.append(item) + continue + command = build_facility_command(item) + if command is not None: + new_list.append(command) + continue + # An element can be a string that refers to another element if isinstance(item, str): # Wrap the string in a Placeholder name object diff --git a/tests/pals_files/sets/bad_set_key.pals.yaml b/tests/pals_files/sets/bad_set_key.pals.yaml new file mode 100644 index 0000000..7073006 --- /dev/null +++ b/tests/pals_files/sets/bad_set_key.pals.yaml @@ -0,0 +1,8 @@ +# The `set` command has a fixed set of properties, so a misspelled one is an +# error rather than a silently kept extra. +PALS: + facility: + - set: + parameter: d1>length + value: 1.0 + absolut_error: 0.001 diff --git a/tests/pals_files/sets/bad_set_sequence.pals.yaml b/tests/pals_files/sets/bad_set_sequence.pals.yaml new file mode 100644 index 0000000..70dff45 --- /dev/null +++ b/tests/pals_files/sets/bad_set_sequence.pals.yaml @@ -0,0 +1,5 @@ +# The `set` command holds its properties in a mapping, not a sequence. +PALS: + facility: + - set: + - parameter: d1>length diff --git a/tests/pals_files/sets/bad_sets_entry.pals.yaml b/tests/pals_files/sets/bad_sets_entry.pals.yaml new file mode 100644 index 0000000..72cc0fd --- /dev/null +++ b/tests/pals_files/sets/bad_sets_entry.pals.yaml @@ -0,0 +1,6 @@ +# Each `sets` entry writes one parameter, so it holds exactly one key. +PALS: + facility: + - sets: + - d1>length: 1.0 + q1>length: 2.0 diff --git a/tests/pals_files/sets/bad_sets_mapping.pals.yaml b/tests/pals_files/sets/bad_sets_mapping.pals.yaml new file mode 100644 index 0000000..06c5fc6 --- /dev/null +++ b/tests/pals_files/sets/bad_sets_mapping.pals.yaml @@ -0,0 +1,5 @@ +# The compact `sets` form is a sequence of pairs, not a mapping. +PALS: + facility: + - sets: + d1>length: 1.0 diff --git a/tests/pals_files/sets/set_command.pals.yaml b/tests/pals_files/sets/set_command.pals.yaml new file mode 100644 index 0000000..c98b7c9 --- /dev/null +++ b/tests/pals_files/sets/set_command.pals.yaml @@ -0,0 +1,20 @@ +# The `set` command in its mapping form: an expression value that uses +# PARAMETER, a pattern that matches several elements, and the optional error +# terms. +PALS: + version: null + + facility: + - d1: + kind: Drift + length: 1.0 + + - set: + parameter: d1>length + value: 2 * PARAMETER + + - set: + parameter: B1.*>BendP.e1 + value: 0.25 + absolute_error: 0.001 + relative_error: 0.02 diff --git a/tests/pals_files/sets/sets_compact.pals.yaml b/tests/pals_files/sets/sets_compact.pals.yaml new file mode 100644 index 0000000..35cdb5d --- /dev/null +++ b/tests/pals_files/sets/sets_compact.pals.yaml @@ -0,0 +1,14 @@ +# The compact `sets` form: an ordered sequence of parameter/value pairs. Being +# a sequence, it keeps its order and may name the same parameter twice. +PALS: + version: null + + facility: + - d1: + kind: Drift + length: 1.0 + + - sets: + - d1>length: 2 * 1.5 + - q1>MagneticMultipoleP.Kn1L: 0.25 + - d1>length: 3.0 diff --git a/tests/standard_examples_known_failures.txt b/tests/standard_examples_known_failures.txt index ddd81f7..da18211 100644 --- a/tests/standard_examples_known_failures.txt +++ b/tests/standard_examples_known_failures.txt @@ -4,7 +4,6 @@ # to load as an error, so this list shrinks as support lands. Blank lines and # `#` comments are ignored. -# The sequence form of `variables` and the compact `sets` form are not -# modeled. +# The sequence form of `variables` is not modeled, and expressions are not +# accepted where a parameter is typed as a number (e.g. `length: 0.1*log(x)`). unit_tests/expressions/inline_expressions.pals.yaml -unit_tests/sets/sets_compact.pals.yaml diff --git a/tests/test_commands.py b/tests/test_commands.py new file mode 100644 index 0000000..8d84d4b --- /dev/null +++ b/tests/test_commands.py @@ -0,0 +1,165 @@ +"""Tests for the commands a `facility` list can hold. + +A facility entry keyed by a reserved node keyword (`set`, `sets`) is a command +rather than a lattice element. The documents used here live under +tests/pals_files/sets. +""" + +import pathlib + +import pydantic +import pytest + +import pals + +PALS_FILES = pathlib.Path(__file__).parent / "pals_files" +SETS = PALS_FILES / "sets" + + +def test_set_command(): + """The `set` mapping form, with and without the optional error terms.""" + root = pals.load(str(SETS / "set_command.pals.yaml")) + + assert [type(entry).__name__ for entry in root.facility] == [ + "Drift", + "SetCommand", + "SetCommand", + ] + + # An expression value is recorded verbatim; this implementation builds the + # exact representation and does not evaluate it. + first = root.facility[1] + assert first.parameter == "d1>length" + assert first.value == "2 * PARAMETER" + assert first.absolute_error is None + assert first.relative_error is None + + # A numeric value stays numeric, and the error terms are read. + second = root.facility[2] + assert second.parameter == "B1.*>BendP.e1" + assert second.value == 0.25 + assert isinstance(second.value, float) + assert second.absolute_error == 0.001 + assert second.relative_error == 0.02 + + +def test_set_is_not_an_element(): + """A `set` entry is a command, not an element that happens to be so named. + + Before commands were modeled, `set` satisfied the `{name: properties}` + element shape and was read as a lattice element. + """ + root = pals.load(str(SETS / "set_command.pals.yaml")) + + command = root.facility[1] + assert isinstance(command, pals.SetCommand) + assert isinstance(command, pals.FacilityCommand) + assert not isinstance(command, pals.kinds.mixin.BaseElement) + + +def test_sets_compact(): + """The compact `sets` form keeps its order and its repeated parameters.""" + root = pals.load(str(SETS / "sets_compact.pals.yaml")) + + command = root.facility[1] + assert isinstance(command, pals.SetsCommand) + assert [(pair.parameter, pair.value) for pair in command.sets] == [ + ("d1>length", "2 * 1.5"), + ("q1>MagneticMultipoleP.Kn1L", 0.25), + ("d1>length", 3.0), + ] + # Each pair is a `set` of its own, so both forms can be read alike. + assert all(isinstance(pair, pals.SetCommand) for pair in command.sets) + + +def test_set_dump(): + """Commands serialize back into the one-key form the standard writes.""" + root = pals.load(str(SETS / "set_command.pals.yaml")) + facility = root.model_dump()["PALS"]["facility"] + + # Properties the file did not write stay out of the output. + assert facility[1] == {"set": {"parameter": "d1>length", "value": "2 * PARAMETER"}} + assert facility[2] == { + "set": { + "parameter": "B1.*>BendP.e1", + "value": 0.25, + "absolute_error": 0.001, + "relative_error": 0.02, + } + } + + root = pals.load(str(SETS / "sets_compact.pals.yaml")) + facility = root.model_dump()["PALS"]["facility"] + assert facility[1] == { + "sets": [ + {"d1>length": "2 * 1.5"}, + {"q1>MagneticMultipoleP.Kn1L": 0.25}, + {"d1>length": 3.0}, + ] + } + + +@pytest.mark.parametrize("suffix", [".yaml", ".json"]) +@pytest.mark.parametrize("name", ["set_command", "sets_compact"]) +def test_command_roundtrip(tmp_path, name, suffix): + """Commands survive a store/load round trip in every supported format.""" + root = pals.load(str(SETS / f"{name}.pals.yaml")) + + test_file = tmp_path / f"{name}.pals{suffix}" + pals.store(str(test_file), root) + reloaded = pals.load(str(test_file)) + + assert reloaded == root + + +def test_commands_built_in_python(): + """A document assembled from command objects round trips as well.""" + root = pals.PALSroot( + facility=[ + pals.Drift(name="d1", length=1.0), + pals.SetCommand(parameter="d1>length", value=2.0), + pals.SetsCommand(sets=[pals.SetCommand(parameter="d1>length", value=3.0)]), + ] + ) + + assert isinstance(root.facility[1], pals.SetCommand) + assert isinstance(root.facility[2], pals.SetsCommand) + + facility = root.model_dump()["PALS"]["facility"] + assert facility[1] == {"set": {"parameter": "d1>length", "value": 2.0}} + assert facility[2] == {"sets": [{"d1>length": 3.0}]} + + +def test_commands_only_in_the_facility(): + """The standard places commands in the facility, so an element list has no + reserved keys and a `set` entry there is not read as a command.""" + line = pals.BeamLine( + name="line", + line=[{"set": {"parameter": "d1>length", "value": 2.0}}], + ) + + assert not isinstance(line.line[0], pals.FacilityCommand) + + +def test_sets_must_be_a_sequence(): + """The compact form is a sequence of pairs, not a mapping.""" + with pytest.raises(TypeError, match="'sets' command must be a list"): + pals.load(str(SETS / "bad_sets_mapping.pals.yaml")) + + +def test_sets_entry_holds_one_parameter(): + """Each entry of the compact form writes exactly one parameter.""" + with pytest.raises(ValueError, match="exactly one key"): + pals.load(str(SETS / "bad_sets_entry.pals.yaml")) + + +def test_set_must_be_a_mapping(): + """The `set` command holds its properties in a mapping.""" + with pytest.raises(TypeError, match="'set' command must be a dict"): + pals.load(str(SETS / "bad_set_sequence.pals.yaml")) + + +def test_set_rejects_unknown_properties(): + """A misspelled property is an error, not a silently kept extra.""" + with pytest.raises(pydantic.ValidationError, match="absolut_error"): + pals.load(str(SETS / "bad_set_key.pals.yaml")) diff --git a/tests/validate_standard_examples.py b/tests/validate_standard_examples.py index 1ff0543..0516690 100644 --- a/tests/validate_standard_examples.py +++ b/tests/validate_standard_examples.py @@ -5,6 +5,11 @@ section they are sub-level include fragments, spliced into (and read through) the file that includes them. +A few files additionally get structural spot-checks (see SPOT_CHECKS), because +loading without an error is not by itself evidence that a file was read +correctly: a facility entry that matches no element still lands somewhere in +the element union. + Files this implementation cannot read yet are recorded in the known-failures list (tests/standard_examples_known_failures.txt): a failure of a listed file is expected, and a listed file that starts to load is reported so the list @@ -26,7 +31,7 @@ import pals -def check_fodo(lattice): +def check_fodo(root): """Structural spot-checks of the introductory example fodo.pals.yaml.""" from pals.kinds import PlaceholderName from pals.kinds.BeamLine import BeamLine @@ -34,17 +39,41 @@ def check_fodo(lattice): from pals.kinds.Lattice import Lattice from pals.kinds.Quadrupole import Quadrupole - assert isinstance(lattice.facility[0], Drift) - assert lattice.facility[0].name == "drift1" - assert isinstance(lattice.facility[1], Quadrupole) - assert lattice.facility[1].name == "quad1" - assert isinstance(lattice.facility[2], BeamLine) - assert lattice.facility[2].name == "fodo_cell" - assert isinstance(lattice.facility[3], BeamLine) - assert lattice.facility[3].name == "fodo_channel" - assert isinstance(lattice.facility[4], Lattice) - assert lattice.facility[4].name == "fodo_lattice" - assert isinstance(lattice.facility[5], PlaceholderName) + assert isinstance(root.facility[0], Drift) + assert root.facility[0].name == "drift1" + assert isinstance(root.facility[1], Quadrupole) + assert root.facility[1].name == "quad1" + assert isinstance(root.facility[2], BeamLine) + assert root.facility[2].name == "fodo_cell" + assert isinstance(root.facility[3], BeamLine) + assert root.facility[3].name == "fodo_channel" + assert isinstance(root.facility[4], Lattice) + assert root.facility[4].name == "fodo_lattice" + assert isinstance(root.facility[5], PlaceholderName) + + +def check_sets_compact(root): + """Structural spot-checks of unit_tests/sets/sets_compact.pals.yaml. + + A file that merely loads is not evidence that it was read correctly: a + facility entry that matches no element still lands somewhere in the element + union. This asserts the compact `sets` form is read as the command it is. + """ + from pals.commands import SetsCommand + + commands = [item for item in root.facility if isinstance(item, SetsCommand)] + assert len(commands) == 1 + assert [(pair.parameter, pair.value) for pair in commands[0].sets] == [ + ("Q1>MagneticMultipoleP.Kn1L", 0.25), + ("D1>length", "2 * 1.5"), + ] + + +# Files that get structural spot-checks beyond loading, by root-relative path. +SPOT_CHECKS = { + "fodo.pals.yaml": check_fodo, + "unit_tests/sets/sets_compact.pals.yaml": check_sets_compact, +} def read_known_failures(path): @@ -84,9 +113,10 @@ def main(): error = None # Capture load errors so expected failures can be distinguished. try: - lattice = pals.load(str(path)) - if rel == "fodo.pals.yaml": - check_fodo(lattice) + loaded = pals.load(str(path)) + spot_check = SPOT_CHECKS.get(rel) + if spot_check is not None: + spot_check(loaded) except Exception as e: # noqa: BLE001 -- any reader failure counts error = e if rel in known: