Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 26 additions & 31 deletions pyaml/lattice/attribute_linker.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from dataclasses import dataclass

import at
from pydantic import ConfigDict

from pyaml.common.element import Element
from pyaml.lattice.lattice_elements_linker import (
Expand All @@ -8,37 +9,34 @@
LinkerIdentifier,
)

PYAMLCLASS = "PyAtAttributeElementsLinker"
from ..validation import DynamicValidation, register_schema

PYAMLCLASS = "PyAtAttributeElementsLinker"

class ConfigModel(LinkerConfigModel):
"""Base configuration model for linker definitions.

This class defines the configuration structure used to instantiate
a specific linking strategy. Each concrete implementation of a
`LatticeElementsLinker` may define its own subclass extending this model
to include additional configuration parameters.
@dataclass
class PyAtAttributeConfigModel(LinkerConfigModel):
"""Configuration model for ``PyAtAttributeElementsLinker``.

Attributes
Parameters
----------
model_config : ConfigDict
Pydantic configuration allowing arbitrary field types and forbidding
unexpected extra keys.
attribute_name : str
Name of the PyAT element attribute used to identify matching
lattice elements.
"""

model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
attribute_name: str


class PyAtAttributeIdentifier(LinkerIdentifier):
"""Abstract base class for identifiers used to match PyAML and PyAT elements.

The identifier acts as an intermediate representation between the PyAML
configuration and the PyAT lattice. Its exact structure depends on the
linking strategy (e.g., family name, element index, or user-defined tag).
"""Identifier based on a PyAT element attribute.

Subclasses should define the fields and logic necessary to represent
a unique reference to one or more PyAT elements.
Parameters
----------
attribute_name : str
Name of the PyAT attribute used for matching.
identifier
Expected value of the attribute.
"""

def __init__(self, attribute_name: str, identifier):
Expand All @@ -49,20 +47,17 @@ def __repr__(self):
return f"{self.attribute_name}={self.identifier}"


class PyAtAttributeElementsLinker(LatticeElementsLinker):
"""Abstract base class defining the interface for PyAT–PyAML element linking.

Implementations of this class define how PyAML elements are matched
to PyAT elements based on a given linking strategy (e.g., by family name,
by index, or by a custom attribute).
@register_schema
class PyAtAttributeElementsLinker(LatticeElementsLinker, DynamicValidation):
"""Link lattice elements using a specified PyAT element attribute.

Parameters
----------
config_model : ConfigModel
The configuration model for the linking strategy.
This linker associates PyAML elements with PyAT elements by comparing
the value of a configurable PyAT attribute against the identifier
extracted from the PyAML element.
"""

def __init__(self, config_model: ConfigModel):
def __init__(self, attribute_name: str):
config_model = PyAtAttributeConfigModel(attribute_name)
super().__init__(config_model)

def get_element_identifier(self, element: Element) -> LinkerIdentifier:
Expand Down
29 changes: 13 additions & 16 deletions pyaml/lattice/lattice_elements_linker.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,27 @@
from abc import ABCMeta, abstractmethod
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Iterable

import at
from at import Lattice
from pydantic import BaseModel, ConfigDict

from pyaml import PyAMLException
from pyaml.common.element import Element


class LinkerConfigModel(BaseModel):
class LinkerConfigModel(ABC):
"""Base configuration model for linker definitions.

This class defines the configuration structure used to instantiate
a specific linking strategy. Each concrete implementation of a
`LatticeElementsLinker` may define its own subclass extending this model
to include additional configuration parameters.

Attributes
----------
model_config : ConfigDict
Pydantic configuration allowing arbitrary field types and forbidding
unexpected extra keys.
"""

model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
pass


class LinkerIdentifier(metaclass=ABCMeta):
class LinkerIdentifier(ABC):
"""Abstract base class for identifiers used to match PyAML and PyAT elements.

The identifier acts as an intermediate representation between the PyAML
Expand All @@ -41,7 +35,7 @@ class LinkerIdentifier(metaclass=ABCMeta):
pass


class LatticeElementsLinker(metaclass=ABCMeta):
class LatticeElementsLinker(ABC):
"""Abstract base class defining the interface for PyAT–PyAML element linking.

Implementations of this class define how PyAML elements are matched
Expand All @@ -50,6 +44,8 @@ class LatticeElementsLinker(metaclass=ABCMeta):

Parameters
----------
attribute_name: str

linker_config_model : LinkerConfigModel
The configuration model for the linking strategy.

Expand All @@ -61,7 +57,7 @@ class LatticeElementsLinker(metaclass=ABCMeta):

def __init__(self, linker_config_model: LinkerConfigModel):
self.linker_config_model = linker_config_model
self.lattice: Lattice = None
self.lattice: Lattice | None = None

def set_lattice(self, lattice: Lattice):
"""
Expand Down Expand Up @@ -97,9 +93,10 @@ def get_element_identifier(self, element: Element) -> LinkerIdentifier:

def _iter_matches(self, identifier: LinkerIdentifier) -> Iterable[at.Element]:
"""Yield all elements in the lattice whose matches the identifier."""
for elem in self.lattice:
if self._test_at_element(identifier, elem):
yield elem
if self.lattice:
for elem in self.lattice:
if self._test_at_element(identifier, elem):
yield elem

def get_at_elements(self, element_id: LinkerIdentifier | list[LinkerIdentifier]) -> list[at.Element]:
"""Return a list of PyAT elements matching the given identifiers.
Expand Down
105 changes: 59 additions & 46 deletions pyaml/lattice/simulator.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
from pathlib import Path

import at
from pydantic import BaseModel, ConfigDict

from ..bpm.bpm import BPM
from ..common.abstract_aggregator import ScalarAggregator
from ..common.element import Element
from ..common.element import Element, __pyaml_repr__
from ..common.element_holder import ElementHolder
from ..common.exception import PyAMLException
from ..configuration import ROOT
Expand Down Expand Up @@ -38,61 +37,67 @@
from ..rf.rf_transmitter import RFTransmitter
from ..tuning_tools.measurement_tool import MeasurementTool
from ..tuning_tools.tuning_tool import TuningTool
from .attribute_linker import (
ConfigModel as PyAtAttrLinkerConfigModel,
)
from .attribute_linker import (
PyAtAttributeElementsLinker,
)
from ..validation import DynamicValidation, register_schema
from .lattice_elements_linker import LatticeElementsLinker

# Define the main class name for this module
PYAMLCLASS = "Simulator"


class ConfigModel(BaseModel):
"""
Configuration model for Simulator

Parameters
----------
name : str
Simulator name
lattice : str
AT lattice file
mat_key : str, optional
AT lattice ring name
linker : LatticeElementsLinker, optional
The linker configuration model
description : str , optional
Simulator description
"""

model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")

name: str
lattice: str
mat_key: str = None
linker: LatticeElementsLinker = None
description: str | None = None
@register_schema
class Simulator(ElementHolder, DynamicValidation):
"""Simulator interface backed by a PyAT lattice.

The simulator loads a PyAT lattice from disk and attaches PyAML
elements to their corresponding PyAT elements. Once attached, the
resulting device objects expose read/write interfaces operating
directly on the simulation model.

class Simulator(ElementHolder):
"""
Class that implements access to AT simulator
Elements may be matched either using the default name-based lookup
or a custom :class:`LatticeElementsLinker`.
"""

def __init__(self, cfg: ConfigModel):
def __init__(
self,
name: str,
lattice: str,
mat_key: str | None = None,
linker: LatticeElementsLinker | None = None,
description: str | None = None,
):
"""Create a simulator from a PyAT lattice.

Parameters
----------
name : str
Name of the simulator.
lattice : str
Path to the PyAT lattice file, relative to the configured
PyAML root directory.
mat_key : str, optional
Variable name of the lattice when loading a MATLAB ``.mat``
lattice file.
linker : LatticeElementsLinker, optional
Custom linker used to associate PyAML elements with PyAT
lattice elements. If omitted, elements are matched by name.
description : str, optional
Human-readable description of the simulator.
"""

super().__init__()
self._cfg = cfg
path: Path = ROOT.get() / cfg.lattice
self._name = name
self._lattice = lattice
self._mat_key = mat_key
self.description = description

if self._cfg.mat_key is None:
path: Path = ROOT.get() / self._lattice

if self._mat_key is None:
self.ring = at.load_lattice(path)
else:
self.ring = at.load_lattice(path, mat_key=f"{self._cfg.mat_key}")
self.ring = at.load_lattice(path, mat_key=f"{self._mat_key}")

self._linker = cfg.linker
self._linker = linker
if self._linker:
self._linker.set_lattice(self.ring)
else:
Expand All @@ -104,16 +109,24 @@ def __init__(self, cfg: ConfigModel):
self._elements_indexing[e.FamName] = [e]

def name(self) -> str:
return self._cfg.name
return self._name

@property
def lattice(self) -> str:
return self._lattice

def get_lattice(self) -> at.Lattice:
return self.ring

def get_description(self) -> str:
@property
def mat_key(self) -> str | None:
return self._mat_key

def get_description(self) -> str | None:
"""
Returns the description of the accelerator
"""
return self._cfg.description
return self.description

def create_magnet_strength_aggregator(self, magnets: list[Magnet]) -> ScalarAggregator:
# No magnet aggregator for simulator
Expand Down Expand Up @@ -345,4 +358,4 @@ def get_at_elems(self, element: Element) -> list[at.Element]:
return [elts[idx] for idx in indices]

def __repr__(self):
return repr(self._cfg).replace("ConfigModel", self.__class__.__name__)
return __pyaml_repr__(self)
3 changes: 2 additions & 1 deletion pyaml/validation/validation_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import inspect
import logging
from abc import ABCMeta
from typing import Any

from pydantic import BaseModel, ConfigDict, create_model
Expand All @@ -23,7 +24,7 @@ class ValidationModel(PyAMLBaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")


class ValidationMeta(type):
class ValidationMeta(ABCMeta):
"""
Metaclass that validates constructor arguments before object creation.

Expand Down
13 changes: 5 additions & 8 deletions tests/lattice/test_linkers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@

from pyaml import PyAMLException
from pyaml.accelerator import Accelerator
from pyaml.lattice.attribute_linker import (
ConfigModel as AttrConfigModel,
)
from pyaml.lattice.attribute_linker import (
PyAtAttributeElementsLinker,
PyAtAttributeIdentifier,
Expand Down Expand Up @@ -40,7 +37,7 @@ def test_conf_with_linker():
def test_attribute_identifier_from_pyaml_name(lattice_with_custom_attr):
"""We bind to AT element attribute 'Tag';
identifier value comes from PyAML element .name"""
linker = PyAtAttributeElementsLinker(AttrConfigModel(attribute_name="Tag"))
linker = PyAtAttributeElementsLinker(attribute_name="Tag")
linker.set_lattice(lattice_with_custom_attr)
pyaml_elem = DummyPyAMLElement(name="QF") # identifier="QF"
ident = linker.get_element_identifier(pyaml_elem)
Expand All @@ -50,7 +47,7 @@ def test_attribute_identifier_from_pyaml_name(lattice_with_custom_attr):


def test_attribute_get_at_elements_all_matches(lattice_with_custom_attr):
linker = PyAtAttributeElementsLinker(AttrConfigModel(attribute_name="Tag"))
linker = PyAtAttributeElementsLinker(attribute_name="Tag")
linker.set_lattice(lattice_with_custom_attr)
ident = PyAtAttributeIdentifier("Tag", "QF")
matches = linker.get_at_elements(ident)
Expand All @@ -60,7 +57,7 @@ def test_attribute_get_at_elements_all_matches(lattice_with_custom_attr):


def test_attribute_get_at_element_first_match(lattice_with_custom_attr):
linker = PyAtAttributeElementsLinker(AttrConfigModel(attribute_name="Tag"))
linker = PyAtAttributeElementsLinker(attribute_name="Tag")
linker.set_lattice(lattice_with_custom_attr)
ident = PyAtAttributeIdentifier("Tag", "QD")
first = linker.get_at_element(ident)
Expand All @@ -73,7 +70,7 @@ def test_attribute_get_at_element_first_match(lattice_with_custom_attr):


def test_attribute_no_match_raises(lattice_with_custom_attr):
linker = PyAtAttributeElementsLinker(AttrConfigModel(attribute_name="Tag"))
linker = PyAtAttributeElementsLinker(attribute_name="Tag")
linker.set_lattice(lattice_with_custom_attr)
ident = PyAtAttributeIdentifier("Tag", "ZZ")
with pytest.raises(PyAMLException):
Expand All @@ -83,7 +80,7 @@ def test_attribute_no_match_raises(lattice_with_custom_attr):


def test_attribute_multiple_identifiers_accumulate(lattice_with_custom_attr):
linker = PyAtAttributeElementsLinker(AttrConfigModel(attribute_name="Tag"))
linker = PyAtAttributeElementsLinker(attribute_name="Tag")
linker.set_lattice(lattice_with_custom_attr)
ids = [PyAtAttributeIdentifier("Tag", "QF"), PyAtAttributeIdentifier("Tag", "QD")]
res = linker.get_at_elements(ids)
Expand Down
Loading