1414from .MADXParser import MADXParser
1515
1616WARN_PREFIX = "[WARNING MADX-ImpactX-Translator] "
17+ TRACKED_TRANSLATION_OPTIONS = ("rbarc" , "thin_foc" )
1718
1819
1920class MADXImpactXTranslatorWarning (UserWarning ):
@@ -64,6 +65,12 @@ def lattice(parsed_beamline, nslice=1, freq0=0.0, options=None):
6465 :param nslice: number of ds slices per element
6566 :param freq0: revolution frequency in MHz (from BEAM command)
6667 :return: list of translated dictionaries
68+
69+ Maintainer note (one-place-of-truth philosophy):
70+ - Keep element-type mapping in one place (`madx_to_impactx_dict` below).
71+ - Keep bend-body model choice in one place (`make_bend_body_element` below).
72+ - Prefer composition from existing ImpactX elements over silent drops.
73+ - Every physics fallback should `_warn(...)` and include a TODO comment.
6774 """
6875
6976 # mapping is "MAD-X": "ImpactX",
@@ -104,6 +111,10 @@ def lattice(parsed_beamline, nslice=1, freq0=0.0, options=None):
104111 # rbarc=true, thin_foc=true
105112 rbarc = bool (options .get ("rbarc" , True ))
106113 thin_foc = bool (options .get ("thin_foc" , True ))
114+ rad_to_deg = 180.0 / math .pi
115+ # MAD-X bend maps are driven by ANGLE/TILT; K0/K0S are obsolete for map construction.
116+ # H1/H2 (pole-face curvature) still affect MAD-X fringe modeling and are not translated here.
117+ unsupported_bend_attrs = ("k0" , "k0s" , "h1" , "h2" )
107118 warned_once = set ()
108119
109120 class _TrackedElement (dict ):
@@ -163,6 +174,55 @@ def append_thin_with_length(thin_element, length, element_name):
163174 else :
164175 impactx_beamline .append (thin_element )
165176
177+ def make_bend_body_element (
178+ * ,
179+ name ,
180+ ds ,
181+ rc ,
182+ tilt_degree ,
183+ k1 ,
184+ k1s ,
185+ k2 ,
186+ k2s ,
187+ k3 ,
188+ k3s ,
189+ ):
190+ """Create the most faithful currently available ImpactX bend body model.
191+
192+ Priority:
193+ 1) ExactCFbend when skew or higher body multipoles are present
194+ 2) CFbend for pure dipole+quadrupole
195+ 3) Sbend for pure dipole geometry
196+ """
197+ use_exact_cfbend = any (abs (v ) > 0.0 for v in (k1s , k2 , k2s , k3 , k3s ))
198+ if use_exact_cfbend :
199+ curvature = 1.0 / rc if rc != 0.0 else 0.0
200+ return elements .ExactCFbend (
201+ name = name ,
202+ ds = ds ,
203+ k_normal = [curvature , k1 , k2 , k3 ],
204+ k_skew = [0.0 , k1s , k2s , k3s ],
205+ unit = 0 ,
206+ rotation = tilt_degree ,
207+ nslice = nslice ,
208+ )
209+ if abs (k1 ) > 0.0 :
210+ return elements .CFbend (
211+ name = name ,
212+ ds = ds ,
213+ rc = rc ,
214+ k = k1 ,
215+ rotation = tilt_degree ,
216+ nslice = nslice ,
217+ )
218+ return elements .Sbend (
219+ name = name ,
220+ ds = ds ,
221+ rc = rc ,
222+ rotation = tilt_degree ,
223+ nslice = nslice ,
224+ )
225+
166226 for d_raw in parsed_beamline :
167227 d = _TrackedElement (d_raw )
168228 # print(d)
@@ -177,7 +237,7 @@ def append_thin_with_length(thin_element, length, element_name):
177237 ds = d .get ("l" , 0.0 )
178238 k1 = d .get ("k1" , 0.0 )
179239 k1s = d .get ("k1s" , 0.0 )
180- tilt_degree = d .get ("tilt" , 0.0 ) * 180.0 / math . pi
240+ tilt_degree = d .get ("tilt" , 0.0 ) * rad_to_deg
181241 if ds > 0 :
182242 if abs (k1s ) > 0 :
183243 impactx_beamline .append (
@@ -231,7 +291,7 @@ def append_thin_with_length(thin_element, length, element_name):
231291 hgap = d .get ("hgap" , 0.0 )
232292 fint = d .get ("fint" , 0.0 )
233293 fintx = d .get ("fintx" , fint )
234- tilt_degree = d .get ("tilt" , 0.0 ) * 180.0 / math . pi
294+ tilt_degree = d .get ("tilt" , 0.0 ) * rad_to_deg
235295 if ds > 0 :
236296 if abs (angle ) > 0 :
237297 rc = ds / angle
@@ -256,45 +316,20 @@ def append_thin_with_length(thin_element, length, element_name):
256316 )
257317 )
258318
259- use_exact_cfbend = any (
260- abs (v ) > 0.0 for v in (k1s , k2 , k2s , k3 , k3s )
261- )
262- if use_exact_cfbend :
263- # ExactCFbend can represent curvilinear combined-function
264- # bends including sextupole/octupole body terms.
265- curvature = 1.0 / rc if rc != 0.0 else 0.0
266- impactx_beamline .append (
267- elements .ExactCFbend (
268- name = d ["name" ],
269- ds = ds ,
270- k_normal = [curvature , k1 , k2 , k3 ],
271- k_skew = [0.0 , k1s , k2s , k3s ],
272- unit = 0 ,
273- rotation = tilt_degree ,
274- nslice = nslice ,
275- )
276- )
277- elif abs (k1 ) > 0 :
278- impactx_beamline .append (
279- elements .CFbend (
280- name = d ["name" ],
281- ds = ds ,
282- rc = rc ,
283- k = k1 ,
284- rotation = tilt_degree ,
285- nslice = nslice ,
286- )
287- )
288- else :
289- impactx_beamline .append (
290- elements .Sbend (
291- name = d ["name" ],
292- ds = ds ,
293- rc = rc ,
294- rotation = tilt_degree ,
295- nslice = nslice ,
296- )
319+ impactx_beamline .append (
320+ make_bend_body_element (
321+ name = d ["name" ],
322+ ds = ds ,
323+ rc = rc ,
324+ tilt_degree = tilt_degree ,
325+ k1 = k1 ,
326+ k1s = k1s ,
327+ k2 = k2 ,
328+ k2s = k2s ,
329+ k3 = k3 ,
330+ k3s = k3s ,
297331 )
332+ )
298333
299334 if has_edges :
300335 impactx_beamline .append (
@@ -340,7 +375,7 @@ def append_thin_with_length(thin_element, length, element_name):
340375 impactx_beamline .append (
341376 elements .ThinDipole (name = d ["name" ], theta = angle , rc = 0.0 )
342377 )
343- for attr in ( "k0" , "k0s" , "h1" , "h2" ) :
378+ for attr in unsupported_bend_attrs :
344379 if abs (d .get (attr , 0.0 )) > 0 :
345380 # TODO(audit): Map higher-order/extended SBEND attributes when
346381 # ImpactX provides a direct equivalent in this translator path.
@@ -362,7 +397,7 @@ def append_thin_with_length(thin_element, length, element_name):
362397 k2s = d .get ("k2s" , 0.0 )
363398 k3 = d .get ("k3" , 0.0 )
364399 k3s = d .get ("k3s" , 0.0 )
365- tilt_degree = d .get ("tilt" , 0.0 ) * 180.0 / math . pi
400+ tilt_degree = d .get ("tilt" , 0.0 ) * rad_to_deg
366401
367402 if l_chord > 0 :
368403 # RBARC option controls how RBEND L is interpreted:
@@ -397,43 +432,20 @@ def append_thin_with_length(thin_element, length, element_name):
397432 )
398433
399434 # Body: prefer combined-function model if representable.
400- use_exact_cfbend = any (
401- abs (v ) > 0.0 for v in (k1s , k2 , k2s , k3 , k3s )
402- )
403- if use_exact_cfbend :
404- curvature = 1.0 / rc if rc != 0.0 else 0.0
405- impactx_beamline .append (
406- elements .ExactCFbend (
407- name = d ["name" ],
408- ds = l_arc ,
409- k_normal = [curvature , k1 , k2 , k3 ],
410- k_skew = [0.0 , k1s , k2s , k3s ],
411- unit = 0 ,
412- rotation = tilt_degree ,
413- nslice = nslice ,
414- )
415- )
416- elif abs (k1 ) > 0 :
417- impactx_beamline .append (
418- elements .CFbend (
419- name = d ["name" ],
420- ds = l_arc ,
421- rc = rc ,
422- k = k1 ,
423- rotation = tilt_degree ,
424- nslice = nslice ,
425- )
426- )
427- else :
428- impactx_beamline .append (
429- elements .Sbend (
430- name = d ["name" ],
431- ds = l_arc ,
432- rc = rc ,
433- rotation = tilt_degree ,
434- nslice = nslice ,
435- )
435+ impactx_beamline .append (
436+ make_bend_body_element (
437+ name = d ["name" ],
438+ ds = l_arc ,
439+ rc = rc ,
440+ tilt_degree = tilt_degree ,
441+ k1 = k1 ,
442+ k1s = k1s ,
443+ k2 = k2 ,
444+ k2s = k2s ,
445+ k3 = k3 ,
446+ k3s = k3s ,
436447 )
448+ )
437449 # Exit DipEdge
438450 # Exit DipEdge
439451 impactx_beamline .append (
@@ -461,7 +473,7 @@ def append_thin_with_length(thin_element, length, element_name):
461473 impactx_beamline .append (
462474 elements .ThinDipole (name = d ["name" ], theta = angle , rc = 0.0 )
463475 )
464- for attr in ( "k0" , "k0s" , "h1" , "h2" ) :
476+ for attr in unsupported_bend_attrs :
465477 if abs (d .get (attr , 0.0 )) > 0 :
466478 # TODO(audit): Map higher-order/extended RBEND attributes when
467479 # ImpactX provides a direct equivalent in this translator path.
@@ -473,7 +485,7 @@ def append_thin_with_length(thin_element, length, element_name):
473485 ks = d .get ("ks" , 0.0 )
474486 ksi = d .get ("ksi" , 0.0 )
475487 lrad = d .get ("lrad" , 0.0 )
476- tilt_degree = d .get ("tilt" , 0.0 ) * 180.0 / math . pi
488+ tilt_degree = d .get ("tilt" , 0.0 ) * rad_to_deg
477489 if ds > 0 :
478490 impactx_beamline .append (
479491 elements .Sol (
@@ -528,14 +540,14 @@ def append_thin_with_length(thin_element, length, element_name):
528540 g = 2.0 * d .get ("hgap" , 0.0 ),
529541 K2 = d .get ("fint" , 0.0 ),
530542 location = location ,
531- rotation = d .get ("tilt" , 0.0 ) * 180.0 / math . pi ,
543+ rotation = d .get ("tilt" , 0.0 ) * rad_to_deg ,
532544 )
533545 )
534546 elif d ["type" ] in ("kicker" , "tkicker" ):
535547 # Corner case: ImpactX Kicker is thin. For MAD-X finite L, we preserve
536548 # placement/length with a drift-kick-drift composition.
537549 ds = d .get ("l" , 0.0 )
538- tilt_degree = d .get ("tilt" , 0.0 ) * 180.0 / math . pi
550+ tilt_degree = d .get ("tilt" , 0.0 ) * rad_to_deg
539551 if abs (d .get ("sinkick" , 0.0 )) > 0 :
540552 # TODO(audit): Map sinusoidally driven kicker modes (SINKICK/SINPEAK/SINTUNE/SINPHASE).
541553 _warn (
@@ -553,7 +565,7 @@ def append_thin_with_length(thin_element, length, element_name):
553565 )
554566 elif d ["type" ] == "hkicker" :
555567 ds = d .get ("l" , 0.0 )
556- tilt_degree = d .get ("tilt" , 0.0 ) * 180.0 / math . pi
568+ tilt_degree = d .get ("tilt" , 0.0 ) * rad_to_deg
557569 if abs (d .get ("sinkick" , 0.0 )) > 0 :
558570 # TODO(audit): Map sinusoidally driven kicker modes (SINKICK/SINPEAK/SINTUNE/SINPHASE).
559571 _warn (
@@ -571,7 +583,7 @@ def append_thin_with_length(thin_element, length, element_name):
571583 )
572584 elif d ["type" ] == "vkicker" :
573585 ds = d .get ("l" , 0.0 )
574- tilt_degree = d .get ("tilt" , 0.0 ) * 180.0 / math . pi
586+ tilt_degree = d .get ("tilt" , 0.0 ) * rad_to_deg
575587 if abs (d .get ("sinkick" , 0.0 )) > 0 :
576588 # TODO(audit): Map sinusoidally driven kicker modes (SINKICK/SINPEAK/SINTUNE/SINPHASE).
577589 _warn (
@@ -601,7 +613,7 @@ def append_thin_with_length(thin_element, length, element_name):
601613 ds = d .get ("l" , 0.0 )
602614 k2 = d .get ("k2" , 0.0 )
603615 k2s = d .get ("k2s" , 0.0 )
604- tilt_degree = d .get ("tilt" , 0.0 ) * 180.0 / math . pi
616+ tilt_degree = d .get ("tilt" , 0.0 ) * rad_to_deg
605617 if ds > 0 :
606618 impactx_beamline .append (
607619 elements .ExactMultipole (
@@ -633,7 +645,7 @@ def append_thin_with_length(thin_element, length, element_name):
633645 ds = d .get ("l" , 0.0 )
634646 k3 = d .get ("k3" , 0.0 )
635647 k3s = d .get ("k3s" , 0.0 )
636- tilt_degree = d .get ("tilt" , 0.0 ) * 180.0 / math . pi
648+ tilt_degree = d .get ("tilt" , 0.0 ) * rad_to_deg
637649 if ds > 0 :
638650 impactx_beamline .append (
639651 elements .ExactMultipole (
@@ -668,7 +680,7 @@ def append_thin_with_length(thin_element, length, element_name):
668680 ksl = d .get ("ksl" , [])
669681 ds = d .get ("l" , 0.0 )
670682 lrad = d .get ("lrad" , 0.0 )
671- tilt_degree = d .get ("tilt" , 0.0 ) * 180.0 / math . pi
683+ tilt_degree = d .get ("tilt" , 0.0 ) * rad_to_deg
672684 max_order = max (len (knl ), len (ksl )) - 1
673685
674686 if ds > 0.0 :
@@ -746,7 +758,7 @@ def append_thin_with_length(thin_element, length, element_name):
746758 lag = d .get ("lag" , 0.0 ) # fraction of 2pi
747759 harmon = d .get ("harmon" , 1.0 )
748760 phase = lag * 360.0 # convert to degrees
749- tilt_degree = d .get ("tilt" , 0.0 ) * 180.0 / math . pi
761+ tilt_degree = d .get ("tilt" , 0.0 ) * rad_to_deg
750762 if "freq" in d and d .get ("freq" , 0.0 ) > 0.0 :
751763 # MAD-X RFCAVITY frequency is specified in MHz.
752764 freq = d .get ("freq" , 0.0 ) * 1.0e6
0 commit comments