"""
Functions relating to particle generation
"""
import numpy as np
from numpy.linalg import cholesky, LinAlgError, det
from scipy.linalg import block_diag, eig
from warnings import warn
from ..lattice import AtError, AtWarning, Lattice, Orbit, random
# from ..physics.amat import jmat
__all__ = ["beam", "sigma_matrix", "emittances_from_beam"]
_j2 = np.array([[0.0, 1.0], [-1.0, 0.0]])
_jmat = block_diag(_j2, _j2, _j2)
# noinspection PyPep8Naming
[docs]
def sigma_matrix(ring: Lattice = None, **kwargs):
r"""
Calculate the correlation matrix :math:`\Sigma` to be used for particle
generation
Parameters:
ring: Lattice object or list of twiss parameters.
Keyword Args:
beam: Particle coordinates
twiss_in: Data structure containing input twiss parameters.
betax (float): Input horizontal beta function [m]
alphax (float): Input horizontal alpha function [m]
emitx (float): Horizontal emittance [m.rad]
betay (float): Input vertical beta function [m]
alphay (float): Input vertical alpha function [m]
emity (float): Vertical emittance [m.rad]
blength (float): One sigma bunch length [m]
espread (float): One sigma energy spread [dp/p]
Returns:
sigma_matrix: (6, 6) :math:`\Sigma`-matrix
*beam* is a (6, nparticles) coordinates array of a particle population.
If provided, all other parameters are ignored.
If *twiss_in* is provided, its *R* or *alpha* and *beta* fields are used,
*emitx* and *emity* must be provided.
If *ring* is provided:
* for a 6d lattice, :py:func:`.ohmi_envelope` is called to compute the
equilibrium emittances. *emitx*, *emity* may overload these values.
* for a 4d lattice, *emitx*, *emity* must be provided.
Otherwise, *alphax*, *betax*, *emitx*, *alphay*, *betay* and *emity*
must be provided. The result is an uncoupled :math:`\Sigma`-matrix
If *espread* and *blength* are not specified, the results is a
monochromatic beam.
"""
def require(mess, *keys):
miss = [key for key in keys if key not in kwargs]
if len(miss) == 0:
return [kwargs.pop(key) for key in keys]
else:
raise AtError("{0}: missing {1}".format(mess, ", ".join(miss)))
def ignore(mess):
ok = kwargs.keys()
if any(ok):
warn(AtWarning("{0}: {1} ignored".format(mess, ", ".join(ok))))
def r_s():
rs = np.zeros((2, 2))
if any(("espread" in kwargs, "blength" in kwargs)):
blength, espread = require(
'Both "blength" and "espread" are required', "blength", "espread"
)
ems = blength * espread
if ems > 0.0:
rs = np.array([[espread / blength, 0], [0, blength / espread]])
else:
ems = None
return ems, rs
def r_4d(ax, ay, bx, by, rs):
r6 = np.zeros((3, 6, 6))
r6[0, :2, :2] = np.array([[bx, -ax], [-ax, (1 + ax**2) / bx]])
r6[1, 2:4, 2:4] = np.array([[by, -ay], [-ay, (1 + ay**2) / by]])
r6[2, 4:, 4:] = rs
return r6
def r_expand(rmat, rs):
if rmat.shape[0] < 3:
r6 = np.zeros((3, 6, 6))
r6[:2, :4, :4] = rmat
r6[2, 4:, 4:] = rs
else:
r6 = rmat
return r6
def sigma_from_rmat(rmat, emit, embl):
if emit is None:
emx, emy = require("Emittances must be provided", "emitx", "emity")
ems = embl
else:
emx = kwargs.pop("emitx", emit[0])
emy = kwargs.pop("emity", emit[1])
ems = emit[2] if embl is None else embl
sigm = emx * rmat[0, :, :] + emy * rmat[1, :, :]
if ems is None:
warn(AtWarning("Monochromatic beam: no energy spread"))
else:
sigm += ems * rmat[2, :, :]
return sigm
if ring is not None:
kwargs["ring"] = ring
if "beam" in kwargs:
message = "'beam' is provided"
beam = kwargs.pop("beam")
orbit = kwargs.pop("orbit", None)
if orbit is None:
orbit = np.mean(beam, axis=1)
beam = beam - orbit.reshape((6, 1))
sigmat = (beam @ beam.T) / beam.shape[1]
elif "twiss_in" in kwargs:
twin = kwargs.pop("twiss_in")
message = "'twiss_in' is provided"
em56, r56 = r_s()
try:
Rin = twin["R"]
except (ValueError, KeyError): # record arrays throw ValueError !
Rm = r_4d(
twin["alpha"][0],
twin["alpha"][1],
twin["beta"][0],
twin["beta"][1],
r56,
)
else:
Rm = r_expand(Rin, r56)
sigmat = sigma_from_rmat(Rm, None, em56)
elif "ring" in kwargs:
ring = kwargs.pop("ring")
message = "'ring' is provided"
em56, r56 = r_s()
el0, _, _ = ring.linopt6(orbit=kwargs.pop("orbit", None))
orbit = el0.closed_orbit
Rm = r_expand(el0["R"], r56)
if ring.is_6d:
_, beam0, _ = ring.ohmi_envelope(orbit=orbit)
sigmat = sigma_from_rmat(Rm, beam0.mode_emittances, em56)
else:
sigmat = sigma_from_rmat(Rm, None, em56)
else:
message = "Uncoupled beam"
alphax, alphay, betax, betay = require(
'Neither "twiss_in" nor "ring" is provided',
"alphax",
"alphay",
"betax",
"betay",
)
# orbit = kwargs.pop('orbit', np.zeros(6))
em56, r56 = r_s()
Rm = r_4d(alphax, alphay, betax, betay, r56)
sigmat = sigma_from_rmat(Rm, None, em56)
ignore(message)
return sigmat
[docs]
def beam(nparts: int, sigma, orbit: Orbit = None):
r"""
Generates an array of random particles according to the given
:math:`\Sigma`-matrix
Parameters:
nparts: Number of particles
sigma: :math:`\Sigma`-matrix as calculated by
:py:func:`sigma_matrix`
orbit: An orbit can be provided to give a center of
mass offset to the distribution
Returns:
particle_dist: a (6, *nparts*) matrix of coordinates
"""
def _get_single_plane(slc):
try:
return cholesky(sigma[slc, slc])
except LinAlgError:
return np.zeros((2, 2))
v = random.thread.normal(size=(sigma.shape[1], nparts))
try:
# Try full 6x6 matrix
lmat = cholesky(sigma)
except LinAlgError:
lmat = np.zeros((6, 6))
try:
# Try x-y 4x4 matrix
sel = range(4)
idx = np.ix_(sel, sel)
lmat[idx] = cholesky(sigma[idx])
lmat[4:, 4:] = _get_single_plane(slice(4, 6))
except LinAlgError:
try:
# Try x-delta 4x4 matrix
sel = [0, 1, 4, 5]
idx = np.ix_(sel, sel)
lmat[idx] = cholesky(sigma[idx])
lmat[2:4, 2:4] = _get_single_plane(slice(2, 4))
except LinAlgError:
# Uncoupled
lmat[:2, :2] = _get_single_plane(slice(2))
lmat[2:4, 2:4] = _get_single_plane(slice(2, 4))
lmat[4:, 4:] = _get_single_plane(slice(4, 6))
particle_dist = np.asfortranarray(lmat @ v)
if orbit is not None:
particle_dist += orbit.reshape((6, 1))
return particle_dist
[docs]
def emittances_from_beam(beam=None, sigma_mat=None):
r"""
Computes the eigen and projected emittances from a particle distribution
or a :math:`\Sigma`-matrix
Parameters:
beam: particle distribution. ``6xn`` array of ``n`` particles
sigma_mat: :math:`\Sigma`-matrix as calculated by :py:func:`sigma_matrix`
Returns:
eig_emittances: a (3,) array of eigen emitances
proj_emittances: a (3,) array of projected emitances
"""
if beam is None:
if sigma_mat is None:
msg = "emittance calculation requires either beam or sigma_matrix"
raise AtError(msg)
else:
if sigma_mat is not None:
msg = "beam provided, sigma_matrix ignored in emittance calculation"
warn(AtWarning(msg))
sigma_mat = sigma_matrix(beam=beam)
eig_emittances, _ = eig(sigma_mat @ _jmat)
eig_emittances = np.unique(abs(eig_emittances))
submat = [slice(0, 2), slice(2, 4), slice(6, 3, -1)]
proj_emittances = np.sqrt([det(sigma_mat[s, s]) for s in submat])
# Trick to reorder eigen-emittances
idx = [np.argmin(abs(proj_emittances - em)) for em in eig_emittances]
eig_emittances = eig_emittances[idx]
return eig_emittances, proj_emittances