1use std::collections::BTreeMap;
7use std::str::FromStr;
8
9use pyo3::exceptions::{PyTypeError, PyValueError};
10use pyo3::prelude::*;
11
12use numpy::{IntoPyArray, PyArray1, PyArray2, PyArrayMethods};
13
14use nucleide_nuclei::NuclideId;
15
16#[pyfunction]
18fn version() -> &'static str {
19 env!("CARGO_PKG_VERSION")
20}
21
22fn wrap_nucid_err(e: nucleide_nuclei::Error) -> PyErr {
23 PyValueError::new_err(e.to_string())
24}
25
26#[pyclass(name = "Nuclide")]
32struct PyNuclide {
33 inner: NuclideId,
34}
35
36#[pymethods]
37impl PyNuclide {
38 #[new]
40 fn new(name: &str) -> PyResult<Self> {
41 NuclideId::from_name(name)
42 .map(|inner| Self { inner })
43 .map_err(wrap_nucid_err)
44 }
45
46 #[getter]
48 fn name(&self) -> String {
49 self.inner.to_name()
50 }
51
52 #[getter]
54 fn nucid(&self) -> u32 {
55 self.inner.nucid()
56 }
57
58 #[getter]
60 fn zzaaam(&self) -> u32 {
61 self.inner.zzaaam()
62 }
63
64 #[getter]
66 fn z(&self) -> u32 {
67 self.inner.z()
68 }
69
70 #[getter]
72 fn a(&self) -> u32 {
73 self.inner.a()
74 }
75
76 #[getter]
78 fn state(&self) -> u32 {
79 self.inner.state()
80 }
81
82 #[getter]
84 fn zaid(&self) -> u32 {
85 nucleide_nuclei::dialects::to_zaid(self.inner)
86 }
87
88 #[getter]
90 fn zzllaaam(&self) -> String {
91 nucleide_nuclei::dialects::zzllaaam(self.inner)
92 }
93
94 #[getter]
96 fn serpent(&self) -> String {
97 nucleide_nuclei::dialects::serpent(self.inner)
98 }
99
100 #[getter]
102 fn nist(&self) -> String {
103 nucleide_nuclei::dialects::nist(self.inner)
104 }
105
106 #[getter]
108 fn cinder(&self) -> u32 {
109 nucleide_nuclei::dialects::to_cinder(self.inner)
110 }
111
112 #[getter]
114 fn alara(&self) -> String {
115 nucleide_nuclei::dialects::alara(self.inner)
116 }
117
118 #[getter]
120 fn sza(&self) -> u32 {
121 nucleide_nuclei::dialects::to_sza(self.inner)
122 }
123
124 fn fluka(&self) -> PyResult<&'static str> {
126 nucleide_nuclei::dialects::id_to_fluka(self.inner)
127 .map_err(|e| PyValueError::new_err(e.to_string()))
128 }
129
130 #[getter]
132 fn mass(&self) -> Option<f64> {
133 nucleide_nuclei::data::atomic_mass(self.inner.nucid())
134 }
135
136 #[getter]
138 fn abundance(&self) -> Option<f64> {
139 nucleide_nuclei::data::natural_abundance(self.inner.nucid())
140 }
141
142 fn __repr__(&self) -> String {
143 format!("Nuclide({})", self.inner.to_name())
144 }
145}
146
147#[pyfunction]
149fn from_zaid(zaid: u32) -> PyResult<PyNuclide> {
150 nucleide_nuclei::dialects::from_zaid(zaid)
151 .map(|inner| PyNuclide { inner })
152 .map_err(|e| PyValueError::new_err(e.to_string()))
153}
154
155fn lookup(key: &Bound<'_, PyAny>, f: impl Fn(u32) -> Option<f64>) -> PyResult<Option<f64>> {
156 if let Ok(nucid) = key.extract::<u32>() {
157 return Ok(f(nucid));
158 }
159 if let Ok(name) = key.extract::<&str>() {
160 let id = NuclideId::from_name(name).map_err(wrap_nucid_err)?;
161 return Ok(f(id.nucid()));
162 }
163 Err(PyTypeError::new_err("expected int nucid or str name"))
164}
165
166#[pyfunction]
168fn atomic_mass(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
169 lookup(key, nucleide_nuclei::data::atomic_mass)
170}
171
172#[pyfunction]
174fn natural_abundance(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
175 lookup(key, nucleide_nuclei::data::natural_abundance)
176}
177
178#[pyclass(name = "Particle")]
180struct PyParticle {
181 inner: nucleide_nuclei::particles::ParticleId,
182}
183
184#[pymethods]
185impl PyParticle {
186 #[new]
188 fn new(spec: &Bound<'_, PyAny>) -> PyResult<Self> {
189 let inner = if let Ok(pdc) = spec.extract::<i32>() {
190 nucleide_nuclei::particles::ParticleId::from_pdc(pdc)
191 .ok_or_else(|| PyValueError::new_err(format!("unknown PDC code {pdc}")))?
192 } else if let Ok(s) = spec.extract::<&str>() {
193 s.parse::<nucleide_nuclei::particles::ParticleId>()
194 .map_err(|e| PyValueError::new_err(e.to_string()))?
195 } else {
196 return Err(PyTypeError::new_err("expected str alias or int PDC"));
197 };
198 Ok(Self { inner })
199 }
200
201 #[getter]
202 fn name(&self) -> &'static str {
203 self.inner.name()
204 }
205
206 #[getter]
207 fn describe(&self) -> &'static str {
208 self.inner.describe()
209 }
210
211 fn mcnp(&self) -> Option<&'static str> {
212 self.inner.mcnp()
213 }
214 fn mcnp6(&self) -> Option<&'static str> {
215 self.inner.mcnp6()
216 }
217 fn fluka(&self) -> Option<&'static str> {
218 self.inner.fluka()
219 }
220 fn geant4(&self) -> Option<&'static str> {
221 self.inner.geant4()
222 }
223
224 fn __repr__(&self) -> String {
225 format!("Particle('{}')", self.inner.name())
226 }
227}
228
229#[pyfunction]
231fn rxname_id(name: &str) -> PyResult<u32> {
232 nucleide_nuclei::rxname::name_to_id(name).map_err(|e| PyValueError::new_err(e.to_string()))
233}
234
235#[pyfunction]
237fn rxname_name(id: u32) -> Option<&'static str> {
238 nucleide_nuclei::rxname::id_to_name(id)
239}
240
241#[pyfunction]
243fn rxname_mt(id: u32) -> i32 {
244 nucleide_nuclei::rxname::id_to_mt(id)
245}
246
247fn io_err(e: nucleide_mcnp_io::xsdir::Error) -> PyErr {
252 PyValueError::new_err(e.to_string())
253}
254fn m_err<T>(r: Result<T, impl std::fmt::Display>) -> PyResult<T> {
255 r.map_err(|e| PyValueError::new_err(e.to_string()))
256}
257
258#[pyclass(name = "XsdirTable")]
260struct PyXsdirTable {
261 inner: nucleide_mcnp_io::xsdir::XsdirTable,
262}
263
264#[pymethods]
265impl PyXsdirTable {
266 #[getter]
267 fn name(&self) -> &str {
268 &self.inner.name
269 }
270 #[getter]
271 fn awr(&self) -> f64 {
272 self.inner.awr
273 }
274 #[getter]
275 fn filename(&self) -> &str {
276 &self.inner.filename
277 }
278 #[getter]
279 fn filetype(&self) -> i64 {
280 self.inner.filetype
281 }
282 #[getter]
283 fn address(&self) -> i64 {
284 self.inner.address
285 }
286 #[getter]
287 fn tablelength(&self) -> i64 {
288 self.inner.tablelength
289 }
290 #[getter]
291 fn temperature(&self) -> Option<f64> {
292 self.inner.temperature
293 }
294 #[getter]
295 fn ptable(&self) -> bool {
296 self.inner.ptable
297 }
298 fn zaid(&self) -> &str {
300 self.inner.zaid()
301 }
302 fn to_serpent(&self, directory: &str) -> PyResult<String> {
304 m_err(self.inner.to_serpent(directory))
305 }
306 fn __repr__(&self) -> String {
307 format!("<XsdirTable: {}>", self.inner.name)
308 }
309}
310
311#[pyclass(name = "Xsdir")]
313struct PyXsdir {
314 inner: nucleide_mcnp_io::xsdir::Xsdir,
315}
316
317#[pymethods]
318impl PyXsdir {
319 #[getter]
320 fn datapath(&self) -> Option<&str> {
321 self.inner.datapath.as_deref()
322 }
323 #[getter]
325 fn awr(&self) -> BTreeMap<u32, f64> {
326 self.inner.awr.clone()
327 }
328 #[getter]
330 fn tables(&self) -> Vec<PyXsdirTable> {
331 self.inner
332 .tables
333 .iter()
334 .map(|t| PyXsdirTable { inner: t.clone() })
335 .collect()
336 }
337 fn find_table(&self, name: &str) -> Vec<PyXsdirTable> {
339 self.inner
340 .find_table(name)
341 .into_iter()
342 .map(|t| PyXsdirTable { inner: t.clone() })
343 .collect()
344 }
345 fn nucs(&self) -> Vec<u32> {
347 self.inner.nucs().iter().map(|n| n.nucid()).collect()
348 }
349}
350
351#[pyfunction]
353fn read_xsdir(path: &str) -> PyResult<PyXsdir> {
354 nucleide_mcnp_io::xsdir::Xsdir::from_file(path)
355 .map(|inner| PyXsdir { inner })
356 .map_err(io_err)
357}
358
359#[pyclass(name = "MeshTally")]
361struct PyMeshTally {
362 inner: nucleide_mcnp_io::meshtal::MeshTallyData,
363}
364
365#[pymethods]
366impl PyMeshTally {
367 #[getter]
368 fn tally_number(&self) -> u32 {
369 self.inner.tally_number
370 }
371 #[getter]
373 fn particle(&self) -> char {
374 self.inner.particle.letter()
375 }
376 #[getter]
377 fn dose_response(&self) -> bool {
378 self.inner.dose_response
379 }
380 #[getter]
381 fn x_bounds(&self) -> Vec<f64> {
382 self.inner.x_bounds.clone()
383 }
384 #[getter]
385 fn y_bounds(&self) -> Vec<f64> {
386 self.inner.y_bounds.clone()
387 }
388 #[getter]
389 fn z_bounds(&self) -> Vec<f64> {
390 self.inner.z_bounds.clone()
391 }
392 #[getter]
393 fn e_bounds(&self) -> Vec<f64> {
394 self.inner.e_bounds.clone()
395 }
396 fn dims(&self) -> [usize; 3] {
398 self.inner.dims()
399 }
400 fn num_ves(&self) -> usize {
401 self.inner.num_ves()
402 }
403 fn num_e_groups(&self) -> usize {
404 self.inner.num_e_groups()
405 }
406 fn cell(&self, i: usize, j: usize, k: usize) -> (Vec<f64>, Vec<f64>) {
408 let (r, e) = self.inner.cell(i, j, k);
409 (r.to_vec(), e.to_vec())
410 }
411 fn cell_total(&self, i: usize, j: usize, k: usize) -> (f64, f64) {
413 self.inner.cell_total(i, j, k)
414 }
415 #[getter]
417 fn result(&self) -> Vec<Vec<f64>> {
418 self.inner.result.clone()
419 }
420 #[getter]
422 fn rel_error(&self) -> Vec<Vec<f64>> {
423 self.inner.rel_error.clone()
424 }
425 #[getter]
427 fn total_result(&self) -> Vec<f64> {
428 self.inner.total_result.clone()
429 }
430 #[getter]
432 fn total_rel_error(&self) -> Vec<f64> {
433 self.inner.total_rel_error.clone()
434 }
435 fn to_list(&self) -> (Vec<Vec<f64>>, Vec<Vec<f64>>) {
440 (self.inner.result.clone(), self.inner.rel_error.clone())
441 }
442 fn totals_list(&self) -> (Vec<f64>, Vec<f64>) {
444 (
445 self.inner.total_result.clone(),
446 self.inner.total_rel_error.clone(),
447 )
448 }
449 #[allow(clippy::type_complexity)]
459 fn result_array<'py>(
460 &self,
461 py: Python<'py>,
462 ) -> PyResult<(Bound<'py, PyArray2<f64>>, Bound<'py, PyArray2<f64>>)> {
463 let n_ve = self.inner.num_ves();
464 let n_g = self.inner.num_e_groups();
465 let flatten = |rows: &[Vec<f64>], name: &str| -> PyResult<Vec<f64>> {
466 if rows.len() != n_ve {
467 return Err(PyValueError::new_err(format!(
468 "tally {name}: expected {n_ve} rows, found {}",
469 rows.len()
470 )));
471 }
472 let mut flat = Vec::with_capacity(n_ve * n_g);
473 for (ve, row) in rows.iter().enumerate() {
474 if row.len() != n_g {
475 return Err(PyValueError::new_err(format!(
476 "tally {name}: row {ve} has {} groups, expected {n_g}",
477 row.len()
478 )));
479 }
480 flat.extend_from_slice(row);
481 }
482 Ok(flat)
483 };
484 let flat_r = flatten(&self.inner.result, "result")?;
485 let flat_e = flatten(&self.inner.rel_error, "rel_error")?;
486 let arr_r = m_err(
487 flat_r
488 .into_pyarray(py)
489 .reshape((n_ve, n_g))
490 .map_err(|e| e.to_string()),
491 )?;
492 let arr_e = m_err(
493 flat_e
494 .into_pyarray(py)
495 .reshape((n_ve, n_g))
496 .map_err(|e| e.to_string()),
497 )?;
498 Ok((arr_r, arr_e))
499 }
500 #[allow(clippy::type_complexity)]
506 fn totals_array<'py>(
507 &self,
508 py: Python<'py>,
509 ) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
510 Ok((
511 self.inner.total_result.clone().into_pyarray(py),
512 self.inner.total_rel_error.clone().into_pyarray(py),
513 ))
514 }
515}
516
517#[pyclass(name = "Meshtal")]
519struct PyMeshtal {
520 inner: nucleide_mcnp_io::meshtal::Meshtal,
521}
522
523#[pymethods]
524impl PyMeshtal {
525 #[getter]
526 fn version(&self) -> &str {
527 &self.inner.version
528 }
529 #[getter]
530 fn ld(&self) -> &str {
531 &self.inner.ld
532 }
533 #[getter]
534 fn title(&self) -> &str {
535 &self.inner.title
536 }
537 #[getter]
538 fn histories(&self) -> u64 {
539 self.inner.histories
540 }
541 #[getter]
543 fn tallies(&self) -> BTreeMap<u32, PyMeshTally> {
544 self.inner
545 .tallies
546 .iter()
547 .map(|(k, v)| (*k, PyMeshTally { inner: v.clone() }))
548 .collect()
549 }
550}
551
552#[pyfunction]
554fn read_meshtal(path: &str) -> PyResult<PyMeshtal> {
555 m_err(nucleide_mcnp_io::meshtal::Meshtal::from_file(path).map(|inner| PyMeshtal { inner }))
556}
557
558#[pyclass(name = "Wwinp")]
560struct PyWwinp {
561 inner: nucleide_mcnp_io::wwinp::Wwinp,
562}
563
564#[pymethods]
565impl PyWwinp {
566 #[getter]
567 fn ni(&self) -> u32 {
568 self.inner.ni
569 }
570 #[getter]
571 fn nr(&self) -> u32 {
572 self.inner.nr
573 }
574 #[getter]
575 fn ne(&self) -> Vec<u32> {
576 self.inner.ne.clone()
577 }
578 #[getter]
579 fn nf(&self) -> [u32; 3] {
580 self.inner.nf
581 }
582 #[getter]
583 fn origin(&self) -> [f64; 3] {
584 self.inner.origin
585 }
586 #[getter]
587 fn nc(&self) -> [u32; 3] {
588 self.inner.nc
589 }
590 #[getter]
592 fn cm(&self) -> Vec<Vec<f64>> {
593 self.inner.cm.clone()
594 }
595 #[getter]
597 fn bounds(&self) -> Vec<Vec<f64>> {
598 self.inner.bounds.clone()
599 }
600 #[getter]
602 fn e(&self) -> Vec<Vec<f64>> {
603 self.inner.e.clone()
604 }
605 fn ww_row(&self, particle: usize, group: usize) -> Vec<f64> {
607 self.inner.ww[particle][group].clone()
608 }
609 fn ww_column(&self, particle: usize, ve: usize) -> Vec<f64> {
611 self.inner.ww_column(particle, ve)
612 }
613 fn ww_row_array<'py>(
622 &self,
623 py: Python<'py>,
624 particle: usize,
625 group: usize,
626 ) -> PyResult<Bound<'py, PyArray1<f64>>> {
627 let row = self
628 .inner
629 .ww
630 .get(particle)
631 .and_then(|groups| groups.get(group))
632 .ok_or_else(|| {
633 PyValueError::new_err(format!(
634 "ww_row_array: particle {particle} group {group} out of range"
635 ))
636 })?;
637 Ok(row.clone().into_pyarray(py))
638 }
639 fn ww_column_array<'py>(
646 &self,
647 py: Python<'py>,
648 particle: usize,
649 ve: usize,
650 ) -> PyResult<Bound<'py, PyArray1<f64>>> {
651 let groups = self.inner.ww.get(particle).ok_or_else(|| {
652 PyValueError::new_err(format!("ww_column_array: particle {particle} out of range"))
653 })?;
654 if groups.is_empty() {
655 return Err(PyValueError::new_err(format!(
656 "ww_column_array: particle {particle} has no groups"
657 )));
658 }
659 let nft = groups[0].len();
660 if ve >= nft {
661 return Err(PyValueError::new_err(format!(
662 "ww_column_array: ve {ve} out of range for {nft} volume elements"
663 )));
664 }
665 for (g, row) in groups.iter().enumerate() {
666 if row.len() != nft {
667 return Err(PyValueError::new_err(format!(
668 "ww particle {particle}: group {g} has {} values, expected {nft}",
669 row.len()
670 )));
671 }
672 }
673 let col: Vec<f64> = groups.iter().map(|row| row[ve]).collect();
674 Ok(col.into_pyarray(py))
675 }
676 fn ww_particle_array<'py>(
687 &self,
688 py: Python<'py>,
689 particle: usize,
690 ) -> PyResult<Bound<'py, PyArray2<f64>>> {
691 let groups = self.inner.ww.get(particle).ok_or_else(|| {
692 PyValueError::new_err(format!(
693 "ww_particle_array: particle {particle} out of range"
694 ))
695 })?;
696 if groups.is_empty() {
697 return Err(PyValueError::new_err(format!(
698 "ww_particle_array: particle {particle} has no groups"
699 )));
700 }
701 let nft = groups[0].len();
702 let mut flat = Vec::with_capacity(groups.len() * nft);
703 for (g, row) in groups.iter().enumerate() {
704 if row.len() != nft {
705 return Err(PyValueError::new_err(format!(
706 "ww particle {particle}: group {g} has {} values, expected {nft}",
707 row.len()
708 )));
709 }
710 flat.extend_from_slice(row);
711 }
712 let n_g = groups.len();
713 m_err(
714 flat.into_pyarray(py)
715 .reshape((n_g, nft))
716 .map_err(|e| e.to_string()),
717 )
718 }
719}
720
721#[pyfunction]
723fn read_wwinp(path: &str) -> PyResult<PyWwinp> {
724 m_err(nucleide_mcnp_io::wwinp::Wwinp::from_file(path).map(|inner| PyWwinp { inner }))
725}
726
727fn mctal_card_dict<'py>(
731 py: Python<'py>,
732 card: &nucleide_mcnp_io::mctal::BinCard,
733) -> PyResult<pyo3::Bound<'py, pyo3::types::PyDict>> {
734 let c = pyo3::types::PyDict::new(py);
735 c.set_item("count", card.count)?;
736 c.set_item("values", card.values.clone())?;
737 c.set_item("variant", card.variant.map(|v| v.to_string()))?;
738 c.set_item("flag", card.flag)?;
739 Ok(c)
740}
741
742#[pyclass(name = "Mctal")]
743struct PyMctal {
744 inner: nucleide_mcnp_io::mctal::Mctal,
745}
746
747#[pymethods]
748impl PyMctal {
749 #[getter]
750 fn code_name(&self) -> &str {
751 &self.inner.code_name
752 }
753 #[getter]
754 fn comment(&self) -> &str {
755 &self.inner.comment
756 }
757 #[getter]
758 fn n_histories(&self) -> u64 {
759 self.inner.n_histories
760 }
761 #[getter]
762 fn n_cycles(&self) -> usize {
763 self.inner.n_cycles
764 }
765 #[getter]
766 fn n_inactive(&self) -> usize {
767 self.inner.n_inactive
768 }
769 #[getter]
770 fn vars_per_cycle(&self) -> usize {
771 self.inner.vars_per_cycle
772 }
773 #[getter]
774 fn k_col(&self) -> Vec<f64> {
775 self.inner.k_col.clone()
776 }
777 #[getter]
778 fn k_abs(&self) -> Vec<f64> {
779 self.inner.k_abs.clone()
780 }
781 #[getter]
782 fn k_path(&self) -> Vec<f64> {
783 self.inner.k_path.clone()
784 }
785 #[getter]
786 fn prompt_life_col(&self) -> Vec<f64> {
787 self.inner.prompt_life_col.clone()
788 }
789 #[getter]
790 fn prompt_life_path(&self) -> Vec<f64> {
791 self.inner.prompt_life_path.clone()
792 }
793 #[getter]
796 fn averages(&self) -> Vec<BTreeMap<String, f64>> {
797 self.inner
798 .averages
799 .iter()
800 .map(|a| {
801 let mut m = BTreeMap::new();
802 m.insert("avg_k_col".into(), a.avg_k_col.0);
803 m.insert("avg_k_col_stdev".into(), a.avg_k_col.1);
804 m.insert("avg_k_abs".into(), a.avg_k_abs.0);
805 m.insert("avg_k_abs_stdev".into(), a.avg_k_abs.1);
806 m.insert("avg_k_path".into(), a.avg_k_path.0);
807 m.insert("avg_k_path_stdev".into(), a.avg_k_path.1);
808 m.insert("avg_k_combined".into(), a.avg_k_combined.0);
809 m.insert("avg_k_combined_stdev".into(), a.avg_k_combined.1);
810 m.insert("avg_k_combined_active".into(), a.avg_k_combined_active.0);
811 m.insert(
812 "avg_k_combined_active_stdev".into(),
813 a.avg_k_combined_active.1,
814 );
815 m.insert("prompt_life_combined".into(), a.prompt_life_combined.0);
816 m.insert(
817 "prompt_life_combined_stdev".into(),
818 a.prompt_life_combined.1,
819 );
820 m.insert("cycle_histories".into(), a.cycle_histories);
821 m.insert("fom".into(), a.fom);
822 m
823 })
824 .collect()
825 }
826 #[allow(clippy::type_complexity)]
837 fn k_arrays<'py>(
838 &self,
839 py: Python<'py>,
840 ) -> PyResult<(
841 Bound<'py, PyArray1<f64>>,
842 Bound<'py, PyArray1<f64>>,
843 Bound<'py, PyArray1<f64>>,
844 Bound<'py, PyArray1<f64>>,
845 Bound<'py, PyArray1<f64>>,
846 )> {
847 Ok((
848 self.inner.k_col.clone().into_pyarray(py),
849 self.inner.k_abs.clone().into_pyarray(py),
850 self.inner.k_path.clone().into_pyarray(py),
851 self.inner.prompt_life_col.clone().into_pyarray(py),
852 self.inner.prompt_life_path.clone().into_pyarray(py),
853 ))
854 }
855 fn averages_array<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArray2<f64>>> {
867 let n = self.inner.averages.len();
868 let mut flat = Vec::with_capacity(n * 14);
869 for a in &self.inner.averages {
870 flat.extend_from_slice(&[
871 a.avg_k_col.0,
872 a.avg_k_col.1,
873 a.avg_k_abs.0,
874 a.avg_k_abs.1,
875 a.avg_k_path.0,
876 a.avg_k_path.1,
877 a.avg_k_combined.0,
878 a.avg_k_combined.1,
879 a.avg_k_combined_active.0,
880 a.avg_k_combined_active.1,
881 a.prompt_life_combined.0,
882 a.prompt_life_combined.1,
883 a.cycle_histories,
884 a.fom,
885 ]);
886 }
887 m_err(
888 flat.into_pyarray(py)
889 .reshape((n, 14))
890 .map_err(|e| e.to_string()),
891 )
892 }
893 #[getter]
896 fn npert(&self) -> Option<String> {
897 self.inner.npert.clone()
898 }
899 #[getter]
901 fn tally_nums(&self) -> Vec<u32> {
902 self.inner.tally_nums.clone()
903 }
904 #[getter]
914 fn tallies(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
915 use pyo3::types::PyDict;
916 let mut out = Vec::with_capacity(self.inner.tallies.len());
917 for t in &self.inner.tallies {
918 let d = PyDict::new(py);
919 d.set_item("number", t.number)?;
920 d.set_item("particle_type", t.particle_type)?;
921 d.set_item("detector_type", t.detector_type)?;
922 d.set_item("particle_list", t.particle_list.clone())?;
923 d.set_item("comment", t.comment.clone())?;
924 for (key, card) in [
925 ("f", &t.f),
926 ("d", &t.d),
927 ("u", &t.u),
928 ("s", &t.s),
929 ("m", &t.m),
930 ("c", &t.c),
931 ("e", &t.e),
932 ("t", &t.t),
933 ] {
934 d.set_item(key, mctal_card_dict(py, card)?)?;
935 }
936 let vals: Vec<(f64, f64)> = t.vals.clone();
937 d.set_item("vals", vals)?;
938 let tfc_obj = if let Some(tfc) = &t.tfc {
939 let td = PyDict::new(py);
940 td.set_item("jtf", tfc.jtf.clone())?;
941 let mut rows = Vec::with_capacity(tfc.rows.len());
942 for r in &tfc.rows {
943 let rd = PyDict::new(py);
944 rd.set_item("nps", r.nps)?;
945 rd.set_item("value", r.value)?;
946 rd.set_item("rel_err", r.rel_err)?;
947 rd.set_item("fom", r.fom)?;
948 rows.push(rd.into_any().unbind());
949 }
950 td.set_item("rows", rows)?;
951 td.into_any().unbind()
952 } else {
953 py.None()
954 };
955 d.set_item("tfc", tfc_obj)?;
956 d.set_item("total", t.total_val())?;
957 out.push(d.into_any().unbind());
958 }
959 Ok(out)
960 }
961 #[getter]
967 fn mesh_tallies(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
968 use pyo3::types::PyDict;
969 let mut out = Vec::with_capacity(self.inner.mesh_tallies.len());
970 for t in &self.inner.mesh_tallies {
971 let d = PyDict::new(py);
972 d.set_item("number", t.number)?;
973 d.set_item("particle_type", t.particle_type)?;
974 d.set_item("detector_type", t.detector_type)?;
975 d.set_item("particle_list", t.particle_list.clone())?;
976 d.set_item("comment", t.comment.clone())?;
977 d.set_item("mesh_unknown", t.mesh_unknown)?;
978 d.set_item("ni", t.ni)?;
979 d.set_item("nj", t.nj)?;
980 d.set_item("nk", t.nk)?;
981 d.set_item("dims", t.dims().to_vec())?;
982 d.set_item("num_cells", t.num_cells())?;
983 d.set_item("cora", t.cora.clone())?;
984 d.set_item("corb", t.corb.clone())?;
985 d.set_item("corc", t.corc.clone())?;
986 for (key, card) in [
987 ("d", &t.d),
988 ("u", &t.u),
989 ("s", &t.s),
990 ("m", &t.m),
991 ("c", &t.c),
992 ("e", &t.e),
993 ("t", &t.t),
994 ] {
995 d.set_item(key, mctal_card_dict(py, card)?)?;
996 }
997 let vals: Vec<(f64, f64)> = t.vals.clone();
998 d.set_item("vals", vals)?;
999 d.set_item("total", t.total_val())?;
1000 out.push(d.into_any().unbind());
1001 }
1002 Ok(out)
1003 }
1004 fn tally_vals_array<'py>(
1012 &self,
1013 py: Python<'py>,
1014 number: u32,
1015 ) -> PyResult<Bound<'py, PyArray2<f64>>> {
1016 let tally = self
1017 .inner
1018 .tallies
1019 .iter()
1020 .find(|t| t.number == number)
1021 .ok_or_else(|| {
1022 PyValueError::new_err(format!("mctal has no parsed body for tally {number}"))
1023 })?;
1024 let mut flat = Vec::with_capacity(tally.vals.len() * 2);
1025 for (v, e) in &tally.vals {
1026 flat.push(*v);
1027 flat.push(*e);
1028 }
1029 let n = tally.vals.len();
1030 m_err(
1031 flat.into_pyarray(py)
1032 .reshape((n, 2))
1033 .map_err(|e| e.to_string()),
1034 )
1035 }
1036 fn mesh_tally_vals_array<'py>(
1042 &self,
1043 py: Python<'py>,
1044 number: u32,
1045 ) -> PyResult<Bound<'py, PyArray2<f64>>> {
1046 let tally = self
1047 .inner
1048 .mesh_tallies
1049 .iter()
1050 .find(|t| t.number == number)
1051 .ok_or_else(|| {
1052 PyValueError::new_err(format!("mctal has no parsed mesh body for tally {number}"))
1053 })?;
1054 let mut flat = Vec::with_capacity(tally.vals.len() * 2);
1055 for (v, e) in &tally.vals {
1056 flat.push(*v);
1057 flat.push(*e);
1058 }
1059 let n = tally.vals.len();
1060 m_err(
1061 flat.into_pyarray(py)
1062 .reshape((n, 2))
1063 .map_err(|e| e.to_string()),
1064 )
1065 }
1066}
1067
1068#[pyfunction]
1071fn read_mctal(path: &str) -> PyResult<PyMctal> {
1072 m_err(nucleide_mcnp_io::mctal::Mctal::from_file(path).map(|inner| PyMctal { inner }))
1073}
1074
1075#[pyclass(name = "SurfSrc")]
1077struct PySurfSrc {
1078 inner: nucleide_mcnp_io::surfsrc::SurfSrc,
1079}
1080
1081#[pymethods]
1082impl PySurfSrc {
1083 #[getter]
1084 fn kod(&self) -> String {
1085 self.inner.header.kod.trim_end().to_string()
1086 }
1087 #[getter]
1088 fn ver(&self) -> String {
1089 self.inner.header.ver.trim_end().to_string()
1090 }
1091 #[getter]
1092 fn np1(&self) -> i64 {
1093 self.inner.header.np1
1094 }
1095 #[getter]
1097 fn orignp1(&self) -> i64 {
1098 self.inner.header.orignp1
1099 }
1100 #[getter]
1101 fn nrss(&self) -> i64 {
1102 self.inner.header.nrss
1103 }
1104 #[getter]
1105 fn ncrd(&self) -> i32 {
1106 self.inner.header.ncrd
1107 }
1108 #[getter]
1109 fn njsw(&self) -> i32 {
1110 self.inner.header.njsw
1111 }
1112 #[getter]
1113 fn niss(&self) -> i64 {
1114 self.inner.header.niss
1115 }
1116 fn print_header(&self) -> String {
1118 self.inner.header.print_header()
1119 }
1120 fn tracks(&self) -> PyResult<Vec<BTreeMap<String, f64>>> {
1122 let tracks = self
1123 .inner
1124 .read_tracklist()
1125 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1126 Ok(tracks
1127 .iter()
1128 .map(|t| {
1129 let mut d = BTreeMap::new();
1130 d.insert("nps".into(), t.nps);
1131 d.insert("bitarray".into(), t.bitarray);
1132 d.insert("wgt".into(), t.wgt);
1133 d.insert("erg".into(), t.erg);
1134 d.insert("tme".into(), t.tme);
1135 d.insert("x".into(), t.x);
1136 d.insert("y".into(), t.y);
1137 d.insert("z".into(), t.z);
1138 d.insert("u".into(), t.u);
1139 d.insert("v".into(), t.v);
1140 d.insert("cs".into(), t.cs);
1141 d.insert("w".into(), t.w);
1142 d
1143 })
1144 .collect())
1145 }
1146}
1147
1148#[pyfunction]
1150fn read_ssw(path: &str) -> PyResult<PySurfSrc> {
1151 nucleide_mcnp_io::surfsrc::SurfSrc::open(path)
1152 .map(|inner| PySurfSrc { inner })
1153 .map_err(|e| PyValueError::new_err(e.to_string()))
1154}
1155
1156#[pyclass(name = "PtracFile")]
1158struct PyPtracFile {
1159 inner: nucleide_mcnp_io::ptrac::PtracFile,
1160}
1161
1162#[pymethods]
1163impl PyPtracFile {
1164 #[getter]
1165 fn problem_title(&self) -> &str {
1166 &self.inner.problem_title
1167 }
1168 #[getter]
1170 fn width_code(&self) -> u8 {
1171 match self.inner.format {
1172 nucleide_mcnp_io::ptrac::Format::I4LittleEndian => 0,
1173 nucleide_mcnp_io::ptrac::Format::I8LittleEndian => 1,
1174 }
1175 }
1176 #[getter]
1178 fn variable_nums(&self) -> BTreeMap<String, usize> {
1179 let v = &self.inner.variable_nums;
1180 let mut m = BTreeMap::new();
1181 m.insert("nps".into(), v.nps);
1182 m.insert("src".into(), v.src);
1183 m.insert("bnk".into(), v.bnk);
1184 m.insert("sur".into(), v.sur);
1185 m.insert("col".into(), v.col);
1186 m.insert("ter".into(), v.ter);
1187 m
1188 }
1189 fn events(&self) -> PyResult<Vec<BTreeMap<String, f64>>> {
1191 let events = self
1192 .inner
1193 .events()
1194 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1195 Ok(events
1196 .iter()
1197 .map(|ev| {
1198 let mut d = BTreeMap::new();
1199 d.insert("event_type".to_string(), ev.event_type as f64);
1200 for (n, v) in ev.iter() {
1201 d.insert(n.to_string(), v);
1202 }
1203 d
1204 })
1205 .collect())
1206 }
1207 fn events_array<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArray2<f64>>> {
1218 let events = self
1219 .inner
1220 .events()
1221 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1222 let n = events.len();
1223 let mut flat = Vec::with_capacity(n * PTRAC_EVENT_COLUMNS.len());
1224 for ev in &events {
1225 flat.push(ev.event_type as f64);
1226 for col in &PTRAC_EVENT_COLUMNS[1..] {
1227 flat.push(ev.get(col).unwrap_or(0.0));
1228 }
1229 }
1230 m_err(
1231 flat.into_pyarray(py)
1232 .reshape((n, PTRAC_EVENT_COLUMNS.len()))
1233 .map_err(|e| e.to_string()),
1234 )
1235 }
1236 fn event_field_array<'py>(
1244 &self,
1245 py: Python<'py>,
1246 field: &str,
1247 ) -> PyResult<Bound<'py, PyArray1<f64>>> {
1248 if !PTRAC_EVENT_COLUMNS.contains(&field) {
1249 return Err(PyValueError::new_err(format!(
1250 "unknown PTRAC field `{field}` (expected one of {})",
1251 PTRAC_EVENT_COLUMNS.join(", ")
1252 )));
1253 }
1254 let events = self
1255 .inner
1256 .events()
1257 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1258 let col: Vec<f64> = events
1259 .iter()
1260 .map(|ev| {
1261 if field == "event_type" {
1262 ev.event_type as f64
1263 } else {
1264 ev.get(field).unwrap_or(0.0)
1265 }
1266 })
1267 .collect();
1268 Ok(col.into_pyarray(py))
1269 }
1270}
1271
1272const PTRAC_EVENT_COLUMNS: [&str; 19] = [
1278 "event_type",
1279 "node",
1280 "nsr",
1281 "nsf",
1282 "nxs",
1283 "ntyn",
1284 "ipt",
1285 "ncl",
1286 "mat",
1287 "ncp",
1288 "xxx",
1289 "yyy",
1290 "zzz",
1291 "uuu",
1292 "vvv",
1293 "www",
1294 "erg",
1295 "wgt",
1296 "tme",
1297];
1298
1299#[pyfunction]
1301fn read_ptrac(path: &str) -> PyResult<PyPtracFile> {
1302 nucleide_mcnp_io::ptrac::PtracFile::open(path)
1303 .map(|inner| PyPtracFile { inner })
1304 .map_err(|e| PyValueError::new_err(e.to_string()))
1305}
1306
1307fn mcpl_particle_to_dict(py: Python<'_>, p: &nucleide_mcpl_io::Particle) -> PyResult<Py<PyAny>> {
1310 use pyo3::types::PyDict;
1311 let d = PyDict::new(py);
1312 d.set_item("ekin", p.ekin)?;
1313 d.set_item("polarisation", p.polarisation.to_vec())?;
1314 d.set_item("position", p.position.to_vec())?;
1315 d.set_item("direction", p.direction.to_vec())?;
1316 d.set_item("time", p.time)?;
1317 d.set_item("weight", p.weight)?;
1318 d.set_item("pdgcode", p.pdgcode)?;
1319 d.set_item("userflags", p.userflags)?;
1320 Ok(d.into_any().unbind())
1321}
1322
1323fn mcpl_particle_from_dict(d: &Bound<'_, PyAny>) -> PyResult<nucleide_mcpl_io::Particle> {
1324 let get_f64 = |key: &str| -> PyResult<f64> {
1325 d.get_item(key)
1326 .map_err(|e| PyValueError::new_err(format!("particle missing `{key}`: {e}")))?
1327 .extract()
1328 .map_err(|_| PyValueError::new_err(format!("particle `{key}` must be a float")))
1329 };
1330 let get_vec3 = |key: &str| -> PyResult<[f64; 3]> {
1331 let v: Vec<f64> = d
1332 .get_item(key)
1333 .map_err(|e| PyValueError::new_err(format!("particle missing `{key}`: {e}")))?
1334 .extract()
1335 .map_err(|_| PyValueError::new_err(format!("particle `{key}` must be a 3-list")))?;
1336 if v.len() != 3 {
1337 return Err(PyValueError::new_err(format!(
1338 "particle `{key}` must have exactly 3 entries"
1339 )));
1340 }
1341 Ok([v[0], v[1], v[2]])
1342 };
1343 let pdgcode: i32 = d
1344 .get_item("pdgcode")
1345 .map_err(|e| PyValueError::new_err(format!("particle missing `pdgcode`: {e}")))?
1346 .extract()
1347 .map_err(|_| PyValueError::new_err("particle `pdgcode` must be an int"))?;
1348 let userflags: u32 = d
1349 .get_item("userflags")
1350 .map_err(|e| PyValueError::new_err(format!("particle missing `userflags`: {e}")))?
1351 .extract()
1352 .map_err(|_| PyValueError::new_err("particle `userflags` must be an int"))?;
1353 Ok(nucleide_mcpl_io::Particle {
1354 ekin: get_f64("ekin")?,
1355 polarisation: get_vec3("polarisation")?,
1356 position: get_vec3("position")?,
1357 direction: get_vec3("direction")?,
1358 time: get_f64("time")?,
1359 weight: get_f64("weight")?,
1360 pdgcode,
1361 userflags,
1362 })
1363}
1364
1365#[pyclass(name = "McplFile")]
1367struct PyMcplFile {
1368 inner: nucleide_mcpl_io::McplFile,
1369}
1370
1371#[pymethods]
1372impl PyMcplFile {
1373 #[getter]
1375 fn version(&self) -> u16 {
1376 self.inner.header.version
1377 }
1378 #[getter]
1380 fn nparticles(&self) -> u64 {
1381 self.inner.header.nparticles
1382 }
1383 #[getter]
1385 fn srcname(&self) -> &str {
1386 &self.inner.header.srcname
1387 }
1388 #[getter]
1390 fn comments(&self) -> Vec<String> {
1391 self.inner.header.comments.clone()
1392 }
1393 #[getter]
1395 fn has_userflags(&self) -> bool {
1396 self.inner.header.has_userflags
1397 }
1398 #[getter]
1400 fn has_polarisation(&self) -> bool {
1401 self.inner.header.has_polarisation
1402 }
1403 #[getter]
1405 fn double_prec(&self) -> bool {
1406 self.inner.header.double_prec
1407 }
1408 #[getter]
1410 fn universal_pdgcode(&self) -> Option<i32> {
1411 self.inner.header.universal_pdgcode
1412 }
1413 #[getter]
1415 fn universal_weight(&self) -> Option<f64> {
1416 self.inner.header.universal_weight
1417 }
1418 #[getter]
1420 fn blobs(&self) -> Vec<(String, Vec<u8>)> {
1421 self.inner
1422 .header
1423 .blobs
1424 .iter()
1425 .map(|b| (b.key.clone(), b.data.clone()))
1426 .collect()
1427 }
1428 fn particles(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
1431 let ps = self
1432 .inner
1433 .particles()
1434 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1435 ps.iter().map(|p| mcpl_particle_to_dict(py, p)).collect()
1436 }
1437}
1438
1439#[pyfunction]
1441fn read_mcpl(path: &str) -> PyResult<PyMcplFile> {
1442 nucleide_mcpl_io::McplFile::open(path)
1443 .map(|inner| PyMcplFile { inner })
1444 .map_err(|e| PyValueError::new_err(e.to_string()))
1445}
1446
1447#[pyfunction]
1457fn write_mcpl(
1458 path: &str,
1459 header: &Bound<'_, PyAny>,
1460 particles: Vec<Bound<'_, PyAny>>,
1461) -> PyResult<()> {
1462 use nucleide_mcpl_io::{Blob, Header};
1463 let get = |key: &str| header.get_item(key);
1464 let srcname: String = get("srcname")
1465 .map_err(|_| PyValueError::new_err("header missing `srcname`"))?
1466 .extract()
1467 .map_err(|_| PyValueError::new_err("header `srcname` must be a str"))?;
1468 let comments: Vec<String> = get("comments")
1469 .map_err(|_| PyValueError::new_err("header missing `comments`"))?
1470 .extract()
1471 .map_err(|_| PyValueError::new_err("header `comments` must be a list of str"))?;
1472 let flag = |key: &str| -> PyResult<bool> {
1473 get(key)
1474 .map_err(|_| PyValueError::new_err(format!("header missing `{key}`")))?
1475 .extract()
1476 .map_err(|_| PyValueError::new_err(format!("header `{key}` must be a bool")))
1477 };
1478 let universal_pdgcode: Option<i32> = get("universal_pdgcode")
1479 .map_err(|_| PyValueError::new_err("header missing `universal_pdgcode`"))?
1480 .extract()
1481 .map_err(|_| PyValueError::new_err("header `universal_pdgcode` must be an int or None"))?;
1482 let universal_weight: Option<f64> = get("universal_weight")
1483 .map_err(|_| PyValueError::new_err("header missing `universal_weight`"))?
1484 .extract()
1485 .map_err(|_| PyValueError::new_err("header `universal_weight` must be a float or None"))?;
1486 let blob_pairs: Vec<(String, Vec<u8>)> = get("blobs")
1487 .map_err(|_| PyValueError::new_err("header missing `blobs`"))?
1488 .extract()
1489 .map_err(|_| {
1490 PyValueError::new_err("header `blobs` must be a list of (key, bytes) pairs")
1491 })?;
1492 let h = Header {
1493 has_userflags: flag("has_userflags")?,
1494 has_polarisation: flag("has_polarisation")?,
1495 double_prec: flag("double_prec")?,
1496 universal_pdgcode,
1497 universal_weight,
1498 srcname,
1499 comments,
1500 blobs: blob_pairs
1501 .into_iter()
1502 .map(|(key, data)| Blob { key, data })
1503 .collect(),
1504 ..Header::default()
1505 };
1506 let ps: Vec<nucleide_mcpl_io::Particle> = particles
1507 .iter()
1508 .map(mcpl_particle_from_dict)
1509 .collect::<PyResult<_>>()?;
1510 nucleide_mcpl_io::write_to_path(path, &h, &ps).map_err(|e| PyValueError::new_err(e.to_string()))
1511}
1512
1513#[pyfunction]
1528#[pyo3(signature = (ssw_path, mcpl_path, surfs, kinds, options=None))]
1529fn ssw2mcpl(
1530 ssw_path: &str,
1531 mcpl_path: &str,
1532 surfs: Vec<u32>,
1533 kinds: Vec<String>,
1534 options: Option<Bound<'_, PyAny>>,
1535) -> PyResult<u64> {
1536 use nucleide_mcnp_io::surfsrc::SurfSrc;
1537 use nucleide_mcpl_io::ssw::{SswParticleKind, SswTrack};
1538 let ssw = SurfSrc::open(ssw_path).map_err(|e| PyValueError::new_err(e.to_string()))?;
1539 let raw = ssw
1540 .read_tracklist()
1541 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1542 if raw.len() != surfs.len() || raw.len() != kinds.len() {
1543 return Err(PyValueError::new_err(format!(
1544 "ssw2mcpl: SSW holds {} tracks but got {} surfs and {} kinds \
1545 (one surf+kind per track required)",
1546 raw.len(),
1547 surfs.len(),
1548 kinds.len()
1549 )));
1550 }
1551 let mut tracks = Vec::with_capacity(raw.len());
1552 for (i, (t, surf, kind)) in raw
1553 .iter()
1554 .zip(surfs)
1555 .zip(kinds.iter())
1556 .map(|((t, s), k)| (t, s, k))
1557 .enumerate()
1558 {
1559 let kind = SswParticleKind::parse(kind).ok_or_else(|| {
1560 PyValueError::new_err(format!(
1561 "track {i} kind `{kind}` unknown (expected one of \
1562 \"neutron\", \"gamma\", \"electron\", \"positron\", \"proton\")"
1563 ))
1564 })?;
1565 tracks.push(SswTrack {
1566 ekin: t.erg,
1567 time_shakes: t.tme,
1568 position: [t.x, t.y, t.z],
1569 direction: [t.u, t.v, t.cs],
1570 weight: t.wgt,
1571 surf,
1572 kind,
1573 });
1574 }
1575 let mut opts = parse_ssw2mcpl_options(options.as_ref())?;
1576 if mcpl_path.ends_with(".gz") {
1577 opts.gzip = true;
1578 }
1579 let bytes = nucleide_mcpl_io::ssw::ssw2mcpl_bytes(&tracks, &opts)
1580 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1581 std::fs::write(mcpl_path, bytes).map_err(|e| PyValueError::new_err(e.to_string()))?;
1582 Ok(tracks.len() as u64)
1583}
1584
1585fn parse_ssw2mcpl_options(
1587 options: Option<&Bound<'_, PyAny>>,
1588) -> PyResult<nucleide_mcpl_io::ssw::Ssw2McplOptions> {
1589 use nucleide_mcpl_io::ssw::{DeckBlob, Ssw2McplOptions};
1590 let mut opts = Ssw2McplOptions::default();
1591 let Some(d) = options else {
1592 return Ok(opts);
1593 };
1594 if !d.is_instance_of::<pyo3::types::PyDict>() {
1595 return Err(PyValueError::new_err("options must be a dict or None"));
1596 }
1597 let flag = |key: &str| -> PyResult<Option<bool>> {
1598 match d.get_item(key) {
1599 Ok(v) => v
1600 .extract()
1601 .map(Some)
1602 .map_err(|_| PyValueError::new_err(format!("options `{key}` must be a bool"))),
1603 Err(_) => Ok(None),
1604 }
1605 };
1606 if let Some(v) = flag("double_prec")? {
1607 opts.double_prec = v;
1608 }
1609 if let Some(v) = flag("surf_to_userflags")? {
1610 opts.surf_to_userflags = v;
1611 }
1612 if let Some(v) = flag("gzip")? {
1613 opts.gzip = v;
1614 }
1615 if let Some(v) = flag("universal_pdg")? {
1616 opts.universal_pdg = v;
1617 }
1618 if let Some(v) = flag("universal_weight")? {
1619 opts.universal_weight = v;
1620 }
1621 if let Ok(v) = d.get_item("polarisation") {
1622 if v.is_none() {
1623 opts.polarisation = None;
1624 } else {
1625 let vec: Vec<f64> = v.extract().map_err(|_| {
1626 PyValueError::new_err("options `polarisation` must be a 3-list or None")
1627 })?;
1628 if vec.len() != 3 {
1629 return Err(PyValueError::new_err(
1630 "options `polarisation` must have exactly 3 entries",
1631 ));
1632 }
1633 opts.polarisation = Some([vec[0], vec[1], vec[2]]);
1634 }
1635 }
1636 if let Ok(v) = d.get_item("srcname") {
1637 opts.srcname = v
1638 .extract()
1639 .map_err(|_| PyValueError::new_err("options `srcname` must be a str"))?;
1640 }
1641 if let Ok(v) = d.get_item("comments") {
1642 opts.comments = v
1643 .extract()
1644 .map_err(|_| PyValueError::new_err("options `comments` must be a list of str"))?;
1645 }
1646 if let Ok(v) = d.get_item("deck_blob") {
1647 if !v.is_none() {
1648 let (key, data): (String, Vec<u8>) = v.extract().map_err(|_| {
1649 PyValueError::new_err("options `deck_blob` must be a (key, bytes) pair or None")
1650 })?;
1651 opts.deck_blob = Some(DeckBlob { key, data });
1652 }
1653 }
1654 Ok(opts)
1655}
1656
1657#[pyfunction]
1671#[pyo3(signature = (mcpl_path, reference_ssw_path, ssw_out_path, surface=None, force_cs_to_one=false, niss=None, allow_polarisation=false))]
1672fn mcpl2ssw(
1673 mcpl_path: &str,
1674 reference_ssw_path: &str,
1675 ssw_out_path: &str,
1676 surface: Option<u32>,
1677 force_cs_to_one: bool,
1678 niss: Option<i64>,
1679 allow_polarisation: bool,
1680) -> PyResult<u64> {
1681 use nucleide_mcnp_io::surfsrc::SurfSrc;
1682 use nucleide_mcpl_io::ssw::Mcpl2SswOptions;
1683 let mcpl = nucleide_mcpl_io::McplFile::open(mcpl_path)
1684 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1685 let particles = mcpl
1686 .particles()
1687 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1688 let reference =
1689 SurfSrc::open(reference_ssw_path).map_err(|e| PyValueError::new_err(e.to_string()))?;
1690 let (header, tracks) = nucleide_mcpl_io::ssw::mcpl2ssw(
1691 &particles,
1692 &reference.header,
1693 &Mcpl2SswOptions {
1694 surface,
1695 force_cs_to_one,
1696 niss_override: niss,
1697 allow_polarisation,
1698 },
1699 )
1700 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1701 nucleide_mcnp_io::surfsrc::write_to_path(ssw_out_path, &header, &tracks)
1702 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1703 Ok(tracks.len() as u64)
1704}
1705
1706#[pyclass(name = "EndlLibrary")]
1708struct PyEndlLibrary {
1709 inner: nucleide_mcnp_io::endl::Library,
1710}
1711
1712#[pymethods]
1713impl PyEndlLibrary {
1714 fn nuclides(&self) -> Vec<i64> {
1716 self.inner.nuclides()
1717 }
1718 #[pyo3(signature = (nuc, p_in, rdesc, rprop, x1=None, p_out=None))]
1725 fn get_rx(
1726 &self,
1727 nuc: &Bound<'_, PyAny>,
1728 p_in: i32,
1729 rdesc: i32,
1730 rprop: i32,
1731 x1: Option<i32>,
1732 p_out: Option<i32>,
1733 ) -> PyResult<Vec<Vec<f64>>> {
1734 let id = if let Ok(n) = nuc.extract::<i64>() {
1735 n
1736 } else if let Ok(name) = nuc.extract::<&str>() {
1737 NuclideId::from_name(name).map_err(wrap_nucid_err)?.nucid() as i64
1738 } else {
1739 return Err(PyTypeError::new_err("expected int nucleus id or str name"));
1740 };
1741 self.inner
1742 .get_rx(id, p_in, rdesc, rprop, x1, p_out)
1743 .map(|rows| rows.to_vec())
1744 .map_err(|e| PyValueError::new_err(e.to_string()))
1745 }
1746}
1747
1748#[pyfunction]
1750fn read_endl(path: &str) -> PyResult<PyEndlLibrary> {
1751 nucleide_mcnp_io::endl::Library::open(path)
1752 .map(|inner| PyEndlLibrary { inner })
1753 .map_err(|e| PyValueError::new_err(e.to_string()))
1754}
1755
1756#[pyfunction]
1758fn endl_endftod(field: &str) -> f64 {
1759 nucleide_mcnp_io::endl::endftod(field)
1760}
1761
1762#[pyfunction]
1770fn combine_ssw_files(output: &str, inputs: Vec<String>) -> PyResult<()> {
1771 nucleide_mcnp_io::surfsrc::combine_files(&inputs, output)
1772 .map_err(|e| PyValueError::new_err(e.to_string()))
1773}
1774
1775#[pyclass(name = "Chain")]
1781struct PyChain {
1782 inner: std::sync::Arc<nucleide_depletion::Chain>,
1783}
1784
1785#[pymethods]
1786impl PyChain {
1787 #[getter]
1789 fn nuclides(&self) -> Vec<String> {
1790 self.inner.nuclides.iter().map(|n| n.name.clone()).collect()
1791 }
1792
1793 fn index_of(&self, name: &str) -> Option<usize> {
1794 self.inner.index_of(name)
1795 }
1796}
1797
1798#[pyfunction]
1800fn read_chain(path: &str) -> PyResult<PyChain> {
1801 nucleide_depletion::Chain::from_file(path)
1802 .map(|inner| PyChain {
1803 inner: std::sync::Arc::new(inner),
1804 })
1805 .map_err(|e| PyValueError::new_err(e.to_string()))
1806}
1807
1808type RateMap = BTreeMap<String, f64>;
1810
1811#[pyclass(name = "DepletionSystem")]
1813struct PyDepletionSystem {
1814 inner: std::sync::Arc<nucleide_depletion::DepletionSystem>,
1815}
1816
1817#[pymethods]
1818impl PyDepletionSystem {
1819 #[pyo3(signature = (n0, dt, order=48, method="cram48"))]
1828 fn solve(
1829 &self,
1830 n0: BTreeMap<String, f64>,
1831 dt: f64,
1832 order: u8,
1833 method: &str,
1834 ) -> PyResult<BTreeMap<String, f64>> {
1835 let method = resolve_method(order, method)?;
1836 nucleide_depletion::deplete_with_method(&self.inner, method, &n0, dt)
1837 .map(|r| r.atoms)
1838 .map_err(|e| PyValueError::new_err(e.to_string()))
1839 }
1840
1841 #[pyo3(signature = (n0, dt, order=48, method="cram48"))]
1847 fn solve_vec(&self, n0: Vec<f64>, dt: f64, order: u8, method: &str) -> PyResult<Vec<f64>> {
1848 let method = resolve_method(order, method)?;
1849 nucleide_depletion::solve_with_method(&self.inner, method, &n0, dt)
1850 .map_err(|e| PyValueError::new_err(e.to_string()))
1851 }
1852}
1853
1854#[pyfunction]
1856fn build_depletion_system(chain: &PyChain, rates: RateMap) -> PyResult<PyDepletionSystem> {
1857 let rs = split_rates(&rates, &chain.inner)?;
1858 nucleide_depletion::DepletionSystem::build((*chain.inner).clone(), &rs)
1859 .map(|sys| PyDepletionSystem {
1860 inner: std::sync::Arc::new(sys),
1861 })
1862 .map_err(|e| PyValueError::new_err(e.to_string()))
1863}
1864
1865fn parse_order(order: u8) -> PyResult<nucleide_depletion::Order> {
1866 match order {
1867 16 => Ok(nucleide_depletion::Order::Order16),
1868 48 => Ok(nucleide_depletion::Order::Order48),
1869 other => Err(PyValueError::new_err(format!(
1870 "unsupported CRAM order {other} (supported: 16, 48)"
1871 ))),
1872 }
1873}
1874
1875fn parse_method(name: &str) -> PyResult<nucleide_depletion::Method> {
1878 name.parse().map_err(|e: String| PyValueError::new_err(e))
1879}
1880
1881fn resolve_method(order: u8, method: &str) -> PyResult<nucleide_depletion::Method> {
1886 let parsed = parse_method(method)?;
1887 if parsed == nucleide_depletion::Method::default_cram() {
1888 parse_order(order).map(nucleide_depletion::Method::Cram)
1889 } else {
1890 Ok(parsed)
1891 }
1892}
1893
1894fn split_rates(
1895 rates: &RateMap,
1896 chain: &nucleide_depletion::Chain,
1897) -> PyResult<nucleide_depletion::ReactionRates> {
1898 let mut out = nucleide_depletion::ReactionRates::new();
1899 for (key, v) in rates {
1900 let (nuc, rx) = key.split_once(':').ok_or_else(|| {
1901 PyValueError::new_err(format!("rate key `{key}` must be `Name:reaction`"))
1902 })?;
1903 let idx = chain
1904 .index_of(nuc)
1905 .ok_or_else(|| PyValueError::new_err(format!("rate for unknown nuclide `{nuc}`")))?;
1906 out.entry(idx).or_default().insert(rx.to_string(), *v);
1907 }
1908 Ok(out)
1909}
1910
1911#[pyfunction]
1921#[pyo3(signature = (chain, n0, dt, rates=None, order=48, method="cram48"))]
1922fn deplete(
1923 chain: &PyChain,
1924 n0: BTreeMap<String, f64>,
1925 dt: f64,
1926 rates: Option<RateMap>,
1927 order: u8,
1928 method: &str,
1929) -> PyResult<BTreeMap<String, f64>> {
1930 let method = resolve_method(order, method)?;
1931 let rates = split_rates(rates.as_ref().unwrap_or(&BTreeMap::new()), &chain.inner)?;
1932 let sys = nucleide_depletion::DepletionSystem::build((*chain.inner).clone(), &rates)
1933 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1934 nucleide_depletion::deplete_with_method(&sys, method, &n0, dt)
1935 .map(|r| r.atoms)
1936 .map_err(|e| PyValueError::new_err(e.to_string()))
1937}
1938
1939#[pyfunction]
1948fn read_serpent(path: &str, kind: &str) -> PyResult<Py<PyAny>> {
1949 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
1950 let table = match kind {
1951 "res" => nucleide_serpent_io::parse_res(&text),
1952 "dep" => nucleide_serpent_io::parse_dep(&text),
1953 "det" => nucleide_serpent_io::parse_det(&text),
1954 other => {
1955 return Err(PyValueError::new_err(format!(
1956 "kind must be res|dep|det, got `{other}`"
1957 )))
1958 }
1959 }
1960 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1961 fn entry_to_py(py: Python<'_>, e: &nucleide_serpent_io::Entry) -> PyResult<Py<PyAny>> {
1962 use nucleide_serpent_io::Entry as E;
1963 let value = match e {
1964 E::Scalar(nucleide_serpent_io::Value::Num(n)) => {
1965 n.into_pyobject(py).unwrap().unbind().into_any()
1966 }
1967 E::Scalar(nucleide_serpent_io::Value::Str(s)) => {
1968 s.into_pyobject(py).unwrap().unbind().into_any()
1969 }
1970 E::Vector(vs) => vs
1971 .iter()
1972 .map(|v| match v {
1973 nucleide_serpent_io::Value::Num(n) => {
1974 n.into_pyobject(py).unwrap().unbind().into_any()
1975 }
1976 nucleide_serpent_io::Value::Str(s) => {
1977 s.into_pyobject(py).unwrap().unbind().into_any()
1978 }
1979 })
1980 .collect::<Vec<_>>()
1981 .into_pyobject(py)
1982 .unwrap()
1983 .unbind()
1984 .into_any(),
1985 E::Matrix(m) => m
1986 .to_rows_f64()
1987 .map_err(|err| PyValueError::new_err(err.to_string()))?
1988 .into_pyobject(py)
1989 .unwrap()
1990 .unbind()
1991 .into_any(),
1992 };
1993 Ok(value)
1994 }
1995 Python::attach(|py| {
1996 let dict = pyo3::types::PyDict::new(py);
1997 for (k, e) in table.iter() {
1998 dict.set_item(k, entry_to_py(py, e)?)?;
1999 }
2000 Ok(dict.into_any().unbind())
2001 })
2002}
2003
2004#[pyclass(name = "UsrbinTally")]
2006struct PyUsrbinTally {
2007 inner: nucleide_fluka_io::usrbin::UsrbinTally,
2008}
2009
2010#[pymethods]
2011impl PyUsrbinTally {
2012 #[getter]
2013 fn name(&self) -> &str {
2014 &self.inner.name
2015 }
2016 #[getter]
2017 fn particle(&self) -> &str {
2018 &self.inner.particle
2019 }
2020 #[getter]
2021 fn nx(&self) -> usize {
2022 self.inner.x_info.bins
2023 }
2024 #[getter]
2025 fn ny(&self) -> usize {
2026 self.inner.y_info.bins
2027 }
2028 #[getter]
2029 fn nz(&self) -> usize {
2030 self.inner.z_info.bins
2031 }
2032 #[getter]
2033 fn x_bounds(&self) -> Vec<f64> {
2034 self.inner.x_bounds.clone()
2035 }
2036 #[getter]
2037 fn y_bounds(&self) -> Vec<f64> {
2038 self.inner.y_bounds.clone()
2039 }
2040 #[getter]
2041 fn z_bounds(&self) -> Vec<f64> {
2042 self.inner.z_bounds.clone()
2043 }
2044 #[getter]
2046 fn data(&self) -> Vec<f64> {
2047 self.inner.part_data.clone()
2048 }
2049 #[getter]
2051 fn error(&self) -> Vec<f64> {
2052 self.inner.error_data.clone()
2053 }
2054 fn dims(&self) -> [usize; 3] {
2055 [self.nx(), self.ny(), self.nz()]
2056 }
2057}
2058
2059#[pyfunction]
2061fn read_usrbin(path: &str) -> PyResult<Vec<PyUsrbinTally>> {
2062 let tallies = nucleide_fluka_io::usrbin::read_usrbin_file(path)
2063 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2064 Ok(tallies
2065 .into_iter()
2066 .map(|inner| PyUsrbinTally { inner })
2067 .collect())
2068}
2069
2070#[pyclass(name = "MagicOutput")]
2072struct PyMagicOutput {
2073 inner: nucleide_vr_tools::magic::MagicOutput,
2074}
2075
2076#[pymethods]
2077impl PyMagicOutput {
2078 #[getter]
2080 fn lower_bounds_ww(&self) -> Vec<f64> {
2081 self.inner.lower_bounds_ww.clone()
2082 }
2083 #[getter]
2084 fn groups_per_ve(&self) -> usize {
2085 self.inner.groups_per_ve
2086 }
2087 #[getter]
2088 fn scale_factors(&self) -> Vec<f64> {
2089 self.inner.scale_factors.clone()
2090 }
2091 #[getter]
2092 fn e_upper_bounds(&self) -> Vec<f64> {
2093 self.inner.e_upper_bounds.clone()
2094 }
2095 #[getter]
2096 fn ww_tag_name(&self) -> &str {
2097 &self.inner.ww_tag_name
2098 }
2099}
2100
2101#[pyfunction]
2103#[pyo3(signature = (tally, per_group=false, tolerance=0.5))]
2104fn magic(tally: &PyMeshTally, per_group: bool, tolerance: f64) -> PyResult<PyMagicOutput> {
2105 let selection = if per_group {
2106 nucleide_vr_tools::magic::MagicSelection::PerGroup
2107 } else {
2108 nucleide_vr_tools::magic::MagicSelection::Total
2109 };
2110 let params = nucleide_vr_tools::magic::MagicParams {
2111 tolerance,
2112 ..Default::default()
2113 };
2114 nucleide_vr_tools::magic::magic_with(&tally.inner, selection, params)
2115 .map(|inner| PyMagicOutput { inner })
2116 .map_err(|e| PyValueError::new_err(e.to_string()))
2117}
2118
2119#[pyclass(name = "AliasTable")]
2121struct PyAliasTable {
2122 inner: nucleide_vr_tools::sampling::AliasTable,
2123}
2124
2125#[pymethods]
2126impl PyAliasTable {
2127 #[new]
2129 fn new(pdf: Vec<f64>) -> PyResult<Self> {
2130 nucleide_vr_tools::sampling::AliasTable::new(&pdf)
2131 .map(|inner| PyAliasTable { inner })
2132 .map_err(|e| PyValueError::new_err(e.to_string()))
2133 }
2134 fn sample(&self, r1: f64, r2: f64) -> usize {
2136 self.inner.sample(r1, r2)
2137 }
2138 #[getter]
2139 fn pdf(&self) -> Vec<f64> {
2140 self.inner.pdf().to_vec()
2141 }
2142 fn __len__(&self) -> usize {
2143 self.inner.len()
2144 }
2145}
2146
2147#[pyclass(name = "MeshSourceSampler")]
2149struct PyMeshSourceSampler {
2150 inner: nucleide_vr_tools::sampling::MeshSourceSampler,
2151}
2152
2153#[pymethods]
2154impl PyMeshSourceSampler {
2155 #[new]
2157 #[pyo3(signature = (tally, mode, user_pdf=None))]
2158 fn new(tally: &PyMeshTally, mode: &str, user_pdf: Option<Vec<f64>>) -> PyResult<Self> {
2159 let user = if matches!(mode, "user") {
2160 Some(user_pdf.ok_or_else(|| PyValueError::new_err("user mode needs user_pdf"))?)
2161 } else {
2162 None
2163 };
2164 let m = match mode {
2165 "analog" => nucleide_vr_tools::sampling::Mode::Analog,
2166 "uniform" => nucleide_vr_tools::sampling::Mode::Uniform,
2167 "user" => nucleide_vr_tools::sampling::Mode::User,
2168 other => {
2169 return Err(PyValueError::new_err(format!(
2170 "mode must be analog|uniform|user, got `{other}`"
2171 )))
2172 }
2173 };
2174 nucleide_vr_tools::sampling::MeshSourceSampler::new(&tally.inner, m, user.as_deref())
2175 .map(|inner| PyMeshSourceSampler { inner })
2176 .map_err(|e| PyValueError::new_err(e.to_string()))
2177 }
2178 fn sample(&self, r1: f64, r2: f64) -> BTreeMap<String, f64> {
2180 let s = self.inner.sample(r1, r2);
2181 let mut d = BTreeMap::new();
2182 d.insert("index".into(), s.index as f64);
2183 d.insert("i".into(), s.i as f64);
2184 d.insert("j".into(), s.j as f64);
2185 d.insert("k".into(), s.k as f64);
2186 d.insert("weight".into(), s.weight);
2187 d
2188 }
2189 fn mode(&self) -> &'static str {
2191 match self.inner.mode() {
2192 nucleide_vr_tools::sampling::Mode::Analog => "analog",
2193 nucleide_vr_tools::sampling::Mode::Uniform => "uniform",
2194 nucleide_vr_tools::sampling::Mode::User => "user",
2195 }
2196 }
2197 fn num_voxels(&self) -> usize {
2199 self.inner.num_voxels()
2200 }
2201 fn table_len(&self) -> usize {
2203 self.inner.table().len()
2204 }
2205}
2206
2207#[pyfunction]
2210#[pyo3(signature = (ssw, path, tracks=None))]
2211fn write_ssw(
2212 ssw: &PySurfSrc,
2213 path: &str,
2214 tracks: Option<Vec<BTreeMap<String, f64>>>,
2215) -> PyResult<()> {
2216 let header = ssw.inner.header.clone();
2217 let track_data: Vec<nucleide_mcnp_io::surfsrc::TrackData> = match tracks {
2218 Some(dict_tracks) => dict_tracks
2219 .iter()
2220 .map(|d| {
2221 let g = |k: &str| d.get(k).copied().unwrap_or(0.0);
2222 let mut record = vec![0.0f64; nucleide_mcnp_io::surfsrc::TrackData::RECORD_WIDTH];
2223 record[0] = g("nps");
2224 record[1] = g("bitarray");
2225 record[2] = g("wgt");
2226 record[3] = g("erg");
2227 record[4] = g("tme");
2228 record[5] = g("x");
2229 record[6] = g("y");
2230 record[7] = g("z");
2231 record[8] = g("u");
2232 record[9] = g("v");
2233 record[10] = g("cs");
2234 nucleide_mcnp_io::surfsrc::TrackData::from_record(record)
2235 })
2236 .collect(),
2237 None => ssw
2238 .inner
2239 .read_tracklist()
2240 .map_err(|e| PyValueError::new_err(e.to_string()))?,
2241 };
2242 let mut f = std::fs::File::create(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
2243 nucleide_mcnp_io::surfsrc::write_to(&mut f, &header, &track_data)
2244 .map_err(|e| PyValueError::new_err(e.to_string()))
2245}
2246
2247#[pyfunction]
2249fn mesh_to_geom(
2250 x_bounds: Vec<f64>,
2251 y_bounds: Vec<f64>,
2252 z_bounds: Vec<f64>,
2253 cell_materials: Vec<Option<(String, f64)>>,
2254 title_card: &str,
2255) -> String {
2256 let opts = nucleide_mcnp_io::deck::DeckOptions {
2257 title_card: title_card.to_string(),
2258 frac_type: nucleide_mcnp_io::deck::FracType::Mass,
2259 };
2260 nucleide_mcnp_io::deck::mesh_to_geom(&x_bounds, &y_bounds, &z_bounds, &cell_materials, &opts)
2261}
2262
2263#[pyfunction]
2274fn alara_parse_deck(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
2275 let owned = text.to_owned();
2276 let deck = py
2277 .detach(move || nucleide_alara_io::AlaraDeck::parse(&owned))
2278 .map_err(ala_err)?;
2279 Ok(deck_to_py(py, &deck))
2280}
2281
2282fn ala_err(e: nucleide_alara_io::Error) -> PyErr {
2283 PyValueError::new_err(e.to_string())
2284}
2285
2286fn deck_to_py(py: Python<'_>, deck: &nucleide_alara_io::AlaraDeck) -> Py<PyAny> {
2287 use pyo3::types::PyDict;
2288 let out = PyDict::new(py);
2289 let block_kinds: Vec<&str> = deck.block_kinds();
2290 out.set_item("block_kinds", block_kinds).ok();
2291 out.set_item("geometry", deck.geometry.as_ref().map(|g| g.kind.clone()))
2292 .ok();
2293 let mixtures: Vec<Py<PyAny>> = deck.mixtures.iter().map(|m| mixture_to_py(py, m)).collect();
2294 out.set_item("mixtures", mixtures).ok();
2295 let fluxes: Vec<Py<PyAny>> = deck.fluxes.iter().map(|f| fluxdef_to_py(py, f)).collect();
2296 out.set_item("fluxes", fluxes).ok();
2297 out.set_item(
2298 "cooling_times_s",
2299 deck.cooling
2300 .as_ref()
2301 .map(|c| c.times_s.clone())
2302 .unwrap_or_default(),
2303 )
2304 .ok();
2305 let schedules: Vec<Py<PyAny>> = deck
2306 .schedules
2307 .iter()
2308 .map(|s| {
2309 let d = PyDict::new(py);
2310 let items: Vec<Vec<String>> = s.items.iter().map(|it| it.tokens.clone()).collect();
2311 d.set_item("name", &s.name).ok();
2312 d.set_item("items", items).ok();
2313 d.into_any().unbind()
2314 })
2315 .collect();
2316 out.set_item("schedules", schedules).ok();
2317 let histories: Vec<Py<PyAny>> = deck
2318 .pulse_histories
2319 .iter()
2320 .map(|h| {
2321 let d = PyDict::new(py);
2322 let levels: Vec<Py<PyAny>> = h
2323 .levels
2324 .iter()
2325 .map(|l| {
2326 let e = PyDict::new(py);
2327 e.set_item("pulses", l.pulses).ok();
2328 e.set_item("delay_s", l.delay_s).ok();
2329 e.into_any().unbind()
2330 })
2331 .collect();
2332 d.set_item("name", &h.name).ok();
2333 d.set_item("levels", levels).ok();
2334 d.into_any().unbind()
2335 })
2336 .collect();
2337 out.set_item("pulse_histories", histories).ok();
2338 let outputs: Vec<Py<PyAny>> = deck
2339 .outputs
2340 .iter()
2341 .map(|o| {
2342 let d = PyDict::new(py);
2343 d.set_item("resolution", &o.resolution).ok();
2344 d.set_item("entries", o.entries.clone()).ok();
2345 d.into_any().unbind()
2346 })
2347 .collect();
2348 out.set_item("outputs", outputs).ok();
2349 out.set_item("truncation", deck.truncation.as_ref().map(|t| t.tolerance))
2350 .ok();
2351 out.into_any().unbind()
2352}
2353
2354fn mixture_to_py(py: Python<'_>, mix: &nucleide_alara_io::deck::Mixture) -> Py<PyAny> {
2355 use pyo3::types::PyDict;
2356 let entries: Vec<Py<PyAny>> = mix
2357 .entries
2358 .iter()
2359 .map(|e| mixture_entry_to_py(py, e))
2360 .collect();
2361 let d = PyDict::new(py);
2362 d.set_item("name", &mix.name).ok();
2363 d.set_item("entries", entries).ok();
2364 d.into_any().unbind()
2365}
2366
2367fn mixture_entry_to_py(py: Python<'_>, entry: &nucleide_alara_io::deck::MixtureEntry) -> Py<PyAny> {
2368 use nucleide_alara_io::deck::MixtureEntry as E;
2369 use pyo3::types::PyDict;
2370 let d = PyDict::new(py);
2371 match entry {
2372 E::Material {
2373 name,
2374 rel_density,
2375 vol_fraction,
2376 } => {
2377 d.set_item("kind", "material").ok();
2378 d.set_item("name", name).ok();
2379 d.set_item("rel_density", *rel_density).ok();
2380 d.set_item("vol_fraction", *vol_fraction).ok();
2381 }
2382 E::Element {
2383 symbol,
2384 rel_density,
2385 vol_fraction,
2386 } => {
2387 d.set_item("kind", "element").ok();
2388 d.set_item("symbol", symbol).ok();
2389 d.set_item("rel_density", *rel_density).ok();
2390 d.set_item("vol_fraction", *vol_fraction).ok();
2391 }
2392 E::Like {
2393 mixture,
2394 rel_density,
2395 } => {
2396 d.set_item("kind", "like").ok();
2397 d.set_item("mixture", mixture).ok();
2398 d.set_item("rel_density", *rel_density).ok();
2399 }
2400 E::Target { target_kind, name } => {
2401 d.set_item("kind", "target").ok();
2402 d.set_item("target_kind", target_kind).ok();
2403 d.set_item("name", name).ok();
2404 }
2405 }
2406 d.into_any().unbind()
2407}
2408
2409fn fluxdef_to_py(py: Python<'_>, flux: &nucleide_alara_io::deck::FluxDef) -> Py<PyAny> {
2410 use pyo3::types::PyDict;
2411 let d = PyDict::new(py);
2412 d.set_item("name", &flux.name).ok();
2413 d.set_item("file", &flux.file).ok();
2414 d.set_item("scale", flux.scale).ok();
2415 d.set_item("skip", flux.skip).ok();
2416 d.set_item("format", &flux.format).ok();
2417 d.into_any().unbind()
2418}
2419
2420#[pyfunction]
2425fn alara_parse_flux(py: Python<'_>, text: &str, name: &str) -> PyResult<Py<PyAny>> {
2426 let owned_text = text.to_owned();
2427 let owned_name = name.to_owned();
2428 let spectra = py
2429 .detach(move || nucleide_alara_io::FluxSpectra::parse(&owned_name, &owned_text))
2430 .map_err(ala_err)?;
2431 use pyo3::types::PyDict;
2432 let d = PyDict::new(py);
2433 d.set_item("name", spectra.name.clone()).ok();
2434 d.set_item("groups_per_interval", spectra.groups_per_interval)
2435 .ok();
2436 d.set_item("num_intervals", spectra.num_intervals()).ok();
2437 let totals: Vec<f64> = spectra.intervals.iter().map(|iv| iv.iter().sum()).collect();
2438 d.set_item("totals", totals).ok();
2439 d.set_item("total", spectra.total()).ok();
2440 d.set_item("intervals", spectra.intervals.clone()).ok();
2441 Ok(d.into_any().unbind())
2442}
2443
2444#[pyfunction]
2450fn alara_parse_output(
2451 py: Python<'_>,
2452 text: &str,
2453 run_lbl: &str,
2454) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
2455 let owned_text = text.to_owned();
2456 let owned_lbl = run_lbl.to_owned();
2457 let rows = py
2458 .detach(move || {
2459 nucleide_alara_io::output::ResponseFrame::parse(&owned_text, &owned_lbl).map(|f| f.rows)
2460 })
2461 .map_err(ala_err)?;
2462 Ok(rows
2463 .iter()
2464 .map(|r| {
2465 let mut d = BTreeMap::new();
2466 d.insert(
2467 "time_s".to_string(),
2468 r.time_s.into_pyobject(py).unwrap().unbind().into_any(),
2469 );
2470 d.insert(
2471 "time_label".to_string(),
2472 r.time_label
2473 .clone()
2474 .into_pyobject(py)
2475 .unwrap()
2476 .unbind()
2477 .into_any(),
2478 );
2479 d.insert(
2480 "nuclide".to_string(),
2481 r.nuclide
2482 .clone()
2483 .into_pyobject(py)
2484 .unwrap()
2485 .unbind()
2486 .into_any(),
2487 );
2488 d.insert(
2489 "half_life_s".to_string(),
2490 r.half_life_s.into_pyobject(py).unwrap().unbind().into_any(),
2491 );
2492 d.insert(
2493 "run_lbl".to_string(),
2494 r.run_lbl
2495 .clone()
2496 .into_pyobject(py)
2497 .unwrap()
2498 .unbind()
2499 .into_any(),
2500 );
2501 d.insert(
2502 "block".to_string(),
2503 r.block
2504 .as_str()
2505 .into_pyobject(py)
2506 .unwrap()
2507 .unbind()
2508 .into_any(),
2509 );
2510 d.insert(
2511 "block_name".to_string(),
2512 r.block_name
2513 .clone()
2514 .into_pyobject(py)
2515 .unwrap()
2516 .unbind()
2517 .into_any(),
2518 );
2519 d.insert(
2520 "block_num".to_string(),
2521 r.block_num.into_pyobject(py).unwrap().unbind().into_any(),
2522 );
2523 d.insert(
2524 "variable".to_string(),
2525 r.variable
2526 .as_str()
2527 .into_pyobject(py)
2528 .unwrap()
2529 .unbind()
2530 .into_any(),
2531 );
2532 d.insert(
2533 "var_unit".to_string(),
2534 r.var_unit
2535 .clone()
2536 .into_pyobject(py)
2537 .unwrap()
2538 .unbind()
2539 .into_any(),
2540 );
2541 d.insert(
2542 "value".to_string(),
2543 r.value.into_pyobject(py).unwrap().unbind().into_any(),
2544 );
2545 d
2546 })
2547 .collect())
2548}
2549
2550#[pyfunction]
2557#[pyo3(signature = (deck_text, top=None))]
2558fn alara_expand_schedule(
2559 py: Python<'_>,
2560 deck_text: &str,
2561 top: Option<&str>,
2562) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
2563 let owned_text = deck_text.to_owned();
2564 let owned_top = top.map(str::to_owned);
2565 let steps = py
2566 .detach(move || expand_deck_schedules(&owned_text, owned_top.as_deref()))
2567 .map_err(PyValueError::new_err)?;
2568 Ok(steps
2569 .into_iter()
2570 .map(|s| {
2571 let mut d = BTreeMap::new();
2572 let cooling = s.is_cooling();
2573 d.insert(
2574 "duration_s".to_string(),
2575 s.duration_s.into_pyobject(py).unwrap().unbind().into_any(),
2576 );
2577 d.insert(
2578 "flux".to_string(),
2579 s.flux
2580 .clone()
2581 .into_pyobject(py)
2582 .unwrap()
2583 .unbind()
2584 .into_any(),
2585 );
2586 d.insert(
2587 "is_cooling".to_string(),
2588 pyo3::types::PyBool::new(py, cooling)
2589 .to_owned()
2590 .into_any()
2591 .unbind(),
2592 );
2593 d
2594 })
2595 .collect())
2596}
2597
2598fn expand_deck_schedules(
2599 deck_text: &str,
2600 top: Option<&str>,
2601) -> Result<Vec<nucleide_alara_io::FlatStep>, String> {
2602 let deck = nucleide_alara_io::AlaraDeck::parse(deck_text).map_err(|e| e.to_string())?;
2603 let mut scheds = Vec::with_capacity(deck.schedules.len());
2604 for raw in &deck.schedules {
2605 let mut items = Vec::with_capacity(raw.items.len());
2606 for entry in &raw.items {
2607 items.push(
2608 parse_deck_sched_item(&entry.tokens)
2609 .map_err(|m| format!("schedule `{}` line {}: {m}", raw.name, entry.line))?,
2610 );
2611 }
2612 scheds.push(nucleide_alara_io::schedule::ScheduleDef {
2613 name: raw.name.clone(),
2614 items,
2615 });
2616 }
2617 let histories: Vec<nucleide_alara_io::schedule::PulseHistory> = deck
2618 .pulse_histories
2619 .iter()
2620 .map(|h| nucleide_alara_io::schedule::PulseHistory {
2621 name: h.name.clone(),
2622 levels: h
2623 .levels
2624 .iter()
2625 .map(|l| nucleide_alara_io::schedule::PulseLevel {
2626 count: l.pulses,
2627 delay_s: l.delay_s,
2628 })
2629 .collect(),
2630 })
2631 .collect();
2632 match top {
2633 Some(name) => {
2634 nucleide_alara_io::expand_from(name, &scheds, &histories).map_err(|e| e.to_string())
2635 }
2636 None => nucleide_alara_io::expand(&scheds, &histories).map_err(|e| e.to_string()),
2637 }
2638}
2639
2640fn parse_deck_sched_item(tokens: &[String]) -> Result<nucleide_alara_io::SchedItem, String> {
2641 match tokens {
2642 [op_text, op_unit, flux, history, delay_text, delay_unit] => {
2643 let op: f64 = op_text
2644 .parse()
2645 .map_err(|_| format!("expected operating time, found `{op_text}`"))?;
2646 let delay: f64 = delay_text
2647 .parse()
2648 .map_err(|_| format!("expected delay, found `{delay_text}`"))?;
2649 let op_time_s =
2650 nucleide_alara_io::parse_time_to_seconds(op, op_unit).map_err(|e| e.to_string())?;
2651 let delay_s = nucleide_alara_io::parse_time_to_seconds(delay, delay_unit)
2652 .map_err(|e| e.to_string())?;
2653 Ok(nucleide_alara_io::SchedItem::Pulse {
2654 op_time_s,
2655 flux: flux.clone(),
2656 history: history.clone(),
2657 delay_s,
2658 })
2659 }
2660 [name, history, delay_text, delay_unit] => {
2661 let delay: f64 = delay_text
2662 .parse()
2663 .map_err(|_| format!("expected delay, found `{delay_text}`"))?;
2664 let delay_s = nucleide_alara_io::parse_time_to_seconds(delay, delay_unit)
2665 .map_err(|e| e.to_string())?;
2666 Ok(nucleide_alara_io::SchedItem::SubSchedule {
2667 name: name.clone(),
2668 history: history.clone(),
2669 delay_s,
2670 })
2671 }
2672 _ => Err(format!(
2673 "expected 4- or 6-token schedule item, found {}",
2674 tokens.join(" ")
2675 )),
2676 }
2677}
2678
2679#[pyfunction]
2685fn half_life(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2686 lookup(key, nucleide_nuclei::data::half_life)
2687}
2688
2689#[pyfunction]
2691fn decay_constant(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2692 lookup(key, nucleide_nuclei::data::decay_constant)
2693}
2694
2695#[pyfunction]
2697fn q_value_capture(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2698 lookup(key, nucleide_nuclei::data::q_value_neutron_capture)
2699}
2700
2701#[pyfunction]
2703fn q_value_alpha(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2704 lookup(key, nucleide_nuclei::data::q_value_alpha)
2705}
2706
2707#[pyfunction]
2711fn read_inp(path: &str) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
2712 let mats = nucleide_mcnp_io::inp::materials_from_file(path)
2713 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2714 Python::attach(|py| {
2715 Ok(mats
2716 .into_iter()
2717 .map(|m| {
2718 let mut d = BTreeMap::new();
2719 d.insert(
2720 "number".to_string(),
2721 m.number.into_pyobject(py).unwrap().unbind().into_any(),
2722 );
2723 let fr: BTreeMap<String, f64> = m
2724 .fractions
2725 .iter()
2726 .map(|(id, f)| (id.to_name(), *f))
2727 .collect();
2728 d.insert(
2729 "fractions".to_string(),
2730 fr.into_pyobject(py).unwrap().unbind().into_any(),
2731 );
2732 d.insert(
2733 "fraction_type".to_string(),
2734 match m.fraction_type {
2735 nucleide_mcnp_io::inp::FracKind::Atom => "atom",
2736 nucleide_mcnp_io::inp::FracKind::Mass => "mass",
2737 }
2738 .into_pyobject(py)
2739 .unwrap()
2740 .unbind()
2741 .into_any(),
2742 );
2743 d.insert(
2744 "density".to_string(),
2745 m.density.into_pyobject(py).unwrap().unbind().into_any(),
2746 );
2747 d.insert(
2748 "comments".to_string(),
2749 m.comments
2750 .join(" ")
2751 .into_pyobject(py)
2752 .unwrap()
2753 .unbind()
2754 .into_any(),
2755 );
2756 d
2757 })
2758 .collect())
2759 })
2760}
2761
2762fn comp_to_material(comp: BTreeMap<String, f64>) -> PyResult<nucleide_material::Material> {
2763 let mut mat = nucleide_material::Material::new();
2764 for (name, grams) in &comp {
2765 let id = nucleide_nuclei::NuclideId::from_name(name)
2766 .map_err(|e| PyValueError::new_err(format!("`{name}`: {e}")))?;
2767 mat.add_nuclide(id, *grams);
2768 }
2769 Ok(mat)
2770}
2771
2772#[pyfunction]
2775fn from_formula(formula: &str) -> PyResult<BTreeMap<String, f64>> {
2776 use nucleide_material::AbundanceProvider;
2777 let parsed = nucleide_material::parse_formula(formula)
2778 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2779 let mut nat = Vec::new();
2781 for (z, count) in &parsed {
2782 if let Some(isotopes) = nucleide_material::NaturalAbundances.natural_isotopes(*z) {
2783 for (id, frac) in isotopes {
2784 nat.push((id, frac * count));
2785 }
2786 }
2787 }
2788 let total: f64 = nat.iter().map(|(_, c)| c).sum();
2789 if total <= 0.0 {
2790 return Err(PyValueError::new_err("empty formula expansion"));
2791 }
2792 let mut out: BTreeMap<String, f64> = BTreeMap::new();
2793 for (id, atoms) in nat {
2794 *out.entry(id.to_name()).or_insert(0.0) += atoms / total;
2795 }
2796 Ok(out)
2797}
2798
2799#[pyfunction]
2802fn activity(comp: BTreeMap<String, f64>) -> PyResult<BTreeMap<String, f64>> {
2803 let mat = comp_to_material(comp)?;
2804 let analytics = nucleide_material::Analytics {
2805 masses: &nucleide_material::Ame2020,
2806 decays: &nucleide_material::ChainDecays,
2807 };
2808 let per_nuc = mat
2809 .activity(&analytics)
2810 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2811 let specific = mat
2812 .specific_activity(&analytics)
2813 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2814 let mut out: BTreeMap<String, f64> = per_nuc
2815 .into_iter()
2816 .map(|(id, v)| (id.to_name(), v))
2817 .collect();
2818 out.insert("specific".to_string(), specific);
2819 Ok(out)
2820}
2821
2822#[pyfunction]
2824fn to_xml(comp: BTreeMap<String, f64>, name: &str, density: f64, units: &str) -> PyResult<String> {
2825 let mat = comp_to_material(comp)?;
2826 mat.to_xml(name, density, units)
2827 .map_err(|e| PyValueError::new_err(e.to_string()))
2828}
2829
2830#[pyclass(name = "Cascade")]
2832struct PyCascade {
2833 inner: std::sync::Mutex<nucleide_enrichment::Cascade>,
2834}
2835
2836#[pymethods]
2837impl PyCascade {
2838 #[staticmethod]
2840 fn default_uranium() -> Self {
2841 Self {
2842 inner: std::sync::Mutex::new(nucleide_enrichment::default_uranium_cascade()),
2843 }
2844 }
2845
2846 #[new]
2849 #[allow(non_snake_case)]
2850 #[allow(clippy::too_many_arguments)]
2851 fn new(
2852 alpha: f64,
2853 Mstar: f64,
2854 j: u32,
2855 k: u32,
2856 N: f64,
2857 M: f64,
2858 x_feed_j: f64,
2859 x_prod_j: f64,
2860 x_tail_j: f64,
2861 mat_feed: BTreeMap<String, f64>,
2862 ) -> PyResult<Self> {
2863 let mut feed = BTreeMap::new();
2864 for (name, frac) in mat_feed {
2865 let id = NuclideId::from_name(&name).map_err(wrap_nucid_err)?;
2866 feed.insert(id, frac);
2867 }
2868 let casc = nucleide_enrichment::Cascade {
2869 alpha,
2870 Mstar,
2871 j: NuclideId::from_nucid(j),
2872 k: NuclideId::from_nucid(k),
2873 N,
2874 M,
2875 x_feed_j,
2876 x_prod_j,
2877 x_tail_j,
2878 mat_feed: nucleide_enrichment::Stream::with_total_mass(feed, 1.0),
2879 mat_prod: nucleide_enrichment::Stream::new(),
2880 mat_tail: nucleide_enrichment::Stream::new(),
2881 l_t_per_feed: 0.0,
2882 swu_per_feed: 0.0,
2883 swu_per_prod: 0.0,
2884 };
2885 Ok(Self {
2886 inner: std::sync::Mutex::new(casc),
2887 })
2888 }
2889
2890 #[pyo3(signature = (tolerance=None, max_iterations=None))]
2892 fn solve(&self, tolerance: Option<f64>, max_iterations: Option<u32>) -> PyResult<()> {
2893 let tol = tolerance.unwrap_or(nucleide_enrichment::DEFAULT_TOLERANCE);
2894 let iters = max_iterations.unwrap_or(nucleide_enrichment::DEFAULT_MAX_ITER);
2895 let mut c = self
2896 .inner
2897 .lock()
2898 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
2899 *c = nucleide_enrichment::solve_numeric(&c, tol, iters)
2900 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2901 Ok(())
2902 }
2903
2904 #[pyo3(signature = (tolerance=None, max_iterations=None))]
2906 fn solve_multicomponent(
2907 &self,
2908 tolerance: Option<f64>,
2909 max_iterations: Option<u32>,
2910 ) -> PyResult<()> {
2911 let tol = tolerance.unwrap_or(nucleide_enrichment::DEFAULT_TOLERANCE);
2912 let iters = max_iterations.unwrap_or(nucleide_enrichment::DEFAULT_MAX_ITER);
2913 let mut c = self
2914 .inner
2915 .lock()
2916 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
2917 *c = nucleide_enrichment::multicomponent(&c, tol, iters)
2918 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2919 Ok(())
2920 }
2921
2922 #[getter]
2923 fn alpha(&self) -> PyResult<f64> {
2924 Ok(self
2925 .inner
2926 .lock()
2927 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2928 .alpha)
2929 }
2930 #[getter]
2931 #[allow(non_snake_case)]
2932 fn Mstar(&self) -> PyResult<f64> {
2933 Ok(self
2934 .inner
2935 .lock()
2936 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2937 .Mstar)
2938 }
2939 #[getter]
2940 #[allow(non_snake_case)]
2941 fn N(&self) -> PyResult<f64> {
2942 Ok(self
2943 .inner
2944 .lock()
2945 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2946 .N)
2947 }
2948 #[getter]
2949 #[allow(non_snake_case)]
2950 fn M(&self) -> PyResult<f64> {
2951 Ok(self
2952 .inner
2953 .lock()
2954 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2955 .M)
2956 }
2957 #[getter]
2958 fn x_feed_j(&self) -> PyResult<f64> {
2959 Ok(self
2960 .inner
2961 .lock()
2962 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2963 .x_feed_j)
2964 }
2965 #[getter]
2966 fn x_prod_j(&self) -> PyResult<f64> {
2967 Ok(self
2968 .inner
2969 .lock()
2970 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2971 .x_prod_j)
2972 }
2973 #[getter]
2974 fn x_tail_j(&self) -> PyResult<f64> {
2975 Ok(self
2976 .inner
2977 .lock()
2978 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2979 .x_tail_j)
2980 }
2981 #[getter]
2982 fn l_t_per_feed(&self) -> PyResult<f64> {
2983 Ok(self
2984 .inner
2985 .lock()
2986 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2987 .l_t_per_feed)
2988 }
2989 #[getter]
2990 fn swu_per_feed(&self) -> PyResult<f64> {
2991 Ok(self
2992 .inner
2993 .lock()
2994 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2995 .swu_per_feed)
2996 }
2997 #[getter]
2998 fn swu_per_prod(&self) -> PyResult<f64> {
2999 Ok(self
3000 .inner
3001 .lock()
3002 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3003 .swu_per_prod)
3004 }
3005 #[getter]
3007 fn mat_feed(&self) -> PyResult<BTreeMap<String, f64>> {
3008 Ok(self
3009 .inner
3010 .lock()
3011 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3012 .mat_feed
3013 .comp
3014 .iter()
3015 .map(|(id, frac)| (id.to_name(), *frac))
3016 .collect())
3017 }
3018 #[getter]
3020 fn mat_prod(&self) -> PyResult<BTreeMap<String, f64>> {
3021 Ok(self
3022 .inner
3023 .lock()
3024 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3025 .mat_prod
3026 .comp
3027 .iter()
3028 .map(|(id, frac)| (id.to_name(), *frac))
3029 .collect())
3030 }
3031 #[getter]
3033 fn mat_tail(&self) -> PyResult<BTreeMap<String, f64>> {
3034 Ok(self
3035 .inner
3036 .lock()
3037 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3038 .mat_tail
3039 .comp
3040 .iter()
3041 .map(|(id, frac)| (id.to_name(), *frac))
3042 .collect())
3043 }
3044 fn separative_work_per_product(&self) -> PyResult<f64> {
3046 let c = self
3047 .inner
3048 .lock()
3049 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
3050 Ok(nucleide_enrichment::swu_per_prod(
3051 c.x_feed_j, c.x_prod_j, c.x_tail_j,
3052 ))
3053 }
3054
3055 fn __repr__(&self) -> PyResult<String> {
3056 let c = self
3057 .inner
3058 .lock()
3059 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
3060 Ok(format!(
3061 "Cascade(alpha={}, Mstar={}, x_prod_j={:.5})",
3062 c.alpha, c.Mstar, c.x_prod_j
3063 ))
3064 }
3065}
3066
3067#[pyfunction]
3071fn enrichment_value_func(x: f64) -> f64 {
3072 nucleide_enrichment::value_func(x)
3073}
3074
3075#[pyfunction]
3079fn enrichment_swu_per_feed(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
3080 nucleide_enrichment::swu_per_feed(x_feed, x_prod, x_tail)
3081}
3082
3083#[pyfunction]
3087fn enrichment_swu_per_prod(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
3088 nucleide_enrichment::swu_per_prod(x_feed, x_prod, x_tail)
3089}
3090
3091#[pyfunction]
3095fn enrichment_swu_per_tail(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
3096 nucleide_enrichment::swu_per_tail(x_feed, x_prod, x_tail)
3097}
3098
3099#[pyclass(name = "MaterialsCompendium")]
3101struct PyMaterialsCompendium {
3102 inner: nucleide_material::MaterialsLibrary,
3103}
3104
3105#[pymethods]
3106impl PyMaterialsCompendium {
3107 #[staticmethod]
3109 fn load(path: &str) -> PyResult<Self> {
3110 nucleide_material::MaterialsLibrary::from_file(path)
3111 .map(|inner| PyMaterialsCompendium { inner })
3112 .map_err(|e| PyValueError::new_err(e.to_string()))
3113 }
3114
3115 fn __len__(&self) -> usize {
3116 self.inner.len()
3117 }
3118
3119 fn names(&self) -> Vec<String> {
3121 self.inner.names().into_iter().map(String::from).collect()
3122 }
3123
3124 #[pyo3(signature = (name, as_material=false))]
3128 #[allow(clippy::type_complexity)]
3129 fn get(&self, name: &str, as_material: bool) -> PyResult<Option<BTreeMap<String, Py<PyAny>>>> {
3130 let entry = match self.inner.get(name) {
3131 Some(e) => e,
3132 None => return Ok(None),
3133 };
3134 let named_fractions = if as_material {
3136 Some(
3137 entry
3138 .to_material()
3139 .map_err(|e| PyValueError::new_err(e.to_string()))?,
3140 )
3141 } else {
3142 None
3143 };
3144
3145 Ok(Python::attach(|py| {
3146 let mut d: BTreeMap<String, Py<PyAny>> = BTreeMap::new();
3147 d.insert(
3148 "name".into(),
3149 entry
3150 .name
3151 .as_str()
3152 .into_pyobject(py)
3153 .unwrap()
3154 .unbind()
3155 .into_any(),
3156 );
3157 d.insert(
3158 "mat_num".into(),
3159 entry.mat_num.into_pyobject(py).unwrap().unbind().into_any(),
3160 );
3161 d.insert(
3162 "density".into(),
3163 entry.density.into_pyobject(py).unwrap().unbind().into_any(),
3164 );
3165 match &named_fractions {
3166 Some(mat) => {
3167 let fr: BTreeMap<String, f64> =
3168 mat.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect();
3169 d.insert(
3170 "fractions".into(),
3171 fr.into_pyobject(py).unwrap().unbind().into_any(),
3172 );
3173 }
3174 None => {
3175 let fr = entry.weight_fractions();
3176 d.insert(
3177 "fractions".into(),
3178 fr.into_pyobject(py).unwrap().unbind().into_any(),
3179 );
3180 }
3181 }
3182 Some(d)
3183 }))
3184 }
3185}
3186
3187#[pyfunction]
3196fn isotxs_parse(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
3197 let owned = text.to_owned();
3198 let lib = py
3199 .detach(move || nucleide_cccc_io::IsotxsLib::parse(&owned))
3200 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3201 Ok(isotxs_to_py(py, &lib))
3202}
3203
3204fn isotxs_to_py(py: Python<'_>, lib: &nucleide_cccc_io::IsotxsLib) -> Py<PyAny> {
3205 use pyo3::types::PyDict;
3206 let out = PyDict::new(py);
3207 let nuclides: Vec<Py<PyAny>> = lib
3208 .nuclides
3209 .iter()
3210 .map(|n| {
3211 let d = PyDict::new(py);
3212 d.set_item("label", &n.label).ok();
3213 d.set_item("zaid", &n.zaid).ok();
3214 d.set_item("groups", n.groups).ok();
3215 d.set_item("total_xs", n.total_xs.clone()).ok();
3216 d.into_any().unbind()
3217 })
3218 .collect();
3219 out.set_item("nuclides", nuclides).ok();
3220 out.into_any().unbind()
3221}
3222
3223#[pyfunction]
3229#[pyo3(signature = (text, kind="rtflux"))]
3230fn rtflux_parse(py: Python<'_>, text: &str, kind: &str) -> PyResult<Py<PyAny>> {
3231 let flux_kind = match kind.to_ascii_lowercase().as_str() {
3232 "rtflux" => nucleide_cccc_io::rtflux::FluxKind::Rtflux,
3233 "atflux" => nucleide_cccc_io::rtflux::FluxKind::Atflux,
3234 "rzflux" => nucleide_cccc_io::rtflux::FluxKind::Rzflux,
3235 other => {
3236 return Err(PyValueError::new_err(format!(
3237 "kind must be rtflux|atflux|rzflux, got `{other}`"
3238 )))
3239 }
3240 };
3241 let owned = text.to_owned();
3242 let flux = py
3243 .detach(move || nucleide_cccc_io::FluxFile::parse(flux_kind, &owned))
3244 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3245 use pyo3::types::PyDict;
3246 let d = PyDict::new(py);
3247 d.set_item("kind", flux.kind.keyword()).ok();
3248 d.set_item("groups", flux.groups).ok();
3249 d.set_item("per_point", flux.per_point).ok();
3250 d.set_item("npoints", flux.npoints()).ok();
3251 d.set_item("values", flux.values.clone()).ok();
3252 d.set_item("total", flux.total()).ok();
3253 Ok(d.into_any().unbind())
3254}
3255
3256fn partisn_deck_from_dict(
3257 deck: &Bound<'_, pyo3::types::PyDict>,
3258) -> PyResult<nucleide_cccc_io::PartisnDeck> {
3259 let title: String = match deck.get_item("title")? {
3260 Some(v) => v
3261 .extract()
3262 .map_err(|_| PyValueError::new_err("partisn deck `title` must be str"))?,
3263 None => return Err(PyValueError::new_err("partisn deck missing `title`")),
3264 };
3265 let dim: u8 = match deck.get_item("dim")? {
3266 Some(v) => v
3267 .extract()
3268 .map_err(|_| PyValueError::new_err("partisn deck `dim` must be 1, 2, or 3"))?,
3269 None => return Err(PyValueError::new_err("partisn deck missing `dim`")),
3270 };
3271 let zones_value = match deck.get_item("zones")? {
3272 Some(v) => v,
3273 None => return Err(PyValueError::new_err("partisn deck missing `zones`")),
3274 };
3275 let zone_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = zones_value
3276 .extract()
3277 .map_err(|_| PyValueError::new_err("partisn deck `zones` must be a list of dicts"))?;
3278 let mut zones = Vec::with_capacity(zone_dicts.len());
3279 for z in &zone_dicts {
3280 let id: u32 = match z.get_item("id")? {
3281 Some(v) => v
3282 .extract()
3283 .map_err(|_| PyValueError::new_err("partisn zone `id` must be int"))?,
3284 None => return Err(PyValueError::new_err("partisn zone missing `id`")),
3285 };
3286 let material: String = match z.get_item("material")? {
3287 Some(v) => v
3288 .extract()
3289 .map_err(|_| PyValueError::new_err("partisn zone `material` must be str"))?,
3290 None => return Err(PyValueError::new_err("partisn zone missing `material`")),
3291 };
3292 let isotxs_labels: Vec<String> = match z.get_item("isotxs_labels")? {
3293 Some(v) => v.extract().map_err(|_| {
3294 PyValueError::new_err("partisn zone `isotxs_labels` must be a list of str")
3295 })?,
3296 None => {
3297 return Err(PyValueError::new_err(
3298 "partisn zone missing `isotxs_labels`",
3299 ))
3300 }
3301 };
3302 let density: f64 = match z.get_item("density")? {
3303 Some(v) => v
3304 .extract()
3305 .map_err(|_| PyValueError::new_err("partisn zone `density` must be float"))?,
3306 None => return Err(PyValueError::new_err("partisn zone missing `density`")),
3307 };
3308 zones.push(nucleide_cccc_io::partisn::PartisnZone {
3309 id,
3310 material,
3311 isotxs_labels,
3312 density,
3313 });
3314 }
3315 let source: Option<String> = match deck.get_item("source")? {
3316 Some(v) if v.is_none() => None,
3317 Some(v) => Some(
3318 v.extract()
3319 .map_err(|_| PyValueError::new_err("partisn deck `source` must be str or None"))?,
3320 ),
3321 None => None,
3322 };
3323 Ok(nucleide_cccc_io::PartisnDeck {
3324 title,
3325 dim,
3326 zones,
3327 source,
3328 })
3329}
3330
3331#[pyfunction]
3336fn partisn_render(py: Python<'_>, deck: &Bound<'_, pyo3::types::PyDict>) -> PyResult<String> {
3337 let rust_deck = partisn_deck_from_dict(deck)?;
3338 Ok(py.detach(move || rust_deck.render()))
3339}
3340
3341#[pyfunction]
3346fn partisn_validate(
3347 py: Python<'_>,
3348 deck: &Bound<'_, pyo3::types::PyDict>,
3349 isotxs_text: &str,
3350) -> PyResult<()> {
3351 let rust_deck = partisn_deck_from_dict(deck)?;
3352 let owned = isotxs_text.to_owned();
3353 let lib = py
3354 .detach(move || nucleide_cccc_io::IsotxsLib::parse(&owned))
3355 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3356 rust_deck
3357 .validate(&lib)
3358 .map_err(|e| PyValueError::new_err(e.to_string()))
3359}
3360
3361fn fispact_row_to_map(
3366 py: Python<'_>,
3367 r: &nucleide_alara_io::output::ResponseRow,
3368) -> BTreeMap<String, Py<PyAny>> {
3369 let mut d = BTreeMap::new();
3370 d.insert(
3371 "time_s".to_string(),
3372 r.time_s.into_pyobject(py).unwrap().unbind().into_any(),
3373 );
3374 d.insert(
3375 "time_label".to_string(),
3376 r.time_label
3377 .clone()
3378 .into_pyobject(py)
3379 .unwrap()
3380 .unbind()
3381 .into_any(),
3382 );
3383 d.insert(
3384 "nuclide".to_string(),
3385 r.nuclide
3386 .clone()
3387 .into_pyobject(py)
3388 .unwrap()
3389 .unbind()
3390 .into_any(),
3391 );
3392 d.insert(
3393 "half_life_s".to_string(),
3394 r.half_life_s.into_pyobject(py).unwrap().unbind().into_any(),
3395 );
3396 d.insert(
3397 "run_lbl".to_string(),
3398 r.run_lbl
3399 .clone()
3400 .into_pyobject(py)
3401 .unwrap()
3402 .unbind()
3403 .into_any(),
3404 );
3405 d.insert(
3406 "block".to_string(),
3407 r.block
3408 .as_str()
3409 .into_pyobject(py)
3410 .unwrap()
3411 .unbind()
3412 .into_any(),
3413 );
3414 d.insert(
3415 "block_name".to_string(),
3416 r.block_name
3417 .clone()
3418 .into_pyobject(py)
3419 .unwrap()
3420 .unbind()
3421 .into_any(),
3422 );
3423 d.insert(
3424 "block_num".to_string(),
3425 r.block_num.into_pyobject(py).unwrap().unbind().into_any(),
3426 );
3427 d.insert(
3428 "variable".to_string(),
3429 r.variable
3430 .as_str()
3431 .into_pyobject(py)
3432 .unwrap()
3433 .unbind()
3434 .into_any(),
3435 );
3436 d.insert(
3437 "var_unit".to_string(),
3438 r.var_unit
3439 .clone()
3440 .into_pyobject(py)
3441 .unwrap()
3442 .unbind()
3443 .into_any(),
3444 );
3445 d.insert(
3446 "value".to_string(),
3447 r.value.into_pyobject(py).unwrap().unbind().into_any(),
3448 );
3449 d
3450}
3451
3452#[pyfunction]
3458fn fispact_parse_output(
3459 py: Python<'_>,
3460 text: &str,
3461 run_lbl: &str,
3462) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
3463 let owned_text = text.to_owned();
3464 let owned_lbl = run_lbl.to_owned();
3465 let rows = py
3466 .detach(move || {
3467 nucleide_fispact_io::parse_to_frame(&owned_text, &owned_lbl).map(|f| f.rows)
3468 })
3469 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3470 Ok(rows.iter().map(|r| fispact_row_to_map(py, r)).collect())
3471}
3472
3473#[pyfunction]
3483fn origen_parse_tape5(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
3484 let owned = text.to_owned();
3485 let tape = py
3486 .detach(move || nucleide_origen_io::Tape5::parse(&owned))
3487 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3488 use pyo3::types::PyDict;
3489 let out = PyDict::new(py);
3490 out.set_item("titles", tape.titles.clone()).ok();
3491 let steps: Vec<Py<PyAny>> = tape
3492 .irradiation_steps
3493 .iter()
3494 .map(|s| {
3495 let d = PyDict::new(py);
3496 d.set_item("flux", s.flux).ok();
3497 d.set_item("days", s.days).ok();
3498 d.into_any().unbind()
3499 })
3500 .collect();
3501 out.set_item("irradiation_steps", steps).ok();
3502 let materials: Vec<Py<PyAny>> = tape
3503 .materials
3504 .iter()
3505 .map(|m| {
3506 let d = PyDict::new(py);
3507 d.set_item("name", &m.name).ok();
3508 let entries: Vec<Py<PyAny>> = m
3509 .grams
3510 .iter()
3511 .map(|(nuclide, grams)| {
3512 let e = PyDict::new(py);
3513 e.set_item("nuclide", nuclide).ok();
3514 e.set_item("grams", *grams).ok();
3515 e.into_any().unbind()
3516 })
3517 .collect();
3518 d.set_item("entries", entries).ok();
3519 d.into_any().unbind()
3520 })
3521 .collect();
3522 out.set_item("materials", materials).ok();
3523 Ok(out.into_any().unbind())
3524}
3525
3526#[pyfunction]
3531fn origen_parse_tape6(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
3532 let owned = text.to_owned();
3533 let tape = py
3534 .detach(move || nucleide_origen_io::Tape6::parse(&owned))
3535 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3536 use pyo3::types::PyDict;
3537 let out = PyDict::new(py);
3538 let records: Vec<Py<PyAny>> = tape
3539 .records
3540 .iter()
3541 .map(|r| {
3542 let d = PyDict::new(py);
3543 d.set_item("nuclide", &r.nuclide).ok();
3544 d.set_item("grams", r.grams).ok();
3545 d.set_item("activity_bq", r.activity_bq).ok();
3546 d.into_any().unbind()
3547 })
3548 .collect();
3549 out.set_item("records", records).ok();
3550 out.set_item("total_activity", tape.total_activity()).ok();
3551 Ok(out.into_any().unbind())
3552}
3553
3554#[pyfunction]
3558fn origen_parse_tape9(py: Python<'_>, text: &str) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
3559 let owned = text.to_owned();
3560 let entries = py
3561 .detach(move || nucleide_origen_io::Tape9Entry::parse(&owned))
3562 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3563 Ok(entries
3564 .iter()
3565 .map(|e| {
3566 let mut d = BTreeMap::new();
3567 d.insert(
3568 "nuclide".to_string(),
3569 e.nuclide
3570 .clone()
3571 .into_pyobject(py)
3572 .unwrap()
3573 .unbind()
3574 .into_any(),
3575 );
3576 d.insert(
3577 "decay_const".to_string(),
3578 e.decay_const.into_pyobject(py).unwrap().unbind().into_any(),
3579 );
3580 d
3581 })
3582 .collect())
3583}
3584
3585fn r2s_workflow_to_py(py: Python<'_>, workflow: &nucleide_r2s::R2sWorkflow) -> Py<PyAny> {
3590 use pyo3::types::PyDict;
3591 let out = PyDict::new(py);
3592 let steps: Vec<Py<PyAny>> = workflow
3593 .steps
3594 .iter()
3595 .map(|s| {
3596 let d = PyDict::new(py);
3597 d.set_item("zone", &s.zone).ok();
3598 d.set_item("flux", &s.flux).ok();
3599 d.into_any().unbind()
3600 })
3601 .collect();
3602 out.set_item("steps", steps).ok();
3603 out.set_item("cooling_s", workflow.cooling_s.clone()).ok();
3604 out.set_item("top_schedule", &workflow.top_schedule).ok();
3605 out.into_any().unbind()
3606}
3607
3608fn r2s_workflow_from_dict(
3609 workflow: &Bound<'_, pyo3::types::PyDict>,
3610) -> PyResult<nucleide_r2s::R2sWorkflow> {
3611 let steps_value = match workflow.get_item("steps")? {
3612 Some(v) => v,
3613 None => return Err(PyValueError::new_err("r2s workflow missing `steps`")),
3614 };
3615 let step_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = steps_value
3616 .extract()
3617 .map_err(|_| PyValueError::new_err("r2s workflow `steps` must be a list of dicts"))?;
3618 let mut steps = Vec::with_capacity(step_dicts.len());
3619 for s in &step_dicts {
3620 let zone: String = match s.get_item("zone")? {
3621 Some(v) => v
3622 .extract()
3623 .map_err(|_| PyValueError::new_err("r2s step `zone` must be str"))?,
3624 None => return Err(PyValueError::new_err("r2s step missing `zone`")),
3625 };
3626 let flux: String = match s.get_item("flux")? {
3627 Some(v) => v
3628 .extract()
3629 .map_err(|_| PyValueError::new_err("r2s step `flux` must be str"))?,
3630 None => return Err(PyValueError::new_err("r2s step missing `flux`")),
3631 };
3632 steps.push(nucleide_r2s::R2sStep { zone, flux });
3633 }
3634 let cooling_s: Vec<f64> = match workflow.get_item("cooling_s")? {
3635 Some(v) => v.extract().map_err(|_| {
3636 PyValueError::new_err("r2s workflow `cooling_s` must be a list of float")
3637 })?,
3638 None => return Err(PyValueError::new_err("r2s workflow missing `cooling_s`")),
3639 };
3640 let top_schedule: String = match workflow.get_item("top_schedule")? {
3641 Some(v) => v
3642 .extract()
3643 .map_err(|_| PyValueError::new_err("r2s workflow `top_schedule` must be str"))?,
3644 None => return Err(PyValueError::new_err("r2s workflow missing `top_schedule`")),
3645 };
3646 Ok(nucleide_r2s::R2sWorkflow {
3647 steps,
3648 cooling_s,
3649 top_schedule,
3650 })
3651}
3652
3653#[pyfunction]
3658fn r2s_from_deck(py: Python<'_>, deck_text: &str) -> PyResult<Py<PyAny>> {
3659 let owned = deck_text.to_owned();
3660 let workflow = py
3661 .detach(move || {
3662 let deck = nucleide_alara_io::AlaraDeck::parse(&owned)
3663 .map_err(|e| nucleide_r2s::Error::Invalid(e.to_string()))?;
3664 nucleide_r2s::R2sWorkflow::from_deck(&deck)
3665 })
3666 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3667 Ok(r2s_workflow_to_py(py, &workflow))
3668}
3669
3670#[pyfunction]
3675fn r2s_validate(
3676 py: Python<'_>,
3677 workflow: &Bound<'_, pyo3::types::PyDict>,
3678 deck_text: &str,
3679) -> PyResult<()> {
3680 let rust_workflow = r2s_workflow_from_dict(workflow)?;
3681 let owned = deck_text.to_owned();
3682 let deck = py
3683 .detach(move || nucleide_alara_io::AlaraDeck::parse(&owned))
3684 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3685 rust_workflow
3686 .validate_against(&deck)
3687 .map_err(|e| PyValueError::new_err(e.to_string()))
3688}
3689
3690#[pyfunction]
3695#[pyo3(signature = (deck_text, top=None))]
3696fn r2s_expand(
3697 py: Python<'_>,
3698 deck_text: &str,
3699 top: Option<&str>,
3700) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
3701 let owned_text = deck_text.to_owned();
3702 let owned_top = top.map(str::to_owned);
3703 let steps = py
3704 .detach(move || {
3705 let deck = nucleide_alara_io::AlaraDeck::parse(&owned_text)
3706 .map_err(|e| nucleide_r2s::Error::Invalid(e.to_string()))?;
3707 let mut workflow = nucleide_r2s::R2sWorkflow::from_deck(&deck)?;
3708 if let Some(top) = owned_top {
3709 workflow.top_schedule = top;
3710 }
3711 workflow.expand(&deck, &[])
3712 })
3713 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3714 Ok(steps
3715 .into_iter()
3716 .map(|s| {
3717 let mut d = BTreeMap::new();
3718 let cooling = s.is_cooling();
3719 d.insert(
3720 "duration_s".to_string(),
3721 s.duration_s.into_pyobject(py).unwrap().unbind().into_any(),
3722 );
3723 d.insert(
3724 "flux".to_string(),
3725 s.flux.into_pyobject(py).unwrap().unbind().into_any(),
3726 );
3727 d.insert(
3728 "is_cooling".to_string(),
3729 pyo3::types::PyBool::new(py, cooling)
3730 .to_owned()
3731 .into_any()
3732 .unbind(),
3733 );
3734 d
3735 })
3736 .collect())
3737}
3738
3739#[pyfunction]
3750fn r2s_assemble(
3751 py: Python<'_>,
3752 output_text: &str,
3753 run_lbl: &str,
3754 zone: &str,
3755 groups: usize,
3756) -> PyResult<Py<PyAny>> {
3757 let owned_text = output_text.to_owned();
3758 let owned_lbl = run_lbl.to_owned();
3759 let owned_zone = zone.to_owned();
3760 let source = py
3761 .detach(move || {
3762 let frame = nucleide_alara_io::output::ResponseFrame::parse(&owned_text, &owned_lbl)
3763 .map_err(|e| nucleide_r2s::Error::Invalid(e.to_string()))?;
3764 Ok::<_, nucleide_r2s::Error>(nucleide_r2s::photon::assemble(
3765 &frame,
3766 &owned_zone,
3767 groups,
3768 ))
3769 })
3770 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3771 use pyo3::types::PyDict;
3772 let out = PyDict::new(py);
3773 out.set_item("zone", source.zone.clone()).ok();
3774 out.set_item("groups", source.groups.clone()).ok();
3775 out.set_item("total", source.total()).ok();
3776 Ok(out.into_any().unbind())
3777}
3778
3779#[pyfunction]
3788#[pyo3(signature = (totals, zone_of_voxel, split=false))]
3789fn r2s_tag_zone_strength(
3790 py: Python<'_>,
3791 totals: Vec<f64>,
3792 zone_of_voxel: Vec<usize>,
3793 split: bool,
3794) -> PyResult<Py<PyAny>> {
3795 let zones: Vec<nucleide_r2s::photon::ZonePhotonSource> = totals
3796 .into_iter()
3797 .enumerate()
3798 .map(|(i, total)| {
3799 let groups = if total == 0.0 {
3800 Vec::new()
3801 } else {
3802 vec![total]
3803 };
3804 nucleide_r2s::photon::ZonePhotonSource {
3805 zone: format!("zone{i}"),
3806 groups,
3807 }
3808 })
3809 .collect();
3810 let tags = if split {
3811 nucleide_r2s::tags::split_zone_totals(&zones, &zone_of_voxel)
3812 } else {
3813 nucleide_r2s::tags::tag_zone_totals(&zones, &zone_of_voxel)
3814 }
3815 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3816 use pyo3::types::PyDict;
3817 let out = PyDict::new(py);
3818 out.set_item("n_zones", tags.n_zones).ok();
3819 out.set_item("zone_of_voxel", tags.zone_of_voxel.clone())
3820 .ok();
3821 out.set_item("source_strength", tags.source_strength.clone())
3822 .ok();
3823 out.set_item("decay_time_s", tags.decay_time_s.clone()).ok();
3824 out.set_item("total", tags.total_strength()).ok();
3825 Ok(out.into_any().unbind())
3826}
3827
3828#[pyfunction]
3837fn r2s_photon_group_sums(
3838 py: Python<'_>,
3839 photon_text: &str,
3840 nuclides: Vec<String>,
3841 time_s: f64,
3842) -> PyResult<Py<PyAny>> {
3843 let source = nucleide_alara_io::photon::PhotonSource::from_str(photon_text)
3844 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3845 let names: Vec<&str> = nuclides.iter().map(String::as_str).collect();
3846 let at = nucleide_r2s::tags::photon_groups_at(&source, &names, time_s);
3847 let sums = nucleide_r2s::tags::sum_group_strengths(&at)
3848 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3849 use pyo3::types::PyDict;
3850 let out = PyDict::new(py);
3851 let rows: Vec<Py<PyAny>> = at
3852 .iter()
3853 .map(|g| {
3854 let d = PyDict::new(py);
3855 d.set_item("nuclide", g.nuclide.clone()).ok();
3856 d.set_item("time_s", g.time_s).ok();
3857 d.set_item("strengths", g.strengths.clone()).ok();
3858 d.into_any().unbind()
3859 })
3860 .collect();
3861 out.set_item("groups", rows).ok();
3862 out.set_item("sums", sums.clone()).ok();
3863 out.set_item("total", sums.iter().sum::<f64>()).ok();
3864 Ok(out.into_any().unbind())
3865}
3866
3867fn snapshot_dict_str(
3868 zone: &Bound<'_, pyo3::types::PyDict>,
3869 key: &str,
3870 what: &str,
3871) -> PyResult<String> {
3872 match zone.get_item(key)? {
3873 Some(v) => v
3874 .extract()
3875 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be str"))),
3876 None => Err(PyValueError::new_err(format!(
3877 "snapshot {what} missing `{key}`"
3878 ))),
3879 }
3880}
3881
3882fn snapshot_dict_opt_str(
3883 zone: &Bound<'_, pyo3::types::PyDict>,
3884 key: &str,
3885 what: &str,
3886) -> PyResult<Option<String>> {
3887 match zone.get_item(key)? {
3888 Some(v) if v.is_none() => Ok(None),
3889 Some(v) => v
3890 .extract::<String>()
3891 .map(Some)
3892 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be str"))),
3893 None => Ok(None),
3894 }
3895}
3896
3897fn snapshot_dict_f64(
3898 zone: &Bound<'_, pyo3::types::PyDict>,
3899 key: &str,
3900 what: &str,
3901) -> PyResult<f64> {
3902 match zone.get_item(key)? {
3903 Some(v) => v
3904 .extract()
3905 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be float"))),
3906 None => Err(PyValueError::new_err(format!(
3907 "snapshot {what} missing `{key}`"
3908 ))),
3909 }
3910}
3911
3912fn snapshot_dict_opt_f64(
3913 zone: &Bound<'_, pyo3::types::PyDict>,
3914 key: &str,
3915 what: &str,
3916) -> PyResult<Option<f64>> {
3917 match zone.get_item(key)? {
3918 Some(v) if v.is_none() => Ok(None),
3919 Some(v) => v
3920 .extract::<f64>()
3921 .map(Some)
3922 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be float"))),
3923 None => Ok(None),
3924 }
3925}
3926
3927fn snapshot_zone_from_dict(
3928 zone: &Bound<'_, pyo3::types::PyDict>,
3929) -> PyResult<nucleide_r2s::snapshot::SnapshotZone> {
3930 let id = snapshot_dict_str(zone, "id", "zone")?;
3931 let volume_cm3 = snapshot_dict_f64(zone, "volume_cm3", "zone")?;
3932 let composition: BTreeMap<String, f64> = match zone.get_item("composition")? {
3933 Some(v) => v.extract().map_err(|_| {
3934 PyValueError::new_err("snapshot zone `composition` must be a dict of str to float")
3935 })?,
3936 None => return Err(PyValueError::new_err("snapshot zone missing `composition`")),
3937 };
3938 Ok(nucleide_r2s::snapshot::SnapshotZone {
3939 zone: id,
3940 volume_cm3,
3941 zbottom_cm: snapshot_dict_opt_f64(zone, "zbottom_cm", "zone")?,
3942 ztop_cm: snapshot_dict_opt_f64(zone, "ztop_cm", "zone")?,
3943 material: snapshot_dict_opt_str(zone, "material", "zone")?,
3944 xs_type: snapshot_dict_opt_str(zone, "xs_type", "zone")?,
3945 temperature_c: snapshot_dict_opt_f64(zone, "temperature_C", "zone")?,
3946 composition: composition.into_iter().collect(),
3947 flux_name: snapshot_dict_opt_str(zone, "flux", "zone")?,
3948 })
3949}
3950
3951fn snapshot_input_from_dict(
3952 snapshot: &Bound<'_, pyo3::types::PyDict>,
3953) -> PyResult<nucleide_r2s::snapshot::SnapshotInput> {
3954 let zone_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = match snapshot.get_item("zones")? {
3955 Some(v) => v
3956 .extract()
3957 .map_err(|_| PyValueError::new_err("snapshot `zones` must be a list of dicts"))?,
3958 None => return Err(PyValueError::new_err("snapshot missing `zones`")),
3959 };
3960 let mut zones = Vec::with_capacity(zone_dicts.len());
3961 for z in &zone_dicts {
3962 zones.push(snapshot_zone_from_dict(z)?);
3963 }
3964 let flux_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = match snapshot.get_item("flux_defs")? {
3965 Some(v) => v
3966 .extract()
3967 .map_err(|_| PyValueError::new_err("snapshot `flux_defs` must be a list of dicts"))?,
3968 None => return Err(PyValueError::new_err("snapshot missing `flux_defs`")),
3969 };
3970 let mut flux_defs = Vec::with_capacity(flux_dicts.len());
3971 for f in &flux_dicts {
3972 flux_defs.push(nucleide_r2s::snapshot::SnapshotFluxDef {
3973 name: snapshot_dict_str(f, "name", "flux")?,
3974 file: snapshot_dict_str(f, "file", "flux")?,
3975 scale: snapshot_dict_f64(f, "scale", "flux")?,
3976 });
3977 }
3978 let cooling_s: Vec<f64> = match snapshot.get_item("cooling_s")? {
3979 Some(v) => v
3980 .extract()
3981 .map_err(|_| PyValueError::new_err("snapshot `cooling_s` must be a list of float"))?,
3982 None => return Err(PyValueError::new_err("snapshot missing `cooling_s`")),
3983 };
3984 Ok(nucleide_r2s::snapshot::SnapshotInput {
3985 zones,
3986 flux_defs,
3987 cooling_s,
3988 schedule_text: snapshot_dict_opt_str(snapshot, "schedule_text", "snapshot")?,
3989 output: snapshot_dict_opt_str(snapshot, "output", "snapshot")?,
3990 })
3991}
3992
3993#[pyfunction]
4009fn r2s_from_snapshot(
4010 py: Python<'_>,
4011 snapshot: &Bound<'_, pyo3::types::PyDict>,
4012) -> PyResult<Py<PyAny>> {
4013 let input = snapshot_input_from_dict(snapshot)?;
4014 let (workflow, template, decks) = py
4015 .detach(move || nucleide_r2s::snapshot::snapshot_workflow(&input))
4016 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4017 use pyo3::types::PyDict;
4018 let out = PyDict::new(py);
4019 out.set_item("workflow", r2s_workflow_to_py(py, &workflow))
4020 .ok();
4021 out.set_item("deck", template.to_string()).ok();
4022 let deck_texts: Vec<String> = decks.iter().map(ToString::to_string).collect();
4023 out.set_item("decks", deck_texts).ok();
4024 Ok(out.into_any().unbind())
4025}
4026
4027fn parse_integrator(name: &str) -> PyResult<nucleide_depletion::Integrator> {
4045 use nucleide_depletion::Integrator as I;
4046 if name.eq_ignore_ascii_case("predictor") {
4047 return Ok(I::Predictor);
4048 }
4049 if name.eq_ignore_ascii_case("cecm") {
4050 return Ok(I::Cecm);
4051 }
4052 if name.eq_ignore_ascii_case("cf4") {
4053 return Ok(I::Cf4);
4054 }
4055 Err(PyValueError::new_err(format!(
4056 "unsupported integrator `{name}` (supported: predictor, cecm, cf4)"
4057 )))
4058}
4059
4060#[pyfunction]
4073#[pyo3(signature = (chain, n0, dts, rates=None, rates_list=None, integrator="predictor", order=48, method="cram48"))]
4074#[allow(clippy::too_many_arguments)]
4075fn deplete_series(
4076 chain: &PyChain,
4077 n0: BTreeMap<String, f64>,
4078 dts: Vec<f64>,
4079 rates: Option<RateMap>,
4080 rates_list: Option<Vec<Option<RateMap>>>,
4081 integrator: &str,
4082 order: u8,
4083 method: &str,
4084) -> PyResult<Py<PyAny>> {
4085 use nucleide_depletion::{DepletionSystem, ReactionRates, Step};
4086 let integrator = parse_integrator(integrator)?;
4087 let method = resolve_method(order, method)?;
4088 if let Some(list) = &rates_list {
4089 if list.len() != dts.len() {
4090 return Err(PyValueError::new_err(format!(
4091 "rates_list has {} entries but dts has {}",
4092 list.len(),
4093 dts.len()
4094 )));
4095 }
4096 }
4097 if dts.is_empty() {
4098 return Err(PyValueError::new_err("dts must not be empty"));
4099 }
4100 let mut n0_vec = vec![0.0; chain.inner.len()];
4102 for (name, value) in &n0 {
4103 let idx = chain.inner.index_of(name).ok_or_else(|| {
4104 PyValueError::new_err(format!("unknown nuclide `{name}` for this chain"))
4105 })?;
4106 n0_vec[idx] = *value;
4107 }
4108 let empty = BTreeMap::new();
4109 let mut steps = Vec::with_capacity(dts.len());
4110 for (i, dt) in dts.iter().enumerate() {
4111 let step_rates = rates_list
4112 .as_ref()
4113 .and_then(|list| list[i].as_ref())
4114 .or(rates.as_ref())
4115 .unwrap_or(&empty);
4116 let rs = split_rates(step_rates, &chain.inner)?;
4117 steps.push(Step::new(*dt, rs));
4118 }
4119 let template = DepletionSystem::build((*chain.inner).clone(), &ReactionRates::new())
4122 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4123 let series =
4126 nucleide_depletion::integrate_with_method(&template, &n0_vec, &steps, integrator, method)
4127 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4128 let names: Vec<&str> = template
4129 .chain
4130 .nuclides
4131 .iter()
4132 .map(|nuc| nuc.name.as_str())
4133 .collect();
4134 let keyed = |rows: &[Vec<f64>]| -> Vec<BTreeMap<String, f64>> {
4135 rows.iter()
4136 .map(|row| {
4137 names
4138 .iter()
4139 .zip(row)
4140 .map(|(name, v)| ((*name).to_string(), *v))
4141 .collect()
4142 })
4143 .collect()
4144 };
4145 let atoms = keyed(&series.atoms[1..]);
4147 let activity = keyed(&series.activity[1..]);
4148 let decay_heat = keyed(&series.decay_heat[1..]);
4149 let times = series.times[1..].to_vec();
4150 Ok(Python::attach(|py| {
4151 use pyo3::types::PyDict;
4152 let out = PyDict::new(py);
4153 out.set_item("times", ×).ok();
4154 out.set_item("atoms", &atoms).ok();
4155 out.set_item("activity", &activity).ok();
4156 out.set_item("decay_heat", &decay_heat).ok();
4157 out.into_any().unbind()
4158 }))
4159}
4160
4161#[pyfunction]
4166fn simple_xs(name: &str) -> PyResult<Option<(f64, f64)>> {
4167 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4168 Ok(nucleide_nuclei::data::simple_xs_by_name(name))
4169}
4170
4171#[pyfunction]
4176fn scattering_length(name: &str) -> PyResult<Option<f64>> {
4177 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4178 Ok(nucleide_nuclei::data::scattering_length_by_name(name).map(|(b_coh, _)| b_coh))
4179}
4180
4181#[pyfunction]
4185fn decay_energy(name: &str) -> PyResult<Option<f64>> {
4186 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4187 Ok(nucleide_nuclei::data::decay_energy_mev_by_name(name))
4188}
4189
4190#[pyfunction]
4196fn decay_branches(name: &str) -> PyResult<Vec<(String, f64, String)>> {
4197 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4198 Ok(nucleide_nuclei::data::decay_branches_by_name(name)
4199 .unwrap_or_default()
4200 .into_iter()
4201 .map(|b| {
4202 (
4203 nucleide_nuclei::NuclideId::from_nucid(b.progeny).to_name(),
4204 b.branching_fraction,
4205 b.mode.as_str().to_string(),
4206 )
4207 })
4208 .collect())
4209}
4210
4211#[pyfunction]
4217fn decay_branch_fraction(parent: &str, progeny: &str) -> PyResult<Option<f64>> {
4218 NuclideId::from_name(parent).map_err(wrap_nucid_err)?;
4219 NuclideId::from_name(progeny).map_err(wrap_nucid_err)?;
4220 Ok(nucleide_nuclei::data::branching_fraction_by_name(
4221 parent, progeny,
4222 ))
4223}
4224
4225type PyFissionYieldSets = Vec<(f64, Vec<(String, f64, f64)>)>;
4235
4236#[pyfunction]
4237#[pyo3(signature = (parent, origin="n", kind="independent"))]
4238fn fission_yields(parent: &str, origin: &str, kind: &str) -> PyResult<PyFissionYieldSets> {
4239 NuclideId::from_name(parent).map_err(wrap_nucid_err)?;
4240 let origin = nucleide_nuclei::data::FissionYieldOrigin::parse(origin).ok_or_else(|| {
4241 PyValueError::new_err(format!(
4242 "unknown fission-yield origin `{origin}` (expected `n` or `sf`)"
4243 ))
4244 })?;
4245 let kind = nucleide_nuclei::data::FissionYieldKind::parse(kind).ok_or_else(|| {
4246 PyValueError::new_err(format!(
4247 "unknown fission-yield kind `{kind}` (expected `independent` or `cumulative`)"
4248 ))
4249 })?;
4250 Ok(
4251 nucleide_nuclei::data::fission_yields_by_name(parent, origin, kind)
4252 .unwrap_or_default()
4253 .into_iter()
4254 .map(|set| {
4255 (
4256 set.energy_ev,
4257 set.products
4258 .into_iter()
4259 .map(|p| {
4260 (
4261 nucleide_nuclei::NuclideId::from_nucid(p.progeny).to_name(),
4262 p.yield_fraction,
4263 p.uncertainty,
4264 )
4265 })
4266 .collect(),
4267 )
4268 })
4269 .collect(),
4270 )
4271}
4272
4273#[pyfunction]
4280fn fission_yield(parent: &str, progeny: &str) -> PyResult<Option<f64>> {
4281 NuclideId::from_name(parent).map_err(wrap_nucid_err)?;
4282 NuclideId::from_name(progeny).map_err(wrap_nucid_err)?;
4283 Ok(nucleide_nuclei::data::fission_yield_by_name(
4284 parent, progeny,
4285 ))
4286}
4287
4288#[pyfunction]
4294fn normalize_nuclide(name: &str) -> PyResult<String> {
4295 Ok(nucleide_nuclei::dialects::normalize_nuclide_name(name)
4296 .map_err(|e| PyValueError::new_err(e.to_string()))?
4297 .to_name())
4298}
4299
4300#[pyfunction]
4307fn decay_heat(comp: BTreeMap<String, f64>) -> PyResult<f64> {
4308 let mat = comp_to_material(comp)?;
4309 let analytics = nucleide_material::Analytics {
4310 masses: &nucleide_material::Ame2020,
4311 decays: &nucleide_material::ChainDecays,
4312 };
4313 mat.total_decay_heat(&analytics, &nucleide_material::DecayEnergies)
4314 .map_err(|e| PyValueError::new_err(e.to_string()))
4315}
4316
4317fn parse_dose_pathway(s: &str) -> PyResult<nucleide_material::DosePathway> {
4318 nucleide_material::DosePathway::parse(s).ok_or_else(|| {
4319 PyValueError::new_err(format!(
4320 "unknown dose pathway `{s}` (supported: air, soil, ingest, inhale)"
4321 ))
4322 })
4323}
4324
4325fn parse_dose_source(s: &str) -> PyResult<nucleide_material::DoseSource> {
4326 nucleide_material::DoseSource::parse(s).ok_or_else(|| {
4327 PyValueError::new_err(format!(
4328 "unknown dose source `{s}` (supported: EPA, DOE, GENII)"
4329 ))
4330 })
4331}
4332
4333#[pyfunction]
4340#[pyo3(signature = (name, pathway, source="EPA"))]
4341fn dose_factor(name: &str, pathway: &str, source: &str) -> PyResult<Option<f64>> {
4342 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4343 let p = parse_dose_pathway(pathway)?;
4344 let s = parse_dose_source(source)?;
4345 Ok(nucleide_nuclei::data::dose_factor_by_name(name, p, s))
4346}
4347
4348#[pyfunction]
4360#[pyo3(signature = (comp, pathway, source="EPA"))]
4361fn dose_per_g(comp: BTreeMap<String, f64>, pathway: &str, source: &str) -> PyResult<f64> {
4362 let mat = comp_to_material(comp)?;
4363 let analytics = nucleide_material::Analytics {
4364 masses: &nucleide_material::Ame2020,
4365 decays: &nucleide_material::ChainDecays,
4366 };
4367 let p = parse_dose_pathway(pathway)?;
4368 let s = parse_dose_source(source)?;
4369 mat.total_dose_per_g(&analytics, &nucleide_material::DoseFactors, p, s)
4370 .map_err(|e| PyValueError::new_err(e.to_string()))
4371}
4372
4373#[pyfunction]
4381#[allow(clippy::type_complexity)]
4382fn separate_material(
4383 comp: BTreeMap<String, f64>,
4384 effs: BTreeMap<String, f64>,
4385) -> PyResult<(BTreeMap<String, f64>, BTreeMap<String, f64>)> {
4386 let mat = comp_to_material(comp)?;
4387 let mut table = Vec::with_capacity(effs.len());
4388 for (name, eff) in &effs {
4389 let id = NuclideId::from_name(name)
4390 .map_err(|e| PyValueError::new_err(format!("`{name}`: {e}")))?;
4391 table.push((id, *eff));
4392 }
4393 let (product, tails) = mat
4394 .separate(&table)
4395 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4396 let named =
4397 |m: nucleide_material::Material| m.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect();
4398 Ok((named(product), named(tails)))
4399}
4400
4401#[pyfunction]
4408fn blend_material(parts: Vec<(BTreeMap<String, f64>, f64)>) -> PyResult<BTreeMap<String, f64>> {
4409 let mats: Vec<nucleide_material::Material> = parts
4410 .iter()
4411 .map(|(comp, _)| comp_to_material(comp.clone()))
4412 .collect::<PyResult<_>>()?;
4413 let refs: Vec<(&nucleide_material::Material, f64)> =
4414 mats.iter().zip(parts.iter().map(|(_, r)| *r)).collect();
4415 let out = nucleide_material::Material::blend(&refs)
4416 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4417 Ok(out.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect())
4418}
4419
4420#[pyclass(name = "Cusum")]
4428struct PyCusum {
4429 inner: nucleide_material::Cusum,
4430}
4431
4432#[pymethods]
4433impl PyCusum {
4434 #[new]
4437 #[pyo3(signature = (ref_shift_k=0.5, alarm_h=4.0, startup=10))]
4438 fn new(ref_shift_k: f64, alarm_h: f64, startup: usize) -> PyResult<Self> {
4439 nucleide_material::Cusum::new(ref_shift_k, alarm_h, startup)
4440 .map(|inner| Self { inner })
4441 .map_err(|e| PyValueError::new_err(e.to_string()))
4442 }
4443
4444 fn update(&mut self, x: f64) -> bool {
4446 self.inner.update(x)
4447 }
4448
4449 fn status(&self) -> bool {
4451 self.inner.status()
4452 }
4453
4454 fn statistic(&self) -> f64 {
4456 self.inner.statistic()
4457 }
4458
4459 fn count(&self) -> usize {
4461 self.inner.count()
4462 }
4463
4464 fn mean(&self) -> f64 {
4466 self.inner.mean()
4467 }
4468
4469 fn variance(&self) -> f64 {
4471 self.inner.variance()
4472 }
4473
4474 fn std(&self) -> f64 {
4476 self.inner.std()
4477 }
4478
4479 fn reset(&mut self) {
4481 self.inner.reset();
4482 }
4483}
4484
4485#[pyclass(name = "DeckProblem")]
4491struct PyDeckProblem {
4492 inner: std::sync::Mutex<nucleide_mcnp_io::problem::DeckProblem>,
4493}
4494
4495fn deck_cell_dict(cell: &nucleide_mcnp_io::cell::CellCard) -> BTreeMap<String, String> {
4496 let mut d = BTreeMap::new();
4497 d.insert("num".to_string(), cell.num.to_string());
4498 d.insert("mat".to_string(), cell.mat.to_string());
4499 d.insert(
4500 "dens".to_string(),
4501 cell.dens.map(|v| v.to_string()).unwrap_or_default(),
4502 );
4503 d.insert("geom".to_string(), cell.geom.render());
4504 d.insert("params".to_string(), cell.params.join(" "));
4505 d
4506}
4507
4508#[pymethods]
4509impl PyDeckProblem {
4510 #[staticmethod]
4512 fn loads(text: &str) -> PyResult<Self> {
4513 nucleide_mcnp_io::problem::parse_deck(text)
4514 .map(|inner| Self {
4515 inner: std::sync::Mutex::new(inner),
4516 })
4517 .map_err(|e| PyValueError::new_err(e.to_string()))
4518 }
4519
4520 #[getter]
4522 fn message(&self) -> PyResult<String> {
4523 Ok(self
4524 .inner
4525 .lock()
4526 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4527 .message
4528 .clone())
4529 }
4530
4531 #[getter]
4533 fn title(&self) -> PyResult<String> {
4534 Ok(self
4535 .inner
4536 .lock()
4537 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4538 .title
4539 .clone())
4540 }
4541
4542 #[getter]
4545 fn cells(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4546 Ok(self
4547 .inner
4548 .lock()
4549 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4550 .cells
4551 .iter()
4552 .map(deck_cell_dict)
4553 .collect())
4554 }
4555
4556 #[getter]
4559 fn surfs(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4560 Ok(self
4561 .inner
4562 .lock()
4563 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4564 .surfs
4565 .iter()
4566 .map(|s| {
4567 let mut d = BTreeMap::new();
4568 d.insert("num".to_string(), s.num.to_string());
4569 d.insert("reflecting".to_string(), s.reflecting.to_string());
4570 d.insert(
4571 "transform".to_string(),
4572 s.transform.map(|v| v.to_string()).unwrap_or_default(),
4573 );
4574 d.insert(
4575 "periodic".to_string(),
4576 s.periodic.map(|v| v.to_string()).unwrap_or_default(),
4577 );
4578 d.insert("kind".to_string(), s.kind.keyword().to_string());
4579 d.insert(
4580 "coeffs".to_string(),
4581 s.coeffs
4582 .iter()
4583 .map(|v| v.to_string())
4584 .collect::<Vec<_>>()
4585 .join(" "),
4586 );
4587 d
4588 })
4589 .collect())
4590 }
4591
4592 #[getter]
4594 fn material_numbers(&self) -> PyResult<Vec<u32>> {
4595 Ok(self
4596 .inner
4597 .lock()
4598 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4599 .materials
4600 .iter()
4601 .map(|m| m.number)
4602 .collect())
4603 }
4604
4605 #[getter]
4607 fn data_names(&self) -> PyResult<Vec<String>> {
4608 Ok(self
4609 .inner
4610 .lock()
4611 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4612 .data
4613 .iter()
4614 .map(|d| d.name.clone())
4615 .collect())
4616 }
4617
4618 fn dumps(&self) -> PyResult<String> {
4620 let guard = self
4621 .inner
4622 .lock()
4623 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?;
4624 Ok(nucleide_mcnp_io::problem::write_deck(&guard))
4625 }
4626
4627 fn set_cell_density(&self, cell: u32, dens: f64) -> PyResult<()> {
4629 self.inner
4630 .lock()
4631 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4632 .set_cell_density(cell, dens)
4633 .map_err(|e| PyValueError::new_err(e.to_string()))
4634 }
4635
4636 fn set_cell_material(&self, cell: u32, mat: u32) -> PyResult<()> {
4638 self.inner
4639 .lock()
4640 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4641 .set_cell_material(cell, mat)
4642 .map_err(|e| PyValueError::new_err(e.to_string()))
4643 }
4644
4645 #[getter]
4647 fn mode(&self) -> PyResult<BTreeMap<String, String>> {
4648 let mode = self
4649 .inner
4650 .lock()
4651 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4652 .mode()
4653 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4654 let mut d = BTreeMap::new();
4655 d.insert("particles".to_string(), mode.particles.join(" "));
4656 Ok(d)
4657 }
4658
4659 #[getter]
4662 fn transforms(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4663 let transforms = self
4664 .inner
4665 .lock()
4666 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4667 .transforms()
4668 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4669 Ok(transforms
4670 .iter()
4671 .map(|t| {
4672 let mut d = BTreeMap::new();
4673 d.insert("number".to_string(), t.number.to_string());
4674 d.insert(
4675 "displacement".to_string(),
4676 t.displacement
4677 .iter()
4678 .map(|v| v.to_string())
4679 .collect::<Vec<_>>()
4680 .join(" "),
4681 );
4682 d.insert(
4683 "rotation".to_string(),
4684 t.rotation
4685 .iter()
4686 .map(|v| v.to_string())
4687 .collect::<Vec<_>>()
4688 .join(" "),
4689 );
4690 d.insert("in_degrees".to_string(), t.is_in_degrees.to_string());
4691 d.insert("main_to_aux".to_string(), t.is_main_to_aux.to_string());
4692 d.insert("hidden".to_string(), t.hidden.to_string());
4693 d
4694 })
4695 .collect())
4696 }
4697
4698 #[getter]
4701 fn universes(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4702 let universes = self
4703 .inner
4704 .lock()
4705 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4706 .universes()
4707 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4708 Ok(universes
4709 .iter()
4710 .map(|u| {
4711 let mut d = BTreeMap::new();
4712 d.insert("number".to_string(), u.number.to_string());
4713 d.insert(
4714 "cells".to_string(),
4715 u.cells
4716 .iter()
4717 .map(|v| v.to_string())
4718 .collect::<Vec<_>>()
4719 .join(" "),
4720 );
4721 d.insert(
4722 "not_truncated".to_string(),
4723 u.not_truncated
4724 .iter()
4725 .map(|v| v.to_string())
4726 .collect::<Vec<_>>()
4727 .join(" "),
4728 );
4729 d
4730 })
4731 .collect())
4732 }
4733
4734 #[getter]
4736 fn lattices(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4737 let lattices = self
4738 .inner
4739 .lock()
4740 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4741 .lattices()
4742 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4743 Ok(lattices
4744 .iter()
4745 .map(|l| {
4746 let mut d = BTreeMap::new();
4747 d.insert("cell".to_string(), l.cell.to_string());
4748 d.insert("lattice".to_string(), l.lattice.to_string());
4749 d
4750 })
4751 .collect())
4752 }
4753
4754 #[getter]
4758 fn fills(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4759 use nucleide_mcnp_io::semantic::{FillTarget, FillTransform};
4760 let fills = self
4761 .inner
4762 .lock()
4763 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4764 .fills()
4765 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4766 Ok(fills
4767 .iter()
4768 .map(|f| {
4769 let mut d = BTreeMap::new();
4770 d.insert("cell".to_string(), f.cell.to_string());
4771 match &f.target {
4772 FillTarget::Single(u) => {
4773 d.insert("kind".to_string(), "single".to_string());
4774 d.insert("universe".to_string(), u.to_string());
4775 d.insert("min_index".to_string(), String::new());
4776 d.insert("max_index".to_string(), String::new());
4777 d.insert("universes".to_string(), String::new());
4778 }
4779 FillTarget::Matrix {
4780 min_index,
4781 max_index,
4782 universes,
4783 } => {
4784 d.insert("kind".to_string(), "matrix".to_string());
4785 d.insert("universe".to_string(), String::new());
4786 d.insert(
4787 "min_index".to_string(),
4788 min_index
4789 .iter()
4790 .map(|v| v.to_string())
4791 .collect::<Vec<_>>()
4792 .join(" "),
4793 );
4794 d.insert(
4795 "max_index".to_string(),
4796 max_index
4797 .iter()
4798 .map(|v| v.to_string())
4799 .collect::<Vec<_>>()
4800 .join(" "),
4801 );
4802 d.insert(
4803 "universes".to_string(),
4804 universes
4805 .iter()
4806 .map(|u| {
4807 u.map(|v| v.to_string()).unwrap_or_else(|| "-".to_string())
4808 })
4809 .collect::<Vec<_>>()
4810 .join(" "),
4811 );
4812 }
4813 }
4814 match &f.transform {
4815 None => {
4816 d.insert("transform".to_string(), String::new());
4817 d.insert("hidden_transform".to_string(), String::new());
4818 }
4819 Some(FillTransform::Reference(n)) => {
4820 d.insert("transform".to_string(), n.to_string());
4821 d.insert("hidden_transform".to_string(), String::new());
4822 }
4823 Some(FillTransform::Hidden(t)) => {
4824 d.insert("transform".to_string(), String::new());
4825 let mut coords: Vec<String> =
4826 t.displacement.iter().map(|v| v.to_string()).collect();
4827 coords.extend(t.rotation.iter().map(|v| v.to_string()));
4828 d.insert("hidden_transform".to_string(), coords.join(" "));
4829 }
4830 }
4831 d.insert("in_degrees".to_string(), f.in_degrees.to_string());
4832 d
4833 })
4834 .collect())
4835 }
4836
4837 #[getter]
4839 fn importances(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4840 let importances = self
4841 .inner
4842 .lock()
4843 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4844 .importances()
4845 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4846 Ok(importances
4847 .iter()
4848 .map(|v| {
4849 let mut d = BTreeMap::new();
4850 d.insert("cell".to_string(), v.cell.to_string());
4851 d.insert("particle".to_string(), v.particle.clone());
4852 d.insert("value".to_string(), v.value.to_string());
4853 d
4854 })
4855 .collect())
4856 }
4857
4858 #[getter]
4860 fn volumes(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4861 let volumes = self
4862 .inner
4863 .lock()
4864 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4865 .volumes()
4866 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4867 Ok(volumes
4868 .iter()
4869 .map(|v| {
4870 let mut d = BTreeMap::new();
4871 d.insert("cell".to_string(), v.cell.to_string());
4872 d.insert("volume".to_string(), v.volume.to_string());
4873 d
4874 })
4875 .collect())
4876 }
4877
4878 #[getter]
4881 fn tallies(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4882 let tallies = self
4883 .inner
4884 .lock()
4885 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4886 .tallies()
4887 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4888 Ok(tallies
4889 .iter()
4890 .map(|t| {
4891 let mut d = BTreeMap::new();
4892 d.insert("number".to_string(), t.number.to_string());
4893 d.insert("type".to_string(), t.tally_type.to_string());
4894 d.insert("particles".to_string(), t.particles.join(","));
4895 d.insert("entries".to_string(), t.entries.join(" "));
4896 d.insert("fm".to_string(), t.fm.clone().unwrap_or_default().join(" "));
4897 d.insert(
4898 "e_bins".to_string(),
4899 t.e_bins.clone().unwrap_or_default().join(" "),
4900 );
4901 d
4902 })
4903 .collect())
4904 }
4905
4906 fn validate(&self) -> PyResult<()> {
4909 self.inner
4910 .lock()
4911 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4912 .validate()
4913 .map_err(|e| PyValueError::new_err(e.to_string()))
4914 }
4915
4916 fn validation_notes(&self) -> PyResult<Vec<String>> {
4918 Ok(self
4919 .inner
4920 .lock()
4921 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4922 .validation_notes())
4923 }
4924
4925 fn set_mode(&self, particles: Vec<String>) -> PyResult<()> {
4927 self.inner
4928 .lock()
4929 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4930 .set_mode(particles)
4931 .map_err(|e| PyValueError::new_err(e.to_string()))
4932 }
4933
4934 fn set_cell_universe(&self, cell: u32, universe: u32, not_truncated: bool) -> PyResult<()> {
4936 self.inner
4937 .lock()
4938 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4939 .set_cell_universe(cell, universe, not_truncated)
4940 .map_err(|e| PyValueError::new_err(e.to_string()))
4941 }
4942
4943 fn set_cell_lattice(&self, cell: u32, lattice: Option<u8>) -> PyResult<()> {
4945 self.inner
4946 .lock()
4947 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4948 .set_cell_lattice(cell, lattice)
4949 .map_err(|e| PyValueError::new_err(e.to_string()))
4950 }
4951
4952 fn set_cell_fill(&self, cell: u32, universe: u32) -> PyResult<()> {
4954 self.inner
4955 .lock()
4956 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4957 .set_cell_fill(cell, universe)
4958 .map_err(|e| PyValueError::new_err(e.to_string()))
4959 }
4960}
4961
4962#[pyfunction]
4964fn read_deck(path: &str) -> PyResult<PyDeckProblem> {
4965 nucleide_mcnp_io::problem::parse_deck_file(path)
4966 .map(|inner| PyDeckProblem {
4967 inner: std::sync::Mutex::new(inner),
4968 })
4969 .map_err(|e| PyValueError::new_err(e.to_string()))
4970}
4971
4972#[pyfunction]
4974fn parse_deck(text: &str) -> PyResult<PyDeckProblem> {
4975 PyDeckProblem::loads(text)
4976}
4977
4978#[pyclass(name = "Inventory")]
4980struct PyInventory {
4981 chain: std::sync::Arc<nucleide_depletion::Chain>,
4982 atoms: BTreeMap<String, f64>,
4983}
4984
4985fn inventory_sys(
4986 chain: &nucleide_depletion::Chain,
4987 rates: &RateMap,
4988) -> PyResult<nucleide_depletion::DepletionSystem> {
4989 let rs = split_rates(rates, chain)?;
4990 nucleide_depletion::DepletionSystem::build(chain.clone(), &rs)
4991 .map_err(|e| PyValueError::new_err(e.to_string()))
4992}
4993
4994fn parse_quantity_unit(unit: &str) -> PyResult<nucleide_depletion::QuantityUnit> {
4995 nucleide_depletion::QuantityUnit::from_str(unit)
4996 .map_err(|e| PyValueError::new_err(format!("{e:?}")))
4997}
4998
4999#[pymethods]
5000impl PyInventory {
5001 #[new]
5004 #[pyo3(signature = (chain, comp, units="atoms"))]
5005 fn new(chain: &PyChain, comp: BTreeMap<String, f64>, units: &str) -> PyResult<Self> {
5006 let unit = parse_quantity_unit(units)?;
5007 let sys = inventory_sys(&chain.inner, &BTreeMap::new())?;
5008 let inv = nucleide_depletion::DecayInventory::from_units(&comp, unit, &sys)
5009 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5010 Ok(Self {
5011 chain: chain.inner.clone(),
5012 atoms: inv.atoms,
5013 })
5014 }
5015
5016 fn numbers(&self) -> BTreeMap<String, f64> {
5018 self.atoms.clone()
5019 }
5020
5021 #[pyo3(signature = (dt, time_unit="s", rates=None, order=48, method="cram48"))]
5028 fn decay(
5029 &self,
5030 dt: f64,
5031 time_unit: &str,
5032 rates: Option<RateMap>,
5033 order: u8,
5034 method: &str,
5035 ) -> PyResult<Self> {
5036 let method = resolve_method(order, method)?;
5037 let unit = nucleide_depletion::inventory::time_unit_from_str(time_unit)
5038 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5039 let seconds = dt * unit.as_seconds();
5040 let empty = BTreeMap::new();
5041 let step_rates = rates.as_ref().unwrap_or(&empty);
5042 let template = inventory_sys(&self.chain, step_rates)?;
5043 let steps = vec![nucleide_depletion::Step::new(
5046 seconds,
5047 split_rates(step_rates, &self.chain)?,
5048 )];
5049 let series = nucleide_depletion::integrate_with_method(
5050 &template,
5051 &chain_vec(&self.chain, &self.atoms)?,
5052 &steps,
5053 nucleide_depletion::Integrator::Predictor,
5054 method,
5055 )
5056 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5057 let names: Vec<String> = self.chain.nuclides.iter().map(|n| n.name.clone()).collect();
5058 let atoms = names
5059 .iter()
5060 .zip(series.atoms.last().cloned().unwrap_or_default())
5061 .map(|(n, v)| (n.clone(), v))
5062 .collect();
5063 Ok(Self {
5064 chain: self.chain.clone(),
5065 atoms,
5066 })
5067 }
5068
5069 fn activities(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
5071 let unit = parse_quantity_unit(units)?;
5072 let sys = inventory_sys(&self.chain, &BTreeMap::new())?;
5073 let inv = nucleide_depletion::DecayInventory {
5074 atoms: self.atoms.clone(),
5075 };
5076 inv.activities(&sys, unit)
5077 .map_err(|e| PyValueError::new_err(e.to_string()))
5078 }
5079
5080 fn masses(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
5082 let unit = parse_quantity_unit(units)?;
5083 let inv = nucleide_depletion::DecayInventory {
5084 atoms: self.atoms.clone(),
5085 };
5086 inv.masses(unit)
5087 .map_err(|e| PyValueError::new_err(e.to_string()))
5088 }
5089
5090 fn moles(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
5092 let unit = parse_quantity_unit(units)?;
5093 let inv = nucleide_depletion::DecayInventory {
5094 atoms: self.atoms.clone(),
5095 };
5096 inv.moles(unit)
5097 .map_err(|e| PyValueError::new_err(e.to_string()))
5098 }
5099
5100 fn activity_fractions(&self) -> PyResult<BTreeMap<String, f64>> {
5102 let sys = inventory_sys(&self.chain, &BTreeMap::new())?;
5103 let inv = nucleide_depletion::DecayInventory {
5104 atoms: self.atoms.clone(),
5105 };
5106 inv.activity_fractions(&sys)
5107 .map_err(|e| PyValueError::new_err(e.to_string()))
5108 }
5109
5110 fn mass_fractions(&self) -> PyResult<BTreeMap<String, f64>> {
5112 let inv = nucleide_depletion::DecayInventory {
5113 atoms: self.atoms.clone(),
5114 };
5115 inv.mass_fractions()
5116 .map_err(|e| PyValueError::new_err(e.to_string()))
5117 }
5118
5119 fn mole_fractions(&self) -> BTreeMap<String, f64> {
5121 nucleide_depletion::DecayInventory {
5122 atoms: self.atoms.clone(),
5123 }
5124 .mole_fractions()
5125 }
5126
5127 fn half_lives_readable(&self) -> BTreeMap<String, String> {
5129 nucleide_depletion::DecayInventory {
5130 atoms: self.atoms.clone(),
5131 }
5132 .half_lives_readable()
5133 }
5134
5135 fn add(&self, other: &Self) -> Self {
5137 let a = nucleide_depletion::DecayInventory {
5138 atoms: self.atoms.clone(),
5139 };
5140 let b = nucleide_depletion::DecayInventory {
5141 atoms: other.atoms.clone(),
5142 };
5143 Self {
5144 chain: self.chain.clone(),
5145 atoms: a.add(&b).atoms,
5146 }
5147 }
5148
5149 fn sub(&self, other: &Self) -> Self {
5151 let a = nucleide_depletion::DecayInventory {
5152 atoms: self.atoms.clone(),
5153 };
5154 let b = nucleide_depletion::DecayInventory {
5155 atoms: other.atoms.clone(),
5156 };
5157 Self {
5158 chain: self.chain.clone(),
5159 atoms: a.sub(&b).atoms,
5160 }
5161 }
5162
5163 fn mul(&self, scalar: f64) -> Self {
5165 let a = nucleide_depletion::DecayInventory {
5166 atoms: self.atoms.clone(),
5167 };
5168 Self {
5169 chain: self.chain.clone(),
5170 atoms: a.mul(scalar).atoms,
5171 }
5172 }
5173
5174 fn div(&self, scalar: f64) -> Self {
5176 let a = nucleide_depletion::DecayInventory {
5177 atoms: self.atoms.clone(),
5178 };
5179 Self {
5180 chain: self.chain.clone(),
5181 atoms: a.div(scalar).atoms,
5182 }
5183 }
5184
5185 fn to_csv(&self) -> String {
5187 nucleide_depletion::DecayInventory {
5188 atoms: self.atoms.clone(),
5189 }
5190 .to_csv()
5191 }
5192
5193 #[staticmethod]
5195 fn from_csv(chain: &PyChain, text: &str) -> PyResult<Self> {
5196 let inv = nucleide_depletion::DecayInventory::from_csv(text)
5198 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5199 for name in inv.atoms.keys() {
5200 if chain.inner.index_of(name).is_none() {
5201 return Err(PyValueError::new_err(format!(
5202 "unknown nuclide `{name}` for this chain"
5203 )));
5204 }
5205 }
5206 Ok(Self {
5207 chain: chain.inner.clone(),
5208 atoms: inv.atoms,
5209 })
5210 }
5211}
5212
5213fn chain_vec(
5215 chain: &nucleide_depletion::Chain,
5216 atoms: &BTreeMap<String, f64>,
5217) -> PyResult<Vec<f64>> {
5218 let mut vec = vec![0.0; chain.len()];
5219 for (name, value) in atoms {
5220 let idx = chain.index_of(name).ok_or_else(|| {
5221 PyValueError::new_err(format!("unknown nuclide `{name}` for this chain"))
5222 })?;
5223 vec[idx] = *value;
5224 }
5225 Ok(vec)
5226}
5227
5228#[pyfunction]
5230#[pyo3(signature = (chain, n0, dt, rates=None))]
5231fn cumulative_decays(
5232 chain: &PyChain,
5233 n0: BTreeMap<String, f64>,
5234 dt: f64,
5235 rates: Option<RateMap>,
5236) -> PyResult<BTreeMap<String, f64>> {
5237 let empty = BTreeMap::new();
5238 let sys = inventory_sys(&chain.inner, rates.as_ref().unwrap_or(&empty))?;
5239 let vec = chain_vec(&chain.inner, &n0)?;
5240 let out = nucleide_depletion::cumulative_decays(&sys, &vec, dt)
5241 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5242 Ok(chain
5243 .inner
5244 .nuclides
5245 .iter()
5246 .zip(out)
5247 .map(|(nuc, v)| (nuc.name.clone(), v))
5248 .collect())
5249}
5250
5251#[pyfunction]
5253fn progeny(chain: &PyChain, name: &str) -> Vec<(String, f64, String)> {
5254 nucleide_depletion::progeny(&chain.inner, name)
5255}
5256
5257#[pyfunction]
5259fn branching_fraction(chain: &PyChain, parent: &str, child: &str) -> Option<f64> {
5260 nucleide_depletion::branching_fraction(&chain.inner, parent, child)
5261}
5262
5263#[pyfunction]
5265fn decay_mode(chain: &PyChain, parent: &str, child: &str) -> Option<String> {
5266 nucleide_depletion::decay_mode(&chain.inner, parent, child)
5267}
5268
5269#[pyfunction]
5271fn chain_edges(chain: &PyChain) -> Vec<(String, String, f64, String)> {
5272 nucleide_depletion::chain_edges(&chain.inner)
5273}
5274
5275#[pyfunction]
5277fn armi_to_nucid(name: &str) -> PyResult<PyNuclide> {
5278 nucleide_nuclei::armi::armi_name_to_nucid(name)
5279 .map(|inner| PyNuclide { inner })
5280 .map_err(|e| PyValueError::new_err(e.to_string()))
5281}
5282
5283#[pyfunction]
5285fn nucid_to_armi(nuclide: &PyNuclide) -> String {
5286 nucleide_nuclei::armi::nucid_to_armi_label(nuclide.inner)
5287}
5288
5289#[pyfunction]
5291fn mcc3_to_nucid(name: &str) -> PyResult<PyNuclide> {
5292 nucleide_nuclei::armi::mcc3_to_nucid(name)
5293 .map(|inner| PyNuclide { inner })
5294 .map_err(|e| PyValueError::new_err(e.to_string()))
5295}
5296
5297#[pyfunction]
5302#[pyo3(signature = (comp, widths=None))]
5303fn check_labels(
5304 comp: BTreeMap<String, f64>,
5305 widths: Option<Vec<usize>>,
5306) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
5307 let mat = comp_to_material(comp)?;
5308 let widths = widths.unwrap_or_else(|| nucleide_material::DEFAULT_WIDTHS.to_vec());
5309 let collisions = nucleide_material::check_labels(&mat, &widths);
5310 Python::attach(|py| {
5311 Ok(collisions
5312 .into_iter()
5313 .map(|c| {
5314 let mut d = BTreeMap::new();
5315 d.insert(
5316 "truncated".to_string(),
5317 c.truncated.into_pyobject(py).unwrap().unbind().into_any(),
5318 );
5319 d.insert(
5320 "width".to_string(),
5321 c.width.into_pyobject(py).unwrap().unbind().into_any(),
5322 );
5323 let members: Vec<String> = c.members.iter().map(|id| id.to_name()).collect();
5324 d.insert(
5325 "members".to_string(),
5326 members.into_pyobject(py).unwrap().unbind().into_any(),
5327 );
5328 d
5329 })
5330 .collect())
5331 })
5332}
5333
5334#[pyfunction]
5336fn audit_material(comp: BTreeMap<String, f64>) -> PyResult<Vec<BTreeMap<String, String>>> {
5337 let mat = comp_to_material(comp)?;
5338 Ok(nucleide_material::audit(&mat, &nucleide_material::Ame2020)
5339 .into_iter()
5340 .map(|issue| {
5341 let mut d = BTreeMap::new();
5342 d.insert("kind".to_string(), format!("{:?}", issue.kind));
5343 d.insert("detail".to_string(), issue.detail);
5344 d
5345 })
5346 .collect())
5347}
5348
5349#[pyfunction]
5356#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
5357#[allow(clippy::too_many_arguments)]
5358fn emit_cards(
5359 comp: BTreeMap<String, f64>,
5360 name: &str,
5361 density: Option<f64>,
5362 mcnp_number: u32,
5363 xs_suffix: &str,
5364 serpent_lib: &str,
5365 fluka_fid: u32,
5366 partisn_zone: u32,
5367) -> PyResult<BTreeMap<String, String>> {
5368 let (emitted, _) = emit_drift_inner(
5369 comp,
5370 name,
5371 density,
5372 mcnp_number,
5373 xs_suffix,
5374 serpent_lib,
5375 fluka_fid,
5376 partisn_zone,
5377 )?;
5378 Ok(emitted
5379 .into_iter()
5380 .map(|e| (e.code.to_string(), e.text))
5381 .collect())
5382}
5383
5384#[pyfunction]
5388#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
5389#[allow(clippy::too_many_arguments)]
5390fn emit_drift_table(
5391 comp: BTreeMap<String, f64>,
5392 name: &str,
5393 density: Option<f64>,
5394 mcnp_number: u32,
5395 xs_suffix: &str,
5396 serpent_lib: &str,
5397 fluka_fid: u32,
5398 partisn_zone: u32,
5399) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
5400 let (_, table) = emit_drift_inner(
5401 comp,
5402 name,
5403 density,
5404 mcnp_number,
5405 xs_suffix,
5406 serpent_lib,
5407 fluka_fid,
5408 partisn_zone,
5409 )?;
5410 drift_table_to_py(table)
5411}
5412
5413fn drift_table_to_py(
5414 table: nucleide_emit::DriftTable,
5415) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
5416 Python::attach(|py| {
5417 Ok(table
5418 .rows
5419 .into_iter()
5420 .map(|r| {
5421 let mut d = BTreeMap::new();
5422 d.insert(
5423 "code".to_string(),
5424 r.code
5425 .to_string()
5426 .into_pyobject(py)
5427 .unwrap()
5428 .unbind()
5429 .into_any(),
5430 );
5431 d.insert(
5432 "mass_in".to_string(),
5433 r.mass_in.into_pyobject(py).unwrap().unbind().into_any(),
5434 );
5435 d.insert(
5436 "mass_out".to_string(),
5437 r.mass_out.into_pyobject(py).unwrap().unbind().into_any(),
5438 );
5439 d.insert(
5440 "rel_drift".to_string(),
5441 r.rel_drift.into_pyobject(py).unwrap().unbind().into_any(),
5442 );
5443 let dropped: Vec<BTreeMap<String, Py<PyAny>>> = r
5444 .dropped
5445 .into_iter()
5446 .map(|x| {
5447 let mut dd = BTreeMap::new();
5448 dd.insert(
5449 "nuclide".to_string(),
5450 x.id.to_name()
5451 .into_pyobject(py)
5452 .unwrap()
5453 .unbind()
5454 .into_any(),
5455 );
5456 dd.insert(
5457 "mass".to_string(),
5458 x.mass.into_pyobject(py).unwrap().unbind().into_any(),
5459 );
5460 dd.insert(
5461 "reason".to_string(),
5462 x.reason.into_pyobject(py).unwrap().unbind().into_any(),
5463 );
5464 dd
5465 })
5466 .collect();
5467 d.insert(
5468 "dropped".to_string(),
5469 dropped.into_pyobject(py).unwrap().unbind().into_any(),
5470 );
5471 d.insert(
5472 "reparsed".to_string(),
5473 pyo3::types::PyBool::new(py, r.reparsed)
5474 .to_owned()
5475 .into_any()
5476 .unbind(),
5477 );
5478 d
5479 })
5480 .collect())
5481 })
5482}
5483
5484#[allow(clippy::too_many_arguments)]
5485fn emit_drift_inner(
5486 comp: BTreeMap<String, f64>,
5487 name: &str,
5488 density: Option<f64>,
5489 mcnp_number: u32,
5490 xs_suffix: &str,
5491 serpent_lib: &str,
5492 fluka_fid: u32,
5493 partisn_zone: u32,
5494) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
5495 let mut mat = comp_to_material(comp)?;
5496 mat.set_density(density);
5497 emit_drift_with_mat(
5498 mat,
5499 name,
5500 mcnp_number,
5501 xs_suffix,
5502 serpent_lib,
5503 fluka_fid,
5504 partisn_zone,
5505 )
5506}
5507
5508#[allow(clippy::too_many_arguments)]
5509fn emit_drift_with_mat(
5510 mat: nucleide_material::Material,
5511 name: &str,
5512 mcnp_number: u32,
5513 xs_suffix: &str,
5514 serpent_lib: &str,
5515 fluka_fid: u32,
5516 partisn_zone: u32,
5517) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
5518 let mut opts = nucleide_emit::EmitOptions::new(name);
5519 opts.mcnp_number = mcnp_number;
5520 opts.xs_suffix = xs_suffix.to_string();
5521 opts.serpent_lib = serpent_lib.to_string();
5522 opts.fluka_fid = fluka_fid;
5523 opts.partisn_zone = partisn_zone;
5524 nucleide_emit::emit_drift(&mat, &opts).map_err(|e| PyValueError::new_err(e.to_string()))
5525}
5526
5527#[allow(clippy::too_many_arguments)]
5528fn emit_armi_drift_inner(
5529 comp: BTreeMap<String, f64>,
5530 name: &str,
5531 density: Option<f64>,
5532 mcnp_number: u32,
5533 xs_suffix: &str,
5534 serpent_lib: &str,
5535 fluka_fid: u32,
5536 partisn_zone: u32,
5537) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
5538 let mat = nucleide_emit::armi::from_armi_mass_fracs(comp, density)
5541 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5542 emit_drift_with_mat(
5543 mat,
5544 name,
5545 mcnp_number,
5546 xs_suffix,
5547 serpent_lib,
5548 fluka_fid,
5549 partisn_zone,
5550 )
5551}
5552
5553#[pyfunction]
5561#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
5562#[allow(clippy::too_many_arguments)]
5563fn emit_armi_cards(
5564 comp: BTreeMap<String, f64>,
5565 name: &str,
5566 density: Option<f64>,
5567 mcnp_number: u32,
5568 xs_suffix: &str,
5569 serpent_lib: &str,
5570 fluka_fid: u32,
5571 partisn_zone: u32,
5572) -> PyResult<BTreeMap<String, String>> {
5573 let (emitted, _) = emit_armi_drift_inner(
5574 comp,
5575 name,
5576 density,
5577 mcnp_number,
5578 xs_suffix,
5579 serpent_lib,
5580 fluka_fid,
5581 partisn_zone,
5582 )?;
5583 Ok(emitted
5584 .into_iter()
5585 .map(|e| (e.code.to_string(), e.text))
5586 .collect())
5587}
5588
5589#[pyfunction]
5593#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
5594#[allow(clippy::too_many_arguments)]
5595fn emit_armi_drift_table(
5596 comp: BTreeMap<String, f64>,
5597 name: &str,
5598 density: Option<f64>,
5599 mcnp_number: u32,
5600 xs_suffix: &str,
5601 serpent_lib: &str,
5602 fluka_fid: u32,
5603 partisn_zone: u32,
5604) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
5605 let (_, table) = emit_armi_drift_inner(
5606 comp,
5607 name,
5608 density,
5609 mcnp_number,
5610 xs_suffix,
5611 serpent_lib,
5612 fluka_fid,
5613 partisn_zone,
5614 )?;
5615 drift_table_to_py(table)
5616}
5617
5618fn parse_reactivity(
5631 spec: &BTreeMap<String, Py<PyAny>>,
5632 py: Python<'_>,
5633) -> PyResult<nucleide_kinetics::Reactivity> {
5634 use nucleide_kinetics::Reactivity as R;
5635 let kind: String = get_str(spec, py, "kind", "reactivity spec needs a `kind`")?;
5636 let num = |key: &str| -> PyResult<f64> { get_num(spec, py, key) };
5637 let vec = |key: &str| -> PyResult<Vec<f64>> { get_vec(spec, py, key) };
5638 let r = match kind.as_str() {
5639 "constant" => R::Constant { rho: num("rho")? },
5640 "step" => R::Step {
5641 t_step: num("t_step")?,
5642 rho_init: num("rho_init")?,
5643 rho_final: num("rho_final")?,
5644 },
5645 "impulse" => R::Impulse {
5646 t_start: num("t_start")?,
5647 t_end: num("t_end")?,
5648 rho_init: num("rho_init")?,
5649 rho_max: num("rho_max")?,
5650 },
5651 "ramp" => R::Ramp {
5652 t_start: num("t_start")?,
5653 t_end: num("t_end")?,
5654 rho_init: num("rho_init")?,
5655 rho_rise: num("rho_rise")?,
5656 rho_final: num("rho_final")?,
5657 },
5658 "polyline" => R::Polyline {
5659 times: vec("times")?,
5660 values: vec("values")?,
5661 },
5662 other => {
5663 return Err(PyValueError::new_err(format!(
5664 "unknown reactivity kind `{other}` (supported: constant, step, impulse, ramp, polyline)"
5665 )))
5666 }
5667 };
5668 r.validate()
5669 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5670 Ok(r)
5671}
5672
5673fn get_str(
5674 spec: &BTreeMap<String, Py<PyAny>>,
5675 py: Python<'_>,
5676 key: &str,
5677 missing: &str,
5678) -> PyResult<String> {
5679 spec.get(key)
5680 .ok_or_else(|| PyValueError::new_err(missing.to_string()))?
5681 .extract::<String>(py)
5682 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a string")))
5683}
5684
5685fn get_num(spec: &BTreeMap<String, Py<PyAny>>, py: Python<'_>, key: &str) -> PyResult<f64> {
5686 spec.get(key)
5687 .ok_or_else(|| PyValueError::new_err(format!("reactivity spec missing `{key}`")))?
5688 .extract::<f64>(py)
5689 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
5690}
5691
5692fn get_vec(spec: &BTreeMap<String, Py<PyAny>>, py: Python<'_>, key: &str) -> PyResult<Vec<f64>> {
5693 spec.get(key)
5694 .ok_or_else(|| PyValueError::new_err(format!("reactivity spec missing `{key}`")))?
5695 .extract::<Vec<f64>>(py)
5696 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a list of numbers")))
5697}
5698
5699fn kinetics_params(
5700 betas: Vec<f64>,
5701 lambdas: Vec<f64>,
5702 lambda_gen: f64,
5703) -> PyResult<nucleide_kinetics::KineticParams> {
5704 nucleide_kinetics::KineticParams::new(betas, lambdas, lambda_gen)
5705 .map_err(|e| PyValueError::new_err(e.to_string()))
5706}
5707
5708#[pyfunction]
5719#[pyo3(signature = (betas, lambdas, lambda_gen, rho, t, n0, c0=None, method="trapezoidal", rtol=1e-9, atol=1e-12, dt_min=1e-14, dt_max=None, max_steps=1000000))]
5720#[allow(clippy::too_many_arguments)]
5721fn kinetics_solve(
5722 py: Python<'_>,
5723 betas: Vec<f64>,
5724 lambdas: Vec<f64>,
5725 lambda_gen: f64,
5726 rho: BTreeMap<String, Py<PyAny>>,
5727 t: Vec<f64>,
5728 n0: f64,
5729 c0: Option<Vec<f64>>,
5730 method: &str,
5731 rtol: f64,
5732 atol: f64,
5733 dt_min: f64,
5734 dt_max: Option<f64>,
5735 max_steps: usize,
5736) -> PyResult<Py<PyAny>> {
5737 use nucleide_kinetics::{Method as M, SolverOptions};
5738 let params = kinetics_params(betas, lambdas, lambda_gen)?;
5739 let rho = parse_reactivity(&rho, py)?;
5740 let grid =
5741 nucleide_kinetics::TimeGrid::new(t).map_err(|e| PyValueError::new_err(e.to_string()))?;
5742 let state = nucleide_kinetics::State::new(¶ms, n0, c0)
5743 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5744 let method = if method.eq_ignore_ascii_case("trapezoidal") {
5745 M::Trapezoidal
5746 } else if method.eq_ignore_ascii_case("backward_euler") {
5747 M::BackwardEuler
5748 } else {
5749 return Err(PyValueError::new_err(format!(
5750 "unknown kinetics method `{method}` (supported: trapezoidal, backward_euler)"
5751 )));
5752 };
5753 let opts = SolverOptions {
5754 method,
5755 rtol,
5756 atol,
5757 dt_min,
5758 dt_max: dt_max.unwrap_or(f64::INFINITY),
5759 max_steps,
5760 };
5761 let sol = nucleide_kinetics::solve(¶ms, &rho, &grid, &state, &opts)
5762 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5763 use pyo3::types::PyDict;
5764 let out = PyDict::new(py);
5765 out.set_item("times", &sol.times).ok();
5766 out.set_item("n", &sol.n).ok();
5767 out.set_item("C", &sol.c).ok();
5768 out.set_item("n0", sol.initial.n0).ok();
5769 out.set_item("C0", &sol.initial.c0).ok();
5770 Ok(out.into_any().unbind())
5771}
5772
5773#[pyfunction]
5775fn kinetics_equilibrium(
5776 betas: Vec<f64>,
5777 lambdas: Vec<f64>,
5778 lambda_gen: f64,
5779 n0: f64,
5780) -> PyResult<Vec<f64>> {
5781 kinetics_params(betas, lambdas, lambda_gen)?
5782 .equilibrium_precursors(n0)
5783 .map_err(|e| PyValueError::new_err(e.to_string()))
5784}
5785
5786#[pyfunction]
5788#[pyo3(signature = (betas, lambdas, lambda_gen, rho, n0, c0=None))]
5789fn kinetics_initial_rate(
5790 py: Python<'_>,
5791 betas: Vec<f64>,
5792 lambdas: Vec<f64>,
5793 lambda_gen: f64,
5794 rho: BTreeMap<String, Py<PyAny>>,
5795 n0: f64,
5796 c0: Option<Vec<f64>>,
5797) -> PyResult<f64> {
5798 let params = kinetics_params(betas, lambdas, lambda_gen)?;
5799 let rho = parse_reactivity(&rho, py)?;
5800 let state = nucleide_kinetics::State::new(¶ms, n0, c0)
5801 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5802 Ok(nucleide_kinetics::solve::initial_rate(
5803 ¶ms, &rho, &state,
5804 ))
5805}
5806
5807#[pyfunction]
5809fn kinetics_inhour_rho(
5810 betas: Vec<f64>,
5811 lambdas: Vec<f64>,
5812 lambda_gen: f64,
5813 omega: f64,
5814) -> PyResult<f64> {
5815 let params = kinetics_params(betas, lambdas, lambda_gen)?;
5816 nucleide_kinetics::rho_of_omega(¶ms, omega)
5817 .map_err(|e| PyValueError::new_err(e.to_string()))
5818}
5819
5820#[pyfunction]
5822fn kinetics_stable_period(
5823 betas: Vec<f64>,
5824 lambdas: Vec<f64>,
5825 lambda_gen: f64,
5826 rho: f64,
5827) -> PyResult<f64> {
5828 let params = kinetics_params(betas, lambdas, lambda_gen)?;
5829 nucleide_kinetics::stable_period(¶ms, rho).map_err(|e| PyValueError::new_err(e.to_string()))
5830}
5831
5832#[pyfunction]
5837fn kinetics_prompt_jump(
5838 n_before: f64,
5839 rho_before: f64,
5840 rho_after: f64,
5841 beta_total: f64,
5842) -> PyResult<f64> {
5843 nucleide_kinetics::prompt_jump(n_before, rho_before, rho_after, beta_total)
5844 .map_err(|e| PyValueError::new_err(e.to_string()))
5845}
5846
5847#[pyfunction]
5853fn spectroscopy_rect_smooth(counts: Vec<f64>, m: i64) -> PyResult<Vec<f64>> {
5854 let w = usize::try_from(m).map_err(|_| {
5855 PyValueError::new_err(format!("spectroscopy: smoothing width {m} is less than 3"))
5856 })?;
5857 nucleide_spectroscopy::rect_smooth(&counts, w).map_err(|e| PyValueError::new_err(e.to_string()))
5858}
5859
5860#[pyfunction]
5862fn spectroscopy_five_point_smooth(counts: Vec<f64>) -> PyResult<Vec<f64>> {
5863 nucleide_spectroscopy::five_point_smooth(&counts)
5864 .map_err(|e| PyValueError::new_err(e.to_string()))
5865}
5866
5867#[pyfunction]
5869fn spectroscopy_calc_bg(
5870 counts: Vec<f64>,
5871 channels: Vec<f64>,
5872 c1: i64,
5873 c2: i64,
5874 m: i64,
5875) -> PyResult<f64> {
5876 nucleide_spectroscopy::calc_bg(&counts, &channels, c1, c2, m)
5877 .map_err(|e| PyValueError::new_err(e.to_string()))
5878}
5879
5880#[pyfunction]
5882fn spectroscopy_gross_count(
5883 counts: Vec<f64>,
5884 channels: Vec<f64>,
5885 c1: i64,
5886 c2: i64,
5887) -> PyResult<f64> {
5888 nucleide_spectroscopy::gross_count(&counts, &channels, c1, c2)
5889 .map_err(|e| PyValueError::new_err(e.to_string()))
5890}
5891
5892#[pyfunction]
5894fn spectroscopy_net_counts(
5895 counts: Vec<f64>,
5896 channels: Vec<f64>,
5897 c1: i64,
5898 c2: i64,
5899 m: i64,
5900) -> PyResult<f64> {
5901 nucleide_spectroscopy::net_counts(&counts, &channels, c1, c2, m)
5902 .map_err(|e| PyValueError::new_err(e.to_string()))
5903}
5904
5905#[pyfunction]
5907fn spectroscopy_energy_bins(channels: Vec<f64>, calib_e_fit: Vec<f64>) -> PyResult<Vec<f64>> {
5908 nucleide_spectroscopy::energy_bins(&channels, &calib_e_fit)
5909 .map_err(|e| PyValueError::new_err(e.to_string()))
5910}
5911
5912#[pyfunction]
5914fn spectroscopy_detector_efficiency(
5915 energy_mev: f64,
5916 eff_coeff: Vec<f64>,
5917 eff_fit: i64,
5918) -> PyResult<f64> {
5919 nucleide_spectroscopy::detector_efficiency(energy_mev, &eff_coeff, eff_fit)
5920 .map_err(|e| PyValueError::new_err(e.to_string()))
5921}
5922
5923#[pyfunction]
5929#[pyo3(signature = (energies, effs, weights, order, eff_fit=1))]
5930fn spectroscopy_fit_efficiency(
5931 energies: Vec<f64>,
5932 effs: Vec<f64>,
5933 weights: Vec<f64>,
5934 order: usize,
5935 eff_fit: i64,
5936) -> PyResult<Vec<f64>> {
5937 nucleide_spectroscopy::fit_efficiency(&energies, &effs, &weights, order, eff_fit)
5938 .map_err(|e| PyValueError::new_err(e.to_string()))
5939}
5940
5941fn atomic_key(atomic: &BTreeMap<String, f64>, key: &str) -> PyResult<f64> {
5943 atomic
5944 .get(key)
5945 .copied()
5946 .ok_or_else(|| PyValueError::new_err(format!("atomic constants missing `{key}`")))
5947}
5948
5949#[pyfunction]
5958#[pyo3(signature = (atomic, k_conv=None, l_conv=None))]
5959fn spectroscopy_xray_lines(
5960 atomic: BTreeMap<String, f64>,
5961 k_conv: Option<f64>,
5962 l_conv: Option<f64>,
5963) -> PyResult<Vec<(f64, f64)>> {
5964 let data = nucleide_spectroscopy::AtomicData {
5965 k_shell_fluor: atomic_key(&atomic, "k_shell_fluor")?,
5966 l_shell_fluor: atomic_key(&atomic, "l_shell_fluor")?,
5967 prob: atomic_key(&atomic, "prob")?,
5968 kb_to_ka: atomic_key(&atomic, "kb_to_ka")?,
5969 ka2_to_ka1: atomic_key(&atomic, "ka2_to_ka1")?,
5970 ka1_en_kev: atomic_key(&atomic, "ka1_en_kev")?,
5971 ka2_en_kev: atomic_key(&atomic, "ka2_en_kev")?,
5972 kb_en_kev: atomic_key(&atomic, "kb_en_kev")?,
5973 l_en_kev: atomic_key(&atomic, "l_en_kev")?,
5974 };
5975 let present = |v: Option<f64>| v.filter(|x| !x.is_nan());
5977 Ok(
5978 nucleide_spectroscopy::xray_lines(&data, present(k_conv), present(l_conv))
5979 .iter()
5980 .map(|l| (l.energy_kev, l.intensity))
5981 .collect(),
5982 )
5983}
5984
5985#[pyfunction]
6000#[pyo3(signature = (lines, x=0.0, y=0.0, z=0.0, u=0.0, v=0.0, w=0.0, weight=1.0, particle="Neutron", version=5))]
6001#[allow(clippy::too_many_arguments)]
6002fn spectroscopy_sdef_decay_source(
6003 lines: Vec<(f64, f64)>,
6004 x: f64,
6005 y: f64,
6006 z: f64,
6007 u: f64,
6008 v: f64,
6009 w: f64,
6010 weight: f64,
6011 particle: &str,
6012 version: u32,
6013) -> PyResult<(Vec<(f64, f64)>, String)> {
6014 let particle = particle
6015 .parse::<nucleide_nuclei::particles::ParticleId>()
6016 .map_err(|e| PyValueError::new_err(format!("spectroscopy: particle {e}")))?;
6017 let source = nucleide_spectroscopy::PointSource {
6018 x,
6019 y,
6020 z,
6021 u,
6022 v,
6023 w,
6024 weight,
6025 particle,
6026 };
6027 nucleide_spectroscopy::sdef_card(&lines, &source, version)
6028 .map_err(|e| PyValueError::new_err(e.to_string()))
6029}
6030
6031fn spectrum_to_py(
6033 py: Python<'_>,
6034 spec: &nucleide_spectroscopy::GammaSpectrum,
6035) -> PyResult<Py<PyAny>> {
6036 use pyo3::types::PyDict;
6037 let d = PyDict::new(py);
6038 let s = &spec.spectrum;
6039 d.set_item("spec_name", &s.spec_name)?;
6040 d.set_item("start_chan_num", s.start_chan_num)?;
6041 d.set_item("num_channels", s.num_channels)?;
6042 d.set_item("channels", &s.channels)?;
6043 d.set_item("counts", &s.counts)?;
6044 d.set_item("ebin", &s.ebin)?;
6045 d.set_item("real_time", spec.real_time)?;
6046 d.set_item("live_time", spec.live_time)?;
6047 d.set_item("dead_time", spec.dead_time())?;
6048 d.set_item("det_id", &spec.det_id)?;
6049 d.set_item("det_descp", &spec.det_descp)?;
6050 d.set_item("start_date", &spec.start_date)?;
6051 d.set_item("start_time", &spec.start_time)?;
6052 d.set_item("calib_e_fit", &spec.calib_e_fit)?;
6053 d.set_item("calib_fwhm_fit", &spec.calib_fwhm_fit)?;
6054 d.set_item("file_name", &spec.file_name)?;
6055 Ok(d.into_any().unbind())
6056}
6057
6058#[pyfunction]
6060fn spectroscopy_parse_dollar_spe(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
6061 let spec = nucleide_spectroscopy::parse_dollar_spe(text, "")
6062 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6063 spectrum_to_py(py, &spec)
6064}
6065
6066#[pyfunction]
6068fn spectroscopy_parse_spe(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
6069 let spec = nucleide_spectroscopy::parse_plain_spe(text, "")
6070 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6071 spectrum_to_py(py, &spec)
6072}
6073
6074#[pyfunction]
6076fn spectroscopy_read_dollar_spe(py: Python<'_>, path: &str) -> PyResult<Py<PyAny>> {
6077 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
6078 let spec = nucleide_spectroscopy::parse_dollar_spe(&text, path)
6079 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6080 spectrum_to_py(py, &spec)
6081}
6082
6083#[pyfunction]
6085fn spectroscopy_read_spe(py: Python<'_>, path: &str) -> PyResult<Py<PyAny>> {
6086 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
6087 let spec = nucleide_spectroscopy::parse_plain_spe(&text, path)
6088 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6089 spectrum_to_py(py, &spec)
6090}
6091
6092#[pyfunction]
6096fn spectroscopy_parse_lines_tsv(text: &str) -> PyResult<Vec<(f64, f64)>> {
6097 nucleide_spectroscopy::parse_lines_tsv(text).map_err(|e| PyValueError::new_err(e.to_string()))
6098}
6099
6100#[pyfunction]
6103fn spectroscopy_read_decay_lines(path: &str) -> PyResult<Vec<(f64, f64)>> {
6104 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
6105 nucleide_spectroscopy::parse_lines_tsv(&text).map_err(|e| PyValueError::new_err(e.to_string()))
6106}
6107
6108fn uq_sample_err(e: nucleide_linalg::sample::SampleError) -> PyErr {
6113 PyValueError::new_err(e.to_string())
6114}
6115
6116fn uq_decay_err(e: nucleide_linalg::decay::DecayError) -> PyErr {
6117 PyValueError::new_err(e.to_string())
6118}
6119
6120#[pyfunction]
6128fn uq_sample_mvn(
6129 py: Python<'_>,
6130 mean: Vec<f64>,
6131 cov: Vec<Vec<f64>>,
6132 n: usize,
6133 seed: u64,
6134) -> PyResult<Py<PyAny>> {
6135 use pyo3::types::PyDict;
6136 let set = nucleide_linalg::sample::sample_mvn(&mean, &cov, n, seed).map_err(uq_sample_err)?;
6137 let d = PyDict::new(py);
6138 d.set_item("samples", set.samples)?;
6139 d.set_item("method", set.method.name())?;
6140 match &set.method {
6141 nucleide_linalg::sample::FactorMethod::Cholesky => {
6142 d.set_item("min_eigen", py.None())?;
6143 d.set_item("max_eigen", py.None())?;
6144 }
6145 nucleide_linalg::sample::FactorMethod::EigenClip {
6146 min_eigen,
6147 max_eigen,
6148 } => {
6149 d.set_item("min_eigen", *min_eigen)?;
6150 d.set_item("max_eigen", *max_eigen)?;
6151 }
6152 }
6153 Ok(d.into_any().unbind())
6154}
6155
6156#[pyfunction]
6158fn uq_sample_mean(samples: Vec<Vec<f64>>) -> PyResult<Vec<f64>> {
6159 nucleide_linalg::sample::sample_mean(&samples).map_err(uq_sample_err)
6160}
6161
6162#[pyfunction]
6164fn uq_sample_cov(samples: Vec<Vec<f64>>) -> PyResult<Vec<Vec<f64>>> {
6165 nucleide_linalg::sample::sample_cov(&samples).map_err(uq_sample_err)
6166}
6167
6168#[pyfunction]
6174fn uq_check_convergence(
6175 py: Python<'_>,
6176 mean: Vec<f64>,
6177 cov: Vec<Vec<f64>>,
6178 samples: Vec<Vec<f64>>,
6179 mean_tol: f64,
6180 cov_tol: f64,
6181) -> PyResult<Py<PyAny>> {
6182 use pyo3::types::PyDict;
6183 let rep = nucleide_linalg::sample::check_convergence(&mean, &cov, &samples, mean_tol, cov_tol)
6184 .map_err(uq_sample_err)?;
6185 let d = PyDict::new(py);
6186 d.set_item("mean_err_max", rep.mean_err_max)?;
6187 d.set_item("cov_err_fro", rep.cov_err_fro)?;
6188 d.set_item("mean_tol", rep.mean_tol)?;
6189 d.set_item("cov_tol", rep.cov_tol)?;
6190 d.set_item("passed", rep.passed)?;
6191 Ok(d.into_any().unbind())
6192}
6193
6194#[pyfunction]
6197fn uq_perturb_branches(base: Vec<f64>, rel: Vec<f64>) -> PyResult<Vec<f64>> {
6198 nucleide_linalg::decay::perturb_branches(&base, &rel).map_err(uq_decay_err)
6199}
6200
6201#[pyfunction]
6205fn uq_perturb_energies(base: Vec<f64>, delta: Vec<f64>, convention: &str) -> PyResult<Vec<f64>> {
6206 let conv = nucleide_linalg::sample::PerturbConvention::parse(convention)
6207 .map_err(PyValueError::new_err)?;
6208 nucleide_linalg::decay::perturb_energies(&base, &delta, conv).map_err(uq_decay_err)
6209}
6210
6211#[pyfunction]
6218fn uq_sample_lognormal(
6219 py: Python<'_>,
6220 mean_log: Vec<f64>,
6221 cov: Vec<Vec<f64>>,
6222 n: usize,
6223 seed: u64,
6224) -> PyResult<Py<PyAny>> {
6225 use pyo3::types::PyDict;
6226 let set = nucleide_linalg::sample::sample_lognormal(&mean_log, &cov, n, seed)
6227 .map_err(uq_sample_err)?;
6228 let d = PyDict::new(py);
6229 d.set_item("samples", set.samples)?;
6230 d.set_item("method", set.method.name())?;
6231 match &set.method {
6232 nucleide_linalg::sample::FactorMethod::Cholesky => {
6233 d.set_item("min_eigen", py.None())?;
6234 d.set_item("max_eigen", py.None())?;
6235 }
6236 nucleide_linalg::sample::FactorMethod::EigenClip {
6237 min_eigen,
6238 max_eigen,
6239 } => {
6240 d.set_item("min_eigen", *min_eigen)?;
6241 d.set_item("max_eigen", *max_eigen)?;
6242 }
6243 }
6244 Ok(d.into_any().unbind())
6245}
6246
6247#[pyfunction]
6254fn uq_sample_lhs(
6255 py: Python<'_>,
6256 mean: Vec<f64>,
6257 cov: Vec<Vec<f64>>,
6258 n: usize,
6259 seed: u64,
6260) -> PyResult<Py<PyAny>> {
6261 use pyo3::types::PyDict;
6262 let set = nucleide_linalg::sample::sample_lhs(&mean, &cov, n, seed).map_err(uq_sample_err)?;
6263 let d = PyDict::new(py);
6264 d.set_item("samples", set.samples)?;
6265 d.set_item("method", set.method.name())?;
6266 match &set.method {
6267 nucleide_linalg::sample::FactorMethod::Cholesky => {
6268 d.set_item("min_eigen", py.None())?;
6269 d.set_item("max_eigen", py.None())?;
6270 }
6271 nucleide_linalg::sample::FactorMethod::EigenClip {
6272 min_eigen,
6273 max_eigen,
6274 } => {
6275 d.set_item("min_eigen", *min_eigen)?;
6276 d.set_item("max_eigen", *max_eigen)?;
6277 }
6278 }
6279 Ok(d.into_any().unbind())
6280}
6281
6282#[pyfunction]
6285fn uq_lognormal_mean(mean_log: Vec<f64>, cov: Vec<Vec<f64>>) -> PyResult<Vec<f64>> {
6286 nucleide_linalg::sample::lognormal_mean(&mean_log, &cov).map_err(uq_sample_err)
6287}
6288
6289#[pyfunction]
6292fn uq_lognormal_cov(mean_log: Vec<f64>, cov: Vec<Vec<f64>>) -> PyResult<Vec<Vec<f64>>> {
6293 nucleide_linalg::sample::lognormal_cov(&mean_log, &cov).map_err(uq_sample_err)
6294}
6295
6296#[pyfunction]
6298fn uq_passthrough(delta: Vec<f64>) -> PyResult<Vec<f64>> {
6299 nucleide_linalg::decay::passthrough(&delta).map_err(uq_decay_err)
6300}
6301
6302#[pyfunction]
6305fn uq_perturb_fission_yields(base: Vec<f64>, rel: Vec<f64>) -> PyResult<Vec<f64>> {
6306 nucleide_linalg::decay::perturb_fission_yields(&base, &rel).map_err(uq_decay_err)
6307}
6308
6309fn parse_projectile(flag: &str) -> PyResult<nucleide_nuclei::rxname::Projectile> {
6314 flag.parse::<nucleide_nuclei::rxname::Projectile>()
6315 .map_err(|e| PyValueError::new_err(e.to_string()))
6316}
6317
6318fn resolve_rx_id(spec: &Bound<'_, PyAny>) -> PyResult<u32> {
6319 if let Ok(id) = spec.extract::<u32>() {
6320 return Ok(id);
6321 }
6322 if let Ok(s) = spec.extract::<&str>() {
6323 return nucleide_nuclei::rxname::name_to_id(s)
6324 .map_err(|e| PyValueError::new_err(e.to_string()));
6325 }
6326 Err(PyTypeError::new_err(
6327 "expected reaction id (int) or name (str)",
6328 ))
6329}
6330
6331#[pyfunction]
6333fn rxname_label(id: u32) -> &'static str {
6334 nucleide_nuclei::rxname::label(id)
6335}
6336
6337#[pyfunction]
6339fn rxname_doc(id: u32) -> &'static str {
6340 nucleide_nuclei::rxname::doc(id)
6341}
6342
6343#[pyfunction]
6345fn rxname_reaction(py: Python<'_>, id: u32) -> PyResult<Option<Py<PyAny>>> {
6346 use pyo3::types::PyDict;
6347 Ok(nucleide_nuclei::rxname::reaction(id).map(|r| {
6348 let d = PyDict::new(py);
6349 d.set_item("id", r.id).ok();
6350 d.set_item("name", r.name).ok();
6351 d.set_item("mt", r.mt).ok();
6352 d.set_item("label", r.label).ok();
6353 d.set_item("doc", r.doc).ok();
6354 d.into_any().unbind()
6355 }))
6356}
6357
6358#[pyfunction]
6360#[pyo3(signature = (from_nucid, to_nucid, projectile="n"))]
6361fn rxname_id_from_nucdelta(from_nucid: u32, to_nucid: u32, projectile: &str) -> PyResult<u32> {
6362 let p = parse_projectile(projectile)?;
6363 nucleide_nuclei::rxname::id_from_nucdelta(from_nucid, to_nucid, p)
6364 .map_err(|e| PyValueError::new_err(e.to_string()))
6365}
6366
6367#[pyfunction]
6369#[pyo3(signature = (parent, rx, projectile="n"))]
6370fn rxname_child(parent: &str, rx: &Bound<'_, PyAny>, projectile: &str) -> PyResult<String> {
6371 let p = parse_projectile(projectile)?;
6372 let rx = resolve_rx_id(rx)?;
6373 let parent_id = NuclideId::from_name(parent)
6374 .map_err(|e| PyValueError::new_err(format!("`{parent}`: {e}")))?;
6375 nucleide_nuclei::rxname::child(parent_id, rx, p)
6376 .map(|id| id.to_name())
6377 .map_err(|e| PyValueError::new_err(e.to_string()))
6378}
6379
6380#[pyfunction]
6382#[pyo3(signature = (child, rx, projectile="n"))]
6383fn rxname_parent(child: &str, rx: &Bound<'_, PyAny>, projectile: &str) -> PyResult<String> {
6384 let p = parse_projectile(projectile)?;
6385 let rx = resolve_rx_id(rx)?;
6386 let child_id = NuclideId::from_name(child)
6387 .map_err(|e| PyValueError::new_err(format!("`{child}`: {e}")))?;
6388 nucleide_nuclei::rxname::parent(child_id, rx, p)
6389 .map(|id| id.to_name())
6390 .map_err(|e| PyValueError::new_err(e.to_string()))
6391}
6392
6393#[pyfunction]
6395fn particle_is_valid(spec: &str) -> bool {
6396 nucleide_nuclei::particles::is_valid(spec)
6397}
6398
6399#[pyfunction]
6401fn particle_is_valid_pdc(n: i32) -> bool {
6402 nucleide_nuclei::particles::is_valid_pdc(n)
6403}
6404
6405#[pyfunction]
6407fn particle_is_hydrogen(spec: &str) -> bool {
6408 nucleide_nuclei::particles::is_hydrogen(spec)
6409}
6410
6411#[pyfunction]
6413fn particle_is_heavy_ion(spec: &str) -> bool {
6414 nucleide_nuclei::particles::is_heavy_ion(spec)
6415}
6416
6417#[pyfunction]
6419#[pyo3(signature = (name, source="EPA"))]
6420fn dose_f1(name: &str, source: &str) -> PyResult<Option<f64>> {
6421 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
6422 let s = parse_dose_source(source)?;
6423 Ok(nucleide_nuclei::data::dose_f1_by_name(name, s))
6424}
6425
6426#[pyfunction]
6428#[pyo3(signature = (name, source="EPA"))]
6429fn dose_lung_model(name: &str, source: &str) -> PyResult<Option<char>> {
6430 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
6431 let s = parse_dose_source(source)?;
6432 Ok(nucleide_nuclei::data::dose_lung_model_by_name(name, s))
6433}
6434
6435fn bare_element_z(name: &str) -> Option<u32> {
6437 let t = name.trim();
6438 if t.is_empty() {
6439 return None;
6440 }
6441 let mut chars = t.chars();
6442 let first = chars.next()?.to_uppercase().next()?;
6443 let rest: String = chars.collect::<String>().to_lowercase();
6444 let canon = format!("{first}{rest}");
6445 nucleide_nuclei::element_z(&canon)
6446}
6447
6448fn mat_from_comp_elements(comp: BTreeMap<String, f64>) -> PyResult<nucleide_material::Material> {
6449 let mut mat = nucleide_material::Material::new();
6450 for (name, grams) in &comp {
6451 let id = match NuclideId::from_name(name) {
6452 Ok(id) => id,
6453 Err(_) => match bare_element_z(name) {
6454 Some(z) => NuclideId::from_nucid(z * 10_000_000),
6455 None => {
6456 return Err(PyValueError::new_err(format!(
6457 "`{name}`: unknown nuclide or element"
6458 )));
6459 }
6460 },
6461 };
6462 mat.add_nuclide(id, *grams);
6463 }
6464 Ok(mat)
6465}
6466
6467fn mat_to_comp_elements(mat: &nucleide_material::Material) -> BTreeMap<String, f64> {
6468 let mut out = BTreeMap::new();
6469 for (&id, &grams) in &mat.comp {
6470 let key = if id.a() == 0 && id.state() == 0 {
6471 nucleide_nuclei::element_symbol(id.z())
6472 .unwrap_or("X")
6473 .to_string()
6474 } else {
6475 id.to_name()
6476 };
6477 *out.entry(key).or_insert(0.0) += grams;
6478 }
6479 out
6480}
6481
6482#[pyfunction]
6486fn mix_by_mass(parts: Vec<(BTreeMap<String, f64>, f64)>) -> PyResult<BTreeMap<String, f64>> {
6487 let mats: Vec<nucleide_material::Material> = parts
6488 .iter()
6489 .map(|(comp, _)| mat_from_comp_elements(comp.clone()))
6490 .collect::<PyResult<_>>()?;
6491 let refs: Vec<(&nucleide_material::Material, f64)> =
6492 mats.iter().zip(parts.iter().map(|(_, w)| *w)).collect();
6493 let out = nucleide_material::Material::mix_by_mass(&refs)
6494 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6495 Ok(mat_to_comp_elements(&out))
6496}
6497
6498#[pyfunction]
6502fn mix_by_volume(parts: Vec<(BTreeMap<String, f64>, f64, f64)>) -> PyResult<BTreeMap<String, f64>> {
6503 let mut mats: Vec<nucleide_material::Material> = Vec::with_capacity(parts.len());
6504 for (comp, _, density) in &parts {
6505 let mut m = mat_from_comp_elements(comp.clone())?;
6506 m.set_density(Some(*density));
6507 mats.push(m);
6508 }
6509 let refs: Vec<(&nucleide_material::Material, f64)> =
6510 mats.iter().zip(parts.iter().map(|(_, v, _)| *v)).collect();
6511 let out = nucleide_material::Material::mix_by_volume(&refs)
6512 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6513 Ok(mat_to_comp_elements(&out))
6514}
6515
6516#[pyfunction]
6518fn specific_activity(comp: BTreeMap<String, f64>) -> PyResult<f64> {
6519 let mat = mat_from_comp_elements(comp)?;
6520 let analytics = nucleide_material::Analytics {
6521 masses: &nucleide_material::Ame2020,
6522 decays: &nucleide_material::ChainDecays,
6523 };
6524 mat.specific_activity(&analytics)
6525 .map_err(|e| PyValueError::new_err(e.to_string()))
6526}
6527
6528#[pyfunction]
6532#[pyo3(signature = (entries, cross_sections=None))]
6533fn materials_doc_to_xml(
6534 entries: Vec<(String, BTreeMap<String, f64>, f64)>,
6535 cross_sections: Option<String>,
6536) -> PyResult<String> {
6537 let mut doc = nucleide_material::MaterialsDoc::new();
6538 if let Some(path) = cross_sections {
6539 doc = doc.cross_sections(path);
6540 }
6541 for (name, comp, density) in entries {
6542 let mut mat = mat_from_comp_elements(comp)?;
6543 mat.set_density(Some(density));
6544 doc = doc.push(name, mat);
6545 }
6546 doc.to_xml()
6547 .map_err(|e| PyValueError::new_err(e.to_string()))
6548}
6549
6550#[pyfunction]
6554fn expand_elements(comp: BTreeMap<String, f64>) -> PyResult<BTreeMap<String, f64>> {
6555 let mut mat = mat_from_comp_elements(comp)?;
6556 mat.expand_elements(
6557 &nucleide_material::Ame2020,
6558 &nucleide_material::NaturalAbundances,
6559 )
6560 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6561 Ok(mat_to_comp_elements(&mat))
6562}
6563
6564#[pyfunction]
6566fn collapse_elements(comp: BTreeMap<String, f64>) -> PyResult<BTreeMap<String, f64>> {
6567 let mat = mat_from_comp_elements(comp)?;
6568 Ok(mat_to_comp_elements(&mat.collapse_elements()))
6569}
6570
6571fn parse_fluka_nuc(spec: &str) -> PyResult<nucleide_fluka_io::material::FlukaNuc> {
6572 use nucleide_fluka_io::material::FlukaNuc;
6573 if let Ok(id) = NuclideId::from_name(spec) {
6574 return Ok(FlukaNuc::Nuclide(id));
6575 }
6576 if let Some(z) = bare_element_z(spec) {
6577 return Ok(FlukaNuc::Element(z));
6578 }
6579 if let Ok(z) = spec.trim().parse::<u32>() {
6580 if nucleide_nuclei::element_symbol(z).is_some() {
6581 return Ok(FlukaNuc::Element(z));
6582 }
6583 }
6584 Err(PyValueError::new_err(format!(
6585 "`{spec}`: unknown nuclide or element"
6586 )))
6587}
6588
6589#[pyfunction]
6591fn fluka_material_str(fid: u32, nuc: &str, density: f64) -> PyResult<String> {
6592 let parsed = parse_fluka_nuc(nuc)?;
6593 nucleide_fluka_io::material::material_str(fid, parsed, density)
6594 .map_err(|e| PyValueError::new_err(e.to_string()))
6595}
6596
6597#[pyfunction]
6601#[pyo3(signature = (fid, compound_name, density, frac_type="mass", components=None))]
6602fn fluka_compound_str(
6603 fid: u32,
6604 compound_name: &str,
6605 density: f64,
6606 frac_type: &str,
6607 components: Option<Vec<(String, f64)>>,
6608) -> PyResult<String> {
6609 use nucleide_fluka_io::material::{Component, FracType};
6610 let frac = match frac_type.trim().to_ascii_lowercase().as_str() {
6611 "mass" => FracType::Mass,
6612 "atom" => FracType::Atom,
6613 other => {
6614 return Err(PyValueError::new_err(format!(
6615 "frac_type must be mass|atom, got `{other}`"
6616 )));
6617 }
6618 };
6619 let pairs = components.unwrap_or_default();
6620 let comps: Vec<Component> = pairs
6621 .iter()
6622 .map(|(nuc, frac)| parse_fluka_nuc(nuc).map(|n| Component::new(n, *frac)))
6623 .collect::<PyResult<_>>()?;
6624 nucleide_fluka_io::material::compound_str(fid, compound_name, density, frac, &comps)
6625 .map_err(|e| PyValueError::new_err(e.to_string()))
6626}
6627
6628#[pyfunction]
6630fn fluka_builtin_set() -> Vec<String> {
6631 let mut out: Vec<String> = nucleide_fluka_io::material::builtin_set()
6632 .into_iter()
6633 .map(str::to_string)
6634 .collect();
6635 out.sort();
6636 out
6637}
6638
6639#[pyfunction]
6641fn alara_validate_deck(text: &str) -> PyResult<()> {
6642 let deck = nucleide_alara_io::AlaraDeck::parse(text)
6643 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6644 deck.validate()
6645 .map_err(|e| PyValueError::new_err(e.to_string()))
6646}
6647
6648#[pyfunction]
6650fn alara_check_block(block: &str, line: usize) -> PyResult<()> {
6651 nucleide_alara_io::AlaraDeck::check_block(block, line)
6652 .map_err(|e| PyValueError::new_err(e.to_string()))
6653}
6654
6655#[pyfunction]
6657fn alara_flux_total(name: &str, text: &str) -> PyResult<f64> {
6658 nucleide_alara_io::FluxSpec::parse(name, text)
6659 .map(|f| f.total())
6660 .map_err(|e| PyValueError::new_err(e.to_string()))
6661}
6662
6663#[pyfunction]
6665fn alara_flux_len(name: &str, text: &str) -> PyResult<usize> {
6666 nucleide_alara_io::FluxSpec::parse(name, text)
6667 .map(|f| f.len())
6668 .map_err(|e| PyValueError::new_err(e.to_string()))
6669}
6670
6671#[pyfunction]
6673fn alara_output_totals(
6674 py: Python<'_>,
6675 text: &str,
6676 run_lbl: &str,
6677) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
6678 let owned_text = text.to_owned();
6679 let owned_lbl = run_lbl.to_owned();
6680 let frame = py
6681 .detach(move || {
6682 nucleide_alara_io::output::ResponseFrame::parse(&owned_text, &owned_lbl)
6683 .map(|f| f.totals())
6684 })
6685 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6686 Ok(frame
6687 .rows
6688 .iter()
6689 .map(|r| fispact_row_to_map(py, r))
6690 .collect())
6691}
6692
6693#[pyfunction]
6695fn alara_output_total_activity(text: &str, run_lbl: &str) -> PyResult<f64> {
6696 nucleide_alara_io::output::ResponseFrame::parse(text, run_lbl)
6697 .map(|f| f.total_activity())
6698 .map_err(|e| PyValueError::new_err(e.to_string()))
6699}
6700
6701#[pyfunction]
6703fn alara_photon_total_strength(text: &str) -> PyResult<f64> {
6704 nucleide_alara_io::PhotonSource::from_str(text)
6705 .map(|p| p.total_strength())
6706 .map_err(|e| PyValueError::new_err(e.to_string()))
6707}
6708
6709#[pyfunction]
6711#[pyo3(signature = (deck_text, top=None))]
6712fn alara_schedule_total_time(deck_text: &str, top: Option<&str>) -> PyResult<f64> {
6713 let owned = deck_text.to_owned();
6714 let owned_top = top.map(str::to_owned);
6715 let steps =
6716 expand_deck_schedules(&owned, owned_top.as_deref()).map_err(PyValueError::new_err)?;
6717 Ok(nucleide_alara_io::schedule::total_time(&steps))
6718}
6719
6720#[pyfunction]
6722fn origen_tape6_find(py: Python<'_>, text: &str, nuclide: &str) -> PyResult<Option<Py<PyAny>>> {
6723 use pyo3::types::PyDict;
6724 let owned = text.to_owned();
6725 let query = nuclide.to_owned();
6726 let found = py
6727 .detach(move || nucleide_origen_io::Tape6::parse(&owned).map(|t| t.find(&query).cloned()))
6728 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6729 Ok(found.map(|r| {
6730 let d = PyDict::new(py);
6731 d.set_item("nuclide", &r.nuclide).ok();
6732 d.set_item("grams", r.grams).ok();
6733 d.set_item("activity_bq", r.activity_bq).ok();
6734 d.into_any().unbind()
6735 }))
6736}
6737
6738#[pyfunction]
6740fn origen_tape6_total_activity(text: &str) -> PyResult<f64> {
6741 nucleide_origen_io::Tape6::parse(text)
6742 .map(|t| t.total_activity())
6743 .map_err(|e| PyValueError::new_err(e.to_string()))
6744}
6745
6746#[pyfunction]
6748fn origen_tape9_find(py: Python<'_>, text: &str, nuclide: &str) -> PyResult<Option<Py<PyAny>>> {
6749 use pyo3::types::PyDict;
6750 let owned = text.to_owned();
6751 let query = nuclide.to_owned();
6752 let found = py
6753 .detach(move || {
6754 nucleide_origen_io::Tape9Entry::parse(&owned)
6755 .map(|entries| nucleide_origen_io::Tape9Entry::find(&entries, &query).cloned())
6756 })
6757 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6758 Ok(found.map(|e| {
6759 let d = PyDict::new(py);
6760 d.set_item("nuclide", &e.nuclide).ok();
6761 d.set_item("decay_const", e.decay_const).ok();
6762 d.into_any().unbind()
6763 }))
6764}
6765
6766#[pyfunction]
6768#[pyo3(signature = (text, kind="rtflux"))]
6769fn cccc_rtflux_npoints(text: &str, kind: &str) -> PyResult<usize> {
6770 let flux_kind = parse_flux_kind(kind)?;
6771 nucleide_cccc_io::FluxFile::parse(flux_kind, text)
6772 .map(|f| f.npoints())
6773 .map_err(|e| PyValueError::new_err(e.to_string()))
6774}
6775
6776#[pyfunction]
6778#[pyo3(signature = (text, kind="rtflux", index=0))]
6779fn cccc_rtflux_point(text: &str, kind: &str, index: usize) -> PyResult<Option<Vec<f64>>> {
6780 let flux_kind = parse_flux_kind(kind)?;
6781 let flux = nucleide_cccc_io::FluxFile::parse(flux_kind, text)
6782 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6783 Ok(flux.point(index).map(<[f64]>::to_vec))
6784}
6785
6786#[pyfunction]
6788#[pyo3(signature = (text, kind="rtflux"))]
6789fn cccc_rtflux_total(text: &str, kind: &str) -> PyResult<f64> {
6790 let flux_kind = parse_flux_kind(kind)?;
6791 nucleide_cccc_io::FluxFile::parse(flux_kind, text)
6792 .map(|f| f.total())
6793 .map_err(|e| PyValueError::new_err(e.to_string()))
6794}
6795
6796fn parse_flux_kind(kind: &str) -> PyResult<nucleide_cccc_io::rtflux::FluxKind> {
6797 match kind.to_ascii_lowercase().as_str() {
6798 "rtflux" => Ok(nucleide_cccc_io::rtflux::FluxKind::Rtflux),
6799 "atflux" => Ok(nucleide_cccc_io::rtflux::FluxKind::Atflux),
6800 "rzflux" => Ok(nucleide_cccc_io::rtflux::FluxKind::Rzflux),
6801 other => Err(PyValueError::new_err(format!(
6802 "kind must be rtflux|atflux|rzflux, got `{other}`"
6803 ))),
6804 }
6805}
6806
6807#[pyfunction]
6809fn cccc_isotxs_find(py: Python<'_>, text: &str, label: &str) -> PyResult<Option<Py<PyAny>>> {
6810 use pyo3::types::PyDict;
6811 let owned = text.to_owned();
6812 let query = label.to_owned();
6813 let found = py
6814 .detach(move || {
6815 nucleide_cccc_io::IsotxsLib::parse(&owned).map(|lib| lib.find(&query).cloned())
6816 })
6817 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6818 Ok(found.map(|n| {
6819 let d = PyDict::new(py);
6820 d.set_item("label", &n.label).ok();
6821 d.set_item("zaid", &n.zaid).ok();
6822 d.set_item("groups", n.groups).ok();
6823 d.set_item("total_xs", n.total_xs.clone()).ok();
6824 d.into_any().unbind()
6825 }))
6826}
6827
6828#[pyfunction]
6830fn cccc_isotxs_len(text: &str) -> PyResult<usize> {
6831 nucleide_cccc_io::IsotxsLib::parse(text)
6832 .map(|lib| lib.len())
6833 .map_err(|e| PyValueError::new_err(e.to_string()))
6834}
6835
6836#[pyfunction]
6838fn fispact_is_output(path: &str) -> bool {
6839 nucleide_fispact_io::is_fispact_output(path)
6840}
6841
6842#[pyfunction]
6844fn enrichment_prod_per_feed(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
6845 nucleide_enrichment::prod_per_feed(x_feed, x_prod, x_tail)
6846}
6847
6848#[pyfunction]
6850fn enrichment_tail_per_feed(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
6851 nucleide_enrichment::tail_per_feed(x_feed, x_prod, x_tail)
6852}
6853
6854#[pyfunction]
6856fn enrichment_tail_per_prod(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
6857 nucleide_enrichment::tail_per_prod(x_feed, x_prod, x_tail)
6858}
6859
6860#[pyfunction]
6862fn enrichment_feed_per_prod(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
6863 nucleide_enrichment::feed_per_prod(x_feed, x_prod, x_tail)
6864}
6865
6866#[pyfunction]
6868fn enrichment_feed_per_tail(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
6869 nucleide_enrichment::feed_per_tail(x_feed, x_prod, x_tail)
6870}
6871
6872#[pyfunction]
6874fn enrichment_prod_per_tail(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
6875 nucleide_enrichment::prod_per_tail(x_feed, x_prod, x_tail)
6876}
6877
6878#[pyfunction]
6880#[allow(non_snake_case)]
6881fn enrichment_alphastar_i(alpha: f64, Mstar: f64, M_i: f64) -> f64 {
6882 nucleide_enrichment::alphastar_i(alpha, Mstar, M_i)
6883}
6884
6885#[pyfunction]
6892fn kinetics_from_ifp(
6893 py: Python<'_>,
6894 betas: Vec<f64>,
6895 lambda_gen: f64,
6896 lambdas: Vec<f64>,
6897) -> PyResult<Py<PyAny>> {
6898 use pyo3::types::PyDict;
6899 let params = nucleide_kinetics::KineticParams::from_ifp(betas, lambda_gen, lambdas)
6900 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6901 let d = PyDict::new(py);
6902 d.set_item("betas", params.betas()).ok();
6903 d.set_item("lambdas", params.lambdas()).ok();
6904 d.set_item("lambda_gen", params.lambda_gen()).ok();
6905 d.set_item("beta_total", params.beta_total()).ok();
6906 d.set_item("groups", params.groups()).ok();
6907 Ok(d.into_any().unbind())
6908}
6909
6910#[pyfunction]
6913#[pyo3(signature = (tally, selection="total", tolerance=0.5, null_value=0.0))]
6914fn magic_with(
6915 tally: &PyMeshTally,
6916 selection: &str,
6917 tolerance: f64,
6918 null_value: f64,
6919) -> PyResult<PyMagicOutput> {
6920 let sel = match selection.trim().to_ascii_lowercase().as_str() {
6921 "total" => nucleide_vr_tools::magic::MagicSelection::Total,
6922 "per_group" | "pergroup" | "per-group" => {
6923 nucleide_vr_tools::magic::MagicSelection::PerGroup
6924 }
6925 other => {
6926 return Err(PyValueError::new_err(format!(
6927 "selection must be total|per_group, got `{other}`"
6928 )));
6929 }
6930 };
6931 let params = nucleide_vr_tools::magic::MagicParams {
6932 tolerance,
6933 null_value,
6934 };
6935 nucleide_vr_tools::magic::magic_with(&tally.inner, sel, params)
6936 .map(|inner| PyMagicOutput { inner })
6937 .map_err(|e| PyValueError::new_err(e.to_string()))
6938}
6939
6940#[pyfunction]
6942fn mcpl_statsum_validate(comment: &str) -> PyResult<String> {
6943 nucleide_mcpl_io::statsum_validate(comment)
6944 .map(str::to_string)
6945 .map_err(PyValueError::new_err)
6946}
6947
6948#[pyfunction]
6950fn mcpl_statsum_comment(key: &str, value: f64) -> PyResult<String> {
6951 nucleide_mcpl_io::statsum_comment(key, value).map_err(|e| PyValueError::new_err(e.to_string()))
6952}
6953
6954#[pymodule]
6956fn _internal(m: &Bound<'_, PyModule>) -> PyResult<()> {
6957 m.add_function(wrap_pyfunction!(version, m)?)?;
6958 m.add_function(wrap_pyfunction!(from_zaid, m)?)?;
6959 m.add_function(wrap_pyfunction!(atomic_mass, m)?)?;
6960 m.add_function(wrap_pyfunction!(natural_abundance, m)?)?;
6961 m.add_function(wrap_pyfunction!(rxname_id, m)?)?;
6962 m.add_function(wrap_pyfunction!(rxname_name, m)?)?;
6963 m.add_function(wrap_pyfunction!(rxname_mt, m)?)?;
6964 m.add_function(wrap_pyfunction!(rxname_label, m)?)?;
6965 m.add_function(wrap_pyfunction!(rxname_doc, m)?)?;
6966 m.add_function(wrap_pyfunction!(rxname_reaction, m)?)?;
6967 m.add_function(wrap_pyfunction!(rxname_id_from_nucdelta, m)?)?;
6968 m.add_function(wrap_pyfunction!(rxname_child, m)?)?;
6969 m.add_function(wrap_pyfunction!(rxname_parent, m)?)?;
6970 m.add_function(wrap_pyfunction!(particle_is_valid, m)?)?;
6971 m.add_function(wrap_pyfunction!(particle_is_valid_pdc, m)?)?;
6972 m.add_function(wrap_pyfunction!(particle_is_hydrogen, m)?)?;
6973 m.add_function(wrap_pyfunction!(particle_is_heavy_ion, m)?)?;
6974 m.add_function(wrap_pyfunction!(dose_f1, m)?)?;
6975 m.add_function(wrap_pyfunction!(dose_lung_model, m)?)?;
6976 m.add_function(wrap_pyfunction!(read_xsdir, m)?)?;
6977 m.add_function(wrap_pyfunction!(read_meshtal, m)?)?;
6978 m.add_function(wrap_pyfunction!(read_wwinp, m)?)?;
6979 m.add_function(wrap_pyfunction!(read_mctal, m)?)?;
6980 m.add_function(wrap_pyfunction!(read_ssw, m)?)?;
6981 m.add_function(wrap_pyfunction!(read_ptrac, m)?)?;
6982 m.add_function(wrap_pyfunction!(read_mcpl, m)?)?;
6983 m.add_function(wrap_pyfunction!(write_mcpl, m)?)?;
6984 m.add_function(wrap_pyfunction!(ssw2mcpl, m)?)?;
6985 m.add_function(wrap_pyfunction!(mcpl2ssw, m)?)?;
6986 m.add_function(wrap_pyfunction!(read_endl, m)?)?;
6987 m.add_function(wrap_pyfunction!(endl_endftod, m)?)?;
6988 m.add_function(wrap_pyfunction!(combine_ssw_files, m)?)?;
6989 m.add_function(wrap_pyfunction!(read_chain, m)?)?;
6990 m.add_function(wrap_pyfunction!(build_depletion_system, m)?)?;
6991 m.add_function(wrap_pyfunction!(deplete, m)?)?;
6992 m.add_function(wrap_pyfunction!(deplete_series, m)?)?;
6993 m.add_function(wrap_pyfunction!(simple_xs, m)?)?;
6994 m.add_function(wrap_pyfunction!(scattering_length, m)?)?;
6995 m.add_function(wrap_pyfunction!(decay_energy, m)?)?;
6996 m.add_function(wrap_pyfunction!(decay_branches, m)?)?;
6997 m.add_function(wrap_pyfunction!(decay_branch_fraction, m)?)?;
6998 m.add_function(wrap_pyfunction!(fission_yields, m)?)?;
6999 m.add_function(wrap_pyfunction!(fission_yield, m)?)?;
7000 m.add_function(wrap_pyfunction!(normalize_nuclide, m)?)?;
7001 m.add_function(wrap_pyfunction!(decay_heat, m)?)?;
7002 m.add_function(wrap_pyfunction!(dose_factor, m)?)?;
7003 m.add_function(wrap_pyfunction!(dose_per_g, m)?)?;
7004 m.add_function(wrap_pyfunction!(mix_by_mass, m)?)?;
7005 m.add_function(wrap_pyfunction!(mix_by_volume, m)?)?;
7006 m.add_function(wrap_pyfunction!(specific_activity, m)?)?;
7007 m.add_function(wrap_pyfunction!(materials_doc_to_xml, m)?)?;
7008 m.add_function(wrap_pyfunction!(expand_elements, m)?)?;
7009 m.add_function(wrap_pyfunction!(collapse_elements, m)?)?;
7010 m.add_function(wrap_pyfunction!(read_serpent, m)?)?;
7011 m.add_function(wrap_pyfunction!(read_usrbin, m)?)?;
7012 m.add_function(wrap_pyfunction!(fluka_material_str, m)?)?;
7013 m.add_function(wrap_pyfunction!(fluka_compound_str, m)?)?;
7014 m.add_function(wrap_pyfunction!(fluka_builtin_set, m)?)?;
7015 m.add_function(wrap_pyfunction!(magic, m)?)?;
7016 m.add_function(wrap_pyfunction!(magic_with, m)?)?;
7017 m.add_function(wrap_pyfunction!(write_ssw, m)?)?;
7018 m.add_function(wrap_pyfunction!(mesh_to_geom, m)?)?;
7019 m.add_function(wrap_pyfunction!(half_life, m)?)?;
7020 m.add_function(wrap_pyfunction!(decay_constant, m)?)?;
7021 m.add_function(wrap_pyfunction!(q_value_capture, m)?)?;
7022 m.add_function(wrap_pyfunction!(q_value_alpha, m)?)?;
7023 m.add_function(wrap_pyfunction!(read_inp, m)?)?;
7024 m.add_function(wrap_pyfunction!(from_formula, m)?)?;
7025 m.add_function(wrap_pyfunction!(activity, m)?)?;
7026 m.add_function(wrap_pyfunction!(to_xml, m)?)?;
7027 m.add_function(wrap_pyfunction!(alara_parse_deck, m)?)?;
7028 m.add_function(wrap_pyfunction!(alara_parse_flux, m)?)?;
7029 m.add_function(wrap_pyfunction!(alara_parse_output, m)?)?;
7030 m.add_function(wrap_pyfunction!(alara_expand_schedule, m)?)?;
7031 m.add_function(wrap_pyfunction!(alara_validate_deck, m)?)?;
7032 m.add_function(wrap_pyfunction!(alara_check_block, m)?)?;
7033 m.add_function(wrap_pyfunction!(alara_flux_total, m)?)?;
7034 m.add_function(wrap_pyfunction!(alara_flux_len, m)?)?;
7035 m.add_function(wrap_pyfunction!(alara_output_totals, m)?)?;
7036 m.add_function(wrap_pyfunction!(alara_output_total_activity, m)?)?;
7037 m.add_function(wrap_pyfunction!(alara_photon_total_strength, m)?)?;
7038 m.add_function(wrap_pyfunction!(alara_schedule_total_time, m)?)?;
7039 m.add_function(wrap_pyfunction!(isotxs_parse, m)?)?;
7040 m.add_function(wrap_pyfunction!(rtflux_parse, m)?)?;
7041 m.add_function(wrap_pyfunction!(cccc_rtflux_npoints, m)?)?;
7042 m.add_function(wrap_pyfunction!(cccc_rtflux_point, m)?)?;
7043 m.add_function(wrap_pyfunction!(cccc_rtflux_total, m)?)?;
7044 m.add_function(wrap_pyfunction!(cccc_isotxs_find, m)?)?;
7045 m.add_function(wrap_pyfunction!(cccc_isotxs_len, m)?)?;
7046 m.add_function(wrap_pyfunction!(partisn_render, m)?)?;
7047 m.add_function(wrap_pyfunction!(partisn_validate, m)?)?;
7048 m.add_function(wrap_pyfunction!(fispact_parse_output, m)?)?;
7049 m.add_function(wrap_pyfunction!(fispact_is_output, m)?)?;
7050 m.add_function(wrap_pyfunction!(origen_parse_tape5, m)?)?;
7051 m.add_function(wrap_pyfunction!(origen_parse_tape6, m)?)?;
7052 m.add_function(wrap_pyfunction!(origen_parse_tape9, m)?)?;
7053 m.add_function(wrap_pyfunction!(origen_tape6_find, m)?)?;
7054 m.add_function(wrap_pyfunction!(origen_tape6_total_activity, m)?)?;
7055 m.add_function(wrap_pyfunction!(origen_tape9_find, m)?)?;
7056 m.add_function(wrap_pyfunction!(r2s_from_deck, m)?)?;
7057 m.add_function(wrap_pyfunction!(r2s_from_snapshot, m)?)?;
7058 m.add_function(wrap_pyfunction!(r2s_validate, m)?)?;
7059 m.add_function(wrap_pyfunction!(r2s_expand, m)?)?;
7060 m.add_function(wrap_pyfunction!(r2s_assemble, m)?)?;
7061 m.add_function(wrap_pyfunction!(r2s_tag_zone_strength, m)?)?;
7062 m.add_function(wrap_pyfunction!(r2s_photon_group_sums, m)?)?;
7063 m.add_function(wrap_pyfunction!(kinetics_solve, m)?)?;
7064 m.add_function(wrap_pyfunction!(kinetics_equilibrium, m)?)?;
7065 m.add_function(wrap_pyfunction!(kinetics_initial_rate, m)?)?;
7066 m.add_function(wrap_pyfunction!(kinetics_inhour_rho, m)?)?;
7067 m.add_function(wrap_pyfunction!(kinetics_stable_period, m)?)?;
7068 m.add_function(wrap_pyfunction!(kinetics_prompt_jump, m)?)?;
7069 m.add_function(wrap_pyfunction!(kinetics_from_ifp, m)?)?;
7070 m.add_function(wrap_pyfunction!(spectroscopy_rect_smooth, m)?)?;
7071 m.add_function(wrap_pyfunction!(spectroscopy_five_point_smooth, m)?)?;
7072 m.add_function(wrap_pyfunction!(spectroscopy_calc_bg, m)?)?;
7073 m.add_function(wrap_pyfunction!(spectroscopy_gross_count, m)?)?;
7074 m.add_function(wrap_pyfunction!(spectroscopy_net_counts, m)?)?;
7075 m.add_function(wrap_pyfunction!(spectroscopy_energy_bins, m)?)?;
7076 m.add_function(wrap_pyfunction!(spectroscopy_detector_efficiency, m)?)?;
7077 m.add_function(wrap_pyfunction!(spectroscopy_fit_efficiency, m)?)?;
7078 m.add_function(wrap_pyfunction!(spectroscopy_xray_lines, m)?)?;
7079 m.add_function(wrap_pyfunction!(spectroscopy_sdef_decay_source, m)?)?;
7080 m.add_function(wrap_pyfunction!(spectroscopy_parse_dollar_spe, m)?)?;
7081 m.add_function(wrap_pyfunction!(spectroscopy_parse_spe, m)?)?;
7082 m.add_function(wrap_pyfunction!(spectroscopy_read_dollar_spe, m)?)?;
7083 m.add_function(wrap_pyfunction!(spectroscopy_read_spe, m)?)?;
7084 m.add_function(wrap_pyfunction!(spectroscopy_parse_lines_tsv, m)?)?;
7085 m.add_function(wrap_pyfunction!(spectroscopy_read_decay_lines, m)?)?;
7086 m.add_function(wrap_pyfunction!(uq_sample_mvn, m)?)?;
7087 m.add_function(wrap_pyfunction!(uq_sample_lhs, m)?)?;
7088 m.add_function(wrap_pyfunction!(uq_sample_lognormal, m)?)?;
7089 m.add_function(wrap_pyfunction!(uq_lognormal_mean, m)?)?;
7090 m.add_function(wrap_pyfunction!(uq_lognormal_cov, m)?)?;
7091 m.add_function(wrap_pyfunction!(uq_sample_mean, m)?)?;
7092 m.add_function(wrap_pyfunction!(uq_sample_cov, m)?)?;
7093 m.add_function(wrap_pyfunction!(uq_check_convergence, m)?)?;
7094 m.add_function(wrap_pyfunction!(uq_perturb_branches, m)?)?;
7095 m.add_function(wrap_pyfunction!(uq_perturb_energies, m)?)?;
7096 m.add_function(wrap_pyfunction!(uq_passthrough, m)?)?;
7097 m.add_function(wrap_pyfunction!(uq_perturb_fission_yields, m)?)?;
7098 m.add_function(wrap_pyfunction!(parse_deck, m)?)?;
7099 m.add_function(wrap_pyfunction!(read_deck, m)?)?;
7100 m.add_function(wrap_pyfunction!(cumulative_decays, m)?)?;
7101 m.add_function(wrap_pyfunction!(progeny, m)?)?;
7102 m.add_function(wrap_pyfunction!(branching_fraction, m)?)?;
7103 m.add_function(wrap_pyfunction!(decay_mode, m)?)?;
7104 m.add_function(wrap_pyfunction!(chain_edges, m)?)?;
7105 m.add_function(wrap_pyfunction!(armi_to_nucid, m)?)?;
7106 m.add_function(wrap_pyfunction!(nucid_to_armi, m)?)?;
7107 m.add_function(wrap_pyfunction!(mcc3_to_nucid, m)?)?;
7108 m.add_function(wrap_pyfunction!(check_labels, m)?)?;
7109 m.add_function(wrap_pyfunction!(audit_material, m)?)?;
7110 m.add_function(wrap_pyfunction!(separate_material, m)?)?;
7111 m.add_function(wrap_pyfunction!(blend_material, m)?)?;
7112 m.add_function(wrap_pyfunction!(enrichment_value_func, m)?)?;
7113 m.add_function(wrap_pyfunction!(enrichment_swu_per_feed, m)?)?;
7114 m.add_function(wrap_pyfunction!(enrichment_swu_per_prod, m)?)?;
7115 m.add_function(wrap_pyfunction!(enrichment_swu_per_tail, m)?)?;
7116 m.add_function(wrap_pyfunction!(enrichment_prod_per_feed, m)?)?;
7117 m.add_function(wrap_pyfunction!(enrichment_tail_per_feed, m)?)?;
7118 m.add_function(wrap_pyfunction!(enrichment_tail_per_prod, m)?)?;
7119 m.add_function(wrap_pyfunction!(enrichment_feed_per_prod, m)?)?;
7120 m.add_function(wrap_pyfunction!(enrichment_feed_per_tail, m)?)?;
7121 m.add_function(wrap_pyfunction!(enrichment_prod_per_tail, m)?)?;
7122 m.add_function(wrap_pyfunction!(enrichment_alphastar_i, m)?)?;
7123 m.add_function(wrap_pyfunction!(mcpl_statsum_validate, m)?)?;
7124 m.add_function(wrap_pyfunction!(mcpl_statsum_comment, m)?)?;
7125 m.add_class::<PyCusum>()?;
7126 m.add_function(wrap_pyfunction!(emit_cards, m)?)?;
7127 m.add_function(wrap_pyfunction!(emit_drift_table, m)?)?;
7128 m.add_function(wrap_pyfunction!(emit_armi_cards, m)?)?;
7129 m.add_function(wrap_pyfunction!(emit_armi_drift_table, m)?)?;
7130 m.add_class::<PyNuclide>()?;
7131 m.add_class::<PyParticle>()?;
7132 m.add_class::<PyXsdir>()?;
7133 m.add_class::<PyXsdirTable>()?;
7134 m.add_class::<PyMeshtal>()?;
7135 m.add_class::<PyMeshTally>()?;
7136 m.add_class::<PyWwinp>()?;
7137 m.add_class::<PyMctal>()?;
7138 m.add_class::<PySurfSrc>()?;
7139 m.add_class::<PyPtracFile>()?;
7140 m.add_class::<PyMcplFile>()?;
7141 m.add_class::<PyEndlLibrary>()?;
7142 m.add_class::<PyChain>()?;
7143 m.add_class::<PyDepletionSystem>()?;
7144 m.add_class::<PyUsrbinTally>()?;
7145 m.add_class::<PyMagicOutput>()?;
7146 m.add_class::<PyAliasTable>()?;
7147 m.add_class::<PyMeshSourceSampler>()?;
7148 m.add_class::<PyCascade>()?;
7149 m.add_class::<PyMaterialsCompendium>()?;
7150 m.add_class::<PyDeckProblem>()?;
7151 m.add_class::<PyInventory>()?;
7152 Ok(())
7153}