Skip to main content

pdg_rs/models/
pdgparticle.rs

1use std::{
2    collections::{HashMap, HashSet},
3    fmt::Display,
4};
5
6use rusqlite::{
7    OptionalExtension, Row, params_from_iter,
8    types::{FromSql, FromSqlError, FromSqlResult, ValueRef},
9};
10
11use crate::{
12    DataEntry, DataType, LATEST_EDITION, Pdg, PdgFootnote, PdgId, PdgItem, PdgMeasurement,
13    PdgResult, PdgText,
14};
15
16/// Particle/antiparticle relationship encoded by the PDG particle table.
17#[derive(Copy, Clone, Debug, PartialEq, Eq)]
18pub enum ParticleType {
19    /// Particle entry.
20    Particle,
21    /// Antiparticle entry.
22    Antiparticle,
23    /// Self-conjugate particle entry.
24    SelfConjugate,
25}
26
27impl Display for ParticleType {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(
30            f,
31            "{}",
32            match self {
33                Self::Particle => "Particle",
34                Self::Antiparticle => "Antiparticle",
35                Self::SelfConjugate => "Self-Conjugate",
36            }
37        )
38    }
39}
40
41impl ParticleType {
42    /// Returns the compact PDG database code for this particle type.
43    #[must_use]
44    pub const fn to_code(self) -> &'static str {
45        match self {
46            Self::Particle => "P",
47            Self::Antiparticle => "A",
48            Self::SelfConjugate => "S",
49        }
50    }
51}
52
53impl FromSql for ParticleType {
54    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
55        match value {
56            ValueRef::Text(bytes) => {
57                let s =
58                    std::str::from_utf8(bytes).map_err(|err| FromSqlError::Other(Box::new(err)))?;
59                match s {
60                    "P" => Ok(Self::Particle),
61                    "A" => Ok(Self::Antiparticle),
62                    "S" => Ok(Self::SelfConjugate),
63                    _ => Err(FromSqlError::InvalidType),
64                }
65            }
66            _ => Err(FromSqlError::InvalidType),
67        }
68    }
69}
70
71/// Broad PDG particle class.
72#[derive(Copy, Clone, Debug, PartialEq, Eq)]
73pub enum ParticleClass {
74    /// Gauge or Higgs boson.
75    GaugeBoson,
76    /// Lepton.
77    Lepton,
78    /// Quark.
79    Quark,
80    /// Meson.
81    Meson,
82    /// Baryon.
83    Baryon,
84}
85
86impl ParticleClass {
87    /// Returns the compact PDG database code for this particle class.
88    #[must_use]
89    pub const fn to_code(self) -> &'static str {
90        match self {
91            Self::GaugeBoson => "G",
92            Self::Lepton => "L",
93            Self::Quark => "Q",
94            Self::Meson => "M",
95            Self::Baryon => "B",
96        }
97    }
98}
99
100impl Display for ParticleClass {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        write!(
103            f,
104            "{}",
105            match self {
106                Self::GaugeBoson => "Gauge/Higgs Boson",
107                Self::Lepton => "Lepton",
108                Self::Quark => "Quark",
109                Self::Meson => "Meson",
110                Self::Baryon => "Baryon",
111            }
112        )
113    }
114}
115
116impl FromSql for ParticleClass {
117    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
118        match value {
119            ValueRef::Text(bytes) => {
120                let s =
121                    std::str::from_utf8(bytes).map_err(|err| FromSqlError::Other(Box::new(err)))?;
122                match s {
123                    "G" => Ok(Self::GaugeBoson),
124                    "L" => Ok(Self::Lepton),
125                    "Q" => Ok(Self::Quark),
126                    "M" => Ok(Self::Meson),
127                    "B" => Ok(Self::Baryon),
128                    _ => Err(FromSqlError::InvalidType),
129                }
130            }
131            _ => Err(FromSqlError::InvalidType),
132        }
133    }
134}
135
136/// Electric charge in units of the positron charge.
137#[derive(Copy, Clone, Debug, PartialEq, Eq)]
138pub enum Charge {
139    /// Charge `+2`.
140    PlusPlus,
141    /// Charge `+1`.
142    Plus,
143    /// Charge `0`.
144    Neutral,
145    /// Charge `-1`.
146    Minus,
147    /// Charge `-2`.
148    MinusMinus,
149    /// Charge `+1/3`.
150    PlusOneThird,
151    /// Charge `+2/3`.
152    PlusTwoThirds,
153    /// Charge `-1/3`.
154    MinusOneThird,
155    /// Charge `-2/3`.
156    MinusTwoThirds,
157}
158
159impl Display for Charge {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        write!(
162            f,
163            "{}",
164            match self {
165                Self::PlusPlus => "+2",
166                Self::Plus => "+1",
167                Self::Neutral => "0",
168                Self::Minus => "-1",
169                Self::MinusMinus => "-2",
170                Self::PlusOneThird => "+1/3",
171                Self::PlusTwoThirds => "+2/3",
172                Self::MinusOneThird => "-1/3",
173                Self::MinusTwoThirds => "-2/3",
174            }
175        )
176    }
177}
178
179impl FromSql for Charge {
180    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
181        match value {
182            ValueRef::Real(v) => Self::from_f64(v).ok_or(FromSqlError::InvalidType),
183            _ => Err(FromSqlError::InvalidType),
184        }
185    }
186}
187
188impl Charge {
189    pub(crate) fn as_f64(self) -> f64 {
190        match self {
191            Self::PlusPlus => 2.0,
192            Self::Plus => 1.0,
193            Self::Neutral => 0.0,
194            Self::Minus => -1.0,
195            Self::MinusMinus => -2.0,
196            Self::PlusOneThird => 1.0 / 3.0,
197            Self::PlusTwoThirds => 2.0 / 3.0,
198            Self::MinusOneThird => -1.0 / 3.0,
199            Self::MinusTwoThirds => -2.0 / 3.0,
200        }
201    }
202
203    fn from_f64(value: f64) -> Option<Self> {
204        const EPSILON: f64 = 1e-12;
205        [
206            (2.0, Self::PlusPlus),
207            (1.0, Self::Plus),
208            (0.0, Self::Neutral),
209            (-1.0, Self::Minus),
210            (-2.0, Self::MinusMinus),
211            (1.0 / 3.0, Self::PlusOneThird),
212            (2.0 / 3.0, Self::PlusTwoThirds),
213            (-1.0 / 3.0, Self::MinusOneThird),
214            (-2.0 / 3.0, Self::MinusTwoThirds),
215        ]
216        .into_iter()
217        .find_map(|(charge, variant)| (value - charge).abs().lt(&EPSILON).then_some(variant))
218    }
219}
220
221/// Isospin quantum number.
222#[derive(Copy, Clone, Debug, PartialEq, Eq)]
223pub enum Isospin {
224    /// Isospin `0`.
225    I0,
226    /// Isospin `1/2`.
227    I1,
228    /// Isospin `1`.
229    I2,
230    /// Isospin `3/2`.
231    I3,
232    /// Ambiguous photon-like value `0 or 1`.
233    Photon,
234    /// Unknown isospin.
235    Unknown,
236}
237
238impl Display for Isospin {
239    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240        write!(
241            f,
242            "{}",
243            match self {
244                Self::I0 => "0",
245                Self::I1 => "1/2",
246                Self::I2 => "1",
247                Self::I3 => "3/2",
248                Self::Photon => "0 or 1",
249                Self::Unknown => "Unknown",
250            }
251        )
252    }
253}
254
255impl FromSql for Isospin {
256    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
257        match value {
258            ValueRef::Text(bytes) => {
259                let s =
260                    std::str::from_utf8(bytes).map_err(|err| FromSqlError::Other(Box::new(err)))?;
261                match s {
262                    "0" => Ok(Self::I0),
263                    "0,1" => Ok(Self::Photon),
264                    "1/2" => Ok(Self::I1),
265                    "1" => Ok(Self::I2),
266                    "3/2" => Ok(Self::I3),
267                    "?" => Ok(Self::Unknown),
268                    _ => Err(FromSqlError::InvalidType),
269                }
270            }
271            _ => Err(FromSqlError::InvalidType),
272        }
273    }
274}
275
276/// Angular momentum quantum number.
277#[derive(Clone, Debug, PartialEq, Eq)]
278pub enum AngularMomentum {
279    /// Angular momentum `0`.
280    J0,
281    /// Angular momentum `1/2`.
282    J1,
283    /// Angular momentum `1`.
284    J2,
285    /// Angular momentum `3/2`.
286    J3,
287    /// Angular momentum `2`.
288    J4,
289    /// Angular momentum `5/2`.
290    J5,
291    /// Angular momentum `3`.
292    J6,
293    /// Angular momentum `7/2`.
294    J7,
295    /// Angular momentum `4`.
296    J8,
297    /// Angular momentum `9/2`.
298    J9,
299    /// Angular momentum `5`.
300    J10,
301    /// Angular momentum `11/2`.
302    J11,
303    /// Angular momentum `6`.
304    J12,
305    /// Angular momentum `13/2`.
306    J13,
307    /// Angular momentum `7`.
308    J14,
309    /// Angular momentum `15/2`.
310    J15,
311    /// Custom angular momentum text from the PDG database.
312    Custom(String),
313    /// Unknown angular momentum.
314    Unknown,
315}
316
317impl Display for AngularMomentum {
318    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319        write!(
320            f,
321            "{}",
322            match self {
323                Self::J0 => "0",
324                Self::J1 => "1/2",
325                Self::J2 => "1",
326                Self::J3 => "3/2",
327                Self::J4 => "2",
328                Self::J5 => "5/2",
329                Self::J6 => "3",
330                Self::J7 => "7/2",
331                Self::J8 => "4",
332                Self::J9 => "9/2",
333                Self::J10 => "5",
334                Self::J11 => "11/2",
335                Self::J12 => "6",
336                Self::J13 => "13/2",
337                Self::J14 => "7",
338                Self::J15 => "15/2",
339                Self::Custom(s) => s,
340                Self::Unknown => "Unknown",
341            }
342        )
343    }
344}
345
346impl FromSql for AngularMomentum {
347    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
348        match value {
349            ValueRef::Text(bytes) => {
350                let s =
351                    std::str::from_utf8(bytes).map_err(|err| FromSqlError::Other(Box::new(err)))?;
352                match s {
353                    "0" => Ok(Self::J0),
354                    "1/2" => Ok(Self::J1),
355                    "1" => Ok(Self::J2),
356                    "3/2" => Ok(Self::J3),
357                    "2" => Ok(Self::J4),
358                    "5/2" => Ok(Self::J5),
359                    "3" => Ok(Self::J6),
360                    "7/2" => Ok(Self::J7),
361                    "4" => Ok(Self::J8),
362                    "9/2" => Ok(Self::J9),
363                    "5" => Ok(Self::J10),
364                    "11/2" => Ok(Self::J11),
365                    "6" => Ok(Self::J12),
366                    "13/2" => Ok(Self::J13),
367                    "7" => Ok(Self::J14),
368                    "15/2" => Ok(Self::J15),
369                    "?" => Ok(Self::Unknown),
370                    other => Ok(Self::Custom(other.to_string())),
371                }
372            }
373            _ => Err(FromSqlError::InvalidType),
374        }
375    }
376}
377
378/// Discrete parity-like quantum number.
379#[derive(Copy, Clone, Debug, PartialEq, Eq)]
380pub enum Parity {
381    /// Positive parity.
382    Plus,
383    /// Negative parity.
384    Minus,
385    /// Unknown parity.
386    Unknown,
387}
388
389impl Display for Parity {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        write!(
392            f,
393            "{}",
394            match self {
395                Self::Plus => "+",
396                Self::Minus => "-",
397                Self::Unknown => "Unknown",
398            }
399        )
400    }
401}
402
403impl FromSql for Parity {
404    fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
405        match value {
406            ValueRef::Text(bytes) => {
407                let s =
408                    std::str::from_utf8(bytes).map_err(|err| FromSqlError::Other(Box::new(err)))?;
409                match s {
410                    "+" => Ok(Self::Plus),
411                    "-" => Ok(Self::Minus),
412                    "?" => Ok(Self::Unknown),
413                    _ => Err(FromSqlError::InvalidType),
414                }
415            }
416            _ => Err(FromSqlError::InvalidType),
417        }
418    }
419}
420
421/// Particle row from the PDG database.
422#[derive(Debug, Clone)]
423pub struct PdgParticle<'pdg> {
424    pub(crate) db: &'pdg Pdg,
425    /// PDG identifier for the particle row.
426    pub pdgid: PdgId,
427    /// PDG item name.
428    pub name: String,
429    /// Human-readable PDG description.
430    pub description: String,
431    /// Particle/antiparticle relationship.
432    pub particle_type: ParticleType,
433    /// Broad particle class.
434    pub particle_class: ParticleClass,
435    /// Monte Carlo particle ID, when assigned.
436    pub mcid: Option<isize>,
437    /// Electric charge.
438    pub charge: Charge,
439    /// Isospin quantum number.
440    pub quantum_i: Option<Isospin>,
441    /// G-parity quantum number.
442    pub quantum_g: Option<Parity>,
443    /// Angular momentum quantum number.
444    pub quantum_j: Option<AngularMomentum>,
445    /// Parity quantum number.
446    pub quantum_p: Option<Parity>,
447    /// Charge-conjugation parity quantum number.
448    pub quantum_c: Option<Parity>,
449}
450
451/// Particle property value and its provenance.
452#[derive(Clone, Debug)]
453pub struct ParticleProperty<'pdg> {
454    /// Data type for the property.
455    pub data_type: DataType,
456    /// Data value for the property.
457    pub value: DataEntry<'pdg>,
458    /// Where the property was found.
459    pub source: PropertySource,
460}
461
462/// Source of a [`ParticleProperty`].
463#[derive(Clone, Debug)]
464pub enum PropertySource {
465    /// The property is stored directly under the particle identifier.
466    Direct,
467    /// The property is stored under a child section.
468    Section {
469        /// PDG identifier of the section containing the property.
470        section_pdgid: PdgId,
471    },
472}
473
474impl Display for PropertySource {
475    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476        match self {
477            Self::Direct => f.write_str("Direct"),
478            Self::Section { section_pdgid } => write!(f, "Section {section_pdgid}"),
479        }
480    }
481}
482
483impl Display for PdgParticle<'_> {
484    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
485        write!(
486            f,
487            "{} ({}, {}, {}, charge {})",
488            self.name, self.pdgid, self.particle_class, self.particle_type, self.charge
489        )?;
490
491        if let Some(mcid) = self.mcid {
492            write!(f, ", MCID {mcid}")?;
493        }
494
495        let mut quantum_numbers = Vec::new();
496        if let Some(isospin) = &self.quantum_i {
497            quantum_numbers.push(format!("I={isospin}"));
498        }
499        if let Some(g_parity) = &self.quantum_g {
500            quantum_numbers.push(format!("G={g_parity}"));
501        }
502        if let Some(spin) = &self.quantum_j {
503            quantum_numbers.push(format!("J={spin}"));
504        }
505        if let Some(parity) = &self.quantum_p {
506            quantum_numbers.push(format!("P={parity}"));
507        }
508        if let Some(charge_conjugation) = &self.quantum_c {
509            quantum_numbers.push(format!("C={charge_conjugation}"));
510        }
511
512        if !quantum_numbers.is_empty() {
513            write!(f, ", {}", quantum_numbers.join(", "))?;
514        }
515
516        Ok(())
517    }
518}
519
520impl<'pdg> PdgParticle<'pdg> {
521    pub(crate) fn from_row(db: &'pdg Pdg, row: &Row<'_>) -> rusqlite::Result<Self> {
522        Ok(Self {
523            db,
524            pdgid: row.get(0)?,
525            name: row.get(1)?,
526            description: row.get(2)?,
527            particle_type: row.get(3)?,
528            particle_class: row.get(4)?,
529            mcid: row.get(5)?,
530            charge: row.get(6)?,
531            quantum_i: row.get(7)?,
532            quantum_g: row.get(8)?,
533            quantum_j: row.get(9)?,
534            quantum_p: row.get(10)?,
535            quantum_c: row.get(11)?,
536        })
537    }
538
539    /// Returns a latest-edition property stored directly under this particle.
540    ///
541    /// # Errors
542    ///
543    /// Returns a database error if the data query fails.
544    pub fn direct_property(&self, data_type: DataType) -> PdgResult<Option<DataEntry<'pdg>>> {
545        self.query(data_type, LATEST_EDITION)
546    }
547
548    /// Returns a latest-edition property, checking direct and section data.
549    ///
550    /// # Errors
551    ///
552    /// Returns a database error if data or child identifier queries fail.
553    pub fn property(&self, data_type: DataType) -> PdgResult<Option<ParticleProperty<'pdg>>> {
554        if let Some(value) = self.direct_property(data_type)? {
555            return Ok(Some(ParticleProperty {
556                data_type,
557                value,
558                source: PropertySource::Direct,
559            }));
560        }
561
562        for section in self.db.children_for_pdgid(&self.pdgid)? {
563            if !matches!(section.data_type, DataType::Section) {
564                continue;
565            }
566            for child in self.db.children_for_pdgid(&section.pdgid)? {
567                if child.data_type == data_type {
568                    return self.section_property(&section, &child);
569                }
570            }
571        }
572
573        Ok(None)
574    }
575
576    fn section_property(
577        &self,
578        section: &crate::PdgIdEntry,
579        child: &crate::PdgIdEntry,
580    ) -> PdgResult<Option<ParticleProperty<'pdg>>> {
581        let data = self.db.data_for(&child.pdgid)?;
582        Ok(data.into_iter().next().map(|value| ParticleProperty {
583            data_type: child.data_type,
584            value,
585            source: PropertySource::Section {
586                section_pdgid: section.pdgid.clone(),
587            },
588        }))
589    }
590
591    /// Returns a compact display string for charge and available quantum numbers.
592    #[must_use]
593    pub fn quantum_summary(&self) -> String {
594        [
595            Some(format!("Q={}", self.charge)),
596            self.quantum_i.as_ref().map(|value| format!("I={value}")),
597            self.quantum_g.as_ref().map(|value| format!("G={value}")),
598            self.quantum_j.as_ref().map(|value| format!("J={value}")),
599            self.quantum_p.as_ref().map(|value| format!("P={value}")),
600            self.quantum_c.as_ref().map(|value| format!("C={value}")),
601        ]
602        .into_iter()
603        .flatten()
604        .collect::<Vec<_>>()
605        .join(", ")
606    }
607
608    /// Loads text blocks attached to this particle.
609    ///
610    /// # Errors
611    ///
612    /// Returns a database error if the text query fails.
613    pub fn texts(&self) -> PdgResult<Vec<PdgText>> {
614        self.db.texts_for(&self.pdgid)
615    }
616
617    /// Loads footnotes attached to this particle.
618    ///
619    /// # Errors
620    ///
621    /// Returns a database error if the footnote query fails.
622    pub fn footnotes(&self) -> PdgResult<Vec<PdgFootnote>> {
623        self.db.footnotes_for(&self.pdgid)
624    }
625
626    /// Loads the item hierarchy node for this particle.
627    ///
628    /// # Errors
629    ///
630    /// Returns a database error if the item query fails.
631    pub fn item(&self) -> PdgResult<Option<PdgItem<'pdg>>> {
632        self.db.item(&self.name)
633    }
634
635    /// Loads child items for this particle's item name.
636    ///
637    /// # Errors
638    ///
639    /// Returns a database error if the item hierarchy query fails.
640    pub fn item_children(&self) -> PdgResult<Vec<crate::PdgItemChild<'pdg>>> {
641        self.db.item_children(&self.name)
642    }
643
644    /// Loads parent items for this particle's item name.
645    ///
646    /// # Errors
647    ///
648    /// Returns a database error if the item hierarchy query fails.
649    pub fn parent_items(&self) -> PdgResult<Vec<PdgItem<'pdg>>> {
650        self.db.item_parents(&self.name)
651    }
652
653    /// Returns sibling particles related through the item hierarchy.
654    ///
655    /// # Errors
656    ///
657    /// Returns a database error if hierarchy or particle queries fail.
658    pub fn related_particles(&self) -> PdgResult<Vec<Self>> {
659        let mut related_particles = Vec::new();
660        let mut seen = HashSet::new();
661        seen.insert(self.name.clone());
662
663        for parent in self.parent_items()? {
664            for child in self.db.item_children(parent.name)? {
665                let Some(particle) = child.particle else {
666                    continue;
667                };
668                if seen.insert(particle.name.clone()) {
669                    related_particles.push(particle);
670                }
671            }
672        }
673
674        Ok(related_particles)
675    }
676
677    /// Loads measurements supporting a property data type.
678    ///
679    /// # Errors
680    ///
681    /// Returns a database error if the property or measurement queries fail.
682    pub fn measurements_for(&self, data_type: DataType) -> PdgResult<Vec<PdgMeasurement>> {
683        Ok(match self.property(data_type)? {
684            Some(property) => self.db.measurements_for(property.value.pdgid)?,
685            None => Vec::new(),
686        })
687    }
688
689    /// Returns the latest-edition mass entry, if available.
690    ///
691    /// # Errors
692    ///
693    /// Returns a database error if the property query fails.
694    pub fn mass(&self) -> PdgResult<Option<DataEntry<'pdg>>> {
695        Ok(self
696            .property(DataType::Mass)?
697            .map(|property| property.value))
698    }
699    /// Returns the latest-edition lifetime entry, if available.
700    ///
701    /// # Errors
702    ///
703    /// Returns a database error if the property query fails.
704    pub fn lifetime(&self) -> PdgResult<Option<DataEntry<'pdg>>> {
705        Ok(self
706            .property(DataType::Lifetime)?
707            .map(|property| property.value))
708    }
709    /// Returns the latest-edition full-width entry, if available.
710    ///
711    /// # Errors
712    ///
713    /// Returns a database error if the property query fails.
714    pub fn width(&self) -> PdgResult<Option<DataEntry<'pdg>>> {
715        Ok(self
716            .property(DataType::FullWidth)?
717            .map(|property| property.value))
718    }
719    /// Returns exclusive and inclusive branching fractions for this particle.
720    ///
721    /// # Errors
722    ///
723    /// Returns a database error if branching data, decay products, or related
724    /// data cannot be loaded.
725    pub fn branching_fractions(&self) -> PdgResult<Vec<BranchingFraction<'pdg>>> {
726        let mut branching_fractions = self.branching_fractions_for(&[
727            (
728                DataType::ExclusiveBranchingFraction,
729                BranchingFractionKind::Exclusive,
730            ),
731            (
732                DataType::ExclusiveBranchingFraction1,
733                BranchingFractionKind::Exclusive,
734            ),
735            (
736                DataType::ExclusiveBranchingFraction2,
737                BranchingFractionKind::Exclusive,
738            ),
739            (
740                DataType::ExclusiveBranchingFraction3,
741                BranchingFractionKind::Exclusive,
742            ),
743            (
744                DataType::ExclusiveBranchingFraction4,
745                BranchingFractionKind::Exclusive,
746            ),
747            (
748                DataType::ExclusiveBranchingFraction5,
749                BranchingFractionKind::Exclusive,
750            ),
751            (
752                DataType::InclusiveBranchingFraction,
753                BranchingFractionKind::Inclusive,
754            ),
755            (
756                DataType::InclusiveBranchingFraction1,
757                BranchingFractionKind::Inclusive,
758            ),
759            (
760                DataType::InclusiveBranchingFraction2,
761                BranchingFractionKind::Inclusive,
762            ),
763            (
764                DataType::InclusiveBranchingFraction3,
765                BranchingFractionKind::Inclusive,
766            ),
767            (
768                DataType::InclusiveBranchingFraction4,
769                BranchingFractionKind::Inclusive,
770            ),
771            (
772                DataType::InclusiveBranchingFraction5,
773                BranchingFractionKind::Inclusive,
774            ),
775        ])?;
776        self.attach_decay_products(&mut branching_fractions)?;
777        self.attach_related_data(&mut branching_fractions)?;
778        Ok(branching_fractions
779            .into_iter()
780            .map(|branching_fraction| branching_fraction.data)
781            .collect())
782    }
783    /// Returns exclusive branching fractions for this particle.
784    ///
785    /// # Errors
786    ///
787    /// Returns a database error if branching data, decay products, or related
788    /// data cannot be loaded.
789    pub fn exclusive_branching_fractions(&self) -> PdgResult<Vec<BranchingFraction<'pdg>>> {
790        let mut branching_fractions = self.branching_fractions_for(&[
791            (
792                DataType::ExclusiveBranchingFraction,
793                BranchingFractionKind::Exclusive,
794            ),
795            (
796                DataType::ExclusiveBranchingFraction1,
797                BranchingFractionKind::Exclusive,
798            ),
799            (
800                DataType::ExclusiveBranchingFraction2,
801                BranchingFractionKind::Exclusive,
802            ),
803            (
804                DataType::ExclusiveBranchingFraction3,
805                BranchingFractionKind::Exclusive,
806            ),
807            (
808                DataType::ExclusiveBranchingFraction4,
809                BranchingFractionKind::Exclusive,
810            ),
811            (
812                DataType::ExclusiveBranchingFraction5,
813                BranchingFractionKind::Exclusive,
814            ),
815        ])?;
816        self.attach_decay_products(&mut branching_fractions)?;
817        self.attach_related_data(&mut branching_fractions)?;
818        Ok(branching_fractions
819            .into_iter()
820            .map(|branching_fraction| branching_fraction.data)
821            .collect())
822    }
823    /// Returns inclusive branching fractions for this particle.
824    ///
825    /// # Errors
826    ///
827    /// Returns a database error if branching data, decay products, or related
828    /// data cannot be loaded.
829    pub fn inclusive_branching_fractions(&self) -> PdgResult<Vec<BranchingFraction<'pdg>>> {
830        let mut branching_fractions = self.branching_fractions_for(&[
831            (
832                DataType::InclusiveBranchingFraction,
833                BranchingFractionKind::Inclusive,
834            ),
835            (
836                DataType::InclusiveBranchingFraction1,
837                BranchingFractionKind::Inclusive,
838            ),
839            (
840                DataType::InclusiveBranchingFraction2,
841                BranchingFractionKind::Inclusive,
842            ),
843            (
844                DataType::InclusiveBranchingFraction3,
845                BranchingFractionKind::Inclusive,
846            ),
847            (
848                DataType::InclusiveBranchingFraction4,
849                BranchingFractionKind::Inclusive,
850            ),
851            (
852                DataType::InclusiveBranchingFraction5,
853                BranchingFractionKind::Inclusive,
854            ),
855        ])?;
856        self.attach_decay_products(&mut branching_fractions)?;
857        self.attach_related_data(&mut branching_fractions)?;
858        Ok(branching_fractions
859            .into_iter()
860            .map(|branching_fraction| branching_fraction.data)
861            .collect())
862    }
863    /// Returns branching-ratio rows for this particle.
864    ///
865    /// # Errors
866    ///
867    /// Returns a database error if branching-ratio data cannot be loaded.
868    pub fn branching_ratios(&self) -> PdgResult<Vec<BranchingRatio<'pdg>>> {
869        Ok(self
870            .decay_data(DataType::BranchingRatio, LATEST_EDITION)?
871            .into_iter()
872            .map(|data| BranchingRatio {
873                pdgid: data.pdgid,
874                description: data.description,
875                mode_number: data.mode_number,
876                value: data.data,
877            })
878            .collect())
879    }
880    /// Queries all latest matching data entries and maps them through `predicate`.
881    ///
882    /// Entries for which `predicate` returns `None` are omitted.
883    ///
884    /// # Errors
885    ///
886    /// Returns a database error if the data query fails.
887    pub fn query_all_map<P, T>(
888        &self,
889        data_type: DataType,
890        edition: impl Into<String>,
891        predicate: P,
892    ) -> PdgResult<Vec<T>>
893    where
894        P: Fn(DataEntry<'pdg>) -> Option<T>,
895    {
896        Ok(self
897            .query_all(data_type, edition)?
898            .into_iter()
899            .filter_map(predicate)
900            .collect())
901    }
902    /// Queries the first matching data entry and maps it through `predicate`.
903    ///
904    /// # Errors
905    ///
906    /// Returns a database error if the data query fails.
907    pub fn query_map<P, T>(
908        &self,
909        data_type: DataType,
910        edition: impl Into<String>,
911        predicate: P,
912    ) -> PdgResult<Option<T>>
913    where
914        P: Fn(DataEntry<'pdg>) -> Option<T>,
915    {
916        Ok(self.query(data_type, edition)?.and_then(predicate))
917    }
918    /// Returns all data entries of `data_type` for a PDG edition.
919    ///
920    /// # Errors
921    ///
922    /// Returns a database error if the data query fails.
923    pub fn query_all(
924        &self,
925        data_type: DataType,
926        edition: impl Into<String>,
927    ) -> PdgResult<Vec<DataEntry<'pdg>>> {
928        let sql = format!(
929            "SELECT {} FROM pdgdata JOIN pdgid ON pdgid.id = pdgdata.pdgid_id WHERE pdgid.data_type = ?1 AND pdgid.parent_pdgid = ?2 AND pdgdata.edition = ?3 ORDER BY edition DESC, pdgdata.sort ASC",
930            DataEntry::COLUMNS
931        );
932        let mut stmt = self.db.db().prepare(&sql)?;
933        Ok(stmt
934            .query_map([data_type.to_code(), &self.pdgid, &edition.into()], |row| {
935                DataEntry::from_row(self.db, row)
936            })?
937            .collect::<Result<Vec<_>, _>>()?)
938    }
939    /// Returns the first data entry of `data_type` for a PDG edition.
940    ///
941    /// # Errors
942    ///
943    /// Returns a database error if the data query fails.
944    pub fn query(
945        &self,
946        data_type: DataType,
947        edition: impl Into<String>,
948    ) -> PdgResult<Option<DataEntry<'pdg>>> {
949        let sql = format!(
950            "SELECT {} FROM pdgdata JOIN pdgid ON pdgid.id = pdgdata.pdgid_id WHERE pdgid.data_type = ?1 AND pdgid.parent_pdgid = ?2 AND pdgdata.edition = ?3 ORDER BY edition DESC, pdgdata.sort ASC",
951            DataEntry::COLUMNS
952        );
953        let mut stmt = self.db.db().prepare(&sql)?;
954        Ok(stmt
955            .query_row([data_type.to_code(), &self.pdgid, &edition.into()], |row| {
956                DataEntry::from_row(self.db, row)
957            })
958            .optional()?)
959    }
960
961    fn branching_fractions_for(
962        &self,
963        data_types: &[(DataType, BranchingFractionKind)],
964    ) -> PdgResult<Vec<BranchingFractionWithSort<'pdg>>> {
965        let mut branching_fractions = Vec::new();
966        for (data_type, kind) in data_types {
967            branching_fractions.extend(
968                self.decay_data(*data_type, LATEST_EDITION)?
969                    .into_iter()
970                    .map(|data| BranchingFractionWithSort {
971                        data: BranchingFraction {
972                            pdgid: data.pdgid,
973                            description: data.description,
974                            mode_number: data.mode_number,
975                            value: data.data,
976                            kind: *kind,
977                            products: Vec::new(),
978                            related_data: Vec::new(),
979                        },
980                        sort: data.sort,
981                    }),
982            );
983        }
984        branching_fractions.sort_by_key(|branching_fraction| branching_fraction.sort);
985        Ok(branching_fractions)
986    }
987
988    fn decay_data(
989        &self,
990        data_type: DataType,
991        edition: impl Into<String>,
992    ) -> PdgResult<Vec<DecayData<'pdg>>> {
993        let data_type = data_type.to_code();
994        let edition = edition.into();
995        let sql = format!(
996            "SELECT {}, pdgid.description, pdgid.mode_number, pdgid.sort FROM pdgdata JOIN pdgid ON pdgid.id = pdgdata.pdgid_id WHERE pdgid.data_type = ?1 AND pdgid.parent_pdgid = ?2 AND pdgdata.edition = ?3 ORDER BY pdgid.sort ASC, pdgdata.sort ASC",
997            DataEntry::COLUMNS
998        );
999        let mut stmt = self.db.db().prepare(&sql)?;
1000        Ok(stmt
1001            .query_map([data_type, &self.pdgid, &edition], |row| {
1002                DecayData::from_row(self.db, row)
1003            })?
1004            .collect::<Result<Vec<_>, _>>()?)
1005    }
1006
1007    fn attach_decay_products(
1008        &self,
1009        branching_fractions: &mut [BranchingFractionWithSort<'pdg>],
1010    ) -> PdgResult<()> {
1011        if branching_fractions.is_empty() {
1012            return Ok(());
1013        }
1014
1015        let pdgids = branching_fractions
1016            .iter()
1017            .map(|branching_fraction| branching_fraction.data.pdgid.as_str())
1018            .collect::<Vec<_>>();
1019        let placeholders = std::iter::repeat_n("?", pdgids.len())
1020            .collect::<Vec<_>>()
1021            .join(", ");
1022        let sql = format!(
1023            "SELECT pdgid, name, is_outgoing, multiplier FROM pdgdecay WHERE pdgid IN ({placeholders}) ORDER BY pdgid ASC, sort ASC"
1024        );
1025        let mut stmt = self.db.db().prepare(&sql)?;
1026        let mut products_by_pdgid: HashMap<PdgId, Vec<DecayProduct<'pdg>>> = HashMap::new();
1027        let rows = stmt.query_map(params_from_iter(pdgids), |row| {
1028            Ok((
1029                row.get::<_, PdgId>(0)?,
1030                DecayProduct {
1031                    db: self.db,
1032                    name: row.get(1)?,
1033                    is_outgoing: row.get(2)?,
1034                    multiplier: row.get::<_, i64>(3)?,
1035                },
1036            ))
1037        })?;
1038
1039        for row in rows {
1040            let (pdgid, product) = row?;
1041            products_by_pdgid.entry(pdgid).or_default().push(product);
1042        }
1043
1044        for branching_fraction in branching_fractions {
1045            branching_fraction.data.products = products_by_pdgid
1046                .remove(&branching_fraction.data.pdgid)
1047                .unwrap_or_default();
1048        }
1049        Ok(())
1050    }
1051
1052    fn attach_related_data(
1053        &self,
1054        branching_fractions: &mut [BranchingFractionWithSort<'pdg>],
1055    ) -> PdgResult<()> {
1056        if branching_fractions.is_empty() {
1057            return Ok(());
1058        }
1059
1060        let pdgids = branching_fractions
1061            .iter()
1062            .map(|branching_fraction| branching_fraction.data.pdgid.as_str())
1063            .collect::<Vec<_>>();
1064        let placeholders = std::iter::repeat_n("?", pdgids.len())
1065            .collect::<Vec<_>>()
1066            .join(", ");
1067        let sql = format!(
1068            "SELECT {}, target.description, target.mode_number, target.data_type, pdgid_map.source FROM pdgid_map JOIN pdgid target ON target.id = pdgid_map.target_id JOIN pdgdata ON pdgdata.pdgid_id = target.id WHERE pdgid_map.source IN ({placeholders}) AND pdgdata.edition = ? ORDER BY pdgid_map.source ASC, pdgid_map.sort ASC, pdgdata.sort ASC",
1069            DataEntry::COLUMNS
1070        );
1071
1072        let mut params = pdgids;
1073        params.push(LATEST_EDITION);
1074        let mut stmt = self.db.db().prepare(&sql)?;
1075        let mut related_by_pdgid: HashMap<PdgId, Vec<RelatedDataEntry<'pdg>>> = HashMap::new();
1076        let rows = stmt.query_map(params_from_iter(params), |row| {
1077            Ok((
1078                row.get::<_, PdgId>(DataEntry::COLUMN_COUNT + 3)?,
1079                RelatedDataEntry::from_row(self.db, row),
1080            ))
1081        })?;
1082
1083        for row in rows {
1084            let (pdgid, related_data) = row?;
1085            if let Ok(related_data) = related_data {
1086                related_by_pdgid
1087                    .entry(pdgid)
1088                    .or_default()
1089                    .push(related_data);
1090            }
1091        }
1092
1093        for branching_fraction in branching_fractions {
1094            branching_fraction.data.related_data = related_by_pdgid
1095                .remove(&branching_fraction.data.pdgid)
1096                .unwrap_or_default();
1097        }
1098        Ok(())
1099    }
1100}
1101
1102#[derive(Debug)]
1103struct DecayData<'pdg> {
1104    pdgid: PdgId,
1105    description: String,
1106    mode_number: Option<u32>,
1107    data: DataEntry<'pdg>,
1108    sort: u32,
1109}
1110
1111impl<'pdg> DecayData<'pdg> {
1112    fn from_row(db: &'pdg Pdg, row: &Row<'_>) -> rusqlite::Result<Self> {
1113        let data = DataEntry::from_row(db, row)?;
1114        Ok(Self {
1115            pdgid: data.pdgid.clone(),
1116            description: row.get(DataEntry::COLUMN_COUNT)?,
1117            mode_number: row.get::<_, Option<u32>>(DataEntry::COLUMN_COUNT + 1)?,
1118            data,
1119            sort: row.get::<_, u32>(DataEntry::COLUMN_COUNT + 2)?,
1120        })
1121    }
1122}
1123
1124#[derive(Debug)]
1125struct BranchingFractionWithSort<'pdg> {
1126    data: BranchingFraction<'pdg>,
1127    sort: u32,
1128}
1129
1130/// Whether a branching fraction is exclusive or inclusive.
1131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1132pub enum BranchingFractionKind {
1133    /// Exclusive branching fraction.
1134    Exclusive,
1135    /// Inclusive branching fraction.
1136    Inclusive,
1137}
1138
1139impl Display for BranchingFractionKind {
1140    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1141        f.write_str(match self {
1142            Self::Exclusive => "Exclusive",
1143            Self::Inclusive => "Inclusive",
1144        })
1145    }
1146}
1147
1148/// Decay product listed for a branching-fraction row.
1149#[derive(Debug, Clone)]
1150pub struct DecayProduct<'pdg> {
1151    pub(crate) db: &'pdg Pdg,
1152    /// PDG item name for the product.
1153    pub name: String,
1154    /// Whether this product is outgoing from the decay.
1155    pub is_outgoing: bool,
1156    /// Multiplicity of this product in the decay row.
1157    pub multiplier: i64,
1158}
1159
1160impl<'pdg> DecayProduct<'pdg> {
1161    /// Loads the PDG item for this decay product.
1162    ///
1163    /// # Errors
1164    ///
1165    /// Returns a database error if the item query fails.
1166    pub fn item(&self) -> PdgResult<Option<PdgItem<'pdg>>> {
1167        self.db.item(&self.name)
1168    }
1169
1170    /// Loads the particle for this decay product, when it names a particle.
1171    ///
1172    /// # Errors
1173    ///
1174    /// Returns a database error if the particle query fails.
1175    pub fn particle(&self) -> PdgResult<Option<PdgParticle<'pdg>>> {
1176        self.db.particle(&self.name)
1177    }
1178
1179    /// Loads child items for this decay product name.
1180    ///
1181    /// # Errors
1182    ///
1183    /// Returns a database error if the hierarchy query fails.
1184    pub fn children(&self) -> PdgResult<Vec<crate::PdgItemChild<'pdg>>> {
1185        self.db.item_children(&self.name)
1186    }
1187
1188    /// Loads parent items for this decay product name.
1189    ///
1190    /// # Errors
1191    ///
1192    /// Returns a database error if the hierarchy query fails.
1193    pub fn parents(&self) -> PdgResult<Vec<PdgItem<'pdg>>> {
1194        self.db.item_parents(&self.name)
1195    }
1196}
1197
1198/// Branching-fraction data for a particle decay mode.
1199#[derive(Debug, Clone)]
1200pub struct BranchingFraction<'pdg> {
1201    /// PDG identifier for the branching-fraction row.
1202    pub pdgid: PdgId,
1203    /// Human-readable decay description.
1204    pub description: String,
1205    /// PDG mode number, when available.
1206    pub mode_number: Option<u32>,
1207    /// Numeric value for the branching fraction.
1208    pub value: DataEntry<'pdg>,
1209    /// Exclusive or inclusive branching-fraction kind.
1210    pub kind: BranchingFractionKind,
1211    /// Decay products attached to the branching-fraction row.
1212    pub products: Vec<DecayProduct<'pdg>>,
1213    /// Other data rows mapped to this branching-fraction row.
1214    pub related_data: Vec<RelatedDataEntry<'pdg>>,
1215}
1216
1217impl BranchingFraction<'_> {
1218    /// Loads measurements supporting this branching fraction.
1219    ///
1220    /// # Errors
1221    ///
1222    /// Returns a database error if the measurement query fails.
1223    pub fn measurements(&self) -> PdgResult<Vec<PdgMeasurement>> {
1224        self.value.measurements()
1225    }
1226
1227    /// Loads footnotes attached to this branching fraction.
1228    ///
1229    /// # Errors
1230    ///
1231    /// Returns a database error if the footnote query fails.
1232    pub fn footnotes(&self) -> PdgResult<Vec<PdgFootnote>> {
1233        self.value.footnotes()
1234    }
1235
1236    /// Loads text blocks attached to this branching fraction.
1237    ///
1238    /// # Errors
1239    ///
1240    /// Returns a database error if the text query fails.
1241    pub fn texts(&self) -> PdgResult<Vec<PdgText>> {
1242        self.value.texts()
1243    }
1244}
1245
1246/// Data row related to a branching fraction through the PDG mapping table.
1247#[derive(Debug, Clone)]
1248pub struct RelatedDataEntry<'pdg> {
1249    /// PDG identifier for the related row.
1250    pub pdgid: PdgId,
1251    /// Human-readable PDG description.
1252    pub description: String,
1253    /// Data type for the related row.
1254    pub data_type: DataType,
1255    /// PDG mode number, when available.
1256    pub mode_number: Option<u32>,
1257    /// Numeric value for the related row.
1258    pub value: DataEntry<'pdg>,
1259}
1260
1261impl<'pdg> RelatedDataEntry<'pdg> {
1262    fn from_row(db: &'pdg Pdg, row: &Row<'_>) -> rusqlite::Result<Self> {
1263        let data = DataEntry::from_row(db, row)?;
1264        Ok(Self {
1265            pdgid: data.pdgid.clone(),
1266            description: row.get(DataEntry::COLUMN_COUNT)?,
1267            mode_number: row.get::<_, Option<u32>>(DataEntry::COLUMN_COUNT + 1)?,
1268            data_type: row.get(DataEntry::COLUMN_COUNT + 2)?,
1269            value: data,
1270        })
1271    }
1272
1273    /// Loads measurements supporting this related data row.
1274    ///
1275    /// # Errors
1276    ///
1277    /// Returns a database error if the measurement query fails.
1278    pub fn measurements(&self) -> PdgResult<Vec<PdgMeasurement>> {
1279        self.value.measurements()
1280    }
1281
1282    /// Loads footnotes attached to this related data row.
1283    ///
1284    /// # Errors
1285    ///
1286    /// Returns a database error if the footnote query fails.
1287    pub fn footnotes(&self) -> PdgResult<Vec<PdgFootnote>> {
1288        self.value.footnotes()
1289    }
1290
1291    /// Loads text blocks attached to this related data row.
1292    ///
1293    /// # Errors
1294    ///
1295    /// Returns a database error if the text query fails.
1296    pub fn texts(&self) -> PdgResult<Vec<PdgText>> {
1297        self.value.texts()
1298    }
1299}
1300
1301/// Branching-ratio data for a particle.
1302#[derive(Debug, Clone)]
1303pub struct BranchingRatio<'pdg> {
1304    /// PDG identifier for the branching-ratio row.
1305    pub pdgid: PdgId,
1306    /// Human-readable PDG description.
1307    pub description: String,
1308    /// PDG mode number, when available.
1309    pub mode_number: Option<u32>,
1310    /// Numeric value for the branching ratio.
1311    pub value: DataEntry<'pdg>,
1312}
1313
1314impl BranchingRatio<'_> {
1315    /// Loads measurements supporting this branching ratio.
1316    ///
1317    /// # Errors
1318    ///
1319    /// Returns a database error if the measurement query fails.
1320    pub fn measurements(&self) -> PdgResult<Vec<PdgMeasurement>> {
1321        self.value.measurements()
1322    }
1323
1324    /// Loads footnotes attached to this branching ratio.
1325    ///
1326    /// # Errors
1327    ///
1328    /// Returns a database error if the footnote query fails.
1329    pub fn footnotes(&self) -> PdgResult<Vec<PdgFootnote>> {
1330        self.value.footnotes()
1331    }
1332
1333    /// Loads text blocks attached to this branching ratio.
1334    ///
1335    /// # Errors
1336    ///
1337    /// Returns a database error if the text query fails.
1338    pub fn texts(&self) -> PdgResult<Vec<PdgText>> {
1339        self.value.texts()
1340    }
1341}
1342
1343#[cfg(test)]
1344mod tests {
1345    use crate::{
1346        AngularMomentum, BranchingFractionKind, Charge, DataType, DecayStateExpansion, Isospin,
1347        LimitType, Parity, ParticleClass, ParticleSearchQuery, ParticleType, Pdg, PdgItemType,
1348        PropertySource,
1349    };
1350
1351    fn test_pdg() -> Pdg {
1352        Pdg::open_path(concat!(
1353            env!("CARGO_MANIFEST_DIR"),
1354            "/data/pdgall-2025-v0.2.2.sqlite"
1355        ))
1356        .unwrap()
1357    }
1358
1359    #[test]
1360    fn displays_particle_identity_and_quantum_numbers() {
1361        let db = test_pdg();
1362        let pion = db.particle("pi+").unwrap().unwrap();
1363
1364        assert_eq!(
1365            pion.to_string(),
1366            "pi+ (S008, Meson, Particle, charge +1), MCID 211, I=1, G=-, J=0, P=-"
1367        );
1368    }
1369
1370    #[test]
1371    fn displays_self_conjugate_particles() {
1372        let db = test_pdg();
1373        let photon = db.particle("gamma").unwrap().unwrap();
1374
1375        assert_eq!(
1376            photon.to_string(),
1377            "gamma (S000, Gauge/Higgs Boson, Self-Conjugate, charge 0), MCID 22, I=0 or 1, J=1, P=-, C=-"
1378        );
1379    }
1380
1381    #[test]
1382    fn displays_fractional_charges() {
1383        let db = test_pdg();
1384
1385        let down_quark = db.particle("d").unwrap().unwrap();
1386        assert_eq!(
1387            down_quark.to_string(),
1388            "d (Q001, Quark, Particle, charge -1/3), MCID 1, I=1/2, J=1/2, P=+"
1389        );
1390
1391        let up_quark = db.particle("u").unwrap().unwrap();
1392        assert_eq!(
1393            up_quark.to_string(),
1394            "u (Q002, Quark, Particle, charge +2/3), MCID 2, I=1/2, J=1/2, P=+"
1395        );
1396
1397        let antidown_quark = db.particle("dbar").unwrap().unwrap();
1398        assert_eq!(
1399            antidown_quark.to_string(),
1400            "dbar (Q001, Quark, Antiparticle, charge +1/3), MCID -1, I=1/2, J=1/2, P=-"
1401        );
1402
1403        let antiup_quark = db.particle("ubar").unwrap().unwrap();
1404        assert_eq!(
1405            antiup_quark.to_string(),
1406            "ubar (Q002, Quark, Antiparticle, charge -2/3), MCID -2, I=1/2, J=1/2, P=-"
1407        );
1408    }
1409
1410    #[test]
1411    fn classifies_representative_particles() {
1412        let db = test_pdg();
1413
1414        let pion = db.particle("pi+").unwrap().unwrap();
1415        assert_eq!(pion.description, "pi+-");
1416        assert_eq!(pion.particle_class, ParticleClass::Meson);
1417        assert_eq!(
1418            db.particle("p").unwrap().unwrap().particle_class,
1419            ParticleClass::Baryon
1420        );
1421        assert_eq!(
1422            db.particle("e-").unwrap().unwrap().particle_class,
1423            ParticleClass::Lepton
1424        );
1425        assert_eq!(
1426            db.particle("d").unwrap().unwrap().particle_class,
1427            ParticleClass::Quark
1428        );
1429        assert_eq!(
1430            db.particle("gamma").unwrap().unwrap().particle_class,
1431            ParticleClass::GaugeBoson
1432        );
1433    }
1434
1435    #[test]
1436    fn checks_particle_classes() {
1437        let db = test_pdg();
1438
1439        let pion = db.particle("pi+").unwrap().unwrap();
1440        assert!(pion.particle_class == ParticleClass::Meson);
1441        assert!(db.particle("p").unwrap().unwrap().particle_class == ParticleClass::Baryon);
1442        assert!(db.particle("e-").unwrap().unwrap().particle_class == ParticleClass::Lepton);
1443        assert!(db.particle("d").unwrap().unwrap().particle_class == ParticleClass::Quark);
1444        assert!(db.particle("gamma").unwrap().unwrap().particle_class == ParticleClass::GaugeBoson);
1445    }
1446
1447    #[test]
1448    fn queries_particles_by_class() {
1449        let db = test_pdg();
1450        let leptons = db
1451            .search_particles(ParticleSearchQuery::new().class(ParticleClass::Lepton))
1452            .unwrap();
1453
1454        assert!(
1455            leptons
1456                .iter()
1457                .all(|particle| particle.particle_class == ParticleClass::Lepton)
1458        );
1459        assert!(leptons.iter().any(|particle| particle.name == "e-"));
1460        assert!(leptons.iter().any(|particle| particle.name == "mu-"));
1461        assert!(leptons.iter().any(|particle| particle.name == "nu_e"));
1462        assert!(!leptons.iter().any(|particle| particle.name == "pi+"));
1463    }
1464
1465    #[test]
1466    fn searches_particles_by_class() {
1467        let db = test_pdg();
1468        let pion_mesons = db
1469            .search_particles(
1470                ParticleSearchQuery::new()
1471                    .name_contains("pi")
1472                    .class(ParticleClass::Meson),
1473            )
1474            .unwrap();
1475        let pion_baryons = db
1476            .search_particles(
1477                ParticleSearchQuery::new()
1478                    .name_contains("pi")
1479                    .class(ParticleClass::Baryon),
1480            )
1481            .unwrap();
1482
1483        assert!(!pion_mesons.is_empty());
1484        assert!(
1485            pion_mesons
1486                .iter()
1487                .all(|particle| particle.particle_class == ParticleClass::Meson)
1488        );
1489        assert!(pion_mesons.iter().any(|particle| particle.name == "pi+"));
1490        assert!(!pion_baryons.iter().any(|particle| particle.name == "pi+"));
1491    }
1492
1493    #[test]
1494    fn searches_by_class_and_angular_momentum() {
1495        let db = test_pdg();
1496        let vector_mesons = db
1497            .search_particles(
1498                ParticleSearchQuery::new()
1499                    .class(ParticleClass::Meson)
1500                    .angular_momentum(AngularMomentum::J2),
1501            )
1502            .unwrap();
1503
1504        assert!(!vector_mesons.is_empty());
1505        assert!(vector_mesons.iter().all(|particle| {
1506            particle.particle_class == ParticleClass::Meson
1507                && particle.quantum_j == Some(AngularMomentum::J2)
1508        }));
1509    }
1510
1511    #[test]
1512    fn searches_by_particle_type_charge_and_quantum_numbers() {
1513        let db = test_pdg();
1514        let scalar_neutral_mesons = db
1515            .search_particles(
1516                ParticleSearchQuery::new()
1517                    .class(ParticleClass::Meson)
1518                    .particle_type(ParticleType::SelfConjugate)
1519                    .charge(Charge::Neutral)
1520                    .isospin(Isospin::I0)
1521                    .g_parity(Parity::Plus)
1522                    .angular_momentum(AngularMomentum::J0)
1523                    .parity(Parity::Plus)
1524                    .charge_conjugation(Parity::Plus),
1525            )
1526            .unwrap();
1527
1528        assert!(
1529            scalar_neutral_mesons
1530                .iter()
1531                .any(|particle| particle.name == "f_0(980)0")
1532        );
1533        assert!(scalar_neutral_mesons.iter().all(|particle| {
1534            particle.particle_class == ParticleClass::Meson
1535                && particle.particle_type == ParticleType::SelfConjugate
1536                && particle.charge == Charge::Neutral
1537                && particle.quantum_i == Some(Isospin::I0)
1538                && particle.quantum_g == Some(Parity::Plus)
1539                && particle.quantum_j == Some(AngularMomentum::J0)
1540                && particle.quantum_p == Some(Parity::Plus)
1541                && particle.quantum_c == Some(Parity::Plus)
1542        }));
1543    }
1544
1545    #[test]
1546    fn searches_for_missing_optional_quantum_numbers() {
1547        let db = test_pdg();
1548        let particles = db
1549            .search_particles(
1550                ParticleSearchQuery::new()
1551                    .name_contains("p")
1552                    .g_parity(None)
1553                    .charge_conjugation(None),
1554            )
1555            .unwrap();
1556
1557        assert!(particles.iter().any(|particle| particle.name == "p"));
1558        assert!(
1559            particles
1560                .iter()
1561                .all(|particle| particle.quantum_g.is_none() && particle.quantum_c.is_none())
1562        );
1563    }
1564
1565    #[test]
1566    fn searches_by_normalized_mass_range() {
1567        let db = test_pdg();
1568        let light_mesons = db
1569            .search_particles(
1570                ParticleSearchQuery::new()
1571                    .class(ParticleClass::Meson)
1572                    .mass_range_mev(100.0, 150.0),
1573            )
1574            .unwrap();
1575
1576        assert!(light_mesons.iter().any(|particle| particle.name == "pi+"));
1577        assert!(light_mesons.iter().any(|particle| particle.name == "pi-"));
1578        assert!(light_mesons.iter().any(|particle| particle.name == "pi0"));
1579    }
1580
1581    #[test]
1582    fn range_searches_use_section_derived_properties() {
1583        let db = test_pdg();
1584        let particles = db
1585            .search_particles(
1586                ParticleSearchQuery::new()
1587                    .name_contains("a_0(")
1588                    .mass_range_mev(1500.0, 2000.0),
1589            )
1590            .unwrap();
1591
1592        assert!(
1593            particles
1594                .iter()
1595                .any(|particle| particle.name == "a_0(1710)0")
1596        );
1597        assert!(
1598            !particles
1599                .iter()
1600                .any(|particle| particle.name == "a_0(980)0")
1601        );
1602    }
1603
1604    #[test]
1605    fn searches_by_ambiguous_width_range() {
1606        let db = test_pdg();
1607        let particles = db
1608            .search_particles(ParticleSearchQuery::new().width_range_mev(0.0, 50.0))
1609            .unwrap();
1610
1611        assert!(
1612            particles
1613                .iter()
1614                .any(|particle| particle.name == "f_0(980)0")
1615        );
1616        assert!(
1617            !particles
1618                .iter()
1619                .any(|particle| particle.name == "D_1(2430)0")
1620        );
1621        assert!(particles.iter().any(|particle| particle.name == "e-"));
1622    }
1623
1624    #[test]
1625    fn searches_by_lifetime_range() {
1626        let db = test_pdg();
1627        let particles = db
1628            .search_particles(ParticleSearchQuery::new().lifetime_range_seconds(1e-8, 1e-7))
1629            .unwrap();
1630
1631        assert!(particles.iter().any(|particle| particle.name == "pi+"));
1632        assert!(!particles.iter().any(|particle| particle.name == "p"));
1633    }
1634
1635    #[test]
1636    fn searches_by_decay_final_states() {
1637        let db = test_pdg();
1638        let sigma_modes = db
1639            .search_particles(
1640                ParticleSearchQuery::new()
1641                    .class(ParticleClass::Baryon)
1642                    .decays_to(["p", "K-"])
1643                    .mass_range_mev(0.0, 2000.0),
1644            )
1645            .unwrap();
1646
1647        assert!(
1648            sigma_modes
1649                .iter()
1650                .any(|particle| particle.name == "Lambda(1520)0")
1651        );
1652        assert!(
1653            sigma_modes
1654                .iter()
1655                .any(|particle| particle.name == "Sigma(1385)0")
1656        );
1657        assert!(
1658            !sigma_modes
1659                .iter()
1660                .any(|particle| particle.name == "Xi_b()-")
1661        );
1662    }
1663
1664    #[test]
1665    fn decay_contains_allows_extra_final_states() {
1666        let db = test_pdg();
1667        let particles = db
1668            .search_particles(
1669                ParticleSearchQuery::new()
1670                    .class(ParticleClass::Baryon)
1671                    .decay_contains(["p", "K-"]),
1672            )
1673            .unwrap();
1674
1675        assert!(particles.iter().any(|particle| particle.name == "Xi_b()-"));
1676    }
1677
1678    #[test]
1679    fn literal_exact_decay_search_does_not_expand_state_names() {
1680        let db = test_pdg();
1681        let particles = db
1682            .search_particles(
1683                ParticleSearchQuery::new()
1684                    .class(ParticleClass::Baryon)
1685                    .decays_to(["p", "K-"])
1686                    .decay_state_expansion(DecayStateExpansion::Literal),
1687            )
1688            .unwrap();
1689
1690        assert!(
1691            !particles
1692                .iter()
1693                .any(|particle| particle.name == "Lambda(1520)0")
1694        );
1695    }
1696
1697    #[test]
1698    fn searches_by_decay_initial_and_final_states() {
1699        let db = test_pdg();
1700        let pion_modes = db
1701            .search_particles(
1702                ParticleSearchQuery::new()
1703                    .decays_from(["pi+"])
1704                    .decays_to(["mu+", "nu_mu"]),
1705            )
1706            .unwrap();
1707
1708        assert!(pion_modes.iter().any(|particle| particle.name == "pi+"));
1709    }
1710
1711    #[test]
1712    fn searches_decay_states_using_item_expansion() {
1713        let db = test_pdg();
1714        let kaon_modes = db
1715            .search_particles(
1716                ParticleSearchQuery::new()
1717                    .decays_from(["K+"])
1718                    .decay_contains(["pi"]),
1719            )
1720            .unwrap();
1721
1722        assert!(kaon_modes.iter().any(|particle| particle.name == "K+"));
1723    }
1724
1725    #[test]
1726    fn neutral_kaon_exact_decay_search_includes_kaon_family_modes() {
1727        let db = test_pdg();
1728        let particles = db
1729            .search_particles(
1730                ParticleSearchQuery::new()
1731                    .class(ParticleClass::Meson)
1732                    .decays_to(["K(S)0", "K(S)0"]),
1733            )
1734            .unwrap();
1735
1736        assert!(
1737            particles
1738                .iter()
1739                .any(|particle| particle.name == "f_0(980)0")
1740        );
1741        assert!(
1742            particles
1743                .iter()
1744                .any(|particle| particle.name == "a_0(980)0")
1745        );
1746        assert!(
1747            particles
1748                .iter()
1749                .any(|particle| particle.name == "f_2^'(1525)0")
1750        );
1751        assert!(
1752            particles
1753                .iter()
1754                .any(|particle| particle.name == "a_2(1320)0")
1755        );
1756        assert!(
1757            particles
1758                .iter()
1759                .any(|particle| particle.name == "a_0(1710)0")
1760        );
1761        assert!(
1762            particles
1763                .iter()
1764                .any(|particle| particle.name == "f_2(1910)0")
1765        );
1766    }
1767
1768    #[test]
1769    fn literal_neutral_kaon_exact_decay_search_does_not_expand_family_modes() {
1770        let db = test_pdg();
1771        let particles = db
1772            .search_particles(
1773                ParticleSearchQuery::new()
1774                    .class(ParticleClass::Meson)
1775                    .decays_to(["K(S)0", "K(S)0"])
1776                    .decay_state_expansion(DecayStateExpansion::Literal),
1777            )
1778            .unwrap();
1779
1780        assert!(
1781            !particles
1782                .iter()
1783                .any(|particle| particle.name == "f_0(980)0")
1784        );
1785        assert!(
1786            !particles
1787                .iter()
1788                .any(|particle| particle.name == "a_0(980)0")
1789        );
1790    }
1791
1792    #[test]
1793    fn loads_items_by_name() {
1794        let db = test_pdg();
1795        let pion_pair = db.item("pi+-").unwrap().unwrap();
1796
1797        assert_eq!(pion_pair.name, "pi+-");
1798        assert_eq!(pion_pair.item_type, PdgItemType::ChargeMultiplet);
1799        assert!(db.item("NO_SUCH_ITEM").unwrap().is_none());
1800    }
1801
1802    #[test]
1803    fn loads_item_children_with_particles() {
1804        let db = test_pdg();
1805        let pion_children = db.item_children("pi+-").unwrap();
1806
1807        assert_eq!(pion_children.len(), 2);
1808        assert_eq!(pion_children[0].item.name, "pi+");
1809        assert_eq!(pion_children[0].sort, 0);
1810        assert_eq!(pion_children[0].item.item_type, PdgItemType::Particle);
1811        assert_eq!(pion_children[0].particle.as_ref().unwrap().name, "pi+");
1812        assert_eq!(pion_children[1].item.name, "pi-");
1813        assert_eq!(pion_children[1].sort, 1);
1814
1815        let w_children = db.item_children("W").unwrap();
1816        assert_eq!(
1817            w_children
1818                .iter()
1819                .map(|child| child.item.name.as_str())
1820                .collect::<Vec<_>>(),
1821            vec!["W+", "W-"]
1822        );
1823        assert!(db.item_children("NO_SUCH_ITEM").unwrap().is_empty());
1824    }
1825
1826    #[test]
1827    fn item_exposes_own_navigation() {
1828        let db = test_pdg();
1829        let pion = db.item("pi+").unwrap().unwrap();
1830        let kaon_group = db.item("K").unwrap().unwrap();
1831
1832        assert_eq!(pion.particle().unwrap().unwrap().name, "pi+");
1833        assert!(kaon_group.particle().unwrap().is_none());
1834        assert!(
1835            pion.parents()
1836                .unwrap()
1837                .iter()
1838                .any(|item| item.name == "pi" && item.item_type == PdgItemType::Group)
1839        );
1840        assert!(
1841            kaon_group
1842                .children()
1843                .unwrap()
1844                .iter()
1845                .any(|child| child.item.name == "K(S)0")
1846        );
1847        assert!(
1848            kaon_group
1849                .related_particles()
1850                .unwrap()
1851                .iter()
1852                .any(|particle| particle.name == "K+")
1853        );
1854    }
1855
1856    #[test]
1857    fn particle_exposes_item_context() {
1858        let db = test_pdg();
1859        let pion = db.particle("pi+").unwrap().unwrap();
1860
1861        assert_eq!(pion.item().unwrap().unwrap().name, "pi+");
1862        let parent_items = pion.parent_items().unwrap();
1863
1864        assert!(
1865            parent_items
1866                .iter()
1867                .any(|item| item.name == "pi+-" && item.item_type == PdgItemType::ChargeMultiplet)
1868        );
1869        assert!(
1870            parent_items
1871                .iter()
1872                .any(|item| item.name == "pi" && item.item_type == PdgItemType::Group)
1873        );
1874    }
1875
1876    #[test]
1877    fn particle_exposes_related_particles() {
1878        let db = test_pdg();
1879        let pion = db.particle("pi+").unwrap().unwrap();
1880        let related_particles = pion.related_particles().unwrap();
1881
1882        assert!(
1883            related_particles
1884                .iter()
1885                .any(|particle| particle.name == "pi-")
1886        );
1887        assert!(
1888            related_particles
1889                .iter()
1890                .any(|particle| particle.name == "pi0")
1891        );
1892        assert!(
1893            !related_particles
1894                .iter()
1895                .any(|particle| particle.name == "pi+")
1896        );
1897    }
1898
1899    #[test]
1900    fn loads_texts_for_data_entries() {
1901        let db = test_pdg();
1902        let texts = db.texts_for("S008M").unwrap();
1903
1904        assert_eq!(texts.len(), 1);
1905        assert_eq!(texts[0].pdgid, "S008M");
1906        assert_eq!(texts[0].text_type, "h");
1907        assert_eq!(texts[0].sort, 1);
1908        assert!(
1909            texts[0]
1910                .text
1911                .as_ref()
1912                .unwrap()
1913                .contains("charged pion mass measurements")
1914        );
1915    }
1916
1917    #[test]
1918    fn loads_footnotes_for_data_entries() {
1919        let db = test_pdg();
1920        let footnotes = db.footnotes_for("S008M").unwrap();
1921
1922        assert!(footnotes.len() >= 10);
1923        assert_eq!(footnotes[0].pdgid.as_deref(), Some("S008M"));
1924        assert_eq!(footnotes[0].index, Some(1));
1925        assert!(footnotes[0].text.as_ref().unwrap().contains("DAUM 2019"));
1926    }
1927
1928    #[test]
1929    fn particle_forwards_text_and_footnote_lookups() {
1930        let db = test_pdg();
1931        let pion = db.particle("pi+").unwrap().unwrap();
1932
1933        assert!(pion.texts().unwrap().is_empty());
1934        assert!(pion.footnotes().unwrap().is_empty());
1935    }
1936
1937    #[test]
1938    fn loads_measurements_for_data_entries() {
1939        let db = test_pdg();
1940        let measurements = db.measurements_for("S008M").unwrap();
1941        let first = measurements.first().unwrap();
1942        let first_value = first.values.first().unwrap();
1943        let first_footnote = first.footnotes.first().unwrap();
1944
1945        assert_eq!(first.pdgid, "S008M");
1946        assert_eq!(first.reference.document_id.trim(), "DAUM 2019");
1947        assert_eq!(first.reference.publication_year, Some(2019));
1948        assert_eq!(
1949            first.reference.doi.as_deref(),
1950            Some("10.1016/j.physletb.2019.07.027")
1951        );
1952        assert!(
1953            first
1954                .reference
1955                .title
1956                .as_ref()
1957                .unwrap()
1958                .contains("charged and neutral pion masses")
1959        );
1960        assert_eq!(first_value.column_name.as_deref(), Some("VALUE"));
1961        assert_eq!(
1962            first_value.display_value_text.as_deref(),
1963            Some("139.57021 +-0.00014")
1964        );
1965        assert_eq!(first_value.unit_text.as_deref(), Some("MeV"));
1966        assert!(first_value.used_in_average);
1967        assert!(first_value.used_in_fit);
1968        assert_eq!(first_footnote.pdgid.as_deref(), Some("S008M"));
1969        assert_eq!(first_footnote.index, Some(1));
1970        assert!(first_footnote.text.as_ref().unwrap().contains("DAUM 2019"));
1971        assert!(!first_footnote.changebar);
1972    }
1973
1974    #[test]
1975    fn particle_loads_measurements_for_data_type() {
1976        let db = test_pdg();
1977        let pion = db.particle("pi+").unwrap().unwrap();
1978        let particle_measurements = pion.measurements_for(DataType::Mass).unwrap();
1979        let direct_measurements = db.measurements_for("S008M").unwrap();
1980
1981        assert_eq!(particle_measurements.len(), direct_measurements.len());
1982        assert_eq!(
1983            particle_measurements[0].reference.document_id,
1984            direct_measurements[0].reference.document_id
1985        );
1986    }
1987
1988    #[test]
1989    fn missing_measurements_return_empty_vec() {
1990        let db = test_pdg();
1991
1992        assert!(db.measurements_for("NO_SUCH_PDGID").unwrap().is_empty());
1993    }
1994
1995    #[test]
1996    fn lifetime_uses_lifetime_data_type() {
1997        let db = test_pdg();
1998        let pion = db.particle("pi+").unwrap().unwrap();
1999
2000        let mass = pion.mass().unwrap().unwrap();
2001        let lifetime = pion.lifetime().unwrap().unwrap();
2002
2003        assert!(mass.value.unwrap() > 100.0);
2004        assert!(lifetime.value.unwrap() < 0.000_001);
2005    }
2006
2007    #[test]
2008    fn width_uses_full_width_data_type() {
2009        let db = test_pdg();
2010        let z_boson = db.particle("Z0").unwrap().unwrap();
2011
2012        let width = z_boson.width().unwrap().unwrap();
2013
2014        assert!(width.value.unwrap() > 2.0);
2015        assert_eq!(width.unit_text, "GeV");
2016        assert_eq!(width.pdgid, "S044W");
2017    }
2018
2019    #[test]
2020    fn properties_fall_back_to_section_children() {
2021        let db = test_pdg();
2022        let a0 = db.particle("a_0(980)0").unwrap().unwrap();
2023
2024        let mass = a0.mass().unwrap().unwrap();
2025        let width = a0.width().unwrap().unwrap();
2026        let mass_property = a0.property(DataType::Mass).unwrap().unwrap();
2027
2028        assert_eq!(mass.pdgid, "M036MX");
2029        assert_eq!(mass.to_string(), "980+-20 MeV");
2030        assert_eq!(width.pdgid, "M036W1");
2031        assert_eq!(width.to_string(), "50 to 100 MeV");
2032        assert!(matches!(
2033            mass_property.source,
2034            PropertySource::Section { ref section_pdgid } if section_pdgid == "M036205"
2035        ));
2036    }
2037
2038    #[test]
2039    fn exclusive_branching_fractions_include_decay_products() {
2040        let db = test_pdg();
2041        let pion = db.particle("pi+").unwrap().unwrap();
2042
2043        let branching_fractions = pion.exclusive_branching_fractions().unwrap();
2044        let muon_mode = branching_fractions
2045            .iter()
2046            .find(|branching_fraction| branching_fraction.pdgid == "S008.1")
2047            .unwrap();
2048
2049        assert_eq!(muon_mode.description, "pi+ --> mu+ nu_mu");
2050        assert_eq!(muon_mode.mode_number, Some(1));
2051        assert_eq!(muon_mode.kind, BranchingFractionKind::Exclusive);
2052        assert_eq!(muon_mode.products.len(), 3);
2053        assert_eq!(muon_mode.products[0].name, "pi+");
2054        assert!(!muon_mode.products[0].is_outgoing);
2055        assert_eq!(muon_mode.products[1].name, "mu+");
2056        assert!(muon_mode.products[1].is_outgoing);
2057
2058        let muon_product = &muon_mode.products[1];
2059        assert_eq!(muon_product.item().unwrap().unwrap().name, "mu+");
2060        assert_eq!(muon_product.particle().unwrap().unwrap().name, "mu+");
2061        assert!(
2062            muon_product
2063                .parents()
2064                .unwrap()
2065                .iter()
2066                .any(|item| item.name == "mu")
2067        );
2068        assert_eq!(
2069            muon_mode.measurements().unwrap().len(),
2070            db.measurements_for(muon_mode.pdgid.clone()).unwrap().len()
2071        );
2072        assert_eq!(
2073            muon_mode.footnotes().unwrap().len(),
2074            db.footnotes_for(muon_mode.pdgid.clone()).unwrap().len()
2075        );
2076        assert_eq!(
2077            muon_mode.texts().unwrap().len(),
2078            db.texts_for(muon_mode.pdgid.clone()).unwrap().len()
2079        );
2080    }
2081
2082    #[test]
2083    fn branching_fractions_include_related_ratios() {
2084        let db = test_pdg();
2085        let pion = db.particle("pi+").unwrap().unwrap();
2086
2087        let branching_fractions = pion.exclusive_branching_fractions().unwrap();
2088        let muon_mode = branching_fractions
2089            .iter()
2090            .find(|branching_fraction| branching_fraction.pdgid == "S008.1")
2091            .unwrap();
2092        let related_ratio = muon_mode
2093            .related_data
2094            .iter()
2095            .find(|related_data| related_data.pdgid == "S008R10")
2096            .unwrap();
2097
2098        assert_eq!(related_ratio.data_type, DataType::BranchingRatio);
2099        assert!(related_ratio.description.contains("G(pi+ --> e+ nu_e)"));
2100        assert!(related_ratio.value.value.unwrap() > 0.0);
2101        assert!(!related_ratio.value.display_value_text.is_empty());
2102
2103        assert_eq!(
2104            related_ratio.measurements().unwrap().len(),
2105            db.measurements_for(related_ratio.pdgid.clone())
2106                .unwrap()
2107                .len()
2108        );
2109        assert!(!related_ratio.footnotes().unwrap().is_empty());
2110        assert!(!related_ratio.texts().unwrap().is_empty());
2111    }
2112
2113    #[test]
2114    fn branching_fractions_preserve_non_ratio_related_data() {
2115        let db = test_pdg();
2116        let kaon = db.particle("K+").unwrap().unwrap();
2117
2118        let branching_fractions = kaon.exclusive_branching_fractions().unwrap();
2119        let muon_mode = branching_fractions
2120            .iter()
2121            .find(|branching_fraction| branching_fraction.pdgid == "S010.1")
2122            .unwrap();
2123
2124        assert!(
2125            muon_mode
2126                .related_data
2127                .iter()
2128                .any(|related_data| related_data.pdgid == "S010T"
2129                    && related_data.data_type == DataType::Lifetime
2130                    && related_data.description == "K+- MEAN LIFE"
2131                    && related_data.value.value.unwrap() > 0.0)
2132        );
2133    }
2134
2135    #[test]
2136    fn inclusive_and_exclusive_branching_fractions_are_grouped() {
2137        let db = test_pdg();
2138        let b0 = db.particle("B0").unwrap().unwrap();
2139
2140        let inclusive = b0.inclusive_branching_fractions().unwrap();
2141        let exclusive = b0.exclusive_branching_fractions().unwrap();
2142        let all = b0.branching_fractions().unwrap();
2143
2144        assert!(inclusive.iter().any(|mode| mode.pdgid == "S042.94"));
2145        assert!(exclusive.iter().any(|mode| mode.pdgid == "S042.30"));
2146        assert_eq!(all.len(), inclusive.len() + exclusive.len());
2147        let inclusive_position = all.iter().position(|mode| mode.pdgid == "S042.94").unwrap();
2148        let exclusive_position = all.iter().position(|mode| mode.pdgid == "S042.30").unwrap();
2149
2150        assert!(inclusive_position < exclusive_position);
2151    }
2152
2153    #[test]
2154    fn branching_ratios_include_descriptions() {
2155        let db = test_pdg();
2156        let pion = db.particle("pi+").unwrap().unwrap();
2157
2158        let branching_ratios = pion.branching_ratios().unwrap();
2159        let ratio = branching_ratios
2160            .iter()
2161            .find(|ratio| ratio.pdgid == "S008R2")
2162            .unwrap();
2163
2164        assert_eq!(ratio.description, "G(pi+ --> e+ nu_e)/G(total)");
2165        assert!(ratio.value.value.unwrap() > 0.0);
2166        assert_eq!(
2167            ratio.measurements().unwrap().len(),
2168            db.measurements_for(ratio.pdgid.clone()).unwrap().len()
2169        );
2170        assert!(!ratio.texts().unwrap().is_empty());
2171    }
2172
2173    #[test]
2174    fn branching_ratios_include_errors() {
2175        let db = test_pdg();
2176        let sigma = db
2177            .search_particles(ParticleSearchQuery::new().name_contains("Sigma(2010)"))
2178            .unwrap()
2179            .remove(0);
2180
2181        let branching_ratios = sigma.branching_ratios().unwrap();
2182        let ratio = branching_ratios
2183            .iter()
2184            .find(|ratio| ratio.pdgid == "B002R1")
2185            .unwrap();
2186
2187        assert_eq!(ratio.value.error_positive, Some(0.03));
2188        assert_eq!(ratio.value.error_negative, Some(0.03));
2189    }
2190
2191    #[test]
2192    fn mass_includes_confidence_level_and_limit_type() {
2193        let db = test_pdg();
2194        let down_quark = db.particle("d").unwrap().unwrap();
2195        let mass = down_quark.mass().unwrap().unwrap();
2196
2197        assert_eq!(mass.confidence_level, Some(90.0));
2198
2199        let n1895 = db.particle("N(1895)0").unwrap().unwrap();
2200        let mass_limit = n1895.mass().unwrap().unwrap();
2201
2202        assert_eq!(mass_limit.limit_type, Some(LimitType::Range));
2203    }
2204
2205    #[test]
2206    fn lifetime_includes_confidence_level_and_limit_type() {
2207        let db = test_pdg();
2208        let proton = db.particle("p").unwrap().unwrap();
2209        let lifetime = proton.lifetime().unwrap().unwrap();
2210
2211        assert_eq!(lifetime.confidence_level, Some(90.0));
2212        assert_eq!(lifetime.limit_type, Some(LimitType::LowerLimit));
2213    }
2214
2215    #[test]
2216    fn width_preserves_confidence_level_and_limit_type() {
2217        let db = test_pdg();
2218        let d_star = db.particle("D^*(2007)0").unwrap().unwrap();
2219        let width = d_star.width().unwrap().unwrap();
2220
2221        assert_eq!(width.confidence_level, Some(90.0));
2222        assert_eq!(width.limit_type, Some(LimitType::UpperLimit));
2223    }
2224
2225    #[test]
2226    fn data_entry_loads_measurements_from_own_pdgid() {
2227        let db = test_pdg();
2228        let pion = db.particle("pi+").unwrap().unwrap();
2229        let mass = pion.mass().unwrap().unwrap();
2230
2231        let entry_measurements = mass.measurements().unwrap();
2232        let direct_measurements = db.measurements_for(mass.pdgid).unwrap();
2233
2234        assert_eq!(entry_measurements.len(), direct_measurements.len());
2235        assert_eq!(
2236            entry_measurements[0].reference.document_id,
2237            direct_measurements[0].reference.document_id
2238        );
2239    }
2240
2241    #[test]
2242    fn data_entry_displays_database_display_fields() {
2243        let db = test_pdg();
2244        let z_boson = db.particle("Z0").unwrap().unwrap();
2245        let pion = db.particle("pi+").unwrap().unwrap();
2246        let d_star = db.particle("D^*(2007)0").unwrap().unwrap();
2247        let muon = db.particle("mu+").unwrap().unwrap();
2248
2249        assert_eq!(
2250            z_boson.width().unwrap().unwrap().to_string(),
2251            "2.4955+-0.0023 GeV"
2252        );
2253        assert_eq!(
2254            pion.exclusive_branching_fractions()
2255                .unwrap()
2256                .into_iter()
2257                .find(|mode| mode.pdgid == "S008.1")
2258                .unwrap()
2259                .value
2260                .to_string(),
2261            "99.98770+-0.00004%"
2262        );
2263        assert_eq!(d_star.width().unwrap().unwrap().to_string(), "<2.1 MeV");
2264        assert_eq!(
2265            muon.lifetime().unwrap().unwrap().to_string(),
2266            "2.1969811+-0.0000022E-6 s"
2267        );
2268    }
2269
2270    #[test]
2271    fn upper_limit_branching_fractions_preserve_limit_type() {
2272        let db = test_pdg();
2273        let pion = db.particle("pi+").unwrap().unwrap();
2274
2275        let branching_fractions = pion.exclusive_branching_fractions().unwrap();
2276        let limit_mode = branching_fractions
2277            .iter()
2278            .find(|branching_fraction| branching_fraction.pdgid == "S008.10")
2279            .unwrap();
2280
2281        assert_eq!(limit_mode.value.limit_type, Some(LimitType::UpperLimit));
2282        assert_eq!(limit_mode.value.error_positive, Some(0.0));
2283        assert_eq!(limit_mode.value.error_negative, Some(0.0));
2284    }
2285}