1use serde::{Deserialize, Serialize};
2
3use super::{
4 decay::Decay,
5 graph::ParticleGraph,
6 particle::{resolve_particle_direct, Particle},
7 production::Production,
8 two_to_two::{ResolvedTwoToTwo, TwoToTwoReaction},
9};
10use crate::{
11 data::EventLike,
12 kinematics::{DecayAngles, FrameAxes, RestFrame},
13 quantum::{Channel, Frame},
14 variables::{Mandelstam, Mass, PolAngle, Polarization},
15 vectors::Vec4,
16 LadduError, LadduResult,
17};
18
19#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
21pub enum ReactionTopology {
22 TwoToTwo(TwoToTwoReaction),
24}
25
26#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
28pub struct Reaction {
29 graph: ParticleGraph,
30 topology: ReactionTopology,
31}
32
33impl Reaction {
34 pub fn new(graph: ParticleGraph, topology: ReactionTopology) -> LadduResult<Self> {
36 topology.validate(&graph)?;
37 Ok(Self { graph, topology })
38 }
39
40 pub fn two_to_two(
42 p1: &Particle,
43 p2: &Particle,
44 p3: &Particle,
45 p4: &Particle,
46 ) -> LadduResult<Self> {
47 Self::new(
48 ParticleGraph::new([p1.clone(), p2.clone(), p3.clone(), p4.clone()])?,
49 ReactionTopology::TwoToTwo(TwoToTwoReaction::new(p1, p2, p3, p4)?),
50 )
51 }
52
53 pub const fn graph(&self) -> &ParticleGraph {
55 &self.graph
56 }
57
58 pub const fn topology(&self) -> &ReactionTopology {
60 &self.topology
61 }
62
63 pub fn two_to_two_topology(&self) -> LadduResult<&TwoToTwoReaction> {
65 match &self.topology {
66 ReactionTopology::TwoToTwo(topology) => Ok(topology),
67 }
68 }
69
70 pub fn role(&self, role: &str) -> LadduResult<&Particle> {
72 match &self.topology {
73 ReactionTopology::TwoToTwo(topology) => self.graph.particle(topology.role(role)?),
74 }
75 }
76
77 pub fn p4(&self, event: &dyn EventLike, particle: &str) -> LadduResult<Vec4> {
79 let particle = self.particle(particle)?;
80 match &self.topology {
81 ReactionTopology::TwoToTwo(topology) => {
82 if topology.missing_particle() == Some(particle.label()) {
83 if let Some(index) = topology.missing_index() {
84 return Ok(match index {
85 0 => topology.resolve(&self.graph, event)?.p1(),
86 1 => topology.resolve(&self.graph, event)?.p2(),
87 2 => topology.resolve(&self.graph, event)?.p3(),
88 3 => topology.resolve(&self.graph, event)?.p4(),
89 _ => unreachable!("validated two-to-two slot index"),
90 });
91 }
92 }
93 resolve_particle_direct(event, particle)?.ok_or_else(|| {
94 LadduError::Custom(format!(
95 "missing particle '{}' is not a missing role in this reaction",
96 particle.label()
97 ))
98 })
99 }
100 }
101 }
102
103 pub fn resolve_two_to_two(&self, event: &dyn EventLike) -> LadduResult<ResolvedTwoToTwo> {
105 match &self.topology {
106 ReactionTopology::TwoToTwo(topology) => topology.resolve(&self.graph, event),
107 }
108 }
109
110 pub fn mass(&self, particle: &str) -> Mass {
112 Mass::from_reaction(self.clone(), particle)
113 }
114
115 pub fn decay(&self, parent: &str) -> LadduResult<Decay> {
117 let parent_particle = self.particle(parent)?;
118 self.root_particle_for(parent)?;
119 let daughters = parent_particle.daughters();
120 if daughters.len() != 2 {
121 return Err(LadduError::Custom(
122 "isobar decays must contain exactly two ordered daughters".to_string(),
123 ));
124 }
125 Ok(Decay {
126 reaction: self.clone(),
127 parent: parent.to_string(),
128 daughter_1: daughters[0].label().to_string(),
129 daughter_2: daughters[1].label().to_string(),
130 })
131 }
132
133 pub fn production(&self) -> LadduResult<Production> {
135 let topology = self.two_to_two_topology()?;
136 Ok(Production {
137 reaction: self.clone(),
138 produced: topology.p3().to_string(),
139 recoil: topology.p4().to_string(),
140 })
141 }
142
143 pub fn mandelstam(&self, channel: Channel) -> LadduResult<Mandelstam> {
145 self.topology.require_mandelstam()?;
146 Ok(Mandelstam::new(self.clone(), channel))
147 }
148
149 pub fn pol_angle<A>(&self, angle_aux: A) -> PolAngle
151 where
152 A: Into<String>,
153 {
154 PolAngle::new(self.clone(), angle_aux)
155 }
156
157 pub fn polarization<M, A>(&self, magnitude_aux: M, angle_aux: A) -> Polarization
159 where
160 M: Into<String>,
161 A: Into<String>,
162 {
163 Polarization::new(self.clone(), magnitude_aux, angle_aux)
164 }
165
166 pub fn axes(
168 &self,
169 event: &dyn EventLike,
170 particle: &str,
171 frame: Frame,
172 ) -> LadduResult<FrameAxes> {
173 let (root_index, root) = self.root_particle_for(particle)?;
174 let particle_ref = self.particle(particle)?;
175 if particle_ref == root {
176 let resolved = self.resolve_two_to_two(event)?;
177 let (parent, spectator) = match root_index {
178 2 => (resolved.p3, resolved.p4),
179 3 => (resolved.p4, resolved.p3),
180 _ => {
181 return Err(LadduError::Custom(
182 "only outgoing p3 and p4 decay roots have frame axes".to_string(),
183 ));
184 }
185 };
186 return FrameAxes::from_production_frame(
187 frame,
188 resolved.p1,
189 parent,
190 spectator,
191 resolved.com_boost(),
192 );
193 }
194 let parent = self.parent_of(particle).ok_or_else(|| {
195 LadduError::Custom(format!(
196 "particle '{}' is not connected to the reaction root",
197 particle
198 ))
199 })?;
200 let parent_axes = self.axes(event, &parent, frame)?;
201 parent_axes.for_daughter(self.p4_in_rest_frame_of(event, &parent, particle)?.vec3())
202 }
203
204 pub fn angles_value(
206 &self,
207 event: &dyn EventLike,
208 parent: &str,
209 daughter: &str,
210 frame: Frame,
211 ) -> LadduResult<DecayAngles> {
212 let parent_particle = self.particle(parent)?;
213 if !parent_particle
214 .daughters()
215 .iter()
216 .any(|candidate| candidate.label() == daughter)
217 {
218 return Err(LadduError::Custom(
219 "daughter is not an immediate child of parent".to_string(),
220 ));
221 }
222 let axes = self.axes(event, parent, frame)?;
223 axes.angles(self.p4_in_rest_frame_of(event, parent, daughter)?.vec3())
224 }
225
226 pub fn production_angles_value(
228 &self,
229 event: &dyn EventLike,
230 produced: &str,
231 frame: Frame,
232 ) -> LadduResult<DecayAngles> {
233 let topology = self.two_to_two_topology()?;
234 if produced != topology.p3() {
235 return Err(LadduError::Custom(format!(
236 "particle '{produced}' is not the produced p3 system"
237 )));
238 }
239 let resolved = self.resolve_two_to_two(event)?;
240 let com_boost = resolved.com_boost();
241 let reference = resolved.p1.boost(&com_boost);
242 let produced_p4 = resolved.p3.boost(&com_boost);
243 let produced_vec = produced_p4.vec3();
244 let plane_normal = reference.vec3().cross(&produced_vec);
245 let z = match frame {
246 Frame::Helicity => produced_vec,
247 Frame::GottfriedJackson => reference.vec3(),
248 Frame::Adair => {
249 return Err(LadduError::Custom(
250 "Adair frame construction is not implemented yet".to_string(),
251 ));
252 }
253 };
254 FrameAxes::from_z_and_plane_normal(z, plane_normal)?.angles(produced_vec)
255 }
256
257 pub fn particle(&self, particle: &str) -> LadduResult<&Particle> {
259 self.graph.particle(particle)
260 }
261
262 pub fn contains(&self, particle: &str) -> bool {
264 self.graph.contains(particle)
265 }
266
267 pub fn particles(&self) -> Vec<&Particle> {
269 self.graph.particles()
270 }
271
272 fn root_particle_for(&self, particle: &str) -> LadduResult<(usize, &Particle)> {
273 match &self.topology {
274 ReactionTopology::TwoToTwo(topology) => {
275 let p3 = self.graph.particle(topology.p3())?;
276 let p4 = self.graph.particle(topology.p4())?;
277 for (index, root) in [(2, p3), (3, p4)] {
278 if root.contains_id(particle) {
279 return Ok((index, root));
280 }
281 }
282 }
283 }
284 Err(LadduError::Custom(
285 "particle is not contained in an outgoing reaction root".to_string(),
286 ))
287 }
288
289 fn parent_of(&self, child: &str) -> Option<String> {
290 match &self.topology {
291 ReactionTopology::TwoToTwo(topology) => {
292 let p3 = self.graph.particle(topology.p3()).ok()?;
293 let p4 = self.graph.particle(topology.p4()).ok()?;
294 [p3, p4].into_iter().find_map(|root| {
295 root.parent_of_id(child)
296 .map(|particle| particle.label().to_string())
297 })
298 }
299 }
300 }
301
302 fn p4_in_rest_frame_of(
303 &self,
304 event: &dyn EventLike,
305 frame_particle: &str,
306 target: &str,
307 ) -> LadduResult<Vec4> {
308 let (_, root) = self.root_particle_for(frame_particle)?;
309 if frame_particle == root.label() {
310 let resolved = self.resolve_two_to_two(event)?;
311 let com_boost = resolved.com_boost();
312 let root_rest = RestFrame::new(self.p4(event, root.label())?.boost(&com_boost))?;
313 return Ok(root_rest.transform(self.p4(event, target)?.boost(&com_boost)));
314 }
315 let parent = self.parent_of(frame_particle).ok_or_else(|| {
316 LadduError::Custom("frame particle is not connected to root".to_string())
317 })?;
318 let frame_particle_in_parent = self.p4_in_rest_frame_of(event, &parent, frame_particle)?;
319 let target_in_parent = self.p4_in_rest_frame_of(event, &parent, target)?;
320 Ok(RestFrame::new(frame_particle_in_parent)?.transform(target_in_parent))
321 }
322}
323
324impl ReactionTopology {
325 pub const fn supports_mandelstam(&self) -> bool {
327 matches!(self, ReactionTopology::TwoToTwo(_))
328 }
329
330 pub fn require_mandelstam(&self) -> LadduResult<()> {
332 if self.supports_mandelstam() {
333 Ok(())
334 } else {
335 Err(LadduError::Custom(
336 "Mandelstam variables require a supported production topology".to_string(),
337 ))
338 }
339 }
340
341 pub fn validate(&self, graph: &ParticleGraph) -> LadduResult<()> {
343 match self {
344 ReactionTopology::TwoToTwo(topology) => {
345 for role in ["p1", "p2", "p3", "p4"] {
346 graph.particle(topology.role(role)?)?;
347 }
348 Ok(())
349 }
350 }
351 }
352}