1pub mod angular_momentum {
2 use laddu_core::{
3 allowed_projections, AngularMomentum, LadduError, LadduResult, OrbitalAngularMomentum,
4 Projection,
5 };
6 use num::rational::Ratio;
7 use pyo3::{
8 prelude::*,
9 types::{PyAny, PyBool, PyModule},
10 IntoPyObjectExt,
11 };
12 type PyQuantumNumber = Py<PyAny>;
13
14 pub fn parse_angular_momentum(input: &Bound<'_, PyAny>) -> PyResult<AngularMomentum> {
15 Ok(parse_ratio_like(input).and_then(AngularMomentum::try_from)?)
16 }
17
18 fn parse_ratio_like(input: &Bound<'_, PyAny>) -> LadduResult<Ratio<i32>> {
19 if input.is_instance_of::<PyBool>() {
20 return Err(LadduError::Custom(
21 "quantum number cannot be a bool".to_string(),
22 ));
23 }
24 if let Ok(value) = input.extract::<i32>() {
25 return Ok(Ratio::from_integer(value));
26 }
27 if let Ok(value) = input.extract::<f64>() {
28 let twice = Projection::try_from(value)?.value();
29 return Ok(Ratio::new(twice, 2));
30 }
31 let numerator = input
32 .getattr("numerator")
33 .and_then(|value| value.extract::<i32>());
34 let denominator = input
35 .getattr("denominator")
36 .and_then(|value| value.extract::<i32>());
37 if let (Ok(numerator), Ok(denominator)) = (numerator, denominator) {
38 if denominator == 0 {
39 return Err(LadduError::Custom(
40 "quantum number denominator cannot be zero".to_string(),
41 ));
42 }
43 return Ok(Ratio::new(numerator, denominator));
44 }
45 Err(LadduError::Custom(
46 "quantum number must be an int, float, or fractions.Fraction".to_string(),
47 ))
48 }
49
50 pub fn parse_projection(input: &Bound<'_, PyAny>) -> PyResult<Projection> {
51 Ok(parse_ratio_like(input).and_then(Projection::try_from)?)
52 }
53
54 pub fn parse_orbital_angular_momentum(
55 input: &Bound<'_, PyAny>,
56 ) -> PyResult<OrbitalAngularMomentum> {
57 Ok(parse_ratio_like(input).and_then(OrbitalAngularMomentum::try_from)?)
58 }
59
60 pub fn angular_momentum_to_python(
61 py: Python<'_>,
62 angular_momentum: laddu_core::AngularMomentum,
63 ) -> PyResult<PyQuantumNumber> {
64 let twice = angular_momentum.value() as i32;
65 if twice % 2 == 0 {
66 Ok((twice / 2).into_bound_py_any(py)?.unbind())
67 } else {
68 let fractions = PyModule::import(py, "fractions")?;
69 let fraction = fractions.getattr("Fraction")?;
70 Ok(fraction.call1((twice, 2))?.unbind())
71 }
72 }
73
74 pub fn projection_to_python(
75 py: Python<'_>,
76 projection: Projection,
77 ) -> PyResult<PyQuantumNumber> {
78 let twice = projection.value();
79 if twice % 2 == 0 {
80 Ok((twice / 2).into_bound_py_any(py)?.unbind())
81 } else {
82 let fractions = PyModule::import(py, "fractions")?;
83 let fraction = fractions.getattr("Fraction")?;
84 Ok(fraction.call1((twice, 2))?.unbind())
85 }
86 }
87
88 #[pyfunction(name = "allowed_projections")]
90 pub fn py_allowed_projections(
91 py: Python<'_>,
92 spin: &Bound<'_, PyAny>,
93 ) -> PyResult<Vec<PyQuantumNumber>> {
94 allowed_projections(parse_angular_momentum(spin)?)
95 .into_iter()
96 .map(|projection| projection_to_python(py, projection))
97 .collect()
98 }
99}
100
101use laddu_core::{
102 AllowedPartialWave, Charge, Isospin, LadduError, OrbitalAngularMomentum, Parity, PartialWave,
103 ParticleProperties, RuleSet, SelectionRules, Statistics,
104};
105use pyo3::{
106 exceptions::PyTypeError,
107 prelude::*,
108 types::{PyAny, PyBool},
109 IntoPyObjectExt,
110};
111
112use self::angular_momentum::{
113 angular_momentum_to_python, parse_angular_momentum, parse_orbital_angular_momentum,
114 parse_projection, projection_to_python,
115};
116
117type PyQuantumNumber = Py<PyAny>;
118
119fn parse_parity(input: &Bound<'_, PyAny>) -> PyResult<Parity> {
120 if let Ok(value) = input.extract::<PyParity>() {
121 return Ok(value.0);
122 }
123 if let Ok(value) = input.extract::<String>() {
124 return Ok(value.parse()?);
125 }
126 Err(PyTypeError::new_err(
127 "parity must be a Parity or sign string",
128 ))
129}
130
131fn parse_statistics(input: &Bound<'_, PyAny>) -> PyResult<Statistics> {
132 if let Ok(value) = input.extract::<PyStatistics>() {
133 return Ok(value.into());
134 }
135 if let Ok(value) = input.extract::<String>() {
136 return match value.to_ascii_lowercase().as_str() {
137 "boson" | "bosonic" => Ok(Statistics::Boson),
138 "fermion" | "fermionic" => Ok(Statistics::Fermion),
139 _ => Err(LadduError::ParseError {
140 name: value,
141 object: "Statistics".to_string(),
142 }
143 .into()),
144 };
145 }
146 Err(PyTypeError::new_err(
147 "statistics must be a Statistics value or string",
148 ))
149}
150
151fn parse_charge_input(input: &Bound<'_, PyAny>) -> PyResult<Charge> {
152 if let Ok(value) = input.extract::<PyCharge>() {
153 return Ok(value.0);
154 }
155 if input.is_instance_of::<PyBool>() {
156 return Err(LadduError::Custom("electric charge cannot be a bool".to_string()).into());
157 }
158 if let Ok(value) = input.extract::<i32>() {
159 return Ok(Charge::try_from(num::rational::Ratio::from_integer(value))?);
160 }
161 let numerator = input
162 .getattr("numerator")
163 .and_then(|value| value.extract::<i32>());
164 let denominator = input
165 .getattr("denominator")
166 .and_then(|value| value.extract::<i32>());
167 if let (Ok(numerator), Ok(denominator)) = (numerator, denominator) {
168 if denominator == 0 {
169 return Err(LadduError::Custom(
170 "electric charge denominator cannot be zero".to_string(),
171 )
172 .into());
173 }
174 return Ok(Charge::try_from(num::rational::Ratio::new(
175 numerator,
176 denominator,
177 ))?);
178 }
179 if let Ok(value) = input.extract::<f64>() {
180 return Ok(Charge::try_from(value)?);
181 }
182 Err(PyTypeError::new_err(
183 "electric charge must be an int, float, fractions.Fraction, or Charge",
184 ))
185}
186
187fn charge_to_python(py: Python<'_>, charge: Charge) -> PyResult<PyQuantumNumber> {
188 let thirds = charge.value();
189 if thirds % 3 == 0 {
190 Ok((thirds / 3).into_bound_py_any(py)?.unbind())
191 } else {
192 let fractions = pyo3::types::PyModule::import(py, "fractions")?;
193 let fraction = fractions.getattr("Fraction")?;
194 Ok(fraction.call1((thirds, 3))?.unbind())
195 }
196}
197
198#[pyclass(eq, name = "Parity", module = "laddu", from_py_object)]
199#[derive(Clone, Copy, PartialEq)]
200pub struct PyParity(pub Parity);
201
202#[pymethods]
203impl PyParity {
204 #[new]
205 fn new(value: &str) -> PyResult<Self> {
206 Ok(Self(value.parse()?))
207 }
208
209 #[staticmethod]
210 fn positive() -> Self {
211 Self(Parity::Positive)
212 }
213
214 #[staticmethod]
215 fn negative() -> Self {
216 Self(Parity::Negative)
217 }
218
219 #[getter]
220 fn value(&self) -> i32 {
221 self.0.value()
222 }
223
224 fn __repr__(&self) -> String {
225 format!("Parity('{}')", self.0)
226 }
227
228 fn __str__(&self) -> String {
229 self.0.to_string()
230 }
231}
232
233#[pyclass(eq, eq_int, name = "Statistics", module = "laddu", from_py_object)]
234#[derive(Clone, Copy, PartialEq)]
235pub enum PyStatistics {
236 Boson,
237 Fermion,
238}
239
240impl From<PyStatistics> for Statistics {
241 fn from(value: PyStatistics) -> Self {
242 match value {
243 PyStatistics::Boson => Self::Boson,
244 PyStatistics::Fermion => Self::Fermion,
245 }
246 }
247}
248
249impl From<Statistics> for PyStatistics {
250 fn from(value: Statistics) -> Self {
251 match value {
252 Statistics::Boson => Self::Boson,
253 Statistics::Fermion => Self::Fermion,
254 }
255 }
256}
257
258#[pyclass(eq, name = "Charge", module = "laddu", from_py_object)]
259#[derive(Clone, Copy, PartialEq)]
260pub struct PyCharge(pub Charge);
261
262#[pymethods]
263impl PyCharge {
264 #[new]
265 fn new(value: &Bound<'_, PyAny>) -> PyResult<Self> {
266 Ok(Self(parse_charge_input(value)?))
267 }
268
269 #[getter]
270 fn value(&self, py: Python<'_>) -> PyResult<PyQuantumNumber> {
271 charge_to_python(py, self.0)
272 }
273
274 fn __repr__(&self) -> String {
275 format!("Charge({})", self.0)
276 }
277
278 fn __str__(&self) -> String {
279 self.0.to_string()
280 }
281}
282
283#[pyclass(eq, name = "Isospin", module = "laddu", from_py_object)]
284#[derive(Clone, Copy, PartialEq)]
285pub struct PyIsospin(pub Isospin);
286
287#[pymethods]
288impl PyIsospin {
289 #[new]
290 #[pyo3(signature = (isospin, *, projection=None))]
291 fn new(isospin: &Bound<'_, PyAny>, projection: Option<&Bound<'_, PyAny>>) -> PyResult<Self> {
292 Ok(Self(Isospin::new(
293 parse_angular_momentum(isospin)?,
294 projection.map(parse_projection).transpose()?,
295 )?))
296 }
297
298 #[getter]
299 fn isospin(&self, py: Python<'_>) -> PyResult<PyQuantumNumber> {
300 angular_momentum_to_python(py, self.0.isospin())
301 }
302
303 #[getter]
304 fn projection_unchecked(&self, py: Python<'_>) -> PyResult<Option<PyQuantumNumber>> {
305 self.0
306 .projection
307 .map(|projection| projection_to_python(py, projection))
308 .transpose()
309 }
310
311 #[getter]
312 fn projection(&self, py: Python<'_>) -> PyResult<PyQuantumNumber> {
313 projection_to_python(py, self.0.projection()?)
314 }
315
316 fn __repr__(&self) -> String {
317 match self.0.projection {
318 Some(projection) => format!("Isospin({}, projection={})", self.0.isospin(), projection),
319 None => format!("Isospin({})", self.0.isospin()),
320 }
321 }
322}
323
324#[pyclass(name = "ParticleProperties", module = "laddu", from_py_object)]
325#[derive(Clone)]
326pub struct PyParticleProperties(pub ParticleProperties);
327
328#[pymethods]
329impl PyParticleProperties {
330 #[new]
331 #[pyo3(signature = (name=None, *, species=None, antiparticle_species=None, self_conjugate=None, spin=None, parity=None, c_parity=None, g_parity=None, charge=None, isospin=None, strangeness=None, charm=None, bottomness=None, topness=None, baryon_number=None, electron_lepton_number=None, muon_lepton_number=None, tau_lepton_number=None, statistics=None))]
332 #[allow(clippy::too_many_arguments)]
333 fn new(
334 name: Option<String>,
335 species: Option<String>,
336 antiparticle_species: Option<String>,
337 self_conjugate: Option<bool>,
338 spin: Option<&Bound<'_, PyAny>>,
339 parity: Option<&Bound<'_, PyAny>>,
340 c_parity: Option<&Bound<'_, PyAny>>,
341 g_parity: Option<&Bound<'_, PyAny>>,
342 charge: Option<&Bound<'_, PyAny>>,
343 isospin: Option<PyIsospin>,
344 strangeness: Option<i32>,
345 charm: Option<i32>,
346 bottomness: Option<i32>,
347 topness: Option<i32>,
348 baryon_number: Option<i32>,
349 electron_lepton_number: Option<i32>,
350 muon_lepton_number: Option<i32>,
351 tau_lepton_number: Option<i32>,
352 statistics: Option<&Bound<'_, PyAny>>,
353 ) -> PyResult<Self> {
354 let mut properties = ParticleProperties::unknown();
355 if let Some(name) = name {
356 properties = properties.with_name(name);
357 }
358 if let Some(species) = species {
359 properties = properties.with_species(species);
360 }
361 if let Some(antiparticle_species) = antiparticle_species {
362 properties = properties.with_antiparticle_species(antiparticle_species);
363 }
364 if let Some(self_conjugate) = self_conjugate {
365 properties = properties.with_self_conjugate(self_conjugate);
366 }
367 if let Some(spin) = spin {
368 properties = properties.with_spin(parse_angular_momentum(spin)?);
369 }
370 if let Some(parity) = parity {
371 properties = properties.with_parity(parse_parity(parity)?);
372 }
373 if let Some(c_parity) = c_parity {
374 properties = properties.with_c_parity(parse_parity(c_parity)?);
375 }
376 if let Some(g_parity) = g_parity {
377 properties = properties.with_g_parity(parse_parity(g_parity)?);
378 }
379 if let Some(charge) = charge {
380 properties = properties.with_charge(parse_charge_input(charge)?);
381 }
382 if let Some(isospin) = isospin {
383 properties = properties.with_isospin(isospin.0);
384 }
385 if let Some(strangeness) = strangeness {
386 properties = properties.with_strangeness(strangeness);
387 }
388 if let Some(charm) = charm {
389 properties = properties.with_charm(charm);
390 }
391 if let Some(bottomness) = bottomness {
392 properties = properties.with_bottomness(bottomness);
393 }
394 if let Some(topness) = topness {
395 properties = properties.with_topness(topness);
396 }
397 if let Some(baryon_number) = baryon_number {
398 properties = properties.with_baryon_number(baryon_number);
399 }
400 if let Some(electron_lepton_number) = electron_lepton_number {
401 properties = properties.with_electron_lepton_number(electron_lepton_number);
402 }
403 if let Some(muon_lepton_number) = muon_lepton_number {
404 properties = properties.with_muon_lepton_number(muon_lepton_number);
405 }
406 if let Some(tau_lepton_number) = tau_lepton_number {
407 properties = properties.with_tau_lepton_number(tau_lepton_number);
408 }
409 if let Some(statistics) = statistics {
410 properties = properties.with_statistics(parse_statistics(statistics)?)?;
411 }
412 Ok(Self(properties))
413 }
414
415 #[getter]
416 fn name(&self) -> PyResult<String> {
417 Ok(self.0.name()?)
418 }
419
420 #[getter]
421 fn name_unchecked(&self) -> Option<String> {
422 self.0.name.clone()
423 }
424
425 #[getter]
426 fn species(&self) -> PyResult<String> {
427 Ok(self.0.species()?)
428 }
429
430 #[getter]
431 fn species_unchecked(&self) -> Option<String> {
432 self.0.species.clone()
433 }
434
435 #[getter]
436 fn antiparticle_species(&self) -> PyResult<String> {
437 Ok(self.0.antiparticle_species()?)
438 }
439
440 #[getter]
441 fn antiparticle_species_unchecked(&self) -> Option<String> {
442 self.0.antiparticle_species.clone()
443 }
444
445 #[getter]
446 fn self_conjugate(&self) -> PyResult<bool> {
447 Ok(self.0.self_conjugate()?)
448 }
449
450 #[getter]
451 fn self_conjugate_unchecked(&self) -> Option<bool> {
452 self.0.self_conjugate
453 }
454
455 #[getter]
456 fn spin(&self, py: Python<'_>) -> PyResult<PyQuantumNumber> {
457 angular_momentum_to_python(py, self.0.spin()?)
458 }
459
460 #[getter]
461 fn spin_unchecked(&self, py: Python<'_>) -> PyResult<Option<PyQuantumNumber>> {
462 self.0
463 .spin
464 .map(|spin| angular_momentum_to_python(py, spin))
465 .transpose()
466 }
467
468 #[getter]
469 fn parity(&self) -> PyResult<PyParity> {
470 Ok(PyParity(self.0.parity()?))
471 }
472
473 #[getter]
474 fn parity_unchecked(&self) -> Option<PyParity> {
475 self.0.parity.map(PyParity)
476 }
477
478 #[getter]
479 fn c_parity(&self) -> PyResult<PyParity> {
480 Ok(PyParity(self.0.c_parity()?))
481 }
482
483 #[getter]
484 fn c_parity_unchecked(&self) -> Option<PyParity> {
485 self.0.c_parity.map(PyParity)
486 }
487
488 #[getter]
489 fn g_parity(&self) -> PyResult<PyParity> {
490 Ok(PyParity(self.0.g_parity()?))
491 }
492
493 #[getter]
494 fn g_parity_unchecked(&self) -> Option<PyParity> {
495 self.0.g_parity.map(PyParity)
496 }
497
498 #[getter]
499 fn charge(&self) -> PyResult<PyCharge> {
500 Ok(PyCharge(self.0.charge()?))
501 }
502
503 #[getter]
504 fn charge_unchecked(&self) -> Option<PyCharge> {
505 self.0.charge.map(PyCharge)
506 }
507
508 #[getter]
509 fn isospin(&self) -> PyResult<PyIsospin> {
510 Ok(PyIsospin(self.0.isospin()?))
511 }
512
513 #[getter]
514 fn isospin_unchecked(&self) -> Option<PyIsospin> {
515 self.0.isospin.map(PyIsospin)
516 }
517
518 #[getter]
519 fn strangeness(&self) -> PyResult<i32> {
520 Ok(self.0.strangeness()?)
521 }
522
523 #[getter]
524 fn strangeness_unchecked(&self) -> Option<i32> {
525 self.0.strangeness
526 }
527
528 #[getter]
529 fn charm(&self) -> PyResult<i32> {
530 Ok(self.0.charm()?)
531 }
532
533 #[getter]
534 fn charm_unchecked(&self) -> Option<i32> {
535 self.0.charm
536 }
537
538 #[getter]
539 fn bottomness(&self) -> PyResult<i32> {
540 Ok(self.0.bottomness()?)
541 }
542
543 #[getter]
544 fn bottomness_unchecked(&self) -> Option<i32> {
545 self.0.bottomness
546 }
547
548 #[getter]
549 fn topness(&self) -> PyResult<i32> {
550 Ok(self.0.topness()?)
551 }
552
553 #[getter]
554 fn topness_unchecked(&self) -> Option<i32> {
555 self.0.topness
556 }
557
558 #[getter]
559 fn baryon_number(&self) -> PyResult<i32> {
560 Ok(self.0.baryon_number()?)
561 }
562
563 #[getter]
564 fn baryon_number_unchecked(&self) -> Option<i32> {
565 self.0.baryon_number
566 }
567
568 #[getter]
569 fn electron_lepton_number(&self) -> PyResult<i32> {
570 Ok(self.0.electron_lepton_number()?)
571 }
572
573 #[getter]
574 fn electron_lepton_number_unchecked(&self) -> Option<i32> {
575 self.0.electron_lepton_number
576 }
577
578 #[getter]
579 fn muon_lepton_number(&self) -> PyResult<i32> {
580 Ok(self.0.muon_lepton_number()?)
581 }
582
583 #[getter]
584 fn muon_lepton_number_unchecked(&self) -> Option<i32> {
585 self.0.muon_lepton_number
586 }
587
588 #[getter]
589 fn tau_lepton_number(&self) -> PyResult<i32> {
590 Ok(self.0.tau_lepton_number()?)
591 }
592
593 #[getter]
594 fn tau_lepton_number_unchecked(&self) -> Option<i32> {
595 self.0.tau_lepton_number
596 }
597
598 #[getter]
599 fn statistics(&self) -> PyResult<PyStatistics> {
600 Ok(self.0.statistics()?.into())
601 }
602
603 #[getter]
604 fn statistics_unchecked(&self) -> Option<PyStatistics> {
605 self.0.statistics.map(|s| s.into())
606 }
607
608 fn __repr__(&self) -> String {
609 format!("{:?}", self.0)
610 }
611}
612
613#[pyclass(eq, name = "PartialWave", module = "laddu", from_py_object)]
614#[derive(Clone, PartialEq)]
615pub struct PyPartialWave(pub PartialWave);
616
617#[pymethods]
618impl PyPartialWave {
619 #[new]
620 #[pyo3(signature = (*, j, l, s, label=None))]
621 fn new(
622 j: &Bound<'_, PyAny>,
623 l: &Bound<'_, PyAny>,
624 s: &Bound<'_, PyAny>,
625 label: Option<String>,
626 ) -> PyResult<Self> {
627 let wave = PartialWave::new(
628 parse_angular_momentum(j)?,
629 parse_orbital_angular_momentum(l)?,
630 parse_angular_momentum(s)?,
631 )?;
632 Ok(Self(match label {
633 Some(label) => wave.with_label(label),
634 None => wave,
635 }))
636 }
637
638 #[getter]
639 fn j(&self, py: Python<'_>) -> PyResult<PyQuantumNumber> {
640 angular_momentum_to_python(py, self.0.j)
641 }
642
643 #[getter]
644 fn l(&self) -> u32 {
645 self.0.l.value()
646 }
647
648 #[getter]
649 fn s(&self, py: Python<'_>) -> PyResult<PyQuantumNumber> {
650 angular_momentum_to_python(py, self.0.s)
651 }
652
653 #[getter]
654 fn label(&self) -> String {
655 self.0.label.clone()
656 }
657
658 fn __repr__(&self) -> String {
659 format!("PartialWave('{}')", self.0.label)
660 }
661
662 fn __str__(&self) -> String {
663 self.0.to_string()
664 }
665}
666
667#[pyclass(eq, name = "AllowedPartialWave", module = "laddu", from_py_object)]
668#[derive(Clone, PartialEq)]
669pub struct PyAllowedPartialWave(pub AllowedPartialWave);
670
671#[pymethods]
672impl PyAllowedPartialWave {
673 #[getter]
674 fn wave(&self) -> PyPartialWave {
675 PyPartialWave(self.0.wave.clone())
676 }
677
678 #[getter]
679 fn parity(&self) -> Option<PyParity> {
680 self.0.parity.map(PyParity)
681 }
682
683 #[getter]
684 fn c_parity(&self) -> Option<PyParity> {
685 self.0.c_parity.map(PyParity)
686 }
687
688 fn __repr__(&self) -> String {
689 format!("{:?}", self.0)
690 }
691}
692
693#[pyclass(eq, name = "RuleSet", module = "laddu", from_py_object)]
694#[derive(Clone, PartialEq)]
695pub struct PyRuleSet(pub RuleSet);
696
697#[pymethods]
698impl PyRuleSet {
699 #[new]
700 fn new() -> Self {
701 Self(RuleSet::default())
702 }
703
704 #[staticmethod]
705 fn angular() -> Self {
706 Self(RuleSet::angular())
707 }
708
709 #[staticmethod]
710 fn strong() -> Self {
711 Self(RuleSet::strong())
712 }
713
714 #[staticmethod]
715 fn electromagnetic() -> Self {
716 Self(RuleSet::electromagnetic())
717 }
718
719 #[staticmethod]
720 fn weak() -> Self {
721 Self(RuleSet::weak())
722 }
723
724 fn __repr__(&self) -> String {
725 format!("{:?}", self.0)
726 }
727}
728
729fn parse_rules(rules: Option<&Bound<'_, PyAny>>) -> PyResult<RuleSet> {
730 let Some(rules) = rules else {
731 return Ok(RuleSet::strong());
732 };
733 if let Ok(rules) = rules.extract::<PyRuleSet>() {
734 return Ok(rules.0);
735 }
736 if let Ok(name) = rules.extract::<String>() {
737 return match name.to_ascii_lowercase().as_str() {
738 "angular" => Ok(RuleSet::angular()),
739 "strong" => Ok(RuleSet::strong()),
740 "electromagnetic" | "em" => Ok(RuleSet::electromagnetic()),
741 "weak" => Ok(RuleSet::weak()),
742 _ => Err(LadduError::ParseError {
743 name,
744 object: "RuleSet".to_string(),
745 }
746 .into()),
747 };
748 }
749 Err(PyTypeError::new_err(
750 "rules must be a RuleSet or preset string",
751 ))
752}
753
754#[pyclass(name = "SelectionRules", module = "laddu", from_py_object)]
755#[derive(Clone)]
756pub struct PySelectionRules(pub SelectionRules);
757
758#[pymethods]
759impl PySelectionRules {
760 #[new]
761 #[pyo3(signature = (*, max_l=6, rules=None))]
762 fn new(max_l: u32, rules: Option<&Bound<'_, PyAny>>) -> PyResult<Self> {
763 Ok(Self(SelectionRules {
764 max_l: OrbitalAngularMomentum::integer(max_l),
765 rules: parse_rules(rules)?,
766 }))
767 }
768
769 #[staticmethod]
770 fn coupled_spins(
771 py: Python<'_>,
772 spin_1: &Bound<'_, PyAny>,
773 spin_2: &Bound<'_, PyAny>,
774 ) -> PyResult<Vec<PyQuantumNumber>> {
775 SelectionRules::coupled_spins(
776 parse_angular_momentum(spin_1)?,
777 parse_angular_momentum(spin_2)?,
778 )
779 .into_iter()
780 .map(|spin| angular_momentum_to_python(py, spin))
781 .collect()
782 }
783
784 fn allowed_partial_waves(
785 &self,
786 parent: &PyParticleProperties,
787 daughter_1: &PyParticleProperties,
788 daughter_2: &PyParticleProperties,
789 ) -> Vec<PyAllowedPartialWave> {
790 self.0
791 .allowed_partial_waves(&parent.0, (&daughter_1.0, &daughter_2.0))
792 .into_iter()
793 .map(PyAllowedPartialWave)
794 .collect()
795 }
796
797 fn __repr__(&self) -> String {
798 format!("{:?}", self.0)
799 }
800}
801
802#[pyfunction(name = "coupled_spins")]
804pub fn py_coupled_spins(
805 py: Python<'_>,
806 spin_1: &Bound<'_, PyAny>,
807 spin_2: &Bound<'_, PyAny>,
808) -> PyResult<Vec<PyQuantumNumber>> {
809 PySelectionRules::coupled_spins(py, spin_1, spin_2)
810}
811
812#[pyfunction(name = "allowed_partial_waves", signature = (parent, daughter_1, daughter_2, *, max_l=6, rules=None))]
814pub fn py_allowed_partial_waves(
815 parent: &PyParticleProperties,
816 daughter_1: &PyParticleProperties,
817 daughter_2: &PyParticleProperties,
818 max_l: u32,
819 rules: Option<&Bound<'_, PyAny>>,
820) -> PyResult<Vec<PyAllowedPartialWave>> {
821 Ok(PySelectionRules::new(max_l, rules)?
822 .0
823 .allowed_partial_waves(&parent.0, (&daughter_1.0, &daughter_2.0))
824 .into_iter()
825 .map(PyAllowedPartialWave)
826 .collect())
827}