r"""Definition of :py:class:`.Observable` objects used in matching and
response matrices.
Observables are ways to monitor various lattice parameters and use them in
matching and correction procedures.
Class hierarchy
---------------
:py:class:`Observable`\ (evalfun, name, target, weight, bounds, ...)
:py:class:`RingObservable`\ (fun, ...)
:py:func:`GlobalOpticsObservable`\ (attrname, plane, name, ...)
:py:class:`EmittanceObservable`\ (attrname, plane, name, ...)
:py:class:`ElementObservable`\ (fun, ...)
:py:class:`OrbitObservable`\ (refpts, axis, name, ...)
:py:class:`TrajectoryObservable`\ (refpts, axis, name, ...)
:py:class:`MatrixObservable`\ (refpts, axis, name, ...)
:py:class:`LocalOpticsObservable`\ (refpts, attrname, axis, name, ...)
:py:class:`LatticeObservable`\ (refpts, attrname, index, name, ...)
:py:class:`GeometryObservable`\ (refpts, attrname, name, ...)
:py:class:`.RDTObservable`\ (refpts, RDTname, name, ...)
:py:class:`.Observable`\ s are usually not evaluated directly, but through a
container which performs the required optics computation and feeds each
:py:class:`.Observable` with its specific data. After evaluation, each
:py:class:`.Observable` provides the following properties:
- :py:attr:`~Observable.value`
- :py:attr:`~Observable.weight`
- :py:attr:`~Observable.weighted_value`: :pycode:`value / weight`
- :py:attr:`~Observable.deviation`: :pycode:`value - target`
- :py:attr:`~Observable.residual`: :pycode:`((value - target)/weight)**2`
"""
from __future__ import annotations
__all__ = [
"Observable",
"RingObservable",
"EmittanceObservable",
"GlobalOpticsObservable",
"ElementObservable",
"OrbitObservable",
"GeometryObservable",
"LocalOpticsObservable",
"LatticeObservable",
"MatrixObservable",
"TrajectoryObservable",
"Need",
]
from collections.abc import Callable, Set
from functools import partial
from enum import Enum
from itertools import repeat
from typing import Union
# For sys.version_info.minor < 9:
from typing import Tuple
import numpy as np
import numpy.typing as npt
from ..lattice import AtError, AxisDef, axis_, plane_
from ..lattice import Lattice, Refpts, End
RefIndex = Union[int, Tuple[int, ...], slice]
# Observables must be pickleable. For this, the evaluation function must be a
# module-level function. No inner, nested function is allowed. So nested
# functions are replaced be module-level callable class instances:
class _Convolve:
def __init__(self, modfun, fun, *args, **kwargs):
self.modfun = modfun
self.fun = fun
self.args = args
self.kwargs = kwargs
def __call__(self, *a):
return self.modfun(self.fun(*a), *self.args, **self.kwargs)
class _ArrayAccess:
"""Access a selected item in an array."""
def __init__(self, index):
self.index = _all_rows(index)
def __call__(self, data):
index = self.index
return data if index is None else data[self.index]
def _record_access(param, index, data):
"""Access a selected item in a record array"""
val = getattr(data, param)
return val if index is None else val[index]
def _fun_access(fun, index, data):
"""Access a selected item in the output of a user-defined function"""
val = fun(data)
return val if index is None else val[index]
def _muf_access(_, index, data):
mu = _record_access("mu", index, data)
return np.remainder(mu, 2.0 * np.pi)
def _mu2pi_access(_, index, data):
mu = _record_access("mu", index, data)
return mu / 2.0 / np.pi
def _mu2pif_access(_, index, data):
mu = _record_access("mu", index, data)
return np.remainder(mu / 2.0 / np.pi, 1.0)
_opdata = {
"muf": _muf_access,
"mu2pi": _mu2pi_access,
"mu2pif": _mu2pif_access,
}
_arrayproc = {
None: None,
"real": np.real,
"imag": np.imag,
"abs": np.absolute,
"angle": np.angle,
"log": np.log,
"exp": np.exp,
"sqrt": np.sqrt,
}
_statproc = {
None: None,
"mean": np.mean,
"std": np.std,
"var": np.var,
"min": np.min,
"max": np.max,
}
def _all_rows(index: RefIndex | None):
"""Prepends "all rows" (":") to an index tuple."""
if index is None:
return None
if isinstance(index, tuple):
return (slice(None),) + index
else:
return slice(None), index
def _get_fun(fname, fdict) -> Callable:
"""Get a processing from its name"""
if callable(fname):
return fname
else:
return fdict[fname]
class _Tune:
"""Get integer tune from the phase advance."""
def __init__(self, idx: RefIndex):
self.fun = partial(_record_access, "mu", _all_rows(idx))
def __call__(self, data):
mu = self.fun(data)
return np.squeeze(mu, axis=0) / 2.0 / np.pi
class _Ring:
"""Get an attribute of a lattice element."""
def __init__(self, attrname, index, refpts):
self.get_val = partial(_record_access, attrname, index)
self.refpts = refpts
def __call__(self, ring):
vals = [self.get_val(el) for el in ring.select(self.refpts)]
return np.array(vals)
[docs]
class Need(Enum):
"""Defines the computation requirements for an :py:class:`Observable`."""
#: Provides the *ring* data to the evaluation function
RING = 1
#: Specify :py:func:`.find_orbit` computation and provide its *orbit* output
#: to the evaluation function
ORBIT = 2
#: Specify :py:func:`.find_m44` or :py:func:`.find_m44` computation and
#: provide its *m44* or *m66* output to the evaluation function
MATRIX = 3
#: Specify :py:func:`.get_optics` computation and provide its *ringdata* output
#: to the evaluation function
GLOBALOPTICS = 4
#: Specify :py:func:`.get_optics` computation and provide its *elemdata* output
#: to the evaluation function
LOCALOPTICS = 5
#: Specify :py:func:`.lattice_pass` computation and provide its *r_out*
#: to the evaluation function
TRAJECTORY = 6
#: Specify :py:func:`.envelope_parameters` computation and provide its
#: *params* output to the evaluation function
EMITTANCE = 7
#: Associated with LOCALOPTICS, require local optics computation at all
#: points: slower but avoids jumps in phase advance
ALL_POINTS = 8
#: Associated with LOCALOPTICS, require the *get_chrom* keyword
CHROMATICITY = 9
#: Associated with LOCALOPTICS, require the *get_w* keyword
W_FUNCTIONS = 10
#: Specify :py:meth:`~.Lattice.get_geometry` computation and provide its output
#: to the evaluation function
GEOMETRY = 11
#: Specify :py:func:`RDT <.get_rdts>` computation and provide its output to
#: the evaluation function
RDT = 12
#: Associated with RDT: request 2nd order calculation
RDT_2ND_ORDER = 13
[docs]
class Observable:
"""Base class for Observables. Can be used for user-defined observables."""
def __init__(
self,
fun: Callable,
*args,
name: str | None = None,
target: npt.ArrayLike | None = None,
weight: npt.ArrayLike = 1.0,
bounds=(0.0, 0.0),
needs: Set[Need] | None = None,
postfun: Callable | str | None = None,
**kwargs,
):
r"""Args:
name: Observable name. If :py:obj:`None`, an explicit
name will be generated
fun: :ref:`evaluation function <base_eval>`
*args: Arguments provided to the evaluation function
target: Target value for a constraint. If :py:obj:`None`
(default), the residual will always be zero.
weight: Weight factor: the residual is
:pycode:`((value-target)/weight)**2`
bounds: Tuple of lower and upper bounds. The parameter
is constrained in the interval
[*target*\ +\ *low_bound* *target*\ +\ *up_bound*]
postfun: Post-processing function. It can be any numpy ufunc or a
function name in {"real", "imag", "abs", "angle", "log", "exp", "sqrt"}.
needs: Set of requirements. This selects the data provided
to the evaluation function. *needs* items are members of the
:py:class:`Need` enumeration
Keyword Args:
**kwargs: Keyword arguments provided to the evaluation function
The *target*, *weight* and *bounds* inputs must be broadcastable to the
shape of *value*.
.. _base_eval:
.. rubric:: User-defined evaluation function
The general form is:
:pycode:`value = fun(*data, *args, **kwargs)`
- *data* depends on the *needs* argument, and by default is empty. If several
needed values are specified, their order is: *ring*, *orbit*, *m44/m66*,
*ringdata*, *elemdata*, *r_out*, *params*, *geomdata*,
- *args* are the positional arguments provided to the observable constructor,
- *kwargs* are the keyword arguments provided to the observable constructor,
- *value* is the value of the observable.
For user-defined evaluation functions using linear optics data or
emittance data, it is recommended to use
:py:class:`LocalOpticsObservable`, :py:obj:`GlobalOpticsObservable`
or :py:class:`EmittanceObservable` which provide the corresponding
*data* argument.
"""
name = fun.__name__ if name is None else name
postfun = _get_fun(postfun, _arrayproc)
if postfun:
name = f"{postfun.__name__}({name})"
fun = _Convolve(postfun, fun)
self.fun: Callable = fun #: Evaluation function
self.needs: Set[Need] = needs or set() #: Set of requirements
self.name: str = name #: Observable name
self.target: npt.ArrayLike | None = target #: Target value
self.w: npt.NDArray[float] = np.asarray(weight, dtype=float)
self.lbound, self.ubound = bounds
self.initial: npt.NDArray[float] | None = None
self._value: npt.NDArray[float] | Exception | None = None
self._shape: tuple[int, ...] | None = None
self.args = args
self.kwargs = kwargs
def __str__(self):
"""Return the string representation of the Observable."""
return "\n".join((self._header(), self._all_lines()))
@staticmethod
def _header():
"""Header line."""
fstring = "\n {:<12} {:>16} {:>16} {:>16} {:>16} {:>16} "
return fstring.format(
"location", "Initial", "Actual", "Low bound", "High bound", "deviation"
)
@staticmethod
def _line(loc, *items):
def pitem(v):
if v is None:
return f" {'-':>14} "
elif isinstance(v, Exception):
return f" {type(v).__name__:>16} "
elif isinstance(v, np.ndarray) and v.ndim > 0:
if v.size == 0:
return f" {'[]':16} "
else:
return f" [{v.flat[0]:< 10.4} ...] "
else:
return f" {v: 16.6} "
its = [pitem(v) for v in items]
return f" {loc:<12}" + "".join(its)
def _all_lines(self):
vnow = self._value
if vnow is None or isinstance(vnow, Exception):
deviation = None
vmin = None
vmax = None
else:
deviation = self.deviation
if self.target is None:
vmin = None
vmax = None
else:
target = np.broadcast_to(self.target, vnow.shape) # type: ignore
vmin = target + self.lbound
vmax = target + self.ubound
values = self._line("", self.initial, vnow, vmin, vmax, deviation)
return "\n".join((self.name, values))
def _setup(self, ring: Lattice):
"""Setup function called when the observable is added to a list."""
pass
[docs]
def evaluate(self, *data, initial: bool = False) -> npt.NDArray[float] | Exception:
"""Compute and store the value of the observable.
The direct evaluation of a single :py:class:`Observable` is normally
not used. This method is called by the :py:class:`.ObservableList`
container which provides the *data* arguments.
Args:
*data: Raw data, provided by :py:class:`.ObservableList` and
sent to the evaluation function
initial: It :py:obj:`None`, store the result as the initial
value
Returns:
value: The value of the observable or the error in evaluation
"""
for d in data:
if isinstance(d, Exception):
message = f"Evaluation of {self.name} failed: {d.args[0]}"
err = type(d)(message).with_traceback(d.__traceback__)
self._value = err
return err
val = np.asarray(self.fun(*data, *self.args, **self.kwargs))
if initial:
self.initial = val
self._shape = val.shape
self._value = val
return val
[docs]
def check(self) -> bool:
"""Check if evaluation is done
Returns:
ok: :py:obj:`True` if evaluation is done, :py:obj:`False` otherwise.
Raises:
AtError: if the value is doubtful: evaluation failed, empty value…
"""
return self.value is not None
[docs]
@staticmethod
def check_value(value: npt.NDArray[float] | Exception) -> npt.NDArray[float]:
if isinstance(value, Exception):
raise type(value)(value.args[0]) from value
return value
@property
def value(self) -> npt.NDArray[float]:
"""Value of the observable."""
return self.check_value(self._value)
@property
def weight(self) -> npt.NDArray[float]:
"""Observable weight."""
return np.broadcast_to(self.w, self._value.shape) # type: ignore
@weight.setter
def weight(self, w: npt.ArrayLike):
self.w = np.asarray(w, dtype=float)
@property
def weighted_value(self) -> npt.NDArray[float]:
"""Weighted value of the Observable, computed as
:pycode:`weighted_value = value/weight`.
"""
return self.value / self.w
@property
def deviation(self) -> npt.NDArray[float]:
"""Deviation from target value, computed as
:pycode:`deviation = value-target`.
If *target* is :py:obj:`None`, the deviation is zero for any value.
"""
vnow = self.value
if vnow is None:
deviation = None
elif self.target is None:
deviation = np.broadcast_to(0.0, vnow.shape)
else:
vsh = vnow.shape
diff = np.atleast_1d(vnow - np.broadcast_to(self.target, vsh))
lb = diff - self.lbound
ub = diff - self.ubound
lb[lb >= 0] = 0
ub[ub <= 0] = 0
deviation = (lb + ub).reshape(vsh)
return deviation
@property
def weighted_deviation(self) -> npt.NDArray[float]:
""":pycode:`weighted_deviation = (value-target)/weight`.
If *target* is :py:obj:`None`, the weighted deviation is zero for any value.
"""
return self.deviation / self.w
@property
def residual(self) -> npt.NDArray[float]:
"""residual, computed as :pycode:`residual = ((value-target)/weight)**2`.
If *target* is :py:obj:`None`, the residual is zero for any value.
"""
# absolute necessary for complex data
return np.absolute(self.weighted_deviation) ** 2
@staticmethod
def _set_name(name, param, index):
"""Compute a default observable names."""
if name is None:
if (
index is Ellipsis
or index is None
or (isinstance(index, str) and index in {":", "..."})
):
subscript = ""
elif isinstance(index, tuple):
ids = ", ".join(str(k) for k in index)
subscript = f"[{ids}]"
else:
subscript = f"[{index}]"
if callable(param):
try:
base = param.__name__
except AttributeError:
base = "<function>"
else:
base = param
name = base + subscript
return name
[docs]
class RingObservable(Observable):
"""Observe any user-defined property of a ring."""
def __init__(
self,
fun: Callable,
name: str | None = None,
**kwargs,
):
r"""Args:
fun: :ref:`user-defined evaluation function <ring_eval>`
name: Observable name. If :py:obj:`None`, an explicit
name will be generated.
Keyword Args:
target: Target value for a constraint. If :py:obj:`None`
(default), the residual will always be zero.
weight: Weight factor: the residual is
:pycode:`((value-target)/weight)**2`
bounds: Tuple of lower and upper bounds. The parameter
is constrained in the interval
[*target*\ +\ *low_bound* *target*\ +\ *up_bound*]
postfun: Post-processing function. It can be any numpy ufunc or a
function name in {"real", "imag", "abs", "angle", "log", "exp", "sqrt"}.
The *target*, *weight* and *bounds* inputs must be broadcastable to the
shape of *value*.
.. _ring_eval:
.. rubric:: User-defined evaluation function
It is called as:
:pycode:`value = fun(ring)`
- *ring* is the lattice description,
- *value* is the value of the Observable.
Examples:
>>> def circumference(ring):
... return ring.get_s_pos(len(ring))[0]
>>> obs = RingObservable(circumference)
Defines an Observable for the ring circumference.
>>> def momentum_compaction(ring):
... return ring.get_mcf()
>>> obs = RingObservable(momentum_compaction)
Defines an Observable for the momentum compaction factor.
"""
needs = {Need.RING}
name = self._set_name(name, fun, None)
super().__init__(fun, name=name, needs=needs, **kwargs)
[docs]
class ElementObservable(Observable):
"""Base class for Observables linked to a position in the lattice."""
def __init__(
self,
fun: Callable,
refpts: Refpts,
name: str | None = None,
statfun: Callable | str | None = None,
postfun: Callable | str | None = None,
**kwargs,
):
r"""Args:
fun: :ref:`evaluation function <base_eval>`
refpts: Observation points.
See ":ref:`Selecting elements in a lattice <refpts>`"
name: Observable name. If :py:obj:`None`, an explicit
name will be generated
postfun: Post-processing function. It can be any numpy ufunc or a
function name in {"real", "imag", "abs", "angle", "log", "exp", "sqrt"}.
statfun: Statistics post-processing function. it can be a numpy
function or a function name in {"mean", "std", "var", "min", "max"}.
Example: :pycode:`statfun=numpy.mean`.
Keyword Args:
target: Target value for a constraint. If :py:obj:`None`
(default), the residual will always be zero.
weight: Weight factor: the residual is
:pycode:`((value-target)/weight)**2`
bounds: Tuple of lower and upper bounds. The parameter
is constrained in the interval
[*target*\ +\ *low_bound* *target*\ +\ *up_bound*]
.. rubric:: Shape of the value
A :pycode:`nrefs` dimension, with :pycode:`nrefs` the number
of reference points, is prepended to the shape of the output of *fun*,
**even if nrefs == 1**. The *target*, *weight* and *bounds* inputs must be
broadcastable to the shape of *value*.
"""
name = fun.__name__ if name is None else name
postfun = _get_fun(postfun, _arrayproc)
if postfun:
name = f"{postfun.__name__}({name})"
fun = _Convolve(postfun, fun)
statfun = _get_fun(statfun, _statproc)
if statfun:
summary = kwargs.pop("summary", True)
name = f"{statfun.__name__}({name})"
fun = _Convolve(statfun, fun, axis=0)
else:
summary = kwargs.pop("summary", False)
super().__init__(fun, name=name, **kwargs)
self.summary = summary
self.refpts = refpts
self._boolrefs = None
self._excluded = None
self._locations = [""]
[docs]
def check(self) -> bool:
ok = super().check()
shp = self._shape
if ok and shp and shp[0] <= 0:
raise AtError(
f"Observable {self.name!r}: No location selected in the lattice."
)
return ok
def _all_lines(self):
if self.summary:
return super()._all_lines()
else:
vnow = self._value
if vnow is None or isinstance(vnow, Exception):
vnow = repeat(vnow)
deviation = repeat(None)
vmin = repeat(None)
vmax = repeat(None)
else:
deviation = self.deviation
if self.target is None:
vmin = repeat(None)
vmax = repeat(None)
else:
target = np.broadcast_to(self.target, vnow.shape) # type: ignore
vmin = target + self.lbound
vmax = target + self.ubound
vini = self.initial
if vini is None:
vini = repeat(None)
viter = zip(self._locations, vini, vnow, vmin, vmax, deviation)
values = "\n".join(self._line(*vv) for vv in viter)
return "\n".join((self.name, values))
def _setup(self, ring: Lattice):
boolrefs = ring.get_bool_index(self.refpts)
excluded = ring.get_bool_index(self._excluded)
boolrefs &= ~excluded
self._boolrefs = boolrefs
locs = [el.FamName for el in ring.select(self._boolrefs[:-1])]
if boolrefs[-1]:
locs.append("End")
self._locations = locs
[docs]
class GeometryObservable(ElementObservable):
"""Observe the geometrical parameters of the reference trajectory.
Process the *geomdata* output of :py:func:`.get_geometry`.
"""
_field_list = {"x", "y", "angle"}
def __init__(self, refpts: Refpts, param: str, name: str | None = None, **kwargs):
# noinspection PyUnresolvedReferences
r"""Args:
refpts: Observation points.
See ":ref:`Selecting elements in a lattice <refpts>`"
param: Geometry parameter name: one in {'x', 'y', 'angle'}
name: Observable name. If :py:obj:`None`, an explicit
name will be generated.
Keyword Args:
statfun: Post-processing function called on the value of the
observable. Example: :pycode:`statfun=numpy.mean`
target: Target value for a constraint. If :py:obj:`None`
(default), the residual will always be zero.
weight: Weight factor: the residual is
:pycode:`((value-target)/weight)**2`
bounds: Tuple of lower and upper bounds. The parameter
is constrained in the interval
[*target*\ +\ *low_bound* *target*\ +\ *up_bound*]
The *target*, *weight* and *bounds* inputs must be broadcastable to the
shape of *value*.
Example:
>>> obs = GeometryObservable(at.Monitor, param="x")
Observe x coordinate of monitors
"""
if param not in self._field_list:
raise ValueError(f"Expected {param!r} to be one of {self._field_list!r}")
name = self._set_name(name, "geometry", param)
fun = partial(_record_access, param, None)
needs = {Need.GEOMETRY}
super().__init__(fun, refpts, needs=needs, name=name, **kwargs)
[docs]
class OrbitObservable(ElementObservable):
"""Observe the closed orbit coordinates at selected locations.
Process the *orbit* output of :py:func:`.find_orbit`.
"""
def __init__(
self, refpts: Refpts, axis: AxisDef = None, name: str | None = None, **kwargs
):
# noinspection PyUnresolvedReferences
r"""Args:
refpts: Observation points.
See ":ref:`Selecting elements in a lattice <refpts>`"
axis: Index in the orbit vector, If :py:obj:`None`,
the whole vector is specified
name: Observable name. If :py:obj:`None`, an explicit
name will be generated.
Keyword Args:
postfun: Post-processing function. It can be any numpy ufunc or a
function name in {"real", "imag", "abs", "angle", "log", "exp", "sqrt"}.
statfun: Statistics post-processing function. it can be a numpy
function or a function name in {"mean", "std", "var", "min", "max"}.
Example: :pycode:`statfun=numpy.mean`.
target: Target value for a constraint. If :py:obj:`None`
(default), the residual will always be zero.
weight: Weight factor: the residual is
:pycode:`((value-target)/weight)**2`
bounds: Tuple of lower and upper bounds. The parameter
is constrained in the interval
[*target*\ +\ *low_bound* *target*\ +\ *up_bound*]
.. rubric:: Shape of the value
If *axis* is :py:obj:`None` (whole orbit vector), then *value* has shape
:pycode:`(nrefs, 6)` with :pycode:`nrefs` number of reference points: a
:pycode:`nrefs` dimension is prepended to the shape of the orbit vector,
**even if nrefs == 1**. A single coordinate has shape :pycode:`(nrefs,)`.
The *target*, *weight* and *bounds* inputs must be broadcastable to the shape
of *value*.
Example:
>>> obs = OrbitObservable(at.Monitor, axis=0)
Observe the horizontal closed orbit at monitor locations
"""
name = self._set_name(name, "orbit", axis_(axis, key="code"))
fun = _ArrayAccess(axis_(axis, key="index"))
needs = {Need.ORBIT}
super().__init__(fun, refpts, needs=needs, name=name, **kwargs)
[docs]
class MatrixObservable(ElementObservable):
"""Observe the closed orbit at selected locations.
Processs the result of calling :py:func:`.find_m44` or :py:func:`.find_m44`
depending upon :py:meth:`~.Lattice.is_6d`.
"""
def __init__(
self,
refpts: Refpts,
axis: AxisDef = Ellipsis,
name: str | None = None,
**kwargs,
):
# noinspection PyUnresolvedReferences
r"""Args:
refpts: Observation points.
See ":ref:`Selecting elements in a lattice <refpts>`"
axis: Index in the transfer matrix, If :py:obj:`Ellipsis`,
the whole matrix is specified
name: Observable name. If :py:obj:`None`, an explicit
name will be generated.
Keyword Args:
postfun: Post-processing function. It can be any numpy ufunc or a
function name in {"real", "imag", "abs", "angle", "log", "exp", "sqrt"}.
statfun: Statistics post-processing function. it can be a numpy
function or a function name in {"mean", "std", "var", "min", "max"}.
Example: :pycode:`statfun=numpy.mean`.
target: Target value for a constraint. If :py:obj:`None`
(default), the residual will always be zero.
weight: Weight factor: the residual is
:pycode:`((value-target)/weight)**2`
bounds: Tuple of lower and upper bounds. The parameter
is constrained in the interval
[*target*\ +\ *low_bound* *target*\ +\ *up_bound*]
The *target*, *weight* and *bounds* inputs must be broadcastable to the
shape of *value*.
Example:
>>> obs = MatrixObservable(at.Monitor, axis=("x", "px"))
Observe the transfer matrix from origin to monitor locations and
extract T[0,1]
"""
name = self._set_name(name, "matrix", axis_(axis, key="code"))
fun = _ArrayAccess(axis_(axis, key="index"))
needs = {Need.MATRIX}
super().__init__(fun, refpts, needs=needs, name=name, **kwargs)
class _GlobalOpticsObservable(Observable):
def __init__(
self, param: str, plane: AxisDef = None, name: str | None = None, **kwargs
):
# noinspection PyUnresolvedReferences
r"""Args:
param: Optics parameter name (see :py:func:`.get_optics`)
or user-defined evaluation function called as:
:pycode:`value = fun(ringdata, ring=ring)` and returning the value of
the Observable
plane: Index in the parameter array, If :py:obj:`None`,
the whole array is specified
name: Observable name. If :py:obj:`None`, an explicit
name will be generated.
Keyword Args:
target: Target value for a constraint. If :py:obj:`None`
(default), the residual will always be zero.
weight: Weight factor: the residual is
:pycode:`((value-target)/weight)**2`
bounds: Tuple of lower and upper bounds. The parameter
is constrained in the interval
[*target*\ +\ *low_bound* *target*\ +\ *up_bound*]
postfun: Post-processing function. It can be any numpy ufunc or a
function name in {"real", "imag", "abs", "angle", "log", "exp", "sqrt"}.
The *target*, *weight* and *bounds* inputs must be broadcastable to the
shape of *value*.
"""
needs = {Need.GLOBALOPTICS}
name = self._set_name(name, param, plane_(plane, key="code"))
if callable(param):
fun = partial(_fun_access, param, plane_(plane, key="index"))
needs.add(Need.CHROMATICITY)
else:
fun = partial(_record_access, param, plane_(plane, key="index"))
if param == "chromaticity":
needs.add(Need.CHROMATICITY)
super().__init__(fun, needs=needs, name=name, **kwargs)
[docs]
class LocalOpticsObservable(ElementObservable):
"""Observe a local optics parameter at selected locations.
Process the local output of :py:func:`.get_optics`.
"""
def __init__(
self,
refpts: Refpts,
param: str | Callable,
plane: AxisDef = Ellipsis,
name: str | None = None,
all_points: bool = False,
**kwargs,
):
# noinspection PyUnresolvedReferences
r"""Args:
refpts: Observation points.
See ":ref:`Selecting elements in a lattice <refpts>`"
param: :ref:`Optics parameter name <localoptics_param>`
or :ref:`user-defined evaluation function <localoptics_eval>`
plane: Index in the parameter array, If :py:obj:`Ellipsis`,
the whole array is specified
name: Observable name. If :py:obj:`None`, an explicit
name will be generated
all_points: Compute the local optics at all elements. This avoids
discontinuities in phase advances. This is automatically set for the
'mu' parameter, but may need to be specified for user-defined evaluation
functions using the phase advance.
Keyword Args:
summary: Set to :py:obj:`True` if the user-defined
evaluation function returns a single item (see below)
postfun: Post-processing function. It can be any numpy ufunc or a
function name in {"real", "imag", "abs", "angle", "log", "exp", "sqrt"}.
statfun: Statistics post-processing function. it can be a numpy
function or a function name in {"mean", "std", "var", "min", "max"}.
Example: :pycode:`statfun=numpy.mean`.
target: Target value for a constraint. If :py:obj:`None`
(default), the residual will always be zero.
weight: Weight factor: the residual is
:pycode:`((value-target)/weight)**2`
bounds: Tuple of lower and upper bounds. The parameter
is constrained in the interval
[*target*\ +\ *low_bound* *target*\ +\ *up_bound*]
.. rubric:: Shape of the value
If the requested attribute has shape :pycode:`shp`, then
*value* has shape :pycode:`(nrefs,) + shp` with :pycode:`nrefs` number of
reference points: a :pycode:`nrefs` dimension is prepended to the shape of
the attribute, **even if nrefs == 1**. The *target*, *weight* and *bounds*
inputs must be broadcastable to the shape of *value*. For instance, a *target*
with shape :pycode:`shp` will automatically broadcast and apply to all
reference points.
.. _localoptics_param:
.. rubric:: Optics parameter name
In addition to :py:func:`.get_optics` parameter names, LocalOpticsObservable
adds 3 parameters: *muf*, *mu2pi* and *mu2pif*:
================ ===================================================
**s_pos** longitudinal position [m]
**M** (6, 6) transfer matrix M from the beginning of ring
to the entrance of the element
**closed_orbit** (6,) closed orbit vector
**dispersion** (4,) dispersion vector
**A** (6, 6) A-matrix
**R** (3, 6, 6) R-matrices
**beta** :math:`\left[ \beta_x,\beta_y \right]` vector
**alpha** :math:`\left[ \alpha_x,\alpha_y \right]` vector
**mu** :math:`\left[ \mu_x,\mu_y \right]`, betatron phase
**mu2pi** :math:`\left[ \mu_x,\mu_y \right]/2\pi`, reduced betatron
phase
**muf** :math:`\left[ \mu_x,\mu_y \right]`, betatron phase
(modulo :math:`2\pi`)
**mu2pif** :math:`\mathrm{frac}(\left[ \mu_x,\mu_y \right]/2\pi)`,
fractional part of the reduced betatron phase
**W** :math:`\left[ W_x,W_y \right]` only if *get_w*
is :py:obj:`True`: chromatic amplitude function
**Wp** :math:`\left[ Wp_x,Wp_y \right]` only if *get_w*
is :py:obj:`True`: chromatic phase function
**dalpha** (2,) alpha derivative vector
(:math:`\Delta \alpha/ \delta_p`)
**dbeta** (2,) beta derivative vector
(:math:`\Delta \beta/ \delta_p`)
**dmu** (2,) mu derivative vector
(:math:`\Delta \mu/ \delta_p`)
**ddispersion** (4,) dispersion derivative vector
(:math:`\Delta D/ \delta_p`)
**dR** (3, 6, 6) R derivative vector
(:math:`\Delta R/ \delta_p`)
================ ===================================================
.. _localoptics_eval:
.. rubric:: User-defined evaluation function
The observable value is computed as:
:pycode:`value = fun(elemdata)[plane]`
- *elemdata* is the output of :py:func:`.get_optics`, evaluated at the *refpts*
of the observable,
- *value* is the value of the Observable and must have one line per
refpoint. Alternatively, it may be a single line, but then the
*summary* keyword must be set to :py:obj:`True`.
- the *plane* keyword then selects the desired values in the function output.
Examples:
>>> obs = LocalOpticsObservable(at.Monitor, "beta")
Observe the beta in both planes at all :py:class:`.Monitor`
locations
>>> obs = LocalOpticsObservable(
... at.Quadrupole, "beta", plane="y", statfun=np.max
... )
Observe the maximum vertical beta in Quadrupoles
>>> def phase_advance(elemdata):
... mu = elemdata.mu
... return mu[-1] - mu[0]
>>>
>>> allobs.append(
... LocalOpticsObservable(
... [33, 101], phase_advance, plane="y", all_points=True, summary=True
... )
... )
The user-defined evaluation function computes the phase-advance
between the 1st and last given reference points, here the elements
33 and 101 of the lattice
"""
if param in {"M", "closed_orbit", "dispersion", "A", "R"}:
ax_ = axis_
else:
ax_ = plane_
needs = {Need.LOCALOPTICS}
name = self._set_name(name, param, ax_(plane, key="code"))
index = _all_rows(ax_(plane, key="index"))
if callable(param):
fun = partial(_fun_access, param, ax_(plane, key="index"))
else:
fun = partial(_opdata.get(param, _record_access), param, index)
if param in {"mu", "mu2pi"} or all_points:
needs.add(Need.ALL_POINTS)
if param in {"W", "Wp", "dalpha", "dbeta", "dmu", "ddispersion", "dR"}:
needs.add(Need.W_FUNCTIONS)
super().__init__(fun, refpts, needs=needs, name=name, **kwargs)
[docs]
class LatticeObservable(ElementObservable):
"""Observe an attribute of selected lattice elements."""
def __init__(
self,
refpts: Refpts,
attrname: str,
index: int | None = None,
name: str | None = None,
**kwargs,
):
# noinspection PyUnresolvedReferences
r"""Args:
refpts: Elements to be observed
See ":ref:`Selecting elements in a lattice <refpts>`"
attrname: Attribute name
index: Index in the attribute array. If :py:obj:`None`,
the whole array is specified
name: Observable name. If :py:obj:`None`, an explicit
name will be generated.
Keyword Args:
postfun: Post-processing function. It can be any numpy ufunc or a
function name in {"real", "imag", "abs", "angle", "log", "exp", "sqrt"}.
statfun: Statistics post-processing function. it can be a numpy
function or a function name in {"mean", "std", "var", "min", "max"}.
Example: :pycode:`statfun=numpy.mean`.
Example:
>>> obs = LatticeObservable(
... at.Sextupole, "KickAngle", index=0, statfun=np.sum
... )
Observe the sum of horizontal kicks in Sextupoles
"""
fun = _Ring(attrname, index, refpts)
needs = {Need.RING}
name = self._set_name(name, attrname, index)
super().__init__(fun, refpts, needs=needs, name=name, **kwargs)
[docs]
class TrajectoryObservable(ElementObservable):
"""Observe trajectory coordinates at selected locations.
Process the *r_out* output if :py:meth:`.Lattice.track`
"""
def __init__(
self,
refpts: Refpts,
axis: AxisDef = Ellipsis,
name: str | None = None,
**kwargs,
):
r"""Args:
refpts: Observation points.
See ":ref:`Selecting elements in a lattice <refpts>`"
axis: Index in the orbit array, If :py:obj:`Ellipsis`,
the whole array is specified
name: Observable name. If :py:obj:`None`, an explicit
name will be generated.
Keyword Args:
postfun: Post-processing function. It can be any numpy ufunc or a
function name in {"real", "imag", "abs", "angle", "log", "exp", "sqrt"}.
statfun: Statistics post-processing function. it can be a numpy
function or a function name in {"mean", "std", "var", "min", "max"}.
Example: :pycode:`statfun=numpy.mean`.
target: Target value for a constraint. If :py:obj:`None`
(default), the residual will always be zero.
weight: Weight factor: the residual is
:pycode:`((value-target)/weight)**2`
bounds: Tuple of lower and upper bounds. The parameter
is constrained in the interval
[*target*\ +\ *low_bound* *target*\ +\ *up_bound*]
The *target*, *weight* and *bounds* inputs must be broadcastable to the
shape of *value*.
"""
name = self._set_name(name, "trajectory", axis_(axis, key="code"))
fun = _ArrayAccess(axis_(axis, key="index"))
needs = {Need.TRAJECTORY}
super().__init__(fun, refpts, needs=needs, name=name, **kwargs)
[docs]
class EmittanceObservable(Observable):
"""Observe emittance-related parameters.
Process the output of :py:func:`.envelope_parameters`.
"""
def __init__(
self, param: str, plane: AxisDef = None, name: str | None = None, **kwargs
):
r"""Args:
param: Parameter name (see :py:func:`.envelope_parameters`) or
:ref:`user-defined evaluation function <emittance_eval>`
plane: One out of {0, 'x', 'h', 'H'} for horizontal plane,
one out of {1, 'y', 'v', 'V'} for vertival plane or one out of
{2, 'z', 'l', 'L'} for longitudinal plane
name: Observable name. If :py:obj:`None`, an explicit
name will be generated.
Keyword Args:
postfun: Post-processing function. It can be any numpy ufunc or a
function name in {"real", "imag", "abs", "angle", "log", "exp", "sqrt"}.
statfun: Statistics post-processing function. it can be a numpy
function or a function name in {"mean", "std", "var", "min", "max"}.
Example: :pycode:`statfun=numpy.mean`.
target: Target value for a constraint. If :py:obj:`None`
(default), the residual will always be zero.
weight: Weight factor: the residual is
:pycode:`((value-target)/weight)**2`
bounds: Tuple of lower and upper bounds. The parameter
is constrained in the interval
[*target*\ +\ *low_bound* *target*\ +\ *up_bound*]
.. _emittance_eval:
.. rubric:: User-defined evaluation function
It is called as:
:pycode:`value = fun(paramdata)`
*paramdata* if the :py:class:`.RingParameters` object returned by
:py:func:`.envelope_parameters`.
*value* is the value of the Observable.
Example:
>>> EmittanceObservable("emittances", plane="h")
Observe the horizontal emittance
"""
name = self._set_name(name, param, plane_(plane, key="code"))
if callable(param):
fun = param
else:
fun = partial(_record_access, param, plane_(plane, key="index"))
needs = {Need.EMITTANCE}
super().__init__(fun, needs=needs, name=name, **kwargs)
# noinspection PyPep8Naming
[docs]
def GlobalOpticsObservable(
param: str,
plane: AxisDef = Ellipsis,
name: str | None = None,
use_integer: bool = False,
**kwargs,
):
# noinspection PyUnresolvedReferences
r"""Observe a global optics parameter.
Process the *ringdata* output of :py:func:`.get_optics`.
Args:
param: Optics parameter name (see :py:func:`.get_optics`)
or :ref:`user-defined evaluation function <globaloptics_eval>`
plane: Index in the parameter array, If :py:obj:`Ellipsis`,
the whole array is specified
name: Observable name. If :py:obj:`None`, an explicit
name will be generated
use_integer: For *'tune'* parameter: derive the tune from the
phase advance to avoid skipping integers. Slower than looking only
at the fractional part
Keyword Args:
target: Target value for a constraint. If :py:obj:`None`
(default), the residual will always be zero.
weight: Weight factor: the residual is
:pycode:`((value-target)/weight)**2`
bounds: Tuple of lower and upper bounds. The parameter
is constrained in the interval
[*target*\ +\ *low_bound* *target*\ +\ *up_bound*]
postfun: Post-processing function. It can be any numpy ufunc or a
function name in {"real", "imag", "abs", "angle", "log", "exp", "sqrt"}.
The *target*, *weight* and *bounds* inputs must be broadcastable to the
shape of *value*.
.. _globaloptics_eval:
.. rubric:: User-defined evaluation function
It is called as:
:pycode:`value = fun(ring, ringdata)`
- *ringdata* is the output of :py:func:`.get_optics`,
- *value* is the value of the Observable.
Examples:
>>> obs = GlobalOpticsObservable("tune", use_integer=True)
Observe the tune in both planes, including the integer part (slower)
>>> obs = GlobalOpticsObservable("chromaticity", plane="v")
Observe the vertical chromaticity
"""
if param == "tune" and use_integer:
# noinspection PyProtectedMember
name = ElementObservable._set_name(name, param, plane_(plane, key="code"))
return LocalOpticsObservable(
End,
_Tune(plane_(plane, key="index")),
name=name,
summary=True,
all_points=True,
**kwargs,
)
else:
return _GlobalOpticsObservable(param, plane=plane, name=name, **kwargs)