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#[pyclass(name = "KdeSampler")]
2209struct PyKdeSampler {
2210 inner: nucleide_vr_tools::kde::KdeSampler,
2211}
2212
2213#[pymethods]
2214impl PyKdeSampler {
2215 #[new]
2218 #[pyo3(signature = (samples, bandwidth=None))]
2219 fn new(samples: Vec<Vec<f64>>, bandwidth: Option<&Bound<'_, PyAny>>) -> PyResult<Self> {
2220 let rule = match bandwidth {
2221 None => nucleide_vr_tools::kde::Bandwidth::Silverman,
2222 Some(b) => {
2223 if let Ok(name) = b.extract::<String>() {
2224 match name.as_str() {
2225 "silverman" => nucleide_vr_tools::kde::Bandwidth::Silverman,
2226 other => {
2227 return Err(PyValueError::new_err(format!(
2228 "bandwidth must be silverman or a width list, got `{other}`"
2229 )))
2230 }
2231 }
2232 } else {
2233 let widths = b.extract::<Vec<f64>>().map_err(|_| {
2234 PyValueError::new_err("bandwidth must be silverman or a width list")
2235 })?;
2236 nucleide_vr_tools::kde::Bandwidth::Fixed(widths)
2237 }
2238 }
2239 };
2240 nucleide_vr_tools::kde::KdeSampler::fit(&samples, rule)
2241 .map(|inner| PyKdeSampler { inner })
2242 .map_err(|e| PyValueError::new_err(e.to_string()))
2243 }
2244 fn pdf(&self, point: Vec<f64>) -> PyResult<f64> {
2246 self.inner
2247 .pdf(&point)
2248 .map_err(|e| PyValueError::new_err(e.to_string()))
2249 }
2250 fn draw(&self, u: f64, normals: Vec<f64>) -> PyResult<Vec<f64>> {
2252 self.inner
2253 .draw(u, &normals)
2254 .map_err(|e| PyValueError::new_err(e.to_string()))
2255 }
2256 fn bandwidths(&self) -> Vec<f64> {
2258 self.inner.bandwidths().to_vec()
2259 }
2260 fn n_samples(&self) -> usize {
2262 self.inner.n_samples()
2263 }
2264}
2265
2266#[pyfunction]
2269#[pyo3(signature = (ssw, path, tracks=None))]
2270fn write_ssw(
2271 ssw: &PySurfSrc,
2272 path: &str,
2273 tracks: Option<Vec<BTreeMap<String, f64>>>,
2274) -> PyResult<()> {
2275 let header = ssw.inner.header.clone();
2276 let track_data: Vec<nucleide_mcnp_io::surfsrc::TrackData> = match tracks {
2277 Some(dict_tracks) => dict_tracks
2278 .iter()
2279 .map(|d| {
2280 let g = |k: &str| d.get(k).copied().unwrap_or(0.0);
2281 let mut record = vec![0.0f64; nucleide_mcnp_io::surfsrc::TrackData::RECORD_WIDTH];
2282 record[0] = g("nps");
2283 record[1] = g("bitarray");
2284 record[2] = g("wgt");
2285 record[3] = g("erg");
2286 record[4] = g("tme");
2287 record[5] = g("x");
2288 record[6] = g("y");
2289 record[7] = g("z");
2290 record[8] = g("u");
2291 record[9] = g("v");
2292 record[10] = g("cs");
2293 nucleide_mcnp_io::surfsrc::TrackData::from_record(record)
2294 })
2295 .collect(),
2296 None => ssw
2297 .inner
2298 .read_tracklist()
2299 .map_err(|e| PyValueError::new_err(e.to_string()))?,
2300 };
2301 let mut f = std::fs::File::create(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
2302 nucleide_mcnp_io::surfsrc::write_to(&mut f, &header, &track_data)
2303 .map_err(|e| PyValueError::new_err(e.to_string()))
2304}
2305
2306#[pyfunction]
2308fn mesh_to_geom(
2309 x_bounds: Vec<f64>,
2310 y_bounds: Vec<f64>,
2311 z_bounds: Vec<f64>,
2312 cell_materials: Vec<Option<(String, f64)>>,
2313 title_card: &str,
2314) -> String {
2315 let opts = nucleide_mcnp_io::deck::DeckOptions {
2316 title_card: title_card.to_string(),
2317 frac_type: nucleide_mcnp_io::deck::FracType::Mass,
2318 };
2319 nucleide_mcnp_io::deck::mesh_to_geom(&x_bounds, &y_bounds, &z_bounds, &cell_materials, &opts)
2320}
2321
2322#[pyfunction]
2333fn alara_parse_deck(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
2334 let owned = text.to_owned();
2335 let deck = py
2336 .detach(move || nucleide_alara_io::AlaraDeck::parse(&owned))
2337 .map_err(ala_err)?;
2338 Ok(deck_to_py(py, &deck))
2339}
2340
2341fn ala_err(e: nucleide_alara_io::Error) -> PyErr {
2342 PyValueError::new_err(e.to_string())
2343}
2344
2345fn deck_to_py(py: Python<'_>, deck: &nucleide_alara_io::AlaraDeck) -> Py<PyAny> {
2346 use pyo3::types::PyDict;
2347 let out = PyDict::new(py);
2348 let block_kinds: Vec<&str> = deck.block_kinds();
2349 out.set_item("block_kinds", block_kinds).ok();
2350 out.set_item("geometry", deck.geometry.as_ref().map(|g| g.kind.clone()))
2351 .ok();
2352 let mixtures: Vec<Py<PyAny>> = deck.mixtures.iter().map(|m| mixture_to_py(py, m)).collect();
2353 out.set_item("mixtures", mixtures).ok();
2354 let fluxes: Vec<Py<PyAny>> = deck.fluxes.iter().map(|f| fluxdef_to_py(py, f)).collect();
2355 out.set_item("fluxes", fluxes).ok();
2356 out.set_item(
2357 "cooling_times_s",
2358 deck.cooling
2359 .as_ref()
2360 .map(|c| c.times_s.clone())
2361 .unwrap_or_default(),
2362 )
2363 .ok();
2364 let schedules: Vec<Py<PyAny>> = deck
2365 .schedules
2366 .iter()
2367 .map(|s| {
2368 let d = PyDict::new(py);
2369 let items: Vec<Vec<String>> = s.items.iter().map(|it| it.tokens.clone()).collect();
2370 d.set_item("name", &s.name).ok();
2371 d.set_item("items", items).ok();
2372 d.into_any().unbind()
2373 })
2374 .collect();
2375 out.set_item("schedules", schedules).ok();
2376 let histories: Vec<Py<PyAny>> = deck
2377 .pulse_histories
2378 .iter()
2379 .map(|h| {
2380 let d = PyDict::new(py);
2381 let levels: Vec<Py<PyAny>> = h
2382 .levels
2383 .iter()
2384 .map(|l| {
2385 let e = PyDict::new(py);
2386 e.set_item("pulses", l.pulses).ok();
2387 e.set_item("delay_s", l.delay_s).ok();
2388 e.into_any().unbind()
2389 })
2390 .collect();
2391 d.set_item("name", &h.name).ok();
2392 d.set_item("levels", levels).ok();
2393 d.into_any().unbind()
2394 })
2395 .collect();
2396 out.set_item("pulse_histories", histories).ok();
2397 let outputs: Vec<Py<PyAny>> = deck
2398 .outputs
2399 .iter()
2400 .map(|o| {
2401 let d = PyDict::new(py);
2402 d.set_item("resolution", &o.resolution).ok();
2403 d.set_item("entries", o.entries.clone()).ok();
2404 d.into_any().unbind()
2405 })
2406 .collect();
2407 out.set_item("outputs", outputs).ok();
2408 out.set_item("truncation", deck.truncation.as_ref().map(|t| t.tolerance))
2409 .ok();
2410 out.into_any().unbind()
2411}
2412
2413fn mixture_to_py(py: Python<'_>, mix: &nucleide_alara_io::deck::Mixture) -> Py<PyAny> {
2414 use pyo3::types::PyDict;
2415 let entries: Vec<Py<PyAny>> = mix
2416 .entries
2417 .iter()
2418 .map(|e| mixture_entry_to_py(py, e))
2419 .collect();
2420 let d = PyDict::new(py);
2421 d.set_item("name", &mix.name).ok();
2422 d.set_item("entries", entries).ok();
2423 d.into_any().unbind()
2424}
2425
2426fn mixture_entry_to_py(py: Python<'_>, entry: &nucleide_alara_io::deck::MixtureEntry) -> Py<PyAny> {
2427 use nucleide_alara_io::deck::MixtureEntry as E;
2428 use pyo3::types::PyDict;
2429 let d = PyDict::new(py);
2430 match entry {
2431 E::Material {
2432 name,
2433 rel_density,
2434 vol_fraction,
2435 } => {
2436 d.set_item("kind", "material").ok();
2437 d.set_item("name", name).ok();
2438 d.set_item("rel_density", *rel_density).ok();
2439 d.set_item("vol_fraction", *vol_fraction).ok();
2440 }
2441 E::Element {
2442 symbol,
2443 rel_density,
2444 vol_fraction,
2445 } => {
2446 d.set_item("kind", "element").ok();
2447 d.set_item("symbol", symbol).ok();
2448 d.set_item("rel_density", *rel_density).ok();
2449 d.set_item("vol_fraction", *vol_fraction).ok();
2450 }
2451 E::Like {
2452 mixture,
2453 rel_density,
2454 } => {
2455 d.set_item("kind", "like").ok();
2456 d.set_item("mixture", mixture).ok();
2457 d.set_item("rel_density", *rel_density).ok();
2458 }
2459 E::Target { target_kind, name } => {
2460 d.set_item("kind", "target").ok();
2461 d.set_item("target_kind", target_kind).ok();
2462 d.set_item("name", name).ok();
2463 }
2464 }
2465 d.into_any().unbind()
2466}
2467
2468fn fluxdef_to_py(py: Python<'_>, flux: &nucleide_alara_io::deck::FluxDef) -> Py<PyAny> {
2469 use pyo3::types::PyDict;
2470 let d = PyDict::new(py);
2471 d.set_item("name", &flux.name).ok();
2472 d.set_item("file", &flux.file).ok();
2473 d.set_item("scale", flux.scale).ok();
2474 d.set_item("skip", flux.skip).ok();
2475 d.set_item("format", &flux.format).ok();
2476 d.into_any().unbind()
2477}
2478
2479#[pyfunction]
2484fn alara_parse_flux(py: Python<'_>, text: &str, name: &str) -> PyResult<Py<PyAny>> {
2485 let owned_text = text.to_owned();
2486 let owned_name = name.to_owned();
2487 let spectra = py
2488 .detach(move || nucleide_alara_io::FluxSpectra::parse(&owned_name, &owned_text))
2489 .map_err(ala_err)?;
2490 use pyo3::types::PyDict;
2491 let d = PyDict::new(py);
2492 d.set_item("name", spectra.name.clone()).ok();
2493 d.set_item("groups_per_interval", spectra.groups_per_interval)
2494 .ok();
2495 d.set_item("num_intervals", spectra.num_intervals()).ok();
2496 let totals: Vec<f64> = spectra.intervals.iter().map(|iv| iv.iter().sum()).collect();
2497 d.set_item("totals", totals).ok();
2498 d.set_item("total", spectra.total()).ok();
2499 d.set_item("intervals", spectra.intervals.clone()).ok();
2500 Ok(d.into_any().unbind())
2501}
2502
2503#[pyfunction]
2509fn alara_parse_output(
2510 py: Python<'_>,
2511 text: &str,
2512 run_lbl: &str,
2513) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
2514 let owned_text = text.to_owned();
2515 let owned_lbl = run_lbl.to_owned();
2516 let rows = py
2517 .detach(move || {
2518 nucleide_alara_io::output::ResponseFrame::parse(&owned_text, &owned_lbl).map(|f| f.rows)
2519 })
2520 .map_err(ala_err)?;
2521 Ok(rows
2522 .iter()
2523 .map(|r| {
2524 let mut d = BTreeMap::new();
2525 d.insert(
2526 "time_s".to_string(),
2527 r.time_s.into_pyobject(py).unwrap().unbind().into_any(),
2528 );
2529 d.insert(
2530 "time_label".to_string(),
2531 r.time_label
2532 .clone()
2533 .into_pyobject(py)
2534 .unwrap()
2535 .unbind()
2536 .into_any(),
2537 );
2538 d.insert(
2539 "nuclide".to_string(),
2540 r.nuclide
2541 .clone()
2542 .into_pyobject(py)
2543 .unwrap()
2544 .unbind()
2545 .into_any(),
2546 );
2547 d.insert(
2548 "half_life_s".to_string(),
2549 r.half_life_s.into_pyobject(py).unwrap().unbind().into_any(),
2550 );
2551 d.insert(
2552 "run_lbl".to_string(),
2553 r.run_lbl
2554 .clone()
2555 .into_pyobject(py)
2556 .unwrap()
2557 .unbind()
2558 .into_any(),
2559 );
2560 d.insert(
2561 "block".to_string(),
2562 r.block
2563 .as_str()
2564 .into_pyobject(py)
2565 .unwrap()
2566 .unbind()
2567 .into_any(),
2568 );
2569 d.insert(
2570 "block_name".to_string(),
2571 r.block_name
2572 .clone()
2573 .into_pyobject(py)
2574 .unwrap()
2575 .unbind()
2576 .into_any(),
2577 );
2578 d.insert(
2579 "block_num".to_string(),
2580 r.block_num.into_pyobject(py).unwrap().unbind().into_any(),
2581 );
2582 d.insert(
2583 "variable".to_string(),
2584 r.variable
2585 .as_str()
2586 .into_pyobject(py)
2587 .unwrap()
2588 .unbind()
2589 .into_any(),
2590 );
2591 d.insert(
2592 "var_unit".to_string(),
2593 r.var_unit
2594 .clone()
2595 .into_pyobject(py)
2596 .unwrap()
2597 .unbind()
2598 .into_any(),
2599 );
2600 d.insert(
2601 "value".to_string(),
2602 r.value.into_pyobject(py).unwrap().unbind().into_any(),
2603 );
2604 d
2605 })
2606 .collect())
2607}
2608
2609#[pyfunction]
2616#[pyo3(signature = (deck_text, top=None))]
2617fn alara_expand_schedule(
2618 py: Python<'_>,
2619 deck_text: &str,
2620 top: Option<&str>,
2621) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
2622 let owned_text = deck_text.to_owned();
2623 let owned_top = top.map(str::to_owned);
2624 let steps = py
2625 .detach(move || expand_deck_schedules(&owned_text, owned_top.as_deref()))
2626 .map_err(PyValueError::new_err)?;
2627 Ok(steps
2628 .into_iter()
2629 .map(|s| {
2630 let mut d = BTreeMap::new();
2631 let cooling = s.is_cooling();
2632 d.insert(
2633 "duration_s".to_string(),
2634 s.duration_s.into_pyobject(py).unwrap().unbind().into_any(),
2635 );
2636 d.insert(
2637 "flux".to_string(),
2638 s.flux
2639 .clone()
2640 .into_pyobject(py)
2641 .unwrap()
2642 .unbind()
2643 .into_any(),
2644 );
2645 d.insert(
2646 "is_cooling".to_string(),
2647 pyo3::types::PyBool::new(py, cooling)
2648 .to_owned()
2649 .into_any()
2650 .unbind(),
2651 );
2652 d
2653 })
2654 .collect())
2655}
2656
2657fn expand_deck_schedules(
2658 deck_text: &str,
2659 top: Option<&str>,
2660) -> Result<Vec<nucleide_alara_io::FlatStep>, String> {
2661 let deck = nucleide_alara_io::AlaraDeck::parse(deck_text).map_err(|e| e.to_string())?;
2662 let mut scheds = Vec::with_capacity(deck.schedules.len());
2663 for raw in &deck.schedules {
2664 let mut items = Vec::with_capacity(raw.items.len());
2665 for entry in &raw.items {
2666 items.push(
2667 parse_deck_sched_item(&entry.tokens)
2668 .map_err(|m| format!("schedule `{}` line {}: {m}", raw.name, entry.line))?,
2669 );
2670 }
2671 scheds.push(nucleide_alara_io::schedule::ScheduleDef {
2672 name: raw.name.clone(),
2673 items,
2674 });
2675 }
2676 let histories: Vec<nucleide_alara_io::schedule::PulseHistory> = deck
2677 .pulse_histories
2678 .iter()
2679 .map(|h| nucleide_alara_io::schedule::PulseHistory {
2680 name: h.name.clone(),
2681 levels: h
2682 .levels
2683 .iter()
2684 .map(|l| nucleide_alara_io::schedule::PulseLevel {
2685 count: l.pulses,
2686 delay_s: l.delay_s,
2687 })
2688 .collect(),
2689 })
2690 .collect();
2691 match top {
2692 Some(name) => {
2693 nucleide_alara_io::expand_from(name, &scheds, &histories).map_err(|e| e.to_string())
2694 }
2695 None => nucleide_alara_io::expand(&scheds, &histories).map_err(|e| e.to_string()),
2696 }
2697}
2698
2699fn parse_deck_sched_item(tokens: &[String]) -> Result<nucleide_alara_io::SchedItem, String> {
2700 match tokens {
2701 [op_text, op_unit, flux, history, delay_text, delay_unit] => {
2702 let op: f64 = op_text
2703 .parse()
2704 .map_err(|_| format!("expected operating time, found `{op_text}`"))?;
2705 let delay: f64 = delay_text
2706 .parse()
2707 .map_err(|_| format!("expected delay, found `{delay_text}`"))?;
2708 let op_time_s =
2709 nucleide_alara_io::parse_time_to_seconds(op, op_unit).map_err(|e| e.to_string())?;
2710 let delay_s = nucleide_alara_io::parse_time_to_seconds(delay, delay_unit)
2711 .map_err(|e| e.to_string())?;
2712 Ok(nucleide_alara_io::SchedItem::Pulse {
2713 op_time_s,
2714 flux: flux.clone(),
2715 history: history.clone(),
2716 delay_s,
2717 })
2718 }
2719 [name, history, delay_text, delay_unit] => {
2720 let delay: f64 = delay_text
2721 .parse()
2722 .map_err(|_| format!("expected delay, found `{delay_text}`"))?;
2723 let delay_s = nucleide_alara_io::parse_time_to_seconds(delay, delay_unit)
2724 .map_err(|e| e.to_string())?;
2725 Ok(nucleide_alara_io::SchedItem::SubSchedule {
2726 name: name.clone(),
2727 history: history.clone(),
2728 delay_s,
2729 })
2730 }
2731 _ => Err(format!(
2732 "expected 4- or 6-token schedule item, found {}",
2733 tokens.join(" ")
2734 )),
2735 }
2736}
2737
2738#[pyfunction]
2744fn half_life(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2745 lookup(key, nucleide_nuclei::data::half_life)
2746}
2747
2748#[pyfunction]
2750fn decay_constant(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2751 lookup(key, nucleide_nuclei::data::decay_constant)
2752}
2753
2754#[pyfunction]
2756fn q_value_capture(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2757 lookup(key, nucleide_nuclei::data::q_value_neutron_capture)
2758}
2759
2760#[pyfunction]
2762fn q_value_alpha(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2763 lookup(key, nucleide_nuclei::data::q_value_alpha)
2764}
2765
2766#[pyfunction]
2770fn read_inp(path: &str) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
2771 let mats = nucleide_mcnp_io::inp::materials_from_file(path)
2772 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2773 Python::attach(|py| {
2774 Ok(mats
2775 .into_iter()
2776 .map(|m| {
2777 let mut d = BTreeMap::new();
2778 d.insert(
2779 "number".to_string(),
2780 m.number.into_pyobject(py).unwrap().unbind().into_any(),
2781 );
2782 let fr: BTreeMap<String, f64> = m
2783 .fractions
2784 .iter()
2785 .map(|(id, f)| (id.to_name(), *f))
2786 .collect();
2787 d.insert(
2788 "fractions".to_string(),
2789 fr.into_pyobject(py).unwrap().unbind().into_any(),
2790 );
2791 d.insert(
2792 "fraction_type".to_string(),
2793 match m.fraction_type {
2794 nucleide_mcnp_io::inp::FracKind::Atom => "atom",
2795 nucleide_mcnp_io::inp::FracKind::Mass => "mass",
2796 }
2797 .into_pyobject(py)
2798 .unwrap()
2799 .unbind()
2800 .into_any(),
2801 );
2802 d.insert(
2803 "density".to_string(),
2804 m.density.into_pyobject(py).unwrap().unbind().into_any(),
2805 );
2806 d.insert(
2807 "comments".to_string(),
2808 m.comments
2809 .join(" ")
2810 .into_pyobject(py)
2811 .unwrap()
2812 .unbind()
2813 .into_any(),
2814 );
2815 d
2816 })
2817 .collect())
2818 })
2819}
2820
2821fn comp_to_material(comp: BTreeMap<String, f64>) -> PyResult<nucleide_material::Material> {
2822 let mut mat = nucleide_material::Material::new();
2823 for (name, grams) in &comp {
2824 let id = nucleide_nuclei::NuclideId::from_name(name)
2825 .map_err(|e| PyValueError::new_err(format!("`{name}`: {e}")))?;
2826 mat.add_nuclide(id, *grams);
2827 }
2828 Ok(mat)
2829}
2830
2831#[pyfunction]
2834fn from_formula(formula: &str) -> PyResult<BTreeMap<String, f64>> {
2835 use nucleide_material::AbundanceProvider;
2836 let parsed = nucleide_material::parse_formula(formula)
2837 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2838 let mut nat = Vec::new();
2840 for (z, count) in &parsed {
2841 if let Some(isotopes) = nucleide_material::NaturalAbundances.natural_isotopes(*z) {
2842 for (id, frac) in isotopes {
2843 nat.push((id, frac * count));
2844 }
2845 }
2846 }
2847 let total: f64 = nat.iter().map(|(_, c)| c).sum();
2848 if total <= 0.0 {
2849 return Err(PyValueError::new_err("empty formula expansion"));
2850 }
2851 let mut out: BTreeMap<String, f64> = BTreeMap::new();
2852 for (id, atoms) in nat {
2853 *out.entry(id.to_name()).or_insert(0.0) += atoms / total;
2854 }
2855 Ok(out)
2856}
2857
2858#[pyfunction]
2861fn activity(comp: BTreeMap<String, f64>) -> PyResult<BTreeMap<String, f64>> {
2862 let mat = comp_to_material(comp)?;
2863 let analytics = nucleide_material::Analytics {
2864 masses: &nucleide_material::Ame2020,
2865 decays: &nucleide_material::ChainDecays,
2866 };
2867 let per_nuc = mat
2868 .activity(&analytics)
2869 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2870 let specific = mat
2871 .specific_activity(&analytics)
2872 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2873 let mut out: BTreeMap<String, f64> = per_nuc
2874 .into_iter()
2875 .map(|(id, v)| (id.to_name(), v))
2876 .collect();
2877 out.insert("specific".to_string(), specific);
2878 Ok(out)
2879}
2880
2881#[pyfunction]
2883fn to_xml(comp: BTreeMap<String, f64>, name: &str, density: f64, units: &str) -> PyResult<String> {
2884 let mat = comp_to_material(comp)?;
2885 mat.to_xml(name, density, units)
2886 .map_err(|e| PyValueError::new_err(e.to_string()))
2887}
2888
2889#[pyclass(name = "Cascade")]
2891struct PyCascade {
2892 inner: std::sync::Mutex<nucleide_enrichment::Cascade>,
2893}
2894
2895#[pymethods]
2896impl PyCascade {
2897 #[staticmethod]
2899 fn default_uranium() -> Self {
2900 Self {
2901 inner: std::sync::Mutex::new(nucleide_enrichment::default_uranium_cascade()),
2902 }
2903 }
2904
2905 #[new]
2908 #[allow(non_snake_case)]
2909 #[allow(clippy::too_many_arguments)]
2910 fn new(
2911 alpha: f64,
2912 Mstar: f64,
2913 j: u32,
2914 k: u32,
2915 N: f64,
2916 M: f64,
2917 x_feed_j: f64,
2918 x_prod_j: f64,
2919 x_tail_j: f64,
2920 mat_feed: BTreeMap<String, f64>,
2921 ) -> PyResult<Self> {
2922 let mut feed = BTreeMap::new();
2923 for (name, frac) in mat_feed {
2924 let id = NuclideId::from_name(&name).map_err(wrap_nucid_err)?;
2925 feed.insert(id, frac);
2926 }
2927 let casc = nucleide_enrichment::Cascade {
2928 alpha,
2929 Mstar,
2930 j: NuclideId::from_nucid(j),
2931 k: NuclideId::from_nucid(k),
2932 N,
2933 M,
2934 x_feed_j,
2935 x_prod_j,
2936 x_tail_j,
2937 mat_feed: nucleide_enrichment::Stream::with_total_mass(feed, 1.0),
2938 mat_prod: nucleide_enrichment::Stream::new(),
2939 mat_tail: nucleide_enrichment::Stream::new(),
2940 l_t_per_feed: 0.0,
2941 swu_per_feed: 0.0,
2942 swu_per_prod: 0.0,
2943 };
2944 Ok(Self {
2945 inner: std::sync::Mutex::new(casc),
2946 })
2947 }
2948
2949 #[pyo3(signature = (tolerance=None, max_iterations=None))]
2951 fn solve(&self, tolerance: Option<f64>, max_iterations: Option<u32>) -> PyResult<()> {
2952 let tol = tolerance.unwrap_or(nucleide_enrichment::DEFAULT_TOLERANCE);
2953 let iters = max_iterations.unwrap_or(nucleide_enrichment::DEFAULT_MAX_ITER);
2954 let mut c = self
2955 .inner
2956 .lock()
2957 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
2958 *c = nucleide_enrichment::solve_numeric(&c, tol, iters)
2959 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2960 Ok(())
2961 }
2962
2963 #[pyo3(signature = (tolerance=None, max_iterations=None))]
2965 fn solve_multicomponent(
2966 &self,
2967 tolerance: Option<f64>,
2968 max_iterations: Option<u32>,
2969 ) -> PyResult<()> {
2970 let tol = tolerance.unwrap_or(nucleide_enrichment::DEFAULT_TOLERANCE);
2971 let iters = max_iterations.unwrap_or(nucleide_enrichment::DEFAULT_MAX_ITER);
2972 let mut c = self
2973 .inner
2974 .lock()
2975 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
2976 *c = nucleide_enrichment::multicomponent(&c, tol, iters)
2977 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2978 Ok(())
2979 }
2980
2981 #[getter]
2982 fn alpha(&self) -> PyResult<f64> {
2983 Ok(self
2984 .inner
2985 .lock()
2986 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2987 .alpha)
2988 }
2989 #[getter]
2990 #[allow(non_snake_case)]
2991 fn Mstar(&self) -> PyResult<f64> {
2992 Ok(self
2993 .inner
2994 .lock()
2995 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2996 .Mstar)
2997 }
2998 #[getter]
2999 #[allow(non_snake_case)]
3000 fn N(&self) -> PyResult<f64> {
3001 Ok(self
3002 .inner
3003 .lock()
3004 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3005 .N)
3006 }
3007 #[getter]
3008 #[allow(non_snake_case)]
3009 fn M(&self) -> PyResult<f64> {
3010 Ok(self
3011 .inner
3012 .lock()
3013 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3014 .M)
3015 }
3016 #[getter]
3017 fn x_feed_j(&self) -> PyResult<f64> {
3018 Ok(self
3019 .inner
3020 .lock()
3021 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3022 .x_feed_j)
3023 }
3024 #[getter]
3025 fn x_prod_j(&self) -> PyResult<f64> {
3026 Ok(self
3027 .inner
3028 .lock()
3029 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3030 .x_prod_j)
3031 }
3032 #[getter]
3033 fn x_tail_j(&self) -> PyResult<f64> {
3034 Ok(self
3035 .inner
3036 .lock()
3037 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3038 .x_tail_j)
3039 }
3040 #[getter]
3041 fn l_t_per_feed(&self) -> PyResult<f64> {
3042 Ok(self
3043 .inner
3044 .lock()
3045 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3046 .l_t_per_feed)
3047 }
3048 #[getter]
3049 fn swu_per_feed(&self) -> PyResult<f64> {
3050 Ok(self
3051 .inner
3052 .lock()
3053 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3054 .swu_per_feed)
3055 }
3056 #[getter]
3057 fn swu_per_prod(&self) -> PyResult<f64> {
3058 Ok(self
3059 .inner
3060 .lock()
3061 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3062 .swu_per_prod)
3063 }
3064 #[getter]
3066 fn mat_feed(&self) -> PyResult<BTreeMap<String, f64>> {
3067 Ok(self
3068 .inner
3069 .lock()
3070 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3071 .mat_feed
3072 .comp
3073 .iter()
3074 .map(|(id, frac)| (id.to_name(), *frac))
3075 .collect())
3076 }
3077 #[getter]
3079 fn mat_prod(&self) -> PyResult<BTreeMap<String, f64>> {
3080 Ok(self
3081 .inner
3082 .lock()
3083 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3084 .mat_prod
3085 .comp
3086 .iter()
3087 .map(|(id, frac)| (id.to_name(), *frac))
3088 .collect())
3089 }
3090 #[getter]
3092 fn mat_tail(&self) -> PyResult<BTreeMap<String, f64>> {
3093 Ok(self
3094 .inner
3095 .lock()
3096 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3097 .mat_tail
3098 .comp
3099 .iter()
3100 .map(|(id, frac)| (id.to_name(), *frac))
3101 .collect())
3102 }
3103 fn separative_work_per_product(&self) -> PyResult<f64> {
3105 let c = self
3106 .inner
3107 .lock()
3108 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
3109 Ok(nucleide_enrichment::swu_per_prod(
3110 c.x_feed_j, c.x_prod_j, c.x_tail_j,
3111 ))
3112 }
3113
3114 fn __repr__(&self) -> PyResult<String> {
3115 let c = self
3116 .inner
3117 .lock()
3118 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
3119 Ok(format!(
3120 "Cascade(alpha={}, Mstar={}, x_prod_j={:.5})",
3121 c.alpha, c.Mstar, c.x_prod_j
3122 ))
3123 }
3124}
3125
3126#[pyfunction]
3130fn enrichment_value_func(x: f64) -> f64 {
3131 nucleide_enrichment::value_func(x)
3132}
3133
3134#[pyfunction]
3138fn enrichment_swu_per_feed(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
3139 nucleide_enrichment::swu_per_feed(x_feed, x_prod, x_tail)
3140}
3141
3142#[pyfunction]
3146fn enrichment_swu_per_prod(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
3147 nucleide_enrichment::swu_per_prod(x_feed, x_prod, x_tail)
3148}
3149
3150#[pyfunction]
3154fn enrichment_swu_per_tail(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
3155 nucleide_enrichment::swu_per_tail(x_feed, x_prod, x_tail)
3156}
3157
3158#[pyclass(name = "MaterialsCompendium")]
3160struct PyMaterialsCompendium {
3161 inner: nucleide_material::MaterialsLibrary,
3162}
3163
3164#[pymethods]
3165impl PyMaterialsCompendium {
3166 #[staticmethod]
3168 fn load(path: &str) -> PyResult<Self> {
3169 nucleide_material::MaterialsLibrary::from_file(path)
3170 .map(|inner| PyMaterialsCompendium { inner })
3171 .map_err(|e| PyValueError::new_err(e.to_string()))
3172 }
3173
3174 fn __len__(&self) -> usize {
3175 self.inner.len()
3176 }
3177
3178 fn names(&self) -> Vec<String> {
3180 self.inner.names().into_iter().map(String::from).collect()
3181 }
3182
3183 #[pyo3(signature = (name, as_material=false))]
3187 #[allow(clippy::type_complexity)]
3188 fn get(&self, name: &str, as_material: bool) -> PyResult<Option<BTreeMap<String, Py<PyAny>>>> {
3189 let entry = match self.inner.get(name) {
3190 Some(e) => e,
3191 None => return Ok(None),
3192 };
3193 let named_fractions = if as_material {
3195 Some(
3196 entry
3197 .to_material()
3198 .map_err(|e| PyValueError::new_err(e.to_string()))?,
3199 )
3200 } else {
3201 None
3202 };
3203
3204 Ok(Python::attach(|py| {
3205 let mut d: BTreeMap<String, Py<PyAny>> = BTreeMap::new();
3206 d.insert(
3207 "name".into(),
3208 entry
3209 .name
3210 .as_str()
3211 .into_pyobject(py)
3212 .unwrap()
3213 .unbind()
3214 .into_any(),
3215 );
3216 d.insert(
3217 "mat_num".into(),
3218 entry.mat_num.into_pyobject(py).unwrap().unbind().into_any(),
3219 );
3220 d.insert(
3221 "density".into(),
3222 entry.density.into_pyobject(py).unwrap().unbind().into_any(),
3223 );
3224 match &named_fractions {
3225 Some(mat) => {
3226 let fr: BTreeMap<String, f64> =
3227 mat.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect();
3228 d.insert(
3229 "fractions".into(),
3230 fr.into_pyobject(py).unwrap().unbind().into_any(),
3231 );
3232 }
3233 None => {
3234 let fr = entry.weight_fractions();
3235 d.insert(
3236 "fractions".into(),
3237 fr.into_pyobject(py).unwrap().unbind().into_any(),
3238 );
3239 }
3240 }
3241 Some(d)
3242 }))
3243 }
3244}
3245
3246#[pyfunction]
3255fn isotxs_parse(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
3256 let owned = text.to_owned();
3257 let lib = py
3258 .detach(move || nucleide_cccc_io::IsotxsLib::parse(&owned))
3259 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3260 Ok(isotxs_to_py(py, &lib))
3261}
3262
3263fn isotxs_to_py(py: Python<'_>, lib: &nucleide_cccc_io::IsotxsLib) -> Py<PyAny> {
3264 use pyo3::types::PyDict;
3265 let out = PyDict::new(py);
3266 let nuclides: Vec<Py<PyAny>> = lib
3267 .nuclides
3268 .iter()
3269 .map(|n| {
3270 let d = PyDict::new(py);
3271 d.set_item("label", &n.label).ok();
3272 d.set_item("zaid", &n.zaid).ok();
3273 d.set_item("groups", n.groups).ok();
3274 d.set_item("total_xs", n.total_xs.clone()).ok();
3275 d.into_any().unbind()
3276 })
3277 .collect();
3278 out.set_item("nuclides", nuclides).ok();
3279 out.into_any().unbind()
3280}
3281
3282#[pyfunction]
3288#[pyo3(signature = (text, kind="rtflux"))]
3289fn rtflux_parse(py: Python<'_>, text: &str, kind: &str) -> PyResult<Py<PyAny>> {
3290 let flux_kind = match kind.to_ascii_lowercase().as_str() {
3291 "rtflux" => nucleide_cccc_io::rtflux::FluxKind::Rtflux,
3292 "atflux" => nucleide_cccc_io::rtflux::FluxKind::Atflux,
3293 "rzflux" => nucleide_cccc_io::rtflux::FluxKind::Rzflux,
3294 other => {
3295 return Err(PyValueError::new_err(format!(
3296 "kind must be rtflux|atflux|rzflux, got `{other}`"
3297 )))
3298 }
3299 };
3300 let owned = text.to_owned();
3301 let flux = py
3302 .detach(move || nucleide_cccc_io::FluxFile::parse(flux_kind, &owned))
3303 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3304 use pyo3::types::PyDict;
3305 let d = PyDict::new(py);
3306 d.set_item("kind", flux.kind.keyword()).ok();
3307 d.set_item("groups", flux.groups).ok();
3308 d.set_item("per_point", flux.per_point).ok();
3309 d.set_item("npoints", flux.npoints()).ok();
3310 d.set_item("values", flux.values.clone()).ok();
3311 d.set_item("total", flux.total()).ok();
3312 Ok(d.into_any().unbind())
3313}
3314
3315fn partisn_deck_from_dict(
3316 deck: &Bound<'_, pyo3::types::PyDict>,
3317) -> PyResult<nucleide_cccc_io::PartisnDeck> {
3318 let title: String = match deck.get_item("title")? {
3319 Some(v) => v
3320 .extract()
3321 .map_err(|_| PyValueError::new_err("partisn deck `title` must be str"))?,
3322 None => return Err(PyValueError::new_err("partisn deck missing `title`")),
3323 };
3324 let dim: u8 = match deck.get_item("dim")? {
3325 Some(v) => v
3326 .extract()
3327 .map_err(|_| PyValueError::new_err("partisn deck `dim` must be 1, 2, or 3"))?,
3328 None => return Err(PyValueError::new_err("partisn deck missing `dim`")),
3329 };
3330 let zones_value = match deck.get_item("zones")? {
3331 Some(v) => v,
3332 None => return Err(PyValueError::new_err("partisn deck missing `zones`")),
3333 };
3334 let zone_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = zones_value
3335 .extract()
3336 .map_err(|_| PyValueError::new_err("partisn deck `zones` must be a list of dicts"))?;
3337 let mut zones = Vec::with_capacity(zone_dicts.len());
3338 for z in &zone_dicts {
3339 let id: u32 = match z.get_item("id")? {
3340 Some(v) => v
3341 .extract()
3342 .map_err(|_| PyValueError::new_err("partisn zone `id` must be int"))?,
3343 None => return Err(PyValueError::new_err("partisn zone missing `id`")),
3344 };
3345 let material: String = match z.get_item("material")? {
3346 Some(v) => v
3347 .extract()
3348 .map_err(|_| PyValueError::new_err("partisn zone `material` must be str"))?,
3349 None => return Err(PyValueError::new_err("partisn zone missing `material`")),
3350 };
3351 let isotxs_labels: Vec<String> = match z.get_item("isotxs_labels")? {
3352 Some(v) => v.extract().map_err(|_| {
3353 PyValueError::new_err("partisn zone `isotxs_labels` must be a list of str")
3354 })?,
3355 None => {
3356 return Err(PyValueError::new_err(
3357 "partisn zone missing `isotxs_labels`",
3358 ))
3359 }
3360 };
3361 let density: f64 = match z.get_item("density")? {
3362 Some(v) => v
3363 .extract()
3364 .map_err(|_| PyValueError::new_err("partisn zone `density` must be float"))?,
3365 None => return Err(PyValueError::new_err("partisn zone missing `density`")),
3366 };
3367 zones.push(nucleide_cccc_io::partisn::PartisnZone {
3368 id,
3369 material,
3370 isotxs_labels,
3371 density,
3372 });
3373 }
3374 let source: Option<String> = match deck.get_item("source")? {
3375 Some(v) if v.is_none() => None,
3376 Some(v) => Some(
3377 v.extract()
3378 .map_err(|_| PyValueError::new_err("partisn deck `source` must be str or None"))?,
3379 ),
3380 None => None,
3381 };
3382 Ok(nucleide_cccc_io::PartisnDeck {
3383 title,
3384 dim,
3385 zones,
3386 source,
3387 })
3388}
3389
3390#[pyfunction]
3395fn partisn_render(py: Python<'_>, deck: &Bound<'_, pyo3::types::PyDict>) -> PyResult<String> {
3396 let rust_deck = partisn_deck_from_dict(deck)?;
3397 Ok(py.detach(move || rust_deck.render()))
3398}
3399
3400#[pyfunction]
3405fn partisn_validate(
3406 py: Python<'_>,
3407 deck: &Bound<'_, pyo3::types::PyDict>,
3408 isotxs_text: &str,
3409) -> PyResult<()> {
3410 let rust_deck = partisn_deck_from_dict(deck)?;
3411 let owned = isotxs_text.to_owned();
3412 let lib = py
3413 .detach(move || nucleide_cccc_io::IsotxsLib::parse(&owned))
3414 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3415 rust_deck
3416 .validate(&lib)
3417 .map_err(|e| PyValueError::new_err(e.to_string()))
3418}
3419
3420fn fispact_row_to_map(
3425 py: Python<'_>,
3426 r: &nucleide_alara_io::output::ResponseRow,
3427) -> BTreeMap<String, Py<PyAny>> {
3428 let mut d = BTreeMap::new();
3429 d.insert(
3430 "time_s".to_string(),
3431 r.time_s.into_pyobject(py).unwrap().unbind().into_any(),
3432 );
3433 d.insert(
3434 "time_label".to_string(),
3435 r.time_label
3436 .clone()
3437 .into_pyobject(py)
3438 .unwrap()
3439 .unbind()
3440 .into_any(),
3441 );
3442 d.insert(
3443 "nuclide".to_string(),
3444 r.nuclide
3445 .clone()
3446 .into_pyobject(py)
3447 .unwrap()
3448 .unbind()
3449 .into_any(),
3450 );
3451 d.insert(
3452 "half_life_s".to_string(),
3453 r.half_life_s.into_pyobject(py).unwrap().unbind().into_any(),
3454 );
3455 d.insert(
3456 "run_lbl".to_string(),
3457 r.run_lbl
3458 .clone()
3459 .into_pyobject(py)
3460 .unwrap()
3461 .unbind()
3462 .into_any(),
3463 );
3464 d.insert(
3465 "block".to_string(),
3466 r.block
3467 .as_str()
3468 .into_pyobject(py)
3469 .unwrap()
3470 .unbind()
3471 .into_any(),
3472 );
3473 d.insert(
3474 "block_name".to_string(),
3475 r.block_name
3476 .clone()
3477 .into_pyobject(py)
3478 .unwrap()
3479 .unbind()
3480 .into_any(),
3481 );
3482 d.insert(
3483 "block_num".to_string(),
3484 r.block_num.into_pyobject(py).unwrap().unbind().into_any(),
3485 );
3486 d.insert(
3487 "variable".to_string(),
3488 r.variable
3489 .as_str()
3490 .into_pyobject(py)
3491 .unwrap()
3492 .unbind()
3493 .into_any(),
3494 );
3495 d.insert(
3496 "var_unit".to_string(),
3497 r.var_unit
3498 .clone()
3499 .into_pyobject(py)
3500 .unwrap()
3501 .unbind()
3502 .into_any(),
3503 );
3504 d.insert(
3505 "value".to_string(),
3506 r.value.into_pyobject(py).unwrap().unbind().into_any(),
3507 );
3508 d
3509}
3510
3511#[pyfunction]
3517fn fispact_parse_output(
3518 py: Python<'_>,
3519 text: &str,
3520 run_lbl: &str,
3521) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
3522 let owned_text = text.to_owned();
3523 let owned_lbl = run_lbl.to_owned();
3524 let rows = py
3525 .detach(move || {
3526 nucleide_fispact_io::parse_to_frame(&owned_text, &owned_lbl).map(|f| f.rows)
3527 })
3528 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3529 Ok(rows.iter().map(|r| fispact_row_to_map(py, r)).collect())
3530}
3531
3532#[pyfunction]
3542fn origen_parse_tape5(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
3543 let owned = text.to_owned();
3544 let tape = py
3545 .detach(move || nucleide_origen_io::Tape5::parse(&owned))
3546 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3547 use pyo3::types::PyDict;
3548 let out = PyDict::new(py);
3549 out.set_item("titles", tape.titles.clone()).ok();
3550 let steps: Vec<Py<PyAny>> = tape
3551 .irradiation_steps
3552 .iter()
3553 .map(|s| {
3554 let d = PyDict::new(py);
3555 d.set_item("flux", s.flux).ok();
3556 d.set_item("days", s.days).ok();
3557 d.into_any().unbind()
3558 })
3559 .collect();
3560 out.set_item("irradiation_steps", steps).ok();
3561 let materials: Vec<Py<PyAny>> = tape
3562 .materials
3563 .iter()
3564 .map(|m| {
3565 let d = PyDict::new(py);
3566 d.set_item("name", &m.name).ok();
3567 let entries: Vec<Py<PyAny>> = m
3568 .grams
3569 .iter()
3570 .map(|(nuclide, grams)| {
3571 let e = PyDict::new(py);
3572 e.set_item("nuclide", nuclide).ok();
3573 e.set_item("grams", *grams).ok();
3574 e.into_any().unbind()
3575 })
3576 .collect();
3577 d.set_item("entries", entries).ok();
3578 d.into_any().unbind()
3579 })
3580 .collect();
3581 out.set_item("materials", materials).ok();
3582 Ok(out.into_any().unbind())
3583}
3584
3585#[pyfunction]
3590fn origen_parse_tape6(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
3591 let owned = text.to_owned();
3592 let tape = py
3593 .detach(move || nucleide_origen_io::Tape6::parse(&owned))
3594 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3595 use pyo3::types::PyDict;
3596 let out = PyDict::new(py);
3597 let records: Vec<Py<PyAny>> = tape
3598 .records
3599 .iter()
3600 .map(|r| {
3601 let d = PyDict::new(py);
3602 d.set_item("nuclide", &r.nuclide).ok();
3603 d.set_item("grams", r.grams).ok();
3604 d.set_item("activity_bq", r.activity_bq).ok();
3605 d.into_any().unbind()
3606 })
3607 .collect();
3608 out.set_item("records", records).ok();
3609 out.set_item("total_activity", tape.total_activity()).ok();
3610 Ok(out.into_any().unbind())
3611}
3612
3613#[pyfunction]
3617fn origen_parse_tape9(py: Python<'_>, text: &str) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
3618 let owned = text.to_owned();
3619 let entries = py
3620 .detach(move || nucleide_origen_io::Tape9Entry::parse(&owned))
3621 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3622 Ok(entries
3623 .iter()
3624 .map(|e| {
3625 let mut d = BTreeMap::new();
3626 d.insert(
3627 "nuclide".to_string(),
3628 e.nuclide
3629 .clone()
3630 .into_pyobject(py)
3631 .unwrap()
3632 .unbind()
3633 .into_any(),
3634 );
3635 d.insert(
3636 "decay_const".to_string(),
3637 e.decay_const.into_pyobject(py).unwrap().unbind().into_any(),
3638 );
3639 d
3640 })
3641 .collect())
3642}
3643
3644fn r2s_workflow_to_py(py: Python<'_>, workflow: &nucleide_r2s::R2sWorkflow) -> Py<PyAny> {
3649 use pyo3::types::PyDict;
3650 let out = PyDict::new(py);
3651 let steps: Vec<Py<PyAny>> = workflow
3652 .steps
3653 .iter()
3654 .map(|s| {
3655 let d = PyDict::new(py);
3656 d.set_item("zone", &s.zone).ok();
3657 d.set_item("flux", &s.flux).ok();
3658 d.into_any().unbind()
3659 })
3660 .collect();
3661 out.set_item("steps", steps).ok();
3662 out.set_item("cooling_s", workflow.cooling_s.clone()).ok();
3663 out.set_item("top_schedule", &workflow.top_schedule).ok();
3664 out.into_any().unbind()
3665}
3666
3667fn r2s_workflow_from_dict(
3668 workflow: &Bound<'_, pyo3::types::PyDict>,
3669) -> PyResult<nucleide_r2s::R2sWorkflow> {
3670 let steps_value = match workflow.get_item("steps")? {
3671 Some(v) => v,
3672 None => return Err(PyValueError::new_err("r2s workflow missing `steps`")),
3673 };
3674 let step_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = steps_value
3675 .extract()
3676 .map_err(|_| PyValueError::new_err("r2s workflow `steps` must be a list of dicts"))?;
3677 let mut steps = Vec::with_capacity(step_dicts.len());
3678 for s in &step_dicts {
3679 let zone: String = match s.get_item("zone")? {
3680 Some(v) => v
3681 .extract()
3682 .map_err(|_| PyValueError::new_err("r2s step `zone` must be str"))?,
3683 None => return Err(PyValueError::new_err("r2s step missing `zone`")),
3684 };
3685 let flux: String = match s.get_item("flux")? {
3686 Some(v) => v
3687 .extract()
3688 .map_err(|_| PyValueError::new_err("r2s step `flux` must be str"))?,
3689 None => return Err(PyValueError::new_err("r2s step missing `flux`")),
3690 };
3691 steps.push(nucleide_r2s::R2sStep { zone, flux });
3692 }
3693 let cooling_s: Vec<f64> = match workflow.get_item("cooling_s")? {
3694 Some(v) => v.extract().map_err(|_| {
3695 PyValueError::new_err("r2s workflow `cooling_s` must be a list of float")
3696 })?,
3697 None => return Err(PyValueError::new_err("r2s workflow missing `cooling_s`")),
3698 };
3699 let top_schedule: String = match workflow.get_item("top_schedule")? {
3700 Some(v) => v
3701 .extract()
3702 .map_err(|_| PyValueError::new_err("r2s workflow `top_schedule` must be str"))?,
3703 None => return Err(PyValueError::new_err("r2s workflow missing `top_schedule`")),
3704 };
3705 Ok(nucleide_r2s::R2sWorkflow {
3706 steps,
3707 cooling_s,
3708 top_schedule,
3709 })
3710}
3711
3712#[pyfunction]
3717fn r2s_from_deck(py: Python<'_>, deck_text: &str) -> PyResult<Py<PyAny>> {
3718 let owned = deck_text.to_owned();
3719 let workflow = py
3720 .detach(move || {
3721 let deck = nucleide_alara_io::AlaraDeck::parse(&owned)
3722 .map_err(|e| nucleide_r2s::Error::Invalid(e.to_string()))?;
3723 nucleide_r2s::R2sWorkflow::from_deck(&deck)
3724 })
3725 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3726 Ok(r2s_workflow_to_py(py, &workflow))
3727}
3728
3729#[pyfunction]
3734fn r2s_validate(
3735 py: Python<'_>,
3736 workflow: &Bound<'_, pyo3::types::PyDict>,
3737 deck_text: &str,
3738) -> PyResult<()> {
3739 let rust_workflow = r2s_workflow_from_dict(workflow)?;
3740 let owned = deck_text.to_owned();
3741 let deck = py
3742 .detach(move || nucleide_alara_io::AlaraDeck::parse(&owned))
3743 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3744 rust_workflow
3745 .validate_against(&deck)
3746 .map_err(|e| PyValueError::new_err(e.to_string()))
3747}
3748
3749#[pyfunction]
3754#[pyo3(signature = (deck_text, top=None))]
3755fn r2s_expand(
3756 py: Python<'_>,
3757 deck_text: &str,
3758 top: Option<&str>,
3759) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
3760 let owned_text = deck_text.to_owned();
3761 let owned_top = top.map(str::to_owned);
3762 let steps = py
3763 .detach(move || {
3764 let deck = nucleide_alara_io::AlaraDeck::parse(&owned_text)
3765 .map_err(|e| nucleide_r2s::Error::Invalid(e.to_string()))?;
3766 let mut workflow = nucleide_r2s::R2sWorkflow::from_deck(&deck)?;
3767 if let Some(top) = owned_top {
3768 workflow.top_schedule = top;
3769 }
3770 workflow.expand(&deck, &[])
3771 })
3772 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3773 Ok(steps
3774 .into_iter()
3775 .map(|s| {
3776 let mut d = BTreeMap::new();
3777 let cooling = s.is_cooling();
3778 d.insert(
3779 "duration_s".to_string(),
3780 s.duration_s.into_pyobject(py).unwrap().unbind().into_any(),
3781 );
3782 d.insert(
3783 "flux".to_string(),
3784 s.flux.into_pyobject(py).unwrap().unbind().into_any(),
3785 );
3786 d.insert(
3787 "is_cooling".to_string(),
3788 pyo3::types::PyBool::new(py, cooling)
3789 .to_owned()
3790 .into_any()
3791 .unbind(),
3792 );
3793 d
3794 })
3795 .collect())
3796}
3797
3798#[pyfunction]
3809fn r2s_assemble(
3810 py: Python<'_>,
3811 output_text: &str,
3812 run_lbl: &str,
3813 zone: &str,
3814 groups: usize,
3815) -> PyResult<Py<PyAny>> {
3816 let owned_text = output_text.to_owned();
3817 let owned_lbl = run_lbl.to_owned();
3818 let owned_zone = zone.to_owned();
3819 let source = py
3820 .detach(move || {
3821 let frame = nucleide_alara_io::output::ResponseFrame::parse(&owned_text, &owned_lbl)
3822 .map_err(|e| nucleide_r2s::Error::Invalid(e.to_string()))?;
3823 Ok::<_, nucleide_r2s::Error>(nucleide_r2s::photon::assemble(
3824 &frame,
3825 &owned_zone,
3826 groups,
3827 ))
3828 })
3829 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3830 use pyo3::types::PyDict;
3831 let out = PyDict::new(py);
3832 out.set_item("zone", source.zone.clone()).ok();
3833 out.set_item("groups", source.groups.clone()).ok();
3834 out.set_item("total", source.total()).ok();
3835 Ok(out.into_any().unbind())
3836}
3837
3838#[pyfunction]
3847#[pyo3(signature = (totals, zone_of_voxel, split=false))]
3848fn r2s_tag_zone_strength(
3849 py: Python<'_>,
3850 totals: Vec<f64>,
3851 zone_of_voxel: Vec<usize>,
3852 split: bool,
3853) -> PyResult<Py<PyAny>> {
3854 let zones: Vec<nucleide_r2s::photon::ZonePhotonSource> = totals
3855 .into_iter()
3856 .enumerate()
3857 .map(|(i, total)| {
3858 let groups = if total == 0.0 {
3859 Vec::new()
3860 } else {
3861 vec![total]
3862 };
3863 nucleide_r2s::photon::ZonePhotonSource {
3864 zone: format!("zone{i}"),
3865 groups,
3866 }
3867 })
3868 .collect();
3869 let tags = if split {
3870 nucleide_r2s::tags::split_zone_totals(&zones, &zone_of_voxel)
3871 } else {
3872 nucleide_r2s::tags::tag_zone_totals(&zones, &zone_of_voxel)
3873 }
3874 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3875 use pyo3::types::PyDict;
3876 let out = PyDict::new(py);
3877 out.set_item("n_zones", tags.n_zones).ok();
3878 out.set_item("zone_of_voxel", tags.zone_of_voxel.clone())
3879 .ok();
3880 out.set_item("source_strength", tags.source_strength.clone())
3881 .ok();
3882 out.set_item("decay_time_s", tags.decay_time_s.clone()).ok();
3883 out.set_item("total", tags.total_strength()).ok();
3884 Ok(out.into_any().unbind())
3885}
3886
3887#[pyfunction]
3896fn r2s_photon_group_sums(
3897 py: Python<'_>,
3898 photon_text: &str,
3899 nuclides: Vec<String>,
3900 time_s: f64,
3901) -> PyResult<Py<PyAny>> {
3902 let source = nucleide_alara_io::photon::PhotonSource::from_str(photon_text)
3903 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3904 let names: Vec<&str> = nuclides.iter().map(String::as_str).collect();
3905 let at = nucleide_r2s::tags::photon_groups_at(&source, &names, time_s);
3906 let sums = nucleide_r2s::tags::sum_group_strengths(&at)
3907 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3908 use pyo3::types::PyDict;
3909 let out = PyDict::new(py);
3910 let rows: Vec<Py<PyAny>> = at
3911 .iter()
3912 .map(|g| {
3913 let d = PyDict::new(py);
3914 d.set_item("nuclide", g.nuclide.clone()).ok();
3915 d.set_item("time_s", g.time_s).ok();
3916 d.set_item("strengths", g.strengths.clone()).ok();
3917 d.into_any().unbind()
3918 })
3919 .collect();
3920 out.set_item("groups", rows).ok();
3921 out.set_item("sums", sums.clone()).ok();
3922 out.set_item("total", sums.iter().sum::<f64>()).ok();
3923 Ok(out.into_any().unbind())
3924}
3925
3926fn snapshot_dict_str(
3927 zone: &Bound<'_, pyo3::types::PyDict>,
3928 key: &str,
3929 what: &str,
3930) -> PyResult<String> {
3931 match zone.get_item(key)? {
3932 Some(v) => v
3933 .extract()
3934 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be str"))),
3935 None => Err(PyValueError::new_err(format!(
3936 "snapshot {what} missing `{key}`"
3937 ))),
3938 }
3939}
3940
3941fn snapshot_dict_opt_str(
3942 zone: &Bound<'_, pyo3::types::PyDict>,
3943 key: &str,
3944 what: &str,
3945) -> PyResult<Option<String>> {
3946 match zone.get_item(key)? {
3947 Some(v) if v.is_none() => Ok(None),
3948 Some(v) => v
3949 .extract::<String>()
3950 .map(Some)
3951 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be str"))),
3952 None => Ok(None),
3953 }
3954}
3955
3956fn snapshot_dict_f64(
3957 zone: &Bound<'_, pyo3::types::PyDict>,
3958 key: &str,
3959 what: &str,
3960) -> PyResult<f64> {
3961 match zone.get_item(key)? {
3962 Some(v) => v
3963 .extract()
3964 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be float"))),
3965 None => Err(PyValueError::new_err(format!(
3966 "snapshot {what} missing `{key}`"
3967 ))),
3968 }
3969}
3970
3971fn snapshot_dict_opt_f64(
3972 zone: &Bound<'_, pyo3::types::PyDict>,
3973 key: &str,
3974 what: &str,
3975) -> PyResult<Option<f64>> {
3976 match zone.get_item(key)? {
3977 Some(v) if v.is_none() => Ok(None),
3978 Some(v) => v
3979 .extract::<f64>()
3980 .map(Some)
3981 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be float"))),
3982 None => Ok(None),
3983 }
3984}
3985
3986fn snapshot_zone_from_dict(
3987 zone: &Bound<'_, pyo3::types::PyDict>,
3988) -> PyResult<nucleide_r2s::snapshot::SnapshotZone> {
3989 let id = snapshot_dict_str(zone, "id", "zone")?;
3990 let volume_cm3 = snapshot_dict_f64(zone, "volume_cm3", "zone")?;
3991 let composition: BTreeMap<String, f64> = match zone.get_item("composition")? {
3992 Some(v) => v.extract().map_err(|_| {
3993 PyValueError::new_err("snapshot zone `composition` must be a dict of str to float")
3994 })?,
3995 None => return Err(PyValueError::new_err("snapshot zone missing `composition`")),
3996 };
3997 Ok(nucleide_r2s::snapshot::SnapshotZone {
3998 zone: id,
3999 volume_cm3,
4000 zbottom_cm: snapshot_dict_opt_f64(zone, "zbottom_cm", "zone")?,
4001 ztop_cm: snapshot_dict_opt_f64(zone, "ztop_cm", "zone")?,
4002 material: snapshot_dict_opt_str(zone, "material", "zone")?,
4003 xs_type: snapshot_dict_opt_str(zone, "xs_type", "zone")?,
4004 temperature_c: snapshot_dict_opt_f64(zone, "temperature_C", "zone")?,
4005 composition: composition.into_iter().collect(),
4006 flux_name: snapshot_dict_opt_str(zone, "flux", "zone")?,
4007 })
4008}
4009
4010fn snapshot_input_from_dict(
4011 snapshot: &Bound<'_, pyo3::types::PyDict>,
4012) -> PyResult<nucleide_r2s::snapshot::SnapshotInput> {
4013 let zone_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = match snapshot.get_item("zones")? {
4014 Some(v) => v
4015 .extract()
4016 .map_err(|_| PyValueError::new_err("snapshot `zones` must be a list of dicts"))?,
4017 None => return Err(PyValueError::new_err("snapshot missing `zones`")),
4018 };
4019 let mut zones = Vec::with_capacity(zone_dicts.len());
4020 for z in &zone_dicts {
4021 zones.push(snapshot_zone_from_dict(z)?);
4022 }
4023 let flux_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = match snapshot.get_item("flux_defs")? {
4024 Some(v) => v
4025 .extract()
4026 .map_err(|_| PyValueError::new_err("snapshot `flux_defs` must be a list of dicts"))?,
4027 None => return Err(PyValueError::new_err("snapshot missing `flux_defs`")),
4028 };
4029 let mut flux_defs = Vec::with_capacity(flux_dicts.len());
4030 for f in &flux_dicts {
4031 flux_defs.push(nucleide_r2s::snapshot::SnapshotFluxDef {
4032 name: snapshot_dict_str(f, "name", "flux")?,
4033 file: snapshot_dict_str(f, "file", "flux")?,
4034 scale: snapshot_dict_f64(f, "scale", "flux")?,
4035 });
4036 }
4037 let cooling_s: Vec<f64> = match snapshot.get_item("cooling_s")? {
4038 Some(v) => v
4039 .extract()
4040 .map_err(|_| PyValueError::new_err("snapshot `cooling_s` must be a list of float"))?,
4041 None => return Err(PyValueError::new_err("snapshot missing `cooling_s`")),
4042 };
4043 Ok(nucleide_r2s::snapshot::SnapshotInput {
4044 zones,
4045 flux_defs,
4046 cooling_s,
4047 schedule_text: snapshot_dict_opt_str(snapshot, "schedule_text", "snapshot")?,
4048 output: snapshot_dict_opt_str(snapshot, "output", "snapshot")?,
4049 })
4050}
4051
4052#[pyfunction]
4058fn r2s_snapshot_inventory(
4059 snapshot: &Bound<'_, pyo3::types::PyDict>,
4060) -> PyResult<BTreeMap<String, f64>> {
4061 let input = snapshot_input_from_dict(snapshot)?;
4062 nucleide_r2s::snapshot::snapshot_inventory(&input)
4063 .map(|totals| totals.into_iter().collect())
4064 .map_err(|e| PyValueError::new_err(e.to_string()))
4065}
4066
4067#[pyfunction]
4073fn r2s_expand_sweep(
4074 axes: Vec<BTreeMap<String, Bound<'_, pyo3::types::PyAny>>>,
4075) -> PyResult<Vec<BTreeMap<String, String>>> {
4076 use pyo3::types::PyAnyMethods;
4077 let mut parsed = Vec::with_capacity(axes.len());
4078 for axis in &axes {
4079 let name: String = axis
4080 .get("name")
4081 .and_then(|v| v.extract().ok())
4082 .ok_or_else(|| PyValueError::new_err("sweep axis needs a `name` string"))?;
4083 let values: Vec<f64> = axis
4084 .get("values")
4085 .and_then(|v| v.extract().ok())
4086 .ok_or_else(|| PyValueError::new_err("sweep axis needs a `values` float list"))?;
4087 parsed.push(
4088 nucleide_r2s::sweep::SweepAxis::new(&name, values)
4089 .map_err(|e| PyValueError::new_err(e.to_string()))?,
4090 );
4091 }
4092 let cases = nucleide_r2s::sweep::expand_sweep(&parsed)
4093 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4094 Ok(cases
4095 .into_iter()
4096 .map(|c| {
4097 let mut d = BTreeMap::new();
4098 d.insert("name".to_string(), c.name);
4099 d.insert(
4100 "params".to_string(),
4101 c.params
4102 .iter()
4103 .map(|(k, v)| format!("{k}={v}"))
4104 .collect::<Vec<_>>()
4105 .join(","),
4106 );
4107 d
4108 })
4109 .collect())
4110}
4111#[pyfunction]
4128fn r2s_from_snapshot(
4129 py: Python<'_>,
4130 snapshot: &Bound<'_, pyo3::types::PyDict>,
4131) -> PyResult<Py<PyAny>> {
4132 let input = snapshot_input_from_dict(snapshot)?;
4133 let (workflow, template, decks) = py
4134 .detach(move || nucleide_r2s::snapshot::snapshot_workflow(&input))
4135 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4136 use pyo3::types::PyDict;
4137 let out = PyDict::new(py);
4138 out.set_item("workflow", r2s_workflow_to_py(py, &workflow))
4139 .ok();
4140 out.set_item("deck", template.to_string()).ok();
4141 let deck_texts: Vec<String> = decks.iter().map(ToString::to_string).collect();
4142 out.set_item("decks", deck_texts).ok();
4143 Ok(out.into_any().unbind())
4144}
4145
4146fn parse_integrator(name: &str) -> PyResult<nucleide_depletion::Integrator> {
4164 use nucleide_depletion::Integrator as I;
4165 if name.eq_ignore_ascii_case("predictor") {
4166 return Ok(I::Predictor);
4167 }
4168 if name.eq_ignore_ascii_case("cecm") {
4169 return Ok(I::Cecm);
4170 }
4171 if name.eq_ignore_ascii_case("cf4") {
4172 return Ok(I::Cf4);
4173 }
4174 Err(PyValueError::new_err(format!(
4175 "unsupported integrator `{name}` (supported: predictor, cecm, cf4)"
4176 )))
4177}
4178
4179#[pyfunction]
4192#[pyo3(signature = (chain, n0, dts, rates=None, rates_list=None, integrator="predictor", order=48, method="cram48"))]
4193#[allow(clippy::too_many_arguments)]
4194fn deplete_series(
4195 chain: &PyChain,
4196 n0: BTreeMap<String, f64>,
4197 dts: Vec<f64>,
4198 rates: Option<RateMap>,
4199 rates_list: Option<Vec<Option<RateMap>>>,
4200 integrator: &str,
4201 order: u8,
4202 method: &str,
4203) -> PyResult<Py<PyAny>> {
4204 use nucleide_depletion::{DepletionSystem, ReactionRates, Step};
4205 let integrator = parse_integrator(integrator)?;
4206 let method = resolve_method(order, method)?;
4207 if let Some(list) = &rates_list {
4208 if list.len() != dts.len() {
4209 return Err(PyValueError::new_err(format!(
4210 "rates_list has {} entries but dts has {}",
4211 list.len(),
4212 dts.len()
4213 )));
4214 }
4215 }
4216 if dts.is_empty() {
4217 return Err(PyValueError::new_err("dts must not be empty"));
4218 }
4219 let mut n0_vec = vec![0.0; chain.inner.len()];
4221 for (name, value) in &n0 {
4222 let idx = chain.inner.index_of(name).ok_or_else(|| {
4223 PyValueError::new_err(format!("unknown nuclide `{name}` for this chain"))
4224 })?;
4225 n0_vec[idx] = *value;
4226 }
4227 let empty = BTreeMap::new();
4228 let mut steps = Vec::with_capacity(dts.len());
4229 for (i, dt) in dts.iter().enumerate() {
4230 let step_rates = rates_list
4231 .as_ref()
4232 .and_then(|list| list[i].as_ref())
4233 .or(rates.as_ref())
4234 .unwrap_or(&empty);
4235 let rs = split_rates(step_rates, &chain.inner)?;
4236 steps.push(Step::new(*dt, rs));
4237 }
4238 let template = DepletionSystem::build((*chain.inner).clone(), &ReactionRates::new())
4241 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4242 let series =
4245 nucleide_depletion::integrate_with_method(&template, &n0_vec, &steps, integrator, method)
4246 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4247 let names: Vec<&str> = template
4248 .chain
4249 .nuclides
4250 .iter()
4251 .map(|nuc| nuc.name.as_str())
4252 .collect();
4253 let keyed = |rows: &[Vec<f64>]| -> Vec<BTreeMap<String, f64>> {
4254 rows.iter()
4255 .map(|row| {
4256 names
4257 .iter()
4258 .zip(row)
4259 .map(|(name, v)| ((*name).to_string(), *v))
4260 .collect()
4261 })
4262 .collect()
4263 };
4264 let atoms = keyed(&series.atoms[1..]);
4266 let activity = keyed(&series.activity[1..]);
4267 let decay_heat = keyed(&series.decay_heat[1..]);
4268 let times = series.times[1..].to_vec();
4269 Ok(Python::attach(|py| {
4270 use pyo3::types::PyDict;
4271 let out = PyDict::new(py);
4272 out.set_item("times", ×).ok();
4273 out.set_item("atoms", &atoms).ok();
4274 out.set_item("activity", &activity).ok();
4275 out.set_item("decay_heat", &decay_heat).ok();
4276 out.into_any().unbind()
4277 }))
4278}
4279
4280#[pyfunction]
4285fn simple_xs(name: &str) -> PyResult<Option<(f64, f64)>> {
4286 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4287 Ok(nucleide_nuclei::data::simple_xs_by_name(name))
4288}
4289
4290#[pyfunction]
4295fn scattering_length(name: &str) -> PyResult<Option<f64>> {
4296 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4297 Ok(nucleide_nuclei::data::scattering_length_by_name(name).map(|(b_coh, _)| b_coh))
4298}
4299
4300#[pyfunction]
4304fn decay_energy(name: &str) -> PyResult<Option<f64>> {
4305 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4306 Ok(nucleide_nuclei::data::decay_energy_mev_by_name(name))
4307}
4308
4309#[pyfunction]
4315fn decay_branches(name: &str) -> PyResult<Vec<(String, f64, String)>> {
4316 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4317 Ok(nucleide_nuclei::data::decay_branches_by_name(name)
4318 .unwrap_or_default()
4319 .into_iter()
4320 .map(|b| {
4321 (
4322 nucleide_nuclei::NuclideId::from_nucid(b.progeny).to_name(),
4323 b.branching_fraction,
4324 b.mode.as_str().to_string(),
4325 )
4326 })
4327 .collect())
4328}
4329
4330#[pyfunction]
4336fn decay_branch_fraction(parent: &str, progeny: &str) -> PyResult<Option<f64>> {
4337 NuclideId::from_name(parent).map_err(wrap_nucid_err)?;
4338 NuclideId::from_name(progeny).map_err(wrap_nucid_err)?;
4339 Ok(nucleide_nuclei::data::branching_fraction_by_name(
4340 parent, progeny,
4341 ))
4342}
4343
4344type PyFissionYieldSets = Vec<(f64, Vec<(String, f64, f64)>)>;
4354
4355#[pyfunction]
4356#[pyo3(signature = (parent, origin="n", kind="independent"))]
4357fn fission_yields(parent: &str, origin: &str, kind: &str) -> PyResult<PyFissionYieldSets> {
4358 NuclideId::from_name(parent).map_err(wrap_nucid_err)?;
4359 let origin = nucleide_nuclei::data::FissionYieldOrigin::parse(origin).ok_or_else(|| {
4360 PyValueError::new_err(format!(
4361 "unknown fission-yield origin `{origin}` (expected `n` or `sf`)"
4362 ))
4363 })?;
4364 let kind = nucleide_nuclei::data::FissionYieldKind::parse(kind).ok_or_else(|| {
4365 PyValueError::new_err(format!(
4366 "unknown fission-yield kind `{kind}` (expected `independent` or `cumulative`)"
4367 ))
4368 })?;
4369 Ok(
4370 nucleide_nuclei::data::fission_yields_by_name(parent, origin, kind)
4371 .unwrap_or_default()
4372 .into_iter()
4373 .map(|set| {
4374 (
4375 set.energy_ev,
4376 set.products
4377 .into_iter()
4378 .map(|p| {
4379 (
4380 nucleide_nuclei::NuclideId::from_nucid(p.progeny).to_name(),
4381 p.yield_fraction,
4382 p.uncertainty,
4383 )
4384 })
4385 .collect(),
4386 )
4387 })
4388 .collect(),
4389 )
4390}
4391
4392#[pyfunction]
4399fn fission_yield(parent: &str, progeny: &str) -> PyResult<Option<f64>> {
4400 NuclideId::from_name(parent).map_err(wrap_nucid_err)?;
4401 NuclideId::from_name(progeny).map_err(wrap_nucid_err)?;
4402 Ok(nucleide_nuclei::data::fission_yield_by_name(
4403 parent, progeny,
4404 ))
4405}
4406
4407#[pyfunction]
4413fn normalize_nuclide(name: &str) -> PyResult<String> {
4414 Ok(nucleide_nuclei::dialects::normalize_nuclide_name(name)
4415 .map_err(|e| PyValueError::new_err(e.to_string()))?
4416 .to_name())
4417}
4418
4419#[pyfunction]
4426fn decay_heat(comp: BTreeMap<String, f64>) -> PyResult<f64> {
4427 let mat = comp_to_material(comp)?;
4428 let analytics = nucleide_material::Analytics {
4429 masses: &nucleide_material::Ame2020,
4430 decays: &nucleide_material::ChainDecays,
4431 };
4432 mat.total_decay_heat(&analytics, &nucleide_material::DecayEnergies)
4433 .map_err(|e| PyValueError::new_err(e.to_string()))
4434}
4435
4436fn parse_dose_pathway(s: &str) -> PyResult<nucleide_material::DosePathway> {
4437 nucleide_material::DosePathway::parse(s).ok_or_else(|| {
4438 PyValueError::new_err(format!(
4439 "unknown dose pathway `{s}` (supported: air, soil, ingest, inhale)"
4440 ))
4441 })
4442}
4443
4444fn parse_dose_source(s: &str) -> PyResult<nucleide_material::DoseSource> {
4445 nucleide_material::DoseSource::parse(s).ok_or_else(|| {
4446 PyValueError::new_err(format!(
4447 "unknown dose source `{s}` (supported: EPA, DOE, GENII)"
4448 ))
4449 })
4450}
4451
4452#[pyfunction]
4459#[pyo3(signature = (name, pathway, source="EPA"))]
4460fn dose_factor(name: &str, pathway: &str, source: &str) -> PyResult<Option<f64>> {
4461 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4462 let p = parse_dose_pathway(pathway)?;
4463 let s = parse_dose_source(source)?;
4464 Ok(nucleide_nuclei::data::dose_factor_by_name(name, p, s))
4465}
4466
4467fn wrap_fgr15_err(e: nucleide_nuclei::fgr15::Error) -> PyErr {
4468 PyValueError::new_err(e.to_string())
4469}
4470
4471#[pyfunction]
4482#[pyo3(signature = (text, expected_rows))]
4483fn parse_fgr15_table<'py>(
4484 py: Python<'py>,
4485 text: &str,
4486 expected_rows: usize,
4487) -> PyResult<pyo3::Bound<'py, pyo3::types::PyDict>> {
4488 let table = nucleide_nuclei::fgr15::parse_table(text, expected_rows).map_err(wrap_fgr15_err)?;
4489 let out = pyo3::types::PyDict::new(py);
4490 out.set_item("scenario", table.scenario().as_str())?;
4491 out.set_item("units", table.units())?;
4492 let coefficients = pyo3::types::PyDict::new(py);
4493 for (nucid, row) in table.iter() {
4494 coefficients.set_item(
4495 nucleide_nuclei::fgr15::name_of(NuclideId::from_nucid(nucid)),
4496 row.to_vec(),
4497 )?;
4498 }
4499 out.set_item("coefficients", coefficients)?;
4500 Ok(out)
4501}
4502
4503#[pyfunction]
4509fn fgr15_age_index(age: &str) -> PyResult<usize> {
4510 nucleide_nuclei::fgr15::Fgr15Age::parse(age)
4511 .map(|a| a.index())
4512 .ok_or_else(|| {
4513 PyValueError::new_err(format!(
4514 "unknown FGR 15 age group `{age}` (supported: newborn, 1, 5, 10, 15, adult)"
4515 ))
4516 })
4517}
4518
4519#[pyfunction]
4531#[pyo3(signature = (comp, pathway, source="EPA"))]
4532fn dose_per_g(comp: BTreeMap<String, f64>, pathway: &str, source: &str) -> PyResult<f64> {
4533 let mat = comp_to_material(comp)?;
4534 let analytics = nucleide_material::Analytics {
4535 masses: &nucleide_material::Ame2020,
4536 decays: &nucleide_material::ChainDecays,
4537 };
4538 let p = parse_dose_pathway(pathway)?;
4539 let s = parse_dose_source(source)?;
4540 mat.total_dose_per_g(&analytics, &nucleide_material::DoseFactors, p, s)
4541 .map_err(|e| PyValueError::new_err(e.to_string()))
4542}
4543
4544#[pyfunction]
4552#[allow(clippy::type_complexity)]
4553fn separate_material(
4554 comp: BTreeMap<String, f64>,
4555 effs: BTreeMap<String, f64>,
4556) -> PyResult<(BTreeMap<String, f64>, BTreeMap<String, f64>)> {
4557 let mat = comp_to_material(comp)?;
4558 let mut table = Vec::with_capacity(effs.len());
4559 for (name, eff) in &effs {
4560 let id = NuclideId::from_name(name)
4561 .map_err(|e| PyValueError::new_err(format!("`{name}`: {e}")))?;
4562 table.push((id, *eff));
4563 }
4564 let (product, tails) = mat
4565 .separate(&table)
4566 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4567 let named =
4568 |m: nucleide_material::Material| m.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect();
4569 Ok((named(product), named(tails)))
4570}
4571
4572#[pyfunction]
4579fn blend_material(parts: Vec<(BTreeMap<String, f64>, f64)>) -> PyResult<BTreeMap<String, f64>> {
4580 let mats: Vec<nucleide_material::Material> = parts
4581 .iter()
4582 .map(|(comp, _)| comp_to_material(comp.clone()))
4583 .collect::<PyResult<_>>()?;
4584 let refs: Vec<(&nucleide_material::Material, f64)> =
4585 mats.iter().zip(parts.iter().map(|(_, r)| *r)).collect();
4586 let out = nucleide_material::Material::blend(&refs)
4587 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4588 Ok(out.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect())
4589}
4590
4591#[pyclass(name = "Cusum")]
4599struct PyCusum {
4600 inner: nucleide_material::Cusum,
4601}
4602
4603#[pymethods]
4604impl PyCusum {
4605 #[new]
4608 #[pyo3(signature = (ref_shift_k=0.5, alarm_h=4.0, startup=10))]
4609 fn new(ref_shift_k: f64, alarm_h: f64, startup: usize) -> PyResult<Self> {
4610 nucleide_material::Cusum::new(ref_shift_k, alarm_h, startup)
4611 .map(|inner| Self { inner })
4612 .map_err(|e| PyValueError::new_err(e.to_string()))
4613 }
4614
4615 fn update(&mut self, x: f64) -> bool {
4617 self.inner.update(x)
4618 }
4619
4620 fn status(&self) -> bool {
4622 self.inner.status()
4623 }
4624
4625 fn statistic(&self) -> f64 {
4627 self.inner.statistic()
4628 }
4629
4630 fn count(&self) -> usize {
4632 self.inner.count()
4633 }
4634
4635 fn mean(&self) -> f64 {
4637 self.inner.mean()
4638 }
4639
4640 fn variance(&self) -> f64 {
4642 self.inner.variance()
4643 }
4644
4645 fn std(&self) -> f64 {
4647 self.inner.std()
4648 }
4649
4650 fn reset(&mut self) {
4652 self.inner.reset();
4653 }
4654}
4655
4656#[pyclass(name = "DeckProblem")]
4662struct PyDeckProblem {
4663 inner: std::sync::Mutex<nucleide_mcnp_io::problem::DeckProblem>,
4664}
4665
4666fn deck_cell_dict(cell: &nucleide_mcnp_io::cell::CellCard) -> BTreeMap<String, String> {
4667 let mut d = BTreeMap::new();
4668 d.insert("num".to_string(), cell.num.to_string());
4669 d.insert("mat".to_string(), cell.mat.to_string());
4670 d.insert(
4671 "dens".to_string(),
4672 cell.dens.map(|v| v.to_string()).unwrap_or_default(),
4673 );
4674 d.insert("geom".to_string(), cell.geom.render());
4675 d.insert("params".to_string(), cell.params.join(" "));
4676 d
4677}
4678
4679#[pymethods]
4680impl PyDeckProblem {
4681 #[staticmethod]
4683 fn loads(text: &str) -> PyResult<Self> {
4684 nucleide_mcnp_io::problem::parse_deck(text)
4685 .map(|inner| Self {
4686 inner: std::sync::Mutex::new(inner),
4687 })
4688 .map_err(|e| PyValueError::new_err(e.to_string()))
4689 }
4690
4691 #[getter]
4693 fn message(&self) -> PyResult<String> {
4694 Ok(self
4695 .inner
4696 .lock()
4697 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4698 .message
4699 .clone())
4700 }
4701
4702 #[getter]
4704 fn title(&self) -> PyResult<String> {
4705 Ok(self
4706 .inner
4707 .lock()
4708 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4709 .title
4710 .clone())
4711 }
4712
4713 #[getter]
4716 fn cells(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4717 Ok(self
4718 .inner
4719 .lock()
4720 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4721 .cells
4722 .iter()
4723 .map(deck_cell_dict)
4724 .collect())
4725 }
4726
4727 #[getter]
4730 fn surfs(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4731 Ok(self
4732 .inner
4733 .lock()
4734 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4735 .surfs
4736 .iter()
4737 .map(|s| {
4738 let mut d = BTreeMap::new();
4739 d.insert("num".to_string(), s.num.to_string());
4740 d.insert("reflecting".to_string(), s.reflecting.to_string());
4741 d.insert(
4742 "transform".to_string(),
4743 s.transform.map(|v| v.to_string()).unwrap_or_default(),
4744 );
4745 d.insert(
4746 "periodic".to_string(),
4747 s.periodic.map(|v| v.to_string()).unwrap_or_default(),
4748 );
4749 d.insert("kind".to_string(), s.kind.keyword().to_string());
4750 d.insert(
4751 "coeffs".to_string(),
4752 s.coeffs
4753 .iter()
4754 .map(|v| v.to_string())
4755 .collect::<Vec<_>>()
4756 .join(" "),
4757 );
4758 d
4759 })
4760 .collect())
4761 }
4762
4763 #[getter]
4765 fn material_numbers(&self) -> PyResult<Vec<u32>> {
4766 Ok(self
4767 .inner
4768 .lock()
4769 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4770 .materials
4771 .iter()
4772 .map(|m| m.number)
4773 .collect())
4774 }
4775
4776 #[getter]
4778 fn data_names(&self) -> PyResult<Vec<String>> {
4779 Ok(self
4780 .inner
4781 .lock()
4782 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4783 .data
4784 .iter()
4785 .map(|d| d.name.clone())
4786 .collect())
4787 }
4788
4789 fn dumps(&self) -> PyResult<String> {
4791 let guard = self
4792 .inner
4793 .lock()
4794 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?;
4795 Ok(nucleide_mcnp_io::problem::write_deck(&guard))
4796 }
4797
4798 fn set_cell_density(&self, cell: u32, dens: f64) -> PyResult<()> {
4800 self.inner
4801 .lock()
4802 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4803 .set_cell_density(cell, dens)
4804 .map_err(|e| PyValueError::new_err(e.to_string()))
4805 }
4806
4807 fn set_cell_material(&self, cell: u32, mat: u32) -> PyResult<()> {
4809 self.inner
4810 .lock()
4811 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4812 .set_cell_material(cell, mat)
4813 .map_err(|e| PyValueError::new_err(e.to_string()))
4814 }
4815
4816 #[getter]
4818 fn mode(&self) -> PyResult<BTreeMap<String, String>> {
4819 let mode = self
4820 .inner
4821 .lock()
4822 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4823 .mode()
4824 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4825 let mut d = BTreeMap::new();
4826 d.insert("particles".to_string(), mode.particles.join(" "));
4827 Ok(d)
4828 }
4829
4830 #[getter]
4833 fn transforms(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4834 let transforms = self
4835 .inner
4836 .lock()
4837 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4838 .transforms()
4839 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4840 Ok(transforms
4841 .iter()
4842 .map(|t| {
4843 let mut d = BTreeMap::new();
4844 d.insert("number".to_string(), t.number.to_string());
4845 d.insert(
4846 "displacement".to_string(),
4847 t.displacement
4848 .iter()
4849 .map(|v| v.to_string())
4850 .collect::<Vec<_>>()
4851 .join(" "),
4852 );
4853 d.insert(
4854 "rotation".to_string(),
4855 t.rotation
4856 .iter()
4857 .map(|v| v.to_string())
4858 .collect::<Vec<_>>()
4859 .join(" "),
4860 );
4861 d.insert("in_degrees".to_string(), t.is_in_degrees.to_string());
4862 d.insert("main_to_aux".to_string(), t.is_main_to_aux.to_string());
4863 d.insert("hidden".to_string(), t.hidden.to_string());
4864 d
4865 })
4866 .collect())
4867 }
4868
4869 #[getter]
4872 fn universes(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4873 let universes = self
4874 .inner
4875 .lock()
4876 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4877 .universes()
4878 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4879 Ok(universes
4880 .iter()
4881 .map(|u| {
4882 let mut d = BTreeMap::new();
4883 d.insert("number".to_string(), u.number.to_string());
4884 d.insert(
4885 "cells".to_string(),
4886 u.cells
4887 .iter()
4888 .map(|v| v.to_string())
4889 .collect::<Vec<_>>()
4890 .join(" "),
4891 );
4892 d.insert(
4893 "not_truncated".to_string(),
4894 u.not_truncated
4895 .iter()
4896 .map(|v| v.to_string())
4897 .collect::<Vec<_>>()
4898 .join(" "),
4899 );
4900 d
4901 })
4902 .collect())
4903 }
4904
4905 #[getter]
4907 fn lattices(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4908 let lattices = self
4909 .inner
4910 .lock()
4911 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4912 .lattices()
4913 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4914 Ok(lattices
4915 .iter()
4916 .map(|l| {
4917 let mut d = BTreeMap::new();
4918 d.insert("cell".to_string(), l.cell.to_string());
4919 d.insert("lattice".to_string(), l.lattice.to_string());
4920 d
4921 })
4922 .collect())
4923 }
4924
4925 #[getter]
4929 fn fills(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4930 use nucleide_mcnp_io::semantic::{FillTarget, FillTransform};
4931 let fills = self
4932 .inner
4933 .lock()
4934 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4935 .fills()
4936 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4937 Ok(fills
4938 .iter()
4939 .map(|f| {
4940 let mut d = BTreeMap::new();
4941 d.insert("cell".to_string(), f.cell.to_string());
4942 match &f.target {
4943 FillTarget::Single(u) => {
4944 d.insert("kind".to_string(), "single".to_string());
4945 d.insert("universe".to_string(), u.to_string());
4946 d.insert("min_index".to_string(), String::new());
4947 d.insert("max_index".to_string(), String::new());
4948 d.insert("universes".to_string(), String::new());
4949 }
4950 FillTarget::Matrix {
4951 min_index,
4952 max_index,
4953 universes,
4954 } => {
4955 d.insert("kind".to_string(), "matrix".to_string());
4956 d.insert("universe".to_string(), String::new());
4957 d.insert(
4958 "min_index".to_string(),
4959 min_index
4960 .iter()
4961 .map(|v| v.to_string())
4962 .collect::<Vec<_>>()
4963 .join(" "),
4964 );
4965 d.insert(
4966 "max_index".to_string(),
4967 max_index
4968 .iter()
4969 .map(|v| v.to_string())
4970 .collect::<Vec<_>>()
4971 .join(" "),
4972 );
4973 d.insert(
4974 "universes".to_string(),
4975 universes
4976 .iter()
4977 .map(|u| {
4978 u.map(|v| v.to_string()).unwrap_or_else(|| "-".to_string())
4979 })
4980 .collect::<Vec<_>>()
4981 .join(" "),
4982 );
4983 }
4984 }
4985 match &f.transform {
4986 None => {
4987 d.insert("transform".to_string(), String::new());
4988 d.insert("hidden_transform".to_string(), String::new());
4989 }
4990 Some(FillTransform::Reference(n)) => {
4991 d.insert("transform".to_string(), n.to_string());
4992 d.insert("hidden_transform".to_string(), String::new());
4993 }
4994 Some(FillTransform::Hidden(t)) => {
4995 d.insert("transform".to_string(), String::new());
4996 let mut coords: Vec<String> =
4997 t.displacement.iter().map(|v| v.to_string()).collect();
4998 coords.extend(t.rotation.iter().map(|v| v.to_string()));
4999 d.insert("hidden_transform".to_string(), coords.join(" "));
5000 }
5001 }
5002 d.insert("in_degrees".to_string(), f.in_degrees.to_string());
5003 d
5004 })
5005 .collect())
5006 }
5007
5008 #[getter]
5010 fn importances(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5011 let importances = self
5012 .inner
5013 .lock()
5014 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5015 .importances()
5016 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5017 Ok(importances
5018 .iter()
5019 .map(|v| {
5020 let mut d = BTreeMap::new();
5021 d.insert("cell".to_string(), v.cell.to_string());
5022 d.insert("particle".to_string(), v.particle.clone());
5023 d.insert("value".to_string(), v.value.to_string());
5024 d
5025 })
5026 .collect())
5027 }
5028
5029 #[getter]
5031 fn volumes(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5032 let volumes = self
5033 .inner
5034 .lock()
5035 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5036 .volumes()
5037 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5038 Ok(volumes
5039 .iter()
5040 .map(|v| {
5041 let mut d = BTreeMap::new();
5042 d.insert("cell".to_string(), v.cell.to_string());
5043 d.insert("volume".to_string(), v.volume.to_string());
5044 d
5045 })
5046 .collect())
5047 }
5048
5049 #[getter]
5052 fn tallies(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5053 let tallies = self
5054 .inner
5055 .lock()
5056 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5057 .tallies()
5058 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5059 Ok(tallies
5060 .iter()
5061 .map(|t| {
5062 let mut d = BTreeMap::new();
5063 d.insert("number".to_string(), t.number.to_string());
5064 d.insert("type".to_string(), t.tally_type.to_string());
5065 d.insert("particles".to_string(), t.particles.join(","));
5066 d.insert("entries".to_string(), t.entries.join(" "));
5067 d.insert("fm".to_string(), t.fm.clone().unwrap_or_default().join(" "));
5068 d.insert(
5069 "e_bins".to_string(),
5070 t.e_bins.clone().unwrap_or_default().join(" "),
5071 );
5072 d
5073 })
5074 .collect())
5075 }
5076
5077 #[getter]
5080 fn sdef(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
5081 let guard = self
5082 .inner
5083 .lock()
5084 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?;
5085 let sdef = guard
5086 .sdef()
5087 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5088 sdef.map(|s| sdef_to_py(py, &s)).transpose()
5089 }
5090
5091 fn validate(&self) -> PyResult<()> {
5094 self.inner
5095 .lock()
5096 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5097 .validate()
5098 .map_err(|e| PyValueError::new_err(e.to_string()))
5099 }
5100
5101 fn validation_notes(&self) -> PyResult<Vec<String>> {
5103 Ok(self
5104 .inner
5105 .lock()
5106 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5107 .validation_notes())
5108 }
5109
5110 fn set_mode(&self, particles: Vec<String>) -> PyResult<()> {
5112 self.inner
5113 .lock()
5114 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5115 .set_mode(particles)
5116 .map_err(|e| PyValueError::new_err(e.to_string()))
5117 }
5118
5119 fn set_cell_universe(&self, cell: u32, universe: u32, not_truncated: bool) -> PyResult<()> {
5121 self.inner
5122 .lock()
5123 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5124 .set_cell_universe(cell, universe, not_truncated)
5125 .map_err(|e| PyValueError::new_err(e.to_string()))
5126 }
5127
5128 fn set_cell_lattice(&self, cell: u32, lattice: Option<u8>) -> PyResult<()> {
5130 self.inner
5131 .lock()
5132 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5133 .set_cell_lattice(cell, lattice)
5134 .map_err(|e| PyValueError::new_err(e.to_string()))
5135 }
5136
5137 fn set_cell_fill(&self, cell: u32, universe: u32) -> PyResult<()> {
5139 self.inner
5140 .lock()
5141 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5142 .set_cell_fill(cell, universe)
5143 .map_err(|e| PyValueError::new_err(e.to_string()))
5144 }
5145}
5146
5147#[pyfunction]
5149fn read_deck(path: &str) -> PyResult<PyDeckProblem> {
5150 nucleide_mcnp_io::problem::parse_deck_file(path)
5151 .map(|inner| PyDeckProblem {
5152 inner: std::sync::Mutex::new(inner),
5153 })
5154 .map_err(|e| PyValueError::new_err(e.to_string()))
5155}
5156
5157#[pyfunction]
5159fn parse_deck(text: &str) -> PyResult<PyDeckProblem> {
5160 PyDeckProblem::loads(text)
5161}
5162
5163fn sdef_to_py(py: Python<'_>, sdef: &nucleide_mcnp_io::sdef::SdefProblem) -> PyResult<Py<PyAny>> {
5172 use pyo3::types::PyDict;
5173 let opt3 = |v: &Option<nucleide_mcnp_io::sdef::SdefRef<[f64; 3]>>| {
5174 v.as_ref().map(|r| r.render()).unwrap_or_default()
5175 };
5176 let opt1 = |v: &Option<nucleide_mcnp_io::sdef::SdefRef<f64>>| {
5177 v.as_ref().map(|r| r.render()).unwrap_or_default()
5178 };
5179 let optu = |v: &Option<nucleide_mcnp_io::sdef::SdefRef<u32>>| {
5180 v.as_ref().map(|r| r.render()).unwrap_or_default()
5181 };
5182 let d = PyDict::new(py);
5183 d.set_item("pos", opt3(&sdef.card.pos))?;
5184 d.set_item("cell", optu(&sdef.card.cell))?;
5185 d.set_item("surf", optu(&sdef.card.surf))?;
5186 d.set_item("vec", opt3(&sdef.card.vec))?;
5187 d.set_item("dir", opt1(&sdef.card.dir))?;
5188 d.set_item("erg", opt1(&sdef.card.erg))?;
5189 d.set_item("nrm", opt1(&sdef.card.nrm))?;
5190 d.set_item(
5191 "par",
5192 sdef.card
5193 .par
5194 .as_ref()
5195 .map(|r| r.render())
5196 .unwrap_or_default(),
5197 )?;
5198 d.set_item("wgt", opt1(&sdef.card.wgt))?;
5199 d.set_item("tme", opt1(&sdef.card.tme))?;
5200 d.set_item("ignored", sdef.card.ignored.clone())?;
5201 let dists: Vec<Py<PyAny>> = sdef
5202 .dists
5203 .iter()
5204 .map(|dist| {
5205 let m = PyDict::new(py);
5206 m.set_item("number", dist.number.to_string())?;
5207 m.set_item("si_option", "L")?;
5208 m.set_item("si", dist.si_text())?;
5209 m.set_item(
5210 "sp_option",
5211 dist.sp.as_ref().map(|_| "D").unwrap_or_default(),
5212 )?;
5213 m.set_item("sp", dist.sp_text())?;
5214 m.set_item(
5215 "sb_option",
5216 dist.sb.as_ref().map(|_| "D").unwrap_or_default(),
5217 )?;
5218 m.set_item("sb", dist.sb_text())?;
5219 Ok(m.into_any().unbind())
5220 })
5221 .collect::<PyResult<Vec<_>>>()?;
5222 d.set_item("distributions", dists)?;
5223 d.set_item("card", sdef.emit())?;
5224 Ok(d.into_any().unbind())
5225}
5226
5227#[pyfunction]
5233fn parse_sdef(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
5234 let sdef = nucleide_mcnp_io::sdef::parse_sdef_text(text)
5235 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5236 sdef_to_py(py, &sdef)
5237}
5238
5239fn csg_to_openmc_inner(
5246 deck: &nucleide_mcnp_io::problem::DeckProblem,
5247) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5248 let (xml, table) = nucleide_csg_xlate::deck_csg_to_openmc_xml(deck)
5249 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5250 Ok((
5251 xml,
5252 table
5253 .entries
5254 .into_iter()
5255 .map(|e| {
5256 let mut d = BTreeMap::new();
5257 d.insert("scope".to_string(), e.scope.to_string());
5258 d.insert("target".to_string(), e.target.to_string());
5259 d.insert("action".to_string(), e.action);
5260 d.insert("reason".to_string(), e.reason);
5261 d
5262 })
5263 .collect(),
5264 ))
5265}
5266
5267#[pyfunction]
5270fn parse_csg_to_openmc(text: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5271 let deck = nucleide_mcnp_io::problem::parse_deck(text)
5272 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5273 csg_to_openmc_inner(&deck)
5274}
5275
5276#[pyfunction]
5279fn read_csg_to_openmc(path: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5280 let deck = nucleide_mcnp_io::problem::parse_deck_file(path)
5281 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5282 csg_to_openmc_inner(&deck)
5283}
5284
5285fn csg_to_serpent_inner(
5292 deck: &nucleide_mcnp_io::problem::DeckProblem,
5293) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5294 let (text, table) = nucleide_csg_xlate::deck_csg_to_serpent_input(deck)
5295 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5296 Ok((
5297 text,
5298 table
5299 .entries
5300 .into_iter()
5301 .map(|e| {
5302 let mut d = BTreeMap::new();
5303 d.insert("scope".to_string(), e.scope.to_string());
5304 d.insert("target".to_string(), e.target.to_string());
5305 d.insert("action".to_string(), e.action);
5306 d.insert("reason".to_string(), e.reason);
5307 d
5308 })
5309 .collect(),
5310 ))
5311}
5312
5313#[pyfunction]
5316fn parse_csg_to_serpent(text: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5317 let deck = nucleide_mcnp_io::problem::parse_deck(text)
5318 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5319 csg_to_serpent_inner(&deck)
5320}
5321
5322#[pyfunction]
5325fn read_csg_to_serpent(path: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5326 let deck = nucleide_mcnp_io::problem::parse_deck_file(path)
5327 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5328 csg_to_serpent_inner(&deck)
5329}
5330
5331fn csg_to_phits_inner(
5339 deck: &nucleide_mcnp_io::problem::DeckProblem,
5340) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5341 let (text, table) = nucleide_csg_xlate::deck_csg_to_phits_input(deck)
5342 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5343 Ok((
5344 text,
5345 table
5346 .entries
5347 .into_iter()
5348 .map(|e| {
5349 let mut d = BTreeMap::new();
5350 d.insert("scope".to_string(), e.scope.to_string());
5351 d.insert("target".to_string(), e.target.to_string());
5352 d.insert("action".to_string(), e.action);
5353 d.insert("reason".to_string(), e.reason);
5354 d
5355 })
5356 .collect(),
5357 ))
5358}
5359
5360#[pyfunction]
5363fn parse_csg_to_phits(text: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5364 let deck = nucleide_mcnp_io::problem::parse_deck(text)
5365 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5366 csg_to_phits_inner(&deck)
5367}
5368
5369#[pyfunction]
5372fn read_csg_to_phits(path: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5373 let deck = nucleide_mcnp_io::problem::parse_deck_file(path)
5374 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5375 csg_to_phits_inner(&deck)
5376}
5377
5378#[pyclass(name = "Inventory")]
5380struct PyInventory {
5381 chain: std::sync::Arc<nucleide_depletion::Chain>,
5382 atoms: BTreeMap<String, f64>,
5383}
5384
5385fn inventory_sys(
5386 chain: &nucleide_depletion::Chain,
5387 rates: &RateMap,
5388) -> PyResult<nucleide_depletion::DepletionSystem> {
5389 let rs = split_rates(rates, chain)?;
5390 nucleide_depletion::DepletionSystem::build(chain.clone(), &rs)
5391 .map_err(|e| PyValueError::new_err(e.to_string()))
5392}
5393
5394fn parse_quantity_unit(unit: &str) -> PyResult<nucleide_depletion::QuantityUnit> {
5395 nucleide_depletion::QuantityUnit::from_str(unit)
5396 .map_err(|e| PyValueError::new_err(format!("{e:?}")))
5397}
5398
5399#[pymethods]
5400impl PyInventory {
5401 #[new]
5404 #[pyo3(signature = (chain, comp, units="atoms"))]
5405 fn new(chain: &PyChain, comp: BTreeMap<String, f64>, units: &str) -> PyResult<Self> {
5406 let unit = parse_quantity_unit(units)?;
5407 let sys = inventory_sys(&chain.inner, &BTreeMap::new())?;
5408 let inv = nucleide_depletion::DecayInventory::from_units(&comp, unit, &sys)
5409 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5410 Ok(Self {
5411 chain: chain.inner.clone(),
5412 atoms: inv.atoms,
5413 })
5414 }
5415
5416 fn numbers(&self) -> BTreeMap<String, f64> {
5418 self.atoms.clone()
5419 }
5420
5421 #[pyo3(signature = (dt, time_unit="s", rates=None, order=48, method="cram48"))]
5428 fn decay(
5429 &self,
5430 dt: f64,
5431 time_unit: &str,
5432 rates: Option<RateMap>,
5433 order: u8,
5434 method: &str,
5435 ) -> PyResult<Self> {
5436 let method = resolve_method(order, method)?;
5437 let unit = nucleide_depletion::inventory::time_unit_from_str(time_unit)
5438 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5439 let seconds = dt * unit.as_seconds();
5440 let empty = BTreeMap::new();
5441 let step_rates = rates.as_ref().unwrap_or(&empty);
5442 let template = inventory_sys(&self.chain, step_rates)?;
5443 let steps = vec![nucleide_depletion::Step::new(
5446 seconds,
5447 split_rates(step_rates, &self.chain)?,
5448 )];
5449 let series = nucleide_depletion::integrate_with_method(
5450 &template,
5451 &chain_vec(&self.chain, &self.atoms)?,
5452 &steps,
5453 nucleide_depletion::Integrator::Predictor,
5454 method,
5455 )
5456 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5457 let names: Vec<String> = self.chain.nuclides.iter().map(|n| n.name.clone()).collect();
5458 let atoms = names
5459 .iter()
5460 .zip(series.atoms.last().cloned().unwrap_or_default())
5461 .map(|(n, v)| (n.clone(), v))
5462 .collect();
5463 Ok(Self {
5464 chain: self.chain.clone(),
5465 atoms,
5466 })
5467 }
5468
5469 fn activities(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
5471 let unit = parse_quantity_unit(units)?;
5472 let sys = inventory_sys(&self.chain, &BTreeMap::new())?;
5473 let inv = nucleide_depletion::DecayInventory {
5474 atoms: self.atoms.clone(),
5475 };
5476 inv.activities(&sys, unit)
5477 .map_err(|e| PyValueError::new_err(e.to_string()))
5478 }
5479
5480 fn masses(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
5482 let unit = parse_quantity_unit(units)?;
5483 let inv = nucleide_depletion::DecayInventory {
5484 atoms: self.atoms.clone(),
5485 };
5486 inv.masses(unit)
5487 .map_err(|e| PyValueError::new_err(e.to_string()))
5488 }
5489
5490 fn moles(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
5492 let unit = parse_quantity_unit(units)?;
5493 let inv = nucleide_depletion::DecayInventory {
5494 atoms: self.atoms.clone(),
5495 };
5496 inv.moles(unit)
5497 .map_err(|e| PyValueError::new_err(e.to_string()))
5498 }
5499
5500 fn activity_fractions(&self) -> PyResult<BTreeMap<String, f64>> {
5502 let sys = inventory_sys(&self.chain, &BTreeMap::new())?;
5503 let inv = nucleide_depletion::DecayInventory {
5504 atoms: self.atoms.clone(),
5505 };
5506 inv.activity_fractions(&sys)
5507 .map_err(|e| PyValueError::new_err(e.to_string()))
5508 }
5509
5510 fn mass_fractions(&self) -> PyResult<BTreeMap<String, f64>> {
5512 let inv = nucleide_depletion::DecayInventory {
5513 atoms: self.atoms.clone(),
5514 };
5515 inv.mass_fractions()
5516 .map_err(|e| PyValueError::new_err(e.to_string()))
5517 }
5518
5519 fn mole_fractions(&self) -> BTreeMap<String, f64> {
5521 nucleide_depletion::DecayInventory {
5522 atoms: self.atoms.clone(),
5523 }
5524 .mole_fractions()
5525 }
5526
5527 fn half_lives_readable(&self) -> BTreeMap<String, String> {
5529 nucleide_depletion::DecayInventory {
5530 atoms: self.atoms.clone(),
5531 }
5532 .half_lives_readable()
5533 }
5534
5535 fn add(&self, other: &Self) -> Self {
5537 let a = nucleide_depletion::DecayInventory {
5538 atoms: self.atoms.clone(),
5539 };
5540 let b = nucleide_depletion::DecayInventory {
5541 atoms: other.atoms.clone(),
5542 };
5543 Self {
5544 chain: self.chain.clone(),
5545 atoms: a.add(&b).atoms,
5546 }
5547 }
5548
5549 fn sub(&self, other: &Self) -> Self {
5551 let a = nucleide_depletion::DecayInventory {
5552 atoms: self.atoms.clone(),
5553 };
5554 let b = nucleide_depletion::DecayInventory {
5555 atoms: other.atoms.clone(),
5556 };
5557 Self {
5558 chain: self.chain.clone(),
5559 atoms: a.sub(&b).atoms,
5560 }
5561 }
5562
5563 fn mul(&self, scalar: f64) -> Self {
5565 let a = nucleide_depletion::DecayInventory {
5566 atoms: self.atoms.clone(),
5567 };
5568 Self {
5569 chain: self.chain.clone(),
5570 atoms: a.mul(scalar).atoms,
5571 }
5572 }
5573
5574 fn div(&self, scalar: f64) -> Self {
5576 let a = nucleide_depletion::DecayInventory {
5577 atoms: self.atoms.clone(),
5578 };
5579 Self {
5580 chain: self.chain.clone(),
5581 atoms: a.div(scalar).atoms,
5582 }
5583 }
5584
5585 fn to_csv(&self) -> String {
5587 nucleide_depletion::DecayInventory {
5588 atoms: self.atoms.clone(),
5589 }
5590 .to_csv()
5591 }
5592
5593 #[staticmethod]
5595 fn from_csv(chain: &PyChain, text: &str) -> PyResult<Self> {
5596 let inv = nucleide_depletion::DecayInventory::from_csv(text)
5598 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5599 for name in inv.atoms.keys() {
5600 if chain.inner.index_of(name).is_none() {
5601 return Err(PyValueError::new_err(format!(
5602 "unknown nuclide `{name}` for this chain"
5603 )));
5604 }
5605 }
5606 Ok(Self {
5607 chain: chain.inner.clone(),
5608 atoms: inv.atoms,
5609 })
5610 }
5611}
5612
5613fn chain_vec(
5615 chain: &nucleide_depletion::Chain,
5616 atoms: &BTreeMap<String, f64>,
5617) -> PyResult<Vec<f64>> {
5618 let mut vec = vec![0.0; chain.len()];
5619 for (name, value) in atoms {
5620 let idx = chain.index_of(name).ok_or_else(|| {
5621 PyValueError::new_err(format!("unknown nuclide `{name}` for this chain"))
5622 })?;
5623 vec[idx] = *value;
5624 }
5625 Ok(vec)
5626}
5627
5628#[pyfunction]
5630#[pyo3(signature = (chain, n0, dt, rates=None))]
5631fn cumulative_decays(
5632 chain: &PyChain,
5633 n0: BTreeMap<String, f64>,
5634 dt: f64,
5635 rates: Option<RateMap>,
5636) -> PyResult<BTreeMap<String, f64>> {
5637 let empty = BTreeMap::new();
5638 let sys = inventory_sys(&chain.inner, rates.as_ref().unwrap_or(&empty))?;
5639 let vec = chain_vec(&chain.inner, &n0)?;
5640 let out = nucleide_depletion::cumulative_decays(&sys, &vec, dt)
5641 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5642 Ok(chain
5643 .inner
5644 .nuclides
5645 .iter()
5646 .zip(out)
5647 .map(|(nuc, v)| (nuc.name.clone(), v))
5648 .collect())
5649}
5650
5651#[pyfunction]
5653fn progeny(chain: &PyChain, name: &str) -> Vec<(String, f64, String)> {
5654 nucleide_depletion::progeny(&chain.inner, name)
5655}
5656
5657#[pyfunction]
5659fn branching_fraction(chain: &PyChain, parent: &str, child: &str) -> Option<f64> {
5660 nucleide_depletion::branching_fraction(&chain.inner, parent, child)
5661}
5662
5663#[pyfunction]
5665fn decay_mode(chain: &PyChain, parent: &str, child: &str) -> Option<String> {
5666 nucleide_depletion::decay_mode(&chain.inner, parent, child)
5667}
5668
5669#[pyfunction]
5671fn chain_edges(chain: &PyChain) -> Vec<(String, String, f64, String)> {
5672 nucleide_depletion::chain_edges(&chain.inner)
5673}
5674
5675#[pyfunction]
5677fn armi_to_nucid(name: &str) -> PyResult<PyNuclide> {
5678 nucleide_nuclei::armi::armi_name_to_nucid(name)
5679 .map(|inner| PyNuclide { inner })
5680 .map_err(|e| PyValueError::new_err(e.to_string()))
5681}
5682
5683#[pyfunction]
5685fn nucid_to_armi(nuclide: &PyNuclide) -> String {
5686 nucleide_nuclei::armi::nucid_to_armi_label(nuclide.inner)
5687}
5688
5689#[pyfunction]
5691fn mcc3_to_nucid(name: &str) -> PyResult<PyNuclide> {
5692 nucleide_nuclei::armi::mcc3_to_nucid(name)
5693 .map(|inner| PyNuclide { inner })
5694 .map_err(|e| PyValueError::new_err(e.to_string()))
5695}
5696
5697#[pyfunction]
5702#[pyo3(signature = (comp, widths=None))]
5703fn check_labels(
5704 comp: BTreeMap<String, f64>,
5705 widths: Option<Vec<usize>>,
5706) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
5707 let mat = comp_to_material(comp)?;
5708 let widths = widths.unwrap_or_else(|| nucleide_material::DEFAULT_WIDTHS.to_vec());
5709 let collisions = nucleide_material::check_labels(&mat, &widths);
5710 Python::attach(|py| {
5711 Ok(collisions
5712 .into_iter()
5713 .map(|c| {
5714 let mut d = BTreeMap::new();
5715 d.insert(
5716 "truncated".to_string(),
5717 c.truncated.into_pyobject(py).unwrap().unbind().into_any(),
5718 );
5719 d.insert(
5720 "width".to_string(),
5721 c.width.into_pyobject(py).unwrap().unbind().into_any(),
5722 );
5723 let members: Vec<String> = c.members.iter().map(|id| id.to_name()).collect();
5724 d.insert(
5725 "members".to_string(),
5726 members.into_pyobject(py).unwrap().unbind().into_any(),
5727 );
5728 d
5729 })
5730 .collect())
5731 })
5732}
5733
5734#[pyfunction]
5736fn audit_material(comp: BTreeMap<String, f64>) -> PyResult<Vec<BTreeMap<String, String>>> {
5737 let mat = comp_to_material(comp)?;
5738 Ok(nucleide_material::audit(&mat, &nucleide_material::Ame2020)
5739 .into_iter()
5740 .map(|issue| {
5741 let mut d = BTreeMap::new();
5742 d.insert("kind".to_string(), format!("{:?}", issue.kind));
5743 d.insert("detail".to_string(), issue.detail);
5744 d
5745 })
5746 .collect())
5747}
5748
5749#[pyfunction]
5756#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
5757#[allow(clippy::too_many_arguments)]
5758fn emit_cards(
5759 comp: BTreeMap<String, f64>,
5760 name: &str,
5761 density: Option<f64>,
5762 mcnp_number: u32,
5763 xs_suffix: &str,
5764 serpent_lib: &str,
5765 fluka_fid: u32,
5766 partisn_zone: u32,
5767) -> PyResult<BTreeMap<String, String>> {
5768 let (emitted, _) = emit_drift_inner(
5769 comp,
5770 name,
5771 density,
5772 mcnp_number,
5773 xs_suffix,
5774 serpent_lib,
5775 fluka_fid,
5776 partisn_zone,
5777 )?;
5778 Ok(emitted
5779 .into_iter()
5780 .map(|e| (e.code.to_string(), e.text))
5781 .collect())
5782}
5783
5784#[pyfunction]
5788#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
5789#[allow(clippy::too_many_arguments)]
5790fn emit_drift_table(
5791 comp: BTreeMap<String, f64>,
5792 name: &str,
5793 density: Option<f64>,
5794 mcnp_number: u32,
5795 xs_suffix: &str,
5796 serpent_lib: &str,
5797 fluka_fid: u32,
5798 partisn_zone: u32,
5799) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
5800 let (_, table) = emit_drift_inner(
5801 comp,
5802 name,
5803 density,
5804 mcnp_number,
5805 xs_suffix,
5806 serpent_lib,
5807 fluka_fid,
5808 partisn_zone,
5809 )?;
5810 drift_table_to_py(table)
5811}
5812
5813fn drift_table_to_py(
5814 table: nucleide_emit::DriftTable,
5815) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
5816 Python::attach(|py| {
5817 Ok(table
5818 .rows
5819 .into_iter()
5820 .map(|r| {
5821 let mut d = BTreeMap::new();
5822 d.insert(
5823 "code".to_string(),
5824 r.code
5825 .to_string()
5826 .into_pyobject(py)
5827 .unwrap()
5828 .unbind()
5829 .into_any(),
5830 );
5831 d.insert(
5832 "mass_in".to_string(),
5833 r.mass_in.into_pyobject(py).unwrap().unbind().into_any(),
5834 );
5835 d.insert(
5836 "mass_out".to_string(),
5837 r.mass_out.into_pyobject(py).unwrap().unbind().into_any(),
5838 );
5839 d.insert(
5840 "rel_drift".to_string(),
5841 r.rel_drift.into_pyobject(py).unwrap().unbind().into_any(),
5842 );
5843 let dropped: Vec<BTreeMap<String, Py<PyAny>>> = r
5844 .dropped
5845 .into_iter()
5846 .map(|x| {
5847 let mut dd = BTreeMap::new();
5848 dd.insert(
5849 "nuclide".to_string(),
5850 x.id.to_name()
5851 .into_pyobject(py)
5852 .unwrap()
5853 .unbind()
5854 .into_any(),
5855 );
5856 dd.insert(
5857 "mass".to_string(),
5858 x.mass.into_pyobject(py).unwrap().unbind().into_any(),
5859 );
5860 dd.insert(
5861 "reason".to_string(),
5862 x.reason.into_pyobject(py).unwrap().unbind().into_any(),
5863 );
5864 dd
5865 })
5866 .collect();
5867 d.insert(
5868 "dropped".to_string(),
5869 dropped.into_pyobject(py).unwrap().unbind().into_any(),
5870 );
5871 d.insert(
5872 "reparsed".to_string(),
5873 pyo3::types::PyBool::new(py, r.reparsed)
5874 .to_owned()
5875 .into_any()
5876 .unbind(),
5877 );
5878 d
5879 })
5880 .collect())
5881 })
5882}
5883
5884#[allow(clippy::too_many_arguments)]
5885fn emit_drift_inner(
5886 comp: BTreeMap<String, f64>,
5887 name: &str,
5888 density: Option<f64>,
5889 mcnp_number: u32,
5890 xs_suffix: &str,
5891 serpent_lib: &str,
5892 fluka_fid: u32,
5893 partisn_zone: u32,
5894) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
5895 let mut mat = comp_to_material(comp)?;
5896 mat.set_density(density);
5897 emit_drift_with_mat(
5898 mat,
5899 name,
5900 mcnp_number,
5901 xs_suffix,
5902 serpent_lib,
5903 fluka_fid,
5904 partisn_zone,
5905 )
5906}
5907
5908#[allow(clippy::too_many_arguments)]
5909fn emit_drift_with_mat(
5910 mat: nucleide_material::Material,
5911 name: &str,
5912 mcnp_number: u32,
5913 xs_suffix: &str,
5914 serpent_lib: &str,
5915 fluka_fid: u32,
5916 partisn_zone: u32,
5917) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
5918 let mut opts = nucleide_emit::EmitOptions::new(name);
5919 opts.mcnp_number = mcnp_number;
5920 opts.xs_suffix = xs_suffix.to_string();
5921 opts.serpent_lib = serpent_lib.to_string();
5922 opts.fluka_fid = fluka_fid;
5923 opts.partisn_zone = partisn_zone;
5924 nucleide_emit::emit_drift(&mat, &opts).map_err(|e| PyValueError::new_err(e.to_string()))
5925}
5926
5927#[allow(clippy::too_many_arguments)]
5928fn emit_armi_drift_inner(
5929 comp: BTreeMap<String, f64>,
5930 name: &str,
5931 density: Option<f64>,
5932 mcnp_number: u32,
5933 xs_suffix: &str,
5934 serpent_lib: &str,
5935 fluka_fid: u32,
5936 partisn_zone: u32,
5937) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
5938 let mat = nucleide_emit::armi::from_armi_mass_fracs(comp, density)
5941 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5942 emit_drift_with_mat(
5943 mat,
5944 name,
5945 mcnp_number,
5946 xs_suffix,
5947 serpent_lib,
5948 fluka_fid,
5949 partisn_zone,
5950 )
5951}
5952
5953#[pyfunction]
5961#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
5962#[allow(clippy::too_many_arguments)]
5963fn emit_armi_cards(
5964 comp: BTreeMap<String, f64>,
5965 name: &str,
5966 density: Option<f64>,
5967 mcnp_number: u32,
5968 xs_suffix: &str,
5969 serpent_lib: &str,
5970 fluka_fid: u32,
5971 partisn_zone: u32,
5972) -> PyResult<BTreeMap<String, String>> {
5973 let (emitted, _) = emit_armi_drift_inner(
5974 comp,
5975 name,
5976 density,
5977 mcnp_number,
5978 xs_suffix,
5979 serpent_lib,
5980 fluka_fid,
5981 partisn_zone,
5982 )?;
5983 Ok(emitted
5984 .into_iter()
5985 .map(|e| (e.code.to_string(), e.text))
5986 .collect())
5987}
5988
5989#[pyfunction]
5993#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
5994#[allow(clippy::too_many_arguments)]
5995fn emit_armi_drift_table(
5996 comp: BTreeMap<String, f64>,
5997 name: &str,
5998 density: Option<f64>,
5999 mcnp_number: u32,
6000 xs_suffix: &str,
6001 serpent_lib: &str,
6002 fluka_fid: u32,
6003 partisn_zone: u32,
6004) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
6005 let (_, table) = emit_armi_drift_inner(
6006 comp,
6007 name,
6008 density,
6009 mcnp_number,
6010 xs_suffix,
6011 serpent_lib,
6012 fluka_fid,
6013 partisn_zone,
6014 )?;
6015 drift_table_to_py(table)
6016}
6017
6018fn parse_reactivity(
6031 spec: &BTreeMap<String, Py<PyAny>>,
6032 py: Python<'_>,
6033) -> PyResult<nucleide_kinetics::Reactivity> {
6034 use nucleide_kinetics::Reactivity as R;
6035 let kind: String = get_str(spec, py, "kind", "reactivity spec needs a `kind`")?;
6036 let num = |key: &str| -> PyResult<f64> { get_num(spec, py, key) };
6037 let vec = |key: &str| -> PyResult<Vec<f64>> { get_vec(spec, py, key) };
6038 let r = match kind.as_str() {
6039 "constant" => R::Constant { rho: num("rho")? },
6040 "step" => R::Step {
6041 t_step: num("t_step")?,
6042 rho_init: num("rho_init")?,
6043 rho_final: num("rho_final")?,
6044 },
6045 "impulse" => R::Impulse {
6046 t_start: num("t_start")?,
6047 t_end: num("t_end")?,
6048 rho_init: num("rho_init")?,
6049 rho_max: num("rho_max")?,
6050 },
6051 "ramp" => R::Ramp {
6052 t_start: num("t_start")?,
6053 t_end: num("t_end")?,
6054 rho_init: num("rho_init")?,
6055 rho_rise: num("rho_rise")?,
6056 rho_final: num("rho_final")?,
6057 },
6058 "polyline" => R::Polyline {
6059 times: vec("times")?,
6060 values: vec("values")?,
6061 },
6062 other => {
6063 return Err(PyValueError::new_err(format!(
6064 "unknown reactivity kind `{other}` (supported: constant, step, impulse, ramp, polyline)"
6065 )))
6066 }
6067 };
6068 r.validate()
6069 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6070 Ok(r)
6071}
6072
6073fn get_str(
6074 spec: &BTreeMap<String, Py<PyAny>>,
6075 py: Python<'_>,
6076 key: &str,
6077 missing: &str,
6078) -> PyResult<String> {
6079 spec.get(key)
6080 .ok_or_else(|| PyValueError::new_err(missing.to_string()))?
6081 .extract::<String>(py)
6082 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a string")))
6083}
6084
6085fn get_num(spec: &BTreeMap<String, Py<PyAny>>, py: Python<'_>, key: &str) -> PyResult<f64> {
6086 spec.get(key)
6087 .ok_or_else(|| PyValueError::new_err(format!("reactivity spec missing `{key}`")))?
6088 .extract::<f64>(py)
6089 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
6090}
6091
6092fn get_vec(spec: &BTreeMap<String, Py<PyAny>>, py: Python<'_>, key: &str) -> PyResult<Vec<f64>> {
6093 spec.get(key)
6094 .ok_or_else(|| PyValueError::new_err(format!("reactivity spec missing `{key}`")))?
6095 .extract::<Vec<f64>>(py)
6096 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a list of numbers")))
6097}
6098
6099fn kinetics_params(
6100 betas: Vec<f64>,
6101 lambdas: Vec<f64>,
6102 lambda_gen: f64,
6103) -> PyResult<nucleide_kinetics::KineticParams> {
6104 nucleide_kinetics::KineticParams::new(betas, lambdas, lambda_gen)
6105 .map_err(|e| PyValueError::new_err(e.to_string()))
6106}
6107
6108#[pyfunction]
6119#[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))]
6120#[allow(clippy::too_many_arguments)]
6121fn kinetics_solve(
6122 py: Python<'_>,
6123 betas: Vec<f64>,
6124 lambdas: Vec<f64>,
6125 lambda_gen: f64,
6126 rho: BTreeMap<String, Py<PyAny>>,
6127 t: Vec<f64>,
6128 n0: f64,
6129 c0: Option<Vec<f64>>,
6130 method: &str,
6131 rtol: f64,
6132 atol: f64,
6133 dt_min: f64,
6134 dt_max: Option<f64>,
6135 max_steps: usize,
6136) -> PyResult<Py<PyAny>> {
6137 use nucleide_kinetics::{Method as M, SolverOptions};
6138 let params = kinetics_params(betas, lambdas, lambda_gen)?;
6139 let rho = parse_reactivity(&rho, py)?;
6140 let grid =
6141 nucleide_kinetics::TimeGrid::new(t).map_err(|e| PyValueError::new_err(e.to_string()))?;
6142 let state = nucleide_kinetics::State::new(¶ms, n0, c0)
6143 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6144 let method = if method.eq_ignore_ascii_case("trapezoidal") {
6145 M::Trapezoidal
6146 } else if method.eq_ignore_ascii_case("backward_euler") {
6147 M::BackwardEuler
6148 } else {
6149 return Err(PyValueError::new_err(format!(
6150 "unknown kinetics method `{method}` (supported: trapezoidal, backward_euler)"
6151 )));
6152 };
6153 let opts = SolverOptions {
6154 method,
6155 rtol,
6156 atol,
6157 dt_min,
6158 dt_max: dt_max.unwrap_or(f64::INFINITY),
6159 max_steps,
6160 };
6161 let sol = nucleide_kinetics::solve(¶ms, &rho, &grid, &state, &opts)
6162 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6163 use pyo3::types::PyDict;
6164 let out = PyDict::new(py);
6165 out.set_item("times", &sol.times).ok();
6166 out.set_item("n", &sol.n).ok();
6167 out.set_item("C", &sol.c).ok();
6168 out.set_item("n0", sol.initial.n0).ok();
6169 out.set_item("C0", &sol.initial.c0).ok();
6170 Ok(out.into_any().unbind())
6171}
6172
6173#[pyfunction]
6175fn kinetics_equilibrium(
6176 betas: Vec<f64>,
6177 lambdas: Vec<f64>,
6178 lambda_gen: f64,
6179 n0: f64,
6180) -> PyResult<Vec<f64>> {
6181 kinetics_params(betas, lambdas, lambda_gen)?
6182 .equilibrium_precursors(n0)
6183 .map_err(|e| PyValueError::new_err(e.to_string()))
6184}
6185
6186#[pyfunction]
6188#[pyo3(signature = (betas, lambdas, lambda_gen, rho, n0, c0=None))]
6189fn kinetics_initial_rate(
6190 py: Python<'_>,
6191 betas: Vec<f64>,
6192 lambdas: Vec<f64>,
6193 lambda_gen: f64,
6194 rho: BTreeMap<String, Py<PyAny>>,
6195 n0: f64,
6196 c0: Option<Vec<f64>>,
6197) -> PyResult<f64> {
6198 let params = kinetics_params(betas, lambdas, lambda_gen)?;
6199 let rho = parse_reactivity(&rho, py)?;
6200 let state = nucleide_kinetics::State::new(¶ms, n0, c0)
6201 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6202 Ok(nucleide_kinetics::solve::initial_rate(
6203 ¶ms, &rho, &state,
6204 ))
6205}
6206
6207#[pyfunction]
6209fn kinetics_inhour_rho(
6210 betas: Vec<f64>,
6211 lambdas: Vec<f64>,
6212 lambda_gen: f64,
6213 omega: f64,
6214) -> PyResult<f64> {
6215 let params = kinetics_params(betas, lambdas, lambda_gen)?;
6216 nucleide_kinetics::rho_of_omega(¶ms, omega)
6217 .map_err(|e| PyValueError::new_err(e.to_string()))
6218}
6219
6220#[pyfunction]
6222fn kinetics_stable_period(
6223 betas: Vec<f64>,
6224 lambdas: Vec<f64>,
6225 lambda_gen: f64,
6226 rho: f64,
6227) -> PyResult<f64> {
6228 let params = kinetics_params(betas, lambdas, lambda_gen)?;
6229 nucleide_kinetics::stable_period(¶ms, rho).map_err(|e| PyValueError::new_err(e.to_string()))
6230}
6231
6232#[pyfunction]
6237fn kinetics_prompt_jump(
6238 n_before: f64,
6239 rho_before: f64,
6240 rho_after: f64,
6241 beta_total: f64,
6242) -> PyResult<f64> {
6243 nucleide_kinetics::prompt_jump(n_before, rho_before, rho_after, beta_total)
6244 .map_err(|e| PyValueError::new_err(e.to_string()))
6245}
6246
6247fn parse_tritium_boundary(
6259 spec: &BTreeMap<String, Py<PyAny>>,
6260 py: Python<'_>,
6261) -> PyResult<nucleide_tritium::Boundary> {
6262 use nucleide_tritium::Boundary as B;
6263 let kind: String = spec
6264 .get("kind")
6265 .ok_or_else(|| PyValueError::new_err("boundary spec needs a `kind`"))?
6266 .extract::<String>(py)
6267 .map_err(|_| PyValueError::new_err("`kind` must be a string"))?;
6268 let num = |key: &str| -> PyResult<f64> {
6269 spec.get(key)
6270 .ok_or_else(|| PyValueError::new_err(format!("boundary spec missing `{key}`")))?
6271 .extract::<f64>(py)
6272 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
6273 };
6274 let b = match kind.as_str() {
6275 "dirichlet" => B::dirichlet(num("value")?),
6276 "sieverts" => B::sieverts(num("solubility")?, num("pressure")?),
6277 "henry" => B::henry(num("solubility")?, num("pressure")?),
6278 "recombination" => B::recombination(num("rate")?),
6279 "zero_flux" => Ok(B::ZeroFlux),
6280 other => {
6281 return Err(PyValueError::new_err(format!(
6282 "unknown boundary kind `{other}` (supported: dirichlet, sieverts, henry, recombination, zero_flux)"
6283 )))
6284 }
6285 };
6286 b.map_err(|e| PyValueError::new_err(e.to_string()))
6287}
6288
6289fn parse_tritium_trap(
6294 spec: &BTreeMap<String, Py<PyAny>>,
6295 py: Python<'_>,
6296) -> PyResult<nucleide_tritium::TrapSpec> {
6297 let num = |key: &str| -> PyResult<f64> {
6298 spec.get(key)
6299 .ok_or_else(|| PyValueError::new_err(format!("trap spec missing `{key}`")))?
6300 .extract::<f64>(py)
6301 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
6302 };
6303 let opt = |key: &str| -> PyResult<f64> {
6304 match spec.get(key) {
6305 None => Ok(0.0),
6306 Some(v) => v
6307 .extract::<f64>(py)
6308 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number"))),
6309 }
6310 };
6311 nucleide_tritium::TrapSpec::new(
6312 num("k0")?,
6313 opt("e_k")?,
6314 num("p0")?,
6315 opt("e_p")?,
6316 num("site_density")?,
6317 )
6318 .map_err(|e| PyValueError::new_err(e.to_string()))
6319}
6320
6321#[allow(clippy::too_many_arguments)]
6322fn tritium_params(
6323 py: Python<'_>,
6324 length: f64,
6325 cells: usize,
6326 d0: f64,
6327 e_d: f64,
6328 traps: Vec<BTreeMap<String, Py<PyAny>>>,
6329 temperature: Vec<f64>,
6330 source: Option<Vec<f64>>,
6331) -> PyResult<nucleide_tritium::TransportParams> {
6332 let parsed: Vec<nucleide_tritium::TrapSpec> = traps
6333 .iter()
6334 .map(|s| parse_tritium_trap(s, py))
6335 .collect::<PyResult<_>>()?;
6336 nucleide_tritium::TransportParams::new(
6337 length,
6338 cells,
6339 d0,
6340 e_d,
6341 parsed,
6342 temperature,
6343 source.unwrap_or_default(),
6344 )
6345 .map_err(|e| PyValueError::new_err(e.to_string()))
6346}
6347
6348#[pyfunction]
6358#[pyo3(signature = (length, cells, d0, e_d, traps, temperature, source, left, right))]
6359#[allow(clippy::too_many_arguments)]
6360fn tritium_steady(
6361 py: Python<'_>,
6362 length: f64,
6363 cells: usize,
6364 d0: f64,
6365 e_d: f64,
6366 traps: Vec<BTreeMap<String, Py<PyAny>>>,
6367 temperature: Vec<f64>,
6368 source: Option<Vec<f64>>,
6369 left: BTreeMap<String, Py<PyAny>>,
6370 right: BTreeMap<String, Py<PyAny>>,
6371) -> PyResult<Py<PyAny>> {
6372 let params = tritium_params(py, length, cells, d0, e_d, traps, temperature, source)?;
6373 let left = parse_tritium_boundary(&left, py)?;
6374 let right = parse_tritium_boundary(&right, py)?;
6375 let s = nucleide_tritium::steady_state(¶ms, &left, &right)
6376 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6377 use pyo3::types::PyDict;
6378 let out = PyDict::new(py);
6379 out.set_item("centres", &s.centres).ok();
6380 out.set_item("mobile", &s.mobile).ok();
6381 out.set_item("trapped", &s.trapped).ok();
6382 out.set_item("flux_left", s.flux_left).ok();
6383 out.set_item("flux_right", s.flux_right).ok();
6384 out.set_item("inventory_mobile", s.inventory_mobile).ok();
6385 out.set_item("inventory_trapped", s.inventory_trapped).ok();
6386 Ok(out.into_any().unbind())
6387}
6388
6389#[pyfunction]
6399#[pyo3(signature = (length, cells, d0, e_d, traps, temperature, source, left, right, t, mobile0=None, trapped0=None, method="crank_nicolson", rtol=1e-9, atol=1e-12, dt_min=1e-14, dt_max=None, max_steps=1000000))]
6400#[allow(clippy::too_many_arguments)]
6401fn tritium_transient(
6402 py: Python<'_>,
6403 length: f64,
6404 cells: usize,
6405 d0: f64,
6406 e_d: f64,
6407 traps: Vec<BTreeMap<String, Py<PyAny>>>,
6408 temperature: Vec<f64>,
6409 source: Option<Vec<f64>>,
6410 left: BTreeMap<String, Py<PyAny>>,
6411 right: BTreeMap<String, Py<PyAny>>,
6412 t: Vec<f64>,
6413 mobile0: Option<Vec<f64>>,
6414 trapped0: Option<Vec<Vec<f64>>>,
6415 method: &str,
6416 rtol: f64,
6417 atol: f64,
6418 dt_min: f64,
6419 dt_max: Option<f64>,
6420 max_steps: usize,
6421) -> PyResult<Py<PyAny>> {
6422 use nucleide_tritium::{SolverOptions, Theta};
6423 let params = tritium_params(py, length, cells, d0, e_d, traps, temperature, source)?;
6424 let left = parse_tritium_boundary(&left, py)?;
6425 let right = parse_tritium_boundary(&right, py)?;
6426 let grid =
6427 nucleide_tritium::TimeGrid::new(t).map_err(|e| PyValueError::new_err(e.to_string()))?;
6428 let ntraps = params.traps.len();
6429 let mobile = mobile0.unwrap_or_else(|| vec![0.0; params.cells]);
6430 let trapped = trapped0.unwrap_or_else(|| vec![vec![0.0; ntraps]; params.cells]);
6431 let initial = nucleide_tritium::InitialState::new(¶ms, mobile, trapped)
6432 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6433 let theta = if method.eq_ignore_ascii_case("crank_nicolson") {
6434 Theta::CrankNicolson
6435 } else if method.eq_ignore_ascii_case("backward_euler") {
6436 Theta::BackwardEuler
6437 } else {
6438 return Err(PyValueError::new_err(format!(
6439 "unknown tritium method `{method}` (supported: crank_nicolson, backward_euler)"
6440 )));
6441 };
6442 let opts = SolverOptions {
6443 theta,
6444 rtol,
6445 atol,
6446 dt_min,
6447 dt_max: dt_max.unwrap_or(f64::INFINITY),
6448 max_steps,
6449 };
6450 let sol = nucleide_tritium::solve(¶ms, &left, &right, &grid, &initial, &opts)
6451 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6452 use pyo3::types::PyDict;
6453 let out = PyDict::new(py);
6454 out.set_item("times", &sol.times).ok();
6455 out.set_item("mobile", &sol.mobile).ok();
6456 out.set_item("trapped", &sol.trapped).ok();
6457 out.set_item("flux_left", &sol.flux_left).ok();
6458 out.set_item("flux_right", &sol.flux_right).ok();
6459 Ok(out.into_any().unbind())
6460}
6461
6462#[pyfunction]
6464fn tritium_time_lag(length: f64, diffusivity: f64) -> PyResult<f64> {
6465 nucleide_tritium::time_lag(length, diffusivity)
6466 .map_err(|e| PyValueError::new_err(e.to_string()))
6467}
6468
6469#[pyfunction]
6471fn tritium_breakthrough(diffusivity: f64, length: f64, times: Vec<f64>) -> PyResult<Vec<f64>> {
6472 times
6473 .iter()
6474 .map(|t| {
6475 nucleide_tritium::breakthrough_ratio(diffusivity, length, *t)
6476 .map_err(|e| PyValueError::new_err(e.to_string()))
6477 })
6478 .collect()
6479}
6480
6481#[pyfunction]
6483fn tritium_oriani(diffusivity: f64, equilibrium_constant: f64, site_density: f64) -> PyResult<f64> {
6484 nucleide_tritium::effective_diffusivity(diffusivity, equilibrium_constant, site_density)
6485 .map_err(|e| PyValueError::new_err(e.to_string()))
6486}
6487
6488#[pyfunction]
6490fn tritium_langmuir(site_density: f64, equilibrium_constant: f64, c_mobile: f64) -> PyResult<f64> {
6491 nucleide_tritium::equilibrium_trapped(site_density, equilibrium_constant, c_mobile)
6492 .map_err(|e| PyValueError::new_err(e.to_string()))
6493}
6494
6495#[pyfunction]
6497fn tritium_irreversible_fill(
6498 rate_k: f64,
6499 c_mobile: f64,
6500 site_density: f64,
6501 times: Vec<f64>,
6502) -> PyResult<Vec<f64>> {
6503 times
6504 .iter()
6505 .map(|t| {
6506 nucleide_tritium::irreversible_fill(rate_k, c_mobile, site_density, *t)
6507 .map_err(|e| PyValueError::new_err(e.to_string()))
6508 })
6509 .collect()
6510}
6511
6512#[pyfunction]
6514fn tritium_sieverts(solubility: f64, pressure: f64) -> PyResult<f64> {
6515 nucleide_tritium::sieverts_concentration(solubility, pressure)
6516 .map_err(|e| PyValueError::new_err(e.to_string()))
6517}
6518
6519#[pyfunction]
6521fn tritium_recombination_rate(kr0: f64, e_r: f64, temp: f64) -> PyResult<f64> {
6522 nucleide_tritium::recombination_rate_arrhenius(kr0, e_r, temp)
6523 .map_err(|e| PyValueError::new_err(e.to_string()))
6524}
6525
6526#[pyfunction]
6532fn spectroscopy_rect_smooth(counts: Vec<f64>, m: i64) -> PyResult<Vec<f64>> {
6533 let w = usize::try_from(m).map_err(|_| {
6534 PyValueError::new_err(format!("spectroscopy: smoothing width {m} is less than 3"))
6535 })?;
6536 nucleide_spectroscopy::rect_smooth(&counts, w).map_err(|e| PyValueError::new_err(e.to_string()))
6537}
6538
6539#[pyfunction]
6541fn spectroscopy_five_point_smooth(counts: Vec<f64>) -> PyResult<Vec<f64>> {
6542 nucleide_spectroscopy::five_point_smooth(&counts)
6543 .map_err(|e| PyValueError::new_err(e.to_string()))
6544}
6545
6546#[pyfunction]
6548fn spectroscopy_calc_bg(
6549 counts: Vec<f64>,
6550 channels: Vec<f64>,
6551 c1: i64,
6552 c2: i64,
6553 m: i64,
6554) -> PyResult<f64> {
6555 nucleide_spectroscopy::calc_bg(&counts, &channels, c1, c2, m)
6556 .map_err(|e| PyValueError::new_err(e.to_string()))
6557}
6558
6559#[pyfunction]
6561fn spectroscopy_gross_count(
6562 counts: Vec<f64>,
6563 channels: Vec<f64>,
6564 c1: i64,
6565 c2: i64,
6566) -> PyResult<f64> {
6567 nucleide_spectroscopy::gross_count(&counts, &channels, c1, c2)
6568 .map_err(|e| PyValueError::new_err(e.to_string()))
6569}
6570
6571#[pyfunction]
6573fn spectroscopy_net_counts(
6574 counts: Vec<f64>,
6575 channels: Vec<f64>,
6576 c1: i64,
6577 c2: i64,
6578 m: i64,
6579) -> PyResult<f64> {
6580 nucleide_spectroscopy::net_counts(&counts, &channels, c1, c2, m)
6581 .map_err(|e| PyValueError::new_err(e.to_string()))
6582}
6583
6584#[pyfunction]
6586fn spectroscopy_energy_bins(channels: Vec<f64>, calib_e_fit: Vec<f64>) -> PyResult<Vec<f64>> {
6587 nucleide_spectroscopy::energy_bins(&channels, &calib_e_fit)
6588 .map_err(|e| PyValueError::new_err(e.to_string()))
6589}
6590
6591#[pyfunction]
6593fn spectroscopy_detector_efficiency(
6594 energy_mev: f64,
6595 eff_coeff: Vec<f64>,
6596 eff_fit: i64,
6597) -> PyResult<f64> {
6598 nucleide_spectroscopy::detector_efficiency(energy_mev, &eff_coeff, eff_fit)
6599 .map_err(|e| PyValueError::new_err(e.to_string()))
6600}
6601
6602#[pyfunction]
6608#[pyo3(signature = (energies, effs, weights, order, eff_fit=1))]
6609fn spectroscopy_fit_efficiency(
6610 energies: Vec<f64>,
6611 effs: Vec<f64>,
6612 weights: Vec<f64>,
6613 order: usize,
6614 eff_fit: i64,
6615) -> PyResult<Vec<f64>> {
6616 nucleide_spectroscopy::fit_efficiency(&energies, &effs, &weights, order, eff_fit)
6617 .map_err(|e| PyValueError::new_err(e.to_string()))
6618}
6619
6620fn atomic_key(atomic: &BTreeMap<String, f64>, key: &str) -> PyResult<f64> {
6622 atomic
6623 .get(key)
6624 .copied()
6625 .ok_or_else(|| PyValueError::new_err(format!("atomic constants missing `{key}`")))
6626}
6627
6628#[pyfunction]
6637#[pyo3(signature = (atomic, k_conv=None, l_conv=None))]
6638fn spectroscopy_xray_lines(
6639 atomic: BTreeMap<String, f64>,
6640 k_conv: Option<f64>,
6641 l_conv: Option<f64>,
6642) -> PyResult<Vec<(f64, f64)>> {
6643 let data = nucleide_spectroscopy::AtomicData {
6644 k_shell_fluor: atomic_key(&atomic, "k_shell_fluor")?,
6645 l_shell_fluor: atomic_key(&atomic, "l_shell_fluor")?,
6646 prob: atomic_key(&atomic, "prob")?,
6647 kb_to_ka: atomic_key(&atomic, "kb_to_ka")?,
6648 ka2_to_ka1: atomic_key(&atomic, "ka2_to_ka1")?,
6649 ka1_en_kev: atomic_key(&atomic, "ka1_en_kev")?,
6650 ka2_en_kev: atomic_key(&atomic, "ka2_en_kev")?,
6651 kb_en_kev: atomic_key(&atomic, "kb_en_kev")?,
6652 l_en_kev: atomic_key(&atomic, "l_en_kev")?,
6653 };
6654 let present = |v: Option<f64>| v.filter(|x| !x.is_nan());
6656 Ok(
6657 nucleide_spectroscopy::xray_lines(&data, present(k_conv), present(l_conv))
6658 .iter()
6659 .map(|l| (l.energy_kev, l.intensity))
6660 .collect(),
6661 )
6662}
6663
6664#[pyfunction]
6679#[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))]
6680#[allow(clippy::too_many_arguments)]
6681fn spectroscopy_sdef_decay_source(
6682 lines: Vec<(f64, f64)>,
6683 x: f64,
6684 y: f64,
6685 z: f64,
6686 u: f64,
6687 v: f64,
6688 w: f64,
6689 weight: f64,
6690 particle: &str,
6691 version: u32,
6692) -> PyResult<(Vec<(f64, f64)>, String)> {
6693 let particle = particle
6694 .parse::<nucleide_nuclei::particles::ParticleId>()
6695 .map_err(|e| PyValueError::new_err(format!("spectroscopy: particle {e}")))?;
6696 let source = nucleide_spectroscopy::PointSource {
6697 x,
6698 y,
6699 z,
6700 u,
6701 v,
6702 w,
6703 weight,
6704 particle,
6705 };
6706 nucleide_spectroscopy::sdef_card(&lines, &source, version)
6707 .map_err(|e| PyValueError::new_err(e.to_string()))
6708}
6709
6710fn spectrum_to_py(
6712 py: Python<'_>,
6713 spec: &nucleide_spectroscopy::GammaSpectrum,
6714) -> PyResult<Py<PyAny>> {
6715 use pyo3::types::PyDict;
6716 let d = PyDict::new(py);
6717 let s = &spec.spectrum;
6718 d.set_item("spec_name", &s.spec_name)?;
6719 d.set_item("start_chan_num", s.start_chan_num)?;
6720 d.set_item("num_channels", s.num_channels)?;
6721 d.set_item("channels", &s.channels)?;
6722 d.set_item("counts", &s.counts)?;
6723 d.set_item("ebin", &s.ebin)?;
6724 d.set_item("real_time", spec.real_time)?;
6725 d.set_item("live_time", spec.live_time)?;
6726 d.set_item("dead_time", spec.dead_time())?;
6727 d.set_item("det_id", &spec.det_id)?;
6728 d.set_item("det_descp", &spec.det_descp)?;
6729 d.set_item("start_date", &spec.start_date)?;
6730 d.set_item("start_time", &spec.start_time)?;
6731 d.set_item("calib_e_fit", &spec.calib_e_fit)?;
6732 d.set_item("calib_fwhm_fit", &spec.calib_fwhm_fit)?;
6733 d.set_item("file_name", &spec.file_name)?;
6734 Ok(d.into_any().unbind())
6735}
6736
6737#[pyfunction]
6739fn spectroscopy_parse_dollar_spe(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
6740 let spec = nucleide_spectroscopy::parse_dollar_spe(text, "")
6741 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6742 spectrum_to_py(py, &spec)
6743}
6744
6745#[pyfunction]
6747fn spectroscopy_parse_spe(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
6748 let spec = nucleide_spectroscopy::parse_plain_spe(text, "")
6749 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6750 spectrum_to_py(py, &spec)
6751}
6752
6753#[pyfunction]
6755fn spectroscopy_read_dollar_spe(py: Python<'_>, path: &str) -> PyResult<Py<PyAny>> {
6756 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
6757 let spec = nucleide_spectroscopy::parse_dollar_spe(&text, path)
6758 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6759 spectrum_to_py(py, &spec)
6760}
6761
6762#[pyfunction]
6764fn spectroscopy_read_spe(py: Python<'_>, path: &str) -> PyResult<Py<PyAny>> {
6765 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
6766 let spec = nucleide_spectroscopy::parse_plain_spe(&text, path)
6767 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6768 spectrum_to_py(py, &spec)
6769}
6770
6771#[pyfunction]
6775fn spectroscopy_parse_lines_tsv(text: &str) -> PyResult<Vec<(f64, f64)>> {
6776 nucleide_spectroscopy::parse_lines_tsv(text).map_err(|e| PyValueError::new_err(e.to_string()))
6777}
6778
6779#[pyfunction]
6782fn spectroscopy_read_decay_lines(path: &str) -> PyResult<Vec<(f64, f64)>> {
6783 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
6784 nucleide_spectroscopy::parse_lines_tsv(&text).map_err(|e| PyValueError::new_err(e.to_string()))
6785}
6786
6787fn uq_sample_err(e: nucleide_linalg::sample::SampleError) -> PyErr {
6792 PyValueError::new_err(e.to_string())
6793}
6794
6795fn uq_decay_err(e: nucleide_linalg::decay::DecayError) -> PyErr {
6796 PyValueError::new_err(e.to_string())
6797}
6798
6799#[pyfunction]
6807fn uq_sample_mvn(
6808 py: Python<'_>,
6809 mean: Vec<f64>,
6810 cov: Vec<Vec<f64>>,
6811 n: usize,
6812 seed: u64,
6813) -> PyResult<Py<PyAny>> {
6814 use pyo3::types::PyDict;
6815 let set = nucleide_linalg::sample::sample_mvn(&mean, &cov, n, seed).map_err(uq_sample_err)?;
6816 let d = PyDict::new(py);
6817 d.set_item("samples", set.samples)?;
6818 d.set_item("method", set.method.name())?;
6819 match &set.method {
6820 nucleide_linalg::sample::FactorMethod::Cholesky => {
6821 d.set_item("min_eigen", py.None())?;
6822 d.set_item("max_eigen", py.None())?;
6823 }
6824 nucleide_linalg::sample::FactorMethod::EigenClip {
6825 min_eigen,
6826 max_eigen,
6827 } => {
6828 d.set_item("min_eigen", *min_eigen)?;
6829 d.set_item("max_eigen", *max_eigen)?;
6830 }
6831 }
6832 Ok(d.into_any().unbind())
6833}
6834
6835#[pyfunction]
6837fn uq_sample_mean(samples: Vec<Vec<f64>>) -> PyResult<Vec<f64>> {
6838 nucleide_linalg::sample::sample_mean(&samples).map_err(uq_sample_err)
6839}
6840
6841#[pyfunction]
6843fn uq_sample_cov(samples: Vec<Vec<f64>>) -> PyResult<Vec<Vec<f64>>> {
6844 nucleide_linalg::sample::sample_cov(&samples).map_err(uq_sample_err)
6845}
6846
6847#[pyfunction]
6853fn uq_check_convergence(
6854 py: Python<'_>,
6855 mean: Vec<f64>,
6856 cov: Vec<Vec<f64>>,
6857 samples: Vec<Vec<f64>>,
6858 mean_tol: f64,
6859 cov_tol: f64,
6860) -> PyResult<Py<PyAny>> {
6861 use pyo3::types::PyDict;
6862 let rep = nucleide_linalg::sample::check_convergence(&mean, &cov, &samples, mean_tol, cov_tol)
6863 .map_err(uq_sample_err)?;
6864 let d = PyDict::new(py);
6865 d.set_item("mean_err_max", rep.mean_err_max)?;
6866 d.set_item("cov_err_fro", rep.cov_err_fro)?;
6867 d.set_item("mean_tol", rep.mean_tol)?;
6868 d.set_item("cov_tol", rep.cov_tol)?;
6869 d.set_item("passed", rep.passed)?;
6870 Ok(d.into_any().unbind())
6871}
6872
6873#[pyfunction]
6876fn uq_perturb_branches(base: Vec<f64>, rel: Vec<f64>) -> PyResult<Vec<f64>> {
6877 nucleide_linalg::decay::perturb_branches(&base, &rel).map_err(uq_decay_err)
6878}
6879
6880#[pyfunction]
6884fn uq_perturb_energies(base: Vec<f64>, delta: Vec<f64>, convention: &str) -> PyResult<Vec<f64>> {
6885 let conv = nucleide_linalg::sample::PerturbConvention::parse(convention)
6886 .map_err(PyValueError::new_err)?;
6887 nucleide_linalg::decay::perturb_energies(&base, &delta, conv).map_err(uq_decay_err)
6888}
6889
6890#[pyfunction]
6897fn uq_sample_lognormal(
6898 py: Python<'_>,
6899 mean_log: Vec<f64>,
6900 cov: Vec<Vec<f64>>,
6901 n: usize,
6902 seed: u64,
6903) -> PyResult<Py<PyAny>> {
6904 use pyo3::types::PyDict;
6905 let set = nucleide_linalg::sample::sample_lognormal(&mean_log, &cov, n, seed)
6906 .map_err(uq_sample_err)?;
6907 let d = PyDict::new(py);
6908 d.set_item("samples", set.samples)?;
6909 d.set_item("method", set.method.name())?;
6910 match &set.method {
6911 nucleide_linalg::sample::FactorMethod::Cholesky => {
6912 d.set_item("min_eigen", py.None())?;
6913 d.set_item("max_eigen", py.None())?;
6914 }
6915 nucleide_linalg::sample::FactorMethod::EigenClip {
6916 min_eigen,
6917 max_eigen,
6918 } => {
6919 d.set_item("min_eigen", *min_eigen)?;
6920 d.set_item("max_eigen", *max_eigen)?;
6921 }
6922 }
6923 Ok(d.into_any().unbind())
6924}
6925
6926#[pyfunction]
6933fn uq_sample_lhs(
6934 py: Python<'_>,
6935 mean: Vec<f64>,
6936 cov: Vec<Vec<f64>>,
6937 n: usize,
6938 seed: u64,
6939) -> PyResult<Py<PyAny>> {
6940 use pyo3::types::PyDict;
6941 let set = nucleide_linalg::sample::sample_lhs(&mean, &cov, n, seed).map_err(uq_sample_err)?;
6942 let d = PyDict::new(py);
6943 d.set_item("samples", set.samples)?;
6944 d.set_item("method", set.method.name())?;
6945 match &set.method {
6946 nucleide_linalg::sample::FactorMethod::Cholesky => {
6947 d.set_item("min_eigen", py.None())?;
6948 d.set_item("max_eigen", py.None())?;
6949 }
6950 nucleide_linalg::sample::FactorMethod::EigenClip {
6951 min_eigen,
6952 max_eigen,
6953 } => {
6954 d.set_item("min_eigen", *min_eigen)?;
6955 d.set_item("max_eigen", *max_eigen)?;
6956 }
6957 }
6958 Ok(d.into_any().unbind())
6959}
6960
6961#[pyfunction]
6964fn uq_lognormal_mean(mean_log: Vec<f64>, cov: Vec<Vec<f64>>) -> PyResult<Vec<f64>> {
6965 nucleide_linalg::sample::lognormal_mean(&mean_log, &cov).map_err(uq_sample_err)
6966}
6967
6968#[pyfunction]
6971fn uq_lognormal_cov(mean_log: Vec<f64>, cov: Vec<Vec<f64>>) -> PyResult<Vec<Vec<f64>>> {
6972 nucleide_linalg::sample::lognormal_cov(&mean_log, &cov).map_err(uq_sample_err)
6973}
6974
6975#[pyfunction]
6977fn uq_passthrough(delta: Vec<f64>) -> PyResult<Vec<f64>> {
6978 nucleide_linalg::decay::passthrough(&delta).map_err(uq_decay_err)
6979}
6980
6981#[pyfunction]
6984fn uq_perturb_fission_yields(base: Vec<f64>, rel: Vec<f64>) -> PyResult<Vec<f64>> {
6985 nucleide_linalg::decay::perturb_fission_yields(&base, &rel).map_err(uq_decay_err)
6986}
6987
6988fn parse_projectile(flag: &str) -> PyResult<nucleide_nuclei::rxname::Projectile> {
6993 flag.parse::<nucleide_nuclei::rxname::Projectile>()
6994 .map_err(|e| PyValueError::new_err(e.to_string()))
6995}
6996
6997fn resolve_rx_id(spec: &Bound<'_, PyAny>) -> PyResult<u32> {
6998 if let Ok(id) = spec.extract::<u32>() {
6999 return Ok(id);
7000 }
7001 if let Ok(s) = spec.extract::<&str>() {
7002 return nucleide_nuclei::rxname::name_to_id(s)
7003 .map_err(|e| PyValueError::new_err(e.to_string()));
7004 }
7005 Err(PyTypeError::new_err(
7006 "expected reaction id (int) or name (str)",
7007 ))
7008}
7009
7010#[pyfunction]
7012fn rxname_label(id: u32) -> &'static str {
7013 nucleide_nuclei::rxname::label(id)
7014}
7015
7016#[pyfunction]
7018fn rxname_doc(id: u32) -> &'static str {
7019 nucleide_nuclei::rxname::doc(id)
7020}
7021
7022#[pyfunction]
7024fn rxname_reaction(py: Python<'_>, id: u32) -> PyResult<Option<Py<PyAny>>> {
7025 use pyo3::types::PyDict;
7026 Ok(nucleide_nuclei::rxname::reaction(id).map(|r| {
7027 let d = PyDict::new(py);
7028 d.set_item("id", r.id).ok();
7029 d.set_item("name", r.name).ok();
7030 d.set_item("mt", r.mt).ok();
7031 d.set_item("label", r.label).ok();
7032 d.set_item("doc", r.doc).ok();
7033 d.into_any().unbind()
7034 }))
7035}
7036
7037#[pyfunction]
7039#[pyo3(signature = (from_nucid, to_nucid, projectile="n"))]
7040fn rxname_id_from_nucdelta(from_nucid: u32, to_nucid: u32, projectile: &str) -> PyResult<u32> {
7041 let p = parse_projectile(projectile)?;
7042 nucleide_nuclei::rxname::id_from_nucdelta(from_nucid, to_nucid, p)
7043 .map_err(|e| PyValueError::new_err(e.to_string()))
7044}
7045
7046#[pyfunction]
7048#[pyo3(signature = (parent, rx, projectile="n"))]
7049fn rxname_child(parent: &str, rx: &Bound<'_, PyAny>, projectile: &str) -> PyResult<String> {
7050 let p = parse_projectile(projectile)?;
7051 let rx = resolve_rx_id(rx)?;
7052 let parent_id = NuclideId::from_name(parent)
7053 .map_err(|e| PyValueError::new_err(format!("`{parent}`: {e}")))?;
7054 nucleide_nuclei::rxname::child(parent_id, rx, p)
7055 .map(|id| id.to_name())
7056 .map_err(|e| PyValueError::new_err(e.to_string()))
7057}
7058
7059#[pyfunction]
7061#[pyo3(signature = (child, rx, projectile="n"))]
7062fn rxname_parent(child: &str, rx: &Bound<'_, PyAny>, projectile: &str) -> PyResult<String> {
7063 let p = parse_projectile(projectile)?;
7064 let rx = resolve_rx_id(rx)?;
7065 let child_id = NuclideId::from_name(child)
7066 .map_err(|e| PyValueError::new_err(format!("`{child}`: {e}")))?;
7067 nucleide_nuclei::rxname::parent(child_id, rx, p)
7068 .map(|id| id.to_name())
7069 .map_err(|e| PyValueError::new_err(e.to_string()))
7070}
7071
7072#[pyfunction]
7074fn particle_is_valid(spec: &str) -> bool {
7075 nucleide_nuclei::particles::is_valid(spec)
7076}
7077
7078#[pyfunction]
7080fn particle_is_valid_pdc(n: i32) -> bool {
7081 nucleide_nuclei::particles::is_valid_pdc(n)
7082}
7083
7084#[pyfunction]
7086fn particle_is_hydrogen(spec: &str) -> bool {
7087 nucleide_nuclei::particles::is_hydrogen(spec)
7088}
7089
7090#[pyfunction]
7092fn particle_is_heavy_ion(spec: &str) -> bool {
7093 nucleide_nuclei::particles::is_heavy_ion(spec)
7094}
7095
7096#[pyfunction]
7098#[pyo3(signature = (name, source="EPA"))]
7099fn dose_f1(name: &str, source: &str) -> PyResult<Option<f64>> {
7100 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
7101 let s = parse_dose_source(source)?;
7102 Ok(nucleide_nuclei::data::dose_f1_by_name(name, s))
7103}
7104
7105#[pyfunction]
7107#[pyo3(signature = (name, source="EPA"))]
7108fn dose_lung_model(name: &str, source: &str) -> PyResult<Option<char>> {
7109 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
7110 let s = parse_dose_source(source)?;
7111 Ok(nucleide_nuclei::data::dose_lung_model_by_name(name, s))
7112}
7113
7114fn bare_element_z(name: &str) -> Option<u32> {
7116 let t = name.trim();
7117 if t.is_empty() {
7118 return None;
7119 }
7120 let mut chars = t.chars();
7121 let first = chars.next()?.to_uppercase().next()?;
7122 let rest: String = chars.collect::<String>().to_lowercase();
7123 let canon = format!("{first}{rest}");
7124 nucleide_nuclei::element_z(&canon)
7125}
7126
7127fn mat_from_comp_elements(comp: BTreeMap<String, f64>) -> PyResult<nucleide_material::Material> {
7128 let mut mat = nucleide_material::Material::new();
7129 for (name, grams) in &comp {
7130 let id = match NuclideId::from_name(name) {
7131 Ok(id) => id,
7132 Err(_) => match bare_element_z(name) {
7133 Some(z) => NuclideId::from_nucid(z * 10_000_000),
7134 None => {
7135 return Err(PyValueError::new_err(format!(
7136 "`{name}`: unknown nuclide or element"
7137 )));
7138 }
7139 },
7140 };
7141 mat.add_nuclide(id, *grams);
7142 }
7143 Ok(mat)
7144}
7145
7146fn mat_to_comp_elements(mat: &nucleide_material::Material) -> BTreeMap<String, f64> {
7147 let mut out = BTreeMap::new();
7148 for (&id, &grams) in &mat.comp {
7149 let key = if id.a() == 0 && id.state() == 0 {
7150 nucleide_nuclei::element_symbol(id.z())
7151 .unwrap_or("X")
7152 .to_string()
7153 } else {
7154 id.to_name()
7155 };
7156 *out.entry(key).or_insert(0.0) += grams;
7157 }
7158 out
7159}
7160
7161#[pyfunction]
7165fn mix_by_mass(parts: Vec<(BTreeMap<String, f64>, f64)>) -> PyResult<BTreeMap<String, f64>> {
7166 let mats: Vec<nucleide_material::Material> = parts
7167 .iter()
7168 .map(|(comp, _)| mat_from_comp_elements(comp.clone()))
7169 .collect::<PyResult<_>>()?;
7170 let refs: Vec<(&nucleide_material::Material, f64)> =
7171 mats.iter().zip(parts.iter().map(|(_, w)| *w)).collect();
7172 let out = nucleide_material::Material::mix_by_mass(&refs)
7173 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7174 Ok(mat_to_comp_elements(&out))
7175}
7176
7177#[pyfunction]
7181fn mix_by_volume(parts: Vec<(BTreeMap<String, f64>, f64, f64)>) -> PyResult<BTreeMap<String, f64>> {
7182 let mut mats: Vec<nucleide_material::Material> = Vec::with_capacity(parts.len());
7183 for (comp, _, density) in &parts {
7184 let mut m = mat_from_comp_elements(comp.clone())?;
7185 m.set_density(Some(*density));
7186 mats.push(m);
7187 }
7188 let refs: Vec<(&nucleide_material::Material, f64)> =
7189 mats.iter().zip(parts.iter().map(|(_, v, _)| *v)).collect();
7190 let out = nucleide_material::Material::mix_by_volume(&refs)
7191 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7192 Ok(mat_to_comp_elements(&out))
7193}
7194
7195#[pyfunction]
7197fn specific_activity(comp: BTreeMap<String, f64>) -> PyResult<f64> {
7198 let mat = mat_from_comp_elements(comp)?;
7199 let analytics = nucleide_material::Analytics {
7200 masses: &nucleide_material::Ame2020,
7201 decays: &nucleide_material::ChainDecays,
7202 };
7203 mat.specific_activity(&analytics)
7204 .map_err(|e| PyValueError::new_err(e.to_string()))
7205}
7206
7207#[pyfunction]
7211#[pyo3(signature = (entries, cross_sections=None))]
7212fn materials_doc_to_xml(
7213 entries: Vec<(String, BTreeMap<String, f64>, f64)>,
7214 cross_sections: Option<String>,
7215) -> PyResult<String> {
7216 let mut doc = nucleide_material::MaterialsDoc::new();
7217 if let Some(path) = cross_sections {
7218 doc = doc.cross_sections(path);
7219 }
7220 for (name, comp, density) in entries {
7221 let mut mat = mat_from_comp_elements(comp)?;
7222 mat.set_density(Some(density));
7223 doc = doc.push(name, mat);
7224 }
7225 doc.to_xml()
7226 .map_err(|e| PyValueError::new_err(e.to_string()))
7227}
7228
7229#[pyfunction]
7233fn expand_elements(comp: BTreeMap<String, f64>) -> PyResult<BTreeMap<String, f64>> {
7234 let mut mat = mat_from_comp_elements(comp)?;
7235 mat.expand_elements(
7236 &nucleide_material::Ame2020,
7237 &nucleide_material::NaturalAbundances,
7238 )
7239 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7240 Ok(mat_to_comp_elements(&mat))
7241}
7242
7243#[pyfunction]
7245fn collapse_elements(comp: BTreeMap<String, f64>) -> PyResult<BTreeMap<String, f64>> {
7246 let mat = mat_from_comp_elements(comp)?;
7247 Ok(mat_to_comp_elements(&mat.collapse_elements()))
7248}
7249
7250fn parse_fluka_nuc(spec: &str) -> PyResult<nucleide_fluka_io::material::FlukaNuc> {
7251 use nucleide_fluka_io::material::FlukaNuc;
7252 if let Ok(id) = NuclideId::from_name(spec) {
7253 return Ok(FlukaNuc::Nuclide(id));
7254 }
7255 if let Some(z) = bare_element_z(spec) {
7256 return Ok(FlukaNuc::Element(z));
7257 }
7258 if let Ok(z) = spec.trim().parse::<u32>() {
7259 if nucleide_nuclei::element_symbol(z).is_some() {
7260 return Ok(FlukaNuc::Element(z));
7261 }
7262 }
7263 Err(PyValueError::new_err(format!(
7264 "`{spec}`: unknown nuclide or element"
7265 )))
7266}
7267
7268#[pyfunction]
7270fn fluka_material_str(fid: u32, nuc: &str, density: f64) -> PyResult<String> {
7271 let parsed = parse_fluka_nuc(nuc)?;
7272 nucleide_fluka_io::material::material_str(fid, parsed, density)
7273 .map_err(|e| PyValueError::new_err(e.to_string()))
7274}
7275
7276#[pyfunction]
7280#[pyo3(signature = (fid, compound_name, density, frac_type="mass", components=None))]
7281fn fluka_compound_str(
7282 fid: u32,
7283 compound_name: &str,
7284 density: f64,
7285 frac_type: &str,
7286 components: Option<Vec<(String, f64)>>,
7287) -> PyResult<String> {
7288 use nucleide_fluka_io::material::{Component, FracType};
7289 let frac = match frac_type.trim().to_ascii_lowercase().as_str() {
7290 "mass" => FracType::Mass,
7291 "atom" => FracType::Atom,
7292 other => {
7293 return Err(PyValueError::new_err(format!(
7294 "frac_type must be mass|atom, got `{other}`"
7295 )));
7296 }
7297 };
7298 let pairs = components.unwrap_or_default();
7299 let comps: Vec<Component> = pairs
7300 .iter()
7301 .map(|(nuc, frac)| parse_fluka_nuc(nuc).map(|n| Component::new(n, *frac)))
7302 .collect::<PyResult<_>>()?;
7303 nucleide_fluka_io::material::compound_str(fid, compound_name, density, frac, &comps)
7304 .map_err(|e| PyValueError::new_err(e.to_string()))
7305}
7306
7307#[pyfunction]
7309fn fluka_builtin_set() -> Vec<String> {
7310 let mut out: Vec<String> = nucleide_fluka_io::material::builtin_set()
7311 .into_iter()
7312 .map(str::to_string)
7313 .collect();
7314 out.sort();
7315 out
7316}
7317
7318#[pyfunction]
7320fn alara_validate_deck(text: &str) -> PyResult<()> {
7321 let deck = nucleide_alara_io::AlaraDeck::parse(text)
7322 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7323 deck.validate()
7324 .map_err(|e| PyValueError::new_err(e.to_string()))
7325}
7326
7327#[pyfunction]
7329fn alara_check_block(block: &str, line: usize) -> PyResult<()> {
7330 nucleide_alara_io::AlaraDeck::check_block(block, line)
7331 .map_err(|e| PyValueError::new_err(e.to_string()))
7332}
7333
7334#[pyfunction]
7336fn alara_flux_total(name: &str, text: &str) -> PyResult<f64> {
7337 nucleide_alara_io::FluxSpec::parse(name, text)
7338 .map(|f| f.total())
7339 .map_err(|e| PyValueError::new_err(e.to_string()))
7340}
7341
7342#[pyfunction]
7344fn alara_flux_len(name: &str, text: &str) -> PyResult<usize> {
7345 nucleide_alara_io::FluxSpec::parse(name, text)
7346 .map(|f| f.len())
7347 .map_err(|e| PyValueError::new_err(e.to_string()))
7348}
7349
7350#[pyfunction]
7352fn alara_output_totals(
7353 py: Python<'_>,
7354 text: &str,
7355 run_lbl: &str,
7356) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
7357 let owned_text = text.to_owned();
7358 let owned_lbl = run_lbl.to_owned();
7359 let frame = py
7360 .detach(move || {
7361 nucleide_alara_io::output::ResponseFrame::parse(&owned_text, &owned_lbl)
7362 .map(|f| f.totals())
7363 })
7364 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7365 Ok(frame
7366 .rows
7367 .iter()
7368 .map(|r| fispact_row_to_map(py, r))
7369 .collect())
7370}
7371
7372#[pyfunction]
7374fn alara_output_total_activity(text: &str, run_lbl: &str) -> PyResult<f64> {
7375 nucleide_alara_io::output::ResponseFrame::parse(text, run_lbl)
7376 .map(|f| f.total_activity())
7377 .map_err(|e| PyValueError::new_err(e.to_string()))
7378}
7379
7380#[pyfunction]
7382fn alara_photon_total_strength(text: &str) -> PyResult<f64> {
7383 nucleide_alara_io::PhotonSource::from_str(text)
7384 .map(|p| p.total_strength())
7385 .map_err(|e| PyValueError::new_err(e.to_string()))
7386}
7387
7388#[pyfunction]
7390#[pyo3(signature = (deck_text, top=None))]
7391fn alara_schedule_total_time(deck_text: &str, top: Option<&str>) -> PyResult<f64> {
7392 let owned = deck_text.to_owned();
7393 let owned_top = top.map(str::to_owned);
7394 let steps =
7395 expand_deck_schedules(&owned, owned_top.as_deref()).map_err(PyValueError::new_err)?;
7396 Ok(nucleide_alara_io::schedule::total_time(&steps))
7397}
7398
7399#[pyfunction]
7401fn origen_tape6_find(py: Python<'_>, text: &str, nuclide: &str) -> PyResult<Option<Py<PyAny>>> {
7402 use pyo3::types::PyDict;
7403 let owned = text.to_owned();
7404 let query = nuclide.to_owned();
7405 let found = py
7406 .detach(move || nucleide_origen_io::Tape6::parse(&owned).map(|t| t.find(&query).cloned()))
7407 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7408 Ok(found.map(|r| {
7409 let d = PyDict::new(py);
7410 d.set_item("nuclide", &r.nuclide).ok();
7411 d.set_item("grams", r.grams).ok();
7412 d.set_item("activity_bq", r.activity_bq).ok();
7413 d.into_any().unbind()
7414 }))
7415}
7416
7417#[pyfunction]
7419fn origen_tape6_total_activity(text: &str) -> PyResult<f64> {
7420 nucleide_origen_io::Tape6::parse(text)
7421 .map(|t| t.total_activity())
7422 .map_err(|e| PyValueError::new_err(e.to_string()))
7423}
7424
7425#[pyfunction]
7427fn origen_tape9_find(py: Python<'_>, text: &str, nuclide: &str) -> PyResult<Option<Py<PyAny>>> {
7428 use pyo3::types::PyDict;
7429 let owned = text.to_owned();
7430 let query = nuclide.to_owned();
7431 let found = py
7432 .detach(move || {
7433 nucleide_origen_io::Tape9Entry::parse(&owned)
7434 .map(|entries| nucleide_origen_io::Tape9Entry::find(&entries, &query).cloned())
7435 })
7436 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7437 Ok(found.map(|e| {
7438 let d = PyDict::new(py);
7439 d.set_item("nuclide", &e.nuclide).ok();
7440 d.set_item("decay_const", e.decay_const).ok();
7441 d.into_any().unbind()
7442 }))
7443}
7444
7445#[pyfunction]
7447#[pyo3(signature = (text, kind="rtflux"))]
7448fn cccc_rtflux_npoints(text: &str, kind: &str) -> PyResult<usize> {
7449 let flux_kind = parse_flux_kind(kind)?;
7450 nucleide_cccc_io::FluxFile::parse(flux_kind, text)
7451 .map(|f| f.npoints())
7452 .map_err(|e| PyValueError::new_err(e.to_string()))
7453}
7454
7455#[pyfunction]
7457#[pyo3(signature = (text, kind="rtflux", index=0))]
7458fn cccc_rtflux_point(text: &str, kind: &str, index: usize) -> PyResult<Option<Vec<f64>>> {
7459 let flux_kind = parse_flux_kind(kind)?;
7460 let flux = nucleide_cccc_io::FluxFile::parse(flux_kind, text)
7461 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7462 Ok(flux.point(index).map(<[f64]>::to_vec))
7463}
7464
7465#[pyfunction]
7467#[pyo3(signature = (text, kind="rtflux"))]
7468fn cccc_rtflux_total(text: &str, kind: &str) -> PyResult<f64> {
7469 let flux_kind = parse_flux_kind(kind)?;
7470 nucleide_cccc_io::FluxFile::parse(flux_kind, text)
7471 .map(|f| f.total())
7472 .map_err(|e| PyValueError::new_err(e.to_string()))
7473}
7474
7475fn parse_flux_kind(kind: &str) -> PyResult<nucleide_cccc_io::rtflux::FluxKind> {
7476 match kind.to_ascii_lowercase().as_str() {
7477 "rtflux" => Ok(nucleide_cccc_io::rtflux::FluxKind::Rtflux),
7478 "atflux" => Ok(nucleide_cccc_io::rtflux::FluxKind::Atflux),
7479 "rzflux" => Ok(nucleide_cccc_io::rtflux::FluxKind::Rzflux),
7480 other => Err(PyValueError::new_err(format!(
7481 "kind must be rtflux|atflux|rzflux, got `{other}`"
7482 ))),
7483 }
7484}
7485
7486#[pyfunction]
7488fn cccc_isotxs_find(py: Python<'_>, text: &str, label: &str) -> PyResult<Option<Py<PyAny>>> {
7489 use pyo3::types::PyDict;
7490 let owned = text.to_owned();
7491 let query = label.to_owned();
7492 let found = py
7493 .detach(move || {
7494 nucleide_cccc_io::IsotxsLib::parse(&owned).map(|lib| lib.find(&query).cloned())
7495 })
7496 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7497 Ok(found.map(|n| {
7498 let d = PyDict::new(py);
7499 d.set_item("label", &n.label).ok();
7500 d.set_item("zaid", &n.zaid).ok();
7501 d.set_item("groups", n.groups).ok();
7502 d.set_item("total_xs", n.total_xs.clone()).ok();
7503 d.into_any().unbind()
7504 }))
7505}
7506
7507#[pyfunction]
7509fn cccc_isotxs_len(text: &str) -> PyResult<usize> {
7510 nucleide_cccc_io::IsotxsLib::parse(text)
7511 .map(|lib| lib.len())
7512 .map_err(|e| PyValueError::new_err(e.to_string()))
7513}
7514
7515#[pyfunction]
7517fn fispact_is_output(path: &str) -> bool {
7518 nucleide_fispact_io::is_fispact_output(path)
7519}
7520
7521#[pyfunction]
7523fn enrichment_prod_per_feed(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
7524 nucleide_enrichment::prod_per_feed(x_feed, x_prod, x_tail)
7525}
7526
7527#[pyfunction]
7529fn enrichment_tail_per_feed(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
7530 nucleide_enrichment::tail_per_feed(x_feed, x_prod, x_tail)
7531}
7532
7533#[pyfunction]
7535fn enrichment_tail_per_prod(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
7536 nucleide_enrichment::tail_per_prod(x_feed, x_prod, x_tail)
7537}
7538
7539#[pyfunction]
7541fn enrichment_feed_per_prod(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
7542 nucleide_enrichment::feed_per_prod(x_feed, x_prod, x_tail)
7543}
7544
7545#[pyfunction]
7547fn enrichment_feed_per_tail(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
7548 nucleide_enrichment::feed_per_tail(x_feed, x_prod, x_tail)
7549}
7550
7551#[pyfunction]
7553fn enrichment_prod_per_tail(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
7554 nucleide_enrichment::prod_per_tail(x_feed, x_prod, x_tail)
7555}
7556
7557#[pyfunction]
7559#[allow(non_snake_case)]
7560fn enrichment_alphastar_i(alpha: f64, Mstar: f64, M_i: f64) -> f64 {
7561 nucleide_enrichment::alphastar_i(alpha, Mstar, M_i)
7562}
7563
7564#[pyfunction]
7571fn kinetics_from_ifp(
7572 py: Python<'_>,
7573 betas: Vec<f64>,
7574 lambda_gen: f64,
7575 lambdas: Vec<f64>,
7576) -> PyResult<Py<PyAny>> {
7577 use pyo3::types::PyDict;
7578 let params = nucleide_kinetics::KineticParams::from_ifp(betas, lambda_gen, lambdas)
7579 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7580 let d = PyDict::new(py);
7581 d.set_item("betas", params.betas()).ok();
7582 d.set_item("lambdas", params.lambdas()).ok();
7583 d.set_item("lambda_gen", params.lambda_gen()).ok();
7584 d.set_item("beta_total", params.beta_total()).ok();
7585 d.set_item("groups", params.groups()).ok();
7586 Ok(d.into_any().unbind())
7587}
7588
7589#[pyfunction]
7592#[pyo3(signature = (tally, selection="total", tolerance=0.5, null_value=0.0))]
7593fn magic_with(
7594 tally: &PyMeshTally,
7595 selection: &str,
7596 tolerance: f64,
7597 null_value: f64,
7598) -> PyResult<PyMagicOutput> {
7599 let sel = match selection.trim().to_ascii_lowercase().as_str() {
7600 "total" => nucleide_vr_tools::magic::MagicSelection::Total,
7601 "per_group" | "pergroup" | "per-group" => {
7602 nucleide_vr_tools::magic::MagicSelection::PerGroup
7603 }
7604 other => {
7605 return Err(PyValueError::new_err(format!(
7606 "selection must be total|per_group, got `{other}`"
7607 )));
7608 }
7609 };
7610 let params = nucleide_vr_tools::magic::MagicParams {
7611 tolerance,
7612 null_value,
7613 };
7614 nucleide_vr_tools::magic::magic_with(&tally.inner, sel, params)
7615 .map(|inner| PyMagicOutput { inner })
7616 .map_err(|e| PyValueError::new_err(e.to_string()))
7617}
7618
7619#[pyfunction]
7621fn mcpl_statsum_validate(comment: &str) -> PyResult<String> {
7622 nucleide_mcpl_io::statsum_validate(comment)
7623 .map(str::to_string)
7624 .map_err(PyValueError::new_err)
7625}
7626
7627#[pyfunction]
7629fn mcpl_statsum_comment(key: &str, value: f64) -> PyResult<String> {
7630 nucleide_mcpl_io::statsum_comment(key, value).map_err(|e| PyValueError::new_err(e.to_string()))
7631}
7632
7633#[pymodule]
7635fn _internal(m: &Bound<'_, PyModule>) -> PyResult<()> {
7636 m.add_function(wrap_pyfunction!(version, m)?)?;
7637 m.add_function(wrap_pyfunction!(from_zaid, m)?)?;
7638 m.add_function(wrap_pyfunction!(atomic_mass, m)?)?;
7639 m.add_function(wrap_pyfunction!(natural_abundance, m)?)?;
7640 m.add_function(wrap_pyfunction!(rxname_id, m)?)?;
7641 m.add_function(wrap_pyfunction!(rxname_name, m)?)?;
7642 m.add_function(wrap_pyfunction!(rxname_mt, m)?)?;
7643 m.add_function(wrap_pyfunction!(rxname_label, m)?)?;
7644 m.add_function(wrap_pyfunction!(rxname_doc, m)?)?;
7645 m.add_function(wrap_pyfunction!(rxname_reaction, m)?)?;
7646 m.add_function(wrap_pyfunction!(rxname_id_from_nucdelta, m)?)?;
7647 m.add_function(wrap_pyfunction!(rxname_child, m)?)?;
7648 m.add_function(wrap_pyfunction!(rxname_parent, m)?)?;
7649 m.add_function(wrap_pyfunction!(particle_is_valid, m)?)?;
7650 m.add_function(wrap_pyfunction!(particle_is_valid_pdc, m)?)?;
7651 m.add_function(wrap_pyfunction!(particle_is_hydrogen, m)?)?;
7652 m.add_function(wrap_pyfunction!(particle_is_heavy_ion, m)?)?;
7653 m.add_function(wrap_pyfunction!(dose_f1, m)?)?;
7654 m.add_function(wrap_pyfunction!(dose_lung_model, m)?)?;
7655 m.add_function(wrap_pyfunction!(read_xsdir, m)?)?;
7656 m.add_function(wrap_pyfunction!(read_meshtal, m)?)?;
7657 m.add_function(wrap_pyfunction!(read_wwinp, m)?)?;
7658 m.add_function(wrap_pyfunction!(read_mctal, m)?)?;
7659 m.add_function(wrap_pyfunction!(read_ssw, m)?)?;
7660 m.add_function(wrap_pyfunction!(read_ptrac, m)?)?;
7661 m.add_function(wrap_pyfunction!(read_mcpl, m)?)?;
7662 m.add_function(wrap_pyfunction!(write_mcpl, m)?)?;
7663 m.add_function(wrap_pyfunction!(ssw2mcpl, m)?)?;
7664 m.add_function(wrap_pyfunction!(mcpl2ssw, m)?)?;
7665 m.add_function(wrap_pyfunction!(read_endl, m)?)?;
7666 m.add_function(wrap_pyfunction!(endl_endftod, m)?)?;
7667 m.add_function(wrap_pyfunction!(combine_ssw_files, m)?)?;
7668 m.add_function(wrap_pyfunction!(read_chain, m)?)?;
7669 m.add_function(wrap_pyfunction!(build_depletion_system, m)?)?;
7670 m.add_function(wrap_pyfunction!(deplete, m)?)?;
7671 m.add_function(wrap_pyfunction!(deplete_series, m)?)?;
7672 m.add_function(wrap_pyfunction!(simple_xs, m)?)?;
7673 m.add_function(wrap_pyfunction!(scattering_length, m)?)?;
7674 m.add_function(wrap_pyfunction!(decay_energy, m)?)?;
7675 m.add_function(wrap_pyfunction!(decay_branches, m)?)?;
7676 m.add_function(wrap_pyfunction!(decay_branch_fraction, m)?)?;
7677 m.add_function(wrap_pyfunction!(fission_yields, m)?)?;
7678 m.add_function(wrap_pyfunction!(fission_yield, m)?)?;
7679 m.add_function(wrap_pyfunction!(normalize_nuclide, m)?)?;
7680 m.add_function(wrap_pyfunction!(decay_heat, m)?)?;
7681 m.add_function(wrap_pyfunction!(dose_factor, m)?)?;
7682 m.add_function(wrap_pyfunction!(dose_per_g, m)?)?;
7683 m.add_function(wrap_pyfunction!(parse_fgr15_table, m)?)?;
7684 m.add_function(wrap_pyfunction!(fgr15_age_index, m)?)?;
7685 m.add_function(wrap_pyfunction!(mix_by_mass, m)?)?;
7686 m.add_function(wrap_pyfunction!(mix_by_volume, m)?)?;
7687 m.add_function(wrap_pyfunction!(specific_activity, m)?)?;
7688 m.add_function(wrap_pyfunction!(materials_doc_to_xml, m)?)?;
7689 m.add_function(wrap_pyfunction!(expand_elements, m)?)?;
7690 m.add_function(wrap_pyfunction!(collapse_elements, m)?)?;
7691 m.add_function(wrap_pyfunction!(read_serpent, m)?)?;
7692 m.add_function(wrap_pyfunction!(read_usrbin, m)?)?;
7693 m.add_function(wrap_pyfunction!(fluka_material_str, m)?)?;
7694 m.add_function(wrap_pyfunction!(fluka_compound_str, m)?)?;
7695 m.add_function(wrap_pyfunction!(fluka_builtin_set, m)?)?;
7696 m.add_function(wrap_pyfunction!(magic, m)?)?;
7697 m.add_function(wrap_pyfunction!(magic_with, m)?)?;
7698 m.add_function(wrap_pyfunction!(write_ssw, m)?)?;
7699 m.add_function(wrap_pyfunction!(mesh_to_geom, m)?)?;
7700 m.add_function(wrap_pyfunction!(half_life, m)?)?;
7701 m.add_function(wrap_pyfunction!(decay_constant, m)?)?;
7702 m.add_function(wrap_pyfunction!(q_value_capture, m)?)?;
7703 m.add_function(wrap_pyfunction!(q_value_alpha, m)?)?;
7704 m.add_function(wrap_pyfunction!(read_inp, m)?)?;
7705 m.add_function(wrap_pyfunction!(from_formula, m)?)?;
7706 m.add_function(wrap_pyfunction!(activity, m)?)?;
7707 m.add_function(wrap_pyfunction!(to_xml, m)?)?;
7708 m.add_function(wrap_pyfunction!(alara_parse_deck, m)?)?;
7709 m.add_function(wrap_pyfunction!(alara_parse_flux, m)?)?;
7710 m.add_function(wrap_pyfunction!(alara_parse_output, m)?)?;
7711 m.add_function(wrap_pyfunction!(alara_expand_schedule, m)?)?;
7712 m.add_function(wrap_pyfunction!(alara_validate_deck, m)?)?;
7713 m.add_function(wrap_pyfunction!(alara_check_block, m)?)?;
7714 m.add_function(wrap_pyfunction!(alara_flux_total, m)?)?;
7715 m.add_function(wrap_pyfunction!(alara_flux_len, m)?)?;
7716 m.add_function(wrap_pyfunction!(alara_output_totals, m)?)?;
7717 m.add_function(wrap_pyfunction!(alara_output_total_activity, m)?)?;
7718 m.add_function(wrap_pyfunction!(alara_photon_total_strength, m)?)?;
7719 m.add_function(wrap_pyfunction!(alara_schedule_total_time, m)?)?;
7720 m.add_function(wrap_pyfunction!(isotxs_parse, m)?)?;
7721 m.add_function(wrap_pyfunction!(rtflux_parse, m)?)?;
7722 m.add_function(wrap_pyfunction!(cccc_rtflux_npoints, m)?)?;
7723 m.add_function(wrap_pyfunction!(cccc_rtflux_point, m)?)?;
7724 m.add_function(wrap_pyfunction!(cccc_rtflux_total, m)?)?;
7725 m.add_function(wrap_pyfunction!(cccc_isotxs_find, m)?)?;
7726 m.add_function(wrap_pyfunction!(cccc_isotxs_len, m)?)?;
7727 m.add_function(wrap_pyfunction!(partisn_render, m)?)?;
7728 m.add_function(wrap_pyfunction!(partisn_validate, m)?)?;
7729 m.add_function(wrap_pyfunction!(fispact_parse_output, m)?)?;
7730 m.add_function(wrap_pyfunction!(fispact_is_output, m)?)?;
7731 m.add_function(wrap_pyfunction!(origen_parse_tape5, m)?)?;
7732 m.add_function(wrap_pyfunction!(origen_parse_tape6, m)?)?;
7733 m.add_function(wrap_pyfunction!(origen_parse_tape9, m)?)?;
7734 m.add_function(wrap_pyfunction!(origen_tape6_find, m)?)?;
7735 m.add_function(wrap_pyfunction!(origen_tape6_total_activity, m)?)?;
7736 m.add_function(wrap_pyfunction!(origen_tape9_find, m)?)?;
7737 m.add_function(wrap_pyfunction!(r2s_from_deck, m)?)?;
7738 m.add_function(wrap_pyfunction!(r2s_from_snapshot, m)?)?;
7739 m.add_function(wrap_pyfunction!(r2s_validate, m)?)?;
7740 m.add_function(wrap_pyfunction!(r2s_expand, m)?)?;
7741 m.add_function(wrap_pyfunction!(r2s_assemble, m)?)?;
7742 m.add_function(wrap_pyfunction!(r2s_tag_zone_strength, m)?)?;
7743 m.add_function(wrap_pyfunction!(r2s_photon_group_sums, m)?)?;
7744 m.add_function(wrap_pyfunction!(r2s_snapshot_inventory, m)?)?;
7745 m.add_function(wrap_pyfunction!(r2s_expand_sweep, m)?)?;
7746 m.add_function(wrap_pyfunction!(kinetics_solve, m)?)?;
7747 m.add_function(wrap_pyfunction!(kinetics_equilibrium, m)?)?;
7748 m.add_function(wrap_pyfunction!(kinetics_initial_rate, m)?)?;
7749 m.add_function(wrap_pyfunction!(kinetics_inhour_rho, m)?)?;
7750 m.add_function(wrap_pyfunction!(kinetics_stable_period, m)?)?;
7751 m.add_function(wrap_pyfunction!(kinetics_prompt_jump, m)?)?;
7752 m.add_function(wrap_pyfunction!(kinetics_from_ifp, m)?)?;
7753 m.add_function(wrap_pyfunction!(tritium_steady, m)?)?;
7754 m.add_function(wrap_pyfunction!(tritium_transient, m)?)?;
7755 m.add_function(wrap_pyfunction!(tritium_time_lag, m)?)?;
7756 m.add_function(wrap_pyfunction!(tritium_breakthrough, m)?)?;
7757 m.add_function(wrap_pyfunction!(tritium_oriani, m)?)?;
7758 m.add_function(wrap_pyfunction!(tritium_langmuir, m)?)?;
7759 m.add_function(wrap_pyfunction!(tritium_irreversible_fill, m)?)?;
7760 m.add_function(wrap_pyfunction!(tritium_sieverts, m)?)?;
7761 m.add_function(wrap_pyfunction!(tritium_recombination_rate, m)?)?;
7762 m.add_function(wrap_pyfunction!(spectroscopy_rect_smooth, m)?)?;
7763 m.add_function(wrap_pyfunction!(spectroscopy_five_point_smooth, m)?)?;
7764 m.add_function(wrap_pyfunction!(spectroscopy_calc_bg, m)?)?;
7765 m.add_function(wrap_pyfunction!(spectroscopy_gross_count, m)?)?;
7766 m.add_function(wrap_pyfunction!(spectroscopy_net_counts, m)?)?;
7767 m.add_function(wrap_pyfunction!(spectroscopy_energy_bins, m)?)?;
7768 m.add_function(wrap_pyfunction!(spectroscopy_detector_efficiency, m)?)?;
7769 m.add_function(wrap_pyfunction!(spectroscopy_fit_efficiency, m)?)?;
7770 m.add_function(wrap_pyfunction!(spectroscopy_xray_lines, m)?)?;
7771 m.add_function(wrap_pyfunction!(spectroscopy_sdef_decay_source, m)?)?;
7772 m.add_function(wrap_pyfunction!(spectroscopy_parse_dollar_spe, m)?)?;
7773 m.add_function(wrap_pyfunction!(spectroscopy_parse_spe, m)?)?;
7774 m.add_function(wrap_pyfunction!(spectroscopy_read_dollar_spe, m)?)?;
7775 m.add_function(wrap_pyfunction!(spectroscopy_read_spe, m)?)?;
7776 m.add_function(wrap_pyfunction!(spectroscopy_parse_lines_tsv, m)?)?;
7777 m.add_function(wrap_pyfunction!(spectroscopy_read_decay_lines, m)?)?;
7778 m.add_function(wrap_pyfunction!(uq_sample_mvn, m)?)?;
7779 m.add_function(wrap_pyfunction!(uq_sample_lhs, m)?)?;
7780 m.add_function(wrap_pyfunction!(uq_sample_lognormal, m)?)?;
7781 m.add_function(wrap_pyfunction!(uq_lognormal_mean, m)?)?;
7782 m.add_function(wrap_pyfunction!(uq_lognormal_cov, m)?)?;
7783 m.add_function(wrap_pyfunction!(uq_sample_mean, m)?)?;
7784 m.add_function(wrap_pyfunction!(uq_sample_cov, m)?)?;
7785 m.add_function(wrap_pyfunction!(uq_check_convergence, m)?)?;
7786 m.add_function(wrap_pyfunction!(uq_perturb_branches, m)?)?;
7787 m.add_function(wrap_pyfunction!(uq_perturb_energies, m)?)?;
7788 m.add_function(wrap_pyfunction!(uq_passthrough, m)?)?;
7789 m.add_function(wrap_pyfunction!(uq_perturb_fission_yields, m)?)?;
7790 m.add_function(wrap_pyfunction!(parse_deck, m)?)?;
7791 m.add_function(wrap_pyfunction!(read_deck, m)?)?;
7792 m.add_function(wrap_pyfunction!(parse_sdef, m)?)?;
7793 m.add_function(wrap_pyfunction!(parse_csg_to_openmc, m)?)?;
7794 m.add_function(wrap_pyfunction!(read_csg_to_openmc, m)?)?;
7795 m.add_function(wrap_pyfunction!(parse_csg_to_serpent, m)?)?;
7796 m.add_function(wrap_pyfunction!(read_csg_to_serpent, m)?)?;
7797 m.add_function(wrap_pyfunction!(parse_csg_to_phits, m)?)?;
7798 m.add_function(wrap_pyfunction!(read_csg_to_phits, m)?)?;
7799 m.add_function(wrap_pyfunction!(cumulative_decays, m)?)?;
7800 m.add_function(wrap_pyfunction!(progeny, m)?)?;
7801 m.add_function(wrap_pyfunction!(branching_fraction, m)?)?;
7802 m.add_function(wrap_pyfunction!(decay_mode, m)?)?;
7803 m.add_function(wrap_pyfunction!(chain_edges, m)?)?;
7804 m.add_function(wrap_pyfunction!(armi_to_nucid, m)?)?;
7805 m.add_function(wrap_pyfunction!(nucid_to_armi, m)?)?;
7806 m.add_function(wrap_pyfunction!(mcc3_to_nucid, m)?)?;
7807 m.add_function(wrap_pyfunction!(check_labels, m)?)?;
7808 m.add_function(wrap_pyfunction!(audit_material, m)?)?;
7809 m.add_function(wrap_pyfunction!(separate_material, m)?)?;
7810 m.add_function(wrap_pyfunction!(blend_material, m)?)?;
7811 m.add_function(wrap_pyfunction!(enrichment_value_func, m)?)?;
7812 m.add_function(wrap_pyfunction!(enrichment_swu_per_feed, m)?)?;
7813 m.add_function(wrap_pyfunction!(enrichment_swu_per_prod, m)?)?;
7814 m.add_function(wrap_pyfunction!(enrichment_swu_per_tail, m)?)?;
7815 m.add_function(wrap_pyfunction!(enrichment_prod_per_feed, m)?)?;
7816 m.add_function(wrap_pyfunction!(enrichment_tail_per_feed, m)?)?;
7817 m.add_function(wrap_pyfunction!(enrichment_tail_per_prod, m)?)?;
7818 m.add_function(wrap_pyfunction!(enrichment_feed_per_prod, m)?)?;
7819 m.add_function(wrap_pyfunction!(enrichment_feed_per_tail, m)?)?;
7820 m.add_function(wrap_pyfunction!(enrichment_prod_per_tail, m)?)?;
7821 m.add_function(wrap_pyfunction!(enrichment_alphastar_i, m)?)?;
7822 m.add_function(wrap_pyfunction!(mcpl_statsum_validate, m)?)?;
7823 m.add_function(wrap_pyfunction!(mcpl_statsum_comment, m)?)?;
7824 m.add_class::<PyCusum>()?;
7825 m.add_function(wrap_pyfunction!(emit_cards, m)?)?;
7826 m.add_function(wrap_pyfunction!(emit_drift_table, m)?)?;
7827 m.add_function(wrap_pyfunction!(emit_armi_cards, m)?)?;
7828 m.add_function(wrap_pyfunction!(emit_armi_drift_table, m)?)?;
7829 m.add_class::<PyNuclide>()?;
7830 m.add_class::<PyParticle>()?;
7831 m.add_class::<PyXsdir>()?;
7832 m.add_class::<PyXsdirTable>()?;
7833 m.add_class::<PyMeshtal>()?;
7834 m.add_class::<PyMeshTally>()?;
7835 m.add_class::<PyWwinp>()?;
7836 m.add_class::<PyMctal>()?;
7837 m.add_class::<PySurfSrc>()?;
7838 m.add_class::<PyPtracFile>()?;
7839 m.add_class::<PyMcplFile>()?;
7840 m.add_class::<PyEndlLibrary>()?;
7841 m.add_class::<PyChain>()?;
7842 m.add_class::<PyDepletionSystem>()?;
7843 m.add_class::<PyUsrbinTally>()?;
7844 m.add_class::<PyMagicOutput>()?;
7845 m.add_class::<PyAliasTable>()?;
7846 m.add_class::<PyMeshSourceSampler>()?;
7847 m.add_class::<PyKdeSampler>()?;
7848 m.add_class::<PyCascade>()?;
7849 m.add_class::<PyMaterialsCompendium>()?;
7850 m.add_class::<PyDeckProblem>()?;
7851 m.add_class::<PyInventory>()?;
7852 Ok(())
7853}