"""Magnet tools."""
from __future__ import annotations
from typing import Sequence
import numpy
from scipy.special import comb
from ..lattice.elements import Element
__all__ = [
"feeddown_polynomba",
"feeddown_from_nth_order",
"feeddown_pol_from_element",
]
[docs]
def feeddown_pol_from_element(
ele: Element, verbose: bool = False, **kwargs: dict[str, any]
) -> dict[str, numpy.ndarray]:
"""
Return the feed down polynoms due to a transverse offset on an element.
Parameters:
ele: a ring element.
verbose: prints additional info. Default: False.
kwargs:
offset:(x,y) horizontal and vertical offset in meters.
Default taken from T2 or T1. Otherwise, zero.
Returns:
Dictionary with PolynomB and PolynomA feeddown components.
Note: Thin lens approximation, T2=-T1.
Bending angles are ignored.
"""
# check verbose flags only once
verboseprint = print if verbose else lambda *a, **k: None
# handle cases where polynom a and b are not defined
polb = numpy.array([])
pola = numpy.array([])
if hasattr(ele, "PolynomB"):
polb = ele.PolynomB
else:
verboseprint(f"Element {ele.FamName} has no PolynomB")
if hasattr(ele, "PolynomA"):
pola = ele.PolynomA
else:
verboseprint(f"Element {ele.FamName} has no PolynomA")
# check if user has set offsets
xoffset, yoffset = kwargs.get("offset", (0, 0))
# check if element has T1 and T2. Use one.
if "offset" not in kwargs:
if hasattr(ele, "T2"):
xoffset = ele.T2[0]
yoffset = ele.T2[2]
elif hasattr(ele, "T1"):
xoffset = -ele.T1[0]
yoffset = -ele.T1[2]
else:
verboseprint(f"Element {ele.FamName} has no T1 or T2.")
verboseprint(f"Using offsets xoffset={xoffset}, yoffset={yoffset}.")
# Return the polynoms
return feeddown_polynomba(polb=polb, pola=pola, xoffset=xoffset, yoffset=yoffset)
[docs]
def feeddown_polynomba(
polb: Sequence[float],
pola: Sequence[float],
xoffset: float = 0,
yoffset: float = 0,
verbose: bool = False,
) -> dict[str, numpy.ndarray]:
"""
Return the feeddown due to a transverse offset.
Parameters:
polb: PolynomB.
pola: PolynomA.
xoffset: Default zero. Horizontal offset in meters.
yoffset: Default zero. Vertical offset in meters.
verbose: prints additional info.
Returns:
Dictionary with PolynomB and PolynomA feeddown components.
Note:
If only one polynom is passed, it is assumed to be PolynomB.
"""
# check verbose flags only once
verboseprint = print if verbose else lambda *a, **k: None
# verify polynoms length
maxorda = len(pola)
maxordb = len(polb)
maxord = max(maxorda, maxordb)
polasum = numpy.zeros(max(0, maxord - 1))
polbsum = numpy.zeros(max(0, maxord - 1))
if maxorda == 0 and maxordb == 0:
verboseprint("Both polynoms are zero.")
else:
polbpad = numpy.pad(
polb, (0, maxord - maxordb), "constant", constant_values=(0, 0)
)
polapad = numpy.pad(
pola, (0, maxord - maxorda), "constant", constant_values=(0, 0)
)
verboseprint(f"polb={polb},pola={pola}")
verboseprint(f"xoffset={xoffset},yoffset={yoffset}")
for ith in range(2, maxord + 1):
polbaux_b, polaaux_b = feeddown_from_nth_order(
ith, polbpad[ith - 1], xoffset, yoffset, poltype="B"
)
polbaux_a, polaaux_a = feeddown_from_nth_order(
ith, polapad[ith - 1], xoffset, yoffset, poltype="A"
)
polbshort = polbaux_b + polbaux_a
polashort = polaaux_b + polaaux_a
polbsum = polbsum + numpy.pad(
polbshort, (0, maxord - ith), "constant", constant_values=(0, 0)
)
polasum = polasum + numpy.pad(
polashort, (0, maxord - ith), "constant", constant_values=(0, 0)
)
poldict = {"PolynomB": polbsum, "PolynomA": polasum}
verboseprint(f"poldict={poldict}")
return poldict
[docs]
def feeddown_from_nth_order(
nthorder: int,
nthpolcomp: float,
xoffset: float,
yoffset: float,
poltype: str = "B",
verbose: bool = False,
) -> tuple[numpy.ndarray, numpy.ndarray]:
"""
Return the feeddown polynoms from an nth-order magnet component transverse offset.
Parameters:
nthorder: integer order of the magnet component, e.g. 1 for dipole,
2 for quadrupoles, 3 for sextupoles, etc.
nthpolcomp: float. nth component of the polynom, i.e. the value of
PolynomA/B[nthorder-1].
xoffset: float. Horizontal offset in meters.
yoffset: float. Vertical offset in meters.
poltype: Default 'B'. Could be 'B' or 'A', otherwise ignored.
verbose: print info on input and output parameters.
Returns:
Tuple of two numpy arrays with PolynomB and PolynomA from feeddown.
"""
verboseprint = print if verbose else lambda *a, **k: None
verboseprint(f"nthorder={nthorder},nthpolcomp={nthpolcomp},poltype={poltype}")
verboseprint(f"xoffset={xoffset},yoffset={yoffset}")
fakeimag = {0: 1, 1: 0, 2: -1, 3: 0}
polbaux = numpy.zeros(nthorder - 1)
polaaux = numpy.zeros(nthorder - 1)
if nthpolcomp != 0:
for kterm in range(1, nthorder):
ichoosek = comb(nthorder - 1, kterm)
pascalsn = comb(kterm, numpy.arange(kterm + 1))
powk = numpy.arange(kterm + 1)
powkflip = numpy.arange(kterm, -1, -1)
recoefs = numpy.array([fakeimag[numpy.mod(idx, 4)] for idx in powk])
imcoefs = numpy.array([fakeimag[numpy.mod(idx + 3, 4)] for idx in powk])
commonfactor = nthpolcomp * ichoosek * (-1) ** kterm
repart = (
recoefs
* commonfactor
* (pascalsn * (xoffset**powkflip * yoffset**powk))
)
impart = (
imcoefs
* commonfactor
* (pascalsn * (xoffset**powkflip * yoffset**powk))
)
polbaux[nthorder - kterm - 1] = polbaux[nthorder - kterm - 1] + repart.sum()
polaaux[nthorder - kterm - 1] = polaaux[nthorder - kterm - 1] + impart.sum()
verboseprint(f"polbaux={polbaux},polaaux={polaaux}")
polbout, polaout = polbaux, polaaux
# skew components swap the imaginary and real feed-down and change the sign
# of the real part, i.e. j*j = -1
if poltype == "A":
polbout, polaout = -1 * polaaux, polbaux
return polbout, polaout