Skip to main content

gammalooprs/observables/
events.rs

1use super::clustering::ClusteringResult;
2use crate::momentum::{FourMomentum, Rotation};
3use crate::utils::{F, FloatLike, into_complex_ff64};
4use colored::Colorize;
5use serde::{Deserialize, Serialize};
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 lmb_channel_id: Option<usize>,
24    pub lmb_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}
70
71#[derive(Default, Debug, Clone, Serialize, Deserialize)]
72pub struct GenericEventGroup<T: FloatLike>(pub Vec<GenericEvent<T>>);
73
74impl<T: FloatLike> Deref for GenericEventGroup<T> {
75    type Target = Vec<GenericEvent<T>>;
76
77    fn deref(&self) -> &Self::Target {
78        &self.0
79    }
80}
81
82impl<T: FloatLike> DerefMut for GenericEventGroup<T> {
83    fn deref_mut(&mut self) -> &mut Self::Target {
84        &mut self.0
85    }
86}
87
88impl<T: FloatLike> GenericEventGroup<T> {
89    pub fn to_f64(&self) -> EventGroup {
90        GenericEventGroup(self.0.iter().map(GenericEvent::to_f64).collect())
91    }
92
93    pub fn from_f64(event_group: &EventGroup) -> Self {
94        GenericEventGroup(event_group.0.iter().map(GenericEvent::from_f64).collect())
95    }
96}
97
98#[derive(Default, Debug, Clone, Serialize, Deserialize)]
99pub struct GenericEventGroupList<T: FloatLike>(pub Vec<GenericEventGroup<T>>);
100
101impl<T: FloatLike> Deref for GenericEventGroupList<T> {
102    type Target = Vec<GenericEventGroup<T>>;
103
104    fn deref(&self) -> &Self::Target {
105        &self.0
106    }
107}
108
109impl<T: FloatLike> DerefMut for GenericEventGroupList<T> {
110    fn deref_mut(&mut self) -> &mut Self::Target {
111        &mut self.0
112    }
113}
114
115impl<T: FloatLike> GenericEventGroupList<T> {
116    pub fn to_f64(&self) -> EventGroupList {
117        GenericEventGroupList(self.0.iter().map(GenericEventGroup::to_f64).collect())
118    }
119
120    pub fn from_f64(event_group_list: &EventGroupList) -> Self {
121        GenericEventGroupList(
122            event_group_list
123                .0
124                .iter()
125                .map(GenericEventGroup::from_f64)
126                .collect(),
127        )
128    }
129
130    pub fn push_singleton(&mut self, event: GenericEvent<T>) {
131        self.0.push(GenericEventGroup(vec![event]));
132    }
133}
134
135#[derive(Default, Debug, Clone, Serialize, Deserialize)]
136pub struct GenericAdditionalWeightInfo<T: FloatLike> {
137    pub weights: BTreeMap<AdditionalWeightKey, Complex<F<T>>>,
138}
139
140impl<T: FloatLike> GenericAdditionalWeightInfo<T> {
141    pub fn to_f64(&self) -> GenericAdditionalWeightInfo<f64> {
142        GenericAdditionalWeightInfo {
143            weights: self
144                .weights
145                .iter()
146                .map(|(key, weight)| (*key, into_complex_ff64(weight)))
147                .collect(),
148        }
149    }
150
151    pub fn from_f64(additional_weights: &GenericAdditionalWeightInfo<f64>) -> Self {
152        GenericAdditionalWeightInfo {
153            weights: additional_weights
154                .weights
155                .iter()
156                .map(|(key, weight)| {
157                    (
158                        *key,
159                        Complex::new(F::from_ff64(weight.re), F::from_ff64(weight.im)),
160                    )
161                })
162                .collect(),
163        }
164    }
165}
166
167#[derive(Default, Debug, Clone, Serialize, Deserialize)]
168pub struct GenericEvent<T: FloatLike> {
169    #[allow(clippy::type_complexity)]
170    pub kinematic_configuration: (
171        SmallVec<[FourMomentum<F<T>>; 2]>,
172        SmallVec<[FourMomentum<F<T>>; 4]>,
173    ),
174    pub cut_info: CutInfo,
175    // Stores the fully normalized contribution associated with this event.
176    pub weight: Complex<F<T>>,
177    // Contains additional partial weights for future use to do post-processing reweighting.
178    pub additional_weights: GenericAdditionalWeightInfo<T>,
179    #[serde(skip)]
180    pub derived_observable_data: GenericDerivedEventData<T>,
181}
182
183impl<T: FloatLike> GenericEvent<T> {
184    pub(crate) fn inverse_rotate(&mut self, rotation: &Rotation) {
185        if rotation.is_identity() {
186            return;
187        }
188
189        for momentum in self.kinematic_configuration.0.iter_mut() {
190            *momentum = rotation.inverse_rotate_four(momentum);
191        }
192        for momentum in self.kinematic_configuration.1.iter_mut() {
193            *momentum = rotation.inverse_rotate_four(momentum);
194        }
195
196        // Any cached clustering belongs to the rotated frame and must be recomputed.
197        self.derived_observable_data = GenericDerivedEventData::default();
198    }
199
200    pub fn ensure_clustering_slots(&mut self, n_clusterings: usize) {
201        if self.derived_observable_data.clustered_jets.len() < n_clusterings {
202            self.derived_observable_data
203                .clustered_jets
204                .resize_with(n_clusterings, || None);
205        }
206    }
207
208    pub fn cached_clustering(&self, handle: usize) -> Option<&ClusteringResult<T>> {
209        self.derived_observable_data
210            .clustered_jets
211            .get(handle)
212            .and_then(|result| result.as_ref())
213    }
214
215    pub fn to_f64(&self) -> Event {
216        GenericEvent {
217            kinematic_configuration: (
218                self.kinematic_configuration
219                    .0
220                    .iter()
221                    .map(FourMomentum::to_f64)
222                    .collect(),
223                self.kinematic_configuration
224                    .1
225                    .iter()
226                    .map(FourMomentum::to_f64)
227                    .collect(),
228            ),
229            cut_info: self.cut_info.clone(),
230            weight: into_complex_ff64(&self.weight),
231            additional_weights: self.additional_weights.to_f64(),
232            derived_observable_data: self.derived_observable_data.to_f64(),
233        }
234    }
235
236    pub fn from_f64(event: &Event) -> Self {
237        GenericEvent {
238            kinematic_configuration: (
239                event
240                    .kinematic_configuration
241                    .0
242                    .iter()
243                    .map(FourMomentum::from_ff64)
244                    .collect(),
245                event
246                    .kinematic_configuration
247                    .1
248                    .iter()
249                    .map(FourMomentum::from_ff64)
250                    .collect(),
251            ),
252            cut_info: event.cut_info.clone(),
253            weight: Complex::new(F::from_ff64(event.weight.re), F::from_ff64(event.weight.im)),
254            additional_weights: GenericAdditionalWeightInfo::from_f64(&event.additional_weights),
255            derived_observable_data: GenericDerivedEventData::from_f64(
256                &event.derived_observable_data,
257            ),
258        }
259    }
260}
261
262#[derive(Tabled)]
263struct EventSummaryRow {
264    field: String,
265    value: String,
266}
267
268#[derive(Tabled)]
269struct MomentumRow {
270    #[tabled(rename = "state")]
271    state: String,
272    #[tabled(rename = "PDG")]
273    pdg: String,
274    #[tabled(rename = "E")]
275    e: String,
276    px: String,
277    py: String,
278    pz: String,
279    #[tabled(rename = "sqrt(p^2)")]
280    p2: String,
281}
282
283#[derive(Tabled)]
284struct AdditionalWeightRow {
285    key: String,
286    value: String,
287}
288
289fn display_decimal_precision<T: FloatLike>(value: &F<T>) -> usize {
290    let precision_bits = value.0.get_precision().max(1) as f64;
291    (precision_bits * std::f64::consts::LOG10_2).ceil().max(1.0) as usize
292}
293
294pub(crate) fn format_real_generic<T: FloatLike>(value: &F<T>) -> String {
295    format!("{:+.*e}", display_decimal_precision(value), value)
296}
297
298fn format_count(value: usize) -> String {
299    if value < 1_000 {
300        return value.to_string();
301    }
302
303    let value = value as f64;
304    for (scale, suffix) in [
305        (1_000_000_000_f64, "B"),
306        (1_000_000_f64, "M"),
307        (1_000_f64, "K"),
308    ] {
309        if value >= scale {
310            let scaled = value / scale;
311            let precision = if scaled >= 100.0 {
312                0
313            } else if scaled >= 10.0 {
314                1
315            } else {
316                2
317            };
318            return format!("{scaled:.precision$}{suffix}");
319        }
320    }
321
322    value.round().to_string()
323}
324
325pub(crate) fn format_optional_real_generic<T: FloatLike>(value: Option<&F<T>>) -> String {
326    value
327        .map(format_real_generic)
328        .unwrap_or_else(|| "None".red().to_string())
329}
330
331pub(crate) fn format_complex_generic<T: FloatLike>(value: &Complex<F<T>>) -> String {
332    let precision = display_decimal_precision(&value.re).max(display_decimal_precision(&value.im));
333    format!("{:+.*e} {:+.*e}i", precision, value.re, precision, value.im)
334}
335
336fn format_pdg(pdg: Option<isize>) -> String {
337    pdg.map(|value| value.to_string())
338        .unwrap_or_else(|| "N/A".red().to_string())
339}
340
341fn format_pdg_with_state_color(pdg: Option<isize>, incoming: bool) -> String {
342    let pdg = format_pdg(pdg);
343    if incoming {
344        pdg.bright_blue().to_string()
345    } else {
346        pdg.bright_green().to_string()
347    }
348}
349
350fn format_lmb_channel_edge_ids(edge_ids: Option<&[usize]>) -> String {
351    edge_ids
352        .map(|edge_ids| {
353            format!(
354                "({})",
355                edge_ids
356                    .iter()
357                    .map(|edge_id| edge_id.to_string())
358                    .collect::<Vec<_>>()
359                    .join(",")
360            )
361        })
362        .unwrap_or_else(|| "None".to_string())
363}
364
365fn momentum_mass_squared<T: FloatLike>(momentum: &FourMomentum<F<T>>) -> F<T> {
366    momentum.temporal.value.clone() * momentum.temporal.value.clone()
367        - momentum.spatial.px.clone() * momentum.spatial.px.clone()
368        - momentum.spatial.py.clone() * momentum.spatial.py.clone()
369        - momentum.spatial.pz.clone() * momentum.spatial.pz.clone()
370}
371
372fn momentum_mass<T: FloatLike>(momentum: &FourMomentum<F<T>>) -> F<T> {
373    momentum_mass_squared(momentum).abs().sqrt()
374}
375
376fn format_momentum_rows<T: FloatLike>(
377    state: String,
378    momenta: &[FourMomentum<F<T>>],
379    pdgs: &[isize],
380    incoming: bool,
381) -> Vec<MomentumRow> {
382    momenta
383        .iter()
384        .enumerate()
385        .map(|(index, momentum)| MomentumRow {
386            state: state.clone(),
387            pdg: format_pdg_with_state_color(pdgs.get(index).copied(), incoming),
388            e: format_real_generic(&momentum.temporal.value),
389            px: format_real_generic(&momentum.spatial.px),
390            py: format_real_generic(&momentum.spatial.py),
391            pz: format_real_generic(&momentum.spatial.pz),
392            p2: format_real_generic(&momentum_mass(momentum))
393                .bright_yellow()
394                .to_string(),
395        })
396        .collect()
397}
398
399fn event_zero<T: FloatLike>(event: &GenericEvent<T>) -> F<T> {
400    event
401        .kinematic_configuration
402        .0
403        .first()
404        .map(|momentum| momentum.temporal.value.zero())
405        .or_else(|| {
406            event
407                .kinematic_configuration
408                .1
409                .first()
410                .map(|momentum| momentum.temporal.value.zero())
411        })
412        .unwrap_or_else(|| F(T::new_zero()))
413}
414
415fn conservation_row<T: FloatLike>(event: &GenericEvent<T>) -> MomentumRow {
416    let zero = event_zero(event);
417    let incoming = event.kinematic_configuration.0.iter().fold(
418        (zero.clone(), zero.clone(), zero.clone(), zero.clone()),
419        |acc, momentum| {
420            (
421                acc.0 + momentum.temporal.value.clone(),
422                acc.1 + momentum.spatial.px.clone(),
423                acc.2 + momentum.spatial.py.clone(),
424                acc.3 + momentum.spatial.pz.clone(),
425            )
426        },
427    );
428    let outgoing = event.kinematic_configuration.1.iter().fold(
429        (zero.clone(), zero.clone(), zero.clone(), zero),
430        |acc, momentum| {
431            (
432                acc.0 + momentum.temporal.value.clone(),
433                acc.1 + momentum.spatial.px.clone(),
434                acc.2 + momentum.spatial.py.clone(),
435                acc.3 + momentum.spatial.pz.clone(),
436            )
437        },
438    );
439    let delta = FourMomentum {
440        temporal: crate::momentum::Energy::new(incoming.0 - outgoing.0),
441        spatial: crate::momentum::ThreeMomentum::new(
442            incoming.1 - outgoing.1,
443            incoming.2 - outgoing.2,
444            incoming.3 - outgoing.3,
445        ),
446    };
447
448    MomentumRow {
449        state: "CHECK".bold().bright_yellow().to_string(),
450        pdg: "N/A".bright_yellow().to_string(),
451        e: format_real_generic(&delta.temporal.value)
452            .bright_yellow()
453            .to_string(),
454        px: format_real_generic(&delta.spatial.px)
455            .bright_yellow()
456            .to_string(),
457        py: format_real_generic(&delta.spatial.py)
458            .bright_yellow()
459            .to_string(),
460        pz: format_real_generic(&delta.spatial.pz)
461            .bright_yellow()
462            .to_string(),
463        p2: String::new().bright_yellow().to_string(),
464    }
465}
466
467fn indent_block(block: &str, prefix: &str) -> String {
468    let mut result = String::new();
469    for (index, line) in block.lines().enumerate() {
470        if index > 0 {
471            result.push('\n');
472        }
473        result.push_str(prefix);
474        result.push_str(line);
475    }
476    result
477}
478
479impl fmt::Display for AdditionalWeightKey {
480    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
481        match self {
482            AdditionalWeightKey::FullMultiplicativeFactor => {
483                write!(f, "full multiplicative factor")
484            }
485            AdditionalWeightKey::Original => write!(f, "original"),
486            AdditionalWeightKey::ThresholdCounterterm { subset_index } => {
487                write!(f, "threshold_counterterm:{subset_index}")
488            }
489            AdditionalWeightKey::AmplitudeThresholdCounterterm {
490                esurface_id,
491                overlap_group,
492            } => {
493                write!(f, "threshold_counterterm:{esurface_id}:{overlap_group}")
494            }
495        }
496    }
497}
498
499impl fmt::Display for CutInfo {
500    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501        let rows = vec![
502            EventSummaryRow {
503                field: "graph".to_string(),
504                value: self.graph_id.to_string(),
505            },
506            EventSummaryRow {
507                field: "graph group".to_string(),
508                value: self
509                    .graph_group_id
510                    .map(|value| value.to_string())
511                    .unwrap_or_else(|| "None".to_string()),
512            },
513            EventSummaryRow {
514                field: "orientation".to_string(),
515                value: self
516                    .orientation_id
517                    .map(|value| value.to_string())
518                    .unwrap_or_else(|| "None".to_string()),
519            },
520            EventSummaryRow {
521                field: "cut".to_string(),
522                value: self.cut_id.to_string(),
523            },
524            EventSummaryRow {
525                field: "lmb channel id".to_string(),
526                value: self
527                    .lmb_channel_id
528                    .map(|value| value.to_string())
529                    .unwrap_or_else(|| "None".to_string()),
530            },
531            EventSummaryRow {
532                field: "lmb channel".to_string(),
533                value: format_lmb_channel_edge_ids(self.lmb_channel_edge_ids.as_deref()),
534            },
535        ];
536        write!(f, "{}", Table::new(rows).with(Style::rounded()))
537    }
538}
539
540impl<T: FloatLike> fmt::Display for GenericEvent<T> {
541    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542        let summary = vec![
543            EventSummaryRow {
544                field: "graph".to_string(),
545                value: self.cut_info.graph_id.to_string(),
546            },
547            EventSummaryRow {
548                field: "graph group".to_string(),
549                value: self
550                    .cut_info
551                    .graph_group_id
552                    .map(|value| value.to_string())
553                    .unwrap_or_else(|| "None".to_string()),
554            },
555            EventSummaryRow {
556                field: "orientation".to_string(),
557                value: self
558                    .cut_info
559                    .orientation_id
560                    .map(|value| value.to_string())
561                    .unwrap_or_else(|| "None".to_string()),
562            },
563            EventSummaryRow {
564                field: "cut".to_string(),
565                value: self.cut_info.cut_id.to_string(),
566            },
567            EventSummaryRow {
568                field: "lmb channel id".to_string(),
569                value: self
570                    .cut_info
571                    .lmb_channel_id
572                    .map(|value| value.to_string())
573                    .unwrap_or_else(|| "None".to_string()),
574            },
575            EventSummaryRow {
576                field: "lmb channel".to_string(),
577                value: format_lmb_channel_edge_ids(self.cut_info.lmb_channel_edge_ids.as_deref()),
578            },
579            EventSummaryRow {
580                field: "weight".to_string(),
581                value: format_complex_generic(&self.weight),
582            },
583        ];
584
585        writeln!(f, "{}", "Event".bold().bright_cyan())?;
586        writeln!(f, "{}", Table::new(summary).with(Style::rounded()))?;
587
588        let mut momentum_rows = format_momentum_rows(
589            "IN".bold().bright_blue().to_string(),
590            &self.kinematic_configuration.0,
591            &self.cut_info.particle_pdgs.0,
592            true,
593        );
594        let incoming_row_count = momentum_rows.len();
595        momentum_rows.extend(format_momentum_rows(
596            "OUT".bold().bright_green().to_string(),
597            &self.kinematic_configuration.1,
598            &self.cut_info.particle_pdgs.1,
599            false,
600        ));
601        if !momentum_rows.is_empty() {
602            momentum_rows.push(conservation_row(self));
603            let check_separator_index = momentum_rows.len();
604            let header_separator = (
605                1,
606                HorizontalLine::new('─')
607                    .intersection('┼')
608                    .left('├')
609                    .right('┤'),
610            );
611            let check_separator = (
612                check_separator_index,
613                HorizontalLine::new('─')
614                    .intersection('┼')
615                    .left('├')
616                    .right('┤'),
617            );
618            writeln!(f)?;
619            writeln!(f, "{}", "Kinematics".bold().bright_blue())?;
620            if incoming_row_count > 0 && incoming_row_count < check_separator_index - 1 {
621                let in_out_separator = (
622                    incoming_row_count + 1,
623                    HorizontalLine::new('─')
624                        .intersection('┼')
625                        .left('├')
626                        .right('┤'),
627                );
628                writeln!(
629                    f,
630                    "{}",
631                    Table::new(momentum_rows).with(Style::rounded().horizontals([
632                        header_separator,
633                        in_out_separator,
634                        check_separator,
635                    ]))
636                )?;
637            } else {
638                writeln!(
639                    f,
640                    "{}",
641                    Table::new(momentum_rows)
642                        .with(Style::rounded().horizontals([header_separator, check_separator]))
643                )?;
644            }
645        }
646
647        if !self.additional_weights.weights.is_empty() {
648            let additional_weights = self
649                .additional_weights
650                .weights
651                .iter()
652                .map(|(key, value)| AdditionalWeightRow {
653                    key: key.to_string(),
654                    value: format_complex_generic(value),
655                })
656                .collect::<Vec<_>>();
657
658            writeln!(f)?;
659            writeln!(f, "{}", "Additional weights".bold().bright_magenta())?;
660            write!(
661                f,
662                "{}",
663                Table::new(additional_weights).with(Style::rounded())
664            )?;
665        }
666
667        Ok(())
668    }
669}
670
671impl<T: FloatLike> fmt::Display for GenericEventGroup<T> {
672    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
673        writeln!(
674            f,
675            "{}",
676            format!("Event group ({} event(s))", format_count(self.len()))
677                .bold()
678                .bright_green()
679        )?;
680        for (index, event) in self.iter().enumerate() {
681            writeln!(f, "  {}", format!("Event {index}:").bold().bright_yellow())?;
682            let event_str = event.to_string();
683            writeln!(f, "{}", indent_block(&event_str, "    "))?;
684        }
685        Ok(())
686    }
687}
688
689impl<T: FloatLike> fmt::Display for GenericEventGroupList<T> {
690    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
691        for (index, group) in self.iter().enumerate() {
692            if index > 0 {
693                writeln!(f)?;
694            }
695            writeln!(
696                f,
697                "{}",
698                format!("Event group {index}:").bold().bright_green()
699            )?;
700            writeln!(f, "{}", indent_block(&group.to_string(), "  "))?;
701        }
702        Ok(())
703    }
704}