1use super::clustering::ClusteringResult;
2use crate::momentum::{FourMomentum, Rotation};
3use crate::utils::{F, FloatLike, into_complex_ff64};
4use colored::Colorize;
5use serde::{Deserialize, Deserializer, Serialize, Serializer, ser::SerializeStruct};
6use smallvec::SmallVec;
7use spenso::algebra::complex::Complex;
8use std::collections::BTreeMap;
9use std::fmt;
10use std::ops::{Deref, DerefMut};
11use tabled::{
12 Table, Tabled,
13 settings::{Style, style::HorizontalLine},
14};
15
16#[derive(Default, Debug, Clone, Serialize, Deserialize)]
17pub struct CutInfo {
18 pub particle_pdgs: (SmallVec<[isize; 2]>, SmallVec<[isize; 4]>),
19 pub cut_id: usize,
20 pub graph_id: usize,
21 pub graph_group_id: Option<usize>,
22 pub orientation_id: Option<usize>,
23 pub sampling_channel_id: Option<usize>,
24 pub sampling_channel_edge_ids: Option<SmallVec<[usize; 4]>>,
25}
26
27pub type Event = GenericEvent<f64>;
28pub type EventGroup = GenericEventGroup<f64>;
29pub type EventGroupList = GenericEventGroupList<f64>;
30
31#[derive(Default, Debug, Clone)]
32pub struct GenericDerivedEventData<T: FloatLike> {
33 pub clustered_jets: Vec<Option<ClusteringResult<T>>>,
34}
35
36impl<T: FloatLike> GenericDerivedEventData<T> {
37 pub fn to_f64(&self) -> GenericDerivedEventData<f64> {
38 GenericDerivedEventData {
39 clustered_jets: self
40 .clustered_jets
41 .iter()
42 .map(|result| result.as_ref().map(ClusteringResult::to_f64))
43 .collect(),
44 }
45 }
46
47 pub fn from_f64(data: &GenericDerivedEventData<f64>) -> Self {
48 GenericDerivedEventData {
49 clustered_jets: data
50 .clustered_jets
51 .iter()
52 .map(|result| result.as_ref().map(ClusteringResult::from_f64))
53 .collect(),
54 }
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
59pub enum AdditionalWeightKey {
60 FullMultiplicativeFactor,
61 Original,
62 ThresholdCounterterm {
63 subset_index: usize,
64 },
65 AmplitudeThresholdCounterterm {
66 esurface_id: usize,
67 overlap_group: usize,
68 },
69 AmplitudeThresholdCountertermVariant {
70 variant_id: usize,
71 esurface_id: usize,
72 overlap_group: usize,
73 },
74}
75
76#[derive(Default, Debug, Clone, Serialize, Deserialize)]
77pub struct GenericEventGroup<T: FloatLike>(pub Vec<GenericEvent<T>>);
78
79impl<T: FloatLike> Deref for GenericEventGroup<T> {
80 type Target = Vec<GenericEvent<T>>;
81
82 fn deref(&self) -> &Self::Target {
83 &self.0
84 }
85}
86
87impl<T: FloatLike> DerefMut for GenericEventGroup<T> {
88 fn deref_mut(&mut self) -> &mut Self::Target {
89 &mut self.0
90 }
91}
92
93impl<T: FloatLike> GenericEventGroup<T> {
94 pub fn to_f64(&self) -> EventGroup {
95 GenericEventGroup(self.0.iter().map(GenericEvent::to_f64).collect())
96 }
97
98 pub fn from_f64(event_group: &EventGroup) -> Self {
99 GenericEventGroup(event_group.0.iter().map(GenericEvent::from_f64).collect())
100 }
101}
102
103#[derive(Default, Debug, Clone, Serialize, Deserialize)]
104pub struct GenericEventGroupList<T: FloatLike>(pub Vec<GenericEventGroup<T>>);
105
106impl<T: FloatLike> Deref for GenericEventGroupList<T> {
107 type Target = Vec<GenericEventGroup<T>>;
108
109 fn deref(&self) -> &Self::Target {
110 &self.0
111 }
112}
113
114impl<T: FloatLike> DerefMut for GenericEventGroupList<T> {
115 fn deref_mut(&mut self) -> &mut Self::Target {
116 &mut self.0
117 }
118}
119
120impl<T: FloatLike> GenericEventGroupList<T> {
121 pub fn to_f64(&self) -> EventGroupList {
122 GenericEventGroupList(self.0.iter().map(GenericEventGroup::to_f64).collect())
123 }
124
125 pub fn from_f64(event_group_list: &EventGroupList) -> Self {
126 GenericEventGroupList(
127 event_group_list
128 .0
129 .iter()
130 .map(GenericEventGroup::from_f64)
131 .collect(),
132 )
133 }
134
135 pub fn push_singleton(&mut self, event: GenericEvent<T>) {
136 self.0.push(GenericEventGroup(vec![event]));
137 }
138}
139
140#[derive(Default, Debug, Clone, Deserialize)]
141pub struct GenericAdditionalWeightInfo<T: FloatLike> {
142 #[serde(
143 with = "crate::utils::serde_utils::map_as_pairs",
144 bound(deserialize = "T: Deserialize<'de>")
145 )]
146 pub weights: BTreeMap<AdditionalWeightKey, Complex<F<T>>>,
147 #[serde(default)]
150 pub threshold_counterterms: Option<GenericThresholdCountertermEventInfo<T>>,
151}
152
153impl<T: FloatLike> Serialize for GenericAdditionalWeightInfo<T> {
154 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
155 let include_thresholds =
157 !serializer.is_human_readable() || self.threshold_counterterms.is_some();
158 let mut fields = serializer.serialize_struct(
159 "GenericAdditionalWeightInfo",
160 1 + usize::from(include_thresholds),
161 )?;
162 fields.serialize_field("weights", &self.weights.iter().collect::<Vec<_>>())?;
163 if include_thresholds {
164 fields.serialize_field("threshold_counterterms", &self.threshold_counterterms)?;
165 }
166 fields.end()
167 }
168}
169
170impl<T: FloatLike> GenericAdditionalWeightInfo<T> {
171 pub fn to_f64(&self) -> GenericAdditionalWeightInfo<f64> {
172 GenericAdditionalWeightInfo {
173 weights: self
174 .weights
175 .iter()
176 .map(|(key, weight)| (*key, into_complex_ff64(weight)))
177 .collect(),
178 threshold_counterterms: self
179 .threshold_counterterms
180 .as_ref()
181 .map(GenericThresholdCountertermEventInfo::to_f64),
182 }
183 }
184
185 pub fn from_f64(additional_weights: &GenericAdditionalWeightInfo<f64>) -> Self {
186 GenericAdditionalWeightInfo {
187 weights: additional_weights
188 .weights
189 .iter()
190 .map(|(key, weight)| {
191 (
192 *key,
193 Complex::new(F::from_ff64(weight.re), F::from_ff64(weight.im)),
194 )
195 })
196 .collect(),
197 threshold_counterterms: additional_weights
198 .threshold_counterterms
199 .as_ref()
200 .map(GenericThresholdCountertermEventInfo::from_f64),
201 }
202 }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207#[serde(remote = "Self", tag = "kind", rename_all = "snake_case")]
208pub enum ThresholdCountertermComponentOccurrence {
209 Amplitude {
210 raised_esurface_id: usize,
211 overlap_group: usize,
212 },
213 LocalUnitarity {
214 overlap_groups: SmallVec<[usize; 2]>,
216 left_threshold_order: Option<usize>,
217 right_threshold_order: Option<usize>,
218 lu_cut_order: Option<usize>,
219 },
220}
221
222#[derive(Serialize, Deserialize)]
225#[serde(remote = "ThresholdCountertermComponentOccurrence")]
226enum BinaryThresholdCountertermComponentOccurrence {
227 Amplitude {
228 raised_esurface_id: usize,
229 overlap_group: usize,
230 },
231 LocalUnitarity {
232 overlap_groups: SmallVec<[usize; 2]>,
233 left_threshold_order: Option<usize>,
234 right_threshold_order: Option<usize>,
235 lu_cut_order: Option<usize>,
236 },
237}
238
239impl Serialize for ThresholdCountertermComponentOccurrence {
240 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
241 if serializer.is_human_readable() {
242 Self::serialize(self, serializer)
243 } else {
244 BinaryThresholdCountertermComponentOccurrence::serialize(self, serializer)
245 }
246 }
247}
248
249impl<'de> Deserialize<'de> for ThresholdCountertermComponentOccurrence {
250 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
251 if deserializer.is_human_readable() {
252 Self::deserialize(deserializer)
253 } else {
254 BinaryThresholdCountertermComponentOccurrence::deserialize(deserializer)
255 }
256 }
257}
258
259#[derive(Debug, Clone, Deserialize)]
266pub struct GenericThresholdCountertermComponentWeight<T: FloatLike> {
267 pub component_id: usize,
268 pub occurrence: ThresholdCountertermComponentOccurrence,
269 pub multiplier_values: SmallVec<[F<T>; 2]>,
270 pub effective_multiplier: F<T>,
271 #[serde(default)]
272 pub bare: Option<Complex<F<T>>>,
273 pub weighted: Complex<F<T>>,
274 pub evaluation_skipped: bool,
275}
276
277impl<T: FloatLike> Serialize for GenericThresholdCountertermComponentWeight<T> {
278 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
279 let include_bare = !serializer.is_human_readable() || self.bare.is_some();
280 let mut fields = serializer.serialize_struct(
281 "GenericThresholdCountertermComponentWeight",
282 6 + usize::from(include_bare),
283 )?;
284 fields.serialize_field("component_id", &self.component_id)?;
285 fields.serialize_field("occurrence", &self.occurrence)?;
286 fields.serialize_field("multiplier_values", &self.multiplier_values)?;
287 fields.serialize_field("effective_multiplier", &self.effective_multiplier)?;
288 if include_bare {
289 fields.serialize_field("bare", &self.bare)?;
290 }
291 fields.serialize_field("weighted", &self.weighted)?;
292 fields.serialize_field("evaluation_skipped", &self.evaluation_skipped)?;
293 fields.end()
294 }
295}
296
297impl<T: FloatLike> GenericThresholdCountertermComponentWeight<T> {
298 fn to_f64(&self) -> GenericThresholdCountertermComponentWeight<f64> {
299 GenericThresholdCountertermComponentWeight {
300 component_id: self.component_id,
301 occurrence: self.occurrence.clone(),
302 multiplier_values: self.multiplier_values.iter().map(F::into_ff64).collect(),
303 effective_multiplier: self.effective_multiplier.into_ff64(),
304 bare: self.bare.as_ref().map(into_complex_ff64),
305 weighted: into_complex_ff64(&self.weighted),
306 evaluation_skipped: self.evaluation_skipped,
307 }
308 }
309
310 fn from_f64(
311 component: &GenericThresholdCountertermComponentWeight<f64>,
312 ) -> GenericThresholdCountertermComponentWeight<T> {
313 GenericThresholdCountertermComponentWeight {
314 component_id: component.component_id,
315 occurrence: component.occurrence.clone(),
316 multiplier_values: component
317 .multiplier_values
318 .iter()
319 .copied()
320 .map(F::from_ff64)
321 .collect(),
322 effective_multiplier: F::from_ff64(component.effective_multiplier),
323 bare: component
324 .bare
325 .as_ref()
326 .map(|bare| Complex::new(F::from_ff64(bare.re), F::from_ff64(bare.im))),
327 weighted: Complex::new(
328 F::from_ff64(component.weighted.re),
329 F::from_ff64(component.weighted.im),
330 ),
331 evaluation_skipped: component.evaluation_skipped,
332 }
333 }
334}
335
336#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct GenericThresholdCountertermEventInfo<T: FloatLike> {
340 pub original: Complex<F<T>>,
341 pub components: Vec<GenericThresholdCountertermComponentWeight<T>>,
342}
343
344impl<T: FloatLike> GenericThresholdCountertermEventInfo<T> {
345 pub fn total(&self) -> Complex<F<T>> {
346 self.components
347 .iter()
348 .fold(self.original.clone(), |total, component| {
349 total + &component.weighted
350 })
351 }
352
353 pub fn apply_multiplicative_factor(&mut self, factor: &Complex<F<T>>) {
355 self.original *= factor;
356 for component in &mut self.components {
357 if let Some(bare) = &mut component.bare {
358 *bare *= factor;
359 }
360 component.weighted *= factor;
361 }
362 }
363
364 pub fn to_f64(&self) -> GenericThresholdCountertermEventInfo<f64> {
365 GenericThresholdCountertermEventInfo {
366 original: into_complex_ff64(&self.original),
367 components: self
368 .components
369 .iter()
370 .map(GenericThresholdCountertermComponentWeight::to_f64)
371 .collect(),
372 }
373 }
374
375 pub fn from_f64(info: &GenericThresholdCountertermEventInfo<f64>) -> Self {
376 GenericThresholdCountertermEventInfo {
377 original: Complex::new(
378 F::from_ff64(info.original.re),
379 F::from_ff64(info.original.im),
380 ),
381 components: info
382 .components
383 .iter()
384 .map(GenericThresholdCountertermComponentWeight::from_f64)
385 .collect(),
386 }
387 }
388}
389
390#[derive(Default, Debug, Clone, Serialize, Deserialize)]
391pub struct GenericEvent<T: FloatLike> {
392 #[allow(clippy::type_complexity)]
393 pub kinematic_configuration: (
394 SmallVec<[FourMomentum<F<T>>; 2]>,
395 SmallVec<[FourMomentum<F<T>>; 4]>,
396 ),
397 pub cut_info: CutInfo,
398 pub weight: Complex<F<T>>,
400 pub additional_weights: GenericAdditionalWeightInfo<T>,
402 #[serde(skip)]
403 pub derived_observable_data: GenericDerivedEventData<T>,
404}
405
406impl<T: FloatLike> GenericEvent<T> {
407 pub(crate) fn apply_multiplicative_factor(&mut self, factor: &Complex<F<T>>) {
410 self.weight *= factor;
411 if let Some(decomposition) = &mut self.additional_weights.threshold_counterterms {
412 decomposition.apply_multiplicative_factor(factor);
413 self.weight = decomposition.total();
414 }
415 }
416
417 pub(crate) fn inverse_rotate(&mut self, rotation: &Rotation) {
418 if rotation.is_identity() {
419 return;
420 }
421
422 for momentum in self.kinematic_configuration.0.iter_mut() {
423 *momentum = rotation.inverse_rotate_four(momentum);
424 }
425 for momentum in self.kinematic_configuration.1.iter_mut() {
426 *momentum = rotation.inverse_rotate_four(momentum);
427 }
428
429 self.derived_observable_data = GenericDerivedEventData::default();
431 }
432
433 pub fn ensure_clustering_slots(&mut self, n_clusterings: usize) {
434 if self.derived_observable_data.clustered_jets.len() < n_clusterings {
435 self.derived_observable_data
436 .clustered_jets
437 .resize_with(n_clusterings, || None);
438 }
439 }
440
441 pub fn cached_clustering(&self, handle: usize) -> Option<&ClusteringResult<T>> {
442 self.derived_observable_data
443 .clustered_jets
444 .get(handle)
445 .and_then(|result| result.as_ref())
446 }
447
448 pub fn to_f64(&self) -> Event {
449 GenericEvent {
450 kinematic_configuration: (
451 self.kinematic_configuration
452 .0
453 .iter()
454 .map(FourMomentum::to_f64)
455 .collect(),
456 self.kinematic_configuration
457 .1
458 .iter()
459 .map(FourMomentum::to_f64)
460 .collect(),
461 ),
462 cut_info: self.cut_info.clone(),
463 weight: into_complex_ff64(&self.weight),
464 additional_weights: self.additional_weights.to_f64(),
465 derived_observable_data: self.derived_observable_data.to_f64(),
466 }
467 }
468
469 pub fn from_f64(event: &Event) -> Self {
470 GenericEvent {
471 kinematic_configuration: (
472 event
473 .kinematic_configuration
474 .0
475 .iter()
476 .map(FourMomentum::from_ff64)
477 .collect(),
478 event
479 .kinematic_configuration
480 .1
481 .iter()
482 .map(FourMomentum::from_ff64)
483 .collect(),
484 ),
485 cut_info: event.cut_info.clone(),
486 weight: Complex::new(F::from_ff64(event.weight.re), F::from_ff64(event.weight.im)),
487 additional_weights: GenericAdditionalWeightInfo::from_f64(&event.additional_weights),
488 derived_observable_data: GenericDerivedEventData::from_f64(
489 &event.derived_observable_data,
490 ),
491 }
492 }
493}
494
495#[derive(Tabled)]
496struct EventSummaryRow {
497 field: String,
498 value: String,
499}
500
501#[derive(Tabled)]
502struct MomentumRow {
503 #[tabled(rename = "state")]
504 state: String,
505 #[tabled(rename = "PDG")]
506 pdg: String,
507 #[tabled(rename = "E")]
508 e: String,
509 px: String,
510 py: String,
511 pz: String,
512 #[tabled(rename = "sqrt(p^2)")]
513 p2: String,
514}
515
516#[derive(Tabled)]
517struct AdditionalWeightRow {
518 key: String,
519 value: String,
520}
521
522#[derive(Tabled)]
523struct ThresholdCountertermWeightRow {
524 #[tabled(rename = "component")]
525 component_id: usize,
526 occurrence: String,
527 multipliers: String,
528 effective: String,
529 bare: String,
530 weighted: String,
531 skipped: bool,
532}
533
534fn format_threshold_counterterm_occurrence(
535 occurrence: &ThresholdCountertermComponentOccurrence,
536) -> String {
537 match occurrence {
538 ThresholdCountertermComponentOccurrence::Amplitude {
539 raised_esurface_id,
540 overlap_group,
541 } => format!("amplitude E{raised_esurface_id}, overlap {overlap_group}"),
542 ThresholdCountertermComponentOccurrence::LocalUnitarity {
543 overlap_groups,
544 left_threshold_order,
545 right_threshold_order,
546 lu_cut_order,
547 } => format!(
548 "LU overlaps {:?}, orders L={left_threshold_order:?} R={right_threshold_order:?} cut={lu_cut_order:?}",
549 overlap_groups,
550 ),
551 }
552}
553
554fn display_decimal_precision<T: FloatLike>(value: &F<T>) -> usize {
555 let precision_bits = value.0.get_precision().max(1) as f64;
556 (precision_bits * std::f64::consts::LOG10_2).ceil().max(1.0) as usize
557}
558
559pub(crate) fn format_real_generic<T: FloatLike>(value: &F<T>) -> String {
560 format!("{:+.*e}", display_decimal_precision(value), value)
561}
562
563fn format_count(value: usize) -> String {
564 if value < 1_000 {
565 return value.to_string();
566 }
567
568 let value = value as f64;
569 for (scale, suffix) in [
570 (1_000_000_000_f64, "B"),
571 (1_000_000_f64, "M"),
572 (1_000_f64, "K"),
573 ] {
574 if value >= scale {
575 let scaled = value / scale;
576 let precision = if scaled >= 100.0 {
577 0
578 } else if scaled >= 10.0 {
579 1
580 } else {
581 2
582 };
583 return format!("{scaled:.precision$}{suffix}");
584 }
585 }
586
587 value.round().to_string()
588}
589
590pub(crate) fn format_optional_real_generic<T: FloatLike>(value: Option<&F<T>>) -> String {
591 value
592 .map(format_real_generic)
593 .unwrap_or_else(|| "None".red().to_string())
594}
595
596pub(crate) fn format_complex_generic<T: FloatLike>(value: &Complex<F<T>>) -> String {
597 let precision = display_decimal_precision(&value.re).max(display_decimal_precision(&value.im));
598 format!("{:+.*e} {:+.*e}i", precision, value.re, precision, value.im)
599}
600
601fn format_pdg(pdg: Option<isize>) -> String {
602 pdg.map(|value| value.to_string())
603 .unwrap_or_else(|| "N/A".red().to_string())
604}
605
606fn format_pdg_with_state_color(pdg: Option<isize>, incoming: bool) -> String {
607 let pdg = format_pdg(pdg);
608 if incoming {
609 pdg.bright_blue().to_string()
610 } else {
611 pdg.bright_green().to_string()
612 }
613}
614
615fn format_sampling_channel_edge_ids(edge_ids: Option<&[usize]>) -> String {
616 edge_ids
617 .map(|edge_ids| {
618 format!(
619 "({})",
620 edge_ids
621 .iter()
622 .map(|edge_id| edge_id.to_string())
623 .collect::<Vec<_>>()
624 .join(",")
625 )
626 })
627 .unwrap_or_else(|| "None".to_string())
628}
629
630fn momentum_mass_squared<T: FloatLike>(momentum: &FourMomentum<F<T>>) -> F<T> {
631 momentum.temporal.value.clone() * momentum.temporal.value.clone()
632 - momentum.spatial.px.clone() * momentum.spatial.px.clone()
633 - momentum.spatial.py.clone() * momentum.spatial.py.clone()
634 - momentum.spatial.pz.clone() * momentum.spatial.pz.clone()
635}
636
637fn momentum_mass<T: FloatLike>(momentum: &FourMomentum<F<T>>) -> F<T> {
638 momentum_mass_squared(momentum).abs().sqrt()
639}
640
641fn format_momentum_rows<T: FloatLike>(
642 state: String,
643 momenta: &[FourMomentum<F<T>>],
644 pdgs: &[isize],
645 incoming: bool,
646) -> Vec<MomentumRow> {
647 momenta
648 .iter()
649 .enumerate()
650 .map(|(index, momentum)| MomentumRow {
651 state: state.clone(),
652 pdg: format_pdg_with_state_color(pdgs.get(index).copied(), incoming),
653 e: format_real_generic(&momentum.temporal.value),
654 px: format_real_generic(&momentum.spatial.px),
655 py: format_real_generic(&momentum.spatial.py),
656 pz: format_real_generic(&momentum.spatial.pz),
657 p2: format_real_generic(&momentum_mass(momentum))
658 .bright_yellow()
659 .to_string(),
660 })
661 .collect()
662}
663
664fn event_zero<T: FloatLike>(event: &GenericEvent<T>) -> F<T> {
665 event
666 .kinematic_configuration
667 .0
668 .first()
669 .map(|momentum| momentum.temporal.value.zero())
670 .or_else(|| {
671 event
672 .kinematic_configuration
673 .1
674 .first()
675 .map(|momentum| momentum.temporal.value.zero())
676 })
677 .unwrap_or_else(|| F(T::new_zero()))
678}
679
680fn conservation_row<T: FloatLike>(event: &GenericEvent<T>) -> MomentumRow {
681 let zero = event_zero(event);
682 let incoming = event.kinematic_configuration.0.iter().fold(
683 (zero.clone(), zero.clone(), zero.clone(), zero.clone()),
684 |acc, momentum| {
685 (
686 acc.0 + momentum.temporal.value.clone(),
687 acc.1 + momentum.spatial.px.clone(),
688 acc.2 + momentum.spatial.py.clone(),
689 acc.3 + momentum.spatial.pz.clone(),
690 )
691 },
692 );
693 let outgoing = event.kinematic_configuration.1.iter().fold(
694 (zero.clone(), zero.clone(), zero.clone(), zero),
695 |acc, momentum| {
696 (
697 acc.0 + momentum.temporal.value.clone(),
698 acc.1 + momentum.spatial.px.clone(),
699 acc.2 + momentum.spatial.py.clone(),
700 acc.3 + momentum.spatial.pz.clone(),
701 )
702 },
703 );
704 let delta = FourMomentum {
705 temporal: crate::momentum::Energy::new(incoming.0 - outgoing.0),
706 spatial: crate::momentum::ThreeMomentum::new(
707 incoming.1 - outgoing.1,
708 incoming.2 - outgoing.2,
709 incoming.3 - outgoing.3,
710 ),
711 };
712
713 MomentumRow {
714 state: "CHECK".bold().bright_yellow().to_string(),
715 pdg: "N/A".bright_yellow().to_string(),
716 e: format_real_generic(&delta.temporal.value)
717 .bright_yellow()
718 .to_string(),
719 px: format_real_generic(&delta.spatial.px)
720 .bright_yellow()
721 .to_string(),
722 py: format_real_generic(&delta.spatial.py)
723 .bright_yellow()
724 .to_string(),
725 pz: format_real_generic(&delta.spatial.pz)
726 .bright_yellow()
727 .to_string(),
728 p2: String::new().bright_yellow().to_string(),
729 }
730}
731
732fn indent_block(block: &str, prefix: &str) -> String {
733 let mut result = String::new();
734 for (index, line) in block.lines().enumerate() {
735 if index > 0 {
736 result.push('\n');
737 }
738 result.push_str(prefix);
739 result.push_str(line);
740 }
741 result
742}
743
744impl fmt::Display for AdditionalWeightKey {
745 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
746 match self {
747 AdditionalWeightKey::FullMultiplicativeFactor => {
748 write!(f, "full multiplicative factor")
749 }
750 AdditionalWeightKey::Original => write!(f, "original"),
751 AdditionalWeightKey::ThresholdCounterterm { subset_index } => {
752 write!(f, "threshold_counterterm:{subset_index}")
753 }
754 AdditionalWeightKey::AmplitudeThresholdCounterterm {
755 esurface_id,
756 overlap_group,
757 } => {
758 write!(f, "threshold_counterterm:{esurface_id}:{overlap_group}")
759 }
760 AdditionalWeightKey::AmplitudeThresholdCountertermVariant {
761 variant_id,
762 esurface_id,
763 overlap_group,
764 } => write!(
765 f,
766 "threshold_counterterm_variant:{variant_id}:{esurface_id}:{overlap_group}",
767 ),
768 }
769 }
770}
771
772impl fmt::Display for CutInfo {
773 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
774 let rows = vec![
775 EventSummaryRow {
776 field: "graph".to_string(),
777 value: self.graph_id.to_string(),
778 },
779 EventSummaryRow {
780 field: "graph group".to_string(),
781 value: self
782 .graph_group_id
783 .map(|value| value.to_string())
784 .unwrap_or_else(|| "None".to_string()),
785 },
786 EventSummaryRow {
787 field: "orientation".to_string(),
788 value: self
789 .orientation_id
790 .map(|value| value.to_string())
791 .unwrap_or_else(|| "None".to_string()),
792 },
793 EventSummaryRow {
794 field: "cut".to_string(),
795 value: self.cut_id.to_string(),
796 },
797 EventSummaryRow {
798 field: "lmb channel id".to_string(),
799 value: self
800 .sampling_channel_id
801 .map(|value| value.to_string())
802 .unwrap_or_else(|| "None".to_string()),
803 },
804 EventSummaryRow {
805 field: "lmb channel".to_string(),
806 value: format_sampling_channel_edge_ids(self.sampling_channel_edge_ids.as_deref()),
807 },
808 ];
809 write!(f, "{}", Table::new(rows).with(Style::rounded()))
810 }
811}
812
813impl<T: FloatLike> fmt::Display for GenericEvent<T> {
814 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
815 let summary = vec![
816 EventSummaryRow {
817 field: "graph".to_string(),
818 value: self.cut_info.graph_id.to_string(),
819 },
820 EventSummaryRow {
821 field: "graph group".to_string(),
822 value: self
823 .cut_info
824 .graph_group_id
825 .map(|value| value.to_string())
826 .unwrap_or_else(|| "None".to_string()),
827 },
828 EventSummaryRow {
829 field: "orientation".to_string(),
830 value: self
831 .cut_info
832 .orientation_id
833 .map(|value| value.to_string())
834 .unwrap_or_else(|| "None".to_string()),
835 },
836 EventSummaryRow {
837 field: "cut".to_string(),
838 value: self.cut_info.cut_id.to_string(),
839 },
840 EventSummaryRow {
841 field: "lmb channel id".to_string(),
842 value: self
843 .cut_info
844 .sampling_channel_id
845 .map(|value| value.to_string())
846 .unwrap_or_else(|| "None".to_string()),
847 },
848 EventSummaryRow {
849 field: "lmb channel".to_string(),
850 value: format_sampling_channel_edge_ids(
851 self.cut_info.sampling_channel_edge_ids.as_deref(),
852 ),
853 },
854 EventSummaryRow {
855 field: "weight".to_string(),
856 value: format_complex_generic(&self.weight),
857 },
858 ];
859
860 writeln!(f, "{}", "Event".bold().bright_cyan())?;
861 writeln!(f, "{}", Table::new(summary).with(Style::rounded()))?;
862
863 let mut momentum_rows = format_momentum_rows(
864 "IN".bold().bright_blue().to_string(),
865 &self.kinematic_configuration.0,
866 &self.cut_info.particle_pdgs.0,
867 true,
868 );
869 let incoming_row_count = momentum_rows.len();
870 momentum_rows.extend(format_momentum_rows(
871 "OUT".bold().bright_green().to_string(),
872 &self.kinematic_configuration.1,
873 &self.cut_info.particle_pdgs.1,
874 false,
875 ));
876 if !momentum_rows.is_empty() {
877 momentum_rows.push(conservation_row(self));
878 let check_separator_index = momentum_rows.len();
879 let header_separator = (
880 1,
881 HorizontalLine::new('─')
882 .intersection('┼')
883 .left('├')
884 .right('┤'),
885 );
886 let check_separator = (
887 check_separator_index,
888 HorizontalLine::new('─')
889 .intersection('┼')
890 .left('├')
891 .right('┤'),
892 );
893 writeln!(f)?;
894 writeln!(f, "{}", "Kinematics".bold().bright_blue())?;
895 if incoming_row_count > 0 && incoming_row_count < check_separator_index - 1 {
896 let in_out_separator = (
897 incoming_row_count + 1,
898 HorizontalLine::new('─')
899 .intersection('┼')
900 .left('├')
901 .right('┤'),
902 );
903 writeln!(
904 f,
905 "{}",
906 Table::new(momentum_rows).with(Style::rounded().horizontals([
907 header_separator,
908 in_out_separator,
909 check_separator,
910 ]))
911 )?;
912 } else {
913 writeln!(
914 f,
915 "{}",
916 Table::new(momentum_rows)
917 .with(Style::rounded().horizontals([header_separator, check_separator]))
918 )?;
919 }
920 }
921
922 if !self.additional_weights.weights.is_empty() {
923 let additional_weights = self
924 .additional_weights
925 .weights
926 .iter()
927 .map(|(key, value)| AdditionalWeightRow {
928 key: key.to_string(),
929 value: format_complex_generic(value),
930 })
931 .collect::<Vec<_>>();
932
933 writeln!(f)?;
934 writeln!(f, "{}", "Additional weights".bold().bright_magenta())?;
935 write!(
936 f,
937 "{}",
938 Table::new(additional_weights).with(Style::rounded())
939 )?;
940 }
941
942 if let Some(decomposition) = &self.additional_weights.threshold_counterterms {
943 let summary = [
944 EventSummaryRow {
945 field: "original".to_string(),
946 value: format_complex_generic(&decomposition.original),
947 },
948 EventSummaryRow {
949 field: "total".to_string(),
950 value: format_complex_generic(&decomposition.total()),
951 },
952 ];
953 let components = decomposition
954 .components
955 .iter()
956 .map(|component| ThresholdCountertermWeightRow {
957 component_id: component.component_id,
958 occurrence: format_threshold_counterterm_occurrence(&component.occurrence),
959 multipliers: component
960 .multiplier_values
961 .iter()
962 .map(format_real_generic)
963 .collect::<Vec<_>>()
964 .join(" × "),
965 effective: format_real_generic(&component.effective_multiplier),
966 bare: component
967 .bare
968 .as_ref()
969 .map(format_complex_generic)
970 .unwrap_or_else(|| "—".to_string()),
971 weighted: format_complex_generic(&component.weighted),
972 skipped: component.evaluation_skipped,
973 })
974 .collect::<Vec<_>>();
975
976 writeln!(f)?;
977 writeln!(f, "{}", "Threshold counterterms".bold().bright_magenta())?;
978 writeln!(f, "{}", Table::new(summary).with(Style::rounded()))?;
979 if !components.is_empty() {
980 write!(f, "{}", Table::new(components).with(Style::rounded()))?;
981 }
982 }
983
984 Ok(())
985 }
986}
987
988impl<T: FloatLike> fmt::Display for GenericEventGroup<T> {
989 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
990 writeln!(
991 f,
992 "{}",
993 format!("Event group ({} event(s))", format_count(self.len()))
994 .bold()
995 .bright_green()
996 )?;
997 for (index, event) in self.iter().enumerate() {
998 writeln!(f, " {}", format!("Event {index}:").bold().bright_yellow())?;
999 let event_str = event.to_string();
1000 writeln!(f, "{}", indent_block(&event_str, " "))?;
1001 }
1002 Ok(())
1003 }
1004}
1005
1006impl<T: FloatLike> fmt::Display for GenericEventGroupList<T> {
1007 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1008 for (index, group) in self.iter().enumerate() {
1009 if index > 0 {
1010 writeln!(f)?;
1011 }
1012 writeln!(
1013 f,
1014 "{}",
1015 format!("Event group {index}:").bold().bright_green()
1016 )?;
1017 writeln!(f, "{}", indent_block(&group.to_string(), " "))?;
1018 }
1019 Ok(())
1020 }
1021}
1022
1023#[cfg(test)]
1024mod tests {
1025 use serde_json::json;
1026 use spenso::algebra::complex::Complex;
1027
1028 use crate::utils::{ArbPrec, F, f128};
1029
1030 use super::{
1031 GenericAdditionalWeightInfo, GenericEvent, GenericThresholdCountertermComponentWeight,
1032 GenericThresholdCountertermEventInfo, ThresholdCountertermComponentOccurrence,
1033 };
1034
1035 fn decomposition() -> GenericThresholdCountertermEventInfo<f64> {
1036 GenericThresholdCountertermEventInfo {
1037 original: Complex::new(F(3.0), F(0.5)),
1038 components: vec![
1039 GenericThresholdCountertermComponentWeight {
1040 component_id: 4,
1041 occurrence: ThresholdCountertermComponentOccurrence::Amplitude {
1042 raised_esurface_id: 2,
1043 overlap_group: 1,
1044 },
1045 multiplier_values: smallvec::smallvec![F(0.5)],
1046 effective_multiplier: F(0.5),
1047 bare: Some(Complex::new(F(-2.0), F(1.0))),
1048 weighted: Complex::new(F(-1.0), F(0.5)),
1049 evaluation_skipped: false,
1050 },
1051 GenericThresholdCountertermComponentWeight {
1052 component_id: 5,
1053 occurrence: ThresholdCountertermComponentOccurrence::LocalUnitarity {
1054 overlap_groups: smallvec::smallvec![3],
1055 left_threshold_order: Some(1),
1056 right_threshold_order: None,
1057 lu_cut_order: Some(2),
1058 },
1059 multiplier_values: smallvec::smallvec![F(0.0)],
1060 effective_multiplier: F(0.0),
1061 bare: None,
1062 weighted: Complex::new(F(0.0), F(0.0)),
1063 evaluation_skipped: true,
1064 },
1065 ],
1066 }
1067 }
1068
1069 #[test]
1070 fn absent_decomposition_preserves_legacy_json_shape() {
1071 let info = GenericAdditionalWeightInfo::<f64>::default();
1072 assert_eq!(
1073 serde_json::to_value(&info).unwrap(),
1074 json!({ "weights": [] })
1075 );
1076
1077 let decoded: GenericAdditionalWeightInfo<f64> =
1078 serde_json::from_value(json!({ "weights": [] })).unwrap();
1079 assert!(decoded.threshold_counterterms.is_none());
1080 }
1081
1082 #[test]
1083 fn additional_weights_roundtrip_json_and_binary_with_optional_decomposition() {
1084 let mut iterated = decomposition();
1085 iterated.components[1].occurrence =
1086 ThresholdCountertermComponentOccurrence::LocalUnitarity {
1087 overlap_groups: smallvec::smallvec![3, 7],
1088 left_threshold_order: Some(1),
1089 right_threshold_order: Some(2),
1090 lu_cut_order: Some(2),
1091 };
1092 iterated.components[1].multiplier_values = smallvec::smallvec![F(0.0), F(0.25)];
1093 for threshold_counterterms in [None, Some(iterated)] {
1094 let original = GenericAdditionalWeightInfo::<f64> {
1095 weights: [
1096 (
1097 super::AdditionalWeightKey::Original,
1098 Complex::new(F(3.0), F(0.5)),
1099 ),
1100 (
1101 super::AdditionalWeightKey::AmplitudeThresholdCountertermVariant {
1102 variant_id: 6,
1103 esurface_id: 2,
1104 overlap_group: 1,
1105 },
1106 Complex::new(F(-0.25), F(0.125)),
1107 ),
1108 ]
1109 .into(),
1110 threshold_counterterms,
1111 };
1112 let json = serde_json::to_value(&original).unwrap();
1113 let restored: GenericAdditionalWeightInfo<f64> =
1114 serde_json::from_value(json.clone()).unwrap();
1115 assert_eq!(serde_json::to_value(restored).unwrap(), json);
1116 if original.threshold_counterterms.is_some() {
1117 let components = &json["threshold_counterterms"]["components"];
1118 assert_eq!(components[0]["occurrence"]["kind"], "amplitude");
1119 assert_eq!(components[1]["occurrence"]["kind"], "local_unitarity");
1120 assert_eq!(components[1]["occurrence"]["overlap_groups"], json!([3, 7]));
1121 assert_eq!(components[1]["occurrence"]["right_threshold_order"], 2);
1122 assert_eq!(components[1]["multiplier_values"], json!([0.0, 0.25]));
1123 assert!(components[0].get("bare").is_some());
1124 assert!(components[1].get("bare").is_none());
1125 } else {
1126 assert!(json.get("threshold_counterterms").is_none());
1127 }
1128 let encoded =
1129 bincode::serde::encode_to_vec(&original, bincode::config::standard()).unwrap();
1130 let (restored, consumed): (GenericAdditionalWeightInfo<f64>, _) =
1131 bincode::serde::decode_from_slice(&encoded, bincode::config::standard()).unwrap();
1132 assert_eq!(consumed, encoded.len());
1133 assert_eq!(serde_json::to_value(restored).unwrap(), json);
1134 }
1135 }
1136
1137 #[test]
1138 fn additional_weights_roundtrip_preserves_native_precision() {
1139 let one = F::<f128>::default().one();
1140 let precise_quad = one + one.from_usize(2).powi(-80);
1141 assert_ne!(precise_quad, F::from_ff64(precise_quad.into_ff64()));
1142 let quad = GenericAdditionalWeightInfo::<f128> {
1143 weights: [(
1144 super::AdditionalWeightKey::Original,
1145 Complex::new(precise_quad, -precise_quad),
1146 )]
1147 .into(),
1148 ..Default::default()
1149 };
1150 let json = serde_json::to_vec(&quad).unwrap();
1151 let restored: GenericAdditionalWeightInfo<f128> = serde_json::from_slice(&json).unwrap();
1152 assert_eq!(restored.weights, quad.weights);
1153 let encoded = bincode::serde::encode_to_vec(&quad, bincode::config::standard()).unwrap();
1154 let (restored, consumed): (GenericAdditionalWeightInfo<f128>, _) =
1155 bincode::serde::decode_from_slice(&encoded, bincode::config::standard()).unwrap();
1156 assert_eq!(consumed, encoded.len());
1157 assert_eq!(restored.weights, quad.weights);
1158
1159 let tiny_arb = F::<ArbPrec>::default().from_usize(2).powi(-1400);
1162 assert!(tiny_arb > tiny_arb.zero());
1163 assert_eq!(tiny_arb.into_ff64(), F(0.0));
1164 let arb = GenericAdditionalWeightInfo::<ArbPrec> {
1165 weights: [(
1166 super::AdditionalWeightKey::Original,
1167 Complex::new(tiny_arb.clone(), -tiny_arb),
1168 )]
1169 .into(),
1170 ..Default::default()
1171 };
1172 let json = serde_json::to_vec(&arb).unwrap();
1173 let restored: GenericAdditionalWeightInfo<ArbPrec> = serde_json::from_slice(&json).unwrap();
1174 assert_eq!(restored.weights, arb.weights);
1175 let encoded = bincode::serde::encode_to_vec(&arb, bincode::config::standard()).unwrap();
1176 let (restored, consumed): (GenericAdditionalWeightInfo<ArbPrec>, _) =
1177 bincode::serde::decode_from_slice(&encoded, bincode::config::standard()).unwrap();
1178 assert_eq!(consumed, encoded.len());
1179 assert_eq!(restored.weights, arb.weights);
1180 }
1181
1182 #[test]
1183 fn decomposition_scaling_preserves_multiplier_values_and_additivity() {
1184 let mut info = decomposition();
1185 assert_eq!(info.total(), Complex::new(F(2.0), F(1.0)));
1186
1187 info.apply_multiplicative_factor(&Complex::new(F(4.0), F(0.0)));
1188
1189 assert_eq!(info.total(), Complex::new(F(8.0), F(4.0)));
1190 assert_eq!(info.components[0].bare, Some(Complex::new(F(-8.0), F(4.0))));
1191 assert_eq!(info.components[0].multiplier_values[0], F(0.5));
1192 assert_eq!(info.components[0].effective_multiplier, F(0.5));
1193 assert!(info.components[1].bare.is_none());
1194 assert_eq!(info.components[1].weighted, Complex::new(F(0.0), F(0.0)));
1195 }
1196
1197 #[test]
1198 fn decomposition_converts_between_active_precision_and_f64() {
1199 let original = decomposition();
1200 let precise = GenericThresholdCountertermEventInfo::<f128>::from_f64(&original);
1201 let roundtrip = precise.to_f64();
1202
1203 assert_eq!(roundtrip.total(), original.total());
1204 assert_eq!(roundtrip.components.len(), 2);
1205 assert_eq!(
1206 roundtrip.components[0].multiplier_values,
1207 original.components[0].multiplier_values
1208 );
1209 assert!(roundtrip.components[1].bare.is_none());
1210 assert!(roundtrip.components[1].evaluation_skipped);
1211 }
1212
1213 #[test]
1214 fn event_scaling_reconciles_weight_with_decomposition_total() {
1215 let mut event = GenericEvent::<f64> {
1216 weight: Complex::new(F(99.0), F(0.0)),
1217 additional_weights: GenericAdditionalWeightInfo {
1218 weights: Default::default(),
1219 threshold_counterterms: Some(decomposition()),
1220 },
1221 ..Default::default()
1222 };
1223
1224 event.apply_multiplicative_factor(&Complex::new(F(4.0), F(0.0)));
1225
1226 assert_eq!(event.weight, Complex::new(F(8.0), F(4.0)));
1227 assert_eq!(
1228 event.weight,
1229 event
1230 .additional_weights
1231 .threshold_counterterms
1232 .as_ref()
1233 .unwrap()
1234 .total()
1235 );
1236 }
1237
1238 #[test]
1239 fn event_display_includes_detailed_threshold_components() {
1240 let event = GenericEvent::<f64> {
1241 weight: decomposition().total(),
1242 additional_weights: GenericAdditionalWeightInfo {
1243 weights: Default::default(),
1244 threshold_counterterms: Some(decomposition()),
1245 },
1246 ..Default::default()
1247 };
1248
1249 let rendered = event.to_string();
1250 assert!(rendered.contains("Threshold counterterms"));
1251 assert!(rendered.contains("amplitude E2, overlap 1"));
1252 assert!(rendered.contains("LU overlaps [3]"));
1253 }
1254}