Skip to main content

laddu_core/reaction/
two_to_two.rs

1use serde::{Deserialize, Serialize};
2
3use super::{graph::ParticleGraph, particle::resolve_particle_direct, Particle};
4use crate::{
5    data::EventLike,
6    vectors::{Vec3, Vec4},
7    LadduError, LadduResult,
8};
9
10/// A direct two-to-two reaction preserving `p1 + p2 -> p3 + p4` semantics.
11#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
12pub struct TwoToTwoReaction {
13    p1: String,
14    p2: String,
15    p3: String,
16    p4: String,
17    missing_index: Option<usize>,
18}
19
20impl TwoToTwoReaction {
21    /// Construct a two-to-two reaction.
22    pub fn new(p1: &Particle, p2: &Particle, p3: &Particle, p4: &Particle) -> LadduResult<Self> {
23        let particles = [p1, p2, p3, p4];
24        let missing = particles
25            .iter()
26            .enumerate()
27            .filter_map(|(index, particle)| particle.is_missing().then_some(index))
28            .collect::<Vec<_>>();
29        if missing.len() > 1 {
30            return Err(LadduError::Custom(
31                "two-to-two reaction can contain at most one missing particle".to_string(),
32            ));
33        }
34        Ok(Self {
35            p1: p1.label().to_string(),
36            p2: p2.label().to_string(),
37            p3: p3.label().to_string(),
38            p4: p4.label().to_string(),
39            missing_index: missing.first().copied(),
40        })
41    }
42
43    /// Return the `p1` particle identifier.
44    pub fn p1(&self) -> &str {
45        &self.p1
46    }
47
48    /// Return the `p2` particle identifier.
49    pub fn p2(&self) -> &str {
50        &self.p2
51    }
52
53    /// Return the `p3` particle identifier.
54    pub fn p3(&self) -> &str {
55        &self.p3
56    }
57
58    /// Return the `p4` particle identifier.
59    pub fn p4(&self) -> &str {
60        &self.p4
61    }
62
63    /// Return the zero-based missing particle index, if any.
64    pub const fn missing_index(&self) -> Option<usize> {
65        self.missing_index
66    }
67
68    /// Resolve all four reaction momenta for one event.
69    pub fn resolve(
70        &self,
71        graph: &ParticleGraph,
72        event: &dyn EventLike,
73    ) -> LadduResult<ResolvedTwoToTwo> {
74        let mut momenta = [
75            resolve_particle_direct(event, graph.particle(&self.p1)?)?,
76            resolve_particle_direct(event, graph.particle(&self.p2)?)?,
77            resolve_particle_direct(event, graph.particle(&self.p3)?)?,
78            resolve_particle_direct(event, graph.particle(&self.p4)?)?,
79        ];
80        if let Some(index) = self.missing_index {
81            let missing = match index {
82                0 => momenta[2].unwrap() + momenta[3].unwrap() - momenta[1].unwrap(),
83                1 => momenta[2].unwrap() + momenta[3].unwrap() - momenta[0].unwrap(),
84                2 => momenta[0].unwrap() + momenta[1].unwrap() - momenta[3].unwrap(),
85                3 => momenta[0].unwrap() + momenta[1].unwrap() - momenta[2].unwrap(),
86                _ => unreachable!("validated two-to-two slot index"),
87            };
88            momenta[index] = Some(missing);
89        }
90        Ok(ResolvedTwoToTwo {
91            p1: momenta[0].unwrap(),
92            p2: momenta[1].unwrap(),
93            p3: momenta[2].unwrap(),
94            p4: momenta[3].unwrap(),
95        })
96    }
97
98    pub(super) fn missing_particle(&self) -> Option<&str> {
99        self.missing_index.map(|index| self.slot_unchecked(index))
100    }
101
102    fn slot_unchecked(&self, index: usize) -> &str {
103        match index {
104            0 => &self.p1,
105            1 => &self.p2,
106            2 => &self.p3,
107            3 => &self.p4,
108            _ => unreachable!("validated two-to-two slot index"),
109        }
110    }
111
112    /// Return the particle identifier assigned to a named role.
113    pub fn role(&self, role: &str) -> LadduResult<&str> {
114        match role {
115            "p1" => Ok(&self.p1),
116            "p2" => Ok(&self.p2),
117            "p3" => Ok(&self.p3),
118            "p4" => Ok(&self.p4),
119            _ => Err(LadduError::Custom(format!(
120                "unknown two-to-two reaction role '{role}'"
121            ))),
122        }
123    }
124}
125
126/// Resolved event-level momenta for a two-to-two reaction.
127#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
128pub struct ResolvedTwoToTwo {
129    pub(super) p1: Vec4,
130    pub(super) p2: Vec4,
131    pub(super) p3: Vec4,
132    pub(super) p4: Vec4,
133}
134
135impl ResolvedTwoToTwo {
136    /// Return `p1`.
137    pub const fn p1(self) -> Vec4 {
138        self.p1
139    }
140
141    /// Return `p2`.
142    pub const fn p2(self) -> Vec4 {
143        self.p2
144    }
145
146    /// Return `p3`.
147    pub const fn p3(self) -> Vec4 {
148        self.p3
149    }
150
151    /// Return `p4`.
152    pub const fn p4(self) -> Vec4 {
153        self.p4
154    }
155
156    /// Return the production center-of-momentum boost.
157    pub fn com_boost(self) -> Vec3 {
158        -(self.p1 + self.p2).beta()
159    }
160
161    /// Return the Mandelstam `s` invariant.
162    pub fn s(self) -> f64 {
163        (self.p1 + self.p2).m2()
164    }
165
166    /// Return the Mandelstam `t` invariant.
167    pub fn t(self) -> f64 {
168        (self.p1 - self.p3).m2()
169    }
170
171    /// Return the Mandelstam `u` invariant.
172    pub fn u(self) -> f64 {
173        (self.p1 - self.p4).m2()
174    }
175}