Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a1fde86
Add catalog-based device resolution
gupichon-soleil Apr 20, 2026
9d1fe68
Merge remote-tracking branch 'origin/main' into 187-adding-catalogs-s…
gupichon-soleil Apr 20, 2026
43e88f9
Handle catalogs in configuration manager
gupichon-soleil Apr 20, 2026
75d3b80
Remove dummy attribute_catalog and fix wrongly committed files
gupichon-soleil Apr 20, 2026
4f45cee
Documentation
gupichon-soleil Apr 20, 2026
90fb538
Bind catalogs per control system during key resolution
gupichon-soleil Apr 20, 2026
baecc54
Refactor static catalogs into typed entries
gupichon-soleil Apr 20, 2026
91f74cd
Merge remote-tracking branch 'origin/main' into 187-adding-catalogs-s…
gupichon-soleil Apr 20, 2026
7f7bf3a
Limit catalog-backed device references to BPM models
gupichon-soleil Apr 20, 2026
2b48c68
Require catalog keys for BPM device references
gupichon-soleil Apr 20, 2026
64113ca
Merge remote-tracking branch 'origin/main' into 187-adding-catalogs-s…
gupichon-soleil Apr 20, 2026
e2fcf63
Rollback to origin/main for some files.
gupichon-soleil Apr 21, 2026
a231832
Merge remote-tracking branch 'origin/main' into 187-adding-catalogs-s…
gupichon-soleil Apr 21, 2026
15e60a2
Merge remote-tracking branch 'origin/main' into 187-adding-catalogs-s…
gupichon-soleil Apr 21, 2026
2571119
Merge remote-tracking branch 'origin/main' into 187-adding-catalogs-s…
gupichon-soleil Apr 22, 2026
b404312
Changing the keys for the test catalogs
gupichon-soleil Apr 22, 2026
b776a6d
Adapting examples
gupichon-soleil Apr 22, 2026
a89a1ab
Merge remote-tracking branch 'origin/main' into 187-adding-catalogs-s…
gupichon-soleil Apr 22, 2026
b68816b
Merge origin/main — resolve formatting conflicts from ruff 120→127 li…
gupichon-soleil Apr 23, 2026
8b88938
Leftover from a bug fix for serialized magnets that need to be moved …
gupichon-soleil Apr 24, 2026
9c2f71e
Move catalog indexing responsibility from pyaml BPM models to the CS …
gupichon-soleil Apr 24, 2026
b80107e
Merge origin/main
gupichon-soleil Apr 24, 2026
53aa843
Evolutions on tests to reflect changes in tango-pyaml
gupichon-soleil Apr 24, 2026
4365683
Evolutions on tests to reflect changes in tango-pyaml
gupichon-soleil Apr 24, 2026
e688aa9
Adapting the BESSY2Orbit.yaml to catalogs.
gupichon-soleil Apr 24, 2026
3d9d286
Rename ControlSystem.resolve_device and resolve_devices to get_device…
gupichon-soleil Apr 27, 2026
24040f9
Make ControlSystem.get_device(s) return attached devices
gupichon-soleil Apr 29, 2026
ac4d550
Merge remote-tracking branch 'origin/main' into 187-adding-catalogs-s…
gupichon-soleil Apr 29, 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
1,481 changes: 251 additions & 1,230 deletions examples/BESSY2_example/BESSY2Orbit.yaml
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand "ORBITCC:rdPos@0" It is already a dynamic catalog ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is. You can take a look at the dedicated branch and the PR on pyaml-cs-oa. There is also a PR for tango-pyaml.

I haven't done much testing yet as there are no unit tests in pyaml-cs-oa, but I've confirmed that the file loads successfully.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What prefix does in the catalog, it is very confusing.
Could it explains unwanted creation of unattached object ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I think I understand. The build call is wrong in the dynamic catalog. A catalog should only return a temporary config object.

Large diffs are not rendered by default.

2,163 changes: 2,163 additions & 0 deletions examples/SOLEIL_examples/catalogs.yaml

Large diffs are not rendered by default.

1,800 changes: 360 additions & 1,440 deletions examples/SOLEIL_examples/devices.yaml

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions examples/SOLEIL_examples/p.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ controls:
- type: tango.pyaml.controlsystem
name: live
tango_host: localhost:11000
catalog: bpm-catalog
catalogs:
- catalogs.yaml
arrays: arrays.yaml
devices:
- devices.yaml
Expand Down
30 changes: 30 additions & 0 deletions pyaml/accelerator.py
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catalog config is linked to control system only.
Could the catalogs field be moved in ControlSystem configuration ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to share the catalog between control systems (live and virtual), it would be better to define it at the accelerator level. Otherwise, we would need to update the 'simulators' section to support a more complex structure than just a simple list.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand your point, what simulators do here ?
Catalog are control system specific and it would be logical to have it in the Control section whatever the ControlSystem configuration structure.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .common.element_holder import ElementHolder
from .common.exception import PyAMLConfigException
from .configuration import ConfigurationManager, UnsupportedConfigurationRootError
from .configuration.catalog import Catalog
from .configuration.factory import Factory
from .control.controlsystem import ControlSystem
from .lattice.simulator import Simulator
Expand Down Expand Up @@ -58,6 +59,7 @@ class ConfigModel(BaseModel):
alphac: float | None = None
harmonic_number: int | None = None
controls: list[ControlSystem] = None
catalogs: list[Catalog] = None
simulators: list[Simulator] = None
data_folder: str
description: str | None = None
Expand All @@ -74,6 +76,14 @@ def __init__(self, cfg: ConfigModel):
__live = None
self._controls: dict[str, ElementHolder] = {}
self._simulators: dict[str, ElementHolder] = {}
self._catalogs: dict[str, Catalog] = {}

if cfg.catalogs is not None:
for catalog in cfg.catalogs:
name = catalog.get_name()
if name in self._catalogs:
raise PyAMLConfigException(f"catalog {name} already defined")
self._catalogs[name] = catalog

if cfg.controls is not None:
for c in cfg.controls:
Expand All @@ -82,6 +92,7 @@ def __init__(self, cfg: ConfigModel):
else:
# Add as dynamic attribute
setattr(self, c.name(), c)
c.set_catalog(self._resolve_control_system_catalog(c))
c.fill_device(cfg.devices)
c._peer = self
self._controls[c.name()] = c
Expand Down Expand Up @@ -249,6 +260,14 @@ def modes(self) -> dict[str, "ElementHolder"]:
modes.update(self._controls)
return modes

def get_catalog(self, name: str) -> Catalog:
if name not in self._catalogs:
raise PyAMLConfigException(f"catalog {name} not defined")
return self._catalogs[name]

def catalogs(self) -> dict[str, Catalog]:
return self._catalogs

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

Expand All @@ -270,6 +289,7 @@ def from_dict(config_dict: dict, ignore_external=False) -> "Accelerator":
if ignore_external:
# control systems are external, so remove controls field
config_dict.pop("controls", None)
config_dict.pop("catalogs", None)
# Ensure factory is clean before building a new accelerator
Factory.clear()
return Factory.depth_first_build(config_dict, ignore_external)
Expand Down Expand Up @@ -302,3 +322,13 @@ def load(filename: str, use_fast_loader: bool = False, ignore_external=False) ->
"Use the factory APIs to build sub-elements directly."
) from ex
return manager.build(ignore_external=ignore_external)

def _resolve_control_system_catalog(self, control_system: ControlSystem) -> Catalog | None:
catalog = control_system.get_catalog_config()
if catalog is None:
return None
if isinstance(catalog, str):
return self.get_catalog(catalog)
if isinstance(catalog, Catalog):
return catalog
raise PyAMLConfigException(f"Invalid catalog configuration for control system {control_system.name()}")
1 change: 1 addition & 0 deletions pyaml/apidoc/gen_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"pyaml.common.element",
"pyaml.common.element_holder",
"pyaml.common.exception",
"pyaml.configuration.catalog",
"pyaml.configuration.csvcurve",
"pyaml.configuration.csvmatrix",
"pyaml.configuration.curve",
Expand Down
109 changes: 3 additions & 106 deletions pyaml/bpm/bpm_model.py
Comment thread
gupichon marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
from abc import ABCMeta, abstractmethod

import numpy as np
from numpy.typing import NDArray

from ..control.deviceaccess import DeviceAccess


class BPMModel(metaclass=ABCMeta):
"""
Expand All @@ -13,7 +8,7 @@ class BPMModel(metaclass=ABCMeta):
"""

@abstractmethod
def get_pos_devices(self) -> list[DeviceAccess | None]:
def get_pos_devices(self) -> list[str]:
"""
Get device handles used for position reading

Expand All @@ -25,7 +20,7 @@ def get_pos_devices(self) -> list[DeviceAccess | None]:
pass

@abstractmethod
def get_tilt_device(self) -> DeviceAccess | None:
def get_tilt_device(self) -> str | None:
"""
Get device handle used for tilt access

Expand All @@ -37,7 +32,7 @@ def get_tilt_device(self) -> DeviceAccess | None:
pass

@abstractmethod
def get_offset_devices(self) -> list[DeviceAccess | None]:
def get_offset_devices(self) -> list[str | None]:
"""
Get device handles used for offset access

Expand All @@ -47,101 +42,3 @@ def get_offset_devices(self) -> list[DeviceAccess | None]:
h and v offset devices
"""
pass

def x_pos_index(self) -> int | None:
"""
Returns the index of the horizontal position in
an array, otherwise a scalar value is expected from the
corresponding DeviceAccess

Returns
-------
int
Index in the array, None for a scalar value
"""
return None

def y_pos_index(self) -> int | None:
"""
Returns the index of the veritcal position in
an array, otherwise a scalar value is expected from the
corresponding DeviceAccess

Returns
-------
int
Index in the array, None for a scalar value
"""
return None

def is_pos_indexed(self) -> bool:
"""
Check if position values are indexed (array-based).

Returns
-------
bool
True if both x and y positions are indexed, False otherwise
"""
return self.x_pos_index() is not None and self.y_pos_index() is not None

def tilt_index(self) -> int | None:
"""
Returns the index of the tilt angle in
an array, otherwise a scalar value is expected from the
corresponding DeviceAccess

Returns
-------
int
Index in the array, None for a scalar value
"""
return None

def is_tilt_indexed(self) -> bool:
"""
Check if tilt value is indexed (array-based).

Returns
-------
bool
True if tilt is indexed, False otherwise
"""
return self.tilt_index() is not None

def x_offset_index(self) -> int | None:
"""
Returns the index of the horizontal offset in
an array, otherwise a scalar value is expected from the
corresponding DeviceAccess

Returns
-------
int
Index in the array, None for a scalar value
"""
return None

def y_offset_index(self) -> int | None:
"""
Returns the index of the veritcal offset in
an array, otherwise a scalar value is expected from the
corresponding DeviceAccess

Returns
-------
int
Index in the array, None for a scalar value
"""
return None

def is_offset_indexed(self) -> bool:
"""
Check if offset values are indexed (array-based).

Returns
-------
bool
True if both x and y offsets are indexed, False otherwise
"""
return self.x_offset_index() is not None and self.y_offset_index() is not None
55 changes: 9 additions & 46 deletions pyaml/bpm/bpm_simple_model.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import numpy as np
from numpy.typing import NDArray
from pydantic import BaseModel, ConfigDict

from pyaml.bpm.bpm_model import BPMModel

from ..common.element import __pyaml_repr__
from ..control.deviceaccess import DeviceAccess

# Define the main class name for this module
PYAMLCLASS = "BPMSimpleModel"
Expand All @@ -17,24 +14,16 @@ class ConfigModel(BaseModel):

Parameters
----------
x_pos : DeviceAccess, optional
Horizontal position device
y_pos : DeviceAccess, optional
Vertical position device
x_pos_index : int, optional
Index in the array when specified, otherwise scalar
value is expected
y_pos_index : int, optional
Index in the array when specified, otherwise scalar
value is expected
x_pos : str
Horizontal position catalog key
y_pos : str
Vertical position catalog key
"""

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

x_pos: DeviceAccess | None
y_pos: DeviceAccess | None
x_pos_index: int | None = None
y_pos_index: int | None = None
x_pos: str
y_pos: str


class BPMSimpleModel(BPMModel):
Expand All @@ -48,7 +37,7 @@ def __init__(self, cfg: ConfigModel):
self.__x_pos = cfg.x_pos
self.__y_pos = cfg.y_pos

def get_pos_devices(self) -> list[DeviceAccess | None]:
def get_pos_devices(self) -> list[str]:
"""
Get device handles used for position reading

Expand All @@ -59,7 +48,7 @@ def get_pos_devices(self) -> list[DeviceAccess | None]:
"""
return [self.__x_pos, self.__y_pos]

def get_tilt_device(self) -> DeviceAccess | None:
def get_tilt_device(self) -> str | None:
"""
Get device handle used for tilt access

Expand All @@ -70,7 +59,7 @@ def get_tilt_device(self) -> DeviceAccess | None:
"""
return None

def get_offset_devices(self) -> list[DeviceAccess | None]:
def get_offset_devices(self) -> list[str | None]:
"""
Get device handles used for offset access

Expand All @@ -81,31 +70,5 @@ def get_offset_devices(self) -> list[DeviceAccess | None]:
"""
return [None, None]

def x_pos_index(self) -> int | None:
"""
Returns the index of the horizontal position in
an array, otherwise a scalar value is expected from the
corresponding DeviceAccess

Returns
-------
int
Index in the array, None for a scalar value
"""
return self._cfg.x_pos_index

def y_pos_index(self) -> int | None:
"""
Returns the index of the veritcal position in
an array, otherwise a scalar value is expected from the
corresponding DeviceAccess

Returns
-------
int
Index in the array, None for a scalar value
"""
return self._cfg.y_pos_index

def __repr__(self):
return __pyaml_repr__(self)
Loading
Loading