diff --git a/docs/src/guide/translation.md b/docs/src/guide/translation.md index aeea076..4f3549c 100644 --- a/docs/src/guide/translation.md +++ b/docs/src/guide/translation.md @@ -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 @@ -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. diff --git a/palsparserpy/to_bmad.py b/palsparserpy/to_bmad.py index 83b8946..24e452b 100644 --- a/palsparserpy/to_bmad.py +++ b/palsparserpy/to_bmad.py @@ -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"] @@ -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() @@ -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": @@ -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) diff --git a/tests/test_translate.py b/tests/test_translate.py index a8a8d59..38e4f73 100644 --- a/tests/test_translate.py +++ b/tests/test_translate.py @@ -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