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
55 changes: 29 additions & 26 deletions pyaml/arrays/element_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ..bpm.bpm import BPM
from ..common.element import Element, __pyaml_repr__
from ..common.exception import PyAMLException
from ..common.name_matching import resolve_names
from ..magnet.cfm_magnet import CombinedFunctionMagnet
from ..magnet.magnet import Magnet
from ..magnet.serialized_magnet import SerializedMagnets
Expand Down Expand Up @@ -240,22 +241,6 @@ def mro_as_list(cls: type) -> list[type]:

return self.__create_array("", chosen, elements)

def _select_names(self, pattern: str) -> "ElementArray":
"""Select names without interpreting field selectors.

Parameters
----------
pattern : str
A fnmatch pattern applied to each element name.

Returns
-------
ElementArray
Typed selection in the original order, including an empty array
when no names match.
"""
return self._typed_array([element for element in self if fnmatch.fnmatch(element.get_name(), pattern)])

def __is_bool_mask(self, other: object) -> bool:
"""Return True if 'other' looks like a boolean mask (list or numpy array)."""
# --- numpy boolean array ---
Expand Down Expand Up @@ -576,42 +561,60 @@ def __getitem__(self, key):

Parameters
----------
key : int, slice or str
Element index, slice, name pattern, or existing field selector.
key : int, slice, str, list[str] or tuple[str, ...]
Element index, slice, name pattern (or ``field:pattern`` field
selector), or a list/tuple of name patterns. A name pattern is a
literal name (must match an element in this array), an fnmatch
wildcard (``*``, ``?`` or ``[``), or a ``re:``-prefixed regular
expression. A list or tuple resolves each entry independently
and unions the results.

Returns
-------
Element or ElementArray
Indexed element or an array inferred from all selected elements.
Different Magnet subclasses produce a MagnetArray; mixed element
families produce an ElementArray. Empty selections return an
empty ElementArray.
empty ElementArray, except a literal name pattern (or literal
entry within a list/tuple) matching nothing, which raises.

Raises
------
PyAMLException
If a literal name pattern, or a literal entry within a list or
tuple, matches no element in this array, or a ``re:`` pattern is
not a valid regular expression.

Examples
--------
>>> magnets = sr.design.magnets.get()
>>> subset = magnets[:] # MagnetArray, including mixed magnet classes
>>> correctors = magnets["SH*"] # MagnetArray
>>> correctors = magnets["re:^SH1A-C0[12]-H$"] # MagnetArray
>>> selection = magnets[["SH1A-C01-H", "SH*-V"]] # Union of patterns
"""
if isinstance(key, slice):
# Slicing
r = super().__getitem__(key)

elif isinstance(key, (list, tuple)):
# Selection by a list/tuple of name patterns
matched = set(resolve_names([e.get_name() for e in self], key))
r = [e for e in self if e.get_name() in matched]

elif isinstance(key, str):
fields = key.split(":")
fields = [] if key.startswith("re:") else key.split(":")

if len(fields) <= 1:
# Selection by name
r = []
for e in self:
if fnmatch.fnmatch(e.get_name(), key):
r.append(e)
# Selection by name pattern
matched = set(resolve_names([e.get_name() for e in self], key))
r = [e for e in self if e.get_name() in matched]
else:
# Selection by fields
r = []
for e in self:
txt = self.__eval_field(fields[0], e)
if fnmatch.fnmatch(txt, fields[1]):
if fnmatch.fnmatchcase(txt, fields[1]):
r.append(e)

else:
Expand Down
32 changes: 32 additions & 0 deletions pyaml/common/holders/diagnostic_holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from ...diagnostics.tune_monitor import BetatronTuneMonitor
from ..element import Element, __pyaml_repr__
from ..exception import PyAMLException
from ..name_matching import is_wildcard, resolve_names
from .sub_holders import BPMHolder, BPMsHolder

if TYPE_CHECKING:
Expand Down Expand Up @@ -95,6 +96,37 @@ def get(self, name: str = None) -> "Element | ElementArray":
return ElementArray("", list(self._peer._DIAG.values()))
return self._peer._get_diagnostic(name)

def __getitem__(self, key: str | list[str] | tuple[str, ...]) -> "Element | ElementArray":
"""
Return a diagnostic, or a selection typed as a generic ElementArray.

Parameters
----------
key : str, list[str] or tuple[str, ...]
An exact literal name returns the stored diagnostic. An fnmatch
wildcard, a ``re:``-prefixed regular expression, or a list/tuple
of such patterns returns an ElementArray of matches (possibly
empty).

Returns
-------
Element or ElementArray
The stored diagnostic for an exact literal name, otherwise an
ElementArray of matches.

Raises
------
PyAMLException
If an exact literal name, or a literal entry within a list or
tuple, does not match any diagnostic, or a ``re:`` pattern is
not a valid regular expression.
"""
store = self._peer._DIAG
if isinstance(key, str) and not key.startswith("re:") and not is_wildcard(key):
return self._peer._get_diagnostic(key)
names = resolve_names(store.keys(), key, what="Diagnostic")
return ElementArray("", [store[n] for n in names])

@property
def betatron_tune(self) -> BetatronTuneMonitor:
"""
Expand Down
84 changes: 52 additions & 32 deletions pyaml/common/holders/element_holder.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Store, resolve, and group elements shared by runtime backends."""

import fnmatch
import re
from abc import ABCMeta, abstractmethod
from typing import TYPE_CHECKING, overload

Expand All @@ -17,6 +15,7 @@
from ..abstract_aggregator import ScalarAggregator
from ..element import Element
from ..exception import PyAMLException
from ..name_matching import is_wildcard, resolve_names
from .diagnostic_holder import DiagnosticHolder
from .rf_holder import RFHolder
from .sub_holders import (
Expand Down Expand Up @@ -311,29 +310,31 @@ def create_bpm_aggregators(self, bpms: list[BPM]) -> list[ScalarAggregator | Non

# Elements

def find_elements(self, filter: str) -> list[str]:
def find_elements(self, filter: str | list[str] | tuple[str, ...]) -> list[str]:
"""
Find element names matching a literal, wildcard, or regular expression.
Find element names matching one or several literal, wildcard, or regex patterns.

Parameters
----------
filter : str
Pattern to match. Prefix with ``re:`` for a regular expression.
filter : str, list[str] or tuple[str, ...]
Pattern, or patterns, to match. A pattern is a literal name, an
fnmatch wildcard (``*``, ``?`` or ``[`` anywhere in the string),
or a regular expression prefixed with ``re:``. Several patterns
are resolved independently and unioned, de-duplicated, in
first-encounter order.

Returns
-------
list[str]
Matching element names.
"""
if filter.startswith("re:"):
pattern = re.compile(rf"{filter[3:]}")
elements = [k for k in self._ALL.keys() if pattern.fullmatch(k)]
elif "*" in filter or "?" in filter:
elements = [k for k in self._ALL.keys() if fnmatch.fnmatch(k, filter)]
else:
elements = [filter]

return elements
Raises
------
PyAMLException
If a literal pattern matches no element, or a ``re:`` pattern is
not a valid regular expression.
"""
return resolve_names(self._ALL.keys(), filter, what="Element")

def _fill_array(
self,
Expand Down Expand Up @@ -450,66 +451,85 @@ def __getitem__(self, key: int) -> Element: ...
def __getitem__(self, key: slice) -> ElementArray: ...

@overload
def __getitem__(self, key: str) -> Element | ElementArray | None: ...
def __getitem__(self, key: str) -> Element: ...

@overload
def __getitem__(self, key: list[str] | tuple[str, ...]) -> ElementArray: ...

def __getitem__(self, key: int | slice | str) -> Element | ElementArray | None:
def __getitem__(self, key: int | slice | str | list[str] | tuple[str, ...]) -> Element | ElementArray:
"""Retrieve an element or select a collection.

Parameters
----------
key : int, slice or str
Index in registration order, slice, exact name, or name pattern.
Strings containing ``*``, ``?`` or ``[`` use fnmatch matching.
Other strings are exact registry keys. Colons are literal.
key : int, slice, str, list[str] or tuple[str, ...]
Index in registration order, slice, exact name, name pattern, or
a list/tuple of patterns. Strings containing ``*``, ``?`` or
``[`` use fnmatch matching; a ``re:`` prefix uses a regular
expression instead. Any other string is an exact registry key.
Colons are literal. A list or tuple resolves each entry
independently and unions the results.

Returns
-------
Element or ElementArray or None
An index returns an element. An exact name returns its element
or None. Patterns and slices return the most specific compatible
array, or an empty ElementArray when nothing matches.
Element or ElementArray
An index or an exact literal name returns its element. Patterns,
lists/tuples of patterns, and slices return the most specific
compatible array, or an empty ElementArray when nothing matches.
The full slice ``[:]`` returns a generic ElementArray, like get().

Raises
------
PyAMLException
If an exact literal name, or a literal entry within a list or
tuple, does not match any registered element, or if a ``re:``
pattern is not a valid regular expression.
IndexError
If the index is out of bounds.
TypeError
If the key is neither an integer, a slice, nor a string.
If the key is neither an integer, a slice, a string, nor a
list/tuple of strings.
ValueError
If a slice has a zero step.

Notes
-----
Indices follow insertion order, not necessarily lattice order.
Collections share element references but do not modify the registry.
Field filters and regular expressions are not interpreted here.
Field filters are not interpreted here.

Examples
--------
>>> bpm = sr.live["BPM01"]
>>> missing = sr.live["UNKNOWN"] # None
>>> missing = sr.live["UNKNOWN"] # raises PyAMLException
>>> bpms = sr.live["BPM*"]
>>> bpms = sr.live["BPM0[123]"] # BPM01, BPM02 or BPM03
>>> bpms = sr.live["BPM0[1-3]"] # Same selection using a range
>>> quads = sr.live["Q[FD]*"] # Names starting with QF or QD
>>> bpms = sr.live["BPM0[!3]"] # One character after BPM0, except 3
>>> bpms = sr.live["re:^BPM0[12]$"] # Regular expression
>>> mixed = sr.live[["BPM01", "QF1*"]] # Union of several patterns
>>> first = sr.live[0]
>>> subset = sr.live[1:10]
>>> all_elements = sr.live[:]
"""
if isinstance(key, str):
if any(marker in key for marker in "*?["):
return self.get()._select_names(key)
return self._ALL.get(key)
if key.startswith("re:") or is_wildcard(key):
names = resolve_names(self._ALL.keys(), key)
return self.get()._typed_array([self._ALL[n] for n in names])
if key not in self._ALL:
raise PyAMLException(f"Element {key} not defined")
return self._ALL[key]
if isinstance(key, (list, tuple)):
names = resolve_names(self._ALL.keys(), key)
return self.get()._typed_array([self._ALL[n] for n in names])
if isinstance(key, int):
return list(self._ALL.values())[key]
if isinstance(key, slice):
elements = self.get()
if key == slice(None):
return elements
return elements._typed_array(list(elements)[key])
raise TypeError("ElementHolder keys must be integers, slices or strings")
raise TypeError("ElementHolder keys must be integers, slices, strings, or lists/tuples of strings")

def fill_element_array(self, arrayName: str, elementNames: list[str]):
"""
Expand Down
30 changes: 27 additions & 3 deletions pyaml/common/holders/generic_array_holder.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,17 +110,41 @@ def add(self, arrayName: str, elementNames: list[str]):

def __getitem__(self, key):
"""
Return an element from the aggregate array by index.
Select from the aggregate array of every individual element.

Delegates to :meth:`ElementArray.__getitem__
<pyaml.arrays.element_array.ElementArray.__getitem__>` on ``self.get()``
(the array of every individual element of this type), so ``key``
matches against **individual element names**, not against the
registered array/family names that :meth:`get` searches. These are
deliberately two different, non-overlapping namespaces: ``get(name)``
looks up a configured family (e.g. ``"QForTune"``), while ``[key]``
looks up the elements themselves (e.g. ``"QF1A-C01"`` or ``"QF1*"``).

Parameters
----------
key : int or slice
Index or slice passed to the aggregate array.
key : int, slice, str, list[str] or tuple[str, ...]
Index or slice into the aggregate array, an individual element's
exact name, an fnmatch wildcard or ``re:`` regular expression
over element names, or a list/tuple of such patterns.

Returns
-------
object
Element or sub-array selected by ``key``.

Raises
------
PyAMLException
If ``key`` is an exact literal element name (or a literal entry
within a list or tuple) that matches no individual element, or a
``re:`` pattern is not a valid regular expression.

Examples
--------
>>> family = sr.live.magnets.get("QForTune") # array-name namespace
>>> one_magnet = sr.live.magnets["QF1A-C01"] # element-name namespace
>>> some_magnets = sr.live.magnets["QF1*"] # element-name namespace
"""
return self.get().__getitem__(key)

Expand Down
Loading
Loading