"""
Module to define common elements used in AT.
Each element has a default PassMethod attribute for which it should have the
appropriate attributes. If a different PassMethod is set, it is the caller's
responsibility to ensure that the appropriate attributes are present.
"""
from __future__ import annotations
import abc
import re
from abc import ABC
from collections.abc import Generator, Iterable
from copy import copy, deepcopy
from typing import Any, Optional
import numpy as np
# noinspection PyProtectedMember
from .variables import _nop
_zero6 = np.zeros(6)
_eye6 = np.eye(6, order="F")
def _array(value, shape=(-1,), dtype=np.float64):
# Ensure proper ordering(F) and alignment(A) for "C" access in integrators
return np.require(value, dtype=dtype, requirements=["F", "A"]).reshape(
shape, order="F"
)
def _array66(value):
return _array(value, shape=(6, 6))
def _float(value) -> float:
return float(value)
def _int(value, vmin: int | None = None, vmax: int | None = None) -> int:
intv = int(value)
if vmin is not None and intv < vmin:
raise ValueError(f"Value must be greater of equal to {vmin}")
if vmax is not None and intv > vmax:
raise ValueError(f"Value must be smaller of equal to {vmax}")
return intv
[docs]
class LongtMotion(ABC):
"""Abstract Base class for all Element classes whose instances may modify
the particle momentum
Allows identifying elements potentially inducing longitudinal motion.
Subclasses of :py:class:`LongtMotion` must provide two methods for
enabling longitudinal motion:
* ``_get_longt_motion(self)`` must return the activation state,
* ``set_longt_motion(self, enable, new_pass=None, copy=False, **kwargs)``
must enable or disable longitudinal motion.
"""
@abc.abstractmethod
def _get_longt_motion(self):
return False
# noinspection PyShadowingNames
[docs]
@abc.abstractmethod
def set_longt_motion(self, enable, new_pass=None, copy=False, **kwargs):
"""Enable/Disable longitudinal motion
Parameters:
enable: :py:obj:`True`: for enabling, :py:obj:`False` for
disabling
new_pass: New PassMethod:
* :py:obj:`None`: makes no change,
* ``'auto'``: Uses the default conversion,
* Anything else is used as the new PassMethod.
copy: If True, returns a modified copy of the element,
otherwise modifies the element in-place
"""
# noinspection PyUnresolvedReferences
if new_pass is None or new_pass == self.PassMethod:
return self if copy else None
if copy:
newelem = deepcopy(self)
newelem.PassMethod = new_pass
return newelem
# noinspection PyAttributeOutsideInit
self.PassMethod = new_pass
# noinspection PyUnresolvedReferences
class _DictLongtMotion(LongtMotion):
# noinspection PyShadowingNames
"""Mixin class for elements implementing a 'default_pass' class attribute
:py:class:`DictLongtMotion` provides:
* a :py:meth:`set_longt_motion` method setting the PassMethod according
to the ``default_pass`` dictionary.
* a :py:obj:`.longt_motion` property set to :py:obj:`True` when the
PassMethod is ``default_pass[True]``
The class must have a ``default_pass`` class attribute, a dictionary
such that:
* ``default_pass[False]`` is the PassMethod when radiation is turned
OFF,
* ``default_pass[True]`` is the default PassMethod when radiation is
turned ON.
The :py:class:`DictLongtMotion` class must be set as the first base class.
Example:
>>> class QuantumDiffusion(_DictLongtMotion, Element):
... default_pass = {False: "IdentityPass", True: "QuantDiffPass"}
Defines a class such that :py:meth:`set_longt_motion` will select
``'IdentityPass'`` or ``'IdentityPass'``.
"""
def _get_longt_motion(self):
return self.PassMethod != self.default_pass[False]
# noinspection PyShadowingNames
def set_longt_motion(self, enable, new_pass=None, **kwargs):
if new_pass == "auto":
new_pass = self.default_pass[enable]
return super().set_longt_motion(enable, new_pass=new_pass, **kwargs)
# noinspection PyUnresolvedReferences
class _Radiative(LongtMotion):
# noinspection PyShadowingNames
r"""Mixin class for radiating elements
:py:class:`_Radiative` implements the mechanism for converting the pass
methods of radiating elements. It provides:
* a :py:meth:`set_longt_motion` method setting the PassMethod
according to the following rule:
* ``enable == True``: replace "\*Pass" by "\*RadPass"
* ``enable == False``: replace "\*RadPass" by "\*Pass"
* a :py:obj:`.longt_motion` property set to true when the PassMethod
ends with "RadPass"
The :py:class:`_Radiative` class must be set as the first base class.
Example:
>>> class Multipole(_Radiative, LongElement, ThinMultipole):
Defines a class where :py:meth:`set_longt_motion` will convert the
PassMethod according to the \*Pass or \*RadPass suffix.
"""
def _get_longt_motion(self):
return self.PassMethod.endswith(("RadPass", "QuantPass"))
def _autopass(self, enable):
if enable:
root = self.PassMethod.replace("QuantPass", "Pass").replace(
"RadPass", "Pass"
)
return "".join((root[:-4], "RadPass"))
elif self.longt_motion:
root = self.PassMethod.replace("QuantPass", "Pass").replace(
"RadPass", "Pass"
)
return root
else:
return None
# noinspection PyTypeChecker,PyShadowingNames
def set_longt_motion(self, enable, new_pass=None, copy=False, **kwargs):
if new_pass == "auto":
new_pass = self._autopass(enable)
if new_pass is None or new_pass == self.PassMethod:
return self if copy else None
if enable:
def setpass(el):
el.PassMethod = new_pass
el.Energy = kwargs["energy"]
else:
def setpass(el):
el.PassMethod = new_pass
try:
del el.Energy
except AttributeError:
pass
if copy:
newelem = deepcopy(self)
setpass(newelem)
return newelem
setpass(self)
[docs]
class Radiative(_Radiative):
# noinspection PyShadowingNames
r"""Mixin class for default radiating elements (:py:class:`.Dipole`,
:py:class:`.Quadrupole`, :py:class:`.Wiggler`)
:py:class:`Radiative` is a base class for the subset of radiative elements
considered as the ones to be turned on by default: :py:class:`.Dipole`,
:py:class:`.Quadrupole` and :py:class:`.Wiggler`, excluding the higher
order multipoles.
:py:class:`Radiative` inherits from :py:class:`_Radiative` and does not
add any new functionality. Its purpose is to identify the default set of
radiating elements.
Example:
>>> class Dipole(Radiative, Multipole):
Defines a class belonging to the default radiating elements. It
converts the PassMethod according to the "\*Pass" or "\*RadPass"
suffix.
"""
pass
[docs]
class Collective(_DictLongtMotion):
"""Mixin class for elements representing collective effects
Derived classes will automatically set the
:py:attr:`~Element.is_collective` property when the element is active.
The class must have a ``default_pass`` class attribute, a dictionary such
that:
* ``default_pass[False]`` is the PassMethod when collective effects
are turned OFF,
* ``default_pass[True]`` is the default PassMethod when collective effects
are turned ON.
The :py:class:`Collective` class must be set as the first base class.
Example:
>>> class WakeElement(Collective, Element):
... default_pass = {False: "IdentityPass", True: "WakeFieldPass"}
Defines a class where the :py:attr:`~Element.is_collective` property is
handled
"""
def _get_collective(self):
# noinspection PyUnresolvedReferences
return self.PassMethod != self.default_pass[False]
[docs]
@abc.abstractmethod
def clear_history(self):
pass
[docs]
class Element:
"""Base class for AT elements"""
_BUILD_ATTRIBUTES = ["FamName"]
_conversions = {
"FamName": str,
"PassMethod": str,
"Length": _float,
"R1": _array66,
"R2": _array66,
"T1": lambda v: _array(v, (6,)),
"T2": lambda v: _array(v, (6,)),
"RApertures": lambda v: _array(v, (4,)),
"EApertures": lambda v: _array(v, (2,)),
"KickAngle": lambda v: _array(v, (2,)),
"PolynomB": _array,
"PolynomA": _array,
"BendingAngle": _float,
"MaxOrder": _int,
"NumIntSteps": lambda v: _int(v, vmin=0),
"Energy": _float,
}
_entrance_fields = ["T1", "R1"]
_exit_fields = ["T2", "R2"]
_no_swap = _entrance_fields + _exit_fields
def __init__(self, family_name: str, **kwargs):
"""
Parameters:
family_name: Name of the element
All keywords will be set as attributes of the element
"""
self.FamName = family_name
self.Length = kwargs.pop("Length", 0.0)
self.PassMethod = kwargs.pop("PassMethod", "IdentityPass")
self.update(kwargs)
def __setattr__(self, key, value):
try:
value = self._conversions.get(key, _nop)(value)
except Exception as exc:
exc.args = (f"In element {self.FamName}, parameter {key}: {exc}",)
raise
else:
super().__setattr__(key, value)
def __str__(self):
return "\n".join(
[self.__class__.__name__ + ":"]
+ [f"{k:>14}: {v!s}" for k, v in self.items()]
)
def __repr__(self):
clsname, args, kwargs = self.definition
keywords = [f"{arg!r}" for arg in args]
keywords += [f"{k}={v!r}" for k, v in kwargs.items()]
args = re.sub(r"\n\s*", " ", ", ".join(keywords))
return f"{clsname}({args})"
[docs]
def equals(self, other) -> bool:
"""Whether an element is equivalent to another.
This implementation was found to be too slow for the generic
__eq__ method when comparing lattices.
"""
return repr(self) == repr(other)
[docs]
def divide(self, frac) -> list[Element]:
"""split the element in len(frac) pieces whose length
is frac[i]*self.Length
Parameters:
frac: length of each slice expressed as a fraction of the
initial length. ``sum(frac)`` may differ from 1.
Returns:
elem_list: a list of elements equivalent to the original.
Example:
>>> Drift("dr", 0.5).divide([0.2, 0.6, 0.2])
[Drift('dr', 0.1), Drift('dr', 0.3), Drift('dr', 0.1)]
"""
# Bx default, the element is indivisible
return [self]
[docs]
def swap_faces(self, copy=False):
"""Swap the faces of an element, alignment errors are ignored"""
def swapattr(element, attro, attri):
val = getattr(element, attri)
delattr(element, attri)
return attro, val
if copy:
el = self.copy()
else:
el = self
# Remove and swap entrance and exit attributes
fin = dict(
swapattr(el, kout, kin)
for kin, kout in zip(el._entrance_fields, el._exit_fields)
if kin in vars(el) and kin not in el._no_swap
)
fout = dict(
swapattr(el, kin, kout)
for kin, kout in zip(el._entrance_fields, el._exit_fields)
if kout in vars(el) and kout not in el._no_swap
)
# Apply swapped entrance and exit attributes
for key, value in fin.items():
setattr(el, key, value)
for key, value in fout.items():
setattr(el, key, value)
return el if copy else None
[docs]
def update(self, *args, **kwargs):
"""
update(**kwargs)
update(mapping, **kwargs)
update(iterable, **kwargs)
Update the element attributes with the given arguments
"""
attrs = dict(*args, **kwargs)
for key, value in attrs.items():
setattr(self, key, value)
[docs]
def copy(self) -> Element:
"""Return a shallow copy of the element"""
return copy(self)
[docs]
def deepcopy(self) -> Element:
"""Return a deep copy of the element"""
return deepcopy(self)
@property
def definition(self) -> tuple[str, tuple, dict]:
"""tuple (class_name, args, kwargs) defining the element"""
attrs = dict(self.items())
arguments = tuple(
attrs.pop(k, getattr(self, k)) for k in self._BUILD_ATTRIBUTES
)
defelem = self.__class__(*arguments)
keywords = {
k: v
for k, v in attrs.items()
if not np.array_equal(v, getattr(defelem, k, None))
}
return self.__class__.__name__, arguments, keywords
[docs]
def items(self) -> Generator[tuple[str, Any], None, None]:
"""Iterates through the data members"""
v = vars(self).copy()
for k in ["FamName", "Length", "PassMethod"]:
yield k, v.pop(k)
for k, val in sorted(v.items()):
yield k, val
[docs]
def is_compatible(self, other: Element) -> bool:
"""Checks if another :py:class:`Element` can be merged"""
return False
[docs]
def merge(self, other) -> None:
"""Merge another element"""
if not self.is_compatible(other):
badname = getattr(other, "FamName", type(other))
raise TypeError(f"Cannot merge {self.FamName} and {badname}")
# noinspection PyMethodMayBeStatic
def _get_longt_motion(self):
return False
# noinspection PyMethodMayBeStatic
def _get_collective(self):
return False
@property
def longt_motion(self) -> bool:
""":py:obj:`True` if longitudinal motion is affected by the element"""
return self._get_longt_motion()
@property
def is_collective(self) -> bool:
""":py:obj:`True` if the element involves collective effects"""
return self._get_collective()
[docs]
class LongElement(Element):
"""Base class for long elements"""
_BUILD_ATTRIBUTES = Element._BUILD_ATTRIBUTES + ["Length"]
def __init__(self, family_name: str, length: float, *args, **kwargs):
"""
Args:
family_name: Name of the element
length: Element length [m]
Other arguments and keywords are given to the base class
"""
kwargs.setdefault("Length", length)
# Ancestor may be either Element or ThinMultipole
# noinspection PyArgumentList
super().__init__(family_name, *args, **kwargs)
def _part(self, fr, sumfr):
pp = self.copy()
pp.Length = fr * self.Length
if hasattr(self, "KickAngle"):
pp.KickAngle = fr / sumfr * self.KickAngle
return pp
[docs]
def divide(self, frac) -> list[Element]:
def popattr(element, attr):
val = getattr(element, attr)
delattr(element, attr)
return attr, val
frac = np.asarray(frac, dtype=float)
el = self.copy()
# Remove entrance and exit attributes
fin = dict(
popattr(el, key) for key in vars(self) if key in self._entrance_fields
)
fout = dict(popattr(el, key) for key in vars(self) if key in self._exit_fields)
# Split element
element_list = [el._part(f, np.sum(frac)) for f in frac]
# Restore entrance and exit attributes
for key, value in fin.items():
setattr(element_list[0], key, value)
for key, value in fout.items():
setattr(element_list[-1], key, value)
return element_list
[docs]
def is_compatible(self, other) -> bool:
def compatible_field(fieldname):
f1 = getattr(self, fieldname, None)
f2 = getattr(other, fieldname, None)
if f1 is None and f2 is None: # no such field
return True
elif f1 is None or f2 is None: # only one
return False
else: # both
return np.all(f1 == f2)
if not (type(other) is type(self) and self.PassMethod == other.PassMethod):
return False
for fname in ("RApertures", "EApertures"):
if not compatible_field(fname):
return False
return True
[docs]
def merge(self, other) -> None:
super().merge(other)
self.Length += other.Length
[docs]
class Marker(Element):
"""Marker element"""
[docs]
class Monitor(Element):
"""Monitor element"""
[docs]
class BeamMoments(Element):
"""Element to compute bunches mean and std"""
def __init__(self, family_name: str, **kwargs):
"""
Args:
family_name: Name of the element
Default PassMethod: ``BeamMomentsPass``
"""
kwargs.setdefault("PassMethod", "BeamMomentsPass")
self._stds = np.zeros((6, 1, 1), order="F")
self._means = np.zeros((6, 1, 1), order="F")
super().__init__(family_name, **kwargs)
[docs]
def set_buffers(self, nturns, nbunch):
self._stds = np.zeros((6, nbunch, nturns), order="F")
self._means = np.zeros((6, nbunch, nturns), order="F")
@property
def stds(self):
"""Beam 6d standard deviation"""
return self._stds
@property
def means(self):
"""Beam 6d center of mass"""
return self._means
[docs]
class SliceMoments(Element):
"""Element computing the mean and std of slices"""
_BUILD_ATTRIBUTES = Element._BUILD_ATTRIBUTES + ["nslice"]
_conversions = dict(Element._conversions, nslice=int)
def __init__(self, family_name: str, nslice: int, **kwargs):
"""
Args:
family_name: Name of the element
nslice: Number of slices
Keyword arguments:
startturn: Start turn of the acquisition (Default 0)
endturn: End turn of the acquisition (Default 1)
Default PassMethod: ``SliceMomentsPass``
"""
kwargs.setdefault("PassMethod", "SliceMomentsPass")
self._startturn = kwargs.pop("startturn", 0)
self._endturn = kwargs.pop("endturn", 1)
super().__init__(family_name, nslice=nslice, **kwargs)
self._nbunch = 1
self.startturn = self._startturn
self.endturn = self._endturn
self._dturns = self.endturn - self.startturn
self._stds = np.zeros((3, nslice, self._dturns), order="F")
self._means = np.zeros((3, nslice, self._dturns), order="F")
self._spos = np.zeros((nslice, self._dturns), order="F")
self._weights = np.zeros((nslice, self._dturns), order="F")
self.set_buffers(self._endturn, 1)
[docs]
def set_buffers(self, nturns, nbunch):
self.endturn = min(self.endturn, nturns)
self._dturns = self.endturn - self.startturn
self._nbunch = nbunch
self._stds = np.zeros((3, nbunch * self.nslice, self._dturns), order="F")
self._means = np.zeros((3, nbunch * self.nslice, self._dturns), order="F")
self._spos = np.zeros((nbunch * self.nslice, self._dturns), order="F")
self._weights = np.zeros((nbunch * self.nslice, self._dturns), order="F")
@property
def stds(self):
"""Slices x,y,dp standard deviation"""
return self._stds.reshape((3, self._nbunch, self.nslice, self._dturns))
@property
def means(self):
"""Slices x,y,dp center of mass"""
return self._means.reshape((3, self._nbunch, self.nslice, self._dturns))
@property
def spos(self):
"""Slices s position"""
return self._spos.reshape((self._nbunch, self.nslice, self._dturns))
@property
def weights(self):
"""Slices weights in mA if beam current >0,
otherwise fraction of total number of
particles in the bunch
"""
return self._weights.reshape((self._nbunch, self.nslice, self._dturns))
@property
def startturn(self):
"""Start turn of the acquisition"""
return self._startturn
@startturn.setter
def startturn(self, value):
if value < 0:
raise ValueError("startturn must be greater or equal to 0")
if value >= self._endturn:
raise ValueError("startturn must be smaller than endturn")
self._startturn = value
@property
def endturn(self):
"""End turn of the acquisition"""
return self._endturn
@endturn.setter
def endturn(self, value):
if value <= 0:
raise ValueError("endturn must be greater than 0")
if value <= self._startturn:
raise ValueError("endturn must be greater than startturn")
self._endturn = value
[docs]
class Aperture(Element):
"""Transverse aperture element"""
_BUILD_ATTRIBUTES = Element._BUILD_ATTRIBUTES + ["Limits"]
_conversions = dict(Element._conversions, Limits=lambda v: _array(v, (4,)))
def __init__(self, family_name, limits, **kwargs):
"""
Args:
family_name: Name of the element
limits: (4,) array of physical aperture:
[xmin, xmax, ymin, ymax]
Default PassMethod: ``AperturePass``
"""
kwargs.setdefault("PassMethod", "AperturePass")
super().__init__(family_name, Limits=limits, **kwargs)
[docs]
class LongtAperture(Element):
"""Longitudinal aperture element"""
_BUILD_ATTRIBUTES = Element._BUILD_ATTRIBUTES + ["Limits"]
_conversions = dict(Element._conversions, Limits=lambda v: _array(v, (4,)))
def __init__(self, family_name, limits, **kwargs):
"""
Args:
family_name: Name of the element
limits: (4,) array of physical aperture:
[dpmin, dpmax, ctmin, ctmax]
Default PassMethod: ``LongtAperturePass``
"""
kwargs.setdefault("PassMethod", "LongtAperturePass")
super().__init__(family_name, Limits=limits, **kwargs)
[docs]
class Drift(LongElement):
"""Drift space element"""
def __init__(self, family_name: str, length: float, **kwargs):
"""
Args:
family_name: Name of the element
length: Element length [m]
Default PassMethod: ``DriftPass``
"""
kwargs.setdefault("PassMethod", "DriftPass")
super().__init__(family_name, length, **kwargs)
[docs]
def insert(
self, insert_list: Iterable[tuple[float, Element | None]]
) -> list[Element]:
"""insert elements inside a drift
Arguments:
insert_list: iterable, each item of insert_list is itself an
iterable with 2 objects:
1. the location where the center of the element
will be inserted, given as a fraction of the Drift length.
2. an element to be inserted at that location. If :py:obj:`None`,
the drift will be divided but no element will be inserted.
Returns:
elem_list: a list of elements.
Drifts with negative lengths may be generated if necessary.
Examples:
>>> Drift("dr", 2.0).insert(((0.25, None), (0.75, None)))
[Drift('dr', 0.5), Drift('dr', 1.0), Drift('dr', 0.5)]
>>> Drift("dr", 2.0).insert(((0.0, Marker("m1")), (0.5, Marker("m2"))))
[Marker('m1'), Drift('dr', 1.0), Marker('m2'), Drift('dr', 1.0)]
>>> Drift("dr", 2.0).insert(((0.5, Quadrupole("qp", 0.4, 0.0)),))
[Drift('dr', 0.8), Quadrupole('qp', 0.4), Drift('dr', 0.8)]
"""
frac, elements = zip(*insert_list)
lg = [0.0 if el is None else el.Length for el in elements]
fr = np.asarray(frac, dtype=float)
lg = 0.5 * np.asarray(lg, dtype=float) / self.Length
drfrac = np.hstack((fr - lg, 1.0)) - np.hstack((0.0, fr + lg))
long_elems = drfrac != 0.0
drifts = np.ndarray((len(drfrac),), dtype="O")
drifts[long_elems] = self.divide(drfrac[long_elems])
nline = len(drifts) + len(elements)
line = [None] * nline # type: list[Optional[Element]]
line[::2] = drifts
line[1::2] = elements
return [el for el in line if el is not None]
[docs]
class Collimator(Drift):
"""Collimator element"""
_BUILD_ATTRIBUTES = LongElement._BUILD_ATTRIBUTES + ["RApertures"]
def __init__(self, family_name: str, length: float, limits, **kwargs):
"""
Args:
family_name: Name of the element
length: Element length [m]
limits: (4,) array of physical aperture:
[xmin, xmax, zmin, zmax] [m]
Default PassMethod: ``DriftPass``
"""
super().__init__(family_name, length, RApertures=limits, **kwargs)
[docs]
class ThinMultipole(Element):
"""Thin multipole element"""
_BUILD_ATTRIBUTES = Element._BUILD_ATTRIBUTES + ["PolynomA", "PolynomB"]
def __init__(self, family_name: str, poly_a, poly_b, **kwargs):
"""
Args:
family_name: Name of the element
poly_a: Array of skew multipole components
poly_b: Array of normal multipole components
Keyword arguments:
MaxOrder: Number of desired multipoles. Default: highest
index of non-zero polynomial coefficients
FieldScaling: Scaling factor applied to the magnetic field
(*PolynomA* and *PolynomB*)
Default PassMethod: ``ThinMPolePass``
"""
def getpol(poly):
nonzero = np.flatnonzero(poly != 0.0)
return poly, len(poly), nonzero[-1] if len(nonzero) > 0 else -1
def lengthen(poly, dl):
if dl > 0:
return np.concatenate((poly, np.zeros(dl)))
else:
return poly
# PolynomA and PolynomB and convert to ParamArray
prmpola = self._conversions["PolynomA"](kwargs.pop("PolynomA", poly_a))
prmpolb = self._conversions["PolynomB"](kwargs.pop("PolynomB", poly_b))
poly_a, len_a, ord_a = getpol(prmpola)
poly_b, len_b, ord_b = getpol(prmpolb)
deforder = max(getattr(self, "DefaultOrder", 0), ord_a, ord_b)
# Remove MaxOrder
maxorder = kwargs.pop("MaxOrder", deforder)
kwargs.setdefault("PassMethod", "ThinMPolePass")
super().__init__(family_name, **kwargs)
# Set MaxOrder while PolynomA and PolynomB are not set yet
super().__setattr__("MaxOrder", maxorder)
# Adjust polynom lengths and set them
len_ab = max(self.MaxOrder + 1, len_a, len_b)
self.PolynomA = lengthen(prmpola, len_ab - len_a)
self.PolynomB = lengthen(prmpolb, len_ab - len_b)
def __setattr__(self, key, value):
"""Check the compatibility of MaxOrder, PolynomA and PolynomB"""
polys = ("PolynomA", "PolynomB")
if key in polys:
lmin = self.MaxOrder
if not len(value) > lmin:
raise ValueError(f"Length of {key} must be larger than {lmin}")
elif key == "MaxOrder":
intval = int(value)
lmax = min(len(getattr(self, k)) for k in polys)
if not intval < lmax:
raise ValueError(f"MaxOrder must be smaller than {lmax}")
super().__setattr__(key, value)
[docs]
class Multipole(_Radiative, LongElement, ThinMultipole):
"""Multipole element"""
_BUILD_ATTRIBUTES = LongElement._BUILD_ATTRIBUTES + ["PolynomA", "PolynomB"]
_conversions = dict(ThinMultipole._conversions, K=float, H=float)
def __init__(self, family_name: str, length: float, poly_a, poly_b, **kwargs):
"""
Args:
family_name: Name of the element
length: Element length [m]
poly_a: Array of skew multipole components
poly_b: Array of normal multipole components
Keyword arguments:
MaxOrder: Number of desired multipoles. Default: highest
index of non-zero polynomial coefficients
NumIntSteps: Number of integration steps (default: 10)
KickAngle: Correction deviation angles (H, V)
FieldScaling: Scaling factor applied to the magnetic field
(*PolynomA* and *PolynomB*)
Default PassMethod: ``StrMPoleSymplectic4Pass``
"""
kwargs.setdefault("PassMethod", "StrMPoleSymplectic4Pass")
kwargs.setdefault("NumIntSteps", 10)
super().__init__(family_name, length, poly_a, poly_b, **kwargs)
[docs]
def is_compatible(self, other) -> bool:
if super().is_compatible(other) and self.MaxOrder == other.MaxOrder:
for i in range(self.MaxOrder + 1):
if self.PolynomB[i] != other.PolynomB[i]:
return False
if self.PolynomA[i] != other.PolynomA[i]:
return False
return True
else:
return False
# noinspection PyPep8Naming
@property
def K(self) -> float:
"""Focusing strength [mˆ-2]"""
arr = self.PolynomB
return 0.0 if len(arr) < 2 else float(arr[1])
# noinspection PyPep8Naming
@K.setter
def K(self, strength: float):
self.PolynomB[1] = strength
# noinspection PyPep8Naming
@property
def H(self) -> float:
"""Sextupolar strength [mˆ-3]"""
arr = self.PolynomB
return 0.0 if len(arr) < 3 else float(arr[2])
# noinspection PyPep8Naming
@H.setter
def H(self, strength):
self.PolynomB[2] = strength
[docs]
class Dipole(Radiative, Multipole):
"""Dipole element"""
_BUILD_ATTRIBUTES = LongElement._BUILD_ATTRIBUTES + ["BendingAngle", "K"]
_conversions = dict(
Multipole._conversions,
EntranceAngle=float,
ExitAngle=float,
FringeInt1=float,
FringeInt2=float,
FringeQuadEntrance=int,
FringeQuadExit=int,
FringeBendEntrance=int,
FringeBendExit=int,
)
_entrance_fields = Multipole._entrance_fields + [
"EntranceAngle",
"FringeInt1",
"FringeBendEntrance",
"FringeQuadEntrance",
]
_exit_fields = Multipole._exit_fields + [
"ExitAngle",
"FringeInt2",
"FringeBendExit",
"FringeQuadExit",
]
DefaultOrder = 0
def __init__(
self,
family_name: str,
length: float,
bending_angle: float | None = 0.0,
k: float = 0.0,
**kwargs,
):
"""
Args:
family_name: Name of the element
length: Element length [m]
bending_angle: Bending angle [rd]
k: Focusing strength [m^-2]
Keyword arguments:
EntranceAngle=0.0: entrance angle
ExitAngle=0.0: exit angle
PolynomB: straight multipoles
PolynomA: skew multipoles
MaxOrder=0: Number of desired multipoles
NumIntSt=10: Number of integration steps
FullGap: Magnet full gap
FringeInt1: Extension of the entrance fringe field
FringeInt2: Extension of the exit fringe field
FringeBendEntrance: 1: legacy version Brown First Order (default)
2: SOLEIL close to second order of Brown
3: THOMX
FringeBendExit: See *FringeBendEntrance*
FringeQuadEntrance: 0: no fringe field effect (default)
1: Lee-Whiting's thin lens limit formula
2: elegant-like
FringeQuadExit: See *FringeQuadEntrance*
fringeIntM0: Integrals for FringeQuad method 2
fringeIntP0:
KickAngle: Correction deviation angles (H, V)
FieldScaling: Scaling factor applied to the magnetic field
Available PassMethods: :ref:`BndMPoleSymplectic4Pass`,
:ref:`BendLinearPass`, :ref:`ExactSectorBendPass`,
:ref:`ExactRectangularBendPass`, :ref:`ExactRectBendPass`,
BndStrMPoleSymplectic4Pass
Default PassMethod: :ref:`BndMPoleSymplectic4Pass`
"""
kwargs.setdefault("BendingAngle", bending_angle)
kwargs.setdefault("EntranceAngle", 0.0)
kwargs.setdefault("ExitAngle", 0.0)
kwargs.setdefault("PassMethod", "BndMPoleSymplectic4Pass")
super().__init__(family_name, length, [], [0.0, k], **kwargs)
[docs]
def items(self) -> Generator[tuple[str, Any], None, None]:
yield from super().items()
yield "K", self.K
def _part(self, fr, sumfr):
pp = super()._part(fr, sumfr)
pp.BendingAngle = fr / sumfr * self.BendingAngle
pp.EntranceAngle = 0.0
pp.ExitAngle = 0.0
return pp
[docs]
def is_compatible(self, other) -> bool:
def invrho(dip: Dipole):
return dip.BendingAngle / dip.Length
return (
super().is_compatible(other)
and self.ExitAngle == -other.EntranceAngle
and abs(invrho(self) - invrho(other)) <= 1.0e-6
)
[docs]
def merge(self, other) -> None:
super().merge(other)
# noinspection PyAttributeOutsideInit
self.ExitAngle = other.ExitAngle
self.BendingAngle += other.BendingAngle
# Bend is a synonym of Dipole.
Bend = Dipole
[docs]
class Quadrupole(Radiative, Multipole):
"""Quadrupole element"""
_BUILD_ATTRIBUTES = LongElement._BUILD_ATTRIBUTES + ["K"]
_conversions = dict(
Multipole._conversions, FringeQuadEntrance=int, FringeQuadExit=int
)
_entrance_fields = Multipole._entrance_fields + ["FringeQuadEntrance"]
_exit_fields = Multipole._exit_fields + ["FringeQuadExit"]
DefaultOrder = 1
def __init__(
self, family_name: str, length: float, k: float | None = 0.0, **kwargs
):
"""Quadrupole(FamName, Length, Strength=0, **keywords)
Args:
family_name: Name of the element
length: Element length [m]
k: Focusing strength [mˆ-2]
Keyword Arguments:
PolynomB: straight multipoles
PolynomA: skew multipoles
MaxOrder=1: Number of desired multipoles
NumIntSteps=10: Number of integration steps
FringeQuadEntrance: 0: no fringe field effect (default)
1: Lee-Whiting's thin lens limit formula
2: elegant-like
FringeQuadExit: See ``FringeQuadEntrance``
fringeIntM0: Integrals for FringeQuad method 2
fringeIntP0:
KickAngle: Correction deviation angles (H, V)
FieldScaling: Scaling factor applied to the magnetic field
(*PolynomA* and *PolynomB*)
Default PassMethod: ``StrMPoleSymplectic4Pass``
"""
kwargs.setdefault("PassMethod", "StrMPoleSymplectic4Pass")
super().__init__(family_name, length, [], [0.0, k], **kwargs)
[docs]
def items(self) -> Generator[tuple[str, Any], None, None]:
yield from super().items()
yield "K", self.K
[docs]
class Sextupole(Multipole):
"""Sextupole element"""
_BUILD_ATTRIBUTES = LongElement._BUILD_ATTRIBUTES + ["H"]
DefaultOrder = 2
def __init__(
self, family_name: str, length: float, h: float | None = 0.0, **kwargs
):
"""
Args:
family_name: Name of the element
length: Element length [m]
h: strength [mˆ-3]
Keyword Arguments:
PolynomB: straight multipoles
PolynomA: skew multipoles
MaxOrder: Number of desired multipoles
NumIntSteps=10: Number of integration steps
KickAngle: Correction deviation angles (H, V)
FieldScaling: Scaling factor applied to the magnetic field
(*PolynomA* and *PolynomB*)
Default PassMethod: ``StrMPoleSymplectic4Pass``
"""
kwargs.setdefault("PassMethod", "StrMPoleSymplectic4Pass")
super().__init__(family_name, length, [], [0.0, 0.0, h], **kwargs)
[docs]
def items(self) -> Generator[tuple[str, Any], None, None]:
yield from super().items()
yield "H", self.H
[docs]
class Octupole(Multipole):
"""Octupole element, with no changes from multipole at present"""
_BUILD_ATTRIBUTES = Multipole._BUILD_ATTRIBUTES
DefaultOrder = 3
[docs]
class RFCavity(LongtMotion, LongElement):
"""RF cavity element"""
_BUILD_ATTRIBUTES = LongElement._BUILD_ATTRIBUTES + [
"Voltage",
"Frequency",
"HarmNumber",
"Energy",
]
default_pass = {False: "DriftPass", True: "RFCavityPass"}
_conversions = dict(
LongElement._conversions,
Voltage=float,
Frequency=float,
HarmNumber=int,
TimeLag=float,
)
def __init__(
self,
family_name: str,
length: float,
voltage: float,
frequency: float,
harmonic_number: int,
energy: float,
**kwargs,
):
"""
Args:
family_name: Name of the element
length: Element length [m]
voltage: RF voltage [V]
frequency: RF frequency [Hz]
harmonic_number:
energy: ring energy [eV]
Keyword Arguments:
TimeLag=0: Cavity time lag
Default PassMethod: ``RFCavityPass``
"""
kwargs.setdefault("TimeLag", 0.0)
kwargs.setdefault("PassMethod", self.default_pass[True])
super().__init__(
family_name,
length,
Voltage=voltage,
Frequency=frequency,
HarmNumber=harmonic_number,
Energy=energy,
**kwargs,
)
def _part(self, fr, sumfr):
pp = super()._part(fr, sumfr)
pp.Voltage = fr * self.Voltage
return pp
[docs]
def is_compatible(self, other) -> bool:
return (
super().is_compatible(other)
and self.Frequency == other.Frequency
and self.TimeLag == other.TimeLag
)
[docs]
def merge(self, other) -> None:
super().merge(other)
self.Voltage += other.Voltage
def _get_longt_motion(self):
return self.PassMethod.endswith("CavityPass")
# noinspection PyShadowingNames
[docs]
def set_longt_motion(self, enable, new_pass=None, **kwargs):
if new_pass == "auto":
new_pass = (
self.default_pass[True]
if enable
else ("IdentityPass" if self.Length == 0 else "DriftPass")
)
return super().set_longt_motion(enable, new_pass=new_pass, **kwargs)
[docs]
class M66(Element):
"""Linear (6, 6) transfer matrix"""
_BUILD_ATTRIBUTES = Element._BUILD_ATTRIBUTES + ["M66"]
_conversions = dict(Element._conversions, M66=_array66)
def __init__(self, family_name: str, m66=None, **kwargs):
"""
Args:
family_name: Name of the element
m66: Transfer matrix. Default: Identity matrix
Default PassMethod: ``Matrix66Pass``
"""
if m66 is None:
m66 = np.identity(6)
kwargs.setdefault("PassMethod", "Matrix66Pass")
kwargs.setdefault("M66", m66)
super().__init__(family_name, **kwargs)
[docs]
class SimpleQuantDiff(_DictLongtMotion, Element):
"""
Linear tracking element for a simplified quantum diffusion,
radiation damping and energy loss.
Note: The damping times are needed to compute the correct
kick for the emittance. Radiation damping is NOT applied.
"""
_BUILD_ATTRIBUTES = Element._BUILD_ATTRIBUTES
default_pass = {False: "IdentityPass", True: "SimpleQuantDiffPass"}
def __init__(
self,
family_name: str,
betax: float = 1.0,
betay: float = 1.0,
emitx: float = 0.0,
emity: float = 0.0,
espread: float = 0.0,
taux: float = 0.0,
tauy: float = 0.0,
tauz: float = 0.0,
**kwargs,
):
"""
Args:
family_name: Name of the element
Optional Args:
betax: Horizontal beta function at element [m]
betay: Vertical beta function at element [m]
emitx: Horizontal equilibrium emittance [m.rad]
emity: Vertical equilibrium emittance [m.rad]
espread: Equilibrium energy spread
taux: Horizontal damping time [turns]
tauy: Vertical damping time [turns]
tauz: Longitudinal damping time [turns]
Default PassMethod: ``SimpleQuantDiffPass``
"""
kwargs.setdefault("PassMethod", self.default_pass[True])
assert taux >= 0.0, "taux must be greater than or equal to 0"
self.taux = taux
assert tauy >= 0.0, "tauy must be greater than or equal to 0"
self.tauy = tauy
assert tauz >= 0.0, "tauz must be greater than or equal to 0"
self.tauz = tauz
assert emitx >= 0.0, "emitx must be greater than or equal to 0"
self.emitx = emitx
if emitx > 0.0:
assert taux > 0.0, "if emitx is given, taux must be non zero"
assert emity >= 0.0, "emity must be greater than or equal to 0"
self.emity = emity
if emity > 0.0:
assert tauy > 0.0, "if emity is given, tauy must be non zero"
assert espread >= 0.0, "espread must be greater than or equal to 0"
self.espread = espread
if espread > 0.0:
assert tauz > 0.0, "if espread is given, tauz must be non zero"
self.betax = betax
self.betay = betay
super().__init__(family_name, **kwargs)
[docs]
class SimpleRadiation(_DictLongtMotion, Radiative, Element):
"""Simple radiation damping and energy loss"""
_BUILD_ATTRIBUTES = Element._BUILD_ATTRIBUTES
_conversions = dict(
Element._conversions, U0=float, damp_mat_diag=lambda v: _array(v, shape=(6,))
)
default_pass = {False: "IdentityPass", True: "SimpleRadiationRadPass"}
def __init__(
self,
family_name: str,
taux: float = 0.0,
tauy: float = 0.0,
tauz: float = 0.0,
U0: float = 0.0,
**kwargs,
):
"""
Args:
family_name: Name of the element
Optional Args:
taux: Horizontal damping time [turns]
tauy: Vertical damping time [turns]
tauz: Longitudinal damping time [turns]
U0: Energy loss per turn [eV]
Default PassMethod: ``SimpleRadiationRadPass``
"""
assert taux >= 0.0, "taux must be greater than or equal to 0"
if taux == 0.0:
dampx = 1
else:
dampx = np.exp(-1 / taux)
assert tauy >= 0.0, "tauy must be greater than or equal to 0"
if tauy == 0.0:
dampy = 1
else:
dampy = np.exp(-1 / tauy)
assert tauz >= 0.0, "tauz must be greater than or equal to 0"
if tauz == 0.0:
dampz = 1
else:
dampz = np.exp(-1 / tauz)
kwargs.setdefault("PassMethod", self.default_pass[True])
kwargs.setdefault("U0", U0)
kwargs.setdefault(
"damp_mat_diag", np.array([dampx, dampx, dampy, dampy, dampz, dampz])
)
super().__init__(family_name, **kwargs)
[docs]
class Corrector(LongElement):
"""Corrector element"""
_BUILD_ATTRIBUTES = LongElement._BUILD_ATTRIBUTES + ["KickAngle"]
def __init__(self, family_name: str, length: float, kick_angle, **kwargs):
"""
Args:
family_name: Name of the element
length: Element length [m]
KickAngle: Correction deviation angles (H, V)
Keyword Args:
FieldScaling: Scaling factor applied to the magnetic field
(*KickAngle*)
Default PassMethod: ``CorrectorPass``
"""
kwargs.setdefault("PassMethod", "CorrectorPass")
super().__init__(family_name, length, KickAngle=kick_angle, **kwargs)
[docs]
class Wiggler(Radiative, LongElement):
"""Wiggler element
See atwiggler.m
"""
_BUILD_ATTRIBUTES = LongElement._BUILD_ATTRIBUTES + ["Lw", "Bmax"]
_conversions = dict(
Element._conversions,
Lw=_float,
Bmax=_float,
Energy=_float,
Bx=lambda v: _array(v, (6, -1)),
By=lambda v: _array(v, (6, -1)),
Nstep=int,
Nmeth=int,
NHharm=int,
NVharm=int,
)
# noinspection PyPep8Naming
def __init__(
self,
family_name: str,
length: float,
wiggle_period: float,
b_max: float,
energy: float = 0.0,
*,
Nstep: int | None = 5,
Nmeth: int | None = 4,
By=(1, 1, 0, 1, 1, 0),
Bx=(),
**kwargs,
):
"""
Args:
length: total length of the wiggler
wiggle_period: length must be a multiple of this
b_max: peak wiggler field [Tesla]
energy: beam energy [eV]
Nstep: number of integration steps.
Nmeth: symplectic integration order: 2 or 4
Bx: harmonics for horizontal wiggler: (6, nHharm)
array-like object
By: harmonics for vertical wiggler (6, nHharm)
array-like object
Default PassMethod: ``GWigSymplecticPass``
"""
kwargs.setdefault("PassMethod", "GWigSymplecticPass")
n_wiggles = length / wiggle_period
if abs(round(n_wiggles) - n_wiggles) > 1e-6:
raise ValueError(
"Wiggler: length / wiggle_period is not an "
f"integer. ({length}/{wiggle_period}={n_wiggles})"
)
super().__init__(
family_name,
length,
Lw=wiggle_period,
Bmax=b_max,
Nstep=Nstep,
Nmeth=Nmeth,
By=By,
Bx=Bx,
Energy=energy,
**kwargs,
)
for i, b in enumerate(self.By.T):
dk = abs(b[3] ** 2 - b[4] ** 2 - b[2] ** 2) / abs(b[4])
if dk > 1e-6:
raise ValueError(f"Wiggler(H): kx^2 + kz^2 -ky^2 !=0, i = {i}")
for i, b in enumerate(self.Bx.T):
dk = abs(b[2] ** 2 - b[4] ** 2 - b[3] ** 2) / abs(b[4])
if dk > 1e-6:
raise ValueError(f"Wiggler(V): ky^2 + kz^2 -kx^2 !=0, i = {i}")
self.NHharm = self.By.shape[1]
self.NVharm = self.Bx.shape[1]
[docs]
def divide(self, frac) -> list[Element]:
# A wiggler is indivisible
return [self]
[docs]
class QuantumDiffusion(_DictLongtMotion, Element):
_BUILD_ATTRIBUTES = Element._BUILD_ATTRIBUTES + ["Lmatp"]
default_pass = {False: "IdentityPass", True: "QuantDiffPass"}
_conversions = dict(Element._conversions, Lmatp=_array66)
def __init__(self, family_name: str, lmatp: np.ndarray, **kwargs):
"""Quantum diffusion element
Args:
family_name: Name of the element
lmatp : Diffusion matrix for generation (see
:py:func:`.gen_quantdiff_elem`)
Default PassMethod: ``QuantDiffPass``
"""
kwargs.setdefault("PassMethod", self.default_pass[True])
super().__init__(family_name, Lmatp=lmatp, **kwargs)
[docs]
class EnergyLoss(_DictLongtMotion, Element):
_BUILD_ATTRIBUTES = Element._BUILD_ATTRIBUTES + ["EnergyLoss"]
_conversions = dict(Element._conversions, EnergyLoss=float)
default_pass = {False: "IdentityPass", True: "EnergyLossRadPass"}
def __init__(self, family_name: str, energy_loss: float, **kwargs):
"""Energy loss element
the :py:class:`EnergyLoss` element is taken into account in
:py:func:`.radiation_parameters`: it adds damping by contributing to the
:math:`I_2` integral, thus reducing the equilibrium emittance. But it does not
generate any diffusion. This makes sense only if the losses summarised in
the element occur in non-dispersive locations.
Args:
family_name: Name of the element
energy_loss: Energy loss [eV]
"""
kwargs.setdefault("PassMethod", self.default_pass[False])
super().__init__(family_name, EnergyLoss=energy_loss, **kwargs)
Radiative.register(EnergyLoss)
[docs]
def build_class_map(): # Missing class aliases (Bend)
global CLASS_MAP
def subclasses_recursive(cl):
direct = cl.__subclasses__()
indirect = []
for subclass in direct:
indirect.extend(subclasses_recursive(subclass))
return frozenset([cl] + direct + indirect)
cls_list = subclasses_recursive(Element)
CLASS_MAP = {cls.__name__: cls for cls in cls_list}
[docs]
def get_class_map():
return CLASS_MAP
# build_class_map()
CLASS_MAP = {
k: v for k, v in locals().items() if isinstance(v, type) and issubclass(v, Element)
}