Source code for at.physics.linear

"""
Coupled or non-coupled 4x4 linear motion.
"""

from __future__ import annotations

import warnings
from collections.abc import Callable
from math import sqrt, pi, sin, cos, atan2

import numpy as np
from scipy.linalg import solve

from .amat import a_matrix, jmat, jmatswap
from .harmonic_analysis import get_tunes_harmonic
from .matrix import find_m44, find_m66
from .orbit import find_orbit4, find_orbit6
from ..constants import clight
from ..lattice import AtError, AtWarning, RFCavity
from ..lattice import Lattice, Orbit, Refpts, check_6d, get_s_pos
from ..lattice import DConstant, get_bool_index, get_uint32_index
from ..lattice import frequency_control, set_value_refpts, get_value_refpts
from ..tracking import internal_lpass

__all__ = [
    "avlinopt",
    "get_chrom",
    "get_optics",
    "get_tune",
    "linopt",
    "linopt2",
    "linopt4",
    "linopt6",
]

_S = jmat(1)
_S2 = np.array([[0, 1], [-1, 0]], dtype=np.float64)

# dtype for structured array containing linopt parameters
_DATA2_DTYPE = [
    ("alpha", np.float64, (2,)),
    ("beta", np.float64, (2,)),
    ("mu", np.float64, (2,)),
]

_DATA4_DTYPE = [
    ("alpha", np.float64, (2,)),
    ("beta", np.float64, (2,)),
    ("mu", np.float64, (2,)),
    ("gamma", np.float64),
    ("A", np.float64, (2, 2)),
    ("B", np.float64, (2, 2)),
    ("C", np.float64, (2, 2)),
]

_DATA6_DTYPE = [
    ("alpha", np.float64, (2,)),
    ("beta", np.float64, (2,)),
    ("mu", np.float64, (3,)),
    ("R", np.float64, (3, 6, 6)),
    ("A", np.float64, (6, 6)),
    ("dispersion", np.float64, (4,)),
]

_DATAX_DTYPE = [
    ("alpha", np.float64, (2,)),
    ("beta", np.float64, (2,)),
    ("mu", np.float64, (2,)),
    ("R", np.float64, (2, 4, 4)),
    ("A", np.float64, (4, 4)),
]

_W24_DTYPE = [
    ("W", np.float64, (2,)),
    ("Wp", np.float64, (2,)),
    ("dalpha", np.float64, (2,)),
    ("dbeta", np.float64, (2,)),
    ("dmu", np.float64, (2,)),
    ("ddispersion", np.float64, (4,)),
]


_W6_DTYPE = [
    ("W", np.float64, (2,)),
    ("Wp", np.float64, (2,)),
    ("dalpha", np.float64, (2,)),
    ("dbeta", np.float64, (2,)),
    ("dmu", np.float64, (3,)),
    ("dR", np.float64, (3, 6, 6)),
    ("ddispersion", np.float64, (4,)),
]

_WX_DTYPE = [
    ("W", np.float64, (2,)),
    ("Wp", np.float64, (2,)),
    ("dalpha", np.float64, (2,)),
    ("dbeta", np.float64, (2,)),
    ("dmu", np.float64, (2,)),
    ("dR", np.float64, (2, 4, 4)),
    ("ddispersion", np.float64, (4,)),
]


_IDX_DTYPE = [("idx", np.uint32)]
warnings.filterwarnings("always", category=AtWarning, module=__name__)


def _twiss22(t12, alpha0, beta0):
    """Propagate Twiss parameters."""
    bbb = t12[:, 0, 1]
    aaa = t12[:, 0, 0] * beta0 - bbb * alpha0
    beta = (aaa * aaa + bbb * bbb) / beta0
    alpha = (
        -(aaa * (t12[:, 1, 0] * beta0 - t12[:, 1, 1] * alpha0) + bbb * t12[:, 1, 1])
        / beta0
    )
    mu = np.arctan2(bbb, aaa)
    # Unwrap negative jumps in betatron phase advance
    # dmu = np.diff(np.append([0], mu))
    # jumps = dmu < -1.0e-3
    # mu += np.cumsum(jumps) * 2.0 * np.pi
    return alpha, beta, mu


def _closure(m22):
    diff = (m22[0, 0] - m22[1, 1]) / 2.0
    try:
        sinmu = np.sign(m22[0, 1]) * sqrt(-m22[0, 1] * m22[1, 0] - diff * diff)
        cosmu = 0.5 * np.trace(m22)
        alpha = diff / sinmu
        beta = m22[0, 1] / sinmu
        return alpha, beta, cosmu + sinmu * 1j
    except ValueError:  # Unstable motion
        return np.nan, np.nan, np.nan


# noinspection PyShadowingNames,PyPep8Naming
def _tunes(ring, **kwargs):
    """"""
    if ring.is_6d:
        nd = 3
        mt, _ = find_m66(ring, **kwargs)
    else:
        nd = 2
        mt, _ = find_m44(ring, **kwargs)
    try:
        _, vps = a_matrix(mt)
        tunes = np.mod(np.angle(vps) / 2.0 / pi, 1.0)
    except AtError:
        warnings.warn(AtWarning("Unstable ring"), stacklevel=1)
        tunes = np.empty(nd)
        tunes[:] = np.nan
    return tunes


def _analyze2(mt, ms):
    """Analysis of a 2D 1-turn transfer matrix."""
    A = mt[:2, :2]
    B = mt[2:, 2:]
    alp0_a, bet0_a, vp_a = _closure(A)
    alp0_b, bet0_b, vp_b = _closure(B)
    vps = np.array([vp_a, vp_b])
    el0 = (np.array([alp0_a, alp0_b]), np.array([bet0_a, bet0_b]), 0.0)
    alpha_a, beta_a, mu_a = _twiss22(ms[:, :2, :2], alp0_a, bet0_a)
    alpha_b, beta_b, mu_b = _twiss22(ms[:, 2:, 2:], alp0_b, bet0_b)
    els = (
        np.stack((alpha_a, alpha_b), axis=1),
        np.stack((beta_a, beta_b), axis=1),
        np.stack((mu_a, mu_b), axis=1),
    )
    return vps, _DATA2_DTYPE, el0, els, _W24_DTYPE


def _analyze4(mt, ms):
    """Analysis of a 4D 1-turn transfer matrix according to Sagan, Rubin."""

    def propagate(t12):
        M = t12[:2, :2]
        N = t12[2:, 2:]
        m = t12[:2, 2:]
        n = t12[2:, :2]
        ff = n @ C + g * N
        gamma = sqrt(np.linalg.det(ff))
        e12 = (g * M - m @ _S @ C.T @ _S.T) / gamma
        f12 = ff / gamma
        a12 = e12 @ A @ _S @ e12.T @ _S.T
        b12 = f12 @ B @ _S @ f12.T @ _S.T
        c12 = M @ C + g * m @ _S @ f12.T @ _S.T
        return e12, f12, gamma, a12, b12, c12

    M = mt[:2, :2]
    N = mt[2:, 2:]
    m = mt[:2, 2:]
    n = mt[2:, :2]
    H = m + _S @ n.T @ _S.T
    detH = np.linalg.det(H)
    if detH == 0.0:
        g = 1.0
        C = -H
        A = M
        B = N
    else:
        t = np.trace(M - N)
        t2 = t * t
        t2h = t2 + 4.0 * detH
        g2 = (1.0 + sqrt(t2 / t2h)) / 2
        g = sqrt(g2)
        C = -H * np.sign(t) / (g * sqrt(t2h))
        A = g2 * M - g * (m @ _S @ C.T @ _S.T + C @ n) + C @ N @ _S @ C.T @ _S.T
        B = g2 * N + g * (_S @ C.T @ _S.T @ m + n @ C) + _S @ C.T @ _S.T @ M @ C
    alp0_a, bet0_a, vp_a = _closure(A)
    alp0_b, bet0_b, vp_b = _closure(B)
    vps = np.array([vp_a, vp_b])
    el0 = (np.array([alp0_a, alp0_b]), np.array([bet0_a, bet0_b]), 0.0, g, A, B, C)
    if ms.shape[0] > 0:
        e, f, g, ai, bi, ci = zip(*[propagate(mi) for mi in ms])
        alp_a, bet_a, mu_a = _twiss22(np.array(e), alp0_a, bet0_a)
        alp_b, bet_b, mu_b = _twiss22(np.array(f), alp0_b, bet0_b)
        els = (
            np.stack((alp_a, alp_b), axis=1),
            np.stack((bet_a, bet_b), axis=1),
            np.stack((mu_a, mu_b), axis=1),
            np.array(g),
            np.stack(ai, axis=0),
            np.stack(bi, axis=0),
            np.stack(ci, axis=0),
        )
    else:
        els = (
            np.empty((0, 2)),
            np.empty((0, 2)),
            np.empty((0, 2)),
            np.empty((0,)),
            np.empty((0, 2, 2)),
            np.empty((0, 2, 2)),
            np.empty((0, 2, 2)),
        )
    return vps, _DATA4_DTYPE, el0, els, _W24_DTYPE


def _analyze6(mt, ms):
    """Analysis of a 2D, 4D, 6D 1-turn transfer matrix
    according to Wolski."""

    def get_phase(a22):
        """Return the phase for A standardisation."""
        return atan2(a22[0, 1], a22[0, 0])

    def standardize(aa, slcs):
        """Apply rotation to put A in std form."""

        def rot2(slc):
            rot = -get_phase(aa[slc, slc])
            cs = cos(rot)
            sn = sin(rot)
            return aa[:, slc] @ np.array([[cs, sn], [-sn, cs]])

        return np.concatenate([rot2(slc) for slc in slcs], axis=1)

    def r_matrices(ai):
        # Rk = A * S * Ik * inv(A) * S.T
        def mul2(slc):
            return ai[:, slc] @ tt[slc, slc]

        ais = np.concatenate([mul2(slc) for slc in slices], axis=1)
        invai = solve(ai, ss.T)
        ri = np.array([ais[:, sl] @ invai[sl, :] for sl in slices])
        mui = np.array([get_phase(ai[sl, sl]) for sl in slices])
        return mui, ri, ai

    def propagate4(ri, phi, ai):
        betai = np.stack((ri[..., 0, 0, 0], ri[..., 1, 2, 2]), axis=-1)
        alphai = -np.stack((ri[..., 0, 1, 0], ri[..., 1, 3, 2]), axis=-1)
        return alphai, betai, phi, ri, ai

    def propagate6(ri, phi, ai):
        alphai, betai, phi, ri, ai = propagate4(ri, phi, ai)
        dispersion = ri[..., 2, :4, 4] / ri[..., 2, 4, 4, np.newaxis]
        return alphai, betai, phi, ri, ai, dispersion

    nv = mt.shape[0]
    dms = nv // 2
    slices = [slice(2 * i, 2 * (i + 1)) for i in range(dms)]
    ss = jmat(dms)
    tt = jmatswap(dms)
    if dms >= 3:
        propagate = propagate6
        dtype = _DATA6_DTYPE
        wtype = _W6_DTYPE
    else:
        propagate = propagate4
        dtype = _DATAX_DTYPE
        wtype = _WX_DTYPE
    a0, vps = a_matrix(mt)

    astd = standardize(a0, slices)
    phi0, r0, _ = r_matrices(astd)
    el0 = propagate(r0, phi0, astd)
    if ms.shape[0] > 0:
        ps, rs, aas = zip(*[r_matrices(mi @ astd) for mi in ms])
        els = propagate(np.array(rs), np.array(ps), np.array(aas))
    elif dms >= 3:
        els = (
            np.empty((0, dms)),
            np.empty((0, dms)),
            np.empty((0, dms)),
            np.empty((0, dms, nv, nv)),
            np.empty((0, nv, nv)),
            np.empty((0, 4)),
        )
    else:
        els = (
            np.empty((0, dms)),
            np.empty((0, dms)),
            np.empty((0, dms)),
            np.empty((0, dms, nv, nv)),
            np.empty((0, nv, nv)),
        )
    return vps, dtype, el0, els, wtype


# noinspection PyShadowingNames,PyPep8Naming
def _linopt(
    ring: Lattice,
    analyze,
    refpts=None,
    dp=None,
    dct=None,
    df=None,
    orbit=None,
    twiss_in=None,
    get_chrom=False,
    get_w=False,
    keep_lattice=False,
    mname="M",
    add0=(),
    adds=(),
    cavpts=None,
    **kwargs,
):
    """"""

    def build_sigma(orbit, dp=None):
        """Build the initial distribution at entrance of the transfer line."""
        try:
            d0 = twiss_in["dispersion"]
        except (ValueError, KeyError):  # record arrays throw ValueError !
            d0 = np.zeros((4,))

        try:
            rmat = twiss_in["R"]
        except (ValueError, KeyError):  # record arrays throw ValueError !
            rmat = None

        try:
            alphas = twiss_in["alpha"]
            betas = twiss_in["beta"]
        except (ValueError, KeyError):  # record arrays throw ValueError !
            alphas = np.zeros((2,))
            betas = np.ones((2,))

        dorbit = np.hstack((d0, 1.0, 0.0))
        if orbit is None:
            try:
                orbit = twiss_in["closed_orbit"]
            except (ValueError, KeyError):  # record arrays throw ValueError !
                orbit = np.zeros((6,))
            if dp is not None:
                orbit = orbit + dorbit * dp

        if dp is not None:
            try:
                drmat = twiss_in["dR"]
            except (ValueError, KeyError):  # record arrays throw ValueError !
                drmat = None

            try:
                dd0 = twiss_in["ddispersion"]
                dalpha = twiss_in["dalpha"]
                dbeta = twiss_in["dbeta"]
            except (ValueError, KeyError) as exc:  # record arrays throw ValueError !
                msg = (
                    "'get_w' option for a line requires 'twiss_in' calculated "
                    "with 'get_w' activated"
                )
                raise AtError(msg) from exc

            orbit = orbit + np.hstack((dd0, 1.0, 0.0)) * dp * dp
            dorbit = np.hstack((d0 + dd0 * dp, 1.0, 0.0))

            if (rmat is not None) and (drmat is not None):
                rmat = rmat + drmat * dp
            else:
                alphas = alphas + dalpha * dp
                betas = betas + dbeta * dp

        if rmat is not None:
            # For some reason, "emittances" must be different...
            sigm = rmat[0, ...] + 10.0 * rmat[1, ...]
            if rmat.shape[0] >= 3:
                sigm = sigm + 0.1 * rmat[2, ...]
                dorbit = rmat[2, :, 4] / rmat[2, 4, 4, np.newaxis]
        else:
            slices = [slice(2 * i, 2 * (i + 1)) for i in range(2)]
            ab = np.stack((alphas, betas), axis=1)
            sigm = np.zeros((4, 4))
            for slc, (alpha, beta) in zip(slices, ab):
                gamma = (1.0 + alpha * alpha) / beta
                sigm[slc, slc] = np.array([[beta, -alpha], [-alpha, gamma]])

        return orbit, sigm, dorbit

    def chrom_w(ringup, ringdn, orbitup, orbitdn, refpts=None, **kwargs):
        """Compute the chromaticity and W-functions."""

        # noinspection PyShadowingNames
        def off_momentum(rng, orb0, dp=None, **kwargs):
            if twiss_in is None:
                mt, ms = get_matrix(rng, refpts=refpts, orbit=orb0, **kwargs)
                mxx = mt
                dpup = orb0[4] + 0.5 * dp_step
                dpdn = orb0[4] - 0.5 * dp_step
                o0up = None
                o0dn = None
            else:
                orbit, sigma, dorbit = build_sigma(orb0, dp=dp)
                mt, ms = get_matrix(ring, refpts=refpts, orbit=orbit, **kwargs)
                mxx = sigma @ jmat(sigma.shape[0] // 2)
                o0up = orbit + dorbit * 0.5 * dp_step
                o0dn = orbit - dorbit * 0.5 * dp_step
                dpup = None
                dpdn = None
            _vps, _, el0, els, wtype = analyze(mxx, ms)
            tunes = _tunes(rng, orbit=orb0)
            o0up, oup = get_orbit(
                ring, refpts=refpts, guess=orb0, dp=dpup, orbit=o0up, **kwargs
            )
            o0dn, odn = get_orbit(
                ring, refpts=refpts, guess=orb0, dp=dpdn, orbit=o0dn, **kwargs
            )
            d0 = (o0up - o0dn)[:4] / dp_step
            ds = np.array([(up - dn)[:4] / dp_step for up, dn in zip(oup, odn)])
            return tunes, el0, els, d0, ds, wtype

        def wget(ddp, elup, eldn, has_r):
            """Compute the chromatic amplitude function."""
            (*data_up,) = elup  # Extract alpha and beta
            (*data_dn,) = eldn
            alpha_up, beta_up, mu_up = data_up[:3]
            alpha_dn, beta_dn, mu_dn = data_dn[:3]
            db = np.array(beta_up - beta_dn) / ddp
            mb = (beta_up + beta_dn) / 2
            da = np.array(alpha_up - alpha_dn) / ddp
            ma = (alpha_up + alpha_dn) / 2
            wa = da - ma / mb * db
            wb = db / mb
            ww = np.sqrt(wa**2 + wb**2)
            wp = np.arctan2(wa, wb)
            dmu = np.array(mu_up - mu_dn) / ddp
            data_out = (ww, wp, da, db, dmu)
            if has_r:
                r_up = data_up[3]
                r_dn = data_dn[3]
                data_out += (np.array(r_up - r_dn) / ddp,)
            return data_out

        deltap = orbitup[4] - orbitdn[4]
        (*data_up,) = off_momentum(ringup, orbitup, dp=0.5 * deltap, **kwargs)
        (*data_dn,) = off_momentum(ringdn, orbitdn, dp=-0.5 * deltap, **kwargs)
        tunesup, el0up, elsup, d0up, dsup, wtype = data_up
        tunesdn, el0dn, elsdn, d0dn, dsdn, _ = data_dn
        has_r = len(wtype) == 7
        # in 6D, dp comes out of find_orbit6
        chrom = (tunesup - tunesdn) / deltap
        dd0 = np.array(d0up - d0dn) / deltap
        dds = np.array(dsup - dsdn) / deltap
        data0 = wget(deltap, el0up, el0dn, has_r)
        datas = wget(deltap, elsup, elsdn, has_r)
        data0 = (*data0, dd0)
        datas = (*datas, dds)
        return chrom, data0, datas

    def unwrap(mu):
        """Remove the phase jumps."""
        dmu = np.diff(np.concatenate((np.zeros((1, dms)), mu)), axis=0)
        jumps = dmu < -1.0e-3
        mu += np.cumsum(jumps, axis=0) * 2.0 * np.pi

    dp_step = kwargs.get("DPStep", DConstant.DPStep)
    addtype = kwargs.pop("addtype", [])

    if ring.is_6d:
        get_matrix = find_m66
        get_orbit = find_orbit6
    else:
        get_matrix = find_m44
        get_orbit = find_orbit4

    o0up = None
    o0dn = None
    if twiss_in is None:  # Ring
        if orbit is None:
            orbit, _ = get_orbit(
                ring, dp=dp, dct=dct, df=df, keep_lattice=keep_lattice, **kwargs
            )
            keep_lattice = True
        # Get 1-turn transfer matrix
        mt, ms = get_matrix(ring, refpts=refpts, orbit=orbit, **kwargs)
        mxx = mt
    else:  # Transfer line
        orbit, sigma, dorbit = build_sigma(orbit)
        # Get 1-turn transfer matrix
        mt, ms = get_matrix(ring, refpts=refpts, orbit=orbit, **kwargs)
        mxx = sigma @ jmat(sigma.shape[0] // 2)
        o0up = orbit + dorbit * 0.5 * dp_step
        o0dn = orbit - dorbit * 0.5 * dp_step

    # Perform analysis
    vps, dtype, el0, els, wtype = analyze(mxx, ms)
    tunes = np.mod(np.angle(vps) / 2.0 / pi, 1.0)

    if (get_chrom or get_w) and mt.shape == (6, 6):
        dff = dp_step * ring.disable_6d(copy=True).slip_factor
        cavities = get_bool_index(ring, RFCavity)
        freqs = get_value_refpts(ring, cavities, "Frequency")
        rgup = set_value_refpts(
            ring, cavities, "Frequency", freqs * (1.0 + 0.5 * dff), copy=True
        )
        rgdn = set_value_refpts(
            ring, cavities, "Frequency", freqs * (1.0 - 0.5 * dff), copy=True
        )
        if o0up is None:
            o0up, _ = get_orbit(rgup, guess=orbit, orbit=o0up, **kwargs)
            o0dn, _ = get_orbit(rgdn, guess=orbit, orbit=o0dn, **kwargs)
    else:
        rgup = ring
        rgdn = ring

    # Propagate the closed orbit
    orb0, orbs = get_orbit(ring, refpts=refpts, orbit=orbit, keep_lattice=keep_lattice)
    spos = ring.get_s_pos(refpts)

    nrefs = orbs.shape[0]
    dms = vps.size
    if dms >= 3:  # 6D processing
        dtype = [
            *dtype,
            ("closed_orbit", np.float64, (6,)),
            ("M", np.float64, (2 * dms, 2 * dms)),
            ("s_pos", np.float64),
        ]
        data0 = (orb0, np.identity(2 * dms), 0.0)
        datas = (orbs, ms, spos)
        length = ring.get_s_pos(len(ring))[0]
        damping_rates = -np.log(np.absolute(vps))
        damping_times = length / clight / damping_rates
    else:  # 4D processing
        kwargs["keep_lattice"] = True
        dpup = orb0[4] + 0.5 * dp_step
        dpdn = orb0[4] - 0.5 * dp_step
        o0up, oup = get_orbit(
            ring, refpts=refpts, guess=orb0, dp=dpup, orbit=o0up, **kwargs
        )
        o0dn, odn = get_orbit(
            ring, refpts=refpts, guess=orb0, dp=dpdn, orbit=o0dn, **kwargs
        )
        d0 = (o0up - o0dn)[:4] / dp_step
        ds = np.array([(up - dn)[:4] / dp_step for up, dn in zip(oup, odn)])
        dtype = [
            *dtype,
            ("dispersion", np.float64, (4,)),
            ("closed_orbit", np.float64, (6,)),
            (mname, np.float64, (2 * dms, 2 * dms)),
            ("s_pos", np.float64),
        ]
        data0 = (d0, orb0, mt, get_s_pos(ring, len(ring))[0])
        datas = (ds, orbs, ms, spos)
        damping_times = np.nan

    if get_w:
        dtype = dtype + wtype
        chrom, ddata0, ddatas = chrom_w(rgup, rgdn, o0up, o0dn, refpts, **kwargs)
        data0 = data0 + ddata0
        datas = datas + ddatas
    elif get_chrom:
        tunesup = _tunes(rgup, orbit=o0up)
        tunesdn = _tunes(rgdn, orbit=o0dn)
        deltap = o0up[4] - o0dn[4]
        chrom = (tunesup - tunesdn) / deltap
    else:
        chrom = np.nan

    beamdata = np.array(
        (tunes, chrom, damping_times),
        dtype=[
            ("tune", np.float64, (dms,)),
            ("chromaticity", np.float64, (dms,)),
            ("damping_time", np.float64, (dms,)),
        ],
    ).view(np.recarray)

    dtype = dtype + addtype
    elemdata0 = np.array(el0 + data0 + add0, dtype=dtype).view(np.recarray)
    elemdata = np.recarray((nrefs,), dtype=dtype)
    if nrefs > 0:
        for name, value in zip(np.dtype(dtype).names, els + datas + adds):
            elemdata[name] = value
        unwrap(elemdata.mu)
    return elemdata0, beamdata, elemdata


[docs] @check_6d(False) def linopt2(ring: Lattice, *args, **kwargs): r"""Linear analysis of an uncoupled lattice. :py:func:`linopt2` computes the linear optics parameters on a single cell (*periodicity* is not taken into account). Parameters: ring: Lattice description. Keyword Args: refpts (Refpts): Elements at which data is returned. It can be: 1. an integer in the range [-len(ring), len(ring)-1] selecting the element according to python indexing rules. As a special case, len(ring) is allowed and refers to the end of the last element, 2. an ordered list of such integers without duplicates, 3. a numpy array of booleans of maximum length len(ring)+1, where selected elements are :py:obj:`True`. dp (float): Momentum deviation. Defaults to :py:obj:`None` dct (float): Path lengthening. Defaults to :py:obj:`None` df (float): Deviation of RF frequency. Defaults to :py:obj:`None` orbit (Orbit): Avoids looking for the closed orbit if it is already known ((6,) array) get_chrom (bool): Compute chromaticities. Needs computing the tune at 2 different momentum deviations around the central one. get_w (bool): Computes chromatic amplitude functions (W, WP) [4]_, and derivatives of the dispersion and twiss parameters versus dp. Needs to compute the optics at 2 different momentum deviations around the central one. keep_lattice (bool): Assume no lattice change since the previous tracking. Defaults to :py:obj:`False` XYStep (float): Step size. Default: :py:data:`DConstant.XYStep <.DConstant>` DPStep (float): Momentum step size. Default: :py:data:`DConstant.DPStep <.DConstant>` twiss_in: Initial conditions for transfer line optics. Record array as output by :py:func:`.linopt6`, or dictionary. Keys: R or alpha, beta mandatory (2,) arrays closed_orbit Optional (6,) array, default 0 dispersion Optional (6,) array, default 0 If present, the attribute **R** will be used, otherwise the attributes **alpha** and **beta** will be used. All other attributes are ignored. Returns: elemdata0: Linear optics data at the entrance of the ring ringdata: Lattice properties elemdata: Linear optics at the points referred to by *refpts*, if refpts is :py:obj:`None` an empty lindata structure is returned. .. _linopt2_elemdata: **elemdata** is a record array with fields: ================ =================================================== **s_pos** longitudinal position [m] **M** (4, 4) transfer matrix M from the beginning of ring to the entrance of the element [2]_ **closed_orbit** (6,) closed orbit vector **dispersion** (4,) dispersion vector **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 Warning: if the sampling (number of refpts) is too sparse, i.e. if the phase advance is more than 2*pi between 2 points the integer part will not increment correclty **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`) ================ =================================================== All values given at the entrance of each element specified in refpts. Field values can be obtained with either *lindata['idx']* or *lindata.idx* **ringdata** is a record array with fields: ================= ====== **tune** Fractional tunes **chromaticity** Chromaticities, only computed if *get_chrom* is :py:obj:`True` ================= ====== References: **[1]** D.Edwards,L.Teng IEEE Trans.Nucl.Sci. NS-20, No.3, p.885-888 , 1973 .. [2] E.Courant, H.Snyder **[3]** D.Sagan, D.Rubin Phys.Rev.Spec.Top.-Accelerators and beams, vol.2 (1999) .. [4] Brian W. Montague Report LEP Note 165, CERN, 1979 """ return _linopt(ring, _analyze2, *args, **kwargs)
[docs] @check_6d(False) def linopt4(ring: Lattice, *args, **kwargs): r"""Linear analysis of a H/V coupled lattice. 4D-analysis of coupled motion following Sagan/Rubin [7]_ :py:func:`linopt4` computes the linear optics parameters on a single cell (*periodicity* is not taken into account). Parameters: ring: Lattice description. Keyword Args: refpts (Refpts): Elements at which data is returned. It can be: 1. an integer in the range [-len(ring), len(ring)-1] selecting the element according to python indexing rules. As a special case, len(ring) is allowed and refers to the end of the last element, 2. an ordered list of such integers without duplicates, 3. a numpy array of booleans of maximum length len(ring)+1, where selected elements are :py:obj:`True`. dp (float): Momentum deviation. Defaults to :py:obj:`None` dct (float): Path lengthening. Defaults to :py:obj:`None` df (float): Deviation of RF frequency. Defaults to :py:obj:`None` orbit (Orbit): Avoids looking for the closed orbit if it is already known ((6,) array) get_chrom (bool): Compute chromaticities. Needs computing the tune at 2 different momentum deviations around the central one. get_w (bool): Computes chromatic amplitude functions (W) [8]_. Needs to compute the optics at 2 different momentum deviations around the central one. keep_lattice (bool): Assume no lattice change since the previous tracking. Defaults to :py:obj:`False` XYStep (float): Step size. Default: :py:data:`DConstant.XYStep <.DConstant>` DPStep (float): Momentum step size. Default: :py:data:`DConstant.DPStep <.DConstant>` twiss_in: Initial conditions for transfer line optics. Record array as output by :py:func:`.linopt6`, or dictionary. Keys: R or alpha, beta mandatory (2,) arrays closed_orbit Optional (6,) array, default 0 dispersion Optional (6,) array, default 0 If present, the attribute **R** will be used, otherwise the attributes **alpha** and **beta** will be used. All other attributes are ignored. Returns: elemdata0: Linear optics data at the entrance of the ring ringdata: Lattice properties elemdata: Linear optics at the points refered to by *refpts*, if refpts is :py:obj:`None` an empty lindata structure is returned. .. _linopt4_elemdata: **elemdata** is a record array with fields: ================ =================================================== **s_pos** longitudinal position [m] **M** (4, 4) transfer matrix M from the beginning of ring to the entrance of the element [6]_ **closed_orbit** (6,) closed orbit vector **dispersion** (4,) dispersion vector **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 Warning: if the sampling (number of refpts) is too sparse, i.e. if the phase advance is more than 2*pi between 2 points the integer part will not increment correclty **gamma** gamma parameter of the transformation to eigenmodes [7]_ **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`) ================ =================================================== All values given at the entrance of each element specified in refpts. Field values can be obtained with either *lindata['idx']* or *lindata.idx* **ringdata** is a record array with fields: ================= ====== **tune** Fractional tunes **chromaticity** Chromaticities, only computed if *get_chrom* is :py:obj:`True` ================= ====== References: **[5]** D.Edwards,L.Teng IEEE Trans.Nucl.Sci. NS-20, No.3, p.885-888 , 1973 .. [6] E.Courant, H.Snyder .. [7] D.Sagan, D.Rubin Phys.Rev.Spec.Top.-Accelerators and beams, vol.2 (1999) .. [8] Brian W. Montague Report LEP Note 165, CERN, 1979 """ return _linopt(ring, _analyze4, *args, **kwargs)
[docs] @frequency_control def linopt6(ring: Lattice, *args, **kwargs): r"""Linear analysis of a fully coupled lattice using normal modes. For circular machines, :py:func:`linopt6` analyses * the 4x4 1-turn transfer matrix if *ring* is 4D, or * the 6x6 1-turn transfer matrix if *ring* is 6D. For a transfer line, The "twiss_in" intput must contain either: * a field **R**, as provided by ATLINOPT6, or * the fields **beta** and **alpha**, as provided by linopt and linopt6 :py:func:`linopt6` computes the linear optics parameters on a single cell (*periodicity* is not taken into account). Parameters: ring: Lattice description. Keyword Args: refpts (Refpts): Elements at which data is returned. It can be: 1. an integer in the range [-len(ring), len(ring)-1] selecting the element according to python indexing rules. As a special case, len(ring) is allowed and refers to the end of the last element, 2. an ordered list of such integers without duplicates, 3. a numpy array of booleans of maximum length len(ring)+1, where selected elements are :py:obj:`True`. dp (float): Momentum deviation. Defaults to :py:obj:`None` dct (float): Path lengthening. Defaults to :py:obj:`None` df (float): Deviation of RF frequency. Defaults to :py:obj:`None` orbit (Orbit): Avoids looking for the closed orbit if it is already known ((6,) array) get_chrom (bool): Compute chromaticities. Needs computing the tune at 2 different momentum deviations around the central one. get_w (bool): Computes chromatic amplitude functions (W) [11]_. Needs to compute the optics at 2 different momentum deviations around the central one. keep_lattice (bool): Assume no lattice change since the previous tracking. Defaults to :py:obj:`False` XYStep (float): Step size. Default: :py:data:`DConstant.XYStep <.DConstant>` DPStep (float): Momentum step size. Default: :py:data:`DConstant.DPStep <.DConstant>` twiss_in: Initial conditions for transfer line optics. Record array as output by :py:func:`.linopt6`, or dictionary. Keys: R or alpha, beta mandatory (2,) arrays closed_orbit Optional (6,) array, default 0 dispersion Optional (6,) array, default 0 If present, the attribute **R** will be used, otherwise the attributes **alpha** and **beta** will be used. All other attributes are ignored. cavpts (Refpts): Cavity location for off-momentum tuning Returns: elemdata0: Linear optics data at the entrance of the ring ringdata: Lattice properties elemdata: Linear optics at the points refered to by *refpts*, if refpts is :py:obj:`None` an empty lindata structure is returned. .. _linopt6_elemdata: **elemdata** is a record array with fields: ================ =================================================== **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 Warning: if the sampling (number of refpts) is too sparse, i.e. if the phase advance is more than 2*pi between 2 points the integer part will not increment correclty **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`) ================ =================================================== All values given at the entrance of each element specified in refpts. Field values can be obtained with either *lindata['idx']* or *lindata.idx* **ringdata** is a record array with fields: ================= ====== **tune** Fractional tunes **chromaticity** Chromaticities, only computed if *get_chrom* is :py:obj:`True` **damping_time** Damping times [s] (only if radiation is ON) ================= ====== References: **[9]** Etienne Forest, Phys. Rev. E 58, 2481 – Published 1 August 1998 **[10]** Andrzej Wolski, Phys. Rev. ST Accel. Beams 9, 024001 – Published 3 February 2006 .. [11] Brian W. Montague Report LEP Note 165, CERN, 1979 """ return _linopt(ring, _analyze6, *args, **kwargs)
def linopt_auto(ring: Lattice, *args, **kwargs): """ This is a convenience function to automatically switch to the faster :py:func:`linopt2` in case the *coupled* keyword argument is :py:obj:`False` **and** ring.is_6d is :py:obj:`False`. Otherwise, the default :py:func:`linopt6` is used. Parameters: Same as :py:func:`.linopt2` or :py:func:`.linopt6` Keyword Args; coupled (bool): If set to :py:obj:`False`, H/V coupling will be ignored to simplify the calculation (needs ring.is_6d :py:obj:`False`) Returns: elemdata0: Linear optics data at the entrance of the ring ringdata: Lattice properties elemdata: Linear optics at the points referred to by *refpts*, if refpts is :py:obj:`None` an empty lindata structure is returned. Warning: The output varies depending whether :py:func:`.linopt2` or :py:func:`.linopt6` is called. To be used with care! """ if not (kwargs.pop("coupled", True) or ring.is_6d): return linopt2(ring, *args, **kwargs) else: return linopt6(ring, *args, **kwargs)
[docs] def get_optics( ring: Lattice, refpts: Refpts = None, dp: float | None = None, method: Callable = linopt6, **kwargs, ): """Linear analysis of a fully coupled lattice. :py:func:`get_optics` computes the linear optics parameters on a single cell (*periodicity* is not taken into account). Parameters: ring: Lattice description. refpts: Elements at which data is returned. It can be: 1. an integer in the range [-len(ring), len(ring)-1] selecting the element according to python indexing rules. As a special case, len(ring) is allowed and refers to the end of the last element, 2. an ordered list of such integers without duplicates, 3. a numpy array of booleans of maximum length len(ring)+1, where selected elements are :py:obj:`True`. dp: Momentum deviation. method (Callable): Method for linear optics: :py:obj:`~.linear.linopt2`: no longitudinal motion, no H/V coupling, :py:obj:`~.linear.linopt4`: no longitudinal motion, Sagan/Rubin 4D-analysis of coupled motion, :py:obj:`~.linear.linopt6` (default): with or without longitudinal motion, normal mode analysis Keyword Args: dct (float): Path lengthening. Defaults to :py:obj:`None` df (float): Deviation of RF frequency. Defaults to :py:obj:`None` orbit (Orbit): Avoids looking for the closed orbit if it is already known ((6,) array) get_chrom (bool): Compute chromaticities. Needs computing the tune at 2 different momentum deviations around the central one. get_w (bool): Computes chromatic amplitude functions (W) [4]_. Needs to compute the optics at 2 different momentum deviations around the central one. keep_lattice (bool): Assume no lattice change since the previous tracking. Defaults to :py:obj:`False` XYStep (float): Step size. Default: :py:data:`DConstant.XYStep <.DConstant>` DPStep (float): Momentum step size. Default: :py:data:`DConstant.DPStep <.DConstant>` twiss_in: Initial conditions for transfer line optics. Record array as output by :py:func:`.linopt6`, or dictionary. Keys: R or alpha, beta mandatory (2,) arrays closed_orbit Optional (6,) array, default 0 dispersion Optional (6,) array, default 0 If present, the attribute **R** will be used, otherwise the attributes **alpha** and **beta** will be used. All other attributes are ignored. Returns: elemdata0: Linear optics data at the entrance of the ring ringdata: Lattice properties elemdata: Linear optics at the points refered to by *refpts*, if refpts is :py:obj:`None` an empty lindata structure is returned. **elemdata** is a record array, Its fields depend on the selected method. See :ref:`linopt6 <linopt6_elemdata>`, :ref:`linopt4 <linopt4_elemdata>`, :ref:`linopt2 <linopt2_elemdata>` **ringdata** is a record array with fields: ================= ====== **tune** Fractional tunes **chromaticity** Chromaticities, only computed if *get_chrom* is :py:obj:`True` **damping_time** Damping times [s] (only if radiation is ON) ================= ====== Warning: The format of output record arrays depends on the selected method. See :py:func:`linopt2`, :py:func:`linopt4`, :py:func:`linopt6`. """ return method(ring, refpts=refpts, dp=dp, **kwargs)
# noinspection PyPep8Naming @check_6d(False) def linopt( ring: Lattice, dp: float = 0.0, refpts: Refpts = None, get_chrom: bool = False, **kwargs, ): """Linear analysis of a H/V coupled lattice (deprecated). Parameters: ring: lattice description. dp: momentum deviation. refpts: elements at which data is returned. It can be: 1) an integer in the range [-len(ring), len(ring)-1] selecting the element according to python indexing rules. As a special case, len(ring) is allowed and refers to the end of the last element, 2) an ordered list of such integers without duplicates, 3) a numpy array of booleans of maximum length len(ring)+1, where selected elements are :py:obj:`True`. Keyword Args: orbit avoids looking for the closed orbit if is already known ((6,) array) get_chrom=False compute chromaticities. Needs computing the tune at 2 different momentum deviations around the central one. get_w=False computes chromatic amplitude functions (W) [4]. Needs to compute the optics at 2 different momentum deviations around the central one. keep_lattice Assume no lattice change since the previous tracking. Defaults to :py:obj:`False` XYStep=1.0e-8 transverse step for numerical computation DPStep=1.0E-6 momentum deviation used for computation of chromaticities and dispersion coupled=True if :py:obj:`False`, simplify the calculations by assuming no H/V coupling twiss_in=None Initial conditions for transfer line optics. Record array as output by linopt, or dictionary. Keys: 'alpha' and 'beta' (mandatory) 'closed_orbit', (default 0) 'dispersion' (default 0) All other attributes are ignored. OUTPUT lindata0 linear optics data at the entrance of the ring tune [tune_A, tune_B], linear tunes for the two normal modes of linear motion [1] chrom [ksi_A , ksi_B], chromaticities ksi = d(nu)/(dP/P). Only computed if 'get_chrom' is :py:obj:`True` lindata linear optics at the points referred to by refpts, if refpts is None an empty lindata structure is returned. lindata is a record array with fields: idx element index in the ring s_pos longitudinal position [m] m44 (4, 4) transfer matrix M from the beginning of ring to the entrance of the element [2] closed_orbit (6,) closed orbit vector dispersion (4,) dispersion vector beta [betax, betay] vector alpha [alphax, alphay] vector mu [mux, muy], betatron phase Warning: if the sampling (number of refpts) is too sparse, i.e. if the phase advance is more than 2*pi between 2 points the integer part will not increment correclty W (2,) chromatic amplitude function (only if get_w==True) All values given at the entrance of each element specified in refpts. In case coupled == :py:obj:`True` additional outputs are available: gamma gamma parameter of the transformation to eigenmodes A (2, 2) matrix A in [3] B (2, 2) matrix B in [3] C (2, 2) matrix C in [3] Field values can be obtained with either lindata['idx'] or lindata.idx REFERENCES [1] D.Edwards,L.Teng IEEE Trans.Nucl.Sci. NS-20, No.3, p.885-888, 1973 [2] E.Courant, H.Snyder [3] D.Sagan, D.Rubin Phys.Rev.Spec.Top.-Accelerators and beams, vol.2 (1999) [4] Brian W. Montague Report LEP Note 165, CERN, 1979 :meta private: """ analyze = _analyze4 if kwargs.pop("coupled", True) else _analyze2 eld0, bd, eld = _linopt( ring, analyze, refpts, dp=dp, get_chrom=get_chrom, add0=(0,), adds=(get_uint32_index(ring, refpts),), addtype=[("idx", np.uint32)], mname="m44", **kwargs, ) # noinspection PyUnresolvedReferences return eld0, bd.tune, bd.chromaticity, eld # noinspection PyPep8Naming
[docs] def avlinopt(ring: Lattice, dp: float = 0.0, refpts: Refpts = None, **kwargs): r"""Linear analysis of a lattice with average values. :py:func:`avlinopt` returns average beta, mu, dispersion over the lattice elements. :py:func:`avlinopt` returns average beta, mu, dispersion over the lattice elements. :py:func:`avlinopt` acts on a single cell (*periodicity* is not taken into account). Parameters: ring: Lattice description. dp: Momentum deviation. refpts: Elements at which data is returned. It can be: 1. an integer in the range [-len(ring), len(ring)-1] selecting the element according to python indexing rules. As a special case, len(ring) is allowed and refers to the end of the last element, 2. an ordered list of such integers without duplicates, 3. a numpy array of booleans of maximum length len(ring)+1, where selected elements are :py:obj:`True`. Keyword Args: dct (float): Path lengthening. Defaults to :py:obj:`None` df (float): Deviation of RF frequency. Defaults to :py:obj:`None` orbit (Orbit): Avoids looking for the closed orbit if it is already known ((6,) array) get_chrom (bool): Compute chromaticities. Needs computing the tune at 2 different momentum deviations around the central one. get_w (bool): Computes chromatic amplitude functions (W) [4]_. Needs to compute the optics at 2 different momentum deviations around the central one. keep_lattice (bool): Assume no lattice change since the previous tracking. Defaults to :py:obj:`False` XYStep (float): Step size. Default: :py:data:`DConstant.XYStep <.DConstant>` DPStep (float): Momentum step size. Default: :py:data:`DConstant.DPStep <.DConstant>` twiss_in: Initial conditions for transfer line optics. Record array as output by :py:func:`.linopt6`, or dictionary. Keys: R or alpha, beta mandatory (2,) arrays closed_orbit Optional (6,) array, default 0 dispersion Optional (6,) array, default 0 If present, the attribute **R** will be used, otherwise the attributes **alpha** and **beta** will be used. All other attributes are ignored. Returns: elemdata: Linear optics at the points referred to by *refpts*, if refpts is :py:obj:`None` an empty lindata structure is returned. avebeta: Average beta functions [:math:`\hat{\beta_x},\hat{\beta_y}`] at *refpts* avemu: Average phase advances [:math:`\hat{\mu_x},\hat{\mu_y}`] at *refpts* avedisp: Average dispersion [:math:`\hat{\eta_x}, \hat{\eta'_x}, \hat{\eta_y}, \hat{\eta'_y}`] at *refpts* avespos: Average s position at *refpts* tune: [:math:`\nu_1,\nu_2`], linear tunes for the two normal modes of linear motion [1] chrom: [:math:`\xi_1,\xi_2`], chromaticities See also: :py:func:`linopt4`, :py:func:`get_optics` """ def get_strength(elem): try: k = elem.PolynomB[1] except (AttributeError, IndexError): k = 0.0 return k def get_sext_strength(elem): try: k = elem.PolynomB[2] except (AttributeError, IndexError): k = 0.0 return k def get_roll(elem): try: k = elem.R2[0][0] except (AttributeError, IndexError): k = 1.0 return k def get_dx(elem): try: k = (elem.T2[0] - elem.T1[0]) / 2.0 except (AttributeError, IndexError): k = 0.0 return k def get_bendingangle(elem): try: k = elem.BendingAngle except AttributeError: k = 0.0 return k def get_e1(elem): try: k = elem.EntranceAngle except AttributeError: k = 0.0 return k def get_fint(elem): try: k = elem.FringeInt1 except AttributeError: k = 0.0 return k def get_gap(elem): try: k = elem.FullGap except AttributeError: k = 0.0 return k def foctrigo(k2, lg): sqkl = np.sqrt(k2) * lg return np.sin(sqkl) / sqkl, 1.0 - np.cos(sqkl) def defoctrigo(k2, lg): sqkl = np.sqrt(-k2) * lg return np.sinh(sqkl) / sqkl, 1.0 - np.cosh(sqkl) def betadrift(beta0, alpha0, lg): gamma0 = (alpha0 * alpha0 + 1.0) / beta0 return beta0 - alpha0 * lg + gamma0 * lg * lg / 3 def betafoc(func, beta0, alpha0, k2, lg): gamma0 = (alpha0 * alpha0 + 1.0) / beta0 sini, cosi = func(k2, 2.0 * lg) return ( (beta0 + gamma0 / k2) + (beta0 - gamma0 / k2) * sini - cosi * alpha0 / k2 / lg ) / 2.0 def betalong(beta0, alpha0, k2, lg): r = np.empty_like(beta0) kp = k2 >= 1.0e-7 km = k2 <= -1.0e-7 k0 = np.logical_not(kp | km) r[kp] = betafoc(foctrigo, beta0[kp], alpha0[kp], k2[kp], lg[kp]) r[km] = betafoc(defoctrigo, beta0[km], alpha0[km], k2[km], lg[km]) r[k0] = betadrift(beta0[k0], alpha0[k0], lg[k0]) return r def dispfoc(func, disp0, ir, k2, lg): sini, cosi = func(k2, lg) eta = disp0[:, 0] * sini + disp0[:, 1] * cosi / k2 / lg + ir * (1.0 - sini) / k2 etap = disp0[:, 1] * sini - disp0[:, 0] * cosi / lg + ir * cosi / k2 / lg return np.stack((eta, etap), axis=1) def dispdrift(disp0, ir, lg): eta = disp0[:, 0] + disp0[:, 1] * lg / 2.0 + lg * lg * ir / 6.0 etap = disp0[:, 1] + lg * ir / 2.0 return np.stack((eta, etap), axis=1) def displong(disp0, ir, k2, lg): r = np.empty_like(disp0) kp = k2 >= 1.0e-7 km = k2 <= -1.0e-7 k0 = np.logical_not(kp | km) r[kp] = dispfoc(foctrigo, disp0[kp], ir[kp], k2[kp], lg[kp]) r[km] = dispfoc(defoctrigo, disp0[km], ir[km], k2[km], lg[km]) r[k0] = dispdrift(disp0[k0], ir[k0], lg[k0]) return r # selected list boolrefs = get_bool_index(ring, refpts) L = np.array([el.Length for el in ring[boolrefs[:-1]]]) longelem = boolrefs[:-1].copy() longelem[longelem] = L != 0.0 longi_refpts = np.append(longelem, [False]) longf_refpts = np.roll(longi_refpts, 1) all_refs = boolrefs | longf_refpts _, bd, d_all = get_optics(ring, refpts=all_refs, dp=dp, get_chrom=True, **kwargs) lindata = d_all[boolrefs[all_refs]] # Optics at entrance of selected elements avebeta = lindata.beta.copy() avemu = lindata.mu.copy() avedisp = lindata.dispersion.copy() aves = lindata.s_pos.copy() # Long elements b_long = longi_refpts[boolrefs] ring_long = ring[longelem] L = L[(L != 0.0)] di = d_all[longi_refpts[all_refs]] # Optics at entrance of long elements df = d_all[longf_refpts[all_refs]] # Optics at exit of long elements dx0 = (di.closed_orbit[:, 0] + df.closed_orbit[:, 0]) / 2 K = np.array([get_strength(el) for el in ring_long]) sext_strength = np.array([get_sext_strength(el) for el in ring_long]) roll = np.array([get_roll(el) for el in ring_long]) ba = np.array([get_bendingangle(el) for el in ring_long]) dx = np.array([get_dx(el) for el in ring_long]) irho = ba / L K = K * roll + 2 * sext_strength * (dx0 - dx) Kx = K + irho * irho Ky = -K Kxy = np.stack((Kx, Ky), axis=1) Lxy = np.stack((L, L), axis=1) # Hard edge model on dipoles bend = irho != 0.0 e1 = np.array([get_e1(el) for el in ring_long[bend]]) Fint = np.array([get_fint(el) for el in ring_long[bend]]) gap = np.array([get_gap(el) for el in ring_long[bend]]) d_csi = irho[bend] * gap * Fint * (1.0 + np.sin(e1) ** 2) / np.cos(e1) Cp = np.stack((irho[bend] * np.tan(e1), -irho[bend] * np.tan(e1 - d_csi)), axis=1) di.alpha[bend] -= di.beta[bend] * Cp di.dispersion[np.ix_(bend, [1, 3])] -= di.dispersion[np.ix_(bend, [0, 2])] * Cp avemu[b_long] = 0.5 * (di.mu + df.mu) aves[b_long] = 0.5 * (df.s_pos + di.s_pos) avebeta[b_long] = betalong(di.beta, di.alpha, Kxy, Lxy) avedx = displong(di.dispersion[:, :2], irho, Kx, L) avedy = displong(di.dispersion[:, 2:], np.zeros_like(irho), Ky, L) avedisp[b_long] = np.concatenate((avedx, avedy), axis=1) return lindata, avebeta, avemu, avedisp, aves, bd.tune, bd.chromaticity
[docs] @frequency_control def get_tune( ring: Lattice, *, method: str = "linopt", dp: float | None = None, dct: float | None = None, df: float | None = None, orbit: Orbit | None = None, **kwargs, ): r"""Computes the tunes using several available methods. :py:func:`get_tune` may use several methods depending on a *method* keyword. Parameters: ring: Lattice description method: ``'linopt'`` returns the tunes from the :py:func:`.linopt6` function, ``'fft'`` tracks a single particle and computes the tunes with fft, ``'interp_fft'`` tracks a single particle and computes the tunes with interpolated FFT. dp (float): Momentum deviation. dct (float): Path lengthening. df (float): Deviation of RF frequency. orbit (Orbit): Avoids looking for the closed orbit if it is already known ((6,) array) for the ``'fft'`` and ``'interp_fft'`` methods only: Keyword Args: nturns (int): Number of turns. Default: 512 amplitude (float): Amplitude of oscillation. Default: 1.E-6 remove_dc (bool): Remove the mean of oscillation data. Default: :py:obj:`True` num_harmonics (int): Number of harmonic components to compute (before mask applied, default: 20) fmin (float): Lower tune bound. Default: 0 fmax (float): Upper tune bound. Default: 1 hann (bool): Turn on Hanning window. Default: :py:obj:`False`. Work only for ``'fft'`` get_integer(bool): Turn on integer tune (slower) Returns: tunes (ndarray): array([:math:`\nu_x,\nu_y`]) """ # noinspection PyShadowingNames def gen_centroid(ring, ampl, nturns, remove_dc, ld): nv = ld.A.shape[0] p0 = ld.closed_orbit.copy() p0[0] += ampl p0[2] += ampl if nv >= 6: p0[4] += ampl p1 = np.squeeze(internal_lpass(ring, p0, nturns, len(ring))) if remove_dc: p1 -= np.mean(p1, axis=1, keepdims=True) p2 = solve(ld.A, p1[:nv, :]) return np.conjugate(np.asarray(p2, order="F").T.view(dtype=complex).T) get_integer = kwargs.pop("get_integer", False) if get_integer: assert method == "linopt", "Integer tune only accessible with method=linopt" if method == "linopt": if get_integer: _, _, c = get_optics( ring, refpts=range(len(ring) + 1), dp=dp, dct=dct, df=df, orbit=orbit ) tunes = c.mu[-1] / (2 * np.pi) * ring.periodicity else: tunes = _tunes(ring, dp=dp, dct=dct, df=df, orbit=orbit) tunes, _ = np.modf(tunes * ring.periodicity) else: nturns = kwargs.pop("nturns", 512) ampl = kwargs.pop("ampl", 1.0e-6) remove_dc = kwargs.pop("remove_dc", True) ld, _, _ = linopt6(ring, dp=dp, dct=dct, df=df, orbit=orbit) cents = gen_centroid(ring, ampl, nturns, remove_dc, ld) tunes = get_tunes_harmonic(cents, method=method, **kwargs) tunes, _ = np.modf(tunes * ring.periodicity) return tunes
[docs] @frequency_control def get_chrom( ring: Lattice, *, method: str = "linopt", dp: float | None = None, dct: float | None = None, df: float | None = None, cavpts: Refpts = None, **kwargs, ): r"""Computes the chromaticities using several available methods. :py:func:`get_tune` may use several methods depending on a *method* keyword. Parameters: ring: Lattice description. method: ``'linopt'`` returns the tunes from the :py:func:`linopt6` function, ``'fft'`` tracks a single particle and computes the tunes with :py:func:`~scipy.fftpack.fft`, ``'interp_fft'`` tracks a single particle and computes the tunes with interpolated FFT. dp (float): Momentum deviation. dct (float): Path lengthening. df (float): Deviation of RF frequency. cavpts: If :py:obj:`None`, look for ring.cavpts, or otherwise take all cavities. Keyword Args: DPStep (float): Momentum step for differentiation Default: :py:data:`DConstant.DPStep <.DConstant>` for the ``'fft'`` and ``'interp_fft'`` methods only: Keyword Args: nturns (int): Number of turns. Default: 512 amplitude (float): Amplitude of oscillation. Default: 1.E-6 remove_dc (bool): Remove the mean of oscillation data. Default: :py:obj:`True` num_harmonics (int):Number of harmonic components to compute (before mask applied, default: 20) fmin (float): Lower tune bound. Default: 0 fmax (float): Upper tune bound. Default: 1 hann (bool): Turn on Hanning window. Default: :py:obj:`False`, Work only for ``'fft'`` Returns: chromaticities (ndarray): array([:math:`\xi_x,\xi_y`]) """ dp_step = kwargs.pop("DPStep", DConstant.DPStep) if method == "fft": print("Warning fft method not accurate to get the chromaticity") if ring.is_6d: dff = dp_step * ring.disable_6d(copy=True).slip_factor cavities = get_bool_index(ring, RFCavity) freqs = get_value_refpts(ring, cavities, "Frequency") rgup = set_value_refpts( ring, cavities, "Frequency", freqs * (1.0 + 0.5 * dff), copy=True ) rgdn = set_value_refpts( ring, cavities, "Frequency", freqs * (1.0 - 0.5 * dff), copy=True ) o0up, _ = find_orbit6(rgup, **kwargs) tune_up = get_tune(rgup, method=method, orbit=o0up, **kwargs) o0dn, _ = find_orbit6(rgdn, **kwargs) tune_down = get_tune(rgdn, method=method, orbit=o0dn, **kwargs) dp_step = o0up[4] - o0dn[4] else: if dct is not None or df is not None: dp = find_orbit4(ring, dct=dct, df=df)[0][4] elif dp is None: dp = 0.0 tune_up = get_tune(ring, method=method, dp=dp + 0.5 * dp_step, **kwargs) tune_down = get_tune(ring, method=method, dp=dp - 0.5 * dp_step, **kwargs) return (tune_up - tune_down) / dp_step
Lattice.linopt = linopt Lattice.linopt2 = linopt2 Lattice.linopt4 = linopt4 Lattice.linopt6 = linopt6 Lattice.get_optics = get_optics Lattice.avlinopt = avlinopt Lattice.get_tune = get_tune Lattice.get_chrom = get_chrom