# noinspection PyUnresolvedReferences
r"""Using `MAD-X`_ files with PyAT
==================================
PyAT can read lattice descriptions in Mad-X format (.seq files), and can export
lattices in MAD-X format.
However, because of intrinsic differences between PyAT and MAD-X, some
incompatibilities must be taken into account.
1. Translation losses
---------------------
6-D motion
^^^^^^^^^^^^^^
While AT allows setting 6-D motion and synchrotron radiation on individual elements,
MAD has a global setting for the whole lattice. When reading a MAD-X lattice without
radiation, 6-D motion in the resulting AT lattice is turned off, including in RF
cavities. If the MAD-X lattice has radiation on, 6-D motion is activated on the AT
lattice according to default settings (RF cavities active, radiation in dipoles,
quadrupoles and wigglers).
Combined function magnets
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
MAD has little support for combined function magnets. When exporting a lattice in MAD
format, the main field component for each magnet class in kept but other components
disappear, except in the few cases handled by MAD (quadrupole and sextupole
components in ``SBEND`` or ``RBEND``, skew quad component in ``QUADRUPOLE``, see the
MAD user's reference manual for more).
Multipoles
^^^^^^^^^^^^^^
MAD has no thick multipoles. Multipoles in MAD format (``MULTIPOLE`` element) are
interpreted as :py:class:`.ThinMultipole` elements. In the other direction, an AT
:py:class:`.Multipole` is converted to a thin ``MULTIPOLE`` surrounded by two
drift spaces.
MAD elements absent from AT
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Many MAD elements have no equivalent in AT. They are replaced by
:py:class:`.Marker` or :py:class:`.Drift` elements, depending on their length.
Incompatible attributes
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Some AT element attributes have no MAD equivalent, and vice versa.
When exporting to a MAD-X file:
- `NumIntSteps`, `MaxOrder` are discarded,
- `FringeBendEntrance`, `FringeBendExit`, `FringeQuadEntrance`, `FringeQuadExit` are
discarded,
- `R1`, `R2`, `T1`, `T2` are discarded,
- `RApertures`, `EApertures` are discarded.
When reading a MAD-X file:
- `TILT` is interpreted and converted to `R1` and `R2` attributes,
- `KTAP` is interpreted and converted to `FieldScaling`.
.. _using-mad-x-files:
2. Reading MAD-X files
----------------------
Short way
^^^^^^^^^
The :py:func:`load_madx` function created a :py:class:`.Lattice` from a ``LINE`` or
``SEQUENCE`` in a MAD-X file.
>>> ring = at.load_madx("lattice.seq", use="PS")
>>> print(ring)
Lattice(<1295 elements>, name='PS', energy=1000000000.0, particle=Particle('positron'),
periodicity=1, harmonic_number=0, beam_current=0.0, nbunch=1, use='PS')
Detailed way
^^^^^^^^^^^^
:py:class:`MadxParser` creates an empty database which can be populated with the
elements of a MAD-X file.
>>> parser = at.MadxParser()
The :py:meth:`MadxParser.parse_files` method reads one or several MAD-X files and
populates the parser
>>> parser.parse_files("lattice.seq", use="PS")
The parser can be examined and modified using the standard python syntax:
>>> parser["pr_bht91_f"]
sbend(name=PR.BHT91.F, l=2.1975925, angle='angle.prbhf', k1='k1prbhf+k1prpfwf-k1prf8l')
>>> parser["angle.prbhf"]
0.03135884818
>>> parser["angle.prbhf"] = 0.032
All MAD parameters can be interactively modified and their last value will be taken
into account when generating a PyAT lattice.
The :py:meth:`MadxParser.lattice` method creates a :py:class:`.Lattice` from a ``LINE``
or ``SEQUENCE`` of the parser:
>>> ring = parser.lattice(use="ps")
>>> print(ring)
Lattice(<1295 elements>, name='PS', energy=1000000000.0, particle=Particle('positron'),
periodicity=1, harmonic_number=0, beam_current=0.0, nbunch=1, use='PS')
3. Exporting to MAD-X files
---------------------------
Exporting a PyAT lattice to a MAD-X files produces a single MAD ``SEQUENCE`` of
``LINE``.
See :py:func:`save_madx` for usage.
4. Functions and classes
------------------------
.. _mad-x: https://mad.web.cern.ch/mad/webguide/manual.html
"""
from __future__ import annotations
__all__ = ["MadParameter", "MadxParser", "load_madx", "save_madx"]
import functools
import warnings
# functions known by MAD-X
from math import pi, e, sqrt, exp, log, log10, sin, cos, tan # noqa: F401
from math import asin, acos, atan, sinh, cosh, tanh, erf, erfc # noqa: F401
from itertools import chain
from collections.abc import Sequence, Generator, Iterable
import re
import numpy as np
# constants known by MAD-X
from scipy.constants import c as clight, hbar as _hb, e as qelect
from scipy.constants import physical_constants as _cst
from .allfiles import register_format
from .utils import split_ignoring_parentheses, protect, restore
from .file_input import AnyDescr, ElementDescr, SequenceDescr, BaseParser
from .file_input import LowerCaseParser, UnorderedParser
from .file_input import set_argparser, ignore_names
from .file_output import Exporter
from ..lattice import Lattice, Particle, elements as elt, tilt_elem
_separator = re.compile(r"(?<=[\w.)])\s+(?=[\w.(])")
# Constants known by MAD-X
true = True
false = False
twopi = 2 * pi
degrad = 180.0 / pi
raddeg = pi / 180.0
emass = 1.0e-03 * _cst["electron mass energy equivalent in MeV"][0] # [GeV]
pmass = 1.0e-03 * _cst["proton mass energy equivalent in MeV"][0] # [GeV]
nmass = 1.0e-03 * _cst["neutron mass energy equivalent in MeV"][0] # [GeV]
umass = 1.0e-03 * _cst["atomic mass constant energy equivalent in MeV"][0] # [GeV]
mumass = 1.0e-03 * _cst["muon mass energy equivalent in MeV"][0] # [GeV]
hbar = _hb / qelect * 1.0e-09 # [GeV.s]
erad = _cst["classical electron radius"][0] # [m]
prad = erad * emass / pmass # [m]
[docs]
class MadParameter(str):
"""MAD parameter
A MAD parameter is an expression which can be evaluated n the context
of a MAD parser
"""
def __new__(cls, parser, expr):
return super().__new__(cls, expr)
# noinspection PyUnusedLocal
def __init__(self, parser: _MadParser, expr: str):
"""Args:
parser: MadParser instance defining the context for evaluation
expr: expression to be evaluated
The expression may contain MAD parameter names, arithmetic operators and
mathematical functions known by MAD
"""
self.parser = parser
def __float__(self):
return float(self.parser.evaluate(self))
def __int__(self):
return int(self.parser.evaluate(self))
def __add__(self, other):
return float(self) + float(other)
def __radd__(self, other):
return float(other) + float(self)
def __mul__(self, other):
return float(self) * float(other)
def __rmul__(self, other):
return float(other) * float(self)
def __sub__(self, other):
return float(self) - float(other)
def __rsub__(self, other):
return float(other) - float(self)
def __truediv__(self, other):
return float(self) / float(other)
def __rtruediv__(self, other):
return float(other) / float(self)
def __pow__(self, other):
return pow(float(self), other)
def __rpow__(self, other):
return pow(float(other), float(self))
def __neg__(self):
return -float(self)
def __pos__(self):
return +float(self)
[docs]
def evaluate(self):
return self.parser.evaluate(self)
def sinc(x: float) -> float:
"""Cardinal sine function known by MAD-X"""
if abs(x) < 1e-10:
return x
else:
return sin(x) / x
# -------------------
# Utility functions
# -------------------
def mad_element(func):
"""Decorator for AT elements"""
@functools.wraps(func)
def wrapper(self, *args, tilt=0.0, ktap=0.0, **kwargs):
elems = func(self, *args, **kwargs)
for el in elems:
tilt = float(tilt) # MadParameter conversion
if tilt != 0.0:
tilt_elem(el, tilt)
ktap = float(ktap) # MadParameter conversion
if ktap != 0.0:
el.Scaling = 1.0 + ktap
el.origin = self.origin
return elems
return wrapper
def poly_to_mad(x: Iterable[float], factor: float = 1.0) -> Generator[float]:
"""Convert polynomials from AT to MAD"""
f = 1.0
for n, vx in enumerate(x):
yield factor * float(vx * f)
f *= n + 1
def poly_from_mad(x: Iterable[float], factor: float = 1.0) -> Generator[float]:
"""Convert polynomials from MAD to AT"""
f = 1.0
for n, vx in enumerate(x):
yield factor * float(vx / f)
f *= n + 1
def p_to_at(a: float | Sequence[float]) -> np.ndarray:
"""Convert polynomials from MADX to AT"""
if not isinstance(a, Sequence):
# In case of a single element, we have a scalar instead of a tuple
a = (a,)
return np.fromiter(poly_from_mad(a), dtype=float)
def p_dict(keys: Iterable[str], a: Iterable[float]) -> dict[str, float]:
"""return K1, K2... from an AT Polynom"""
return {k: v for k, v in zip(keys, poly_to_mad(a)) if k and (v != 0.0)}
def p_list(a: Iterable[float], factor: float = 1.0):
"""Return a Polynom list"""
return list(poly_to_mad(a, factor=factor))
# noinspection PyUnusedLocal
def _keyparser(parser, argcount, argstr):
"""Return the pair key, value for the given 'key' argument"""
return argstr, parser.evaluate(argstr)
# ------------------------------
# Base class for MAD-X elements
# ------------------------------
class _MadElement(ElementDescr):
"""Description of MADX elements"""
str_attr = {"apertype", "refer", "refpos", "sequence", "from"}
def __init__(self, *args, at=0.0, **kwargs):
self.at = at
# Cannot use "from" as argument or attribute
setattr(self, "from", kwargs.pop("from", None))
# kwargs.pop("copy", False)
super().__init__(*args, **kwargs)
def limits(self, parser: MadxParser, offset: float, refer: float):
half_length = 0.5 * self.length
offset = offset + refer * half_length + self.at
frm = getattr(self, "from")
if frm is not None:
offset += parser[frm].at
return np.array([-half_length, half_length]) + offset
def meval(self, params: dict):
"""Evaluation of superfluous parameters"""
def mpeval(v):
if isinstance(v, MadParameter):
return v.evaluate()
elif isinstance(v, str):
return v
elif isinstance(v, Sequence):
return tuple(mpeval(a) for a in v)
else:
return v
# return {k: mpeval(v) for k, v in params.items()}
return {}
# ------------------------------
# MAD-X element classes
# ------------------------------
# noinspection PyPep8Naming
class drift(_MadElement):
@mad_element
def to_at(self, l=0.0, **params): # noqa: E741
return [elt.Drift(self.name, l, **self.meval(params))]
# noinspection PyPep8Naming
class marker(_MadElement):
at2mad = {}
@mad_element
def to_at(self, **params):
return [elt.Marker(self.name, **self.meval(params))]
# noinspection PyPep8Naming
class quadrupole(_MadElement):
@mad_element
def to_at(self, l, k1=0.0, k1s=0.0, **params): # noqa: E741
atparams = {}
k1s = float(k1s) # MadParameter conversion # MadParameter conversion
if k1s != 0.0:
atparams["PolynomA"] = [0.0, k1s]
return [elt.Quadrupole(self.name, l, k1, **atparams, **self.meval(params))]
@classmethod
def from_at(cls, kwargs):
el = super().from_at(kwargs)
el.update(p_dict(["K0", "K1"], kwargs.pop("PolynomB", ())))
el.update(p_dict(["K0S", "K1S"], kwargs.pop("PolynomA", ())))
return el
# noinspection PyPep8Naming
class sextupole(_MadElement):
@mad_element
def to_at(self, l, k2=0.0, k2s=0.0, **params): # noqa: E741
atparams = {}
k2s = float(k2s) # MadParameter conversion
if k2s != 0.0:
atparams["PolynomA"] = [0.0, 0.0, k2s / 2.0]
return [elt.Sextupole(self.name, l, k2 / 2.0, **atparams, **self.meval(params))]
@classmethod
def from_at(cls, kwargs):
el = super().from_at(kwargs)
el.update(p_dict(["K0", "K1", "K2"], kwargs.pop("PolynomB", ())))
el.update(p_dict(["K0S", "K1S", "K2S"], kwargs.pop("PolynomA", ())))
return el
# noinspection PyPep8Naming
class octupole(_MadElement):
@mad_element
def to_at(self, l, k3=0.0, k3s=0.0, **params): # noqa: E741
poly_b = [0.0, 0.0, 0.0, k3 / 6.0]
poly_a = [0.0, 0.0, 0.0, k3s / 6.0]
return [elt.Octupole(self.name, l, poly_a, poly_b, **self.meval(params))]
@classmethod
def from_at(cls, kwargs):
el = super().from_at(kwargs)
el.update(p_dict(["K0", "K1", "K2", "K3"], kwargs.pop("PolynomB", ())))
el.update(p_dict(["K0S", "K1S", "K2S", "K3S"], kwargs.pop("PolynomA", ())))
return el
# noinspection PyPep8Naming
class multipole(_MadElement):
at2mad = {}
@mad_element
def to_at(self, knl=(), ksl=(), **params):
params.pop("l", None)
return [
elt.ThinMultipole(
self.name, p_to_at(ksl), p_to_at(knl), **self.meval(params)
)
]
@classmethod
def from_at(cls, kwargs, factor=1.0):
el = super().from_at(kwargs)
el["KNL"] = p_list(kwargs.pop("PolynomB", ()), factor=factor)
el["KSL"] = p_list(kwargs.pop("PolynomA", ()), factor=factor)
return el
# noinspection PyPep8Naming
class sbend(_MadElement):
at2mad = {
"Length": "L",
"BendingAngle": "ANGLE",
"EntranceAngle": "E1",
"ExitAngle": "E2",
}
@mad_element
def to_at(
self,
l, # noqa: E741
angle,
e1=0.0,
e2=0.0,
k1=0.0,
k2=0.0,
k1s=0.0,
hgap=None,
fint=0.0,
**params,
):
atparams = {}
if hgap is not None:
fintx = params.pop("fintx", fint)
atparams.update(FullGap=2.0 * hgap, FringeInt1=fint, FringeInt2=fintx)
k2 = float(k2) # MadParameter conversion
if k2 != 0.0:
atparams["PolynomB"] = [0.0, k1, k2 / 2.0]
k1s = float(k1s) # MadParameter conversion
if k1s != 0.0:
atparams["PolynomA"] = [0.0, k1s]
return [
elt.Dipole(
self.name,
l,
angle,
k1,
EntranceAngle=e1,
ExitAngle=e2,
**atparams,
**self.meval(params),
)
]
@classmethod
def from_at(cls, kwargs):
el = super().from_at(kwargs)
el.update(p_dict(["K0", "K1", "K2"], kwargs.pop("PolynomB", ())))
el.update(p_dict(["K0S", "K1S", "K2S"], kwargs.pop("PolynomA", ())))
return el
# noinspection PyPep8Naming
class rbend(sbend):
@mad_element
def to_at(self, l, angle, e1=0.0, e2=0.0, **params): # noqa: E741
hangle = abs(0.5 * angle)
arclength = l / sinc(hangle)
return super().to_at(arclength, angle, e1=hangle + e1, e2=hangle + e2, **params)
@property
def length(self):
"""Element length"""
hangle = 0.5 * self["angle"]
return self["l"] / sinc(hangle)
# noinspection PyPep8Naming
class kicker(_MadElement):
@mad_element
def to_at(self, l=0.0, hkick=0.0, vkick=0.0, **params): # noqa: E741
kicks = np.array([hkick, vkick], dtype=float)
return [elt.Corrector(self.name, l, kicks, **self.meval(params))]
@classmethod
def from_at(cls, kwargs):
el = super().from_at(kwargs)
kicks = kwargs.pop("KickAngle", (0.0, 0.0))
el["HKICK"] = kicks[0]
el["VKICK"] = kicks[1]
return el
# noinspection PyPep8Naming
class hkicker(kicker):
@mad_element
def to_at(self, l=0.0, kick=0.0, **params): # noqa: E741
return super().to_at(l=l, hkick=kick, **params)
# noinspection PyPep8Naming
class vkicker(kicker):
@mad_element
def to_at(self, l=0.0, kick=0.0, **params): # noqa: E741
return super().to_at(l=l, vkick=kick, **params)
# noinspection PyPep8Naming
class rfcavity(_MadElement):
@mad_element
def to_at(
self,
l=0.0, # noqa: E741
volt=0.0,
freq=np.nan,
lag=0.0,
harmon=0,
**params,
):
cavity = elt.RFCavity(
self.name,
l,
1.0e6 * volt,
1.0e6 * freq,
harmon,
0.0,
PassMethod="IdentityPass" if float(l) == 0.0 else "DriftPass",
**self.meval(params),
)
return [cavity]
@classmethod
def from_at(cls, kwargs):
el = super().from_at(kwargs)
el["VOLT"] = 1.0e-6 * kwargs.pop("Voltage")
el["FREQ"] = 1.0e-6 * kwargs.pop("Frequency")
return el
# noinspection PyPep8Naming
class monitor(_MadElement):
@mad_element
def to_at(self, l=0.0, **params): # noqa: E741
hl = 0.5 * l # MadParameter conversion
if hl == 0.0:
return [elt.Monitor(self.name, **self.meval(params))]
else:
drname = self.name + ".1"
return [
elt.Drift(drname, hl),
elt.Monitor(self.name, **self.meval(params)),
elt.Drift(drname, hl),
]
# noinspection PyPep8Naming
class hmonitor(monitor):
pass
# noinspection PyPep8Naming
class vmonitor(monitor):
pass
# noinspection PyPep8Naming
class instrument(monitor):
pass
ignore_names(
globals(),
_MadElement,
["solenoid", "rfmultipole", "crabcavity", "elseparator", "collimator", "tkicker"],
)
@set_argparser(_keyparser)
def value(**kwargs):
"""VALUE command"""
kwargs.pop("copy", False)
for key, v in kwargs.items():
print(f"{key}: {v}")
class _Line(SequenceDescr):
"""Descriptor for the MADX LINE"""
def __add__(self, other):
return type(self)(chain(self, other))
def __mul__(self, other):
def repeat(n):
for _i in range(n):
yield from self
return type(self)(repeat(other))
def __rmul__(self, other):
return self.__mul__(other)
def expand(self, parser: BaseParser) -> Generator[elt.Element, None, None]:
if self.inverse:
for elem in reversed(self):
if isinstance(elem, AnyDescr): # Element or List
yield from (-elem).expand(parser)
elif isinstance(elem, Sequence): # Other sequence (tuple)
for el in reversed(elem):
yield from (-el).expand(parser)
else:
for elem in self:
if isinstance(elem, AnyDescr): # Element or List
yield from elem.expand(parser)
elif isinstance(elem, Sequence): # Other sequence (tuple, list)
for el in elem:
yield from el.expand(parser)
# noinspection PyPep8Naming
class _Sequence(SequenceDescr):
"""Descriptor for the MADX SEQUENCE"""
_offset = {"entry": 1.0, "centre": 0.0, "exit": -1.0}
def __init__(
self,
*args,
l: float = 0, # noqa: E741
refer: str = "centre",
refpos: str | None = None,
at: float = 0.0,
valid: int = 1,
**kwargs,
):
self.l = l # noqa: E741
try:
self.refer = self._offset[refer]
except KeyError as exc:
raise ValueError(f"REFER must be in {set(self._offset.keys())}") from exc
self.refpos = refpos
self.at = at
# Cannot use "from" as argument or attribute name:
setattr(self, "from", kwargs.pop("from", None))
self.valid = bool(valid)
super().__init__(*args, **kwargs)
def __call__(self, *args, copy: bool = True, **kwargs):
# Never make a copy
super().__call__(*args, copy=False, **kwargs)
return self if copy else None
def reference(self, parser, refer, refpos):
if refpos is None:
orig = 0.5 * (refer - 1.0) * self.length
else:
orig = None
for fname, *anames in self:
if fname == refpos:
# noinspection PyProtectedMember
elem = parser._raw_command(
None, fname, *anames, no_global=True, copy=True
)
orig = -elem.at
break
if orig is None:
raise NameError(
f"REFPOS {refpos!r} is not in the sequence {self.name!r}"
)
frm = getattr(self, "from")
if frm is not None:
orig += parser[frm].at
return orig
def flatten(self, parser, offset: float = 0.0, refer: float = 1.0, refpos=None):
offset = offset + self.reference(parser, refer, refpos) + self.at
for fname, *anames in self:
try:
# noinspection PyProtectedMember
elem = parser._raw_command(
None, fname, *anames, no_global=True, copy=True
)
except Exception as exc:
mess = (f"In sequence {self.name!r}: \"{fname}, {', '.join(anames)}\"",)
exc.args += mess
raise
if isinstance(elem, _Sequence):
yield from elem.flatten(parser, offset, self.refer, elem.refpos)
elif isinstance(elem, _MadElement):
yield elem.limits(parser, offset, self.refer), elem
def expand(self, parser: MadxParser) -> Generator[elt.Element, None, None]:
def insert_drift(dl, el):
nonlocal drcounter
if dl > 1.0e-5:
yield from drift(name=f"drift_{drcounter}", l=dl).expand(parser)
drcounter += 1
elif dl < -1.0e-5:
elemtype = type(el).__name__.upper()
raise ValueError(f"{elemtype}({el.name!r}) is overlapping by {-dl} m")
drcounter = 0
end = 0.0
elem = self
self.at = 0.0
for (entry, ext), elem in self.flatten(parser):
yield from insert_drift(entry - end, elem)
end = ext
yield from elem.expand(parser)
yield from insert_drift(self.length - end, elem) # Final drift
class _BeamDescr(ElementDescr):
"""Descriptor for the MAD-X BEAM object"""
@staticmethod
def to_at(name, *args, **params):
return []
def expand(self, parser: MadxParser) -> dict:
with warnings.catch_warnings():
warnings.simplefilter("ignore", category=UserWarning)
atparticle = Particle(
self["particle"],
rest_energy=self.get("mass", emass),
charge=self.get("charge", 1),
)
mass = 1.0e-09 * atparticle.rest_energy # [GeV]
charge = atparticle.charge
# Respect the precedence of MAD-X
if "energy" in self:
energy = float(self["energy"]) # force evaluation
gamma = energy / mass
# betagamma = sqrt(gamma * gamma - 1.0)
elif "pc" in self:
pc = float(self["pc"]) # force evaluation
betagamma = pc / mass
gamma = sqrt(betagamma * betagamma + 1.0)
elif "gamma" in self:
gamma = float(self["gamma"]) # force evaluation
# betagamma = sqrt(gamma * gamma - 1.0)
elif "beta" in self:
beta = float(self["beta"]) # force evaluation
gamma = 1.0 / sqrt(1.0 - beta * beta)
# betagamma = beta * gamma
elif "brho" in self:
brho = float(self["brho"]) # force evaluation
betagamma = 1.0e-9 * brho * clight * abs(charge) / mass
gamma = sqrt(betagamma * betagamma + 1.0)
else:
gamma = 1.0 / mass
# convert from array scalar to float
energy = float(gamma * mass) # [GeV]
# pc = betagamma * mass # [GeV]
# beta = betagamma / gamma
# brho = 1.0e09 * pc / abs(charge) / clight
return {
"particle": atparticle,
"energy": 1.0e09 * energy, # [eV]
"beam_current": float(self["bcurrent"]), # force deferred evaluation
"nbunch": self["kbunch"],
"periodicity": 1,
"radiate": self["radiate"],
}
class _Call:
"""Implement the CALL Mad command"""
@staticmethod
def argparser(parser, argcount, argstr):
return parser._argparser(
argcount, argstr, pos_args=("file",), str_attr=("file",)
)
def __init__(self, parser: _MadParser):
self.parser = parser
def __call__(self, file=None, name=None, copy=False):
self.parser.parse_files(file, final=False)
class _Beam:
"""Implement the BEAM Mad command"""
default_beam = {
"particle": "positron",
"energy": 1.0, # GeV
"bcurrent": 0.0,
"kbunch": 1,
"radiate": False,
}
@staticmethod
def argparser(parser, argcount, argstr):
return parser._argparser(
argcount,
argstr,
str_attr=("particle", "sequence"),
bool_attr=("radiate", "bunched"),
)
def __init__(self, parser: _MadParser):
self.parser = parser
def __call__(self, sequence=None, **kwargs):
"""create a :py:class:`_BeamDescr` object and store it as 'beam%sequence'"""
name = "beam%" if sequence is None else f"beam%{sequence}"
beamobj = self.parser.get(name, None)
if beamobj is None:
beamobj = _BeamDescr(self.default_beam)
self.parser[name] = beamobj
for k, v in kwargs.items():
beamobj[k] = v
class _MadParser(LowerCaseParser, UnorderedParser):
"""Common class for both MAD8 anf MAD-X parsers"""
_delimiter = ";"
_linecomment = ("!", "//")
_endfile = "return"
_undef_key = "none"
_str_arguments = {"file", "refer", "refpos", "sequence", "from"}
def __init__(self, env: dict, **kwargs):
"""Common behaviour for MAD-X and MAD8
Args:
env: global namespace used for evaluating commands
verbose: If :py:obj:`True`, print details on the processing
strict: If :py:obj:`False`, assign 0 to undefined variables
**kwargs: Initial variable definitions
"""
super().__init__(
env,
call=_Call(self),
beam=_Beam(self),
sequence=_Sequence,
centre="centre",
entry="entry",
exit="exit",
**kwargs,
)
self.current_sequence = None
self["beam"]()
def clear(self):
super().clear()
self.current_sequence = None
self["beam"]()
def _assign_deferred(self, value: str):
"""Deferred assignment"""
if value[0] == "(" and value[-1] == ")":
# Array variable: convert to tuple
value, matches = protect(value[1:-1], fence=(r"\(", r"\)"))
return tuple(
MadParameter(self, v) for v in restore(matches, *value.split(","))
)
else:
# Scalar variable
return MadParameter(self, value)
def _argparser(self, argcount, argstr: str, **kwargs):
key, *value = split_ignoring_parentheses(
argstr, delimiter=":=", fence=('"', '"'), maxsplit=1
)
if value:
return key, self._assign_deferred(value[0])
else:
return super()._argparser(argcount, argstr, **kwargs)
def _assign(self, label: str, key: str, val: str):
# Special treatment of "line=(...)" assignments
if key == "line":
val = val.replace(")", ",)") # For tuples with a single item
return label, _Line(self.evaluate(val), name=label)
else:
return super()._assign(label, key, val)
def _command(self, label: str | None, cmdname: str, *argnames: str, **kwargs):
# Special treatment of SEQUENCE definitions
res = None
if cmdname == "endsequence":
self.current_sequence = None
return None
if self.current_sequence is None:
try:
res = super()._command(label, cmdname, *argnames, **kwargs)
except (KeyError, NameError) as exc:
if cmdname == "sequence":
# if sequence creation failed, create a dummy sequence anyway
res = super()._command(label, cmdname, "valid=0", **kwargs)
# But store the command for later update
self._fallback(exc, None, label, "valid=1", *argnames)
else:
raise
finally:
if isinstance(res, _Sequence):
self.current_sequence = res
else:
if label is None:
self.current_sequence.append((cmdname, *argnames))
else:
try:
res = super()._command(label, cmdname, *argnames, **kwargs)
finally:
self.current_sequence.append((label, *argnames))
return res
def _decode(self, label: str, cmdname: str, *argnames: str) -> None:
left, *right = cmdname.split(":=")
if right:
self[left] = self._assign_deferred(right[0])
else:
super()._decode(label, cmdname, *argnames)
def _format_statement(self, line: str) -> str:
line = _separator.sub(",", line) # Replace space separators with commas
# turn curly braces into parentheses. Must be done before splitting arguments
line = line.replace("{", "(").replace("}", ")")
return super()._format_statement(line)
def _get_beam(self, key: str):
"""Get the beam object for a given sequence"""
try:
beam = self[f"beam%{self._gen_key(key)}"]
except KeyError:
beam = self["beam%"]
return beam
def _generator(self, params: dict) -> Iterable[elt.Element]:
def beta() -> float:
rest_energy = params["particle"].rest_energy
if rest_energy == 0.0:
return 1.0
else:
gamma = float(params["energy"] / rest_energy)
return sqrt(1.0 - 1.0 / gamma / gamma)
# generate the Lattice attributes from the BEAM object
beam = self._get_beam(params["use"]).expand(self)
for key, val in beam.items():
params.setdefault(key, val)
cavities = []
cell_length = 0
for elem in super()._generator(params):
if isinstance(elem, elt.RFCavity):
cavities.append(elem)
cell_length += getattr(elem, "Length", 0.0)
yield elem
rev = beta() * clight / cell_length
# Set the frequency of cavities in which it is not specified
for cav in cavities:
if np.isnan(cav.Frequency):
cav.Frequency = rev * cav.HarmNumber
elif cav.HarmNumber == 0:
cav.HarmNumber = round(cav.Frequency / rev)
def lattice(self, use: str = "ring", **kwargs):
"""Create a lattice from the selected sequence
Parameters:
use: Name of the MAD sequence or line containing the desired
lattice. Default: ``ring``
Keyword Args:
name (str): Name of the lattice. Default: MAD sequence name.
particle(Particle): Circulating particle. Default: from MAD
energy (float): Energy of the lattice [eV]. Default: from MAD
periodicity(int): Number of periods. Default: 1
*: All other keywords will be set as Lattice attributes
"""
part = kwargs.get("particle", None)
if isinstance(part, str):
kwargs["particle"] = Particle(part)
lat = super().lattice(use=use, **kwargs)
try:
radiate = lat.radiate
except AttributeError:
radiate = False
else:
del lat.radiate
# noinspection PyUnboundLocalVariable
if radiate:
lat.enable_6d(copy=False)
return lat
[docs]
class MadxParser(_MadParser):
# noinspection PyUnresolvedReferences
"""MAD-X specific parser
The parser is a subclass of :py:class:`dict` and is database containing all the
MAD-X parameters and objects.
Example:
Parse a 1st file:
>>> parser = at.MadxParser()
>>> parser.parse_file("file1")
Parse another file. This adds the new definitions to the database:
>>> parser.parse_file("file2")
Look at the *rf_on* variable
>>> parser["rf_on"]
0
Get the list of available sequences/lines:
>>> parser.sequences
['arca',
'arca_inj',
'arcb_inj',
'low_emit_ring',
'arc_inj',
'low_emit_ring_inj']
Generate an AT :py:class:`.Lattice` from the *low_emit_ring* sequence
>>> ring1 = parser.lattice(use="low_emit_ring")
Change the value of a variable:
>>> parser["rf_on"] = 1
Generate a new AT :py:class:`.Lattice` taking into account the new variables:
>>> ring2 = parser.lattice(use="low_emit_ring")
Generate an AT :py:class:`.Lattice` from another sequence:
>>> arca = parser.lattice(use="ring")
"""
_continuation = None
_blockcomment = ("/*", "*/")
def __init__(self, **kwargs):
"""
Args:
strict: If :py:obj:`False`, assign 0 to undefined variables
verbose: If :py:obj:`True`, print details on the processing
**kwargs: Initial variable definitions
"""
super().__init__(
globals(),
**kwargs,
)
def _format_command(self, expr: str) -> str:
"""Format a command for evaluation"""
expr = expr.replace("->", ".") # Attribute access: VAR->ATTR
expr = expr.replace("^", "**") # Exponentiation
return super()._format_command(expr)
[docs]
def load_madx(
*files: str,
use: str = "ring",
strict: bool = True,
verbose: bool = False,
**kwargs,
) -> Lattice:
"""Create a :py:class:`.Lattice` from MAD-X files
- The *energy* and *particle* of the generated lattice are taken from the MAD-X
``BEAM`` object, using the MAD-X default parameters: positrons at 1 Gev.
These parameters are overloaded by the value given in the *energy* and
*particle* keyword arguments.
- The radiation state is given by the ``RADIATE`` flag of the ``BEAM`` object,
using the AT defaults: RF cavities active, synchrotron radiation in dipoles and
quadrupoles.
- Long elements are split according to the default AT value of *NumIntSteps* (10).
Parameters:
files: Names of one or several MAD-X files
strict: If :py:obj:`False`, assign 0 to undefined variables
use: Name of the MADX sequence or line containing the desired
lattice. Default: ``ring``
verbose: If :py:obj:`True`, print details on the processing
Keyword Args:
name (str): Name of the lattice. Default: MADX sequence name.
particle(Particle): Circulating particle. Default: from MADX
energy (float): Energy of the lattice [eV]. Default: from MADX
periodicity(int): Number of periods. Default: 1
*: Other keywords will be used as initial variable definitions
Returns:
lattice (Lattice): New :py:class:`.Lattice` object
See Also:
:py:func:`.load_lattice` for a generic lattice-loading function.
"""
parser = MadxParser(strict=strict, verbose=verbose)
params = {
key: kwargs.pop(key)
for key in ("name", "particle", "energy", "periodicity")
if key in kwargs
}
parser.parse_files(*files, **kwargs)
return parser.lattice(use=use, **params)
def longmultipole(kwargs):
length = kwargs.get("Length", 0.0)
if length == 0.0:
return multipole.from_at(kwargs)
else:
drname = kwargs["FamName"] + ".1"
dr1 = drift.from_at({"FamName": drname, "Length": 0.5 * length})
dr2 = drift.from_at({"FamName": drname, "Length": 0.5 * length})
return [dr1, multipole.from_at(kwargs, factor=length), dr2]
def ignore(kwargs):
length = kwargs.get("Length", 0.0)
if length == 0.0:
print(f"{kwargs['name']} is replaced by a marker")
return marker.from_at(kwargs)
else:
print(f"{kwargs['name']} is replaced by a drift")
return drift.from_at(kwargs)
_AT2MAD = {
elt.Quadrupole: quadrupole.from_at,
elt.Sextupole: sextupole.from_at,
elt.Octupole: octupole.from_at,
elt.ThinMultipole: multipole.from_at,
elt.Multipole: longmultipole,
elt.RFCavity: rfcavity.from_at,
elt.Drift: drift.from_at,
elt.Bend: sbend.from_at,
elt.Marker: marker.from_at,
elt.Monitor: monitor.from_at,
elt.Corrector: kicker.from_at,
}
class _MadExporter(Exporter):
use_line = False
def generate_madelems(
self, eltype: type[elt.Element], elemdict: dict
) -> ElementDescr | list[ElementDescr]:
return _AT2MAD.get(eltype, ignore)(elemdict)
def print_beam(self, file):
part = str(self.particle)
if part == "relativistic":
part = "electron"
data = {
"ENERGY": 1.0e-9 * self.energy,
"PARTICLE": part.upper(),
"RADIATE": self.is_6d,
}
attrs = [f"{k}={ElementDescr.attr_format(v)}" for k, v in data.items()]
line = ", ".join(["BEAM".ljust(10)] + attrs)
print(f"{line}{self.delimiter}", file=file)
class _MadxExporter(_MadExporter):
delimiter = ";"
continuation = ""
bool_fmt = {False: "FALSE", True: "TRUE"}
[docs]
def save_madx(ring: Lattice, filename: str | None = None, **kwargs):
"""Save a :py:class:`.Lattice` as a MAD-X file
Args:
ring: lattice
filename: file to be created. If None, write to sys.stdout
Keyword Args:
use (str | None): name of the created SEQUENCE of LINE.
Default: name of the PyAT lattice
use_line (bool): If True, use a MAD "LINE" format. Otherwise, use
a MAD "SEQUENCE"
"""
exporter = _MadxExporter(ring, **kwargs)
exporter.export(filename)
register_format(
".seq",
load_madx,
save_madx,
descr="MAD-X lattice description. See :py:func:`.load_madx`.",
)