Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
63cfd09
First implementation of yellow pages. The type of elements should be …
gupichon-soleil Mar 4, 2026
fc3cfd4
Merge remote-tracking branch 'origin/main' into 200-feature-yellowpag…
gupichon-soleil Mar 5, 2026
28e57e8
Documentation enhancement
gupichon-soleil Mar 5, 2026
8536d7c
Merge remote-tracking branch 'origin/main' into 200-feature-yellowpag…
gupichon-soleil Mar 5, 2026
a354afa
the get method is now internal.
gupichon-soleil Mar 5, 2026
39468ec
Merge remote-tracking branch 'origin/main' into 200-feature-yellowpag…
gupichon-soleil Mar 9, 2026
19b3bd7
Merge remote-tracking branch 'origin/main' into 200-feature-yellowpag…
gupichon-soleil Mar 12, 2026
496ae61
Merge remote-tracking branch 'origin/main' into 200-feature-yellowpag…
gupichon-soleil Mar 12, 2026
4e8cc15
Merge remote-tracking branch 'origin/main' into 200-feature-yellowpag…
gupichon-soleil Mar 13, 2026
05d4f37
YellowPages corrections: the element order is preserved as much as po…
gupichon-soleil Mar 13, 2026
b74e986
Test correction
gupichon-soleil Mar 13, 2026
88476f0
Documentation generation for yellow_pages.py and element_array.py
gupichon-soleil Mar 13, 2026
d06bb52
Typing: _acc is now an "Accelerator"
gupichon-soleil Mar 16, 2026
805fa0a
Doc: include special members for exploratory overloading
gupichon-soleil Mar 16, 2026
69c1dff
Setting the `ElementHolder` discovery methods to protected.
gupichon-soleil Mar 16, 2026
10a5984
Doc enhancement
gupichon-soleil Mar 17, 2026
cbe344a
Doc enhancement, correction of a wrong copy/paste
gupichon-soleil Mar 17, 2026
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
35 changes: 30 additions & 5 deletions pyaml/accelerator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@

from .arrays.array import ArrayConfig
from .common.element import Element
from .common.element_holder import ElementHolder
from .common.exception import PyAMLConfigException
from .configuration.factory import Factory
from .configuration.fileloader import load, set_root_folder
from .control.controlsystem import ControlSystem
from .lattice.simulator import Simulator
from .yellow_pages import YellowPages

# Define the main class name for this module
PYAMLCLASS = "Accelerator"
Expand Down Expand Up @@ -66,24 +68,28 @@ def __init__(self, cfg: ConfigModel):
self._cfg = cfg
__design = None
__live = None
self._controls: dict[str, ElementHolder] = {}
self._simulators: dict[str, ElementHolder] = {}

if cfg.controls is not None:
for c in cfg.controls:
if c.name() == "live":
self.__live = c
else:
# Add as dynacmic attribute
# Add as dynamic attribute
setattr(self, c.name(), c)
c.fill_device(cfg.devices)
self._controls[c.name()] = c

if cfg.simulators is not None:
for s in cfg.simulators:
if s.name() == "design":
self.__design = s
else:
# Add as dynacmic attribute
# Add as dynamic attribute
setattr(self, s.name(), s)
s.fill_device(cfg.devices)
self._simulators[s.name()] = s

if cfg.arrays is not None:
for a in cfg.arrays:
Expand All @@ -97,6 +103,8 @@ def __init__(self, cfg: ConfigModel):
if cfg.energy is not None:
self.set_energy(cfg.energy)

self._yellow_pages = YellowPages(self)

self.post_init()

def set_energy(self, E: float):
Expand Down Expand Up @@ -156,6 +164,25 @@ def design(self) -> Simulator:
"""
return self.__design

@property
def yellow_pages(self) -> YellowPages:
return self._yellow_pages

def simulators(self) -> dict[str, "ElementHolder"]:
"""Return all registered simulator modes."""
return self._simulators

def controls(self) -> dict[str, "ElementHolder"]:
"""Return all registered control modes."""
return self._controls

def modes(self) -> dict[str, "ElementHolder"]:
"""Return all registered control and simulator modes."""
modes: dict[str, "ElementHolder"] = {}
modes.update(self._simulators)
modes.update(self._controls)
return modes

def __repr__(self):
return repr(self._cfg).replace("ConfigModel", self.__class__.__name__)

Expand All @@ -182,9 +209,7 @@ def from_dict(config_dict: dict, ignore_external=False) -> "Accelerator":
return Factory.depth_first_build(config_dict, ignore_external)

@staticmethod
def load(
filename: str, use_fast_loader: bool = False, ignore_external=False
) -> "Accelerator":
def load(filename: str, use_fast_loader: bool = False, ignore_external=False) -> "Accelerator":
"""
Load an accelerator from a config file.

Expand Down
20 changes: 8 additions & 12 deletions pyaml/apidoc/gen_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"pyaml.tuning_tools.orbit_response_matrix",
"pyaml.tuning_tools.response_matrix",
"pyaml.tuning_tools.tune",
"pyaml.yellow_pages",
]


Expand Down Expand Up @@ -111,27 +112,24 @@ def generate_selective_module(m):
file.write(f".. automodule:: {p.__name__}\n")
if len(all_cls) > 0:
# Exclude classes that will be treated by autoclass
file.write(
f" :exclude-members: {','.join([c.__name__ for c in all_cls])}\n\n"
)
file.write(f" :exclude-members: {','.join([c.__name__ for c in all_cls])}\n\n")
for c in all_cls:
file.write(f" .. autoclass:: {c.__name__}\n")
file.write(" :members:\n")
if m in ["pyaml.arrays.element_array"]:
# Include special members for operator overloading
file.write(
" :special-members: __add__, __and__, __or__, __sub__ \n"
)
file.write(" :special-members: __add__, __and__, __or__, __sub__ \n")
if m in ["pyaml.yellow_pages"]:
# Include special members for exploratory overloading
file.write(" :special-members: __dir__, __getattr__, __getitem__, __repr__, __str__ \n")
file.write(" :exclude-members: model_config\n")
file.write(" :undoc-members:\n")
file.write(" :show-inheritance:\n\n")


def generate_toctree(filename: str, title: str, level: int, module: str):
sub_modules = [m for m in modules if m.startswith(module)]
level_path = sorted(
set([m.split(".")[level + 1 : level + 2][0] for m in sub_modules])
)
level_path = sorted(set([m.split(".")[level + 1 : level + 2][0] for m in sub_modules]))

paths = []

Expand Down Expand Up @@ -166,9 +164,7 @@ def gen_doc():
while len(paths) > 0:
npaths = []
for p in paths:
npaths.extend(
generate_toctree(f"api/{'pyaml.' + p}.rst", f"{p}", level, "pyaml." + p)
)
npaths.extend(generate_toctree(f"api/{'pyaml.' + p}.rst", f"{p}", level, "pyaml." + p))
paths = npaths
level += 1
print("done")
33 changes: 11 additions & 22 deletions pyaml/arrays/element_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,7 @@ def __create_array(self, array_name: str, element_type: type, elements: list):
elif issubclass(element_type, Element):
return ElementArray(array_name, elements, self.__use_aggregator)
else:
raise PyAMLException(
f"Unsupported sliced array for type {str(element_type)}"
)
raise PyAMLException(f"Unsupported sliced array for type {str(element_type)}")

def __eval_field(self, attribute_name: str, element: Element) -> str:
function_name = "get_" + attribute_name
Expand All @@ -109,16 +107,14 @@ def __ensure_compatible_operand(self, other: object) -> "ElementArray":
"""Validate the operand used for set-like operations between arrays."""
if not isinstance(other, ElementArray):
raise TypeError(
f"Unsupported operand type(s) for set operation: "
f"'{type(self).__name__}' and '{type(other).__name__}'"
f"Unsupported operand type(s) for set operation: '{type(self).__name__}' and '{type(other).__name__}'"
)

if len(self) > 0 and len(other) > 0:
if self.get_peer() is not None and other.get_peer() is not None:
if self.get_peer() != other.get_peer():
raise PyAMLException(
f"{self.__class__.__name__}: cannot operate on arrays "
"attached to different peers"
f"{self.__class__.__name__}: cannot operate on arrays attached to different peers"
)
return other

Expand Down Expand Up @@ -170,9 +166,7 @@ def __is_bool_mask(self, other: object) -> bool:
pass

# --- python sequence of bools (but not a string/bytes) ---
if isinstance(other, Sequence) and not isinstance(
other, (str, bytes, bytearray)
):
if isinstance(other, Sequence) and not isinstance(other, (str, bytes, bytearray)):
# Avoid treating ElementArray as a mask
if isinstance(other, ElementArray):
return False
Expand All @@ -195,8 +189,7 @@ def __and__(self, other: object):
If ``other`` is an ElementArray, the result contains elements
whose names are present in both arrays.

Example
-------
**Example**

.. code-block:: python

Expand All @@ -208,8 +201,8 @@ def __and__(self, other: object):
If ``other`` is a boolean mask (list[bool] or numpy.ndarray of bool),
elements are kept where the mask is True.

Example
-------
**Example**

.. code-block:: python

>>> mask = cell1.mask_by_type(Magnet)
Expand All @@ -232,8 +225,7 @@ def __and__(self, other: object):
mask = list(other) # works for list/tuple and numpy arrays
if len(mask) != len(self):
raise ValueError(
f"{self.__class__.__name__}: mask length ({len(mask)}) "
f"does not match array length ({len(self)})"
f"{self.__class__.__name__}: mask length ({len(mask)}) does not match array length ({len(self)})"
)
res = [e for e, keep in zip(self, mask, strict=True) if bool(keep)]
return self.__auto_array(res)
Expand Down Expand Up @@ -261,8 +253,7 @@ def __sub__(self, other: object):
If ``other`` is an ElementArray, the result contains elements
whose names are present in ``self`` but not in ``other``.

Example
-------
**Example**

.. code-block:: python

Expand All @@ -275,8 +266,7 @@ def __sub__(self, other: object):
elements are removed where the mask is True.
This is the inverse of ``& mask``.

Example
-------
**Example**

.. code-block:: python

Expand All @@ -300,8 +290,7 @@ def __sub__(self, other: object):
mask = list(other)
if len(mask) != len(self):
raise ValueError(
f"{self.__class__.__name__}: mask length ({len(mask)}) "
f"does not match array length ({len(self)})"
f"{self.__class__.__name__}: mask length ({len(mask)}) does not match array length ({len(self)})"
)
res = [e for e, remove in zip(self, mask, strict=True) if not bool(remove)]
return self.__auto_array(res)
Expand Down
82 changes: 66 additions & 16 deletions pyaml/common/element_holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,22 +110,18 @@ def fill_array(
try:
m = get_func(n)
except Exception as err:
raise PyAMLException(
f"{constructor.__name__} {array_name} : {err} @index {len(a)}"
) from None
raise PyAMLException(f"{constructor.__name__} {array_name} : {err} @index {len(a)}") from None
if m in a:
raise PyAMLException(
f"{constructor.__name__} {array_name} : "
f"duplicate name {name} @index {len(a)}"
f"{constructor.__name__} {array_name} : duplicate name {name} @index {len(a)}"
) from None
a.append(m)
ARR[array_name] = constructor(array_name, a)

def __add(self, array, element: Element):
if element.get_name() in self.__ALL: # Ensure name unicity
raise PyAMLException(
f"Duplicate element {element.__class__.__name__} "
"name {element.get_name()}"
f"Duplicate element {element.__class__.__name__} name {{element.get_name()}}"
) from None
array[element.get_name()] = element
self.__ALL[element.get_name()] = element
Expand Down Expand Up @@ -157,9 +153,7 @@ def get_all_elements(self) -> list[Element]:
# Magnets

def fill_magnet_array(self, arrayName: str, elementNames: list[str]):
self.fill_array(
arrayName, elementNames, self.get_magnet, MagnetArray, self.__MAGNET_ARRAYS
)
self.fill_array(arrayName, elementNames, self.get_magnet, MagnetArray, self.__MAGNET_ARRAYS)

def get_magnet(self, name: str) -> Magnet:
return self.__get("Magnet", name, self.__MAGNETS)
Expand Down Expand Up @@ -191,9 +185,7 @@ def add_cfm_magnet(self, m: Magnet):
self.__add(self.__CFM_MAGNETS, m)

def get_cfm_magnets(self, name: str) -> CombinedFunctionMagnetArray:
return self.__get(
"CombinedFunctionMagnet array", name, self.__CFM_MAGNET_ARRAYS
)
return self.__get("CombinedFunctionMagnet array", name, self.__CFM_MAGNET_ARRAYS)

def get_all_cfm_magnets(self) -> list[CombinedFunctionMagnet]:
return [value for key, value in self.__CFM_MAGNETS.items()]
Expand All @@ -216,9 +208,7 @@ def add_serialized_magnet(self, m: Magnet):
self.__add(self.__SERIALIZED_MAGNETS, m)

def get_serialized_magnets(self, name: str) -> SerializedMagnetsArray:
return self.__get(
"SerializedMagnets array", name, self.__SERIALIZED_MAGNETS_ARRAYS
)
return self.__get("SerializedMagnets array", name, self.__SERIALIZED_MAGNETS_ARRAYS)

def get_all_serialized_magnets(self) -> list[SerializedMagnets]:
return [value for key, value in self.__SERIALIZED_MAGNETS.items()]
Expand Down Expand Up @@ -318,3 +308,63 @@ def add_dispersion_tuning(self, dispersion: Element):
@property
def dispersion(self) -> "Dispersion":
return self.get_dispersion_tuning("DEFAULT_DISPERSION")

def _get_array(self, name: str):
"""
Generic array resolver used by YellowPages.

The method returns the array object referenced by 'name', regardless of its
concrete type.
"""
if name in self.__BPM_ARRAYS:
return self.__BPM_ARRAYS[name]
if name in self.__MAGNET_ARRAYS:
return self.__MAGNET_ARRAYS[name]
if name in self.__CFM_MAGNET_ARRAYS:
return self.__CFM_MAGNET_ARRAYS[name]
if name in self.__SERIALIZED_MAGNETS_ARRAYS:
return self.__SERIALIZED_MAGNETS_ARRAYS[name]
if name in self.__ELEMENT_ARRAYS:
return self.__ELEMENT_ARRAYS[name]

raise PyAMLException(f"Array {name} not defined")

def _get_tool(self, name: str):
"""
Generic tuning tool resolver used by YellowPages.
"""
if name not in self.__TUNING_TOOLS:
raise PyAMLException(f"Tool {name} not defined")
return self.__TUNING_TOOLS[name]

def _get_diagnostic(self, name: str):
"""
Generic diagnostic resolver used by YellowPages.
"""
if name not in self.__DIAG:
raise PyAMLException(f"Diagnostic {name} not defined")
return self.__DIAG[name]

def _list_arrays(self) -> list[str]:
"""
Return all array identifiers available in this holder.
"""
arrays: list[str] = []
arrays.extend(self.__BPM_ARRAYS.keys())
arrays.extend(self.__MAGNET_ARRAYS.keys())
arrays.extend(self.__CFM_MAGNET_ARRAYS.keys())
arrays.extend(self.__SERIALIZED_MAGNETS_ARRAYS.keys())
arrays.extend(self.__ELEMENT_ARRAYS.keys())
return arrays

def _list_tools(self) -> list[str]:
"""
Return all tuning tool identifiers available in this holder.
"""
return list(self.__TUNING_TOOLS.keys())

def _list_diagnostics(self) -> list[str]:
"""
Return all diagnostic identifiers available in this holder.
"""
return list(self.__DIAG.keys())
Loading
Loading