use serde::{Deserialize, Serialize};
use super::{
decay::Decay,
graph::ParticleGraph,
particle::{resolve_particle_direct, Particle},
two_to_two::{ResolvedTwoToTwo, TwoToTwoReaction},
};
use crate::{
data::EventLike,
kinematics::{DecayAngles, FrameAxes, RestFrame},
quantum::{Channel, Frame},
variables::{Mandelstam, Mass, PolAngle, Polarization},
vectors::Vec4,
LadduError, LadduResult,
};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum ReactionTopology {
TwoToTwo(TwoToTwoReaction),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Reaction {
graph: ParticleGraph,
topology: ReactionTopology,
}
impl Reaction {
pub fn new(graph: ParticleGraph, topology: ReactionTopology) -> LadduResult<Self> {
topology.validate(&graph)?;
Ok(Self { graph, topology })
}
pub fn two_to_two(
p1: &Particle,
p2: &Particle,
p3: &Particle,
p4: &Particle,
) -> LadduResult<Self> {
Self::new(
ParticleGraph::new([p1.clone(), p2.clone(), p3.clone(), p4.clone()])?,
ReactionTopology::TwoToTwo(TwoToTwoReaction::new(p1, p2, p3, p4)?),
)
}
pub const fn graph(&self) -> &ParticleGraph {
&self.graph
}
pub const fn topology(&self) -> &ReactionTopology {
&self.topology
}
pub fn two_to_two_topology(&self) -> LadduResult<&TwoToTwoReaction> {
match &self.topology {
ReactionTopology::TwoToTwo(topology) => Ok(topology),
}
}
pub fn role(&self, role: &str) -> LadduResult<&Particle> {
match &self.topology {
ReactionTopology::TwoToTwo(topology) => self.graph.particle(topology.role(role)?),
}
}
pub fn p4(&self, event: &dyn EventLike, particle: &str) -> LadduResult<Vec4> {
let particle = self.particle(particle)?;
match &self.topology {
ReactionTopology::TwoToTwo(topology) => {
if topology.missing_particle() == Some(particle.label()) {
if let Some(index) = topology.missing_index() {
return Ok(match index {
0 => topology.resolve(&self.graph, event)?.p1(),
1 => topology.resolve(&self.graph, event)?.p2(),
2 => topology.resolve(&self.graph, event)?.p3(),
3 => topology.resolve(&self.graph, event)?.p4(),
_ => unreachable!("validated two-to-two slot index"),
});
}
}
resolve_particle_direct(event, particle)?.ok_or_else(|| {
LadduError::Custom(format!(
"missing particle '{}' is not a missing role in this reaction",
particle.label()
))
})
}
}
}
pub fn resolve_two_to_two(&self, event: &dyn EventLike) -> LadduResult<ResolvedTwoToTwo> {
match &self.topology {
ReactionTopology::TwoToTwo(topology) => topology.resolve(&self.graph, event),
}
}
pub fn mass(&self, particle: &str) -> Mass {
Mass::from_reaction(self.clone(), particle)
}
pub fn decay(&self, parent: &str) -> LadduResult<Decay> {
let parent_particle = self.particle(parent)?;
self.root_particle_for(parent)?;
let daughters = parent_particle.daughters();
if daughters.len() != 2 {
return Err(LadduError::Custom(
"isobar decays must contain exactly two ordered daughters".to_string(),
));
}
Ok(Decay {
reaction: self.clone(),
parent: parent.to_string(),
daughter_1: daughters[0].label().to_string(),
daughter_2: daughters[1].label().to_string(),
})
}
pub fn mandelstam(&self, channel: Channel) -> LadduResult<Mandelstam> {
self.topology.require_mandelstam()?;
Ok(Mandelstam::new(self.clone(), channel))
}
pub fn pol_angle<A>(&self, angle_aux: A) -> PolAngle
where
A: Into<String>,
{
PolAngle::new(self.clone(), angle_aux)
}
pub fn polarization<M, A>(&self, magnitude_aux: M, angle_aux: A) -> Polarization
where
M: Into<String>,
A: Into<String>,
{
Polarization::new(self.clone(), magnitude_aux, angle_aux)
}
pub fn axes(
&self,
event: &dyn EventLike,
particle: &str,
frame: Frame,
) -> LadduResult<FrameAxes> {
let (root_index, root) = self.root_particle_for(particle)?;
let particle_ref = self.particle(particle)?;
if particle_ref == root {
let resolved = self.resolve_two_to_two(event)?;
let (parent, spectator) = match root_index {
2 => (resolved.p3, resolved.p4),
3 => (resolved.p4, resolved.p3),
_ => {
return Err(LadduError::Custom(
"only outgoing p3 and p4 decay roots have frame axes".to_string(),
));
}
};
return FrameAxes::from_production_frame(
frame,
resolved.p1,
parent,
spectator,
resolved.com_boost(),
);
}
let parent = self.parent_of(particle).ok_or_else(|| {
LadduError::Custom(format!(
"particle '{}' is not connected to the reaction root",
particle
))
})?;
let parent_axes = self.axes(event, &parent, frame)?;
parent_axes.for_daughter(self.p4_in_rest_frame_of(event, &parent, particle)?.vec3())
}
pub fn angles_value(
&self,
event: &dyn EventLike,
parent: &str,
daughter: &str,
frame: Frame,
) -> LadduResult<DecayAngles> {
let parent_particle = self.particle(parent)?;
if !parent_particle
.daughters()
.iter()
.any(|candidate| candidate.label() == daughter)
{
return Err(LadduError::Custom(
"daughter is not an immediate child of parent".to_string(),
));
}
let axes = self.axes(event, parent, frame)?;
axes.angles(self.p4_in_rest_frame_of(event, parent, daughter)?.vec3())
}
pub fn particle(&self, particle: &str) -> LadduResult<&Particle> {
self.graph.particle(particle)
}
pub fn contains(&self, particle: &str) -> bool {
self.graph.contains(particle)
}
pub fn particles(&self) -> Vec<&Particle> {
self.graph.particles()
}
fn root_particle_for(&self, particle: &str) -> LadduResult<(usize, &Particle)> {
match &self.topology {
ReactionTopology::TwoToTwo(topology) => {
let p3 = self.graph.particle(topology.p3())?;
let p4 = self.graph.particle(topology.p4())?;
for (index, root) in [(2, p3), (3, p4)] {
if root.contains_id(particle) {
return Ok((index, root));
}
}
}
}
Err(LadduError::Custom(
"particle is not contained in an outgoing reaction root".to_string(),
))
}
fn parent_of(&self, child: &str) -> Option<String> {
match &self.topology {
ReactionTopology::TwoToTwo(topology) => {
let p3 = self.graph.particle(topology.p3()).ok()?;
let p4 = self.graph.particle(topology.p4()).ok()?;
[p3, p4].into_iter().find_map(|root| {
root.parent_of_id(child)
.map(|particle| particle.label().to_string())
})
}
}
}
fn p4_in_rest_frame_of(
&self,
event: &dyn EventLike,
frame_particle: &str,
target: &str,
) -> LadduResult<Vec4> {
let (_, root) = self.root_particle_for(frame_particle)?;
if frame_particle == root.label() {
let resolved = self.resolve_two_to_two(event)?;
let com_boost = resolved.com_boost();
let root_rest = RestFrame::new(self.p4(event, root.label())?.boost(&com_boost))?;
return Ok(root_rest.transform(self.p4(event, target)?.boost(&com_boost)));
}
let parent = self.parent_of(frame_particle).ok_or_else(|| {
LadduError::Custom("frame particle is not connected to root".to_string())
})?;
let frame_particle_in_parent = self.p4_in_rest_frame_of(event, &parent, frame_particle)?;
let target_in_parent = self.p4_in_rest_frame_of(event, &parent, target)?;
Ok(RestFrame::new(frame_particle_in_parent)?.transform(target_in_parent))
}
}
impl ReactionTopology {
pub const fn supports_mandelstam(&self) -> bool {
matches!(self, ReactionTopology::TwoToTwo(_))
}
pub fn require_mandelstam(&self) -> LadduResult<()> {
if self.supports_mandelstam() {
Ok(())
} else {
Err(LadduError::Custom(
"Mandelstam variables require a supported production topology".to_string(),
))
}
}
pub fn validate(&self, graph: &ParticleGraph) -> LadduResult<()> {
match self {
ReactionTopology::TwoToTwo(topology) => {
for role in ["p1", "p2", "p3", "p4"] {
graph.particle(topology.role(role)?)?;
}
Ok(())
}
}
}
}