Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/src/guide/translation.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ and their SciBmad/Bmad/MAD-X equivalents.
`ref_geometry`: `ARC`/`CHORD` --> `e = e_rect + angle/2`; `ENTRANCE_COORDS` -->
`e1 = e1_rect`, `e2 = e2_rect + angle`; `EXIT_COORDS` --> `e1 = e1_rect + angle`,
`e2 = e2_rect`.
- Note (Bmad): angle_ref --> angle. e1 --> e1 and e2 --> e2, a Bmad sbend measuring its
faces against the sector geometry too; `e1_rect`/`e2_rect` are converted the same way as
for MAD-X, with the angle worked out from `angle_ref`, or from `g_ref`/`radius_ref` with
`length`, `L_chord` or `L_rectangle`.
- Note (MAD-X): edge1_int --> `fint = 0.5, hgap = 2*edge1_int`, edge2_int -->
`fintx`/`hgapx`; h1 --> h1, h2 --> h2; tilt_ref --> tilt.
- Note (MAD-X): a MAD-X sbend is always an arc with vertically pure multipoles, so a
Expand Down Expand Up @@ -418,6 +422,8 @@ and their SciBmad/Bmad/MAD-X equivalents.

- Note (MAD-X): frequency --> `freq` in MHz, harmon --> `harmon`,
voltage --> `volt` in MV, gradient --> `volt = gradient * L_active`.
- Note (Bmad): gradient --> `voltage = gradient * L_active` (`L_active` defaulting to the
element length), Bmad's `gradient` being a dependent attribute.
- Note (Bmad): phase --> `phi0 = phase / twopi` and multipass_phase -->
`phi0_multipass = multipass_phase / twopi`, Bmad counting RF phases in turns.
- Note (MAD-X): phase --> `lag` in turns, offset by what `zero_phase` measures from.
Expand Down
114 changes: 92 additions & 22 deletions palsparserpy/to_bmad.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
ctrl_variables, facility_props, fill_multipoles, fmt,
name_value_pairs, tilt_rotation, try_float, value_text)
from .node import YAMLNode
from .to_madx import _madx_fold, _madx_times

__all__ = ["BmadEleDef", "BmadBeamline", "BmadController", "BmadLattice",
"pals_to_bmad", "write_bmad_file"]
Expand Down Expand Up @@ -986,25 +987,17 @@ def push_attr(text):
raise ValueError(f"{name}: BeamBeamP not translated yet")
elif key == "BendP":
bendP = props["BendP"]
has_e1 = "e1" in bendP
has_e1_rect = "e1_rect" in bendP
has_e2 = "e2" in bendP
has_e2_rect = "e2_rect" in bendP
if (has_e1 or has_e2) and (has_e1_rect or has_e2_rect):
raise ValueError(f"{name}: should not have both e1 and e1_rect, "
"nor both e2 and e2_rect")

for bkey in bendP.keys():
tmp = ""
if bkey == "radius_ref":
if bkey == "angle_ref":
tmp += f"angle = {bendP['angle_ref'].value},"
elif bkey == "radius_ref":
tmp += f"rho = {bendP['radius_ref'].value},"
elif bkey == "Bn0_ref":
tmp += f"B_field = {bendP['Bn0_ref'].value},"

elif bkey in ("e1", "e1_rect"):
tmp += f"e1 = {bendP['e1'].value},"
elif bkey in ("e2", "e2_rect"):
tmp += f"e2 = {bendP['e2'].value},"
elif bkey in ("e1", "e1_rect", "e2", "e2_rect"):
tmp += _bmad_bend_face(props, bkey, name) + ","

elif bkey == "edge1_int":
val = bendP["edge1_int"].as_float()
Expand Down Expand Up @@ -1169,17 +1162,17 @@ def push_attr(text):
elif rfkey == "voltage":
tmp += f"voltage = {rfP['voltage'].value},"

# Bmad's `gradient` is a dependent attribute, so the voltage is
# stated instead.
elif rfkey == "gradient":
if "L" in props and props["L"].as_float() != 0:
length = props["L"].as_float()
grad = rfP["gradient"].as_float()
tmp += f"voltage = {fmt(grad * length)},"
print(f"{name}: gradient not yet supported, replacing "
"with voltage = gradient * length")
if "L_active" in rfP:
length = rfP["L_active"].value
elif "length" in props:
length = props["length"].value
else:
raise ValueError(
f"{name}: `gradient` not yet supported & `length` is "
"undefined => voltage is undefined")
raise ValueError(f"{name}: `gradient` needs a length to become "
"the voltage Bmad states")
tmp += f"voltage = {_madx_times(rfP['gradient'].value, length)},"

# PALS states RF phases in radians, Bmad in turns.
elif rfkey == "phase":
Expand Down Expand Up @@ -1524,3 +1517,80 @@ def visit(gname):
attrs.append(f"{bkey} = {text}")
out.append(BmadEleDef(gname, f"girder = {slaves}", attrs))
return out


# ---------------------------------------------------------------------------
def _bmad_bend_angle(props: YAMLNode, name: str) -> Optional[str]:
"""The reference bend angle of a ``Bend`` as Bmad text, or ``None`` if the
element states too little to work it out.

PALS states the bend geometry with any two of a curvature (``g_ref``,
``radius_ref``), a length (``length``, ``L_chord``, ``L_rectangle``) and the
angle (``angle_ref``). A curvature given as the field ``Bn0_ref`` needs the
reference rigidity and is not used here.
"""
bendP = props["BendP"]
if "angle_ref" in bendP:
return bendP["angle_ref"].value
if "g_ref" in bendP:
g = bendP["g_ref"].value
elif "radius_ref" in bendP:
r = bendP["radius_ref"].value
g = _madx_fold(f"1 / ({r})", lambda x: 1 / x, r)
else:
return None
if "length" in props:
return _madx_times(g, props["length"].value)
if "L_chord" in bendP:
length = bendP["L_chord"].value
return _madx_fold(f"2 * asin(({g}) * ({length}) / 2)",
lambda a, b: 2 * math.asin(a * b / 2), g, length)
if "L_rectangle" in bendP:
length = bendP["L_rectangle"].value
return _madx_fold(f"asin(({g}) * ({length}))",
lambda a, b: math.asin(a * b), g, length)
return None


# ---------------------------------------------------------------------------
def _bmad_bend_face(props: YAMLNode, key: str, name: str) -> str:
"""The Bmad ``e1``/``e2`` fragment for the PALS pole-face parameter ``key``
of a ``Bend``.

A Bmad ``sbend`` measures its face angles against the sector geometry, as
PALS' own ``e1`` and ``e2`` are, so those come straight across. PALS'
``e1_rect`` and ``e2_rect`` are measured against fiducial lines parallel to
each other, and the bend angle separates the two pairs by a share that
depends on ``ref_geometry`` (bend.md)::

ARC, CHORD e1 = e1_rect + angle/2, e2 = e2_rect + angle/2
ENTRANCE_COORDS e1 = e1_rect, e2 = e2_rect + angle
EXIT_COORDS e1 = e1_rect + angle, e2 = e2_rect

A face given both ways is contradictory, and one given the rectangular way on
a bend whose angle cannot be worked out cannot be converted; both raise an
error.
"""
bendP = props["BendP"]
face = key[:2]
if face in bendP and face + "_rect" in bendP:
raise ValueError(f"{name}: should not have both {face} and {face}_rect")
if not key.endswith("_rect"):
return f"{face} = {bendP[key].value}"

geometry = bendP["ref_geometry"].value if "ref_geometry" in bendP else "ARC"
if geometry == "ENTRANCE_COORDS":
share = 0.0 if face == "e1" else 1.0
elif geometry == "EXIT_COORDS":
share = 1.0 if face == "e1" else 0.0
else:
share = 0.5
rect = bendP[key].value
if share == 0:
return f"{face} = {rect}"
angle = _bmad_bend_angle(props, name)
if angle is None:
raise ValueError(f"{name}: {key} is measured against the bend angle, which "
"is not given")
return f"{face} = " + _madx_fold(f"{rect} + {share} * ({angle})",
lambda r, a: r + share * a, rect, angle)
23 changes: 23 additions & 0 deletions tests/test_translate.py
Original file line number Diff line number Diff line change
Expand Up @@ -1281,3 +1281,26 @@ def test_girders_madx_and_bmad_cannot_express_are_reported(tmp_path):
"elements: q.*"))
with pytest.raises(ValueError, match="neither a list of names nor a single range"):
pals_to_bmad(pattern)


def test_a_rectangular_face_angle_becomes_bmads_sector_one(tmp_path):
# Bmad holds a chord length only as a dependent attribute, so b3 states its arc
# length.
fixture = re.sub(r"\n *L_chord: 1.5", "", MADX_GEOMETRY_FIXTURE)
fixture = re.sub(r"(- b3:\n( *)kind: Bend\n)", r"\1\2length: 1.5\n", fixture)
out = _bmad_text(tmp_path, pals_to_bmad(_parsed(tmp_path, fixture)))
# Curvature and arc length give the angle 0.5; e1_rect picks up half of it, and
# the exit face, given the sector way, comes straight across.
assert "b2: SBend,\n\tL = 2,\n\tg = 0.25,\n\te1 = 0.26,\n\te2 = 0.3" in out
# With ref_geometry EXIT_COORDS the whole angle lands on the entrance face.
assert ("b3: SBend,\n\tL = 1.5,\n\tangle = 0.4,\n\te1 = 0.42000000000000004,"
"\n\te2 = 0.03") in out
# The angle is stated as well as the curvature it came with.
assert "b1: SBend,\n\tangle = 0.25,\n\tg = 0.5\n" in out


def test_an_rf_gradient_becomes_bmads_voltage(tmp_path):
fixture = PLACEMENT_FIXTURE.split("girders:")[0].replace("voltage: 1E6",
"gradient: 2E6")
out = _bmad_text(tmp_path, pals_to_bmad(_parsed(tmp_path, fixture)))
assert "rf1: RFCavity,\n\tL = 1.3,\n\tvoltage = 2600000.0" in out
Loading