Skip to content

Commit 6448a0e

Browse files
DavidSaganclaude
andauthored
Default the bend actual field from g_ref (#73)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent ca2f8c4 commit 6448a0e

2 files changed

Lines changed: 213 additions & 5 deletions

File tree

src/pals_expand.cpp

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1970,6 +1970,51 @@ static void resolve_bend(ryml::Tree& t, size_t ele, double& length,
19701970
get_num_child(t, ele, "length", length);
19711971
}
19721972

1973+
// Default the actual bending field from the reference one (bend.md,
1974+
// `Kn0_from_g_ref`). The field the particle actually sees is the dipole
1975+
// component of MagneticMultipoleP, and unless the author says otherwise a bend
1976+
// bends its own reference particle along the reference curve: `Kn0` = `g_ref`,
1977+
// equivalently `Bn0` = `Bn0_ref`. `Kn0_from_g_ref: false` leaves the component
1978+
// alone, for a bend whose actual field is zero or comes from somewhere else.
1979+
//
1980+
// All four forms of the dipole component count as "set" -- `Kn0L` and `Bn0L`
1981+
// state the same field as `Kn0` and `Bn0`, only integrated over the length --
1982+
// and only the normal ones: a skew dipole is a separate component, not this one.
1983+
// The group is created when the default applies to a bend that carries none,
1984+
// since otherwise the field would have nowhere to go. Whichever form is written
1985+
// here, resolve_magnetic_multipoles fills the other three next.
1986+
static void resolve_bend_actual_field(ryml::Tree& t, size_t ele) {
1987+
size_t bp = t.find_child(ele, ryml::to_csubstr("BendP"));
1988+
if (bp == ryml::NONE) return;
1989+
std::string flag = child_val_str(t, bp, "Kn0_from_g_ref");
1990+
if (!flag.empty() && !is_true_flag(flag)) return; // absent means true
1991+
1992+
size_t mp = t.find_child(ele, ryml::to_csubstr("MagneticMultipoleP"));
1993+
if (mp != ryml::NONE)
1994+
for (const char* k : {"Kn0", "Kn0L", "Bn0", "Bn0L"})
1995+
if (t.find_child(mp, ryml::to_csubstr(k)) != ryml::NONE) return;
1996+
1997+
// g_ref and Bn0_ref are tied together by the curvature linking above, so
1998+
// g_ref is the one to take whenever the reference momentum was known;
1999+
// Bn0_ref carries the field when it was not. A zero reference bend field is
2000+
// no field at all, and a zero is not held.
2001+
double g = 0.0, B = 0.0;
2002+
const char* key = nullptr;
2003+
double value = 0.0;
2004+
if (get_num_child(t, bp, "g_ref", g) && g != 0.0) {
2005+
key = "Kn0";
2006+
value = g;
2007+
} else if (get_num_child(t, bp, "Bn0_ref", B) && B != 0.0) {
2008+
key = "Bn0";
2009+
value = B;
2010+
}
2011+
if (!key) return;
2012+
2013+
if (mp == ryml::NONE)
2014+
mp = find_or_add_map_child(t, ele, "MagneticMultipoleP");
2015+
set_num_child(t, mp, key, value);
2016+
}
2017+
19732018
// Split a multipole component key into its (normal/skew char, order digits),
19742019
// e.g. "Bn2L" -> ('n', "2"). `prefixes` is the pair of accepted leading tokens
19752020
// (magnetic "Bn"/"Bs"/"Kn"/"Ks" collapse to the field forms here; electric
@@ -2087,8 +2132,11 @@ static void compute_dependent(ryml::Tree& t, size_t ele, const std::string& kind
20872132
}
20882133

20892134
std::string ename = ele_name(t, ele);
2090-
if (kind == "Bend")
2135+
if (kind == "Bend") {
20912136
resolve_bend(t, ele, length, has_factor, factor, ename, problems);
2137+
// Before the multipoles: this seeds the dipole component they resolve.
2138+
resolve_bend_actual_field(t, ele);
2139+
}
20922140
resolve_magnetic_multipoles(t, ele, length, has_factor, factor, ename,
20932141
problems);
20942142
resolve_electric_multipoles(t, ele, length, ename, problems);
@@ -2120,9 +2168,11 @@ static void compute_rf_dependent(ryml::Tree& t, size_t ele, double length,
21202168
// Parameters with a non-zero (or enum/boolean) default, filled in for any group
21212169
// that is *present* in the element. The expanded lattice holds every non-zero
21222170
// parameter, so an author who writes a group but omits these gets the defaults
2123-
// made explicit. A group absent from the element is left untouched -- only
2124-
// ReferenceP and FloorP are added by the parser. Parameters whose default is
2125-
// zero/null/false are not listed: they are not "held".
2171+
// made explicit. A group absent from the element is left untouched -- it is
2172+
// added only by the parser (ReferenceP, FloorP) or to hold a derived value with
2173+
// nowhere else to go (MagneticMultipoleP on a curved bend, see
2174+
// resolve_bend_actual_field). Parameters whose default is zero/null/false are
2175+
// not listed: they are not "held".
21262176
struct GroupDefault {
21272177
const char* group;
21282178
const char* key;
@@ -2133,6 +2183,7 @@ static const GroupDefault kGroupDefaults[] = {
21332183
{"RFP", "zero_phase", "ACCELERATING"},
21342184
{"BendP", "ref_geometry", "ARC"},
21352185
{"BendP", "multipole_geometry", "FOLLOWS_REF_GEOMETRY"},
2186+
{"BendP", "Kn0_from_g_ref", "true"},
21362187
{"ApertureP", "shape", "ELLIPTICAL"},
21372188
{"ApertureP", "location", "ENTRANCE_END"},
21382189
{"ApertureP", "aperture_active", "true"},

tests/test_bookkeeper.cpp

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,162 @@ TEST_CASE("Bookkeeper flags an inconsistent bend strength", "[bookkeeper]") {
613613
delete_tree(lat.leftover);
614614
}
615615

616+
TEST_CASE("Bookkeeper defaults the bend actual field from g_ref",
617+
"[bookkeeper]") {
618+
// Kn0_from_g_ref (bend.md) is true by default, so a bend that says nothing
619+
// about its actual field bends its own reference particle along the
620+
// reference curve: Kn0 = g_ref. The element carries no MagneticMultipoleP,
621+
// so the group is created to hold it, and the other three forms of the
622+
// dipole component follow as usual.
623+
struct lattices lat = expand_src(one_ele_yaml(
624+
" - bnd:\n"
625+
" kind: Bend\n"
626+
" length: 2.0\n"
627+
" BendP:\n"
628+
" g_ref: 0.1\n"));
629+
YAMLTreeHandle t = lat.expanded;
630+
631+
REQUIRE(close(param_num(t, "bnd", "MagneticMultipoleP", "Kn0"), 0.1));
632+
REQUIRE(close(param_num(t, "bnd", "MagneticMultipoleP", "Kn0L"), 0.2));
633+
REQUIRE(close_rel(param_num(t, "bnd", "MagneticMultipoleP", "Bn0"),
634+
0.1 / electron_factor()));
635+
REQUIRE(close_rel(param_num(t, "bnd", "MagneticMultipoleP", "Bn0L"),
636+
0.2 / electron_factor()));
637+
// The flag itself is a non-false default of a present group, so it is made
638+
// explicit alongside the enum defaults.
639+
YAMLNodeId bp = get_child_by_key(t, find_by_key(t, "bnd"), "BendP");
640+
REQUIRE(val_eq(t, get_child_by_key(t, bp, "Kn0_from_g_ref"), "true"));
641+
REQUIRE_FALSE(any_problem_contains(lat, "inconsistent"));
642+
643+
free_lattice_problems(lat.problems);
644+
delete_tree(lat.original);
645+
delete_tree(lat.combined);
646+
delete_tree(lat.expanded);
647+
delete_tree(lat.leftover);
648+
}
649+
650+
TEST_CASE("Bookkeeper writes no actual field when Kn0_from_g_ref is false",
651+
"[bookkeeper]") {
652+
// The author has said the actual field is not the reference one. Nothing is
653+
// written, and the bend keeps no multipole group at all.
654+
struct lattices lat = expand_src(one_ele_yaml(
655+
" - bnd:\n"
656+
" kind: Bend\n"
657+
" length: 2.0\n"
658+
" BendP:\n"
659+
" g_ref: 0.1\n"
660+
" Kn0_from_g_ref: false\n"));
661+
YAMLTreeHandle t = lat.expanded;
662+
663+
REQUIRE(get_child_by_key(t, find_by_key(t, "bnd"),
664+
"MagneticMultipoleP") == YAML_NULL_ID);
665+
// The reference bend is untouched: only the actual field is being declined.
666+
REQUIRE(close(param_num(t, "bnd", "BendP", "angle_ref"), 0.2));
667+
668+
free_lattice_problems(lat.problems);
669+
delete_tree(lat.original);
670+
delete_tree(lat.combined);
671+
delete_tree(lat.expanded);
672+
delete_tree(lat.leftover);
673+
}
674+
675+
TEST_CASE("Bookkeeper keeps an actual bend field the author set",
676+
"[bookkeeper]") {
677+
// Kn0 is set to something other than g_ref -- a bend running off its
678+
// reference field, which is exactly what the parameters are separate for.
679+
// The default does not apply, and the difference is not an inconsistency.
680+
struct lattices lat = expand_src(one_ele_yaml(
681+
" - bnd:\n"
682+
" kind: Bend\n"
683+
" length: 2.0\n"
684+
" BendP:\n"
685+
" g_ref: 0.1\n"
686+
" MagneticMultipoleP:\n"
687+
" Kn0: 0.3\n"));
688+
YAMLTreeHandle t = lat.expanded;
689+
690+
REQUIRE(close(param_num(t, "bnd", "MagneticMultipoleP", "Kn0"), 0.3));
691+
REQUIRE(close(param_num(t, "bnd", "MagneticMultipoleP", "Kn0L"), 0.6));
692+
REQUIRE(close(param_num(t, "bnd", "BendP", "g_ref"), 0.1));
693+
REQUIRE_FALSE(any_problem_contains(lat, "inconsistent"));
694+
695+
free_lattice_problems(lat.problems);
696+
delete_tree(lat.original);
697+
delete_tree(lat.combined);
698+
delete_tree(lat.expanded);
699+
delete_tree(lat.leftover);
700+
}
701+
702+
TEST_CASE("Bookkeeper counts an integrated actual field as set",
703+
"[bookkeeper]") {
704+
// Kn0L states the same field as Kn0, just integrated over the length, so
705+
// the default does not apply: Kn0 comes from Kn0L / length = 0.3, not from
706+
// g_ref. A Kn0 of 0.1 here would have contradicted the author's own value.
707+
struct lattices lat = expand_src(one_ele_yaml(
708+
" - bnd:\n"
709+
" kind: Bend\n"
710+
" length: 2.0\n"
711+
" BendP:\n"
712+
" g_ref: 0.1\n"
713+
" MagneticMultipoleP:\n"
714+
" Kn0L: 0.6\n"));
715+
YAMLTreeHandle t = lat.expanded;
716+
717+
REQUIRE(close(param_num(t, "bnd", "MagneticMultipoleP", "Kn0"), 0.3));
718+
REQUIRE_FALSE(any_problem_contains(lat, "inconsistent"));
719+
720+
free_lattice_problems(lat.problems);
721+
delete_tree(lat.original);
722+
delete_tree(lat.combined);
723+
delete_tree(lat.expanded);
724+
delete_tree(lat.leftover);
725+
}
726+
727+
TEST_CASE("Bookkeeper adds the actual field to a group of other multipoles",
728+
"[bookkeeper]") {
729+
// A different order says nothing about the dipole component: the quadrupole
730+
// the author gave stays, and Kn0 joins it in the group already there.
731+
struct lattices lat = expand_src(one_ele_yaml(
732+
" - bnd:\n"
733+
" kind: Bend\n"
734+
" length: 2.0\n"
735+
" BendP:\n"
736+
" g_ref: 0.1\n"
737+
" MagneticMultipoleP:\n"
738+
" Kn1: 0.5\n"));
739+
YAMLTreeHandle t = lat.expanded;
740+
741+
REQUIRE(close(param_num(t, "bnd", "MagneticMultipoleP", "Kn0"), 0.1));
742+
REQUIRE(close(param_num(t, "bnd", "MagneticMultipoleP", "Kn1"), 0.5));
743+
744+
free_lattice_problems(lat.problems);
745+
delete_tree(lat.original);
746+
delete_tree(lat.combined);
747+
delete_tree(lat.expanded);
748+
delete_tree(lat.leftover);
749+
}
750+
751+
TEST_CASE("Bookkeeper gives a straight bend no actual field", "[bookkeeper]") {
752+
// No curvature, so there is no reference field to copy and nothing to hold:
753+
// a zero Kn0 is not written, and no group is created for it.
754+
struct lattices lat = expand_src(one_ele_yaml(
755+
" - bnd:\n"
756+
" kind: Bend\n"
757+
" length: 2.0\n"
758+
" BendP:\n"
759+
" e1: 0.01\n"));
760+
YAMLTreeHandle t = lat.expanded;
761+
762+
REQUIRE(get_child_by_key(t, find_by_key(t, "bnd"),
763+
"MagneticMultipoleP") == YAML_NULL_ID);
764+
765+
free_lattice_problems(lat.problems);
766+
delete_tree(lat.original);
767+
delete_tree(lat.combined);
768+
delete_tree(lat.expanded);
769+
delete_tree(lat.leftover);
770+
}
771+
616772
TEST_CASE("Bookkeeper flags an inconsistent multipole component",
617773
"[bookkeeper]") {
618774
// Bn1 1.2 over length 0.5 implies Bn1L 0.6, but Bn1L is set to 0.5.
@@ -654,7 +810,8 @@ TEST_CASE("Bookkeeper materializes RF enum defaults and L_active",
654810

655811
TEST_CASE("Bookkeeper leaves absent parameter groups alone", "[bookkeeper]") {
656812
// Groups not present in the file are not materialized: a plain drift gains no
657-
// BendP/RFP/multipole groups. Only ReferenceP and FloorP are parser-added.
813+
// BendP/RFP/multipole groups. Only ReferenceP and FloorP are parser-added,
814+
// and MagneticMultipoleP on a curved bend, which has an actual field to hold.
658815
struct lattices lat = expand_bookkeeper();
659816
YAMLTreeHandle t = lat.expanded;
660817
YAMLNodeId d1 = find_by_key(t, "d1");

0 commit comments

Comments
 (0)