Source code for at.lattice.geometry

"""Lattice geometry"""

from __future__ import annotations

__all__ = ["get_geometry"]

from collections.abc import Sequence
from math import sin, cos, atan2, sqrt

import numpy as np

from .elements import Element, Dipole
from .utils import Refpts, All, refpts_count, get_bool_index

_GEOMETRY_EPSIL = 1.0e-3

_GEOMETRY_DTYPE = [
    ("x", np.float64),
    ("y", np.float64),
    ("z", np.float64),
    ("angle", np.float64),
    ("v_angle", np.float64),
]


def _get_geometry2(
    ring: list[Element],
    refpts: Refpts = All,
    start_coordinates: tuple[float, float, float] = (0, 0, 0),
    centered: bool = False,
    regex: bool = False,
):
    # noinspection PyShadowingNames
    r"""Compute the 2D ring geometry in cartesian coordinates

    Parameters:
        ring:               Lattice description.
        refpts:     Element selection key.
          See ":ref:`Selecting elements in a lattice <refpts>`"
        start_coordinates:  *x*, *y*, *angle* at starting point. *x*
          and *y* are ignored if *centered* is :py:obj:`True`.
        centered:           if :py:obj:`True` the coordinates origin is the
          centre of the ring.
        regex: Use regular expression for *refpts* string matching instead of
          Unix shell-style wildcards.

    Returns:
        geomdata:           recarray containing, x, y, angle.
        radius:             machine radius at the beginning of the lattice.

            .. attention::
               This radius is different from the radius usually defined as
               :math:`C/2\pi`

    Example:

       >>> geomdata, radius = get_geometry(ring)
    """

    geom_dtype = [("x", np.float64), ("y", np.float64), ("angle", np.float64)]
    boolrefs = get_bool_index(ring, refpts, endpoint=True, regex=regex)
    nrefs = refpts_count(boolrefs, len(ring))
    geomdata = np.recarray((nrefs,), dtype=geom_dtype)
    xx = np.zeros(len(ring) + 1)
    yy = np.zeros(len(ring) + 1)
    angle = np.zeros(len(ring) + 1)
    x0, y0, t0 = start_coordinates
    x, y = 0.0, 0.0
    t = t0

    xx[0] = x
    yy[0] = y
    angle[0] = t
    for ind, el in enumerate(ring):
        ll = el.Length
        if isinstance(el, Dipole) and el.BendingAngle != 0:
            ang = 0.5 * el.BendingAngle
            ll *= np.sin(ang) / ang
        else:
            ang = 0.0
        t -= ang
        x += ll * np.cos(t)
        y += ll * np.sin(t)
        t -= ang
        xx[ind + 1] = x
        yy[ind + 1] = y
        angle[ind + 1] = t

    dff = (t + _GEOMETRY_EPSIL) % (2.0 * np.pi) - _GEOMETRY_EPSIL
    if abs(dff) < _GEOMETRY_EPSIL:
        xcenter = np.mean(xx)
        ycenter = np.mean(yy)
    elif abs(dff - np.pi) < _GEOMETRY_EPSIL:
        xcenter = 0.5 * x
        ycenter = 0.5 * y
    else:
        num = np.cos(t) * x + np.sin(t) * y
        den = np.sin(t - t0)
        xcenter = -num * np.sin(t0) / den
        ycenter = num * np.cos(t0) / den
    radius = np.sqrt(xcenter * xcenter + ycenter * ycenter)
    if centered:
        xx -= xcenter
        yy -= ycenter
    else:
        xx += x0
        yy += y0
    geomdata["x"] = xx[boolrefs]
    geomdata["y"] = yy[boolrefs]
    geomdata["angle"] = angle[boolrefs]
    return geomdata, radius


[docs] def get_geometry( ring: Sequence[Element], refpts: Refpts = All, start_coordinates: tuple[float, float, float] = (0.0, 0.0, 0.0), h_angle: float = 0.0, v_angle: float = 0.0, centred: bool = False, **kwargs, ): # noinspection PyShadowingNames r"""Compute the 3D ring geometry in cartesian coordinates Parameters: ring: Lattice description. refpts: Element selection key. See ":ref:`Selecting elements in a lattice <refpts>`" start_coordinates: *x*, *y*, *z* at starting point. *x* and *y* are ignored if *centred* is :py:obj:`True`. h_angle: initial horizontal angle. v_angle: initial vertical angle. centred: if :py:obj:`True` the coordinates origin is the centre of the ring. Returns: geomdata: :py:class:`~numpy.recarray` containing the fields *x*, *y*, *z*, *angle*, *v_angle*. radius: machine radius at the beginning of the lattice. .. attention:: This radius is different from the radius usually defined as :math:`C/2\pi` Example: >>> geomdata, _ = get_geometry(ring) >>> xcoord = geomdata.x """ def get_centre(xx, yy, tt): dff = (tt[-1] - tt[0] + _GEOMETRY_EPSIL) % (2.0 * np.pi) - _GEOMETRY_EPSIL if abs(dff) < _GEOMETRY_EPSIL: # Total angle is 2*pi: full ring xcentre = np.mean(xx) ycentre = np.mean(yy) elif abs(dff - np.pi) < _GEOMETRY_EPSIL: # Total angle is pi: half ring xcentre = 0.5 * xx[-1] ycentre = 0.5 * yy[-1] else: c1 = np.cos(tt[0]) s1 = np.sin(tt[0]) c2 = np.cos(tt[-1]) s2 = np.sin(tt[-1]) den = s2 * c1 - s1 * c2 a1 = xx[0] * c1 + yy[0] * s1 a2 = xx[-1] * c2 + yy[-1] * s2 xcentre = (a1 * s2 - a2 * s1) / den ycentre = (a2 * c1 - a1 * c2) / den return xcentre, ycentre def rots(rotmat): cns = rotmat[0, 0] sns = rotmat[0, 2] return np.array([[1.0, 0.0, 0.0], [0, cns, -sns], [0.0, sns, cns]]) def hkick(ang: float): """positive: to the left""" cns = cos(ang) sns = sin(ang) return np.array([[cns, -sns, 0.0], [sns, cns, 0.0], [0.0, 0.0, 1.0]]) def vkick(ang: float): """positive: upward""" cns = cos(ang) sns = sin(ang) return np.array([[cns, 0.0, -sns], [0.0, 1.0, 0.0], [sns, 0.0, cns]]) def increment(xyz, conv, elem): """Propagation across one element""" length = elem.Length if hasattr(elem, "R1"): # inplace matrix multiplication requires numpy >= 1.25 # conv @= rots(elem.R1) ctemp = conv.copy() np.matmul(ctemp, rots(elem.R1), out=conv) if hasattr(elem, "BendingAngle") and elem.BendingAngle != 0.0: ang = 0.5 * elem.BendingAngle rm = hkick(-ang) # conv @= rm ctemp = conv @ rm xyz += ctemp @ np.array([length / ang * sin(ang), 0.0, 0.0]) # conv @= rm np.matmul(ctemp, rm, out=conv) else: xyz += conv @ np.array([length, 0.0, 0.0]) if hasattr(elem, "R2"): # conv @= rots(elem.R2) ctemp = conv.copy() np.matmul(ctemp, rots(elem.R2), out=conv) hangle = atan2(conv[1, 0], conv[0, 0]) vangle = atan2(conv[2, 0], sqrt(conv[0, 0] ** 2 + conv[1, 0] ** 2)) return tuple(xyz) + (hangle, vangle) # Accept the US wording "centered" for compatibility centred = kwargs.pop("centered", centred) boolrefs = get_bool_index(ring, refpts, endpoint=True) x0, y0, z0 = start_coordinates xyzc = np.array(start_coordinates, dtype=np.float64) coord0 = start_coordinates + (h_angle, v_angle) convi = hkick(h_angle) @ vkick(v_angle) coords = [coord0] + [increment(xyzc, convi, el) for el in ring] geomdata = np.rec.array(coords, dtype=_GEOMETRY_DTYPE) xc, yc = get_centre(geomdata.x, geomdata.y, geomdata.angle) radius = np.sqrt((xc - x0) ** 2 + (yc - y0) ** 2) if centred: geomdata.x -= xc geomdata.y -= yc return geomdata[boolrefs], radius