Skip to main content

laddu_core/reaction/
graph.rs

1use std::collections::HashSet;
2
3use serde::{Deserialize, Serialize};
4
5use super::Particle;
6use crate::{LadduError, LadduResult};
7
8/// Owned particle definitions and decay edges for a reaction.
9#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
10pub struct ParticleGraph {
11    roots: Vec<Particle>,
12}
13
14impl ParticleGraph {
15    /// Construct a particle graph from root particles.
16    pub fn new(roots: impl IntoIterator<Item = Particle>) -> LadduResult<Self> {
17        let roots = roots.into_iter().collect::<Vec<_>>();
18        validate_unique_particles(roots.iter())?;
19        Ok(Self { roots })
20    }
21
22    /// Return the graph root particles.
23    pub fn roots(&self) -> &[Particle] {
24        &self.roots
25    }
26
27    /// Return the particle with the given identifier.
28    pub fn particle(&self, particle: &str) -> LadduResult<&Particle> {
29        self.find_particle(particle)
30            .ok_or_else(|| LadduError::Custom(format!("unknown reaction particle '{particle}'")))
31    }
32
33    /// Return whether the graph contains a particle with the given identifier.
34    pub fn contains(&self, particle: &str) -> bool {
35        self.find_particle(particle).is_some()
36    }
37
38    /// Return all particle definitions in graph traversal order.
39    pub fn particles(&self) -> Vec<&Particle> {
40        let mut particles = Vec::new();
41        for root in &self.roots {
42            collect_particles(root, &mut particles);
43        }
44        particles
45    }
46
47    /// Return the graph root containing the particle identifier.
48    pub fn root_containing(&self, particle: &str) -> Option<&Particle> {
49        self.roots.iter().find(|root| root.contains_id(particle))
50    }
51
52    /// Return the parent particle identifier for an immediate or nested child.
53    pub fn parent_of(&self, child: &str) -> Option<&Particle> {
54        self.roots.iter().find_map(|root| root.parent_of_id(child))
55    }
56
57    fn find_particle(&self, particle: &str) -> Option<&Particle> {
58        self.roots
59            .iter()
60            .find_map(|root| find_particle(root, particle))
61    }
62}
63
64fn validate_unique_particles<'a>(roots: impl IntoIterator<Item = &'a Particle>) -> LadduResult<()> {
65    let mut seen = HashSet::new();
66    for root in roots {
67        validate_unique_particle(root, &mut seen)?;
68    }
69    Ok(())
70}
71
72fn validate_unique_particle<'a>(
73    particle: &'a Particle,
74    seen: &mut HashSet<&'a str>,
75) -> LadduResult<()> {
76    if !seen.insert(particle.label()) {
77        return Err(LadduError::Custom(format!(
78            "duplicate reaction particle identifier '{}'",
79            particle.label()
80        )));
81    }
82    for daughter in particle.daughters() {
83        validate_unique_particle(daughter, seen)?;
84    }
85    Ok(())
86}
87
88fn collect_particles<'a>(particle: &'a Particle, particles: &mut Vec<&'a Particle>) {
89    particles.push(particle);
90    for daughter in particle.daughters() {
91        collect_particles(daughter, particles);
92    }
93}
94
95fn find_particle<'a>(particle: &'a Particle, id: &str) -> Option<&'a Particle> {
96    if particle.label() == id {
97        return Some(particle);
98    }
99    particle
100        .daughters()
101        .iter()
102        .find_map(|daughter| find_particle(daughter, id))
103}