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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
18pub enum ParticleType {
19 Particle,
21 Antiparticle,
23 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 #[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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
73pub enum ParticleClass {
74 GaugeBoson,
76 Lepton,
78 Quark,
80 Meson,
82 Baryon,
84}
85
86impl ParticleClass {
87 #[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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
138pub enum Charge {
139 PlusPlus,
141 Plus,
143 Neutral,
145 Minus,
147 MinusMinus,
149 PlusOneThird,
151 PlusTwoThirds,
153 MinusOneThird,
155 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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
223pub enum Isospin {
224 I0,
226 I1,
228 I2,
230 I3,
232 Photon,
234 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#[derive(Clone, Debug, PartialEq, Eq)]
278pub enum AngularMomentum {
279 J0,
281 J1,
283 J2,
285 J3,
287 J4,
289 J5,
291 J6,
293 J7,
295 J8,
297 J9,
299 J10,
301 J11,
303 J12,
305 J13,
307 J14,
309 J15,
311 Custom(String),
313 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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
380pub enum Parity {
381 Plus,
383 Minus,
385 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#[derive(Debug, Clone)]
423pub struct PdgParticle<'pdg> {
424 pub(crate) db: &'pdg Pdg,
425 pub pdgid: PdgId,
427 pub name: String,
429 pub description: String,
431 pub particle_type: ParticleType,
433 pub particle_class: ParticleClass,
435 pub mcid: Option<isize>,
437 pub charge: Charge,
439 pub quantum_i: Option<Isospin>,
441 pub quantum_g: Option<Parity>,
443 pub quantum_j: Option<AngularMomentum>,
445 pub quantum_p: Option<Parity>,
447 pub quantum_c: Option<Parity>,
449}
450
451#[derive(Clone, Debug)]
453pub struct ParticleProperty<'pdg> {
454 pub data_type: DataType,
456 pub value: DataEntry<'pdg>,
458 pub source: PropertySource,
460}
461
462#[derive(Clone, Debug)]
464pub enum PropertySource {
465 Direct,
467 Section {
469 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 pub fn direct_property(&self, data_type: DataType) -> PdgResult<Option<DataEntry<'pdg>>> {
545 self.query(data_type, LATEST_EDITION)
546 }
547
548 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(§ion.pdgid)? {
567 if child.data_type == data_type {
568 return self.section_property(§ion, &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 #[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 pub fn texts(&self) -> PdgResult<Vec<PdgText>> {
614 self.db.texts_for(&self.pdgid)
615 }
616
617 pub fn footnotes(&self) -> PdgResult<Vec<PdgFootnote>> {
623 self.db.footnotes_for(&self.pdgid)
624 }
625
626 pub fn item(&self) -> PdgResult<Option<PdgItem<'pdg>>> {
632 self.db.item(&self.name)
633 }
634
635 pub fn item_children(&self) -> PdgResult<Vec<crate::PdgItemChild<'pdg>>> {
641 self.db.item_children(&self.name)
642 }
643
644 pub fn parent_items(&self) -> PdgResult<Vec<PdgItem<'pdg>>> {
650 self.db.item_parents(&self.name)
651 }
652
653 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 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 pub fn mass(&self) -> PdgResult<Option<DataEntry<'pdg>>> {
695 Ok(self
696 .property(DataType::Mass)?
697 .map(|property| property.value))
698 }
699 pub fn lifetime(&self) -> PdgResult<Option<DataEntry<'pdg>>> {
705 Ok(self
706 .property(DataType::Lifetime)?
707 .map(|property| property.value))
708 }
709 pub fn width(&self) -> PdgResult<Option<DataEntry<'pdg>>> {
715 Ok(self
716 .property(DataType::FullWidth)?
717 .map(|property| property.value))
718 }
719 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1132pub enum BranchingFractionKind {
1133 Exclusive,
1135 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#[derive(Debug, Clone)]
1150pub struct DecayProduct<'pdg> {
1151 pub(crate) db: &'pdg Pdg,
1152 pub name: String,
1154 pub is_outgoing: bool,
1156 pub multiplier: i64,
1158}
1159
1160impl<'pdg> DecayProduct<'pdg> {
1161 pub fn item(&self) -> PdgResult<Option<PdgItem<'pdg>>> {
1167 self.db.item(&self.name)
1168 }
1169
1170 pub fn particle(&self) -> PdgResult<Option<PdgParticle<'pdg>>> {
1176 self.db.particle(&self.name)
1177 }
1178
1179 pub fn children(&self) -> PdgResult<Vec<crate::PdgItemChild<'pdg>>> {
1185 self.db.item_children(&self.name)
1186 }
1187
1188 pub fn parents(&self) -> PdgResult<Vec<PdgItem<'pdg>>> {
1194 self.db.item_parents(&self.name)
1195 }
1196}
1197
1198#[derive(Debug, Clone)]
1200pub struct BranchingFraction<'pdg> {
1201 pub pdgid: PdgId,
1203 pub description: String,
1205 pub mode_number: Option<u32>,
1207 pub value: DataEntry<'pdg>,
1209 pub kind: BranchingFractionKind,
1211 pub products: Vec<DecayProduct<'pdg>>,
1213 pub related_data: Vec<RelatedDataEntry<'pdg>>,
1215}
1216
1217impl BranchingFraction<'_> {
1218 pub fn measurements(&self) -> PdgResult<Vec<PdgMeasurement>> {
1224 self.value.measurements()
1225 }
1226
1227 pub fn footnotes(&self) -> PdgResult<Vec<PdgFootnote>> {
1233 self.value.footnotes()
1234 }
1235
1236 pub fn texts(&self) -> PdgResult<Vec<PdgText>> {
1242 self.value.texts()
1243 }
1244}
1245
1246#[derive(Debug, Clone)]
1248pub struct RelatedDataEntry<'pdg> {
1249 pub pdgid: PdgId,
1251 pub description: String,
1253 pub data_type: DataType,
1255 pub mode_number: Option<u32>,
1257 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 pub fn measurements(&self) -> PdgResult<Vec<PdgMeasurement>> {
1279 self.value.measurements()
1280 }
1281
1282 pub fn footnotes(&self) -> PdgResult<Vec<PdgFootnote>> {
1288 self.value.footnotes()
1289 }
1290
1291 pub fn texts(&self) -> PdgResult<Vec<PdgText>> {
1297 self.value.texts()
1298 }
1299}
1300
1301#[derive(Debug, Clone)]
1303pub struct BranchingRatio<'pdg> {
1304 pub pdgid: PdgId,
1306 pub description: String,
1308 pub mode_number: Option<u32>,
1310 pub value: DataEntry<'pdg>,
1312}
1313
1314impl BranchingRatio<'_> {
1315 pub fn measurements(&self) -> PdgResult<Vec<PdgMeasurement>> {
1321 self.value.measurements()
1322 }
1323
1324 pub fn footnotes(&self) -> PdgResult<Vec<PdgFootnote>> {
1330 self.value.footnotes()
1331 }
1332
1333 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}