1use std::fmt::Display;
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6 quantum::types::Statistics, AngularMomentum, Charge, LadduError, LadduResult,
7 OrbitalAngularMomentum, Parity, Projection,
8};
9
10#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
12pub struct SpinState {
13 spin: AngularMomentum,
14 projection: Projection,
15}
16
17impl SpinState {
18 pub fn new(spin: AngularMomentum, projection: Projection) -> LadduResult<Self> {
20 validate_projection(spin, projection)?;
21 Ok(Self { spin, projection })
22 }
23
24 pub const fn spin(self) -> AngularMomentum {
26 self.spin
27 }
28
29 pub const fn projection(self) -> Projection {
31 self.projection
32 }
33
34 pub fn allowed_projections(spin: AngularMomentum) -> Vec<Self> {
36 let spin_value = spin.value() as i32;
37 (-spin_value..=spin_value)
38 .step_by(2)
39 .map(|projection| Self {
40 spin,
41 projection: Projection::half_integer(projection),
42 })
43 .collect()
44 }
45}
46
47#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
49pub struct Isospin {
50 pub isospin: AngularMomentum,
52 pub projection: Option<Projection>,
54}
55
56impl Isospin {
57 pub fn new(isospin: AngularMomentum, projection: Option<Projection>) -> LadduResult<Self> {
59 if let Some(projection) = projection {
60 validate_projection(isospin, projection)?;
61 }
62 Ok(Self {
63 isospin,
64 projection,
65 })
66 }
67 pub fn isospin(self) -> AngularMomentum {
69 self.isospin
70 }
71 pub fn projection(self) -> LadduResult<Projection> {
75 self.projection
76 .ok_or_else(|| LadduError::MissingParticleProperty {
77 property: "isospin.projection",
78 })
79 }
80}
81
82#[derive(Clone, Debug, Default, Serialize, Deserialize)]
84pub struct ParticleProperties {
85 pub name: Option<String>,
87 pub species: Option<String>,
89 pub antiparticle_species: Option<String>,
91 pub self_conjugate: Option<bool>,
93 pub spin: Option<AngularMomentum>,
95 pub parity: Option<Parity>,
97 pub c_parity: Option<Parity>,
99 pub g_parity: Option<Parity>,
101 pub charge: Option<Charge>,
103 pub isospin: Option<Isospin>,
105 pub strangeness: Option<i32>,
107 pub charm: Option<i32>,
109 pub bottomness: Option<i32>,
111 pub topness: Option<i32>,
113 pub baryon_number: Option<i32>,
115 pub electron_lepton_number: Option<i32>,
117 pub muon_lepton_number: Option<i32>,
119 pub tau_lepton_number: Option<i32>,
121 pub statistics: Option<Statistics>,
123}
124
125impl ParticleProperties {
126 pub fn name(&self) -> LadduResult<String> {
130 self.name
131 .clone()
132 .ok_or_else(|| LadduError::MissingParticleProperty { property: "name" })
133 .clone()
134 }
135 pub fn species(&self) -> LadduResult<String> {
139 self.species
140 .clone()
141 .ok_or_else(|| LadduError::MissingParticleProperty {
142 property: "species",
143 })
144 .clone()
145 }
146 pub fn antiparticle_species(&self) -> LadduResult<String> {
150 self.antiparticle_species
151 .clone()
152 .ok_or_else(|| LadduError::MissingParticleProperty {
153 property: "antiparticle_species",
154 })
155 .clone()
156 }
157 pub fn self_conjugate(&self) -> LadduResult<bool> {
161 self.self_conjugate
162 .ok_or_else(|| LadduError::MissingParticleProperty {
163 property: "self_conjugate",
164 })
165 .clone()
166 }
167 pub fn spin(&self) -> LadduResult<AngularMomentum> {
171 self.spin
172 .ok_or_else(|| LadduError::MissingParticleProperty { property: "spin" })
173 .clone()
174 }
175 pub fn parity(&self) -> LadduResult<Parity> {
179 self.parity
180 .ok_or_else(|| LadduError::MissingParticleProperty { property: "parity" })
181 .clone()
182 }
183 pub fn c_parity(&self) -> LadduResult<Parity> {
187 self.c_parity
188 .ok_or_else(|| LadduError::MissingParticleProperty {
189 property: "c_parity",
190 })
191 .clone()
192 }
193 pub fn g_parity(&self) -> LadduResult<Parity> {
197 self.g_parity
198 .ok_or_else(|| LadduError::MissingParticleProperty {
199 property: "g_parity",
200 })
201 .clone()
202 }
203 pub fn charge(&self) -> LadduResult<Charge> {
207 self.charge
208 .ok_or_else(|| LadduError::MissingParticleProperty { property: "charge" })
209 .clone()
210 }
211 pub fn isospin(&self) -> LadduResult<Isospin> {
215 self.isospin
216 .ok_or_else(|| LadduError::MissingParticleProperty {
217 property: "isospin",
218 })
219 .clone()
220 }
221 pub fn strangeness(&self) -> LadduResult<i32> {
225 self.strangeness
226 .ok_or_else(|| LadduError::MissingParticleProperty {
227 property: "strangeness",
228 })
229 .clone()
230 }
231 pub fn charm(&self) -> LadduResult<i32> {
235 self.charm
236 .ok_or_else(|| LadduError::MissingParticleProperty { property: "charm" })
237 .clone()
238 }
239 pub fn bottomness(&self) -> LadduResult<i32> {
243 self.bottomness
244 .ok_or_else(|| LadduError::MissingParticleProperty {
245 property: "bottomness",
246 })
247 .clone()
248 }
249 pub fn topness(&self) -> LadduResult<i32> {
253 self.topness
254 .ok_or_else(|| LadduError::MissingParticleProperty {
255 property: "topness",
256 })
257 .clone()
258 }
259 pub fn baryon_number(&self) -> LadduResult<i32> {
263 self.baryon_number
264 .ok_or_else(|| LadduError::MissingParticleProperty {
265 property: "baryon_number",
266 })
267 .clone()
268 }
269 pub fn electron_lepton_number(&self) -> LadduResult<i32> {
273 self.electron_lepton_number
274 .ok_or_else(|| LadduError::MissingParticleProperty {
275 property: "electron_lepton_number",
276 })
277 .clone()
278 }
279 pub fn muon_lepton_number(&self) -> LadduResult<i32> {
283 self.muon_lepton_number
284 .ok_or_else(|| LadduError::MissingParticleProperty {
285 property: "muon_lepton_number",
286 })
287 .clone()
288 }
289 pub fn tau_lepton_number(&self) -> LadduResult<i32> {
293 self.tau_lepton_number
294 .ok_or_else(|| LadduError::MissingParticleProperty {
295 property: "tau_lepton_number",
296 })
297 .clone()
298 }
299 pub fn statistics(&self) -> LadduResult<Statistics> {
303 self.statistics
304 .ok_or_else(|| LadduError::MissingParticleProperty {
305 property: "statistics",
306 })
307 .clone()
308 }
309
310 pub fn unknown() -> Self {
312 Self::default()
313 }
314
315 pub fn jp(j: AngularMomentum, p: Parity) -> Self {
317 Self {
318 spin: Some(j),
319 parity: Some(p),
320 statistics: Some(Statistics::from_spin(j)),
321 ..Self::default()
322 }
323 }
324 pub fn jpc(j: AngularMomentum, p: Parity, c: Parity) -> Self {
326 Self {
327 spin: Some(j),
328 parity: Some(p),
329 c_parity: Some(c),
330 statistics: Some(Statistics::from_spin(j)),
331 ..Self::default()
332 }
333 }
334 pub fn with_name(mut self, name: impl Into<String>) -> Self {
336 self.name = Some(name.into());
337 self
338 }
339 pub fn with_species(mut self, species: impl Into<String>) -> Self {
341 self.species = Some(species.into());
342 self
343 }
344 pub fn with_antiparticle_species(mut self, antiparticle_species: impl Into<String>) -> Self {
346 self.antiparticle_species = Some(antiparticle_species.into());
347 self
348 }
349 pub fn with_self_conjugate(mut self, value: bool) -> Self {
351 self.self_conjugate = Some(value);
352 self
353 }
354 pub fn with_spin(mut self, j: AngularMomentum) -> Self {
356 self.spin = Some(j);
357 self.statistics = Some(Statistics::from_spin(j));
358 self
359 }
360 pub fn with_parity(mut self, p: Parity) -> Self {
362 self.parity = Some(p);
363 self
364 }
365 pub fn with_c_parity(mut self, c: Parity) -> Self {
367 self.c_parity = Some(c);
368 self
369 }
370 pub fn with_g_parity(mut self, g: Parity) -> Self {
372 self.g_parity = Some(g);
373 self
374 }
375 pub fn with_charge(mut self, q: Charge) -> Self {
377 self.charge = Some(q);
378 self
379 }
380 pub fn with_isospin(mut self, isospin: Isospin) -> Self {
382 self.isospin = Some(isospin);
383 self
384 }
385 pub fn with_strangeness(mut self, s: i32) -> Self {
387 self.strangeness = Some(s);
388 self
389 }
390 pub fn with_charm(mut self, c: i32) -> Self {
392 self.charm = Some(c);
393 self
394 }
395 pub fn with_bottomness(mut self, b: i32) -> Self {
397 self.bottomness = Some(b);
398 self
399 }
400 pub fn with_topness(mut self, t: i32) -> Self {
402 self.topness = Some(t);
403 self
404 }
405 pub fn with_baryon_number(mut self, b: i32) -> Self {
407 self.baryon_number = Some(b);
408 self
409 }
410 pub fn with_electron_lepton_number(mut self, e: i32) -> Self {
412 self.electron_lepton_number = Some(e);
413 self
414 }
415 pub fn with_muon_lepton_number(mut self, m: i32) -> Self {
417 self.muon_lepton_number = Some(m);
418 self
419 }
420 pub fn with_tau_lepton_number(mut self, t: i32) -> Self {
422 self.tau_lepton_number = Some(t);
423 self
424 }
425 pub fn with_statistics(mut self, s: Statistics) -> LadduResult<Self> {
429 if let Some(spin) = self.spin {
430 if Statistics::from_spin(spin) != s {
431 return Err(LadduError::Custom(
432 "spin and statistics must be consistent".to_string(),
433 ));
434 }
435 }
436 self.statistics = Some(s);
437 Ok(self)
438 }
439
440 pub fn is_antiparticle_of(&self, other: &ParticleProperties) -> bool {
442 let a_species = self.species.as_ref();
443 let b_species = other.species.as_ref();
444
445 let a_anti = self.antiparticle_species.as_ref();
446 let b_anti = other.antiparticle_species.as_ref();
447
448 match (a_species, b_species, a_anti, b_anti) {
449 (Some(a), Some(b), Some(a_bar), Some(b_bar)) => a_bar == b && b_bar == a,
450 (Some(_), Some(b), Some(a_bar), None) => a_bar == b,
451 (Some(a), Some(_), None, Some(b_bar)) => b_bar == a,
452 _ => false,
453 }
454 }
455}
456
457#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
460pub struct PartialWave {
461 pub j: AngularMomentum,
463 pub l: OrbitalAngularMomentum,
465 pub s: AngularMomentum,
467 pub label: String,
469}
470impl PartialWave {
471 pub fn new(
473 j: AngularMomentum,
474 l: OrbitalAngularMomentum,
475 s: AngularMomentum,
476 ) -> LadduResult<Self> {
477 PartialWave::validate_coupling(j, l, s)?;
478 let multiplicity = s.value() + 1;
479 Ok(Self {
480 j,
481 l,
482 s,
483 label: format!("{}{}{}", multiplicity, l, j),
484 })
485 }
486 pub fn with_label(mut self, label: impl Into<String>) -> Self {
488 self.label = label.into();
489 self
490 }
491 pub fn validate_coupling(
493 j: AngularMomentum,
494 l: OrbitalAngularMomentum,
495 s: AngularMomentum,
496 ) -> LadduResult<()> {
497 let l_twice = 2 * l.value();
498 let s_twice = s.value();
499 let j_twice = j.value();
500 let min = l_twice.abs_diff(s_twice);
501 let max = l_twice + s_twice;
502 if j_twice >= min && j_twice <= max && (j_twice - min).is_multiple_of(2) {
503 Ok(())
504 } else {
505 Err(LadduError::Custom(
506 "j, l, and s must be compatible".to_string(),
507 ))
508 }
509 }
510}
511
512impl Display for PartialWave {
513 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
514 write!(f, "{}", self.label)
515 }
516}
517
518#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
520pub struct AllowedPartialWave {
521 pub wave: PartialWave,
523 pub parity: Option<Parity>,
525 pub c_parity: Option<Parity>,
527}
528
529impl AllowedPartialWave {
530 pub fn new(wave: PartialWave, daughters: (&ParticleProperties, &ParticleProperties)) -> Self {
532 Self {
533 parity: Self::infer_parity(daughters, wave.l),
534 c_parity: Self::infer_c_parity(daughters, wave.l, wave.s),
535 wave,
536 }
537 }
538
539 pub fn infer_parity(
541 daughters: (&ParticleProperties, &ParticleProperties),
542 l: OrbitalAngularMomentum,
543 ) -> Option<Parity> {
544 let p_a = daughters.0.parity?;
545 let p_b = daughters.1.parity?;
546
547 let value = p_a.value() * p_b.value() * if l.value() & 1 == 0 { 1 } else { -1 };
548
549 Some(if value == 1 {
550 Parity::Positive
551 } else {
552 Parity::Negative
553 })
554 }
555
556 pub fn infer_c_parity(
558 daughters: (&ParticleProperties, &ParticleProperties),
559 l: OrbitalAngularMomentum,
560 s: AngularMomentum,
561 ) -> Option<Parity> {
562 if !daughters.0.is_antiparticle_of(daughters.1) {
563 return None;
564 }
565
566 let exp_twice = 2 * l.value() + s.value();
567
568 if !exp_twice.is_multiple_of(2) {
569 return None;
570 }
571
572 Some(if (exp_twice / 2).is_multiple_of(2) {
573 Parity::Positive
574 } else {
575 Parity::Negative
576 })
577 }
578}
579
580fn validate_projection(spin: AngularMomentum, projection: Projection) -> LadduResult<()> {
581 if projection.value().unsigned_abs() > spin.value() {
582 return Err(LadduError::Custom(
583 "spin projection must satisfy -J <= m <= J".to_string(),
584 ));
585 }
586 if !spin.has_same_parity_as(projection) {
587 return Err(LadduError::Custom(
588 "spin projection must have the same integer or half-integer parity as spin".to_string(),
589 ));
590 }
591 Ok(())
592}