1use std::{cell::RefCell, collections::HashMap, sync::LazyLock};
2
3use itertools::Itertools;
4use linnet::half_edge::involution::{EdgeIndex, Orientation};
5
6use spenso::{
7 network::{library::symbolic::ETS, tags::SPENSO_TAG},
8 shadowing::symbolica_utils::SpensoPrintSettings,
9 structure::{
10 abstract_index::AIND_SYMBOLS,
11 concrete_index::ExpandedIndex,
12 representation::{Minkowski, RepName},
13 },
14 utils::{to_subscript, to_superscript},
15};
16use symbolica::{
17 atom::{Atom, AtomCore, AtomOrView, AtomView, FunctionBuilder, Symbol},
18 domains::rational::Rational,
19 function, get_symbol,
20 id::Replacement,
21 printer::{PrintState, PrintUserData},
22 symbol,
23};
24use symbolica_utils::{PrintSettingsExt, TypstMode};
25
26use crate::{cff::orientations::GraphOrientation, graph::LoopMomentumBasis};
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
32#[repr(i64)]
33pub(crate) enum UvMomentumProvenanceRole {
34 TaylorFixed = 0,
38 DenominatorDerived = 1,
42 PhysicalSourceFixed = 3,
45 DenominatorDerivedSoft = 4,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
54pub(crate) struct UvDenominatorClassId(pub usize);
55
56impl From<bool> for UvMomentumProvenanceRole {
57 fn from(denominator_derived: bool) -> Self {
58 if denominator_derived {
59 Self::DenominatorDerived
60 } else {
61 Self::TaylorFixed
62 }
63 }
64}
65
66pub struct WildCards {
67 pub edgeid_: Symbol,
68 pub mom_: Symbol,
69 pub mass_: Symbol,
70 pub prop_: Symbol,
71 pub a_: Symbol,
72 pub b_: Symbol,
73 pub c_: Symbol,
74 pub d_: Symbol,
75 pub e_: Symbol,
76 pub f_: Symbol,
77 pub g_: Symbol,
78 pub h_: Symbol,
79 pub i_: Symbol,
80 pub j_: Symbol,
81 pub k_: Symbol,
82 pub l_: Symbol,
83 pub m_: Symbol,
84 pub n_: Symbol,
85 pub o_: Symbol,
86 pub p_: Symbol,
87 pub q_: Symbol,
88 pub r_: Symbol,
89 pub s_: Symbol,
90 pub t_: Symbol,
91 pub u_: Symbol,
92 pub v_: Symbol,
93 pub w_: Symbol,
94 pub x_: Symbol,
95 pub y_: Symbol,
96 pub z_: Symbol,
97 pub a__: Symbol,
98 pub b__: Symbol,
99 pub c__: Symbol,
100 pub d__: Symbol,
101 pub e__: Symbol,
102 pub f__: Symbol,
103 pub g__: Symbol,
104 pub h__: Symbol,
105 pub i__: Symbol,
106 pub j__: Symbol,
107 pub k__: Symbol,
108 pub l__: Symbol,
109 pub m__: Symbol,
110 pub n__: Symbol,
111 pub o__: Symbol,
112 pub p__: Symbol,
113 pub q__: Symbol,
114 pub r__: Symbol,
115 pub s__: Symbol,
116 pub t__: Symbol,
117 pub u__: Symbol,
118 pub v__: Symbol,
119 pub w__: Symbol,
120 pub x__: Symbol,
121 pub y__: Symbol,
122 pub z__: Symbol,
123 pub a___: Symbol,
124 pub b___: Symbol,
125 pub c___: Symbol,
126 pub d___: Symbol,
127 pub e___: Symbol,
128 pub f___: Symbol,
129 pub g___: Symbol,
130 pub h___: Symbol,
131 pub i___: Symbol,
132 pub j___: Symbol,
133 pub k___: Symbol,
134 pub l___: Symbol,
135 pub m___: Symbol,
136 pub n___: Symbol,
137 pub o___: Symbol,
138 pub p___: Symbol,
139 pub q___: Symbol,
140 pub r___: Symbol,
141 pub s___: Symbol,
142 pub t___: Symbol,
143 pub u___: Symbol,
144 pub v___: Symbol,
145 pub w___: Symbol,
146 pub x___: Symbol,
147 pub y___: Symbol,
148 pub z___: Symbol,
149}
150
151pub struct GammaloopSymbols {
152 pub integrand: Symbol,
153 pub residue_map_id: Symbol,
156 pub tree_denom_wrapper: Symbol,
158
159 pub killing_func: Symbol,
160 pub is_function: Symbol,
161 pub is_symbol: Symbol,
162 pub ufozero: Symbol,
163 pub _linear: Symbol,
164 pub linearize: Symbol,
165 pub loop_mom: Symbol,
166 pub edgeid: Symbol,
167 pub uvaind: Symbol,
168 pub edgeaind: Symbol,
169 pub dummyaind: Symbol,
170 pub hedgeaind: Symbol,
171 pub vertexaind: Symbol,
172 pub vertexid: Symbol,
173 pub source_id: Symbol,
174 pub sink_id: Symbol,
175 pub orientation_delta: Symbol,
176 pub ubar: Symbol,
177 pub hfunction_lu_cut: Symbol,
178 pub hfunction_left_th: Symbol,
179 pub hfunction_right_th: Symbol,
180 pub eta: Symbol,
181 pub eta_left: Symbol,
182 pub eta_right: Symbol,
183 pub deta_lu_cut: Symbol,
184 pub deta_left_th: Symbol,
185 pub deta_right_th: Symbol,
186 pub vbar: Symbol,
187 pub ose: Symbol,
188 pub on_shell_energy: Symbol,
191 pub esurface: Symbol,
193 pub energy: Symbol,
194 pub v: Symbol,
195 pub u: Symbol,
196 pub color_wrap: Symbol,
197 pub dim_epsilon: Symbol,
199
200 pub epsilon: Symbol,
201 pub epsilonbar: Symbol,
202 pub rescale: Symbol,
203 pub integrated_loop_scale: Symbol,
205 pub rescale_mass: Symbol,
206 pub rescale_star: Symbol,
207 pub pi: Symbol,
208
209 pub m_uv_expansion: Symbol,
215 pub m_uv_vacuum: Symbol,
220 pub renormalization_localization_scale: Symbol,
222 pub mu_r_sq: Symbol,
223 pub numerator_sampling_scale: Symbol,
224 pub sign: Symbol,
225 pub theta: Symbol,
226 pub broadcasting_sqrt: Symbol,
227 pub heaviside: Symbol,
228 pub selected: Symbol,
230
231 pub expansion: Symbol,
233 pub delta_vec: Symbol,
235 pub emr_mom: Symbol,
237 pub uv_momentum_provenance: Symbol,
243 pub uv_class: Symbol,
245 pub emr_vec: Symbol,
248 pub dot: Symbol,
249 pub external_mom: Symbol,
250 pub dim: Symbol,
251 pub coeff: Symbol,
252
253 pub localizing_integrand: Symbol,
254
255 pub uv_subgraph: Symbol,
256 pub uv_approx: Symbol,
257 pub uv_integrate: Symbol,
258 pub uv_series: Symbol,
259 pub uv_truncate: Symbol,
260 pub ct_marker: Symbol,
262
263 pub nc2_1: Symbol,
264 pub expr: Symbol,
265 pub num: Symbol,
266 pub den: Symbol,
269 pub radius_left: Symbol,
270 pub radius_star_left: Symbol,
271 pub uv_damp_plus_left: Symbol,
272 pub uv_damp_minus_left: Symbol,
273 pub radius_right: Symbol,
274 pub radius_star_right: Symbol,
275 pub uv_damp_plus_right: Symbol,
276 pub uv_damp_minus_right: Symbol,
277 pub thermal_distribution: Symbol,
278 pub thermal_weight_wrapper: Symbol,
279 pub inverse_temperature: Symbol,
280}
281
282impl GammaloopSymbols {
283 pub fn collect_orientation_if<'a>(&self, arg: impl Into<AtomOrView<'a>>) -> Atom {
284 let arg = arg.into();
285 if !arg.as_view().contains_symbol(self.theta) && !arg.as_view().contains_symbol(Symbol::IF)
286 {
287 return arg.into_owned();
288 }
289 let mut collected = arg
290 .replace(self.sign_theta(W_.a_))
291 .with(Symbol::IF.call(Atom::var(W_.a_) + 1))
292 .replace(Symbol::IF.call(W_.a_) * Symbol::IF.call(W_.b_))
293 .repeat()
294 .with(Symbol::IF.call(W_.a_ * W_.b_))
295 .replace(Symbol::IF.call(W_.a_) * W_.b___)
296 .with(Symbol::IF.call_args([Atom::var(W_.a_), Atom::var(W_.b___), Atom::Zero]))
297 .replace(Symbol::IF.call_args([Atom::var(W_.a_), Atom::Zero]))
298 .with(Symbol::IF.call_args([Atom::var(W_.a_), Atom::one(), Atom::Zero]))
299 .replace(Symbol::IF.call_args([Atom::var(W_.a_)]))
300 .with(Symbol::IF.call_args([Atom::var(W_.a_), Atom::one(), Atom::Zero]));
301 collected.repeat_map(Self::collect_orientation_if_terms);
307 collected
308 }
309
310 fn collect_orientation_if_terms(expr: AtomView<'_>) -> Atom {
316 fn key_and_body(term: AtomView<'_>) -> Option<(AtomView<'_>, AtomView<'_>)> {
317 let AtomView::Fun(call) = term else {
318 return None;
319 };
320 let (key, zero, body) = call.iter().collect_tuple()?;
321 (call.get_symbol() == Symbol::IF && zero == 0).then_some((key, body))
322 }
323
324 expr.replace_map(|term, _, out| {
325 if let AtomView::Fun(call) = term
326 && call.get_symbol() != Symbol::IF
327 {
328 out.set_from_view(&term);
331 return;
332 }
333 if let AtomView::Mul(product) = term {
334 let Some((key, _)) = product.iter().find_map(key_and_body) else {
335 return;
336 };
337 let body = Atom::mul_many(product.iter().map(|factor| {
340 key_and_body(factor)
341 .filter(|(factor_key, _)| *factor_key == key)
342 .map_or(factor, |(_, body)| body)
343 }));
344 let guarded = Symbol::IF.call_args([key, Atom::Zero.as_view(), body.as_view()]);
345 **out = Self::collect_orientation_if_terms(guarded.as_view());
346 return;
347 }
348 let AtomView::Add(sum) = term else {
349 return;
350 };
351 let mut groups = HashMap::<_, Vec<_>>::new();
352 for term in sum.iter() {
353 if let Some((key, _)) = key_and_body(term) {
354 groups.entry(key).or_default().push(term);
355 }
356 }
357 groups.retain(|_, group| group.len() > 1);
358 if groups.is_empty() {
359 return;
360 }
361 let mut terms = sum
362 .iter()
363 .filter(|&term| {
364 key_and_body(term).is_none_or(|(key, _)| !groups.contains_key(&key))
365 })
366 .map(AtomOrView::from)
367 .collect_vec();
368 for (key, mut group) in groups {
369 group.sort_by(|a, b| b.get_data().cmp(a.get_data()));
373 let mut group = group.into_iter().map(AtomOrView::from).collect_vec();
374 while group.len() > 1 {
375 let pair = [group.pop().unwrap(), group.pop().unwrap()];
376 let bodies = pair
377 .iter()
378 .map(|term| key_and_body(term.as_view()).unwrap().1);
379 let body = Atom::add_many(bodies);
380 let merged = Symbol::IF.call_args([key, Atom::Zero.as_view(), body.as_view()]);
381 let data = merged.as_view().get_data();
382 let at = group.partition_point(|term| term.as_view().get_data() > data);
383 group.insert(at, merged.into());
384 }
385 terms.extend(group);
386 }
387 **out = Self::collect_orientation_if_terms(Atom::add_many(terms).as_view());
388 })
389 }
390
391 pub fn den<'a>(
392 &self,
393 eid: impl Into<AtomOrView<'a>>,
394 mom: impl Into<AtomOrView<'a>>,
395 mass: impl Into<AtomOrView<'a>>,
396 full_expr: impl Into<AtomOrView<'a>>,
397 ) -> Atom {
398 self.den.call_args([
399 eid.into().as_view(),
400 mom.into().as_view(),
401 mass.into().as_view(),
402 full_expr.into().as_view(),
403 ])
404 }
405
406 pub(crate) fn orientation_delta<O: GraphOrientation>(&self, orientation: &O) -> Atom {
407 let args: Vec<i32> = orientation
408 .orientation()
409 .iter()
410 .map(|(_, t)| match t {
411 Orientation::Default => 1,
412 Orientation::Reversed => -1,
413 Orientation::Undirected => 0,
414 })
415 .collect_vec();
416 FunctionBuilder::new(self.orientation_delta)
417 .add_args(&args)
418 .finish()
419 }
420
421 pub(crate) fn sign_theta<'a>(&self, arg: impl Into<AtomOrView<'a>>) -> Atom {
422 let arg = arg.into();
423
424 function!(self.theta, arg.as_view())
425 }
426
427 pub(crate) fn sign(&self, edge: EdgeIndex) -> Atom {
428 function!(self.sign, Atom::num(edge.0 as i64))
429 }
430}
431
432pub static W_: LazyLock<WildCards> = LazyLock::new(|| WildCards {
433 edgeid_: symbol!("eid_"),
434 mom_: symbol!("mom_"),
435 mass_: symbol!("mass_"),
436 prop_: symbol!("prop_"),
437 a_: symbol!("a_"),
438 b_: symbol!("b_"),
439 c_: symbol!("c_"),
440 d_: symbol!("d_"),
441 e_: symbol!("e_"),
442 f_: symbol!("f_"),
443 g_: symbol!("g_"),
444 h_: symbol!("h_"),
445 i_: symbol!("i_"),
446 j_: symbol!("j_"),
447 k_: symbol!("k_"),
448 l_: symbol!("l_"),
449 m_: symbol!("m_"),
450 n_: symbol!("n_"),
451 o_: symbol!("o_"),
452 p_: symbol!("p_"),
453 q_: symbol!("q_"),
454 r_: symbol!("r_"),
455 s_: symbol!("s_"),
456 t_: symbol!("t_"),
457 u_: symbol!("u_"),
458 v_: symbol!("v_"),
459 w_: symbol!("w_"),
460 x_: symbol!("x_"),
461 y_: symbol!("y_"),
462 z_: symbol!("z_"),
463 a__: symbol!("a__"),
464 b__: symbol!("b__"),
465 c__: symbol!("c__"),
466 d__: symbol!("d__"),
467 e__: symbol!("e__"),
468 f__: symbol!("f__"),
469 g__: symbol!("g__"),
470 h__: symbol!("h__"),
471 i__: symbol!("i__"),
472 j__: symbol!("j__"),
473 k__: symbol!("k__"),
474 l__: symbol!("l__"),
475 m__: symbol!("m__"),
476 n__: symbol!("n__"),
477 o__: symbol!("o__"),
478 p__: symbol!("p__"),
479 q__: symbol!("q__"),
480 r__: symbol!("r__"),
481 s__: symbol!("s__"),
482 t__: symbol!("t__"),
483 u__: symbol!("u__"),
484 v__: symbol!("v__"),
485 w__: symbol!("w__"),
486 x__: symbol!("x__"),
487 y__: symbol!("y__"),
488 z__: symbol!("z__"),
489 a___: symbol!("a___"),
490 b___: symbol!("b___"),
491 c___: symbol!("c___"),
492 d___: symbol!("d___"),
493 e___: symbol!("e___"),
494 f___: symbol!("f___"),
495 g___: symbol!("g___"),
496 h___: symbol!("h___"),
497 i___: symbol!("i___"),
498 j___: symbol!("j___"),
499 k___: symbol!("k___"),
500 l___: symbol!("l___"),
501 m___: symbol!("m___"),
502 n___: symbol!("n___"),
503 o___: symbol!("o___"),
504 p___: symbol!("p___"),
505 q___: symbol!("q___"),
506 r___: symbol!("r___"),
507 s___: symbol!("s___"),
508 t___: symbol!("t___"),
509 u___: symbol!("u___"),
510 v___: symbol!("v___"),
511 w___: symbol!("w___"),
512 x___: symbol!("x___"),
513 y___: symbol!("y___"),
514 z___: symbol!("z___"),
515});
516
517macro_rules! spenso_print_scripted_indexed {
518 ($a:ident, $opt:ident, $symbol:expr) => {
519 spenso_print_scripted_indexed!($a, $opt, $symbol, $symbol)
520 };
521 ($a:ident, $opt:ident, $symbol:expr, $typst_symbol:expr) => {{
522 match $opt.custom_print_mode.get("spenso") {
523 Some(PrintUserData::Integer(i)) => {
524 let SpensoPrintSettings {
525 parens,
526 symbol_scripts,
527 commas,
528 with_dim,
529 ..
530 } = SpensoPrintSettings::from(*i as usize);
531
532 let AtomView::Fun(f) = $a else {
533 return None;
534 };
535
536 let mut argiter = f.iter();
537 let id = argiter.next()?;
538 let Ok(i) = usize::try_from(id) else {
539 return None;
540 };
541
542 let is_typst = $opt.typst_mode().is_some();
543 let mut out = if is_typst {
544 $typst_symbol.to_string()
545 } else {
546 $symbol.to_string()
547 };
548 if is_typst {
549 out.push('_');
550 out.push_str(&i.to_string());
551 } else {
552 out.push_str(&to_subscript(i as isize));
553 }
554 if $opt.color_builtin_symbols && !is_typst {
555 out = nu_ansi_term::Color::Magenta.paint(out).to_string();
556 }
557
558 let mut printed_args = false;
559 for arg in argiter {
560 let hidden_representation = matches!(
561 arg,
562 AtomView::Fun(a)
563 if a.get_symbol().has_tag(&SPENSO_TAG.representation)
564 && a.get_nargs() == 1
565 && !with_dim
566 );
567 if hidden_representation {
568 continue;
569 }
570
571 if printed_args {
572 out.push(if commas { ',' } else { ' ' });
573 } else {
574 if symbol_scripts {
575 out.push('^');
576 }
577 if parens {
578 out.push('(');
579 }
580 printed_args = true;
581 }
582
583 arg.format(&mut out, $opt, PrintState::new()).unwrap();
584 }
585 if printed_args && parens {
586 out.push(')');
587 }
588 Some(out)
589 }
590 _ => None,
591 }
592 }};
593}
594
595macro_rules! spenso_print_simple_indexed {
596 ($a:ident, $opt:ident, $symbol:expr) => {
597 spenso_print_simple_indexed!($a, $opt, $symbol, $symbol)
598 };
599 ($a:ident, $opt:ident, $symbol:expr, $typst_symbol:expr) => {{
600 match $opt.custom_print_mode.get("spenso") {
601 Some(PrintUserData::Integer(_)) => {
602 let AtomView::Fun(f) = $a else {
603 return None;
604 };
605
606 let mut out = $symbol.to_string();
607 let mut args = f.iter();
608
609 let id = args.next().unwrap();
610 let Ok(i) = usize::try_from(id) else {
611 return None;
612 };
613
614 if $opt.typst_mode().is_some() {
615 out = $typst_symbol.to_string();
616 out.push('_');
617 out.push_str(&i.to_string());
618 } else {
619 out.push_str(&to_subscript(i as isize));
620 }
621 let mut first = true;
622 for arg in args {
623 if first {
624 first = false;
625 out.push('(');
626 } else {
627 out.push(',');
628 }
629 arg.format(&mut out, $opt, PrintState::new()).unwrap();
630 }
631 if !first {
632 out.push(')');
633 }
634 Some(out)
635 }
636 _ => None,
637 }
638 }};
639}
640
641macro_rules! spenso_print_uv_unary {
642 ($a:ident, $opt:ident, $prefix:expr, $suffix:expr) => {{
643 match $opt.custom_print_mode.get("spenso") {
644 Some(PrintUserData::Integer(_)) => {
645 let AtomView::Fun(f) = $a else {
646 return None;
647 };
648 if f.get_nargs() != 1 {
649 return None;
650 }
651
652 let mut out = $prefix.to_string();
653 f.iter()
654 .next()
655 .unwrap()
656 .format(&mut out, $opt, PrintState::new())
657 .unwrap();
658 out.push_str($suffix);
659 Some(out)
660 }
661 _ => None,
662 }
663 }};
664}
665
666spenso::symbolica_init_lazy_static! {
667pub static GS, GS_INNER: GammaloopSymbols = || GammaloopSymbols {
668 renormalization_localization_scale: symbol!("rls"),
669 integrand: symbol!("integrand"),
670 residue_map_id: symbol!("residue_map_id"),
671 tree_denom_wrapper: symbol!("tree_denoms"),
672 dim_epsilon: symbol!("ε"),
673 killing_func: symbol!(
674 "killing_func",
675 norm = |f, out| {
676 if let AtomView::Fun(_f) = f {
677 **out = Atom::one()
678 }
679 }
680 ),
681 ufozero: symbol!(
682 "UFO::ZERO",
683 norm = |_, out| {
684 **out = Atom::Zero;
685 }
686 ),
687
688 localizing_integrand: symbol!("int_loc"),
689 uvaind: symbol!(
690 "uvind",
691 print = |a, opt, _state| {
692 match opt.custom_print_mode.get("spenso") {
693 Some(PrintUserData::Integer(_i)) => {
694 let AtomView::Fun(f) = a else {
695 return None;
696 };
697
698 let mut out = "ᵘ".to_string();
699 let mut first = true;
700 for arg in f.iter() {
701 let Ok(i) = isize::try_from(arg) else {
702 return None;
703 };
704
705 if !first {
706 out.push('.');
707 } else {
708 first = false;
709 }
710 out.push_str(&to_superscript(i));
711 }
712 Some(out)
713 }
714 _ => None,
715 }
716 },
717 tags = [SPENSO_TAG.index.clone()]
718 ),
719 edgeaind: symbol!(
720 "edge",
721 print = |a, opt, _state| {
722 match opt.custom_print_mode.get("spenso") {
723 Some(PrintUserData::Integer(_i)) => {
724 let AtomView::Fun(f) = a else {
725 return None;
726 };
727
728 let mut out = "ᵉ".to_string();
729 let mut first = true;
730 for arg in f.iter() {
731 let Ok(i) = isize::try_from(arg) else {
732 return None;
733 };
734
735 if !first {
736 out.push('.');
737 }
738 first = false;
739
740 out.push_str(&to_superscript(i));
741 }
742 Some(out)
743 }
744 _ => None,
745 }
746 },
747 tags = [SPENSO_TAG.index.clone()]
748 ),
749 vertexaind: symbol!(
750 "vertex",
751 print = |a, opt, _state| {
752 match opt.custom_print_mode.get("spenso") {
753 Some(PrintUserData::Integer(_i)) => {
754 let AtomView::Fun(f) = a else {
755 return None;
756 };
757
758 let mut out = "ᵛ".to_string();
759
760 let mut first = true;
761 for arg in f.iter() {
762 let Ok(i) = isize::try_from(arg) else {
763 return None;
764 };
765
766 if !first {
767 out.push('.');
768 }
769 first = false;
770
771 out.push_str(&to_superscript(i));
772 }
773 Some(out)
774 }
775 _ => None,
776 }
777 },
778 tags = [SPENSO_TAG.index.clone()]
779 ),
780 dummyaind: symbol!(
781 "dummy",
782 print = |a, opt, _state| {
783 match opt.custom_print_mode.get("spenso") {
784 Some(PrintUserData::Integer(_i)) => {
785 let AtomView::Fun(f) = a else {
786 return None;
787 };
788
789 let mut out = "ᵈ".to_string();
790 let mut first = true;
791 for arg in f.iter() {
792 let Ok(i) = isize::try_from(arg) else {
793 return None;
794 };
795
796 if !first {
797 out.push('.');
798 }
799 first = false;
800
801 out.push_str(&to_superscript(i));
802 }
803 Some(out)
804 }
805 _ => None,
806 }
807 },
808 tags = [SPENSO_TAG.index.clone()]
809 ),
810 hedgeaind: symbol!(
811 "hedge",
812 print = |a, opt, _state| {
813 match opt.custom_print_mode.get("spenso") {
814 Some(PrintUserData::Integer(i)) => {
815 let AtomView::Fun(f) = a else {
816 return None;
817 };
818 let SpensoPrintSettings {
819 index_subscripts, ..
820 } = SpensoPrintSettings::from(*i as usize);
821
822 let mut out = "".to_string();
823 let mut first = true;
824 for arg in f.iter() {
825 let Ok(i) = isize::try_from(arg) else {
826 return None;
827 };
828
829 if !first {
830 out.push('.');
831 }
832 first = false;
833 if index_subscripts {
834 out.push_str(&to_superscript(i));
835 } else {
836 out.push_str(&to_subscript(i));
837 }
838 }
839 Some(out)
840 }
841 _ => None,
842 }
843 },
844 tags = [SPENSO_TAG.index.clone()]
845 ),
846 uv_subgraph: symbol!(
847 "gammalooprs::uv::subgraph",
848 print = |a, opt, _state| {
849 match opt.custom_print_mode.get("spenso") {
850 Some(PrintUserData::Integer(_)) => {
851 let AtomView::Fun(f) = a else {
852 return None;
853 };
854 if f.get_nargs() != 2 {
855 return None;
856 }
857
858 let mut args = f.iter();
859 let current = args.next().unwrap();
860 let mut out = String::new();
861 if let AtomView::Var(current) = current
862 && current.get_symbol().get_stripped_name().starts_with("S_")
863 {
864 out.push_str(current.get_symbol().get_stripped_name());
865 } else {
866 out.push_str("S_");
867 current
868 .format(&mut out, opt, PrintState::new())
869 .unwrap();
870 }
871 out.push('⊛');
872 let given = args.next().unwrap();
873 if let AtomView::Var(given) = given
874 && let Some(label) =
875 given.get_symbol().get_stripped_name().strip_prefix("S_")
876 {
877 out.push_str(label);
878 } else {
879 given.format(&mut out, opt, PrintState::new()).unwrap();
880 }
881 Some(out)
882 }
883 _ => None,
884 }
885 }
886 ),
887 uv_approx: symbol!(
888 "Approx",
889 print = |a, opt, _state| spenso_print_uv_unary!(a, opt, "K[", "]")
890 ),
891 uv_integrate: symbol!(
892 "Integrate",
893 print = |a, opt, _state| spenso_print_uv_unary!(a, opt, "⟨", "⟩")
894 ),
895 uv_series: symbol!(
896 "Series",
897 print = |a, opt, _state| spenso_print_uv_unary!(a, opt, "Σ(", ")")
898 ),
899 uv_truncate: symbol!(
900 "Truncate",
901 print = |a, opt, _state| {
902 let prefix = if opt.typst_mode().is_some() {
903 r#"op("Tr")("#
904 } else {
905 "Tr("
906 };
907 spenso_print_uv_unary!(a, opt, prefix, ")")
908 }
909 ),
910 ct_marker: symbol!(
911 "CT",
912 print = |a, opt, _state| {
913 spenso_print_uv_unary!(a, opt, "", "").map(|out| {
914 if opt.color_builtin_symbols {
915 nu_ansi_term::Color::Magenta.paint(out).to_string()
916 } else {
917 out
918 }
919 })
920 }
921 ),
922 is_function: symbol!("is_function"),
923 is_symbol: symbol!("is_symbol"),
924 nc2_1: symbol!("NC2_1"),
925 rescale: symbol!("t";Scalar,Real,Positive),
926 integrated_loop_scale: symbol!("uvIntegratedLoopScale";Scalar,Real,Positive),
927 rescale_mass: symbol!("t_m";Scalar,Real,Positive),
928 _linear: symbol!("_linear";Linear),
929 linearize: symbol!(
930 "linearize",
931 norm = |f, out| {
932 if let AtomView::Fun(ff) = f
933 && ff.get_nargs() == 1
934 && let AtomView::Fun(arg) = ff.iter().next().unwrap()
935 {
936 let mut args = vec![];
937 for a in arg.iter() {
938 if let AtomView::Fun(a) = a
939 && a.get_symbol().get_wildcard_level() > 0
940 {
941 return;
942 }
943 if let AtomView::Var(a) = a
944 && a.get_symbol().get_wildcard_level() > 0
945 {
946 return;
947 }
948
949 args.push(a);
950 }
951 let expr = GS
952 ._linear
953 .call_args(args.as_slice())
954 .replace(GS._linear.call_args([W_.a___]))
955 .with(arg.get_symbol().call_args([W_.a___]));
956 println!(":{}->{}", f, expr);
957 **out = expr;
958 }
959 }
960 ),
961 broadcasting_sqrt: symbol!(
962 "broadcasting_sqrt",
963 tag = SPENSO_TAG.broadcast,
964 der = |a, _, out| {
965 **out = Atom::num(1) / (Atom::num(2) * a);
966 }
967 ),
968 heaviside: symbol!("heaviside"),
969 expansion: symbol!("expansion"),
970 rescale_star: symbol!("t⃰"),
971 hfunction_lu_cut: symbol!("h_lu_cut"),
972 hfunction_left_th: symbol!("h_left_th"),
973 hfunction_right_th: symbol!("h_right_th"),
974 eta: symbol!("η"),
975 eta_left: symbol!("η_left"),
976 eta_right: symbol!("η_right"),
977 deta_lu_cut: symbol!("∇η_lu_cut"),
978 deta_left_th: symbol!("∇η_left_th"),
979 deta_right_th: symbol!("∇η_right_th"),
980 edgeid: symbol!("eid"),
981 vertexid: symbol!("vid"),
982 source_id: symbol!("source"),
983 sink_id: symbol!("sink"),
984 sign: symbol!("σ"; Scalar),
985 selected: symbol!("selected"),
986 theta: symbol!("θ"),
987 m_uv_expansion: symbol!(
988 "mUVexp",
989 print = |a, opt, _state| {
990 let AtomView::Var(_a) = a else {
991 return None;
992 };
993 match opt.custom_print_mode.get("spenso") {
994 Some(PrintUserData::Integer(_)) if opt.typst_mode().is_some() => {
995 Some("m_\"UVexp\"".to_string())
996 }
997 _ => None,
998 }
999 }
1000 ),
1001 m_uv_vacuum: symbol!(
1002 "mUV",
1003 print = |a, opt, _state| {
1004 let AtomView::Var(_a) = a else {
1005 return None;
1006 };
1007 match opt.custom_print_mode.get("spenso") {
1008 Some(PrintUserData::Integer(_)) if opt.typst_mode().is_some() => {
1009 Some("m_\"UV\"".to_string())
1010 }
1011 _ => None,
1012 }
1013 }
1014 ),
1015 mu_r_sq: symbol!(
1016 "μᵣ²",
1017 print = |a, opt, _state| {
1018 let AtomView::Var(_a) = a else {
1019 return None;
1020 };
1021 match opt.custom_print_mode.get("spenso") {
1022 Some(PrintUserData::Integer(_)) if opt.typst_mode().is_some() => {
1023 Some("mu_r^2".to_string())
1024 }
1025 _ => None,
1026 }
1027 }
1028 ),
1029 numerator_sampling_scale: symbol!("M"),
1030 delta_vec: ETS.delta,
1031 expr: symbol!(
1032 "expr",
1033 print = |_, opt, _state| {
1034 Some(match opt.typst_mode()? {
1035 TypstMode::Math | TypstMode::Markup => "#expr".to_string(),
1036 TypstMode::Code => "expr".to_string(),
1037 })
1038 }
1039 ),
1040 num: symbol!("num"),
1041 den: symbol!(
1042 "denom",
1043 der = |_, arg, out| {
1044 if arg != 3 {
1045 **out = Atom::Zero;
1046 } else {
1047 **out = Atom::num(1);
1048 }
1049 }
1050 ),
1051 ubar: symbol!(
1052 "ubar",
1053 print = |a, opt, _state| {
1054 spenso_print_scripted_indexed!(a, opt, "u̅", "overline(u)")
1055 },
1056 tags = [SPENSO_TAG.rank1.clone(), SPENSO_TAG.tensor.clone()]
1057 ),
1058 vbar: symbol!(
1059 "vbar",
1060 print = |a, opt, _state| {
1061 spenso_print_scripted_indexed!(a, opt, "v̅", "overline(v)")
1062 },
1063 tags = [SPENSO_TAG.rank1.clone(), SPENSO_TAG.tensor.clone()]
1064 ),
1065 dot: symbol!("dot"),
1066 dim: symbol!("dim"),
1067 v: symbol!(
1068 "v",
1069 print = |a, opt, _state| { spenso_print_scripted_indexed!(a, opt, "v") },
1070 tags = [SPENSO_TAG.rank1.clone(), SPENSO_TAG.tensor.clone()]
1071 ),
1072 u: symbol!(
1073 "u",
1074 print = |a, opt, _state| { spenso_print_scripted_indexed!(a, opt, "u") },
1075 tags = [SPENSO_TAG.rank1.clone(), SPENSO_TAG.tensor.clone()]
1076 ),
1077 emr_mom: symbol!(
1078 "Q",
1079 print = |a, opt, _state| { spenso_print_scripted_indexed!(a, opt, "q") },
1080 tags = [SPENSO_TAG.rank1.clone(), SPENSO_TAG.tensor.clone()]
1081 ),
1082 uv_momentum_provenance: symbol!("gammalooprs::uv::momentum_provenance"),
1083 uv_class: symbol!("gammalooprs::uv::class"),
1084 orientation_delta: symbol!("orientation_delta"),
1085 emr_vec: symbol!(
1086 "Q3",
1087 norm = |f, out| {
1088 if let AtomView::Fun(ff) = f
1089 && ff.get_nargs() == 2
1090 {
1091 let mut iter = ff.iter();
1092 let eid = iter.next().unwrap();
1093 let structure = iter.next().unwrap();
1094 let component = if let AtomView::Fun(cind) = structure
1095 && cind.get_symbol() == AIND_SYMBOLS.cind
1096 && cind.get_nargs() == 1
1097 {
1098 cind.iter()
1099 .next()
1100 .and_then(|i| i64::try_from(i).ok())
1101 .and_then(|i| usize::try_from(i).ok())
1102 } else {
1103 None
1104 };
1105 if i64::try_from(eid).is_ok_and(|eid| eid >= 0)
1106 && let Some(i @ 0..=3) = component
1107 {
1108 if i == 0 {
1109 **out = Atom::Zero;
1110 } else {
1111 let cind = AIND_SYMBOLS.cind.call_args([i as i64]);
1112 **out = get_symbol!("Q")
1113 .unwrap()
1114 .call_args([eid, cind.as_view()])
1115 }
1116 }
1117 }
1118 },
1119 print = |a, opt, _state| {
1120 spenso_print_scripted_indexed!(a, opt, "q⃗", "arrow(q)")
1121 },
1122 tags = [SPENSO_TAG.rank1.clone(), SPENSO_TAG.tensor.clone()]
1123 ),
1124 ose: symbol!(
1125 "OSE"; Scalar;
1126 print = |a, opt, _state| {
1127 spenso_print_simple_indexed!(a, opt, "Eᵒˢ", r#"E^("os")"#)
1128 },
1129 der = |_, arg, out| {
1130 if arg == 1 {
1131 **out = Atom::num(1);
1132 }
1133 }
1134 ),
1135 on_shell_energy: symbol!(
1136 "OnShellEnergy"; Scalar;
1137 norm = |view, out| {
1138 if let AtomView::Fun(call) = view && call.get_nargs() == 2 {
1139 thread_local! {
1143 static CACHE: RefCell<(HashMap<Atom, Atom>, usize)> = RefCell::default();
1144 }
1145 let input = call.get(1);
1146 let cached = CACHE.with_borrow(|(cache, _)| cache.get(input.get_data()).cloned());
1147 let invariant = cached.unwrap_or_else(|| {
1151 let invariant = input.replace_map(|part, _, output| {
1152 if let AtomView::Pow(power) = part {
1153 let (base, exponent) = power.get_base_exp();
1154 if i64::try_from(exponent) == Ok(2) {
1155 let direct = base.expand_num();
1156 let reversed = (-&direct).expand_num();
1157 **output = direct.min(reversed).pow(2);
1158 }
1159 }
1160 }).expand_num();
1161 let bytes = input.get_byte_size() + invariant.as_view().get_byte_size();
1162 const MAX_BYTES: usize = 1024 * 1024;
1163 if bytes <= MAX_BYTES {
1164 CACHE.with_borrow_mut(|(cache, retained_bytes)| {
1165 if cache.len() >= 1024 || *retained_bytes + bytes > MAX_BYTES {
1166 cache.clear();
1167 *retained_bytes = 0;
1168 }
1169 cache.insert(input.to_owned(), invariant.clone());
1170 *retained_bytes += bytes;
1171 });
1172 }
1173 invariant
1174 });
1175 if invariant.as_view() != input {
1176 **out = call.get_symbol().call_args([call.get(0).to_owned(), invariant]);
1177 }
1178 }
1179 },
1180 der = |view, argument, out| {
1181 if let AtomView::Fun(call) = view && call.get_nargs() == 2 {
1182 **out = if argument == 1 {
1183 Atom::num((1, 2)) / view
1184 } else {
1185 Atom::Zero
1186 };
1187 }
1188 }
1189 ),
1190 esurface: symbol!(
1191 "Esurface"; Scalar;
1192 norm = |view, out| {
1193 if let AtomView::Fun(call) = view {
1194 let sum = Atom::add_many(call.iter());
1195 if let AtomView::Add(terms) = sum.as_view() {
1196 let mut poles = std::collections::BTreeMap::<Symbol, i64>::new();
1201 let mut record = |factor: AtomView<'_>| {
1202 if let AtomView::Pow(power) = factor
1203 && let AtomView::Var(variable) = power.get_base()
1204 && let Ok(exponent) = i64::try_from(power.get_exp())
1205 && exponent < 0
1206 {
1207 poles.entry(variable.get_symbol())
1208 .and_modify(|power| *power = (*power).min(exponent))
1209 .or_insert(exponent);
1210 }
1211 };
1212 for term in terms.iter() {
1213 if let AtomView::Mul(product) = term {
1214 product.iter().for_each(&mut record);
1215 } else {
1216 record(term);
1217 }
1218 }
1219 if let Some((variable, exponent)) = poles.first_key_value() {
1220 let factor = Atom::var(*variable).pow(*exponent);
1221 let regular = call.get_symbol().call_args(
1222 terms.iter().map(|term| term / factor.as_view())
1223 );
1224 **out = factor * regular;
1225 return;
1226 }
1227 if !call.iter().eq(terms.iter()) {
1228 **out = call.get_symbol().call_args(terms.iter());
1229 }
1230 } else {
1231 **out = sum;
1232 }
1233 }
1234 },
1235 der = |_, _, out| { **out = Atom::one(); },
1236 eval = symbolica::atom::EvaluationInfo::new()
1237 .register(|arguments: &[f64]| arguments.iter().sum::<f64>())
1238 .register(|arguments: &[symbolica::domains::float::Complex<symbolica::domains::float::Float>]| {
1239 arguments.iter().cloned().reduce(|left, right| left + right)
1240 .expect("a retained CFF surface has at least two summands")
1241 })
1242 ),
1243 energy: symbol!(
1244 "E",
1245 print = |a, opt, _state| { spenso_print_simple_indexed!(a, opt, "E") }
1246 ),
1247 external_mom: symbol!(
1248 "P",
1249 print = |a, opt, _state| { spenso_print_scripted_indexed!(a, opt, "p") },
1250 tags = [SPENSO_TAG.rank1.clone(), SPENSO_TAG.tensor.clone()]
1251 ),
1252 loop_mom: symbol!(
1253 "K",
1254 print = |a, opt, _state| { spenso_print_scripted_indexed!(a, opt, "k") },
1255 tags = [SPENSO_TAG.rank1.clone(), SPENSO_TAG.tensor.clone()]
1256 ),
1257 epsilon: symbol!(
1258 "ϵ",
1259 print = |a, opt, _state| { spenso_print_scripted_indexed!(a, opt, "ϵ") },
1260 tags = [SPENSO_TAG.rank1.clone(), SPENSO_TAG.tensor.clone()]
1261 ),
1262 pi: Symbol::PI,
1263 color_wrap: symbol!("color"),
1264 epsilonbar: symbol!(
1265 "ϵbar",
1266 print = |a, opt, _state| {
1267 spenso_print_scripted_indexed!(a, opt, "ϵ̅", "overline(epsilon.alt)")
1268 },
1269 tags = [SPENSO_TAG.rank1.clone(), SPENSO_TAG.tensor.clone()]
1270 ),
1271 coeff: symbol!("coef"),
1272 radius_left: symbol!("r_left"),
1273 radius_star_left: symbol!("r⃰_left"),
1274 uv_damp_plus_left: symbol!("damp_plus_left"),
1275 uv_damp_minus_left: symbol!("damp_minus_left"),
1276 radius_right: symbol!("r_right"),
1277 radius_star_right: symbol!("r⃰_right"),
1278 uv_damp_plus_right: symbol!("damp_plus_right"),
1279 uv_damp_minus_right: symbol!("damp_minus_right"),
1280 thermal_distribution: three_dimensional_reps::symbols::thermal_distribution_symbol(),
1281 thermal_weight_wrapper: *three_dimensional_reps::symbols::THERMAL_WEIGHT_WRAPPER,
1282 inverse_temperature: symbol!("β"),
1283};
1284}
1285
1286impl GammaloopSymbols {
1287 pub fn wrap_tree_denoms<'a>(&self, arg: impl Into<AtomOrView<'a>>) -> Atom {
1288 self.tree_denom_wrapper.call_args([arg.into().as_view()])
1289 }
1290
1291 pub fn do_dot_product_in_sqrt<'a>(&self, arg: impl Into<AtomOrView<'a>>) -> Atom {
1292 let a = arg.into();
1293
1294 let mink = Minkowski {}.new_rep(4).to_symbolic([Atom::var(W_.i_)]);
1295
1296 let pat = function!(self.emr_vec, W_.a_, mink) * function!(self.emr_vec, W_.b_, mink);
1297 let rhs = function!(
1298 self.emr_mom,
1299 W_.a_,
1300 Atom::from(ExpandedIndex::from_iter([1]))
1301 ) * function!(
1302 self.emr_mom,
1303 W_.b_,
1304 Atom::from(ExpandedIndex::from_iter([1]))
1305 ) + function!(
1306 self.emr_mom,
1307 W_.a_,
1308 Atom::from(ExpandedIndex::from_iter([2]))
1309 ) * function!(
1310 self.emr_mom,
1311 W_.b_,
1312 Atom::from(ExpandedIndex::from_iter([2]))
1313 ) + function!(
1314 self.emr_mom,
1315 W_.a_,
1316 Atom::from(ExpandedIndex::from_iter([3]))
1317 ) * function!(
1318 self.emr_mom,
1319 W_.b_,
1320 Atom::from(ExpandedIndex::from_iter([3]))
1321 );
1322
1323 let a = a.replace(pat).with(rhs);
1324
1325 let pat = function!(self.emr_vec, W_.a_, mink).pow(2);
1326 let rhs = function!(
1327 self.emr_mom,
1328 W_.a_,
1329 Atom::from(ExpandedIndex::from_iter([1]))
1330 )
1331 .pow(2)
1332 + function!(
1333 self.emr_mom,
1334 W_.a_,
1335 Atom::from(ExpandedIndex::from_iter([2]))
1336 )
1337 .pow(2)
1338 + function!(
1339 self.emr_mom,
1340 W_.a_,
1341 Atom::from(ExpandedIndex::from_iter([3]))
1342 )
1343 .pow(2);
1344 a.replace(pat).with(rhs)
1352
1353 }
1369
1370 pub fn to_broadcasting_sqrt<'a>(&self, arg: impl Into<AtomOrView<'a>>) -> Atom {
1371 let a = arg.into();
1372 a.replace_map(|a, _ctx, out| {
1373 if let AtomView::Pow(p) = a {
1374 let (a, b) = p.get_base_exp();
1375 let Ok(exp) = Rational::try_from(b) else {
1376 return;
1377 };
1378
1379 if exp.denominator() == 2 {
1380 **out = self
1381 .broadcasting_sqrt
1382 .call_args([a])
1383 .pow(Atom::num(exp.numerator()));
1384 }
1385 }
1386 })
1387 }
1388
1389 pub(crate) fn heaviside<'a>(&self, arg: impl Into<AtomOrView<'a>>) -> Atom {
1390 let a = arg.into();
1391 function!(self.heaviside, a.as_view())
1392 }
1393
1394 pub(crate) fn emr_mom<'a>(&self, e: EdgeIndex, arg: impl Into<AtomOrView<'a>>) -> Atom {
1395 let a = arg.into();
1396 function!(self.emr_mom, usize::from(e) as i64, a.as_view())
1397 }
1398
1399 pub(crate) fn uv_momentum_provenance_tag<'a>(
1400 &self,
1401 edge: impl Into<AtomOrView<'a>>,
1402 role: impl Into<UvMomentumProvenanceRole>,
1403 momentum: impl Into<AtomOrView<'a>>,
1404 ) -> Atom {
1405 let role = role.into();
1406 self.uv_momentum_provenance.call_args([
1407 edge.into().as_view(),
1408 Atom::num(role as i64).as_view(),
1409 momentum.into().as_view(),
1410 ])
1411 }
1412
1413 pub(crate) fn uv_momentum_provenance_data(
1414 &self,
1415 argument: AtomView<'_>,
1416 ) -> Option<(EdgeIndex, UvMomentumProvenanceRole, Atom)> {
1417 let AtomView::Fun(provenance) = argument else {
1418 return None;
1419 };
1420 if provenance.get_symbol() != self.uv_momentum_provenance || provenance.get_nargs() != 3 {
1421 return None;
1422 }
1423 let edge = EdgeIndex(usize::try_from(provenance.get(0)).ok()?);
1424 let role = match i64::try_from(provenance.get(1)).ok()? {
1425 0 => UvMomentumProvenanceRole::TaylorFixed,
1426 1 => UvMomentumProvenanceRole::DenominatorDerived,
1427 3 => UvMomentumProvenanceRole::PhysicalSourceFixed,
1428 4 => UvMomentumProvenanceRole::DenominatorDerivedSoft,
1429 _ => return None,
1430 };
1431 Some((edge, role, provenance.get(2).to_owned()))
1432 }
1433
1434 pub(crate) fn uv_class_ref(&self, class: UvDenominatorClassId) -> Atom {
1435 self.uv_class.call(class.0)
1436 }
1437
1438 pub(crate) fn uv_class_data(&self, argument: AtomView<'_>) -> Option<UvDenominatorClassId> {
1439 let AtomView::Fun(class) = argument else {
1440 return None;
1441 };
1442 (class.get_symbol() == self.uv_class && class.get_nargs() == 1)
1443 .then(|| usize::try_from(class.get(0)).ok().map(UvDenominatorClassId))
1444 .flatten()
1445 }
1446
1447 pub(crate) fn indexed_momentum(&self, momentum: &Atom, indices: &[Atom]) -> Atom {
1449 momentum.replace_map(|view, _, output| {
1450 let AtomView::Fun(vector) = view else {
1451 return;
1452 };
1453 if vector.get_symbol() == self.emr_mom && vector.get_nargs() == 1 {
1454 let mut component = FunctionBuilder::new(self.emr_mom).add_arg(vector.get(0));
1455 for index in indices {
1456 component = component.add_arg(index);
1457 }
1458 **output = component.finish();
1459 }
1460 })
1461 }
1462
1463 pub(crate) fn erase_uv_momentum_provenance(&self, atom: &Atom) -> Atom {
1468 atom.replace_map(|view, _, output| {
1469 let AtomView::Fun(momentum) = view else {
1470 return;
1471 };
1472 if momentum.get_symbol() != self.emr_mom || momentum.get_nargs() < 1 {
1473 return;
1474 }
1475 let AtomView::Fun(provenance) = momentum.get(0) else {
1476 return;
1477 };
1478 if provenance.get_symbol() != self.uv_momentum_provenance || provenance.get_nargs() != 3
1479 {
1480 return;
1481 }
1482 let indices = momentum
1483 .iter()
1484 .skip(1)
1485 .map(|index| index.to_owned())
1486 .collect::<Vec<_>>();
1487 **output = self.indexed_momentum(&provenance.get(2).to_owned(), &indices);
1488 })
1489 }
1490
1491 pub(crate) fn localizing_integrand(&self, lmb: &LoopMomentumBasis) -> Atom {
1492 let pi_atom = (Symbol::PI).to_atom();
1495 let normalization_term_integral = (pi_atom.pow(2)) / GS.renormalization_localization_scale;
1496
1497 let mut res = Atom::one();
1498
1499 for l in lmb.loop_edges.iter() {
1500 res /= normalization_term_integral.as_view();
1505
1506 let spatial_norm_sq = function!(self.emr_mom, l.0, GS.cind(1)).pow(2)
1507 + function!(self.emr_mom, l.0, GS.cind(2)).pow(2)
1508 + function!(self.emr_mom, l.0, GS.cind(3)).pow(2);
1509
1510 let denominator = spatial_norm_sq
1512 + GS.renormalization_localization_scale * GS.renormalization_localization_scale;
1513 res /= denominator.as_view() * denominator.as_view();
1514 }
1515
1516 if res.is_one() {
1517 res
1518 } else {
1519 function!(self.localizing_integrand, res)
1520 }
1521 }
1522
1523 pub(crate) fn emr_vec<'a>(&self, e: EdgeIndex, structure: impl Into<AtomOrView<'a>>) -> Atom {
1534 function!(
1535 GS.emr_vec,
1536 usize::from(e) as i64,
1537 structure.into().as_view()
1538 )
1539 }
1540
1541 pub(crate) fn cind(&self, e: usize) -> Atom {
1542 AIND_SYMBOLS.cind.call_args([e as i64])
1543 }
1544
1545 pub(crate) fn energy_delta<'a>(&self, index: impl Into<AtomOrView<'a>>) -> Atom {
1546 self.delta_vec
1547 .call_args([self.cind(0), index.into().into_owned()])
1548 }
1549
1550 pub(crate) fn wrap_esurface(&self, sum: &Atom) -> Atom {
1553 if let AtomView::Add(terms) = sum.as_view() {
1554 self.esurface.call_args(terms.iter())
1555 } else {
1556 sum.clone()
1557 }
1558 }
1559
1560 pub(crate) fn retain_esurfaces(&self, cff: &Atom) -> Atom {
1562 cff.replace_map(|view, _, out| {
1563 if let AtomView::Pow(power) = view {
1564 let (base, exponent) = power.get_base_exp();
1565 if matches!(base, AtomView::Add(_))
1566 && i64::try_from(exponent).is_ok_and(|power| power < 0)
1567 && (base.contains_symbol(self.ose) || base.contains_symbol(self.energy))
1568 {
1569 **out = self.wrap_esurface(&base.to_owned()).pow(exponent);
1570 }
1571 }
1572 })
1573 }
1574
1575 pub(crate) fn lower_esurfaces(&self, atom: &Atom) -> Atom {
1578 if !atom.contains_symbol(self.esurface) {
1579 return atom.clone();
1580 }
1581 atom.replace_map_bottom_up(|view, _, out| {
1582 if let AtomView::Fun(call) = view
1583 && call.get_symbol() == self.esurface
1584 {
1585 **out = Atom::add_many(call.iter());
1586 }
1587 })
1588 }
1589
1590 pub(crate) fn ose_full(
1591 &self,
1592 e: EdgeIndex,
1593 lmb_id: EdgeIndex,
1594 e_mass: Atom,
1595 index: Option<Atom>,
1596 ) -> Atom {
1597 let ose = function!(
1598 self.on_shell_energy,
1599 lmb_id.0,
1600 e_mass.pow(2)
1601 + self.emr_vec(e, self.cind(1)).pow(2)
1602 + self.emr_vec(e, self.cind(2)).pow(2)
1603 + self.emr_vec(e, self.cind(3)).pow(2)
1604 );
1605
1606 if let Some(index) = index {
1607 ose * self.energy_delta(index)
1608 } else {
1609 ose
1610 }
1611 }
1612
1613 pub(crate) fn split_mom_pattern(
1614 &self,
1615 e: EdgeIndex,
1616 lmb_id: EdgeIndex,
1617 e_mass: Atom,
1618 ) -> Replacement {
1619 let id = Minkowski {}.to_symbolic([W_.a__]);
1620 Replacement::new(
1621 self.emr_mom(e, &id).to_pattern(),
1622 self.emr_vec(e, &id) + self.ose_full(e, lmb_id, e_mass, Some(id)) * sign_atom(e),
1623 )
1624 }
1625
1626 pub fn split_mom_pattern_simple(&self, e: EdgeIndex) -> Replacement {
1632 let eidc = usize::from(e) as i64;
1633 let index = Minkowski {}.to_symbolic([W_.a__]);
1634 Replacement::new(
1635 self.emr_mom(e, &index).to_pattern(),
1636 function!(GS.ose, eidc) * sign_atom(e) * self.energy_delta(&index)
1637 + function!(GS.emr_vec, eidc, &index),
1638 )
1639 }
1640
1641 pub(crate) fn ose(&self, e: EdgeIndex) -> Atom {
1642 function!(GS.ose, usize::from(e) as i64)
1643 }
1644}
1645
1646pub(crate) fn sign_atom(eid: EdgeIndex) -> Atom {
1647 FunctionBuilder::new(symbol!("σ"))
1648 .add_arg(usize::from(eid) as i64)
1649 .finish()
1650}
1651
1652#[cfg(test)]
1653mod tests {
1654 use insta::assert_snapshot;
1655 use spenso::shadowing::symbolica_utils::LogPrint;
1656
1657 use super::*;
1658
1659 #[test]
1660 fn esurface_preserves_momentum_and_mass_substitutions() {
1661 let t = symbol!("esurface_substitution_test::t"; Scalar);
1662 let m = symbol!("esurface_substitution_test::m"; Scalar);
1663 let q = GS.emr_vec(EdgeIndex(3), GS.cind(1));
1664 let shift = GS.emr_mom(EdgeIndex(0), GS.cind(0));
1665 let energy = function!(GS.on_shell_energy, 3, q.pow(2) + Atom::var(m).pow(2));
1666 let sum = energy + &shift;
1667 let surface = GS.wrap_esurface(&sum);
1668 assert!(surface.contains_symbol(GS.esurface));
1669 for scale in [Atom::var(t), Atom::var(t).pow(-1)] {
1670 let replacements = [
1671 Replacement::new(q.clone(), &q * &scale),
1672 Replacement::new(Atom::var(m), Atom::var(m) * &scale),
1673 Replacement::new(shift.clone(), &shift * &scale),
1674 ];
1675 let transformed = surface.replace_multiple(&replacements);
1676 assert!(transformed.contains_symbol(t));
1677 assert!(
1680 (GS.lower_esurfaces(&transformed) - sum.replace_multiple(&replacements))
1681 .expand()
1682 .is_zero()
1683 );
1684 assert!(
1685 (GS.lower_esurfaces(&transformed.derivative(t))
1686 - sum.replace_multiple(&replacements).derivative(t))
1687 .expand()
1688 .is_zero()
1689 );
1690 }
1691 }
1692
1693 #[test]
1694 fn esurface_retention_is_idempotent_and_preserves_energy_shifts() {
1695 let first = GS.ose(EdgeIndex(1));
1696 let second = GS.ose(EdgeIndex(2));
1697 let shift = GS.emr_mom(EdgeIndex(0), GS.cind(0));
1698 let cff = (&first + &second - &shift).pow(-2) / &first;
1699 let retained = GS.retain_esurfaces(&cff);
1700 assert!(retained.contains_symbol(GS.esurface));
1701 assert_eq!(GS.retain_esurfaces(&retained), retained);
1702 assert_eq!(GS.lower_esurfaces(&retained), cff);
1703 assert_eq!(
1704 GS.lower_esurfaces(&retained.replace(shift.to_pattern()).with(0)),
1705 cff.replace(shift.to_pattern()).with(0),
1706 );
1707 }
1708
1709 #[test]
1710 fn esurface_series_preserves_cancelling_and_singular_leading_terms() {
1711 use symbolica::poly::series::SeriesDepth;
1712
1713 let t = symbol!("esurface_laurent_test::t"; Scalar);
1714 let x = Atom::var(t);
1715 for sum in [
1716 (Atom::one() + &x).pow((1, 2)) - 1,
1717 (Atom::one() + &x).pow((1, 2)) - 1 - &x / 2,
1718 &x + x.pow(-1),
1719 (Atom::one() + &x).pow((1, 2)) / &x - x.pow(-1) - Atom::num((1, 2)),
1720 (Atom::one() + &x).pow((1, 2)) / x.pow(2) - x.pow(-2) - Atom::num((1, 2)) / &x
1721 + Atom::num((1, 8)),
1722 (Atom::one() + &x).pow((1, 2)) / &x - x.pow(-1),
1723 ] {
1724 let wrapped = GS.wrap_esurface(&sum);
1725 assert!((GS.lower_esurfaces(&wrapped) - &sum).expand().is_zero());
1726 for power in [-2, -1, 1, 2] {
1727 let actual = wrapped
1728 .pow(power)
1729 .series(t, 0, SeriesDepth::absolute(2))
1730 .unwrap();
1731 let expected = sum
1732 .pow(power)
1733 .series(t, 0, SeriesDepth::absolute(2))
1734 .unwrap();
1735 assert_eq!(actual.absolute_order(), expected.absolute_order());
1736 assert!(
1738 (GS.lower_esurfaces(&actual.to_atom()) - expected.to_atom())
1739 .expand()
1740 .is_zero()
1741 );
1742 }
1743 }
1744 }
1745
1746 #[test]
1747 fn esurface_coefficients_accept_an_enclosing_momentum_deformation() {
1748 use symbolica::poly::series::SeriesDepth;
1749
1750 let t = symbol!("esurface_nested_test::t"; Scalar);
1751 let u = symbol!("esurface_nested_test::u"; Scalar);
1752 let q = symbol!("esurface_nested_test::q"; Scalar);
1753 let invariant = Atom::var(q).pow(2) + 1;
1754 let sum = (&invariant + Atom::var(t)).pow((1, 2)) + &invariant;
1755 let first = GS
1756 .wrap_esurface(&sum)
1757 .pow(-1)
1758 .series(t, 0, SeriesDepth::absolute(2))
1759 .unwrap()
1760 .to_atom();
1761 let expected_first = sum
1762 .pow(-1)
1763 .series(t, 0, SeriesDepth::absolute(2))
1764 .unwrap()
1765 .to_atom();
1766 assert!(first.contains_symbol(GS.esurface));
1767 let deform = |atom: Atom| atom.replace(q).with(Atom::var(u) + 2);
1768 let actual = deform(first)
1769 .series(u, 0, SeriesDepth::absolute(1))
1770 .unwrap()
1771 .to_atom();
1772 let expected = deform(expected_first)
1773 .series(u, 0, SeriesDepth::absolute(1))
1774 .unwrap()
1775 .to_atom();
1776 assert!((GS.lower_esurfaces(&actual) - expected).expand().is_zero());
1777 }
1778
1779 #[test]
1780 fn compact_energy_derivatives_keep_shift_and_mass_dependence() {
1781 crate::initialisation::test_initialise().unwrap();
1782 let (t, u, m, k, p, q) = symbol!(
1783 "compact_energy_test::t",
1784 "compact_energy_test::u",
1785 "compact_energy_test::m",
1786 "compact_energy_test::k",
1787 "compact_energy_test::p",
1788 "compact_energy_test::q"
1789 );
1790 let momentum = Atom::var(k) + Atom::var(t) * p + Atom::var(u) * q;
1791 let transverse = Atom::var(t) * q;
1792 let longitudinal = Atom::var(u) * p;
1793 let energy = function!(
1794 GS.on_shell_energy,
1795 3,
1796 Atom::var(m).pow(2) + momentum.pow(2) + transverse.pow(2) + longitudinal.pow(2)
1797 );
1798 assert_eq!(
1799 energy,
1800 function!(
1801 GS.on_shell_energy,
1802 3,
1803 Atom::var(m).pow(2)
1804 + (-&momentum).expand_num().pow(2)
1805 + (-&transverse).expand_num().pow(2)
1806 + (-&longitudinal).expand_num().pow(2)
1807 )
1808 );
1809 assert_eq!(energy.derivative(m), Atom::var(m) / &energy);
1810 assert!(
1811 (energy.derivative(t) - (&momentum * p + &transverse * q) / &energy)
1812 .expand()
1813 .is_zero()
1814 );
1815 assert!(
1816 (energy.derivative(u) - (momentum * q + longitudinal * p) / &energy)
1817 .expand()
1818 .is_zero()
1819 );
1820 assert!(energy.contains_symbol(t) && energy.contains_symbol(u));
1821
1822 let lower = |atom: Atom| {
1823 atom.replace(function!(GS.on_shell_energy, W_.a_, W_.prop_))
1824 .with(Atom::var(W_.prop_).pow((1, 2)))
1825 };
1826 let specialize = |atom: Atom| {
1827 atom.replace(m)
1828 .with(3)
1829 .replace(k)
1830 .with(4)
1831 .replace(p)
1832 .with(2)
1833 .replace(q)
1834 .with(1)
1835 };
1836 for exponent in [-3, -1, 1, 2, 3] {
1839 let compact = specialize(energy.pow(exponent));
1840 let explicit = lower(compact.clone());
1841 let actual = compact
1842 .series(t, Atom::Zero, 3)
1843 .unwrap()
1844 .to_atom()
1845 .series(u, Atom::Zero, 2)
1846 .unwrap()
1847 .to_atom();
1848 let expected = explicit
1849 .series(t, Atom::Zero, 3)
1850 .unwrap()
1851 .to_atom()
1852 .series(u, Atom::Zero, 2)
1853 .unwrap()
1854 .to_atom();
1855 assert!(
1858 (lower(actual) - expected).expand().is_zero(),
1859 "power={exponent}"
1860 );
1861 }
1862 }
1863
1864 #[test]
1865 fn compact_energy_invariant_matches_high_order_laurent_coefficients() {
1866 crate::initialisation::test_initialise().unwrap();
1867 let t = symbol!("compact_energy_laurent_test::t");
1868 let parameter = Atom::var(t);
1869 let squared = (¶meter + 1).pow(2)
1872 + (¶meter * 2 + 2).pow(2)
1873 + (Atom::num(2) - ¶meter).pow(2);
1874 let energy = function!(GS.on_shell_energy, 3, &squared);
1875 let actual = (energy / parameter.pow(6))
1876 .series(t, Atom::Zero, 0)
1877 .unwrap();
1878 let expected = (squared.pow((1, 2)) / parameter.pow(6))
1879 .series(t, Atom::Zero, 0)
1880 .unwrap();
1881 assert_eq!(actual.terms().count(), 7);
1882 assert_eq!(
1885 actual
1886 .to_atom()
1887 .replace(function!(GS.on_shell_energy, 3, 9))
1888 .with(3),
1889 expected.to_atom()
1890 );
1891 }
1892
1893 #[test]
1894 fn compact_energy_normalization_preserves_owners_after_cache_eviction() {
1895 crate::initialisation::test_initialise().unwrap();
1896 let t = symbol!("compact_energy_cache_test::t");
1897 let parameter = Atom::var(t);
1898 let invariant = ((¶meter + 1).pow(2) + 3).pow(2) + 4;
1899 let cold = function!(GS.on_shell_energy, 3, &invariant);
1900 let other_owner = function!(GS.on_shell_energy, 7, &invariant);
1901 let AtomView::Fun(cold_call) = cold.as_view() else {
1902 panic!("Expected an owned energy function");
1903 };
1904 let AtomView::Fun(other_call) = other_owner.as_view() else {
1905 panic!("Expected an owned energy function");
1906 };
1907 assert_eq!(other_call.get(0), Atom::num(7).as_view());
1908 assert_eq!(cold_call.get(1), other_call.get(1));
1909 assert_ne!(cold, other_owner);
1910
1911 for offset in 0..1050 {
1914 let _ = function!(GS.on_shell_energy, 3, &invariant + offset);
1915 }
1916 assert_eq!(function!(GS.on_shell_energy, 3, &invariant), cold);
1917 assert_eq!(function!(GS.on_shell_energy, 7, &invariant), other_owner);
1918
1919 let lower = |atom: Atom| {
1920 atom.replace(function!(GS.on_shell_energy, W_.a_, W_.prop_))
1921 .with(Atom::var(W_.prop_).pow((1, 2)))
1922 };
1923 let expected = invariant
1924 .pow((1, 2))
1925 .series(t, Atom::Zero, 3)
1926 .unwrap()
1927 .to_atom();
1928 for energy in [cold, other_owner] {
1929 let actual = energy.series(t, Atom::Zero, 3).unwrap().to_atom();
1930 let (_, difference) = (lower(actual) - &expected)
1933 .to_polynomial_in_algebraic_extension::<u16>(t, &[])
1934 .unwrap();
1935 assert!(difference.is_zero());
1936 }
1937 }
1938
1939 #[test]
1940 fn orientation_collection_preserves_selector_free_factorization() {
1941 let (a, b, c, d, x) = symbol!(
1942 "selector_free_a",
1943 "selector_free_b",
1944 "selector_free_c",
1945 "selector_free_d",
1946 "selector_free_x"
1947 );
1948 let expression = (Atom::var(a) + b).pow(7) * (Atom::var(c) + d).pow(5)
1949 / (Atom::var(x) + Atom::one()).pow(2);
1950 assert_eq!(GS.collect_orientation_if(expression.clone()), expression);
1951 assert_eq!(GS.collect_orientation_if(expression.as_view()), expression);
1952 }
1953
1954 #[test]
1955 fn orientation_collection_merges_same_key_scalar_contributions() {
1956 let (key, a, b, c, d, x) = symbol!(
1957 "selector_merge_key",
1958 "selector_merge_a",
1959 "selector_merge_b",
1960 "selector_merge_c",
1961 "selector_merge_d",
1962 "selector_merge_x"
1963 );
1964 let condition = Atom::var(key) - 4;
1965 let bodies = [
1966 (Atom::var(a) + b).pow(3) * (Atom::var(c) + d).pow(2),
1967 Atom::var(x).pow(-1),
1968 Atom::var(c) + 7,
1969 ];
1970 let unrelated = Atom::var(d)
1972 + Symbol::IF.call_args([Atom::var(key) - 9, Atom::Zero, Atom::var(x).pow(-2)])
1973 + Symbol::IF.call_args([condition.clone(), Atom::var(a), Atom::Zero])
1974 + Symbol::IF.call_args([condition.clone(), Atom::var(b), Atom::var(c)]);
1975 let expression = Atom::add_many(
1976 bodies
1977 .iter()
1978 .map(|body| Symbol::IF.call_args([condition.clone(), Atom::Zero, body.clone()])),
1979 ) + &unrelated;
1980 let expected =
1981 Symbol::IF.call_args([condition, Atom::Zero, Atom::add_many(bodies)]) + unrelated;
1982 let collected = GS.collect_orientation_if(expression);
1983 assert_eq!(collected, expected);
1984 assert_eq!(GS.collect_orientation_if(collected.clone()), collected);
1985 }
1986
1987 #[test]
1988 fn orientation_collection_merges_nested_same_key_contributions() {
1989 let (key, a, b, c, d, w) = symbol!(
1990 "nested_selector_key",
1991 "nested_selector_a",
1992 "nested_selector_b",
1993 "nested_selector_c",
1994 "nested_selector_d",
1995 "nested_selector_w"
1996 );
1997 let select = |offset: i64, body: Atom| {
1998 Symbol::IF.call_args([Atom::var(key) - offset, Atom::Zero, body])
1999 };
2000 let inner = select(1, select(2, Atom::var(a)))
2003 + select(1, select(2, Atom::var(b)))
2004 + select(3, Atom::var(c));
2005 let expression = Atom::var(w) * (Atom::var(d) + Atom::var(w) * inner)
2006 + select(1, Atom::var(c))
2007 + select(1, Atom::var(d));
2008 let expected = Atom::var(w)
2009 * (Atom::var(d)
2010 + Atom::var(w)
2011 * (select(1, select(2, Atom::var(a) + b)) + select(3, Atom::var(c))))
2012 + select(1, Atom::var(c) + d);
2013 let collected = GS.collect_orientation_if(expression);
2014 assert_eq!(collected, expected);
2015 assert_eq!(GS.collect_orientation_if(collected.clone()), collected);
2016 }
2017
2018 #[test]
2019 fn orientation_collection_merges_weighted_factorized_contributions() {
2020 let (key, a, b, c, d, x, alias) = symbol!(
2021 "weighted_selector_key",
2022 "weighted_selector_a",
2023 "weighted_selector_b",
2024 "weighted_selector_c",
2025 "weighted_selector_d",
2026 "weighted_selector_x",
2027 "weighted_selector_alias"
2028 );
2029 let condition = Atom::var(key) - 4;
2030 let select = |body: Atom| Symbol::IF.call_args([condition.clone(), Atom::Zero, body]);
2031 let numerator = (Atom::var(a) + b).pow(3);
2032 let weight = (Atom::var(c) + d).pow(2);
2033 let handle = alias.call(7);
2034 let expression = select(numerator.clone()) * &weight
2035 + Atom::i() * &handle * select(Atom::var(x).pow(-1));
2036 let expected = select(numerator * weight + Atom::i() * &handle * Atom::var(x).pow(-1));
2037 let collected = GS.collect_orientation_if(expression);
2038 assert_eq!(collected, expected);
2039 assert!(collected.contains(&handle));
2040 assert_eq!(GS.collect_orientation_if(collected.clone()), collected);
2041 }
2042
2043 #[test]
2044 fn orientation_collection_reaches_nested_weighted_guards() {
2045 let (key, a, b, c, x, y, z, w) = symbol!(
2046 "nested_weighted_key",
2047 "nested_weighted_a",
2048 "nested_weighted_b",
2049 "nested_weighted_c",
2050 "nested_weighted_x",
2051 "nested_weighted_y",
2052 "nested_weighted_z",
2053 "nested_weighted_w"
2054 );
2055 let select = |body: Atom| Symbol::IF.call_args([Atom::var(key), Atom::Zero, body]);
2056 let expression = Atom::var(w) * (select(Atom::var(a)) * x + select(Atom::var(b)) * y)
2057 + select(Atom::var(c)) * z;
2058 let expected =
2059 select(Atom::var(w) * (Atom::var(a) * x + Atom::var(b) * y) + Atom::var(c) * z);
2060 let collected = GS.collect_orientation_if(expression);
2061 assert_eq!(collected, expected);
2062 assert_eq!(GS.collect_orientation_if(collected.clone()), collected);
2063 }
2064
2065 #[test]
2066 fn orientation_collection_preserves_distinct_weighted_conditions() {
2067 let (key, a, b, c, x, y, z) = symbol!(
2068 "distinct_weighted_key",
2069 "distinct_weighted_a",
2070 "distinct_weighted_b",
2071 "distinct_weighted_c",
2072 "distinct_weighted_x",
2073 "distinct_weighted_y",
2074 "distinct_weighted_z"
2075 );
2076 let select = |offset: i64, body: Atom| {
2077 Symbol::IF.call_args([Atom::var(key) - offset, Atom::Zero, body])
2078 };
2079 let ordinary = Symbol::IF.call_args([Atom::var(key), Atom::var(b), Atom::var(c)]);
2080 let expression = select(1, Atom::var(a)) * x + select(2, Atom::var(b)) * y + &ordinary * z;
2081 let expected = select(1, Atom::var(a) * x) + select(2, Atom::var(b) * y) + ordinary * z;
2082 let collected = GS.collect_orientation_if(expression);
2083 assert_eq!(collected, expected);
2084 assert_eq!(GS.collect_orientation_if(collected.clone()), collected);
2085 }
2086
2087 #[test]
2088 fn orientation_collection_preserves_literal_function_tags() {
2089 let (key, a, w, alias) = symbol!(
2090 "opaque_weighted_key",
2091 "opaque_weighted_a",
2092 "opaque_weighted_w",
2093 "opaque_weighted_alias"
2094 );
2095 let tag = Symbol::IF.call_args([Atom::var(key), Atom::Zero, Atom::var(a)]) * Atom::var(w);
2096 let handle = alias.call(tag);
2097 assert_eq!(GS.collect_orientation_if(handle.clone()), handle);
2098 }
2099
2100 #[test]
2101 fn orientation_collection_combines_same_key_guard_factors() {
2102 let (key, a, b, w) = symbol!(
2103 "guard_product_key",
2104 "guard_product_a",
2105 "guard_product_b",
2106 "guard_product_w"
2107 );
2108 let select = |body: Atom| Symbol::IF.call_args([Atom::var(key), Atom::Zero, body]);
2109 let expression = select(Atom::var(a)) * select(Atom::var(b)) * w;
2110 let expected = select(Atom::var(a) * b * w);
2111 let collected = GS.collect_orientation_if(expression);
2112 assert_eq!(collected, expected);
2113 assert_eq!(GS.collect_orientation_if(collected.clone()), collected);
2114 }
2115
2116 #[test]
2117 fn canonical_uv_class_references_are_distinct_from_physical_owners() {
2118 let class = UvDenominatorClassId(7);
2119 assert_eq!(
2120 GS.uv_class_data(GS.uv_class_ref(class).as_view()),
2121 Some(class)
2122 );
2123 assert_eq!(GS.uv_class_data(Atom::num(7).as_view()), None);
2124 assert_eq!(GS.uv_class_data(GS.uv_class.call(-1).as_view()), None);
2125 assert_eq!(
2126 GS.uv_class_data(
2127 GS.uv_class
2128 .call_args([Atom::num(7), Atom::num(8)])
2129 .as_view()
2130 ),
2131 None
2132 );
2133 let provenance = GS.uv_momentum_provenance_tag(
2134 7,
2135 UvMomentumProvenanceRole::TaylorFixed,
2136 GS.emr_mom.call(7),
2137 );
2138 assert_eq!(GS.uv_class_data(provenance.as_view()), None);
2139 }
2140
2141 #[test]
2142 fn test_print() {
2143 let p = GS.emr_mom(EdgeIndex(1), Atom::Zero);
2144
2145 assert_snapshot!(p.log_print(None),@"[35mq₁[0m(0)")
2146 }
2147
2148 #[test]
2149 fn typst_uses_structural_accents_and_scripts() {
2150 let expressions = [
2151 (
2152 GS.ubar.call_args([Atom::num(1), Atom::num(11)]),
2153 "overline(u)_1^(11)",
2154 ),
2155 (
2156 GS.vbar.call_args([Atom::num(2), Atom::num(12)]),
2157 "overline(v)_2^(12)",
2158 ),
2159 (
2160 GS.ose.call_args([Atom::num(3), Atom::num(13)]),
2161 r#"E^("os")_3(13)"#,
2162 ),
2163 (
2164 GS.emr_vec(EdgeIndex(4), Minkowski {}.new_rep(4).to_symbolic([])),
2165 "arrow(q)_4",
2166 ),
2167 (
2168 GS.epsilonbar.call_args([Atom::num(5), Atom::num(15)]),
2169 "overline(epsilon.alt)_5^(15)",
2170 ),
2171 (
2172 GS.emr_mom.call_args([Atom::num(6), Atom::num(16)]),
2173 "q_6^(16)",
2174 ),
2175 ];
2176
2177 for (expression, expected) in expressions {
2178 assert_eq!(
2179 expression
2180 .printer(SpensoPrintSettings::typst_options())
2181 .to_string(),
2182 expected
2183 );
2184 }
2185 }
2186
2187 #[test]
2188 fn typst_syntax_requires_real_typst_mode() {
2189 let mut symbolica = SpensoPrintSettings::typst().nice_symbolica();
2190 symbolica.color_builtin_symbols = false;
2191
2192 assert_eq!(
2193 GS.ubar
2194 .call_args([Atom::num(1), Atom::num(11)])
2195 .printer(symbolica.clone())
2196 .to_string(),
2197 "u̅₁^(11)"
2198 );
2199 assert_eq!(
2200 Atom::var(GS.m_uv_vacuum).printer(symbolica).to_string(),
2201 "mUV"
2202 );
2203
2204 assert_eq!(
2205 Atom::var(GS.m_uv_expansion)
2206 .printer(SpensoPrintSettings::typst_options())
2207 .to_string(),
2208 r#"m_"UVexp""#
2209 );
2210 assert_eq!(
2211 Atom::var(GS.m_uv_vacuum)
2212 .printer(SpensoPrintSettings::typst_options())
2213 .to_string(),
2214 r#"m_"UV""#
2215 );
2216 assert_eq!(
2217 Atom::var(GS.mu_r_sq)
2218 .printer(SpensoPrintSettings::typst_options())
2219 .to_string(),
2220 "mu_r^2"
2221 );
2222 }
2223}