Skip to content
2 changes: 1 addition & 1 deletion examples/other_examples/ControlSystem/tango/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
sr = Accelerator.load("config.yaml")

# print the BPM position
bpm = sr.live.bpm.get("BPM_C01-01") # bpm is a BPM
bpm = sr.live.diagnostic.bpm.get("BPM_C01-01") # bpm is a BPM
print(bpm.positions.get())

# Direct access to control system
Expand Down
19 changes: 3 additions & 16 deletions examples/use_cases/03-orbit_correction.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -185,24 +185,11 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": null,
"id": "6adb7436",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"BPMs: 320, H correctors: 288, V correctors: 288\n"
]
}
],
"source": [
"bpms = SR.bpms.get(\"BPM\")\n",
"hcorr = SR.magnets.get(\"HCorr\")\n",
"vcorr = SR.magnets.get(\"VCorr\")\n",
"print(f\"BPMs: {len(bpms)}, H correctors: {len(hcorr)}, V correctors: {len(vcorr)}\")"
]
"outputs": [],
"source": "bpms = SR.diagnostic.bpms.get(\"BPM\")\nhcorr = SR.magnets.get(\"HCorr\")\nvcorr = SR.magnets.get(\"VCorr\")\nprint(f\"BPMs: {len(bpms)}, H correctors: {len(hcorr)}, V correctors: {len(vcorr)}\")"
},
{
"cell_type": "markdown",
Expand Down
2 changes: 1 addition & 1 deletion examples/use_cases/03-orbit_correction.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
# In[4]:


bpms = SR.bpms.get("BPM")
bpms = SR.diagnostic.bpms.get("BPM")
hcorr = SR.magnets.get("HCorr")
vcorr = SR.magnets.get("VCorr")
print(f"BPMs: {len(bpms)}, H correctors: {len(hcorr)}, V correctors: {len(vcorr)}")
Expand Down
2 changes: 1 addition & 1 deletion pyaml/arrays/bpm.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,4 @@ def fill_array(self, holder: ElementHolder):
holder : ElementHolder
The element holder to populate the :py:class:`.BPMArray` with.
"""
holder.bpms.add(self._name, self._elements)
holder.diagnostic.bpms.add(self._name, self._elements)
121 changes: 121 additions & 0 deletions pyaml/common/holders/diagnostic_holder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""Holder interface for diagnostics."""

from typing import TYPE_CHECKING

from ...arrays.element_array import ElementArray
from ...diagnostics.tune_monitor import BetatronTuneMonitor
from ..element import Element, __pyaml_repr__
from ..exception import PyAMLException
from .sub_holders import BPMHolder, BPMsHolder

if TYPE_CHECKING:
from .element_holder import ElementHolder


class DiagnosticHolder:
"""
Provide access to diagnostics.

Parameters
----------
peer : 'ElementHolder'
Parent holder containing the diagnostic store.

Attributes
----------
bpm, bpms
Single BPM by name, or a named BPM array.
betatron_tune
Betatron tune monitor configured as ``BETATRON_TUNE``, the default, validated
against its expected class.

Methods
-------
get(name=None)
Return a named diagnostic, or all configured diagnostics when no name is given.

Notes
-----
:meth:`ElementHolder.get_betatron_tune_monitor
<pyaml.common.holders.element_holder.ElementHolder.get_betatron_tune_monitor>` stays
available as the named, untyped lookup. This holder adds the default-name,
type-validated convenience property.

Examples
--------
>>> default_tune_monitor = sr.live.diagnostic.betatron_tune
>>> measured_tune = default_tune_monitor.tune.get()
>>> spare_tune_monitor = sr.live.diagnostic.get("SPARE_BETATRON_TUNE_MONITOR")
>>> all_diagnostics = sr.live.diagnostic.get()
>>> bpm = sr.live.diagnostic.bpm.get("BPM01")
>>> bpms = sr.live.diagnostic.bpms.get("BPMS")
"""

def __init__(self, peer: "ElementHolder"):
"""
Initialize a diagnostic holder for an element holder.
"""
self._peer = peer
self._bpm_holder = BPMHolder(peer)
self._bpms_holder = BPMsHolder(peer)

@property
def bpm(self) -> BPMHolder:
"""Return the bpm."""
return self._bpm_holder

@property
def bpms(self) -> BPMsHolder:
"""Return the bpms."""
return self._bpms_holder

def get(self, name: str = None) -> "Element | ElementArray":
"""
Return a named diagnostic, or all configured diagnostics when no name is given.

Parameters
----------
name : str, optional
Name of the diagnostic to look up, as declared in the configuration. When
omitted, every configured diagnostic is returned instead.

Returns
-------
Element or ElementArray
The diagnostic registered under ``name``, or an
:class:`~pyaml.arrays.element_array.ElementArray` holding every configured
diagnostic, in insertion order, when ``name`` is omitted.

Raises
------
PyAMLException
If ``name`` is given and no diagnostic is registered under it.
"""
if name is None:
return ElementArray("", list(self._peer._DIAG.values()))
return self._peer._get_diagnostic(name)

@property
def betatron_tune(self) -> BetatronTuneMonitor:
"""
Return the betatron tune monitor configured as ``BETATRON_TUNE``.

Returns
-------
BetatronTuneMonitor
Betatron tune monitor registered under ``BETATRON_TUNE``.

Raises
------
PyAMLException
If no diagnostic is registered under ``BETATRON_TUNE``, or if it is not a
:class:`~pyaml.diagnostics.tune_monitor.BetatronTuneMonitor`.
"""
name = "BETATRON_TUNE"
obj = self._peer.get_betatron_tune_monitor(name)
if not isinstance(obj, BetatronTuneMonitor):
raise PyAMLException(f"{name}: BetatronTuneMonitor expected but got {type(obj).__name__}")
return obj

def __repr__(self):
return __pyaml_repr__(self)
37 changes: 20 additions & 17 deletions pyaml/common/holders/element_holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@
from ..abstract_aggregator import ScalarAggregator
from ..element import Element
from ..exception import PyAMLException
from .diagnostic_holder import DiagnosticHolder
from .rf_holder import RFHolder
from .sub_holders import (
BPMHolder,
BPMsHolder,
CombinedFunctionMagnetHolder,
CombinedFunctionMagnetsHolder,
MagnetHolder,
MagnetsHolder,
SerializedMagnetHolder,
SerializedMagnetsHolder,
)
from .tool_holder import ToolHolder

if TYPE_CHECKING:
from ...accelerator import Accelerator
Expand Down Expand Up @@ -57,18 +57,21 @@ class ElementHolder(metaclass=ABCMeta):
----------
magnet, magnets
Single magnet by name, or a named magnet array.
bpm, bpms
Single BPM by name, or a named BPM array.
combined_function_magnet, combined_function_magnets
Single combined-function magnet by name, or a named array.
serialized_magnet, serialized_magnets
Single serialized magnet group by name, or a named array.
rf
RF plant and transmitters of this mode.
diagnostic
Diagnostics of this mode, with typed default-name access.
tool
Tuning and measurement tools of this mode, with typed default-name access.
tune, chromaticity, orbit, dispersion
Tuning tools attached to this mode, looked up by name.
Backward-compatible aliases for ``tool.tune``, ``tool.chromaticity``, ``tool.orbit``
and ``tool.dispersion``.
trm, crm, orm
Response-matrix measurement tools, looked up by name.
Backward-compatible aliases for ``tool.trm``, ``tool.crm`` and ``tool.orm``.

Methods
-------
Expand Down Expand Up @@ -156,9 +159,9 @@ def __init__(self):
self._serialized_magnets_holder = SerializedMagnetsHolder(self)
self._combined_function_magnet_holder = CombinedFunctionMagnetHolder(self)
self._combined_function_magnets_holder = CombinedFunctionMagnetsHolder(self)
self._bpm_holder = BPMHolder(self)
self._bpms_holder = BPMsHolder(self)
self._rf_holder = RFHolder(self)
self._diagnostic_holder = DiagnosticHolder(self)
self._tool_holder = ToolHolder(self)

@property
def peer(self) -> "Accelerator":
Expand Down Expand Up @@ -198,19 +201,19 @@ def combined_function_magnets(self) -> CombinedFunctionMagnetsHolder:
return self._combined_function_magnets_holder

@property
def bpm(self) -> BPMHolder:
"""Return the bpm."""
return self._bpm_holder
def rf(self) -> RFHolder:
"""Return the rf."""
return self._rf_holder

@property
def bpms(self) -> BPMsHolder:
"""Return the bpms."""
return self._bpms_holder
def diagnostic(self) -> DiagnosticHolder:
"""Return the diagnostic."""
return self._diagnostic_holder

@property
def rf(self) -> RFHolder:
"""Return the rf."""
return self._rf_holder
def tool(self) -> ToolHolder:
"""Return the tool."""
return self._tool_holder

def post_init(self):
"""Run post-initialization hooks for every stored element."""
Expand Down
51 changes: 51 additions & 0 deletions pyaml/common/holders/generic_array_holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ class GenericArrayHolder(Generic[T, A]):
Return a named array or a transient array of all elements.
add(arrayName, elementNames)
Create and register a named array from element selectors.

Notes
-----
A configured array is also reachable as an attribute when its name is a
valid Python identifier, e.g. ``holder.QuadForTune`` is equivalent to
``holder.get("QuadForTune")``. Array names appear in ``dir(holder)`` so
interactive completion (IPython, Jupyter) discovers them.
"""

def __init__(
Expand Down Expand Up @@ -117,5 +124,49 @@ def __getitem__(self, key):
"""
return self.get().__getitem__(key)

def __getattr__(self, name: str) -> A:
"""
Return a configured array through attribute access.

Only called when normal attribute lookup fails, so it never shadows
:meth:`get`, :meth:`add`, or any other existing attribute.

Parameters
----------
name : str
Configured array name. Must be a valid Python identifier.

Returns
-------
A
The array registered under ``name``.

Raises
------
AttributeError
If ``name`` starts with an underscore, is not a valid Python
identifier, or does not match a configured array.

Examples
--------
>>> quad_family = sr.live.magnets.get("QuadForTune")
>>> same_quad_family = sr.live.magnets.QuadForTune
"""
if name.startswith("_") or not name.isidentifier() or name not in self._array_store:
raise AttributeError(f"'{type(self).__name__}' object has no array named '{name}'")
return self._array_store[name]

def __dir__(self) -> list[str]:
"""
List attributes, including configured array names.

Returns
-------
list of str
Default attributes plus configured array names that are valid
Python identifiers, for interactive completion (IPython, Jupyter).
"""
return sorted(set(super().__dir__()) | {name for name in self._array_store if name.isidentifier()})

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