laddu_core/utils/
variables.rs

1use dyn_clone::DynClone;
2use serde::{Deserialize, Serialize};
3use std::fmt::{Debug, Display};
4
5#[cfg(feature = "rayon")]
6use rayon::prelude::*;
7
8#[cfg(feature = "mpi")]
9use crate::mpi::LadduMPI;
10use crate::{
11    data::{Dataset, DatasetMetadata, EventData},
12    utils::{
13        enums::{Channel, Frame},
14        vectors::{Vec3, Vec4},
15    },
16    LadduError, LadduResult,
17};
18
19use auto_ops::impl_op_ex;
20
21#[cfg(feature = "mpi")]
22use mpi::{datatype::PartitionMut, topology::SimpleCommunicator, traits::*};
23
24/// Standard methods for extracting some value out of an [`EventData`].
25#[typetag::serde(tag = "type")]
26pub trait Variable: DynClone + Send + Sync + Debug + Display {
27    /// Bind the variable to dataset metadata so that any referenced names can be resolved to
28    /// concrete indices. Implementations that do not require metadata may keep the default
29    /// no-op.
30    fn bind(&mut self, _metadata: &DatasetMetadata) -> LadduResult<()> {
31        Ok(())
32    }
33
34    /// This method takes an [`EventData`] and extracts a single value (like the mass of a particle).
35    fn value(&self, event: &EventData) -> f64;
36
37    /// This method distributes the [`Variable::value`] method over each [`EventData`] in a
38    /// [`Dataset`] (non-MPI version).
39    ///
40    /// # Notes
41    ///
42    /// This method is not intended to be called in analyses but rather in writing methods
43    /// that have `mpi`-feature-gated versions. Most users should just call [`Variable::value_on`] instead.
44    fn value_on_local(&self, dataset: &Dataset) -> LadduResult<Vec<f64>> {
45        let mut variable = dyn_clone::clone_box(self);
46        variable.bind(dataset.metadata())?;
47        #[cfg(feature = "rayon")]
48        let local_values: Vec<f64> = dataset
49            .events
50            .par_iter()
51            .map(|e| variable.value(e))
52            .collect();
53        #[cfg(not(feature = "rayon"))]
54        let local_values: Vec<f64> = dataset.events.iter().map(|e| variable.value(e)).collect();
55        Ok(local_values)
56    }
57
58    /// This method distributes the [`Variable::value`] method over each [`EventData`] in a
59    /// [`Dataset`] (MPI-compatible version).
60    ///
61    /// # Notes
62    ///
63    /// This method is not intended to be called in analyses but rather in writing methods
64    /// that have `mpi`-feature-gated versions. Most users should just call [`Variable::value_on`] instead.
65    #[cfg(feature = "mpi")]
66    fn value_on_mpi(&self, dataset: &Dataset, world: &SimpleCommunicator) -> LadduResult<Vec<f64>> {
67        let local_weights = self.value_on_local(dataset)?;
68        let n_events = dataset.n_events();
69        let mut buffer: Vec<f64> = vec![0.0; n_events];
70        let (counts, displs) = world.get_counts_displs(n_events);
71        {
72            let mut partitioned_buffer = PartitionMut::new(&mut buffer, counts, displs);
73            world.all_gather_varcount_into(&local_weights, &mut partitioned_buffer);
74        }
75        Ok(buffer)
76    }
77
78    /// This method distributes the [`Variable::value`] method over each [`EventData`] in a
79    /// [`Dataset`].
80    fn value_on(&self, dataset: &Dataset) -> LadduResult<Vec<f64>> {
81        #[cfg(feature = "mpi")]
82        {
83            if let Some(world) = crate::mpi::get_world() {
84                return self.value_on_mpi(dataset, &world);
85            }
86        }
87        self.value_on_local(dataset)
88    }
89
90    /// Create an [`VariableExpression`] that evaluates to `self == val`
91    fn eq(&self, val: f64) -> VariableExpression
92    where
93        Self: std::marker::Sized + 'static,
94    {
95        VariableExpression::Eq(dyn_clone::clone_box(self), val)
96    }
97
98    /// Create an [`VariableExpression`] that evaluates to `self < val`
99    fn lt(&self, val: f64) -> VariableExpression
100    where
101        Self: std::marker::Sized + 'static,
102    {
103        VariableExpression::Lt(dyn_clone::clone_box(self), val)
104    }
105
106    /// Create an [`VariableExpression`] that evaluates to `self > val`
107    fn gt(&self, val: f64) -> VariableExpression
108    where
109        Self: std::marker::Sized + 'static,
110    {
111        VariableExpression::Gt(dyn_clone::clone_box(self), val)
112    }
113
114    /// Create an [`VariableExpression`] that evaluates to `self >= val`
115    fn ge(&self, val: f64) -> VariableExpression
116    where
117        Self: std::marker::Sized + 'static,
118    {
119        self.gt(val).or(&self.eq(val))
120    }
121
122    /// Create an [`VariableExpression`] that evaluates to `self <= val`
123    fn le(&self, val: f64) -> VariableExpression
124    where
125        Self: std::marker::Sized + 'static,
126    {
127        self.lt(val).or(&self.eq(val))
128    }
129}
130dyn_clone::clone_trait_object!(Variable);
131
132/// Expressions which can be used to compare [`Variable`]s to [`f64`]s.
133#[derive(Clone, Debug)]
134pub enum VariableExpression {
135    /// Expression which is true when the variable is equal to the float.
136    Eq(Box<dyn Variable>, f64),
137    /// Expression which is true when the variable is less than the float.
138    Lt(Box<dyn Variable>, f64),
139    /// Expression which is true when the variable is greater than the float.
140    Gt(Box<dyn Variable>, f64),
141    /// Expression which is true when both inner expressions are true.
142    And(Box<VariableExpression>, Box<VariableExpression>),
143    /// Expression which is true when either inner expression is true.
144    Or(Box<VariableExpression>, Box<VariableExpression>),
145    /// Expression which is true when the inner expression is false.
146    Not(Box<VariableExpression>),
147}
148
149impl VariableExpression {
150    /// Construct an [`VariableExpression::And`] from the current expression and another.
151    pub fn and(&self, rhs: &VariableExpression) -> VariableExpression {
152        VariableExpression::And(Box::new(self.clone()), Box::new(rhs.clone()))
153    }
154
155    /// Construct an [`VariableExpression::Or`] from the current expression and another.
156    pub fn or(&self, rhs: &VariableExpression) -> VariableExpression {
157        VariableExpression::Or(Box::new(self.clone()), Box::new(rhs.clone()))
158    }
159
160    /// Comple the [`VariableExpression`] into a [`CompiledExpression`] bound to the supplied
161    /// metadata so that all variable references are resolved.
162    pub(crate) fn compile(&self, metadata: &DatasetMetadata) -> LadduResult<CompiledExpression> {
163        let mut compiled = compile_expression(self.clone());
164        compiled.bind(metadata)?;
165        Ok(compiled)
166    }
167}
168impl Display for VariableExpression {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        match self {
171            VariableExpression::Eq(var, val) => {
172                write!(f, "({} == {})", var, val)
173            }
174            VariableExpression::Lt(var, val) => {
175                write!(f, "({} < {})", var, val)
176            }
177            VariableExpression::Gt(var, val) => {
178                write!(f, "({} > {})", var, val)
179            }
180            VariableExpression::And(lhs, rhs) => {
181                write!(f, "({} & {})", lhs, rhs)
182            }
183            VariableExpression::Or(lhs, rhs) => {
184                write!(f, "({} | {})", lhs, rhs)
185            }
186            VariableExpression::Not(inner) => {
187                write!(f, "!({})", inner)
188            }
189        }
190    }
191}
192
193/// A method which negates the given expression.
194pub fn not(expr: &VariableExpression) -> VariableExpression {
195    VariableExpression::Not(Box::new(expr.clone()))
196}
197
198#[rustfmt::skip]
199impl_op_ex!(& |lhs: &VariableExpression, rhs: &VariableExpression| -> VariableExpression{ lhs.and(rhs) });
200#[rustfmt::skip]
201impl_op_ex!(| |lhs: &VariableExpression, rhs: &VariableExpression| -> VariableExpression{ lhs.or(rhs) });
202#[rustfmt::skip]
203impl_op_ex!(! |exp: &VariableExpression| -> VariableExpression{ not(exp) });
204
205#[derive(Debug)]
206enum Opcode {
207    PushEq(usize, f64),
208    PushLt(usize, f64),
209    PushGt(usize, f64),
210    And,
211    Or,
212    Not,
213}
214
215pub(crate) struct CompiledExpression {
216    bytecode: Vec<Opcode>,
217    variables: Vec<Box<dyn Variable>>,
218}
219
220impl CompiledExpression {
221    pub fn bind(&mut self, metadata: &DatasetMetadata) -> LadduResult<()> {
222        for variable in &mut self.variables {
223            variable.bind(metadata)?;
224        }
225        Ok(())
226    }
227
228    /// Evaluate the [`CompiledExpression`] on a given [`EventData`].
229    pub fn evaluate(&self, event: &EventData) -> bool {
230        let mut stack = Vec::with_capacity(self.bytecode.len());
231
232        for op in &self.bytecode {
233            match op {
234                Opcode::PushEq(i, val) => stack.push(self.variables[*i].value(event) == *val),
235                Opcode::PushLt(i, val) => stack.push(self.variables[*i].value(event) < *val),
236                Opcode::PushGt(i, val) => stack.push(self.variables[*i].value(event) > *val),
237                Opcode::Not => {
238                    let a = stack.pop().unwrap();
239                    stack.push(!a);
240                }
241                Opcode::And => {
242                    let b = stack.pop().unwrap();
243                    let a = stack.pop().unwrap();
244                    stack.push(a && b);
245                }
246                Opcode::Or => {
247                    let b = stack.pop().unwrap();
248                    let a = stack.pop().unwrap();
249                    stack.push(a || b);
250                }
251            }
252        }
253
254        stack.pop().unwrap()
255    }
256}
257
258pub(crate) fn compile_expression(expr: VariableExpression) -> CompiledExpression {
259    let mut bytecode = Vec::new();
260    let mut variables: Vec<Box<dyn Variable>> = Vec::new();
261
262    fn compile(
263        expr: VariableExpression,
264        bytecode: &mut Vec<Opcode>,
265        variables: &mut Vec<Box<dyn Variable>>,
266    ) {
267        match expr {
268            VariableExpression::Eq(var, val) => {
269                variables.push(var);
270                bytecode.push(Opcode::PushEq(variables.len() - 1, val));
271            }
272            VariableExpression::Lt(var, val) => {
273                variables.push(var);
274                bytecode.push(Opcode::PushLt(variables.len() - 1, val));
275            }
276            VariableExpression::Gt(var, val) => {
277                variables.push(var);
278                bytecode.push(Opcode::PushGt(variables.len() - 1, val));
279            }
280            VariableExpression::And(lhs, rhs) => {
281                compile(*lhs, bytecode, variables);
282                compile(*rhs, bytecode, variables);
283                bytecode.push(Opcode::And);
284            }
285            VariableExpression::Or(lhs, rhs) => {
286                compile(*lhs, bytecode, variables);
287                compile(*rhs, bytecode, variables);
288                bytecode.push(Opcode::Or);
289            }
290            VariableExpression::Not(inner) => {
291                compile(*inner, bytecode, variables);
292                bytecode.push(Opcode::Not);
293            }
294        }
295    }
296
297    compile(expr, &mut bytecode, &mut variables);
298
299    CompiledExpression {
300        bytecode,
301        variables,
302    }
303}
304
305fn names_to_string(names: &[String]) -> String {
306    names.join(", ")
307}
308
309/// A reusable selection that may span one or more four-momentum names.
310///
311/// Instances are constructed from metadata-facing identifiers and later bound to
312/// column indices so that variable evaluators can resolve aliases or grouped
313/// particles efficiently.
314#[derive(Clone, Debug, Serialize, Deserialize)]
315pub struct P4Selection {
316    names: Vec<String>,
317    #[serde(skip, default)]
318    indices: Vec<usize>,
319}
320
321impl P4Selection {
322    fn new_many<I, S>(names: I) -> Self
323    where
324        I: IntoIterator<Item = S>,
325        S: Into<String>,
326    {
327        Self {
328            names: names.into_iter().map(Into::into).collect(),
329            indices: Vec::new(),
330        }
331    }
332
333    pub(crate) fn with_indices<I, S>(names: I, indices: Vec<usize>) -> Self
334    where
335        I: IntoIterator<Item = S>,
336        S: Into<String>,
337    {
338        Self {
339            names: names.into_iter().map(Into::into).collect(),
340            indices,
341        }
342    }
343
344    /// Returns the metadata names contributing to this selection.
345    pub fn names(&self) -> &[String] {
346        &self.names
347    }
348
349    pub(crate) fn bind(&mut self, metadata: &DatasetMetadata) -> LadduResult<()> {
350        let mut resolved = Vec::with_capacity(self.names.len());
351        for name in &self.names {
352            metadata.append_indices_for_name(name, &mut resolved)?;
353        }
354        self.indices = resolved;
355        Ok(())
356    }
357
358    /// The resolved column indices backing this selection.
359    pub fn indices(&self) -> &[usize] {
360        &self.indices
361    }
362
363    pub(crate) fn momentum(&self, event: &EventData) -> Vec4 {
364        event.get_p4_sum(self.indices())
365    }
366}
367
368/// Helper trait to convert common particle specifications into [`P4Selection`] instances.
369pub trait IntoP4Selection {
370    /// Convert the input into a [`P4Selection`].
371    fn into_selection(self) -> P4Selection;
372}
373
374impl IntoP4Selection for P4Selection {
375    fn into_selection(self) -> P4Selection {
376        self
377    }
378}
379
380impl IntoP4Selection for &P4Selection {
381    fn into_selection(self) -> P4Selection {
382        self.clone()
383    }
384}
385
386impl IntoP4Selection for String {
387    fn into_selection(self) -> P4Selection {
388        P4Selection::new_many(vec![self])
389    }
390}
391
392impl IntoP4Selection for &String {
393    fn into_selection(self) -> P4Selection {
394        P4Selection::new_many(vec![self.clone()])
395    }
396}
397
398impl IntoP4Selection for &str {
399    fn into_selection(self) -> P4Selection {
400        P4Selection::new_many(vec![self.to_string()])
401    }
402}
403
404impl<S> IntoP4Selection for Vec<S>
405where
406    S: Into<String>,
407{
408    fn into_selection(self) -> P4Selection {
409        P4Selection::new_many(self.into_iter().map(Into::into).collect::<Vec<_>>())
410    }
411}
412
413impl<S> IntoP4Selection for &[S]
414where
415    S: Clone + Into<String>,
416{
417    fn into_selection(self) -> P4Selection {
418        P4Selection::new_many(self.iter().cloned().map(Into::into).collect::<Vec<_>>())
419    }
420}
421
422impl<S, const N: usize> IntoP4Selection for [S; N]
423where
424    S: Into<String>,
425{
426    fn into_selection(self) -> P4Selection {
427        P4Selection::new_many(self.into_iter().map(Into::into).collect::<Vec<_>>())
428    }
429}
430
431impl<S, const N: usize> IntoP4Selection for &[S; N]
432where
433    S: Clone + Into<String>,
434{
435    fn into_selection(self) -> P4Selection {
436        P4Selection::new_many(self.iter().cloned().map(Into::into).collect::<Vec<_>>())
437    }
438}
439
440/// A reusable 2-to-2 reaction description shared by several kinematic variables.
441///
442/// A topology records the four canonical vertices $`k_1 + k_2 \to k_3 + k_4`$.
443/// When one vertex is omitted, it is reconstructed by enforcing four-momentum
444/// conservation, which is unambiguous in that frame. Use [`Topology::com_boost_vector`]
445/// and the `*_com` helpers to access particles in the center-of-momentum frame.
446///
447/// ```text
448/// k1  k3
449///  ╲  ╱
450///   â•­â•®
451///   ╰╯
452///  ╱  ╲
453/// k2  k4
454/// ```
455///
456/// Note that variables are typically designed to use $`k_1`$ as the incoming beam, $`k_2`$ as a
457/// target, $`k_3`$ as some resonance, and $`k_4`$ as the recoiling target particle, but this
458/// notation should be extensible to any 2-to-2 reaction.
459#[derive(Clone, Debug, Serialize, Deserialize)]
460pub enum Topology {
461    /// All four vertices are explicitly provided.
462    Full {
463        /// First incoming vertex.
464        k1: P4Selection,
465        /// Second incoming vertex.
466        k2: P4Selection,
467        /// First outgoing vertex.
468        k3: P4Selection,
469        /// Second outgoing vertex.
470        k4: P4Selection,
471    },
472    /// The first incoming vertex (`k1`) is reconstructed.
473    MissingK1 {
474        /// Second incoming vertex.
475        k2: P4Selection,
476        /// First outgoing vertex.
477        k3: P4Selection,
478        /// Second outgoing vertex.
479        k4: P4Selection,
480    },
481    /// The second incoming vertex (`k2`) is reconstructed.
482    MissingK2 {
483        /// First incoming vertex.
484        k1: P4Selection,
485        /// First outgoing vertex.
486        k3: P4Selection,
487        /// Second outgoing vertex.
488        k4: P4Selection,
489    },
490    /// The first outgoing vertex (`k3`) is reconstructed.
491    MissingK3 {
492        /// First incoming vertex.
493        k1: P4Selection,
494        /// Second incoming vertex.
495        k2: P4Selection,
496        /// Second outgoing vertex.
497        k4: P4Selection,
498    },
499    /// The second outgoing vertex (`k4`) is reconstructed.
500    MissingK4 {
501        /// First incoming vertex.
502        k1: P4Selection,
503        /// Second incoming vertex.
504        k2: P4Selection,
505        /// First outgoing vertex.
506        k3: P4Selection,
507    },
508}
509
510impl Topology {
511    /// Construct a topology with all four vertices explicitly defined.
512    pub fn new<K1, K2, K3, K4>(k1: K1, k2: K2, k3: K3, k4: K4) -> Self
513    where
514        K1: IntoP4Selection,
515        K2: IntoP4Selection,
516        K3: IntoP4Selection,
517        K4: IntoP4Selection,
518    {
519        Self::Full {
520            k1: k1.into_selection(),
521            k2: k2.into_selection(),
522            k3: k3.into_selection(),
523            k4: k4.into_selection(),
524        }
525    }
526
527    /// Construct a topology when the first incoming vertex (`k1`) is omitted.
528    pub fn missing_k1<K2, K3, K4>(k2: K2, k3: K3, k4: K4) -> Self
529    where
530        K2: IntoP4Selection,
531        K3: IntoP4Selection,
532        K4: IntoP4Selection,
533    {
534        Self::MissingK1 {
535            k2: k2.into_selection(),
536            k3: k3.into_selection(),
537            k4: k4.into_selection(),
538        }
539    }
540
541    /// Construct a topology when the second incoming vertex (`k2`) is omitted.
542    pub fn missing_k2<K1, K3, K4>(k1: K1, k3: K3, k4: K4) -> Self
543    where
544        K1: IntoP4Selection,
545        K3: IntoP4Selection,
546        K4: IntoP4Selection,
547    {
548        Self::MissingK2 {
549            k1: k1.into_selection(),
550            k3: k3.into_selection(),
551            k4: k4.into_selection(),
552        }
553    }
554
555    /// Construct a topology when the first outgoing vertex (`k3`) is omitted.
556    pub fn missing_k3<K1, K2, K4>(k1: K1, k2: K2, k4: K4) -> Self
557    where
558        K1: IntoP4Selection,
559        K2: IntoP4Selection,
560        K4: IntoP4Selection,
561    {
562        Self::MissingK3 {
563            k1: k1.into_selection(),
564            k2: k2.into_selection(),
565            k4: k4.into_selection(),
566        }
567    }
568
569    /// Construct a topology when the second outgoing vertex (`k4`) is omitted.
570    pub fn missing_k4<K1, K2, K3>(k1: K1, k2: K2, k3: K3) -> Self
571    where
572        K1: IntoP4Selection,
573        K2: IntoP4Selection,
574        K3: IntoP4Selection,
575    {
576        Self::MissingK4 {
577            k1: k1.into_selection(),
578            k2: k2.into_selection(),
579            k3: k3.into_selection(),
580        }
581    }
582
583    /// Bind every vertex to dataset metadata so the particle names resolve to indices.
584    pub fn bind(&mut self, metadata: &DatasetMetadata) -> LadduResult<()> {
585        match self {
586            Topology::Full { k1, k2, k3, k4 } => {
587                k1.bind(metadata)?;
588                k2.bind(metadata)?;
589                k3.bind(metadata)?;
590                k4.bind(metadata)?;
591            }
592            Topology::MissingK1 { k2, k3, k4 } => {
593                k2.bind(metadata)?;
594                k3.bind(metadata)?;
595                k4.bind(metadata)?;
596            }
597            Topology::MissingK2 { k1, k3, k4 } => {
598                k1.bind(metadata)?;
599                k3.bind(metadata)?;
600                k4.bind(metadata)?;
601            }
602            Topology::MissingK3 { k1, k2, k4 } => {
603                k1.bind(metadata)?;
604                k2.bind(metadata)?;
605                k4.bind(metadata)?;
606            }
607            Topology::MissingK4 { k1, k2, k3 } => {
608                k1.bind(metadata)?;
609                k2.bind(metadata)?;
610                k3.bind(metadata)?;
611            }
612        }
613        Ok(())
614    }
615
616    /// Return the velocity vector that boosts lab-frame momenta into the diagram's
617    /// center-of-momentum frame.
618    pub fn com_boost_vector(&self, event: &EventData) -> Vec3 {
619        match self {
620            Topology::Full { k3, k4, .. }
621            | Topology::MissingK1 { k3, k4, .. }
622            | Topology::MissingK2 { k3, k4, .. } => {
623                -(k3.momentum(event) + k4.momentum(event)).beta()
624            }
625            Topology::MissingK3 { k1, k2, .. } | Topology::MissingK4 { k1, k2, .. } => {
626                -(k1.momentum(event) + k2.momentum(event)).beta()
627            }
628        }
629    }
630
631    /// Convenience helper returning the beam four-momentum (`k1`).
632    pub fn k1(&self, event: &EventData) -> Vec4 {
633        match self {
634            Topology::Full { k1, .. }
635            | Topology::MissingK2 { k1, .. }
636            | Topology::MissingK3 { k1, .. }
637            | Topology::MissingK4 { k1, .. } => k1.momentum(event),
638            Topology::MissingK1 { k2, k3, k4 } => {
639                k3.momentum(event) + k4.momentum(event) - k2.momentum(event)
640            }
641        }
642    }
643
644    /// Convenience helper returning the target four-momentum (`k2`).
645    pub fn k2(&self, event: &EventData) -> Vec4 {
646        match self {
647            Topology::Full { k2, .. }
648            | Topology::MissingK1 { k2, .. }
649            | Topology::MissingK3 { k2, .. }
650            | Topology::MissingK4 { k2, .. } => k2.momentum(event),
651            Topology::MissingK2 { k1, k3, k4 } => {
652                k3.momentum(event) + k4.momentum(event) - k1.momentum(event)
653            }
654        }
655    }
656
657    /// Convenience helper returning the resonance four-momentum (`k3`).
658    pub fn k3(&self, event: &EventData) -> Vec4 {
659        match self {
660            Topology::Full { k3, .. }
661            | Topology::MissingK1 { k3, .. }
662            | Topology::MissingK2 { k3, .. }
663            | Topology::MissingK4 { k3, .. } => k3.momentum(event),
664            Topology::MissingK3 { k1, k2, k4 } => {
665                k1.momentum(event) + k2.momentum(event) - k4.momentum(event)
666            }
667        }
668    }
669
670    /// Convenience helper returning the recoil four-momentum (`k4`).
671    pub fn k4(&self, event: &EventData) -> Vec4 {
672        match self {
673            Topology::Full { k4, .. }
674            | Topology::MissingK1 { k4, .. }
675            | Topology::MissingK2 { k4, .. }
676            | Topology::MissingK3 { k4, .. } => k4.momentum(event),
677            Topology::MissingK4 { k1, k2, k3 } => {
678                k1.momentum(event) + k2.momentum(event) - k3.momentum(event)
679            }
680        }
681    }
682
683    /// Beam four-momentum (`k1`) expressed in the center-of-momentum frame.
684    pub fn k1_com(&self, event: &EventData) -> Vec4 {
685        self.k1(event).boost(&self.com_boost_vector(event))
686    }
687
688    /// Target four-momentum (`k2`) expressed in the center-of-momentum frame.
689    pub fn k2_com(&self, event: &EventData) -> Vec4 {
690        self.k2(event).boost(&self.com_boost_vector(event))
691    }
692
693    /// Resonance four-momentum (`k3`) expressed in the center-of-momentum frame.
694    pub fn k3_com(&self, event: &EventData) -> Vec4 {
695        self.k3(event).boost(&self.com_boost_vector(event))
696    }
697
698    /// Recoil four-momentum (`k4`) expressed in the center-of-momentum frame.
699    pub fn k4_com(&self, event: &EventData) -> Vec4 {
700        self.k4(event).boost(&self.com_boost_vector(event))
701    }
702
703    /// Returns the resolved names for `k1` if it was explicitly provided.
704    pub fn k1_names(&self) -> Option<&[String]> {
705        match self {
706            Topology::Full { k1, .. }
707            | Topology::MissingK2 { k1, .. }
708            | Topology::MissingK3 { k1, .. }
709            | Topology::MissingK4 { k1, .. } => Some(k1.names()),
710            Topology::MissingK1 { .. } => None,
711        }
712    }
713
714    /// Returns the resolved names for `k2` if it was explicitly provided.
715    pub fn k2_names(&self) -> Option<&[String]> {
716        match self {
717            Topology::Full { k2, .. }
718            | Topology::MissingK1 { k2, .. }
719            | Topology::MissingK3 { k2, .. }
720            | Topology::MissingK4 { k2, .. } => Some(k2.names()),
721            Topology::MissingK2 { .. } => None,
722        }
723    }
724
725    /// Returns the resolved names for `k3` if it was explicitly provided.
726    pub fn k3_names(&self) -> Option<&[String]> {
727        match self {
728            Topology::Full { k3, .. }
729            | Topology::MissingK1 { k3, .. }
730            | Topology::MissingK2 { k3, .. }
731            | Topology::MissingK4 { k3, .. } => Some(k3.names()),
732            Topology::MissingK3 { .. } => None,
733        }
734    }
735
736    /// Returns the resolved names for `k4` if it was explicitly provided.
737    pub fn k4_names(&self) -> Option<&[String]> {
738        match self {
739            Topology::Full { k4, .. }
740            | Topology::MissingK1 { k4, .. }
741            | Topology::MissingK2 { k4, .. }
742            | Topology::MissingK3 { k4, .. } => Some(k4.names()),
743            Topology::MissingK4 { .. } => None,
744        }
745    }
746}
747
748impl Display for Topology {
749    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
750        write!(
751            f,
752            "Topology(k1=[{}], k2=[{}], k3=[{}], k4=[{}])",
753            format_topology_names(self.k1_names()),
754            format_topology_names(self.k2_names()),
755            format_topology_names(self.k3_names()),
756            format_topology_names(self.k4_names())
757        )
758    }
759}
760
761fn format_topology_names(names: Option<&[String]>) -> String {
762    match names {
763        Some(names) if !names.is_empty() => names_to_string(names),
764        Some(_) => String::new(),
765        None => "<reconstructed>".to_string(),
766    }
767}
768
769#[derive(Clone, Debug, Serialize, Deserialize)]
770struct AuxSelection {
771    name: String,
772    #[serde(skip, default)]
773    index: Option<usize>,
774}
775
776impl AuxSelection {
777    fn new<S: Into<String>>(name: S) -> Self {
778        Self {
779            name: name.into(),
780            index: None,
781        }
782    }
783
784    fn bind(&mut self, metadata: &DatasetMetadata) -> LadduResult<()> {
785        let idx = metadata
786            .aux_index(&self.name)
787            .ok_or_else(|| LadduError::UnknownName {
788                category: "aux",
789                name: self.name.clone(),
790            })?;
791        self.index = Some(idx);
792        Ok(())
793    }
794
795    fn index(&self) -> usize {
796        self.index.expect("AuxSelection must be bound before use")
797    }
798
799    fn name(&self) -> &str {
800        &self.name
801    }
802}
803
804/// A struct for obtaining the mass of a particle by indexing the four-momenta of an event, adding
805/// together multiple four-momenta if more than one entry is given.
806#[derive(Clone, Debug, Serialize, Deserialize)]
807pub struct Mass {
808    constituents: P4Selection,
809}
810impl Mass {
811    /// Create a new [`Mass`] from the sum of the four-momenta identified by `constituents` in the
812    /// [`EventData`]'s `p4s` field.
813    pub fn new<C>(constituents: C) -> Self
814    where
815        C: IntoP4Selection,
816    {
817        Self {
818            constituents: constituents.into_selection(),
819        }
820    }
821}
822impl Display for Mass {
823    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
824        write!(
825            f,
826            "Mass(constituents=[{}])",
827            names_to_string(self.constituents.names())
828        )
829    }
830}
831#[typetag::serde]
832impl Variable for Mass {
833    fn bind(&mut self, metadata: &DatasetMetadata) -> LadduResult<()> {
834        self.constituents.bind(metadata)
835    }
836
837    fn value(&self, event: &EventData) -> f64 {
838        self.constituents.momentum(event).m()
839    }
840}
841
842/// A struct for obtaining the $`\cos\theta`$ (cosine of the polar angle) of a decay product in
843/// a given reference frame of its parent resonance.
844#[derive(Clone, Debug, Serialize, Deserialize)]
845pub struct CosTheta {
846    topology: Topology,
847    daughter: P4Selection,
848    frame: Frame,
849}
850impl Display for CosTheta {
851    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
852        write!(
853            f,
854            "CosTheta(topology={}, daughter=[{}], frame={})",
855            self.topology,
856            names_to_string(self.daughter.names()),
857            self.frame
858        )
859    }
860}
861impl CosTheta {
862    /// Construct the angle given a [`Topology`] describing the production kinematics along with a
863    /// decay daughter of the `k3` resonance. See [`Frame`] for options regarding the reference
864    /// frame.
865    pub fn new<D>(topology: Topology, daughter: D, frame: Frame) -> Self
866    where
867        D: IntoP4Selection,
868    {
869        Self {
870            topology,
871            daughter: daughter.into_selection(),
872            frame,
873        }
874    }
875}
876
877#[typetag::serde]
878impl Variable for CosTheta {
879    fn bind(&mut self, metadata: &DatasetMetadata) -> LadduResult<()> {
880        self.topology.bind(metadata)?;
881        self.daughter.bind(metadata)?;
882        Ok(())
883    }
884
885    fn value(&self, event: &EventData) -> f64 {
886        let com_boost = self.topology.com_boost_vector(event);
887        let beam = self.topology.k1_com(event);
888        let resonance = self.topology.k3_com(event);
889        let daughter = self.daughter.momentum(event).boost(&com_boost);
890        let daughter_res = daughter.boost(&-resonance.beta());
891        let plane_normal = beam.vec3().cross(&resonance.vec3()).unit();
892        let z = match self.frame {
893            Frame::Helicity => {
894                let recoil = self.topology.k4_com(event);
895                let recoil_res = recoil.boost(&-resonance.beta());
896                (-recoil_res.vec3()).unit()
897            }
898            Frame::GottfriedJackson => {
899                let beam_res = beam.boost(&-resonance.beta());
900                beam_res.vec3().unit()
901            }
902        };
903        let x = plane_normal.cross(&z).unit();
904        let y = z.cross(&x).unit();
905        let angles = Vec3::new(
906            daughter_res.vec3().dot(&x),
907            daughter_res.vec3().dot(&y),
908            daughter_res.vec3().dot(&z),
909        );
910        angles.costheta()
911    }
912}
913
914/// A struct for obtaining the $`\phi`$ angle (azimuthal angle) of a decay product in a given
915/// reference frame of its parent resonance.
916#[derive(Clone, Debug, Serialize, Deserialize)]
917pub struct Phi {
918    topology: Topology,
919    daughter: P4Selection,
920    frame: Frame,
921}
922impl Display for Phi {
923    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
924        write!(
925            f,
926            "Phi(topology={}, daughter=[{}], frame={})",
927            self.topology,
928            names_to_string(self.daughter.names()),
929            self.frame
930        )
931    }
932}
933impl Phi {
934    /// Construct the angle given a [`Topology`] describing the production kinematics along with a
935    /// daughter of the resonance defined by `k3`. See [`Frame`] for options regarding the
936    /// reference frame.
937    pub fn new<D>(topology: Topology, daughter: D, frame: Frame) -> Self
938    where
939        D: IntoP4Selection,
940    {
941        Self {
942            topology,
943            daughter: daughter.into_selection(),
944            frame,
945        }
946    }
947}
948#[typetag::serde]
949impl Variable for Phi {
950    fn bind(&mut self, metadata: &DatasetMetadata) -> LadduResult<()> {
951        self.topology.bind(metadata)?;
952        self.daughter.bind(metadata)?;
953        Ok(())
954    }
955
956    fn value(&self, event: &EventData) -> f64 {
957        let com_boost = self.topology.com_boost_vector(event);
958        let beam = self.topology.k1_com(event);
959        let resonance = self.topology.k3_com(event);
960        let daughter = self.daughter.momentum(event).boost(&com_boost);
961        let daughter_res = daughter.boost(&-resonance.beta());
962        let plane_normal = beam.vec3().cross(&resonance.vec3()).unit();
963        let z = match self.frame {
964            Frame::Helicity => {
965                let recoil = self.topology.k4_com(event);
966                let recoil_res = recoil.boost(&-resonance.beta());
967                (-recoil_res.vec3()).unit()
968            }
969            Frame::GottfriedJackson => {
970                let beam_res = beam.boost(&-resonance.beta());
971                beam_res.vec3().unit()
972            }
973        };
974        let x = plane_normal.cross(&z).unit();
975        let y = z.cross(&x).unit();
976        let angles = Vec3::new(
977            daughter_res.vec3().dot(&x),
978            daughter_res.vec3().dot(&y),
979            daughter_res.vec3().dot(&z),
980        );
981        angles.phi()
982    }
983}
984
985/// A struct for obtaining both spherical angles at the same time.
986#[derive(Clone, Debug, Serialize, Deserialize)]
987pub struct Angles {
988    /// See [`CosTheta`].
989    pub costheta: CosTheta,
990    /// See [`Phi`].
991    pub phi: Phi,
992}
993
994impl Display for Angles {
995    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
996        write!(
997            f,
998            "Angles(topology={}, daughter=[{}], frame={})",
999            self.costheta.topology,
1000            names_to_string(self.costheta.daughter.names()),
1001            self.costheta.frame
1002        )
1003    }
1004}
1005impl Angles {
1006    /// Construct the angles given a [`Topology`] along with the daughter selection.
1007    /// See [`Frame`] for options regarding the reference frame.
1008    pub fn new<D>(topology: Topology, daughter: D, frame: Frame) -> Self
1009    where
1010        D: IntoP4Selection,
1011    {
1012        let daughter_vertex = daughter.into_selection();
1013        let costheta = CosTheta::new(topology.clone(), daughter_vertex.clone(), frame);
1014        let phi = Phi::new(topology, daughter_vertex, frame);
1015        Self { costheta, phi }
1016    }
1017}
1018
1019/// A struct defining the polarization angle for a beam relative to the production plane.
1020#[derive(Clone, Debug, Serialize, Deserialize)]
1021pub struct PolAngle {
1022    topology: Topology,
1023    angle_aux: AuxSelection,
1024}
1025impl Display for PolAngle {
1026    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1027        write!(
1028            f,
1029            "PolAngle(topology={}, angle_aux={})",
1030            self.topology,
1031            self.angle_aux.name(),
1032        )
1033    }
1034}
1035impl PolAngle {
1036    /// Constructs the polarization angle given a [`Topology`] describing the production plane and
1037    /// the auxiliary column storing the precomputed angle.
1038    pub fn new<A>(topology: Topology, angle_aux: A) -> Self
1039    where
1040        A: Into<String>,
1041    {
1042        Self {
1043            topology,
1044            angle_aux: AuxSelection::new(angle_aux.into()),
1045        }
1046    }
1047}
1048#[typetag::serde]
1049impl Variable for PolAngle {
1050    fn bind(&mut self, metadata: &DatasetMetadata) -> LadduResult<()> {
1051        self.topology.bind(metadata)?;
1052        self.angle_aux.bind(metadata)?;
1053        Ok(())
1054    }
1055
1056    fn value(&self, event: &EventData) -> f64 {
1057        let beam = self.topology.k1(event);
1058        let recoil = self.topology.k4(event);
1059        let pol_angle = event.aux[self.angle_aux.index()];
1060        let polarization = Vec3::new(pol_angle.cos(), pol_angle.sin(), 0.0);
1061        let y = beam.vec3().cross(&-recoil.vec3()).unit();
1062        let numerator = y.dot(&polarization);
1063        let denominator = beam.vec3().unit().dot(&polarization.cross(&y));
1064        f64::atan2(numerator, denominator)
1065    }
1066}
1067
1068/// A struct defining the polarization magnitude for a beam relative to the production plane.
1069#[derive(Clone, Debug, Serialize, Deserialize)]
1070pub struct PolMagnitude {
1071    magnitude_aux: AuxSelection,
1072}
1073impl Display for PolMagnitude {
1074    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1075        write!(
1076            f,
1077            "PolMagnitude(magnitude_aux={})",
1078            self.magnitude_aux.name(),
1079        )
1080    }
1081}
1082impl PolMagnitude {
1083    /// Constructs the polarization magnitude given the named auxiliary column containing the
1084    /// magnitude value.
1085    pub fn new<S: Into<String>>(magnitude_aux: S) -> Self {
1086        Self {
1087            magnitude_aux: AuxSelection::new(magnitude_aux.into()),
1088        }
1089    }
1090}
1091#[typetag::serde]
1092impl Variable for PolMagnitude {
1093    fn bind(&mut self, metadata: &DatasetMetadata) -> LadduResult<()> {
1094        self.magnitude_aux.bind(metadata)
1095    }
1096
1097    fn value(&self, event: &EventData) -> f64 {
1098        event.aux[self.magnitude_aux.index()]
1099    }
1100}
1101
1102/// A struct for obtaining both the polarization angle and magnitude at the same time.
1103#[derive(Clone, Debug, Serialize, Deserialize)]
1104pub struct Polarization {
1105    /// See [`PolMagnitude`].
1106    pub pol_magnitude: PolMagnitude,
1107    /// See [`PolAngle`].
1108    pub pol_angle: PolAngle,
1109}
1110impl Display for Polarization {
1111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1112        write!(
1113            f,
1114            "Polarization(topology={}, magnitude_aux={}, angle_aux={})",
1115            self.pol_angle.topology,
1116            self.pol_magnitude.magnitude_aux.name(),
1117            self.pol_angle.angle_aux.name(),
1118        )
1119    }
1120}
1121impl Polarization {
1122    /// Constructs the polarization angle and magnitude given a [`Topology`] and distinct
1123    /// auxiliary columns for magnitude and angle.
1124    ///
1125    /// # Panics
1126    ///
1127    /// Panics if `magnitude_aux` and `angle_aux` refer to the same auxiliary column name.
1128    pub fn new<M, A>(topology: Topology, magnitude_aux: M, angle_aux: A) -> Self
1129    where
1130        M: Into<String>,
1131        A: Into<String>,
1132    {
1133        let magnitude_aux = magnitude_aux.into();
1134        let angle_aux = angle_aux.into();
1135        assert!(
1136            magnitude_aux != angle_aux,
1137            "Polarization magnitude and angle must reference distinct auxiliary columns"
1138        );
1139        Self {
1140            pol_magnitude: PolMagnitude::new(magnitude_aux),
1141            pol_angle: PolAngle::new(topology, angle_aux),
1142        }
1143    }
1144}
1145
1146/// A struct used to calculate Mandelstam variables ($`s`$, $`t`$, or $`u`$).
1147///
1148/// By convention, the metric is chosen to be $`(+---)`$ and the variables are defined as follows
1149/// (ignoring factors of $`c`$):
1150///
1151/// $`s = (p_1 + p_2)^2 = (p_3 + p_4)^2`$
1152///
1153/// $`t = (p_1 - p_3)^2 = (p_4 - p_2)^2`$
1154///
1155/// $`u = (p_1 - p_4)^2 = (p_3 - p_2)^2`$
1156#[derive(Clone, Debug, Serialize, Deserialize)]
1157pub struct Mandelstam {
1158    topology: Topology,
1159    channel: Channel,
1160}
1161impl Display for Mandelstam {
1162    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1163        write!(
1164            f,
1165            "Mandelstam(topology={}, channel={})",
1166            self.topology, self.channel,
1167        )
1168    }
1169}
1170impl Mandelstam {
1171    /// Constructs the Mandelstam variable for the given `channel` using the supplied [`Topology`].
1172    pub fn new(topology: Topology, channel: Channel) -> Self {
1173        Self { topology, channel }
1174    }
1175}
1176
1177#[typetag::serde]
1178impl Variable for Mandelstam {
1179    fn bind(&mut self, metadata: &DatasetMetadata) -> LadduResult<()> {
1180        self.topology.bind(metadata)
1181    }
1182
1183    fn value(&self, event: &EventData) -> f64 {
1184        match self.channel {
1185            Channel::S => {
1186                let k1 = self.topology.k1(event);
1187                let k2 = self.topology.k2(event);
1188                (k1 + k2).mag2()
1189            }
1190            Channel::T => {
1191                let k1 = self.topology.k1(event);
1192                let k3 = self.topology.k3(event);
1193                (k1 - k3).mag2()
1194            }
1195            Channel::U => {
1196                let k1 = self.topology.k1(event);
1197                let k4 = self.topology.k4(event);
1198                (k1 - k4).mag2()
1199            }
1200        }
1201    }
1202}
1203
1204#[cfg(test)]
1205mod tests {
1206    use super::*;
1207    use crate::data::{test_dataset, test_event};
1208    use approx::assert_relative_eq;
1209
1210    fn topology_test_metadata() -> DatasetMetadata {
1211        DatasetMetadata::new(
1212            vec!["beam", "target", "resonance", "recoil"],
1213            Vec::<String>::new(),
1214        )
1215        .expect("topology metadata should be valid")
1216    }
1217
1218    fn topology_test_event() -> EventData {
1219        let p1 = Vec4::new(0.0, 0.0, 3.0, 3.5);
1220        let p2 = Vec4::new(0.0, 0.0, -3.0, 3.5);
1221        let p3 = Vec4::new(0.5, -0.25, 1.0, 1.9);
1222        let p4 = p1 + p2 - p3;
1223        EventData {
1224            p4s: vec![p1, p2, p3, p4],
1225            aux: vec![],
1226            weight: 1.0,
1227        }
1228    }
1229
1230    fn reaction_topology() -> Topology {
1231        Topology::missing_k2("beam", ["kshort1", "kshort2"], "proton")
1232    }
1233
1234    #[test]
1235    #[allow(clippy::needless_borrows_for_generic_args)]
1236    fn test_topology_accepts_varied_inputs() {
1237        let topo = Topology::new(
1238            "particle1",
1239            ["particle2a", "particle2b"],
1240            &["particle3"],
1241            "particle4".to_string(),
1242        );
1243        assert_eq!(
1244            topo.k1_names()
1245                .unwrap()
1246                .iter()
1247                .map(String::as_str)
1248                .collect::<Vec<_>>(),
1249            vec!["particle1"]
1250        );
1251        assert_eq!(
1252            topo.k2_names()
1253                .unwrap()
1254                .iter()
1255                .map(String::as_str)
1256                .collect::<Vec<_>>(),
1257            vec!["particle2a", "particle2b"]
1258        );
1259        let missing = Topology::missing_k2("particle1", vec!["particle3"], "particle4");
1260        assert!(missing.k2_names().is_none());
1261        assert!(missing.to_string().contains("<reconstructed>"));
1262    }
1263
1264    #[test]
1265    fn test_topology_reconstructs_missing_vertices() {
1266        let metadata = topology_test_metadata();
1267        let event = topology_test_event();
1268
1269        let mut full = Topology::new("beam", "target", "resonance", "recoil");
1270        full.bind(&metadata).unwrap();
1271        assert_relative_eq!(full.k3(&event), event.p4s[2], epsilon = 1e-12);
1272
1273        let mut missing_k1 = Topology::missing_k1("target", "resonance", "recoil");
1274        missing_k1.bind(&metadata).unwrap();
1275        assert!(missing_k1.k1_names().is_none());
1276        assert_relative_eq!(missing_k1.k1(&event), event.p4s[0], epsilon = 1e-12);
1277
1278        let mut missing_k2 = Topology::missing_k2("beam", "resonance", "recoil");
1279        missing_k2.bind(&metadata).unwrap();
1280        assert!(missing_k2.k2_names().is_none());
1281        assert_relative_eq!(missing_k2.k2(&event), event.p4s[1], epsilon = 1e-12);
1282
1283        let mut missing_k3 = Topology::missing_k3("beam", "target", "recoil");
1284        missing_k3.bind(&metadata).unwrap();
1285        assert!(missing_k3.k3_names().is_none());
1286        assert_relative_eq!(missing_k3.k3(&event), event.p4s[2], epsilon = 1e-12);
1287
1288        let mut missing_k4 = Topology::missing_k4("beam", "target", "resonance");
1289        missing_k4.bind(&metadata).unwrap();
1290        assert!(missing_k4.k4_names().is_none());
1291        assert_relative_eq!(missing_k4.k4(&event), event.p4s[3], epsilon = 1e-12);
1292    }
1293
1294    #[test]
1295    fn test_topology_com_helpers_match_manual_boost() {
1296        let metadata = topology_test_metadata();
1297        let event = topology_test_event();
1298        let mut topo = Topology::new("beam", "target", "resonance", "recoil");
1299        topo.bind(&metadata).unwrap();
1300        let beta = topo.com_boost_vector(&event);
1301        assert_relative_eq!(topo.k1_com(&event), topo.k1(&event).boost(&beta));
1302        assert_relative_eq!(topo.k2_com(&event), topo.k2(&event).boost(&beta));
1303        assert_relative_eq!(topo.k3_com(&event), topo.k3(&event).boost(&beta));
1304        assert_relative_eq!(topo.k4_com(&event), topo.k4(&event).boost(&beta));
1305    }
1306
1307    #[test]
1308    fn test_mass_single_particle() {
1309        let dataset = test_dataset();
1310        let mut mass = Mass::new("proton");
1311        mass.bind(dataset.metadata()).unwrap();
1312        assert_relative_eq!(mass.value(&dataset[0]), 1.007);
1313    }
1314
1315    #[test]
1316    fn test_mass_multiple_particles() {
1317        let dataset = test_dataset();
1318        let mut mass = Mass::new(["kshort1", "kshort2"]);
1319        mass.bind(dataset.metadata()).unwrap();
1320        assert_relative_eq!(mass.value(&dataset[0]), 1.3743786309153077);
1321    }
1322
1323    #[test]
1324    fn test_mass_display() {
1325        let mass = Mass::new(["kshort1", "kshort2"]);
1326        assert_eq!(mass.to_string(), "Mass(constituents=[kshort1, kshort2])");
1327    }
1328
1329    #[test]
1330    fn test_costheta_helicity() {
1331        let dataset = test_dataset();
1332        let mut costheta = CosTheta::new(reaction_topology(), "kshort1", Frame::Helicity);
1333        costheta.bind(dataset.metadata()).unwrap();
1334        assert_relative_eq!(
1335            costheta.value(&dataset[0]),
1336            -0.4611175068834238,
1337            epsilon = 1e-12
1338        );
1339    }
1340
1341    #[test]
1342    fn test_costheta_display() {
1343        let costheta = CosTheta::new(reaction_topology(), "kshort1", Frame::Helicity);
1344        assert_eq!(
1345            costheta.to_string(),
1346            "CosTheta(topology=Topology(k1=[beam], k2=[<reconstructed>], k3=[kshort1, kshort2], k4=[proton]), daughter=[kshort1], frame=Helicity)"
1347        );
1348    }
1349
1350    #[test]
1351    fn test_phi_helicity() {
1352        let dataset = test_dataset();
1353        let mut phi = Phi::new(reaction_topology(), "kshort1", Frame::Helicity);
1354        phi.bind(dataset.metadata()).unwrap();
1355        assert_relative_eq!(phi.value(&dataset[0]), -2.657462587335066, epsilon = 1e-12);
1356    }
1357
1358    #[test]
1359    fn test_phi_display() {
1360        let phi = Phi::new(reaction_topology(), "kshort1", Frame::Helicity);
1361        assert_eq!(
1362            phi.to_string(),
1363            "Phi(topology=Topology(k1=[beam], k2=[<reconstructed>], k3=[kshort1, kshort2], k4=[proton]), daughter=[kshort1], frame=Helicity)"
1364        );
1365    }
1366
1367    #[test]
1368    fn test_costheta_gottfried_jackson() {
1369        let dataset = test_dataset();
1370        let mut costheta = CosTheta::new(reaction_topology(), "kshort1", Frame::GottfriedJackson);
1371        costheta.bind(dataset.metadata()).unwrap();
1372        assert_relative_eq!(
1373            costheta.value(&dataset[0]),
1374            0.09198832278031577,
1375            epsilon = 1e-12
1376        );
1377    }
1378
1379    #[test]
1380    fn test_phi_gottfried_jackson() {
1381        let dataset = test_dataset();
1382        let mut phi = Phi::new(reaction_topology(), "kshort1", Frame::GottfriedJackson);
1383        phi.bind(dataset.metadata()).unwrap();
1384        assert_relative_eq!(phi.value(&dataset[0]), -2.713913199133907, epsilon = 1e-12);
1385    }
1386
1387    #[test]
1388    fn test_angles() {
1389        let dataset = test_dataset();
1390        let mut angles = Angles::new(reaction_topology(), "kshort1", Frame::Helicity);
1391        angles.costheta.bind(dataset.metadata()).unwrap();
1392        angles.phi.bind(dataset.metadata()).unwrap();
1393        assert_relative_eq!(
1394            angles.costheta.value(&dataset[0]),
1395            -0.4611175068834238,
1396            epsilon = 1e-12
1397        );
1398        assert_relative_eq!(
1399            angles.phi.value(&dataset[0]),
1400            -2.657462587335066,
1401            epsilon = 1e-12
1402        );
1403    }
1404
1405    #[test]
1406    fn test_angles_display() {
1407        let angles = Angles::new(reaction_topology(), "kshort1", Frame::Helicity);
1408        assert_eq!(
1409            angles.to_string(),
1410            "Angles(topology=Topology(k1=[beam], k2=[<reconstructed>], k3=[kshort1, kshort2], k4=[proton]), daughter=[kshort1], frame=Helicity)"
1411        );
1412    }
1413
1414    #[test]
1415    fn test_pol_angle() {
1416        let dataset = test_dataset();
1417        let mut pol_angle = PolAngle::new(reaction_topology(), "pol_angle");
1418        pol_angle.bind(dataset.metadata()).unwrap();
1419        assert_relative_eq!(pol_angle.value(&dataset[0]), 1.935929887818673);
1420    }
1421
1422    #[test]
1423    fn test_pol_angle_display() {
1424        let pol_angle = PolAngle::new(reaction_topology(), "pol_angle");
1425        assert_eq!(
1426            pol_angle.to_string(),
1427            "PolAngle(topology=Topology(k1=[beam], k2=[<reconstructed>], k3=[kshort1, kshort2], k4=[proton]), angle_aux=pol_angle)"
1428        );
1429    }
1430
1431    #[test]
1432    fn test_pol_magnitude() {
1433        let dataset = test_dataset();
1434        let mut pol_magnitude = PolMagnitude::new("pol_magnitude");
1435        pol_magnitude.bind(dataset.metadata()).unwrap();
1436        assert_relative_eq!(pol_magnitude.value(&dataset[0]), 0.38562805);
1437    }
1438
1439    #[test]
1440    fn test_pol_magnitude_display() {
1441        let pol_magnitude = PolMagnitude::new("pol_magnitude");
1442        assert_eq!(
1443            pol_magnitude.to_string(),
1444            "PolMagnitude(magnitude_aux=pol_magnitude)"
1445        );
1446    }
1447
1448    #[test]
1449    fn test_polarization() {
1450        let dataset = test_dataset();
1451        let mut polarization = Polarization::new(reaction_topology(), "pol_magnitude", "pol_angle");
1452        polarization.pol_angle.bind(dataset.metadata()).unwrap();
1453        polarization.pol_magnitude.bind(dataset.metadata()).unwrap();
1454        assert_relative_eq!(polarization.pol_angle.value(&dataset[0]), 1.935929887818673);
1455        assert_relative_eq!(polarization.pol_magnitude.value(&dataset[0]), 0.38562805);
1456    }
1457
1458    #[test]
1459    fn test_polarization_display() {
1460        let polarization = Polarization::new(reaction_topology(), "pol_magnitude", "pol_angle");
1461        assert_eq!(
1462            polarization.to_string(),
1463            "Polarization(topology=Topology(k1=[beam], k2=[<reconstructed>], k3=[kshort1, kshort2], k4=[proton]), magnitude_aux=pol_magnitude, angle_aux=pol_angle)"
1464        );
1465    }
1466
1467    #[test]
1468    fn test_mandelstam() {
1469        let dataset = test_dataset();
1470        let metadata = dataset.metadata();
1471        let mut s = Mandelstam::new(reaction_topology(), Channel::S);
1472        let mut t = Mandelstam::new(reaction_topology(), Channel::T);
1473        let mut u = Mandelstam::new(reaction_topology(), Channel::U);
1474        for variable in [&mut s, &mut t, &mut u] {
1475            variable.bind(metadata).unwrap();
1476        }
1477        let event = &dataset[0];
1478        assert_relative_eq!(s.value(event), 18.504011052120063);
1479        assert_relative_eq!(t.value(event), -0.19222859969898076);
1480        assert_relative_eq!(u.value(event), -14.404198931464428);
1481        let mut direct_topology = reaction_topology();
1482        direct_topology.bind(metadata).unwrap();
1483        let k2 = direct_topology.k2(event);
1484        let k3 = direct_topology.k3(event);
1485        let k4 = direct_topology.k4(event);
1486        assert_relative_eq!(s.value(event), (k3 + k4).mag2());
1487        assert_relative_eq!(t.value(event), (k2 - k4).mag2());
1488        assert_relative_eq!(u.value(event), (k3 - k2).mag2());
1489        let m2_beam = test_event().get_p4_sum([0]).m2();
1490        let m2_recoil = test_event().get_p4_sum([1]).m2();
1491        let m2_res = test_event().get_p4_sum([2, 3]).m2();
1492        assert_relative_eq!(
1493            s.value(event) + t.value(event) + u.value(event) - m2_beam - m2_recoil - m2_res,
1494            1.00,
1495            epsilon = 1e-2
1496        );
1497        // Note: not very accurate, but considering the values in test_event only go to about 3
1498        // decimal places, this is probably okay
1499    }
1500
1501    #[test]
1502    fn test_mandelstam_display() {
1503        let s = Mandelstam::new(reaction_topology(), Channel::S);
1504        assert_eq!(
1505            s.to_string(),
1506            "Mandelstam(topology=Topology(k1=[beam], k2=[<reconstructed>], k3=[kshort1, kshort2], k4=[proton]), channel=s)"
1507        );
1508    }
1509
1510    #[test]
1511    fn test_variable_value_on() {
1512        let dataset = test_dataset();
1513        let mass = Mass::new(["kshort1", "kshort2"]);
1514
1515        let values = mass.value_on(&dataset).unwrap();
1516        assert_eq!(values.len(), 1);
1517        assert_relative_eq!(values[0], 1.3743786309153077);
1518    }
1519}