Source code for at.latticetools.observables

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__ = [
    "ElementObservable",
    "EmittanceObservable",
    "GeometryObservable",
    "GlobalOpticsObservable",
    "LatticeObservable",
    "LocalOpticsObservable",
    "MatrixObservable",
    "Need",
    "Observable",
    "OrbitObservable",
    "RingObservable",
    "TrajectoryObservable",
]

from collections.abc import Callable, Set as AbstractSet, Mapping
from functools import partial
from enum import Enum
from itertools import repeat
from typing import ClassVar, Any

import numpy as np
import numpy.typing as npt

from ..lattice import AtError, AxisDef, axis_, plane_
from ..lattice import Lattice, Refpts, End

RefIndex = 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, **kwargs):
    """Access a selected item in the output of a user-defined function."""
    val = fun(data, **kwargs)
    return val if index is None else val[index]


def _muf_access(_, index, data, **kwargs):
    mu = _record_access("mu", index, data, **kwargs)
    return np.remainder(mu, 2.0 * np.pi)


def _mu2pi_access(_, index, data, **kwargs):
    mu = _record_access("mu", index, data, **kwargs)
    return mu / 2.0 / np.pi


def _mu2pif_access(_, index, data, **kwargs):
    mu = _record_access("mu", index, data, **kwargs)
    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 | None:
    """Get a processing from its name."""
    if callable(fname):
        return fname
    else:
        return fdict[fname]


def _subscript(plane):
    idx = axis_(plane, key="index")
    if isinstance(idx, tuple):
        return "".join(str(i) for i in idx)
    else:
        return ""


def _mod_name(name, fun, *args):
    if name:
        if fun:
            name = f"{fun.__name__}({name})"
        if args:
            name = _mod_name(name, *args)
    return name


def _tune(data, **__):
    return data.mu[-1] / 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, lattice, **__):
        vals = [self.get_val(el) for el in lattice.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 #: Specify :py:func:`.ohmi_envelope` computation and provide its *beamdata* output #: to the evaluation function GLOBALEMIT = 14 #: Specify :py:func:`.ohmi_envelope` computation and provide its *emit* output #: to the evaluation function LOCALEMIT = 15 #: For observables needing a 4D lattice NEED_4D = 16 #: For observables needing a 6D lattice NEED_6D = 17
[docs] class Observable: """Base class for Observables. Can be used for user-defined observables.""" # Class attributes _default: ClassVar[tuple] = ("{param}[{plane}]", "{param}", lambda x: x) _pinfo: ClassVar[dict] = {} # Instance attributes name: str #: Observable name. A default name is automatically generated. #: Label used in plot legends. #: #: It may contain LaTeX math code. Example: ``"$\beta_x$"`` will appear as #: :math:`\beta_x`. #: #: By default the label is identical to the name of the Observable. #: Labels starting with a ``_`` will not appear in the legends. label: str #: Line formatting used when plotting the Observable. See #: :py:meth:`~matplotlib.axes.Axes.plot` for a description of line formatting. #: *plot_fmt* may be a :py:class:`str` for simple formatting (ex.: ``"o-"``) or a #: :py:class:`dict` for detailed formatting (ex.: :pycode:`{"linewidth": 3.0}`). plot_fmt: str | Mapping fun: Callable #: Evaluation function. needs: AbstractSet[Need] #: Set of requirements. target: npt.ArrayLike | None #: Target value. eval_args: tuple[Any, ...] #: Evaluation arguments eval_kw: dict[str, Any] #: Evaluation keywords def __init__( self, fun: Callable, *eval_args, name: str | None = None, target: npt.ArrayLike | None = None, weight: npt.ArrayLike = 1.0, bounds=(0.0, 0.0), needs: AbstractSet[Need] | None = None, postfun: Callable | str | None = None, plot_fmt: str | Mapping | None = None, label: str | None = None, **eval_kw, ): r"""Args: name: Observable name. If :py:obj:`None`, an explicit name will be generated fun: :ref:`evaluation function <base_eval>` *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. plot_fmt: Line formatting used when plotting the Observable. See :py:meth:`~matplotlib.axes.Axes.plot` for a description of line formatting. *plot_fmt* may be a :py:class:`str` for simple formatting (ex.: ``"o-"``) or a :py:class:`dict` for detailed formatting (ex.: :pycode:`{"linewidth": 3.0}`). Keyword Args: \*\*eval_kw: 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, *eval_args, **eval_kw)` - *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*, - *eval_args* are the evaluation positional arguments provided to the observable constructor, - *eval_kw* are the evaluation keywords provided to the observable constructor, to the constructor of the enclosing :py:class:`.ObservableList` and to the :py:meth:`~.ObservableList.evaluate` method. - *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 = name or fun.__name__ postfun = _get_fun(postfun, _arrayproc) if postfun: fun = _Convolve(postfun, fun) name = _mod_name(name, postfun) label = _mod_name(label, postfun) self.fun = fun self.needs = needs or set() self.name = name self.target = target 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 if plot_fmt is not None: self.plot_fmt = plot_fmt self.label = label or name self._axis_label = eval_kw.pop("axis_label", None) self.eval_args = eval_args self.eval_kw = eval_kw @classmethod def _ax_lab(cls, param, plane) -> str | None: if callable(param): return None else: _, fmt, code = cls._pinfo.get(param, cls._default) return fmt.format(plane=code(plane)) @classmethod def _pl_lab(cls, param, plane) -> str | None: if callable(param): return None else: fmt, _, code = cls._pinfo.get(param, cls._default) return fmt.format(plane=code(plane)) @property def axis_label(self): """Label used for the y-axes of plots (read only).""" return self._axis_label 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."""
[docs] def evaluate( self, *data, initial: bool = False, **evalkw ) -> 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.eval_args, **(self.eval_kw | evalkw))) 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, needs: Need | None = None, **eval_kw, ): r"""Args: fun: :ref:`user-defined evaluation function <ring_eval>` name: Observable name. If :py:obj:`None`, an explicit name will be generated. needs: :py:obj:`.Need.NEED_4D` or :py:obj:`.Need.NEED_6D`. Ensure that the lattice provided to the evaluation function has the desired property. If :py:obj:`None`, the lattice is the one given to :py:meth:`~.ObservableList.evaluate` unmodified. 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*. .. rubric:: Evaluation keywords These values must be provided to the :py:meth:`~.ObservableList.evaluate` method. Default values may be given at instantiation. * **ring** – Lattice description, * **dp** – Momentum deviation. Defaults to :py:obj:`None`, * **dct** – Path lengthening. Defaults to :py:obj:`None`, * **df** – Deviation from the nominal RF frequency. Defaults to :py:obj:`None`, .. _ring_eval: .. rubric:: User-defined evaluation function It is called as: :pycode:`value = fun(ring, **eval_kw)` - *ring* is the lattice description, - *eval_kw* are the evaluation keywords provided to the observable constructor, to the constructor of the enclosing :py:class:`.ObservableList` and to the :py:meth:`~.ObservableList.evaluate` method. - *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, dp=0.0, **__): ... return ring.get_mcf(dp=dp) >>> obs = RingObservable(momentum_compaction, needs=Need.NEED_4D) Defines an Observable for the momentum compaction factornad make sure that the lattice provided to :py:meth:`~.Lattice.get_mcf` is 4D. """ nds = {Need.RING} if needs is not None: nds.add(needs) name = self._set_name(name, fun, None) super().__init__(fun, name=name, needs=nds, **eval_kw)
[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, summary: bool = False, label: str | None = None, **eval_kw, ): 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 = name or fun.__name__ postfun = _get_fun(postfun, _arrayproc) if postfun: fun = _Convolve(postfun, fun) statfun = _get_fun(statfun, _statproc) if statfun: fun = _Convolve(statfun, fun, axis=0) summary = True name = _mod_name(name, postfun, statfun) label = _mod_name(label, postfun, statfun) super().__init__(fun, name=name, label=label, **eval_kw) 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: msg = f"Observable {self.name!r}: No location selected in the lattice." raise AtError(msg) 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, strict=False ) 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`. """ # Class attributes _pinfo: ClassVar[dict] = {"x": "x [m]", "y": "y [m]", "angle": "angle"} def __init__( self, refpts: Refpts, param: str, name: str | None = None, label: str | None = None, **eval_kw, ): # 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*. .. rubric:: Evaluation keywords These values must be provided to the :py:meth:`~.ObservableList.evaluate` method. Default values may be given at instantiation. * **ring** – Lattice description, Example: >>> obs = GeometryObservable(at.Monitor, param="x") Observe x coordinate of monitors """ if param not in self._pinfo: msg = f"Expected {param!r} to be one of {self._pinfo.keys()!r}" raise ValueError(msg) name = self._set_name(name, "geometry", param) fun = partial(_record_access, param, None) needs = {Need.GEOMETRY} super().__init__( fun, refpts, needs=needs, name=name, label=param, axis_label=self._pinfo[param], **eval_kw, ) if label: self.label = label
[docs] class OrbitObservable(ElementObservable): """Observe the closed orbit coordinates at selected locations. Process the *orbit* output of :py:func:`.find_orbit`. """ # Class attributes _plist: ClassVar = ["x [m]", "$p_x$", "y [m]", "$p_y$", "angle"] def __init__( self, refpts: Refpts, axis: AxisDef = None, name: str | None = None, label: str | None = None, **eval_kw, ): # 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:: Evaluation keywords These values must be provided to the :py:meth:`~.ObservableList.evaluate` method. Default values may be given at instantiation. * **ring** – Lattice description, * **dp** – Momentum deviation. Defaults to :py:obj:`None`, * **dct** – Path lengthening. Defaults to :py:obj:`None`, * **df** – Deviation from the nominal RF frequency. Defaults to :py:obj:`None`, .. 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 """ descr = axis_(axis) name = self._set_name(name, "orbit", descr["code"]) fun = _ArrayAccess(descr["index"]) needs = {Need.ORBIT} super().__init__( fun, refpts, needs=needs, name=name, label=descr["label"], axis_label="".join((descr["label"], descr["unit"])), **eval_kw, ) if label: self.label = label
[docs] class MatrixObservable(ElementObservable): """Observe coefficients of the transfer matrix. Process 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, label: str | None = None, **eval_kw, ): # 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*. .. rubric:: Evaluation keywords These values must be provided to the :py:meth:`~.ObservableList.evaluate` method. Default values may be given at instantiation. * **ring** – Lattice description, * **dp** – Momentum deviation. Defaults to :py:obj:`None`, * **dct** – Path lengthening. Defaults to :py:obj:`None`, * **df** – Deviation from the nominal RF frequency. Defaults to :py:obj:`None`, * **orbit** – Initial orbit. Avoids looking for the closed orbit if it is already known, 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, label=f"$T_{{{_subscript(axis)}}}$", axis_label="T [m]", **eval_kw, ) if label: self.label = label
class _GlobalOpticsObservable(Observable): # Class attributes _pinfo: ClassVar[dict] = { "tune": (r"$\nu_{{{plane}}}$", "Tune", partial(plane_, key="label")), "chromaticity": ( r"$\xi_{{{plane}}}$", "Chromaticity", partial(plane_, key="label"), ), "damping_time": ( r"$\tau_{{{plane}}}$", "Damping time [s]", partial(plane_, key="label"), ), } def __init__( self, param: str | Callable, plane: AxisDef = None, name: str | None = None, label: str | None = None, **eval_kw, ): # noinspection PyUnresolvedReferences r"""Args: param: Optics parameter name (see :py:func:`.get_optics`) or user-defined evaluation function called as: :pycode:`value = fun(ringdata, **eval_kw)` 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*. .. rubric:: Evaluation keywords These values must be provided to the :py:meth:`~.ObservableList.evaluate` method. Default values may be given at instantiation. * **ring** – Lattice description, * **dp** – Momentum deviation. Defaults to :py:obj:`None`, * **dct** – Path lengthening. Defaults to :py:obj:`None`, * **df** – Deviation from the nominal RF frequency. Defaults to :py:obj:`None`, * **orbit** – Initial orbit. Avoids looking for the closed orbit if it is already known, """ needs = {Need.GLOBALOPTICS} name = self._set_name(name, param, plane_(plane, key="code")) index = plane_(plane, key="index") if callable(param): fun = partial(_fun_access, param, index) needs.add(Need.CHROMATICITY) else: fun = partial(_record_access, param, index) if param == "chromaticity": needs.add(Need.CHROMATICITY) super().__init__( fun, needs=needs, name=name, label=self._pl_lab(param, plane), axis_label=self._ax_lab(param, plane), **eval_kw, ) if label: self.label = label
[docs] class LocalOpticsObservable(ElementObservable): """Observe a local optics parameter at selected locations. Process the local output of :py:func:`.get_optics`. """ # Class attributes _pinfo: ClassVar[dict] = { "alpha": (r"$\alpha_{{{plane}}}$", "alpha", partial(plane_, key="label")), "beta": (r"$\beta_{{{plane}}}$", "beta [m]", partial(plane_, key="label")), "gamma": (r"$\gamma_{{{plane}}}$", "gamma", partial(plane_, key="label")), "mu": ( r"$\mu_{{{plane}}}$", "phase advance [rad]", partial(plane_, key="label"), ), "muf": ( r"$\mu_{{{plane}}}$", "phase advance [rad]", partial(plane_, key="label"), ), "mu2pi": ( r"$\mu_{{{plane}}}/2\pi$", "phase advance", partial(plane_, key="label"), ), "mu2pif": ( r"$\mu_{plane}/2 \pi$", "phase advance", partial(plane_, key="label"), ), "closed_orbit": ( r"${{{plane}}}_{{co}}$", "closed orbit", partial(axis_, key="code"), ), "dispersion": ( r"$\eta_{{{plane}}}$", r"dispersion [m]", partial(axis_, key="code"), ), "s_pos": ("s", "s [m]", partial(plane_, key="label")), "M": (r"$M_{{{plane}}}$", "M", _subscript), "A": (r"$A_{{{plane}}}$", "A", _subscript), "B": (r"$B_{{{plane}}}$", "B", _subscript), "C": (r"$C_{{{plane}}}$", "C", _subscript), "R": (r"$R_{{{plane}}}$", "R", _subscript), "W": (r"$W_{{{plane}}}$", "W", partial(plane_, key="label")), "Wp": (r"$Wp_{{{plane}}}$", "Wp", partial(plane_, key="label")), "dalpha": ( r"$\partial \alpha_{{{plane}}}/ \partial \delta$", r"$\partial \alpha / \partial \delta$", partial(plane_, key="label"), ), "dbeta": ( r"$\partial \beta_{{{plane}}}/ \partial \delta$", r"$\partial \beta/ \partial \delta$ [m]", partial(plane_, key="label"), ), "dmu": ( r"$\partial \mu_{{{plane}}}/ \partial \delta$", r"$\partial \mu/ \partial \delta$ [rad]", partial(plane_, key="label"), ), "ddispersion": ( r"$\partial \eta_{{{plane}}}/ \partial \delta$", r"$\partial \eta/ \partial \delta$ [m]", partial(axis_, key="code"), ), "dR": ( r"$\partial R_{{{plane}}}/ \partial \delta$", r"$\partial R/ \partial \delta$", _subscript, ), } _default = ("{param}[{plane}]", "{param}", lambda x: x) def __init__( self, refpts: Refpts, param: str | Callable, *, plane: AxisDef = Ellipsis, name: str | None = None, all_points: bool = False, summary: bool = False, label: str | None = None, **eval_kw, ): # 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. summary: Set to :py:obj:`True` if the user-defined evaluation function returns a single item (see below) instead of one item per refpoint. 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:: Evaluation keywords These values must be provided to the :py:meth:`~.ObservableList.evaluate` method. Default values may be given at instantiation. * **ring** – Lattice description, * **dp** – Momentum deviation. Defaults to :py:obj:`None`, * **dct** – Path lengthening. Defaults to :py:obj:`None`, * **df** – Deviation from the nominal RF frequency. Defaults to :py:obj:`None`, * **orbit** – Initial orbit. Avoids looking for the closed orbit if it is already known, * **twiss_in** – Initial conditions for transfer line optics. See :py:func:`.get_optics`, * **method** – Method for linear optics. Default: :py:obj:`~.linear.linopt6`. .. 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, **eval_kw)[plane]` - *elemdata* is the output of :py:func:`.get_optics`, evaluated at the *refpts* of the observable, - *eval_kw* are the evaluation keywords provided to the observable constructor, to the constructor of the enclosing :py:class:`.ObservableList` and to the :py:meth:`~.ObservableList.evaluate` method, - *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: Observe the beta in both planes at all :py:class:`.Monitor` locations: >>> obs = LocalOpticsObservable(at.Monitor, "beta") Observe the maximum vertical beta in Quadrupoles: >>> obs = LocalOpticsObservable( ... at.Quadrupole, "beta", plane="y", statfun=np.max ... ) 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 >>> def phase_advance(elemdata, **__): ... mu = elemdata.mu ... return mu[-1] - mu[0] >>> >>> obs = LocalOpticsObservable( ... [33, 101], ... phase_advance, ... plane="y", ... all_points=True, ... summary=True, ... ) A user-defined function may accept any evaluation keyword. Here is a function which computes the beam envelope for given emittances and energy spread. We define *emit* and *sigma_e* as evaluation keywords: >>> def beam_size(elemdata, emit=None, sigma_e=None, **__): ... return np.sqrt( ... elemdata.beta*emit + (elemdata.dispersion[:, [0, 2]] * sigma_e)**2 ... ) We instantiate a :py:class:`LocalOpticsObservable` using this function and with default values for emittances and energy spread: >>> obs = LocalOpticsObservable( ... 0, beam_size, emit=[130.0e-12, 10.0e-12], sigma_e=0.9e-3 ... ) >>> allobs = ObservableList([obs]) We can evaluate *obs* with the default emittance values: >>> allobs.evaluate(ring=ring) array([2.9990243e-05, 5.14264471e-06]) We can then evaluate *obs* with arbitrary emittances: >>> allobs.evaluate(ring=ring, emit=[140.0e-12, 20.0e-12]) array([3.11193609e-05, 7.2727979e-06]) """ 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): if summary: fun = partial(_fun_access, param, ax_(plane, key="index")) else: fun = partial(_fun_access, param, 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, summary=summary, label=self._pl_lab(param, plane), axis_label=self._ax_lab(param, plane), **eval_kw, ) if label: self.label = label
[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, label: str | None = None, **eval_kw, ): # 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`. .. rubric:: Evaluation keywords These values must be provided to the :py:meth:`~.ObservableList.evaluate` method. Default values may be given at instantiation. * **ring** – Lattice description, 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, **eval_kw) if label: self.label = label
[docs] class TrajectoryObservable(ElementObservable): """Observe trajectory coordinates at selected locations. Process the *r_out* output if :py:meth:`.Lattice.track` """ # Class attributes _pinfo: ClassVar[dict] = { "x": (r"$x_{{{plane}}}$", "position [m]", lambda x: x), "px": (r"$p_{{x{plane}}}$", "transverse momentum", lambda x: x), "y": (r"$y_{{{plane}}}$", "position [m]", lambda x: x), "py": (r"$p_{{y{plane}}}$", "transverse momentum", lambda x: x), "dp": (r"$\delta_{{{plane}}}$", "off-momentum", lambda x: x), "ct": (r"$\beta c \tau_{{{plane}}}$", "path lengthening [m]", lambda x: x), } def __init__( self, refpts: Refpts, axis: AxisDef = Ellipsis, npart: int = 0, name: str | None = None, label: str | None = None, **eval_kw, ): 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 npart: Particle number, 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*. .. rubric:: Evaluation keywords These values must be provided to the :py:meth:`~.ObservableList.evaluate` method. Default values may be given at instantiation. * **ring** – Lattice description, * **r_in** – Initial coordinates of one or several tracked particles. """ descr = axis_(axis) name = self._set_name(name, "trajectory", descr["code"]) fun = _ArrayAccess((npart, descr["index"])) needs = {Need.TRAJECTORY} super().__init__( fun, refpts, needs=needs, name=name, label=self._pl_lab(descr["code"], npart), axis_label=self._ax_lab(descr["code"], npart), **eval_kw, ) if label: self.label = label
[docs] class EmittanceObservable(Observable): """Observe emittance-related parameters. Output of :py:func:`.radiation_parameters` or :py:func:`.envelope_parameters`. For 4D lattices, the data is extracted form the output of :py:func:`.radiation_parameters`: emittances are computed with the radiation integrals. For 6D lattices, the data is extracted form the output of :py:func:`.envelope_parameters`: emittances are computed by :py:func:`.ohmi_envelope` """ # Class attributes _pinfo: ClassVar[dict] = { "emittances": ( r"$\epsilon_{{{plane}}}$", "Emittance [m]", partial(plane_, key="label"), ), "J": ( r"$\mathrm{{J}}_{{{plane}}}$", "Damping partition number", partial(plane_, key="label"), ), "Tau": ( r"$\tau_{{{plane}}}$", "Damping time [s]", partial(plane_, key="label"), ), "sigma_e": (r"$\sigma_e$", "Energy spread", partial(plane_, key="label")), "sigma_l": (r"$\sigma_l$", "Bunch length [m]", partial(plane_, key="label")), "phi_s": (r"$\phi_s$", "Synchronous phase [rad]", partial(plane_, key="label")), "f_s": ( r"$f_s$", "Synchrotron frequency [Hz]", partial(plane_, key="label"), ), "tunes6": (r"$\nu_{{{plane}}}$", "Tune", partial(plane_, key="label")), "U0": (r"$U_0$", "Energy loss / turn [eV]", partial(plane_, key="label")), "voltage": ("V", "V [V]", partial(plane_, key="label")), } def __init__( self, param: str | Callable, plane: AxisDef = None, name: str | None = None, label: str | None = None, **eval_kw, ): 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 vertical 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*] .. rubric:: Evaluation keywords These values must be provided to the :py:meth:`~.ObservableList.evaluate` method. Default values may be given at instantiation. * **ring** – Lattice description, .. _emittance_eval: .. rubric:: User-defined evaluation function It is called as: :pycode:`value = fun(paramdata, **eval_kw)` - *paramdata* if the :py:class:`.RingParameters` object returned by :py:func:`.envelope_parameters`. - *eval_kw* are the evaluation keywords provided to the observable constructor, to the constructor of the enclosing :py:class:`.ObservableList` and to the :py:meth:`~.ObservableList.evaluate` method, - *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, label=self._pl_lab(param, plane), axis_label=self._ax_lab(param, plane), **eval_kw, ) if label: self.label = label
# noinspection PyPep8Naming
[docs] def GlobalOpticsObservable( param: str, *, plane: AxisDef = Ellipsis, name: str | None = None, use_integer: bool = False, **eval_kw, ): # 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*. .. rubric:: Evaluation keywords These values must be provided to the :py:meth:`~.ObservableList.evaluate` method. Default values may be given at instantiation. * **ring** – Lattice description, * **dp** – Momentum deviation. Defaults to :py:obj:`None`, * **dct** – Path lengthening. Defaults to :py:obj:`None`, * **df** – Deviation from the nominal RF frequency. Defaults to :py:obj:`None`, * **orbit** – Initial orbit. Avoids looking for the closed orbit if it is already known, .. _globaloptics_eval: .. rubric:: User-defined evaluation function It is called as: :pycode:`value = fun(ring, ringdata, **eval_kw)` - *ringdata* is the output of :py:func:`.get_optics`, - *eval_kw* are the evaluation keywords provided to the observable constructor, to the constructor of the enclosing :py:class:`.ObservableList` and to the :py:meth:`~.ObservableList.evaluate` method. - *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, name=name, summary=True, all_points=True, **eval_kw, ) else: return _GlobalOpticsObservable(param, plane=plane, name=name, **eval_kw)