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
727#[pyclass(name = "Mctal")]
729struct PyMctal {
730 inner: nucleide_mcnp_io::mctal::Mctal,
731}
732
733#[pymethods]
734impl PyMctal {
735 #[getter]
736 fn code_name(&self) -> &str {
737 &self.inner.code_name
738 }
739 #[getter]
740 fn comment(&self) -> &str {
741 &self.inner.comment
742 }
743 #[getter]
744 fn n_histories(&self) -> u64 {
745 self.inner.n_histories
746 }
747 #[getter]
748 fn n_cycles(&self) -> usize {
749 self.inner.n_cycles
750 }
751 #[getter]
752 fn n_inactive(&self) -> usize {
753 self.inner.n_inactive
754 }
755 #[getter]
756 fn vars_per_cycle(&self) -> usize {
757 self.inner.vars_per_cycle
758 }
759 #[getter]
760 fn k_col(&self) -> Vec<f64> {
761 self.inner.k_col.clone()
762 }
763 #[getter]
764 fn k_abs(&self) -> Vec<f64> {
765 self.inner.k_abs.clone()
766 }
767 #[getter]
768 fn k_path(&self) -> Vec<f64> {
769 self.inner.k_path.clone()
770 }
771 #[getter]
772 fn prompt_life_col(&self) -> Vec<f64> {
773 self.inner.prompt_life_col.clone()
774 }
775 #[getter]
776 fn prompt_life_path(&self) -> Vec<f64> {
777 self.inner.prompt_life_path.clone()
778 }
779 #[getter]
782 fn averages(&self) -> Vec<BTreeMap<String, f64>> {
783 self.inner
784 .averages
785 .iter()
786 .map(|a| {
787 let mut m = BTreeMap::new();
788 m.insert("avg_k_col".into(), a.avg_k_col.0);
789 m.insert("avg_k_col_stdev".into(), a.avg_k_col.1);
790 m.insert("avg_k_abs".into(), a.avg_k_abs.0);
791 m.insert("avg_k_abs_stdev".into(), a.avg_k_abs.1);
792 m.insert("avg_k_path".into(), a.avg_k_path.0);
793 m.insert("avg_k_path_stdev".into(), a.avg_k_path.1);
794 m.insert("avg_k_combined".into(), a.avg_k_combined.0);
795 m.insert("avg_k_combined_stdev".into(), a.avg_k_combined.1);
796 m.insert("avg_k_combined_active".into(), a.avg_k_combined_active.0);
797 m.insert(
798 "avg_k_combined_active_stdev".into(),
799 a.avg_k_combined_active.1,
800 );
801 m.insert("prompt_life_combined".into(), a.prompt_life_combined.0);
802 m.insert(
803 "prompt_life_combined_stdev".into(),
804 a.prompt_life_combined.1,
805 );
806 m.insert("cycle_histories".into(), a.cycle_histories);
807 m.insert("fom".into(), a.fom);
808 m
809 })
810 .collect()
811 }
812 #[allow(clippy::type_complexity)]
823 fn k_arrays<'py>(
824 &self,
825 py: Python<'py>,
826 ) -> PyResult<(
827 Bound<'py, PyArray1<f64>>,
828 Bound<'py, PyArray1<f64>>,
829 Bound<'py, PyArray1<f64>>,
830 Bound<'py, PyArray1<f64>>,
831 Bound<'py, PyArray1<f64>>,
832 )> {
833 Ok((
834 self.inner.k_col.clone().into_pyarray(py),
835 self.inner.k_abs.clone().into_pyarray(py),
836 self.inner.k_path.clone().into_pyarray(py),
837 self.inner.prompt_life_col.clone().into_pyarray(py),
838 self.inner.prompt_life_path.clone().into_pyarray(py),
839 ))
840 }
841 fn averages_array<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArray2<f64>>> {
853 let n = self.inner.averages.len();
854 let mut flat = Vec::with_capacity(n * 14);
855 for a in &self.inner.averages {
856 flat.extend_from_slice(&[
857 a.avg_k_col.0,
858 a.avg_k_col.1,
859 a.avg_k_abs.0,
860 a.avg_k_abs.1,
861 a.avg_k_path.0,
862 a.avg_k_path.1,
863 a.avg_k_combined.0,
864 a.avg_k_combined.1,
865 a.avg_k_combined_active.0,
866 a.avg_k_combined_active.1,
867 a.prompt_life_combined.0,
868 a.prompt_life_combined.1,
869 a.cycle_histories,
870 a.fom,
871 ]);
872 }
873 m_err(
874 flat.into_pyarray(py)
875 .reshape((n, 14))
876 .map_err(|e| e.to_string()),
877 )
878 }
879 #[getter]
882 fn npert(&self) -> Option<String> {
883 self.inner.npert.clone()
884 }
885 #[getter]
887 fn tally_nums(&self) -> Vec<u32> {
888 self.inner.tally_nums.clone()
889 }
890 #[getter]
897 fn tallies(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
898 use pyo3::types::PyDict;
899 let mut out = Vec::with_capacity(self.inner.tallies.len());
900 for t in &self.inner.tallies {
901 let d = PyDict::new(py);
902 d.set_item("number", t.number)?;
903 d.set_item("particle_type", t.particle_type)?;
904 d.set_item("detector_type", t.detector_type)?;
905 d.set_item("particle_list", t.particle_list.clone())?;
906 d.set_item("comment", t.comment.clone())?;
907 for (key, card) in [
908 ("f", &t.f),
909 ("d", &t.d),
910 ("u", &t.u),
911 ("s", &t.s),
912 ("m", &t.m),
913 ("c", &t.c),
914 ("e", &t.e),
915 ("t", &t.t),
916 ] {
917 let c = PyDict::new(py);
918 c.set_item("count", card.count)?;
919 c.set_item("values", card.values.clone())?;
920 d.set_item(key, c)?;
921 }
922 let vals: Vec<(f64, f64)> = t.vals.clone();
923 d.set_item("vals", vals)?;
924 d.set_item("total", t.total_val())?;
925 out.push(d.into_any().unbind());
926 }
927 Ok(out)
928 }
929 fn tally_vals_array<'py>(
937 &self,
938 py: Python<'py>,
939 number: u32,
940 ) -> PyResult<Bound<'py, PyArray2<f64>>> {
941 let tally = self
942 .inner
943 .tallies
944 .iter()
945 .find(|t| t.number == number)
946 .ok_or_else(|| {
947 PyValueError::new_err(format!("mctal has no parsed body for tally {number}"))
948 })?;
949 let mut flat = Vec::with_capacity(tally.vals.len() * 2);
950 for (v, e) in &tally.vals {
951 flat.push(*v);
952 flat.push(*e);
953 }
954 let n = tally.vals.len();
955 m_err(
956 flat.into_pyarray(py)
957 .reshape((n, 2))
958 .map_err(|e| e.to_string()),
959 )
960 }
961}
962
963#[pyfunction]
965fn read_mctal(path: &str) -> PyResult<PyMctal> {
966 m_err(nucleide_mcnp_io::mctal::Mctal::from_file(path).map(|inner| PyMctal { inner }))
967}
968
969#[pyclass(name = "SurfSrc")]
971struct PySurfSrc {
972 inner: nucleide_mcnp_io::surfsrc::SurfSrc,
973}
974
975#[pymethods]
976impl PySurfSrc {
977 #[getter]
978 fn kod(&self) -> String {
979 self.inner.header.kod.trim_end().to_string()
980 }
981 #[getter]
982 fn ver(&self) -> String {
983 self.inner.header.ver.trim_end().to_string()
984 }
985 #[getter]
986 fn np1(&self) -> i64 {
987 self.inner.header.np1
988 }
989 #[getter]
991 fn orignp1(&self) -> i64 {
992 self.inner.header.orignp1
993 }
994 #[getter]
995 fn nrss(&self) -> i64 {
996 self.inner.header.nrss
997 }
998 #[getter]
999 fn ncrd(&self) -> i32 {
1000 self.inner.header.ncrd
1001 }
1002 #[getter]
1003 fn njsw(&self) -> i32 {
1004 self.inner.header.njsw
1005 }
1006 #[getter]
1007 fn niss(&self) -> i64 {
1008 self.inner.header.niss
1009 }
1010 fn print_header(&self) -> String {
1012 self.inner.header.print_header()
1013 }
1014 fn tracks(&self) -> PyResult<Vec<BTreeMap<String, f64>>> {
1016 let tracks = self
1017 .inner
1018 .read_tracklist()
1019 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1020 Ok(tracks
1021 .iter()
1022 .map(|t| {
1023 let mut d = BTreeMap::new();
1024 d.insert("nps".into(), t.nps);
1025 d.insert("bitarray".into(), t.bitarray);
1026 d.insert("wgt".into(), t.wgt);
1027 d.insert("erg".into(), t.erg);
1028 d.insert("tme".into(), t.tme);
1029 d.insert("x".into(), t.x);
1030 d.insert("y".into(), t.y);
1031 d.insert("z".into(), t.z);
1032 d.insert("u".into(), t.u);
1033 d.insert("v".into(), t.v);
1034 d.insert("cs".into(), t.cs);
1035 d.insert("w".into(), t.w);
1036 d
1037 })
1038 .collect())
1039 }
1040}
1041
1042#[pyfunction]
1044fn read_ssw(path: &str) -> PyResult<PySurfSrc> {
1045 nucleide_mcnp_io::surfsrc::SurfSrc::open(path)
1046 .map(|inner| PySurfSrc { inner })
1047 .map_err(|e| PyValueError::new_err(e.to_string()))
1048}
1049
1050#[pyclass(name = "PtracFile")]
1052struct PyPtracFile {
1053 inner: nucleide_mcnp_io::ptrac::PtracFile,
1054}
1055
1056#[pymethods]
1057impl PyPtracFile {
1058 #[getter]
1059 fn problem_title(&self) -> &str {
1060 &self.inner.problem_title
1061 }
1062 #[getter]
1064 fn width_code(&self) -> u8 {
1065 match self.inner.format {
1066 nucleide_mcnp_io::ptrac::Format::I4LittleEndian => 0,
1067 nucleide_mcnp_io::ptrac::Format::I8LittleEndian => 1,
1068 }
1069 }
1070 #[getter]
1072 fn variable_nums(&self) -> BTreeMap<String, usize> {
1073 let v = &self.inner.variable_nums;
1074 let mut m = BTreeMap::new();
1075 m.insert("nps".into(), v.nps);
1076 m.insert("src".into(), v.src);
1077 m.insert("bnk".into(), v.bnk);
1078 m.insert("sur".into(), v.sur);
1079 m.insert("col".into(), v.col);
1080 m.insert("ter".into(), v.ter);
1081 m
1082 }
1083 fn events(&self) -> PyResult<Vec<BTreeMap<String, f64>>> {
1085 let events = self
1086 .inner
1087 .events()
1088 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1089 Ok(events
1090 .iter()
1091 .map(|ev| {
1092 let mut d = BTreeMap::new();
1093 d.insert("event_type".to_string(), ev.event_type as f64);
1094 for (n, v) in ev.iter() {
1095 d.insert(n.to_string(), v);
1096 }
1097 d
1098 })
1099 .collect())
1100 }
1101 fn events_array<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArray2<f64>>> {
1112 let events = self
1113 .inner
1114 .events()
1115 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1116 let n = events.len();
1117 let mut flat = Vec::with_capacity(n * PTRAC_EVENT_COLUMNS.len());
1118 for ev in &events {
1119 flat.push(ev.event_type as f64);
1120 for col in &PTRAC_EVENT_COLUMNS[1..] {
1121 flat.push(ev.get(col).unwrap_or(0.0));
1122 }
1123 }
1124 m_err(
1125 flat.into_pyarray(py)
1126 .reshape((n, PTRAC_EVENT_COLUMNS.len()))
1127 .map_err(|e| e.to_string()),
1128 )
1129 }
1130 fn event_field_array<'py>(
1138 &self,
1139 py: Python<'py>,
1140 field: &str,
1141 ) -> PyResult<Bound<'py, PyArray1<f64>>> {
1142 if !PTRAC_EVENT_COLUMNS.contains(&field) {
1143 return Err(PyValueError::new_err(format!(
1144 "unknown PTRAC field `{field}` (expected one of {})",
1145 PTRAC_EVENT_COLUMNS.join(", ")
1146 )));
1147 }
1148 let events = self
1149 .inner
1150 .events()
1151 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1152 let col: Vec<f64> = events
1153 .iter()
1154 .map(|ev| {
1155 if field == "event_type" {
1156 ev.event_type as f64
1157 } else {
1158 ev.get(field).unwrap_or(0.0)
1159 }
1160 })
1161 .collect();
1162 Ok(col.into_pyarray(py))
1163 }
1164}
1165
1166const PTRAC_EVENT_COLUMNS: [&str; 19] = [
1172 "event_type",
1173 "node",
1174 "nsr",
1175 "nsf",
1176 "nxs",
1177 "ntyn",
1178 "ipt",
1179 "ncl",
1180 "mat",
1181 "ncp",
1182 "xxx",
1183 "yyy",
1184 "zzz",
1185 "uuu",
1186 "vvv",
1187 "www",
1188 "erg",
1189 "wgt",
1190 "tme",
1191];
1192
1193#[pyfunction]
1195fn read_ptrac(path: &str) -> PyResult<PyPtracFile> {
1196 nucleide_mcnp_io::ptrac::PtracFile::open(path)
1197 .map(|inner| PyPtracFile { inner })
1198 .map_err(|e| PyValueError::new_err(e.to_string()))
1199}
1200
1201fn mcpl_particle_to_dict(py: Python<'_>, p: &nucleide_mcpl_io::Particle) -> PyResult<Py<PyAny>> {
1204 use pyo3::types::PyDict;
1205 let d = PyDict::new(py);
1206 d.set_item("ekin", p.ekin)?;
1207 d.set_item("polarisation", p.polarisation.to_vec())?;
1208 d.set_item("position", p.position.to_vec())?;
1209 d.set_item("direction", p.direction.to_vec())?;
1210 d.set_item("time", p.time)?;
1211 d.set_item("weight", p.weight)?;
1212 d.set_item("pdgcode", p.pdgcode)?;
1213 d.set_item("userflags", p.userflags)?;
1214 Ok(d.into_any().unbind())
1215}
1216
1217fn mcpl_particle_from_dict(d: &Bound<'_, PyAny>) -> PyResult<nucleide_mcpl_io::Particle> {
1218 let get_f64 = |key: &str| -> PyResult<f64> {
1219 d.get_item(key)
1220 .map_err(|e| PyValueError::new_err(format!("particle missing `{key}`: {e}")))?
1221 .extract()
1222 .map_err(|_| PyValueError::new_err(format!("particle `{key}` must be a float")))
1223 };
1224 let get_vec3 = |key: &str| -> PyResult<[f64; 3]> {
1225 let v: Vec<f64> = d
1226 .get_item(key)
1227 .map_err(|e| PyValueError::new_err(format!("particle missing `{key}`: {e}")))?
1228 .extract()
1229 .map_err(|_| PyValueError::new_err(format!("particle `{key}` must be a 3-list")))?;
1230 if v.len() != 3 {
1231 return Err(PyValueError::new_err(format!(
1232 "particle `{key}` must have exactly 3 entries"
1233 )));
1234 }
1235 Ok([v[0], v[1], v[2]])
1236 };
1237 let pdgcode: i32 = d
1238 .get_item("pdgcode")
1239 .map_err(|e| PyValueError::new_err(format!("particle missing `pdgcode`: {e}")))?
1240 .extract()
1241 .map_err(|_| PyValueError::new_err("particle `pdgcode` must be an int"))?;
1242 let userflags: u32 = d
1243 .get_item("userflags")
1244 .map_err(|e| PyValueError::new_err(format!("particle missing `userflags`: {e}")))?
1245 .extract()
1246 .map_err(|_| PyValueError::new_err("particle `userflags` must be an int"))?;
1247 Ok(nucleide_mcpl_io::Particle {
1248 ekin: get_f64("ekin")?,
1249 polarisation: get_vec3("polarisation")?,
1250 position: get_vec3("position")?,
1251 direction: get_vec3("direction")?,
1252 time: get_f64("time")?,
1253 weight: get_f64("weight")?,
1254 pdgcode,
1255 userflags,
1256 })
1257}
1258
1259#[pyclass(name = "McplFile")]
1261struct PyMcplFile {
1262 inner: nucleide_mcpl_io::McplFile,
1263}
1264
1265#[pymethods]
1266impl PyMcplFile {
1267 #[getter]
1269 fn version(&self) -> u16 {
1270 self.inner.header.version
1271 }
1272 #[getter]
1274 fn nparticles(&self) -> u64 {
1275 self.inner.header.nparticles
1276 }
1277 #[getter]
1279 fn srcname(&self) -> &str {
1280 &self.inner.header.srcname
1281 }
1282 #[getter]
1284 fn comments(&self) -> Vec<String> {
1285 self.inner.header.comments.clone()
1286 }
1287 #[getter]
1289 fn has_userflags(&self) -> bool {
1290 self.inner.header.has_userflags
1291 }
1292 #[getter]
1294 fn has_polarisation(&self) -> bool {
1295 self.inner.header.has_polarisation
1296 }
1297 #[getter]
1299 fn double_prec(&self) -> bool {
1300 self.inner.header.double_prec
1301 }
1302 #[getter]
1304 fn universal_pdgcode(&self) -> Option<i32> {
1305 self.inner.header.universal_pdgcode
1306 }
1307 #[getter]
1309 fn universal_weight(&self) -> Option<f64> {
1310 self.inner.header.universal_weight
1311 }
1312 #[getter]
1314 fn blobs(&self) -> Vec<(String, Vec<u8>)> {
1315 self.inner
1316 .header
1317 .blobs
1318 .iter()
1319 .map(|b| (b.key.clone(), b.data.clone()))
1320 .collect()
1321 }
1322 fn particles(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
1325 let ps = self
1326 .inner
1327 .particles()
1328 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1329 ps.iter().map(|p| mcpl_particle_to_dict(py, p)).collect()
1330 }
1331}
1332
1333#[pyfunction]
1335fn read_mcpl(path: &str) -> PyResult<PyMcplFile> {
1336 nucleide_mcpl_io::McplFile::open(path)
1337 .map(|inner| PyMcplFile { inner })
1338 .map_err(|e| PyValueError::new_err(e.to_string()))
1339}
1340
1341#[pyfunction]
1351fn write_mcpl(
1352 path: &str,
1353 header: &Bound<'_, PyAny>,
1354 particles: Vec<Bound<'_, PyAny>>,
1355) -> PyResult<()> {
1356 use nucleide_mcpl_io::{Blob, Header};
1357 let get = |key: &str| header.get_item(key);
1358 let srcname: String = get("srcname")
1359 .map_err(|_| PyValueError::new_err("header missing `srcname`"))?
1360 .extract()
1361 .map_err(|_| PyValueError::new_err("header `srcname` must be a str"))?;
1362 let comments: Vec<String> = get("comments")
1363 .map_err(|_| PyValueError::new_err("header missing `comments`"))?
1364 .extract()
1365 .map_err(|_| PyValueError::new_err("header `comments` must be a list of str"))?;
1366 let flag = |key: &str| -> PyResult<bool> {
1367 get(key)
1368 .map_err(|_| PyValueError::new_err(format!("header missing `{key}`")))?
1369 .extract()
1370 .map_err(|_| PyValueError::new_err(format!("header `{key}` must be a bool")))
1371 };
1372 let universal_pdgcode: Option<i32> = get("universal_pdgcode")
1373 .map_err(|_| PyValueError::new_err("header missing `universal_pdgcode`"))?
1374 .extract()
1375 .map_err(|_| PyValueError::new_err("header `universal_pdgcode` must be an int or None"))?;
1376 let universal_weight: Option<f64> = get("universal_weight")
1377 .map_err(|_| PyValueError::new_err("header missing `universal_weight`"))?
1378 .extract()
1379 .map_err(|_| PyValueError::new_err("header `universal_weight` must be a float or None"))?;
1380 let blob_pairs: Vec<(String, Vec<u8>)> = get("blobs")
1381 .map_err(|_| PyValueError::new_err("header missing `blobs`"))?
1382 .extract()
1383 .map_err(|_| {
1384 PyValueError::new_err("header `blobs` must be a list of (key, bytes) pairs")
1385 })?;
1386 let h = Header {
1387 has_userflags: flag("has_userflags")?,
1388 has_polarisation: flag("has_polarisation")?,
1389 double_prec: flag("double_prec")?,
1390 universal_pdgcode,
1391 universal_weight,
1392 srcname,
1393 comments,
1394 blobs: blob_pairs
1395 .into_iter()
1396 .map(|(key, data)| Blob { key, data })
1397 .collect(),
1398 ..Header::default()
1399 };
1400 let ps: Vec<nucleide_mcpl_io::Particle> = particles
1401 .iter()
1402 .map(mcpl_particle_from_dict)
1403 .collect::<PyResult<_>>()?;
1404 nucleide_mcpl_io::write_to_path(path, &h, &ps).map_err(|e| PyValueError::new_err(e.to_string()))
1405}
1406
1407#[pyfunction]
1420#[pyo3(signature = (ssw_path, mcpl_path, surfs, kinds, options=None))]
1421fn ssw2mcpl(
1422 ssw_path: &str,
1423 mcpl_path: &str,
1424 surfs: Vec<u32>,
1425 kinds: Vec<String>,
1426 options: Option<Bound<'_, PyAny>>,
1427) -> PyResult<u64> {
1428 use nucleide_mcnp_io::surfsrc::SurfSrc;
1429 use nucleide_mcpl_io::ssw::{SswParticleKind, SswTrack};
1430 let ssw = SurfSrc::open(ssw_path).map_err(|e| PyValueError::new_err(e.to_string()))?;
1431 let raw = ssw
1432 .read_tracklist()
1433 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1434 if raw.len() != surfs.len() || raw.len() != kinds.len() {
1435 return Err(PyValueError::new_err(format!(
1436 "ssw2mcpl: SSW holds {} tracks but got {} surfs and {} kinds \
1437 (one surf+kind per track required)",
1438 raw.len(),
1439 surfs.len(),
1440 kinds.len()
1441 )));
1442 }
1443 let mut tracks = Vec::with_capacity(raw.len());
1444 for (i, (t, surf, kind)) in raw
1445 .iter()
1446 .zip(surfs)
1447 .zip(kinds.iter())
1448 .map(|((t, s), k)| (t, s, k))
1449 .enumerate()
1450 {
1451 let kind = SswParticleKind::parse(kind).ok_or_else(|| {
1452 PyValueError::new_err(format!(
1453 "track {i} kind `{kind}` unknown (expected \"neutron\" or \"gamma\")"
1454 ))
1455 })?;
1456 tracks.push(SswTrack {
1457 ekin: t.erg,
1458 time_shakes: t.tme,
1459 position: [t.x, t.y, t.z],
1460 direction: [t.u, t.v, t.cs],
1461 weight: t.wgt,
1462 surf,
1463 kind,
1464 });
1465 }
1466 let mut opts = parse_ssw2mcpl_options(options.as_ref())?;
1467 if mcpl_path.ends_with(".gz") {
1468 opts.gzip = true;
1469 }
1470 let bytes = nucleide_mcpl_io::ssw::ssw2mcpl_bytes(&tracks, &opts)
1471 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1472 std::fs::write(mcpl_path, bytes).map_err(|e| PyValueError::new_err(e.to_string()))?;
1473 Ok(tracks.len() as u64)
1474}
1475
1476fn parse_ssw2mcpl_options(
1478 options: Option<&Bound<'_, PyAny>>,
1479) -> PyResult<nucleide_mcpl_io::ssw::Ssw2McplOptions> {
1480 use nucleide_mcpl_io::ssw::{DeckBlob, Ssw2McplOptions};
1481 let mut opts = Ssw2McplOptions::default();
1482 let Some(d) = options else {
1483 return Ok(opts);
1484 };
1485 if !d.is_instance_of::<pyo3::types::PyDict>() {
1486 return Err(PyValueError::new_err("options must be a dict or None"));
1487 }
1488 let flag = |key: &str| -> PyResult<Option<bool>> {
1489 match d.get_item(key) {
1490 Ok(v) => v
1491 .extract()
1492 .map(Some)
1493 .map_err(|_| PyValueError::new_err(format!("options `{key}` must be a bool"))),
1494 Err(_) => Ok(None),
1495 }
1496 };
1497 if let Some(v) = flag("double_prec")? {
1498 opts.double_prec = v;
1499 }
1500 if let Some(v) = flag("surf_to_userflags")? {
1501 opts.surf_to_userflags = v;
1502 }
1503 if let Some(v) = flag("gzip")? {
1504 opts.gzip = v;
1505 }
1506 if let Ok(v) = d.get_item("srcname") {
1507 opts.srcname = v
1508 .extract()
1509 .map_err(|_| PyValueError::new_err("options `srcname` must be a str"))?;
1510 }
1511 if let Ok(v) = d.get_item("comments") {
1512 opts.comments = v
1513 .extract()
1514 .map_err(|_| PyValueError::new_err("options `comments` must be a list of str"))?;
1515 }
1516 if let Ok(v) = d.get_item("deck_blob") {
1517 if !v.is_none() {
1518 let (key, data): (String, Vec<u8>) = v.extract().map_err(|_| {
1519 PyValueError::new_err("options `deck_blob` must be a (key, bytes) pair or None")
1520 })?;
1521 opts.deck_blob = Some(DeckBlob { key, data });
1522 }
1523 }
1524 Ok(opts)
1525}
1526
1527#[pyfunction]
1537#[pyo3(signature = (mcpl_path, reference_ssw_path, ssw_out_path, surface=None))]
1538fn mcpl2ssw(
1539 mcpl_path: &str,
1540 reference_ssw_path: &str,
1541 ssw_out_path: &str,
1542 surface: Option<u32>,
1543) -> PyResult<u64> {
1544 use nucleide_mcnp_io::surfsrc::SurfSrc;
1545 use nucleide_mcpl_io::ssw::Mcpl2SswOptions;
1546 let mcpl = nucleide_mcpl_io::McplFile::open(mcpl_path)
1547 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1548 let particles = mcpl
1549 .particles()
1550 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1551 let reference =
1552 SurfSrc::open(reference_ssw_path).map_err(|e| PyValueError::new_err(e.to_string()))?;
1553 let (header, tracks) = nucleide_mcpl_io::ssw::mcpl2ssw(
1554 &particles,
1555 &reference.header,
1556 &Mcpl2SswOptions { surface },
1557 )
1558 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1559 nucleide_mcnp_io::surfsrc::write_to_path(ssw_out_path, &header, &tracks)
1560 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1561 Ok(tracks.len() as u64)
1562}
1563
1564#[pyclass(name = "EndlLibrary")]
1566struct PyEndlLibrary {
1567 inner: nucleide_mcnp_io::endl::Library,
1568}
1569
1570#[pymethods]
1571impl PyEndlLibrary {
1572 fn nuclides(&self) -> Vec<i64> {
1574 self.inner.nuclides()
1575 }
1576 #[pyo3(signature = (nuc, p_in, rdesc, rprop, x1=None, p_out=None))]
1583 fn get_rx(
1584 &self,
1585 nuc: &Bound<'_, PyAny>,
1586 p_in: i32,
1587 rdesc: i32,
1588 rprop: i32,
1589 x1: Option<i32>,
1590 p_out: Option<i32>,
1591 ) -> PyResult<Vec<Vec<f64>>> {
1592 let id = if let Ok(n) = nuc.extract::<i64>() {
1593 n
1594 } else if let Ok(name) = nuc.extract::<&str>() {
1595 NuclideId::from_name(name).map_err(wrap_nucid_err)?.nucid() as i64
1596 } else {
1597 return Err(PyTypeError::new_err("expected int nucleus id or str name"));
1598 };
1599 self.inner
1600 .get_rx(id, p_in, rdesc, rprop, x1, p_out)
1601 .map(|rows| rows.to_vec())
1602 .map_err(|e| PyValueError::new_err(e.to_string()))
1603 }
1604}
1605
1606#[pyfunction]
1608fn read_endl(path: &str) -> PyResult<PyEndlLibrary> {
1609 nucleide_mcnp_io::endl::Library::open(path)
1610 .map(|inner| PyEndlLibrary { inner })
1611 .map_err(|e| PyValueError::new_err(e.to_string()))
1612}
1613
1614#[pyfunction]
1616fn endl_endftod(field: &str) -> f64 {
1617 nucleide_mcnp_io::endl::endftod(field)
1618}
1619
1620#[pyfunction]
1628fn combine_ssw_files(output: &str, inputs: Vec<String>) -> PyResult<()> {
1629 nucleide_mcnp_io::surfsrc::combine_files(&inputs, output)
1630 .map_err(|e| PyValueError::new_err(e.to_string()))
1631}
1632
1633#[pyclass(name = "Chain")]
1639struct PyChain {
1640 inner: std::sync::Arc<nucleide_depletion::Chain>,
1641}
1642
1643#[pymethods]
1644impl PyChain {
1645 #[getter]
1647 fn nuclides(&self) -> Vec<String> {
1648 self.inner.nuclides.iter().map(|n| n.name.clone()).collect()
1649 }
1650
1651 fn index_of(&self, name: &str) -> Option<usize> {
1652 self.inner.index_of(name)
1653 }
1654}
1655
1656#[pyfunction]
1658fn read_chain(path: &str) -> PyResult<PyChain> {
1659 nucleide_depletion::Chain::from_file(path)
1660 .map(|inner| PyChain {
1661 inner: std::sync::Arc::new(inner),
1662 })
1663 .map_err(|e| PyValueError::new_err(e.to_string()))
1664}
1665
1666type RateMap = BTreeMap<String, f64>;
1668
1669#[pyclass(name = "DepletionSystem")]
1671struct PyDepletionSystem {
1672 inner: std::sync::Arc<nucleide_depletion::DepletionSystem>,
1673}
1674
1675#[pymethods]
1676impl PyDepletionSystem {
1677 #[pyo3(signature = (n0, dt, order=48, method="cram48"))]
1686 fn solve(
1687 &self,
1688 n0: BTreeMap<String, f64>,
1689 dt: f64,
1690 order: u8,
1691 method: &str,
1692 ) -> PyResult<BTreeMap<String, f64>> {
1693 let method = resolve_method(order, method)?;
1694 nucleide_depletion::deplete_with_method(&self.inner, method, &n0, dt)
1695 .map(|r| r.atoms)
1696 .map_err(|e| PyValueError::new_err(e.to_string()))
1697 }
1698
1699 #[pyo3(signature = (n0, dt, order=48, method="cram48"))]
1705 fn solve_vec(&self, n0: Vec<f64>, dt: f64, order: u8, method: &str) -> PyResult<Vec<f64>> {
1706 let method = resolve_method(order, method)?;
1707 nucleide_depletion::solve_with_method(&self.inner, method, &n0, dt)
1708 .map_err(|e| PyValueError::new_err(e.to_string()))
1709 }
1710}
1711
1712#[pyfunction]
1714fn build_depletion_system(chain: &PyChain, rates: RateMap) -> PyResult<PyDepletionSystem> {
1715 let rs = split_rates(&rates, &chain.inner)?;
1716 nucleide_depletion::DepletionSystem::build((*chain.inner).clone(), &rs)
1717 .map(|sys| PyDepletionSystem {
1718 inner: std::sync::Arc::new(sys),
1719 })
1720 .map_err(|e| PyValueError::new_err(e.to_string()))
1721}
1722
1723fn parse_order(order: u8) -> PyResult<nucleide_depletion::Order> {
1724 match order {
1725 16 => Ok(nucleide_depletion::Order::Order16),
1726 48 => Ok(nucleide_depletion::Order::Order48),
1727 other => Err(PyValueError::new_err(format!(
1728 "unsupported CRAM order {other} (supported: 16, 48)"
1729 ))),
1730 }
1731}
1732
1733fn parse_method(name: &str) -> PyResult<nucleide_depletion::Method> {
1736 name.parse().map_err(|e: String| PyValueError::new_err(e))
1737}
1738
1739fn resolve_method(order: u8, method: &str) -> PyResult<nucleide_depletion::Method> {
1744 let parsed = parse_method(method)?;
1745 if parsed == nucleide_depletion::Method::default_cram() {
1746 parse_order(order).map(nucleide_depletion::Method::Cram)
1747 } else {
1748 Ok(parsed)
1749 }
1750}
1751
1752fn split_rates(
1753 rates: &RateMap,
1754 chain: &nucleide_depletion::Chain,
1755) -> PyResult<nucleide_depletion::ReactionRates> {
1756 let mut out = nucleide_depletion::ReactionRates::new();
1757 for (key, v) in rates {
1758 let (nuc, rx) = key.split_once(':').ok_or_else(|| {
1759 PyValueError::new_err(format!("rate key `{key}` must be `Name:reaction`"))
1760 })?;
1761 let idx = chain
1762 .index_of(nuc)
1763 .ok_or_else(|| PyValueError::new_err(format!("rate for unknown nuclide `{nuc}`")))?;
1764 out.entry(idx).or_default().insert(rx.to_string(), *v);
1765 }
1766 Ok(out)
1767}
1768
1769#[pyfunction]
1779#[pyo3(signature = (chain, n0, dt, rates=None, order=48, method="cram48"))]
1780fn deplete(
1781 chain: &PyChain,
1782 n0: BTreeMap<String, f64>,
1783 dt: f64,
1784 rates: Option<RateMap>,
1785 order: u8,
1786 method: &str,
1787) -> PyResult<BTreeMap<String, f64>> {
1788 let method = resolve_method(order, method)?;
1789 let rates = split_rates(rates.as_ref().unwrap_or(&BTreeMap::new()), &chain.inner)?;
1790 let sys = nucleide_depletion::DepletionSystem::build((*chain.inner).clone(), &rates)
1791 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1792 nucleide_depletion::deplete_with_method(&sys, method, &n0, dt)
1793 .map(|r| r.atoms)
1794 .map_err(|e| PyValueError::new_err(e.to_string()))
1795}
1796
1797#[pyfunction]
1806fn read_serpent(path: &str, kind: &str) -> PyResult<Py<PyAny>> {
1807 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
1808 let table = match kind {
1809 "res" => nucleide_serpent_io::parse_res(&text),
1810 "dep" => nucleide_serpent_io::parse_dep(&text),
1811 "det" => nucleide_serpent_io::parse_det(&text),
1812 other => {
1813 return Err(PyValueError::new_err(format!(
1814 "kind must be res|dep|det, got `{other}`"
1815 )))
1816 }
1817 }
1818 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1819 fn entry_to_py(py: Python<'_>, e: &nucleide_serpent_io::Entry) -> PyResult<Py<PyAny>> {
1820 use nucleide_serpent_io::Entry as E;
1821 let value = match e {
1822 E::Scalar(nucleide_serpent_io::Value::Num(n)) => {
1823 n.into_pyobject(py).unwrap().unbind().into_any()
1824 }
1825 E::Scalar(nucleide_serpent_io::Value::Str(s)) => {
1826 s.into_pyobject(py).unwrap().unbind().into_any()
1827 }
1828 E::Vector(vs) => vs
1829 .iter()
1830 .map(|v| match v {
1831 nucleide_serpent_io::Value::Num(n) => {
1832 n.into_pyobject(py).unwrap().unbind().into_any()
1833 }
1834 nucleide_serpent_io::Value::Str(s) => {
1835 s.into_pyobject(py).unwrap().unbind().into_any()
1836 }
1837 })
1838 .collect::<Vec<_>>()
1839 .into_pyobject(py)
1840 .unwrap()
1841 .unbind()
1842 .into_any(),
1843 E::Matrix(m) => m
1844 .to_rows_f64()
1845 .map_err(|err| PyValueError::new_err(err.to_string()))?
1846 .into_pyobject(py)
1847 .unwrap()
1848 .unbind()
1849 .into_any(),
1850 };
1851 Ok(value)
1852 }
1853 Python::attach(|py| {
1854 let dict = pyo3::types::PyDict::new(py);
1855 for (k, e) in table.iter() {
1856 dict.set_item(k, entry_to_py(py, e)?)?;
1857 }
1858 Ok(dict.into_any().unbind())
1859 })
1860}
1861
1862#[pyclass(name = "UsrbinTally")]
1864struct PyUsrbinTally {
1865 inner: nucleide_fluka_io::usrbin::UsrbinTally,
1866}
1867
1868#[pymethods]
1869impl PyUsrbinTally {
1870 #[getter]
1871 fn name(&self) -> &str {
1872 &self.inner.name
1873 }
1874 #[getter]
1875 fn particle(&self) -> &str {
1876 &self.inner.particle
1877 }
1878 #[getter]
1879 fn nx(&self) -> usize {
1880 self.inner.x_info.bins
1881 }
1882 #[getter]
1883 fn ny(&self) -> usize {
1884 self.inner.y_info.bins
1885 }
1886 #[getter]
1887 fn nz(&self) -> usize {
1888 self.inner.z_info.bins
1889 }
1890 #[getter]
1891 fn x_bounds(&self) -> Vec<f64> {
1892 self.inner.x_bounds.clone()
1893 }
1894 #[getter]
1895 fn y_bounds(&self) -> Vec<f64> {
1896 self.inner.y_bounds.clone()
1897 }
1898 #[getter]
1899 fn z_bounds(&self) -> Vec<f64> {
1900 self.inner.z_bounds.clone()
1901 }
1902 #[getter]
1904 fn data(&self) -> Vec<f64> {
1905 self.inner.part_data.clone()
1906 }
1907 #[getter]
1909 fn error(&self) -> Vec<f64> {
1910 self.inner.error_data.clone()
1911 }
1912 fn dims(&self) -> [usize; 3] {
1913 [self.nx(), self.ny(), self.nz()]
1914 }
1915}
1916
1917#[pyfunction]
1919fn read_usrbin(path: &str) -> PyResult<Vec<PyUsrbinTally>> {
1920 let tallies = nucleide_fluka_io::usrbin::read_usrbin_file(path)
1921 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1922 Ok(tallies
1923 .into_iter()
1924 .map(|inner| PyUsrbinTally { inner })
1925 .collect())
1926}
1927
1928#[pyclass(name = "MagicOutput")]
1930struct PyMagicOutput {
1931 inner: nucleide_vr_tools::magic::MagicOutput,
1932}
1933
1934#[pymethods]
1935impl PyMagicOutput {
1936 #[getter]
1938 fn lower_bounds_ww(&self) -> Vec<f64> {
1939 self.inner.lower_bounds_ww.clone()
1940 }
1941 #[getter]
1942 fn groups_per_ve(&self) -> usize {
1943 self.inner.groups_per_ve
1944 }
1945 #[getter]
1946 fn scale_factors(&self) -> Vec<f64> {
1947 self.inner.scale_factors.clone()
1948 }
1949 #[getter]
1950 fn e_upper_bounds(&self) -> Vec<f64> {
1951 self.inner.e_upper_bounds.clone()
1952 }
1953 #[getter]
1954 fn ww_tag_name(&self) -> &str {
1955 &self.inner.ww_tag_name
1956 }
1957}
1958
1959#[pyfunction]
1961#[pyo3(signature = (tally, per_group=false, tolerance=0.5))]
1962fn magic(tally: &PyMeshTally, per_group: bool, tolerance: f64) -> PyResult<PyMagicOutput> {
1963 let selection = if per_group {
1964 nucleide_vr_tools::magic::MagicSelection::PerGroup
1965 } else {
1966 nucleide_vr_tools::magic::MagicSelection::Total
1967 };
1968 let params = nucleide_vr_tools::magic::MagicParams {
1969 tolerance,
1970 ..Default::default()
1971 };
1972 nucleide_vr_tools::magic::magic_with(&tally.inner, selection, params)
1973 .map(|inner| PyMagicOutput { inner })
1974 .map_err(|e| PyValueError::new_err(e.to_string()))
1975}
1976
1977#[pyclass(name = "AliasTable")]
1979struct PyAliasTable {
1980 inner: nucleide_vr_tools::sampling::AliasTable,
1981}
1982
1983#[pymethods]
1984impl PyAliasTable {
1985 #[new]
1987 fn new(pdf: Vec<f64>) -> PyResult<Self> {
1988 nucleide_vr_tools::sampling::AliasTable::new(&pdf)
1989 .map(|inner| PyAliasTable { inner })
1990 .map_err(|e| PyValueError::new_err(e.to_string()))
1991 }
1992 fn sample(&self, r1: f64, r2: f64) -> usize {
1994 self.inner.sample(r1, r2)
1995 }
1996 #[getter]
1997 fn pdf(&self) -> Vec<f64> {
1998 self.inner.pdf().to_vec()
1999 }
2000 fn __len__(&self) -> usize {
2001 self.inner.len()
2002 }
2003}
2004
2005#[pyclass(name = "MeshSourceSampler")]
2007struct PyMeshSourceSampler {
2008 inner: nucleide_vr_tools::sampling::MeshSourceSampler,
2009}
2010
2011#[pymethods]
2012impl PyMeshSourceSampler {
2013 #[new]
2015 #[pyo3(signature = (tally, mode, user_pdf=None))]
2016 fn new(tally: &PyMeshTally, mode: &str, user_pdf: Option<Vec<f64>>) -> PyResult<Self> {
2017 let user = if matches!(mode, "user") {
2018 Some(user_pdf.ok_or_else(|| PyValueError::new_err("user mode needs user_pdf"))?)
2019 } else {
2020 None
2021 };
2022 let m = match mode {
2023 "analog" => nucleide_vr_tools::sampling::Mode::Analog,
2024 "uniform" => nucleide_vr_tools::sampling::Mode::Uniform,
2025 "user" => nucleide_vr_tools::sampling::Mode::User,
2026 other => {
2027 return Err(PyValueError::new_err(format!(
2028 "mode must be analog|uniform|user, got `{other}`"
2029 )))
2030 }
2031 };
2032 nucleide_vr_tools::sampling::MeshSourceSampler::new(&tally.inner, m, user.as_deref())
2033 .map(|inner| PyMeshSourceSampler { inner })
2034 .map_err(|e| PyValueError::new_err(e.to_string()))
2035 }
2036 fn sample(&self, r1: f64, r2: f64) -> BTreeMap<String, f64> {
2038 let s = self.inner.sample(r1, r2);
2039 let mut d = BTreeMap::new();
2040 d.insert("index".into(), s.index as f64);
2041 d.insert("i".into(), s.i as f64);
2042 d.insert("j".into(), s.j as f64);
2043 d.insert("k".into(), s.k as f64);
2044 d.insert("weight".into(), s.weight);
2045 d
2046 }
2047}
2048
2049#[pyfunction]
2052#[pyo3(signature = (ssw, path, tracks=None))]
2053fn write_ssw(
2054 ssw: &PySurfSrc,
2055 path: &str,
2056 tracks: Option<Vec<BTreeMap<String, f64>>>,
2057) -> PyResult<()> {
2058 let header = ssw.inner.header.clone();
2059 let track_data: Vec<nucleide_mcnp_io::surfsrc::TrackData> = match tracks {
2060 Some(dict_tracks) => dict_tracks
2061 .iter()
2062 .map(|d| {
2063 let g = |k: &str| d.get(k).copied().unwrap_or(0.0);
2064 let mut record = vec![0.0f64; nucleide_mcnp_io::surfsrc::TrackData::RECORD_WIDTH];
2065 record[0] = g("nps");
2066 record[1] = g("bitarray");
2067 record[2] = g("wgt");
2068 record[3] = g("erg");
2069 record[4] = g("tme");
2070 record[5] = g("x");
2071 record[6] = g("y");
2072 record[7] = g("z");
2073 record[8] = g("u");
2074 record[9] = g("v");
2075 record[10] = g("cs");
2076 nucleide_mcnp_io::surfsrc::TrackData::from_record(record)
2077 })
2078 .collect(),
2079 None => ssw
2080 .inner
2081 .read_tracklist()
2082 .map_err(|e| PyValueError::new_err(e.to_string()))?,
2083 };
2084 let mut f = std::fs::File::create(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
2085 nucleide_mcnp_io::surfsrc::write_to(&mut f, &header, &track_data)
2086 .map_err(|e| PyValueError::new_err(e.to_string()))
2087}
2088
2089#[pyfunction]
2091fn mesh_to_geom(
2092 x_bounds: Vec<f64>,
2093 y_bounds: Vec<f64>,
2094 z_bounds: Vec<f64>,
2095 cell_materials: Vec<Option<(String, f64)>>,
2096 title_card: &str,
2097) -> String {
2098 let opts = nucleide_mcnp_io::deck::DeckOptions {
2099 title_card: title_card.to_string(),
2100 frac_type: nucleide_mcnp_io::deck::FracType::Mass,
2101 };
2102 nucleide_mcnp_io::deck::mesh_to_geom(&x_bounds, &y_bounds, &z_bounds, &cell_materials, &opts)
2103}
2104
2105#[pyfunction]
2116fn alara_parse_deck(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
2117 let owned = text.to_owned();
2118 let deck = py
2119 .detach(move || nucleide_alara_io::AlaraDeck::parse(&owned))
2120 .map_err(ala_err)?;
2121 Ok(deck_to_py(py, &deck))
2122}
2123
2124fn ala_err(e: nucleide_alara_io::Error) -> PyErr {
2125 PyValueError::new_err(e.to_string())
2126}
2127
2128fn deck_to_py(py: Python<'_>, deck: &nucleide_alara_io::AlaraDeck) -> Py<PyAny> {
2129 use pyo3::types::PyDict;
2130 let out = PyDict::new(py);
2131 let block_kinds: Vec<&str> = deck.block_kinds();
2132 out.set_item("block_kinds", block_kinds).ok();
2133 out.set_item("geometry", deck.geometry.as_ref().map(|g| g.kind.clone()))
2134 .ok();
2135 let mixtures: Vec<Py<PyAny>> = deck.mixtures.iter().map(|m| mixture_to_py(py, m)).collect();
2136 out.set_item("mixtures", mixtures).ok();
2137 let fluxes: Vec<Py<PyAny>> = deck.fluxes.iter().map(|f| fluxdef_to_py(py, f)).collect();
2138 out.set_item("fluxes", fluxes).ok();
2139 out.set_item(
2140 "cooling_times_s",
2141 deck.cooling
2142 .as_ref()
2143 .map(|c| c.times_s.clone())
2144 .unwrap_or_default(),
2145 )
2146 .ok();
2147 let schedules: Vec<Py<PyAny>> = deck
2148 .schedules
2149 .iter()
2150 .map(|s| {
2151 let d = PyDict::new(py);
2152 let items: Vec<Vec<String>> = s.items.iter().map(|it| it.tokens.clone()).collect();
2153 d.set_item("name", &s.name).ok();
2154 d.set_item("items", items).ok();
2155 d.into_any().unbind()
2156 })
2157 .collect();
2158 out.set_item("schedules", schedules).ok();
2159 let histories: Vec<Py<PyAny>> = deck
2160 .pulse_histories
2161 .iter()
2162 .map(|h| {
2163 let d = PyDict::new(py);
2164 let levels: Vec<Py<PyAny>> = h
2165 .levels
2166 .iter()
2167 .map(|l| {
2168 let e = PyDict::new(py);
2169 e.set_item("pulses", l.pulses).ok();
2170 e.set_item("delay_s", l.delay_s).ok();
2171 e.into_any().unbind()
2172 })
2173 .collect();
2174 d.set_item("name", &h.name).ok();
2175 d.set_item("levels", levels).ok();
2176 d.into_any().unbind()
2177 })
2178 .collect();
2179 out.set_item("pulse_histories", histories).ok();
2180 let outputs: Vec<Py<PyAny>> = deck
2181 .outputs
2182 .iter()
2183 .map(|o| {
2184 let d = PyDict::new(py);
2185 d.set_item("resolution", &o.resolution).ok();
2186 d.set_item("entries", o.entries.clone()).ok();
2187 d.into_any().unbind()
2188 })
2189 .collect();
2190 out.set_item("outputs", outputs).ok();
2191 out.set_item("truncation", deck.truncation.as_ref().map(|t| t.tolerance))
2192 .ok();
2193 out.into_any().unbind()
2194}
2195
2196fn mixture_to_py(py: Python<'_>, mix: &nucleide_alara_io::deck::Mixture) -> Py<PyAny> {
2197 use pyo3::types::PyDict;
2198 let entries: Vec<Py<PyAny>> = mix
2199 .entries
2200 .iter()
2201 .map(|e| mixture_entry_to_py(py, e))
2202 .collect();
2203 let d = PyDict::new(py);
2204 d.set_item("name", &mix.name).ok();
2205 d.set_item("entries", entries).ok();
2206 d.into_any().unbind()
2207}
2208
2209fn mixture_entry_to_py(py: Python<'_>, entry: &nucleide_alara_io::deck::MixtureEntry) -> Py<PyAny> {
2210 use nucleide_alara_io::deck::MixtureEntry as E;
2211 use pyo3::types::PyDict;
2212 let d = PyDict::new(py);
2213 match entry {
2214 E::Material {
2215 name,
2216 rel_density,
2217 vol_fraction,
2218 } => {
2219 d.set_item("kind", "material").ok();
2220 d.set_item("name", name).ok();
2221 d.set_item("rel_density", *rel_density).ok();
2222 d.set_item("vol_fraction", *vol_fraction).ok();
2223 }
2224 E::Element {
2225 symbol,
2226 rel_density,
2227 vol_fraction,
2228 } => {
2229 d.set_item("kind", "element").ok();
2230 d.set_item("symbol", symbol).ok();
2231 d.set_item("rel_density", *rel_density).ok();
2232 d.set_item("vol_fraction", *vol_fraction).ok();
2233 }
2234 E::Like {
2235 mixture,
2236 rel_density,
2237 } => {
2238 d.set_item("kind", "like").ok();
2239 d.set_item("mixture", mixture).ok();
2240 d.set_item("rel_density", *rel_density).ok();
2241 }
2242 E::Target { target_kind, name } => {
2243 d.set_item("kind", "target").ok();
2244 d.set_item("target_kind", target_kind).ok();
2245 d.set_item("name", name).ok();
2246 }
2247 }
2248 d.into_any().unbind()
2249}
2250
2251fn fluxdef_to_py(py: Python<'_>, flux: &nucleide_alara_io::deck::FluxDef) -> Py<PyAny> {
2252 use pyo3::types::PyDict;
2253 let d = PyDict::new(py);
2254 d.set_item("name", &flux.name).ok();
2255 d.set_item("file", &flux.file).ok();
2256 d.set_item("scale", flux.scale).ok();
2257 d.set_item("skip", flux.skip).ok();
2258 d.set_item("format", &flux.format).ok();
2259 d.into_any().unbind()
2260}
2261
2262#[pyfunction]
2267fn alara_parse_flux(py: Python<'_>, text: &str, name: &str) -> PyResult<Py<PyAny>> {
2268 let owned_text = text.to_owned();
2269 let owned_name = name.to_owned();
2270 let spectra = py
2271 .detach(move || nucleide_alara_io::FluxSpectra::parse(&owned_name, &owned_text))
2272 .map_err(ala_err)?;
2273 use pyo3::types::PyDict;
2274 let d = PyDict::new(py);
2275 d.set_item("name", spectra.name.clone()).ok();
2276 d.set_item("groups_per_interval", spectra.groups_per_interval)
2277 .ok();
2278 d.set_item("num_intervals", spectra.num_intervals()).ok();
2279 let totals: Vec<f64> = spectra.intervals.iter().map(|iv| iv.iter().sum()).collect();
2280 d.set_item("totals", totals).ok();
2281 d.set_item("total", spectra.total()).ok();
2282 d.set_item("intervals", spectra.intervals.clone()).ok();
2283 Ok(d.into_any().unbind())
2284}
2285
2286#[pyfunction]
2292fn alara_parse_output(
2293 py: Python<'_>,
2294 text: &str,
2295 run_lbl: &str,
2296) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
2297 let owned_text = text.to_owned();
2298 let owned_lbl = run_lbl.to_owned();
2299 let rows = py
2300 .detach(move || {
2301 nucleide_alara_io::output::ResponseFrame::parse(&owned_text, &owned_lbl).map(|f| f.rows)
2302 })
2303 .map_err(ala_err)?;
2304 Ok(rows
2305 .iter()
2306 .map(|r| {
2307 let mut d = BTreeMap::new();
2308 d.insert(
2309 "time_s".to_string(),
2310 r.time_s.into_pyobject(py).unwrap().unbind().into_any(),
2311 );
2312 d.insert(
2313 "time_label".to_string(),
2314 r.time_label
2315 .clone()
2316 .into_pyobject(py)
2317 .unwrap()
2318 .unbind()
2319 .into_any(),
2320 );
2321 d.insert(
2322 "nuclide".to_string(),
2323 r.nuclide
2324 .clone()
2325 .into_pyobject(py)
2326 .unwrap()
2327 .unbind()
2328 .into_any(),
2329 );
2330 d.insert(
2331 "half_life_s".to_string(),
2332 r.half_life_s.into_pyobject(py).unwrap().unbind().into_any(),
2333 );
2334 d.insert(
2335 "run_lbl".to_string(),
2336 r.run_lbl
2337 .clone()
2338 .into_pyobject(py)
2339 .unwrap()
2340 .unbind()
2341 .into_any(),
2342 );
2343 d.insert(
2344 "block".to_string(),
2345 r.block
2346 .as_str()
2347 .into_pyobject(py)
2348 .unwrap()
2349 .unbind()
2350 .into_any(),
2351 );
2352 d.insert(
2353 "block_name".to_string(),
2354 r.block_name
2355 .clone()
2356 .into_pyobject(py)
2357 .unwrap()
2358 .unbind()
2359 .into_any(),
2360 );
2361 d.insert(
2362 "block_num".to_string(),
2363 r.block_num.into_pyobject(py).unwrap().unbind().into_any(),
2364 );
2365 d.insert(
2366 "variable".to_string(),
2367 r.variable
2368 .as_str()
2369 .into_pyobject(py)
2370 .unwrap()
2371 .unbind()
2372 .into_any(),
2373 );
2374 d.insert(
2375 "var_unit".to_string(),
2376 r.var_unit
2377 .clone()
2378 .into_pyobject(py)
2379 .unwrap()
2380 .unbind()
2381 .into_any(),
2382 );
2383 d.insert(
2384 "value".to_string(),
2385 r.value.into_pyobject(py).unwrap().unbind().into_any(),
2386 );
2387 d
2388 })
2389 .collect())
2390}
2391
2392#[pyfunction]
2399#[pyo3(signature = (deck_text, top=None))]
2400fn alara_expand_schedule(
2401 py: Python<'_>,
2402 deck_text: &str,
2403 top: Option<&str>,
2404) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
2405 let owned_text = deck_text.to_owned();
2406 let owned_top = top.map(str::to_owned);
2407 let steps = py
2408 .detach(move || expand_deck_schedules(&owned_text, owned_top.as_deref()))
2409 .map_err(PyValueError::new_err)?;
2410 Ok(steps
2411 .into_iter()
2412 .map(|s| {
2413 let mut d = BTreeMap::new();
2414 let cooling = s.is_cooling();
2415 d.insert(
2416 "duration_s".to_string(),
2417 s.duration_s.into_pyobject(py).unwrap().unbind().into_any(),
2418 );
2419 d.insert(
2420 "flux".to_string(),
2421 s.flux
2422 .clone()
2423 .into_pyobject(py)
2424 .unwrap()
2425 .unbind()
2426 .into_any(),
2427 );
2428 d.insert(
2429 "is_cooling".to_string(),
2430 pyo3::types::PyBool::new(py, cooling)
2431 .to_owned()
2432 .into_any()
2433 .unbind(),
2434 );
2435 d
2436 })
2437 .collect())
2438}
2439
2440fn expand_deck_schedules(
2441 deck_text: &str,
2442 top: Option<&str>,
2443) -> Result<Vec<nucleide_alara_io::FlatStep>, String> {
2444 let deck = nucleide_alara_io::AlaraDeck::parse(deck_text).map_err(|e| e.to_string())?;
2445 let mut scheds = Vec::with_capacity(deck.schedules.len());
2446 for raw in &deck.schedules {
2447 let mut items = Vec::with_capacity(raw.items.len());
2448 for entry in &raw.items {
2449 items.push(
2450 parse_deck_sched_item(&entry.tokens)
2451 .map_err(|m| format!("schedule `{}` line {}: {m}", raw.name, entry.line))?,
2452 );
2453 }
2454 scheds.push(nucleide_alara_io::schedule::ScheduleDef {
2455 name: raw.name.clone(),
2456 items,
2457 });
2458 }
2459 let histories: Vec<nucleide_alara_io::schedule::PulseHistory> = deck
2460 .pulse_histories
2461 .iter()
2462 .map(|h| nucleide_alara_io::schedule::PulseHistory {
2463 name: h.name.clone(),
2464 levels: h
2465 .levels
2466 .iter()
2467 .map(|l| nucleide_alara_io::schedule::PulseLevel {
2468 count: l.pulses,
2469 delay_s: l.delay_s,
2470 })
2471 .collect(),
2472 })
2473 .collect();
2474 match top {
2475 Some(name) => {
2476 nucleide_alara_io::expand_from(name, &scheds, &histories).map_err(|e| e.to_string())
2477 }
2478 None => nucleide_alara_io::expand(&scheds, &histories).map_err(|e| e.to_string()),
2479 }
2480}
2481
2482fn parse_deck_sched_item(tokens: &[String]) -> Result<nucleide_alara_io::SchedItem, String> {
2483 match tokens {
2484 [op_text, op_unit, flux, history, delay_text, delay_unit] => {
2485 let op: f64 = op_text
2486 .parse()
2487 .map_err(|_| format!("expected operating time, found `{op_text}`"))?;
2488 let delay: f64 = delay_text
2489 .parse()
2490 .map_err(|_| format!("expected delay, found `{delay_text}`"))?;
2491 let op_time_s =
2492 nucleide_alara_io::parse_time_to_seconds(op, op_unit).map_err(|e| e.to_string())?;
2493 let delay_s = nucleide_alara_io::parse_time_to_seconds(delay, delay_unit)
2494 .map_err(|e| e.to_string())?;
2495 Ok(nucleide_alara_io::SchedItem::Pulse {
2496 op_time_s,
2497 flux: flux.clone(),
2498 history: history.clone(),
2499 delay_s,
2500 })
2501 }
2502 [name, history, delay_text, delay_unit] => {
2503 let delay: f64 = delay_text
2504 .parse()
2505 .map_err(|_| format!("expected delay, found `{delay_text}`"))?;
2506 let delay_s = nucleide_alara_io::parse_time_to_seconds(delay, delay_unit)
2507 .map_err(|e| e.to_string())?;
2508 Ok(nucleide_alara_io::SchedItem::SubSchedule {
2509 name: name.clone(),
2510 history: history.clone(),
2511 delay_s,
2512 })
2513 }
2514 _ => Err(format!(
2515 "expected 4- or 6-token schedule item, found {}",
2516 tokens.join(" ")
2517 )),
2518 }
2519}
2520
2521#[pyfunction]
2527fn half_life(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2528 lookup(key, nucleide_nuclei::data::half_life)
2529}
2530
2531#[pyfunction]
2533fn decay_constant(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2534 lookup(key, nucleide_nuclei::data::decay_constant)
2535}
2536
2537#[pyfunction]
2539fn q_value_capture(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2540 lookup(key, nucleide_nuclei::data::q_value_neutron_capture)
2541}
2542
2543#[pyfunction]
2545fn q_value_alpha(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2546 lookup(key, nucleide_nuclei::data::q_value_alpha)
2547}
2548
2549#[pyfunction]
2553fn read_inp(path: &str) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
2554 let mats = nucleide_mcnp_io::inp::materials_from_file(path)
2555 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2556 Python::attach(|py| {
2557 Ok(mats
2558 .into_iter()
2559 .map(|m| {
2560 let mut d = BTreeMap::new();
2561 d.insert(
2562 "number".to_string(),
2563 m.number.into_pyobject(py).unwrap().unbind().into_any(),
2564 );
2565 let fr: BTreeMap<String, f64> = m
2566 .fractions
2567 .iter()
2568 .map(|(id, f)| (id.to_name(), *f))
2569 .collect();
2570 d.insert(
2571 "fractions".to_string(),
2572 fr.into_pyobject(py).unwrap().unbind().into_any(),
2573 );
2574 d.insert(
2575 "fraction_type".to_string(),
2576 match m.fraction_type {
2577 nucleide_mcnp_io::inp::FracKind::Atom => "atom",
2578 nucleide_mcnp_io::inp::FracKind::Mass => "mass",
2579 }
2580 .into_pyobject(py)
2581 .unwrap()
2582 .unbind()
2583 .into_any(),
2584 );
2585 d.insert(
2586 "density".to_string(),
2587 m.density.into_pyobject(py).unwrap().unbind().into_any(),
2588 );
2589 d.insert(
2590 "comments".to_string(),
2591 m.comments
2592 .join(" ")
2593 .into_pyobject(py)
2594 .unwrap()
2595 .unbind()
2596 .into_any(),
2597 );
2598 d
2599 })
2600 .collect())
2601 })
2602}
2603
2604fn comp_to_material(comp: BTreeMap<String, f64>) -> PyResult<nucleide_material::Material> {
2605 let mut mat = nucleide_material::Material::new();
2606 for (name, grams) in &comp {
2607 let id = nucleide_nuclei::NuclideId::from_name(name)
2608 .map_err(|e| PyValueError::new_err(format!("`{name}`: {e}")))?;
2609 mat.add_nuclide(id, *grams);
2610 }
2611 Ok(mat)
2612}
2613
2614#[pyfunction]
2617fn from_formula(formula: &str) -> PyResult<BTreeMap<String, f64>> {
2618 use nucleide_material::AbundanceProvider;
2619 let parsed = nucleide_material::parse_formula(formula)
2620 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2621 let mut nat = Vec::new();
2623 for (z, count) in &parsed {
2624 if let Some(isotopes) = nucleide_material::NaturalAbundances.natural_isotopes(*z) {
2625 for (id, frac) in isotopes {
2626 nat.push((id, frac * count));
2627 }
2628 }
2629 }
2630 let total: f64 = nat.iter().map(|(_, c)| c).sum();
2631 if total <= 0.0 {
2632 return Err(PyValueError::new_err("empty formula expansion"));
2633 }
2634 let mut out: BTreeMap<String, f64> = BTreeMap::new();
2635 for (id, atoms) in nat {
2636 *out.entry(id.to_name()).or_insert(0.0) += atoms / total;
2637 }
2638 Ok(out)
2639}
2640
2641#[pyfunction]
2644fn activity(comp: BTreeMap<String, f64>) -> PyResult<BTreeMap<String, f64>> {
2645 let mat = comp_to_material(comp)?;
2646 let analytics = nucleide_material::Analytics {
2647 masses: &nucleide_material::Ame2020,
2648 decays: &nucleide_material::ChainDecays,
2649 };
2650 let per_nuc = mat
2651 .activity(&analytics)
2652 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2653 let specific = mat
2654 .specific_activity(&analytics)
2655 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2656 let mut out: BTreeMap<String, f64> = per_nuc
2657 .into_iter()
2658 .map(|(id, v)| (id.to_name(), v))
2659 .collect();
2660 out.insert("specific".to_string(), specific);
2661 Ok(out)
2662}
2663
2664#[pyfunction]
2666fn to_xml(comp: BTreeMap<String, f64>, name: &str, density: f64, units: &str) -> PyResult<String> {
2667 let mat = comp_to_material(comp)?;
2668 mat.to_xml(name, density, units)
2669 .map_err(|e| PyValueError::new_err(e.to_string()))
2670}
2671
2672#[pyclass(name = "Cascade")]
2674struct PyCascade {
2675 inner: std::sync::Mutex<nucleide_enrichment::Cascade>,
2676}
2677
2678#[pymethods]
2679impl PyCascade {
2680 #[staticmethod]
2682 fn default_uranium() -> Self {
2683 Self {
2684 inner: std::sync::Mutex::new(nucleide_enrichment::default_uranium_cascade()),
2685 }
2686 }
2687
2688 #[new]
2691 #[allow(non_snake_case)]
2692 #[allow(clippy::too_many_arguments)]
2693 fn new(
2694 alpha: f64,
2695 Mstar: f64,
2696 j: u32,
2697 k: u32,
2698 N: f64,
2699 M: f64,
2700 x_feed_j: f64,
2701 x_prod_j: f64,
2702 x_tail_j: f64,
2703 mat_feed: BTreeMap<String, f64>,
2704 ) -> PyResult<Self> {
2705 let mut feed = BTreeMap::new();
2706 for (name, frac) in mat_feed {
2707 let id = NuclideId::from_name(&name).map_err(wrap_nucid_err)?;
2708 feed.insert(id, frac);
2709 }
2710 let casc = nucleide_enrichment::Cascade {
2711 alpha,
2712 Mstar,
2713 j: NuclideId::from_nucid(j),
2714 k: NuclideId::from_nucid(k),
2715 N,
2716 M,
2717 x_feed_j,
2718 x_prod_j,
2719 x_tail_j,
2720 mat_feed: nucleide_enrichment::Stream::with_total_mass(feed, 1.0),
2721 mat_prod: nucleide_enrichment::Stream::new(),
2722 mat_tail: nucleide_enrichment::Stream::new(),
2723 l_t_per_feed: 0.0,
2724 swu_per_feed: 0.0,
2725 swu_per_prod: 0.0,
2726 };
2727 Ok(Self {
2728 inner: std::sync::Mutex::new(casc),
2729 })
2730 }
2731
2732 #[pyo3(signature = (tolerance=None, max_iterations=None))]
2734 fn solve(&self, tolerance: Option<f64>, max_iterations: Option<u32>) -> PyResult<()> {
2735 let tol = tolerance.unwrap_or(nucleide_enrichment::DEFAULT_TOLERANCE);
2736 let iters = max_iterations.unwrap_or(nucleide_enrichment::DEFAULT_MAX_ITER);
2737 let mut c = self
2738 .inner
2739 .lock()
2740 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
2741 *c = nucleide_enrichment::solve_numeric(&c, tol, iters)
2742 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2743 Ok(())
2744 }
2745
2746 #[pyo3(signature = (tolerance=None, max_iterations=None))]
2748 fn solve_multicomponent(
2749 &self,
2750 tolerance: Option<f64>,
2751 max_iterations: Option<u32>,
2752 ) -> PyResult<()> {
2753 let tol = tolerance.unwrap_or(nucleide_enrichment::DEFAULT_TOLERANCE);
2754 let iters = max_iterations.unwrap_or(nucleide_enrichment::DEFAULT_MAX_ITER);
2755 let mut c = self
2756 .inner
2757 .lock()
2758 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
2759 *c = nucleide_enrichment::multicomponent(&c, tol, iters)
2760 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2761 Ok(())
2762 }
2763
2764 #[getter]
2765 fn alpha(&self) -> PyResult<f64> {
2766 Ok(self
2767 .inner
2768 .lock()
2769 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2770 .alpha)
2771 }
2772 #[getter]
2773 #[allow(non_snake_case)]
2774 fn Mstar(&self) -> PyResult<f64> {
2775 Ok(self
2776 .inner
2777 .lock()
2778 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2779 .Mstar)
2780 }
2781 #[getter]
2782 #[allow(non_snake_case)]
2783 fn N(&self) -> PyResult<f64> {
2784 Ok(self
2785 .inner
2786 .lock()
2787 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2788 .N)
2789 }
2790 #[getter]
2791 #[allow(non_snake_case)]
2792 fn M(&self) -> PyResult<f64> {
2793 Ok(self
2794 .inner
2795 .lock()
2796 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2797 .M)
2798 }
2799 #[getter]
2800 fn x_feed_j(&self) -> PyResult<f64> {
2801 Ok(self
2802 .inner
2803 .lock()
2804 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2805 .x_feed_j)
2806 }
2807 #[getter]
2808 fn x_prod_j(&self) -> PyResult<f64> {
2809 Ok(self
2810 .inner
2811 .lock()
2812 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2813 .x_prod_j)
2814 }
2815 #[getter]
2816 fn x_tail_j(&self) -> PyResult<f64> {
2817 Ok(self
2818 .inner
2819 .lock()
2820 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2821 .x_tail_j)
2822 }
2823 #[getter]
2824 fn l_t_per_feed(&self) -> PyResult<f64> {
2825 Ok(self
2826 .inner
2827 .lock()
2828 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2829 .l_t_per_feed)
2830 }
2831 #[getter]
2832 fn swu_per_feed(&self) -> PyResult<f64> {
2833 Ok(self
2834 .inner
2835 .lock()
2836 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2837 .swu_per_feed)
2838 }
2839 #[getter]
2840 fn swu_per_prod(&self) -> PyResult<f64> {
2841 Ok(self
2842 .inner
2843 .lock()
2844 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2845 .swu_per_prod)
2846 }
2847 #[getter]
2849 fn mat_feed(&self) -> PyResult<BTreeMap<String, f64>> {
2850 Ok(self
2851 .inner
2852 .lock()
2853 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2854 .mat_feed
2855 .comp
2856 .iter()
2857 .map(|(id, frac)| (id.to_name(), *frac))
2858 .collect())
2859 }
2860 #[getter]
2862 fn mat_prod(&self) -> PyResult<BTreeMap<String, f64>> {
2863 Ok(self
2864 .inner
2865 .lock()
2866 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2867 .mat_prod
2868 .comp
2869 .iter()
2870 .map(|(id, frac)| (id.to_name(), *frac))
2871 .collect())
2872 }
2873 #[getter]
2875 fn mat_tail(&self) -> PyResult<BTreeMap<String, f64>> {
2876 Ok(self
2877 .inner
2878 .lock()
2879 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
2880 .mat_tail
2881 .comp
2882 .iter()
2883 .map(|(id, frac)| (id.to_name(), *frac))
2884 .collect())
2885 }
2886 fn separative_work_per_product(&self) -> PyResult<f64> {
2888 let c = self
2889 .inner
2890 .lock()
2891 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
2892 Ok(nucleide_enrichment::swu_per_prod(
2893 c.x_feed_j, c.x_prod_j, c.x_tail_j,
2894 ))
2895 }
2896
2897 fn __repr__(&self) -> PyResult<String> {
2898 let c = self
2899 .inner
2900 .lock()
2901 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
2902 Ok(format!(
2903 "Cascade(alpha={}, Mstar={}, x_prod_j={:.5})",
2904 c.alpha, c.Mstar, c.x_prod_j
2905 ))
2906 }
2907}
2908
2909#[pyfunction]
2913fn enrichment_value_func(x: f64) -> f64 {
2914 nucleide_enrichment::value_func(x)
2915}
2916
2917#[pyfunction]
2921fn enrichment_swu_per_feed(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
2922 nucleide_enrichment::swu_per_feed(x_feed, x_prod, x_tail)
2923}
2924
2925#[pyfunction]
2929fn enrichment_swu_per_prod(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
2930 nucleide_enrichment::swu_per_prod(x_feed, x_prod, x_tail)
2931}
2932
2933#[pyfunction]
2937fn enrichment_swu_per_tail(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
2938 nucleide_enrichment::swu_per_tail(x_feed, x_prod, x_tail)
2939}
2940
2941#[pyclass(name = "MaterialsCompendium")]
2943struct PyMaterialsCompendium {
2944 inner: nucleide_material::MaterialsLibrary,
2945}
2946
2947#[pymethods]
2948impl PyMaterialsCompendium {
2949 #[staticmethod]
2951 fn load(path: &str) -> PyResult<Self> {
2952 nucleide_material::MaterialsLibrary::from_file(path)
2953 .map(|inner| PyMaterialsCompendium { inner })
2954 .map_err(|e| PyValueError::new_err(e.to_string()))
2955 }
2956
2957 fn __len__(&self) -> usize {
2958 self.inner.len()
2959 }
2960
2961 fn names(&self) -> Vec<String> {
2963 self.inner.names().into_iter().map(String::from).collect()
2964 }
2965
2966 #[pyo3(signature = (name, as_material=false))]
2970 #[allow(clippy::type_complexity)]
2971 fn get(&self, name: &str, as_material: bool) -> PyResult<Option<BTreeMap<String, Py<PyAny>>>> {
2972 let entry = match self.inner.get(name) {
2973 Some(e) => e,
2974 None => return Ok(None),
2975 };
2976 let named_fractions = if as_material {
2978 Some(
2979 entry
2980 .to_material()
2981 .map_err(|e| PyValueError::new_err(e.to_string()))?,
2982 )
2983 } else {
2984 None
2985 };
2986
2987 Ok(Python::attach(|py| {
2988 let mut d: BTreeMap<String, Py<PyAny>> = BTreeMap::new();
2989 d.insert(
2990 "name".into(),
2991 entry
2992 .name
2993 .as_str()
2994 .into_pyobject(py)
2995 .unwrap()
2996 .unbind()
2997 .into_any(),
2998 );
2999 d.insert(
3000 "mat_num".into(),
3001 entry.mat_num.into_pyobject(py).unwrap().unbind().into_any(),
3002 );
3003 d.insert(
3004 "density".into(),
3005 entry.density.into_pyobject(py).unwrap().unbind().into_any(),
3006 );
3007 match &named_fractions {
3008 Some(mat) => {
3009 let fr: BTreeMap<String, f64> =
3010 mat.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect();
3011 d.insert(
3012 "fractions".into(),
3013 fr.into_pyobject(py).unwrap().unbind().into_any(),
3014 );
3015 }
3016 None => {
3017 let fr = entry.weight_fractions();
3018 d.insert(
3019 "fractions".into(),
3020 fr.into_pyobject(py).unwrap().unbind().into_any(),
3021 );
3022 }
3023 }
3024 Some(d)
3025 }))
3026 }
3027}
3028
3029#[pyfunction]
3038fn isotxs_parse(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
3039 let owned = text.to_owned();
3040 let lib = py
3041 .detach(move || nucleide_cccc_io::IsotxsLib::parse(&owned))
3042 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3043 Ok(isotxs_to_py(py, &lib))
3044}
3045
3046fn isotxs_to_py(py: Python<'_>, lib: &nucleide_cccc_io::IsotxsLib) -> Py<PyAny> {
3047 use pyo3::types::PyDict;
3048 let out = PyDict::new(py);
3049 let nuclides: Vec<Py<PyAny>> = lib
3050 .nuclides
3051 .iter()
3052 .map(|n| {
3053 let d = PyDict::new(py);
3054 d.set_item("label", &n.label).ok();
3055 d.set_item("zaid", &n.zaid).ok();
3056 d.set_item("groups", n.groups).ok();
3057 d.set_item("total_xs", n.total_xs.clone()).ok();
3058 d.into_any().unbind()
3059 })
3060 .collect();
3061 out.set_item("nuclides", nuclides).ok();
3062 out.into_any().unbind()
3063}
3064
3065#[pyfunction]
3071#[pyo3(signature = (text, kind="rtflux"))]
3072fn rtflux_parse(py: Python<'_>, text: &str, kind: &str) -> PyResult<Py<PyAny>> {
3073 let flux_kind = match kind.to_ascii_lowercase().as_str() {
3074 "rtflux" => nucleide_cccc_io::rtflux::FluxKind::Rtflux,
3075 "atflux" => nucleide_cccc_io::rtflux::FluxKind::Atflux,
3076 "rzflux" => nucleide_cccc_io::rtflux::FluxKind::Rzflux,
3077 other => {
3078 return Err(PyValueError::new_err(format!(
3079 "kind must be rtflux|atflux|rzflux, got `{other}`"
3080 )))
3081 }
3082 };
3083 let owned = text.to_owned();
3084 let flux = py
3085 .detach(move || nucleide_cccc_io::FluxFile::parse(flux_kind, &owned))
3086 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3087 use pyo3::types::PyDict;
3088 let d = PyDict::new(py);
3089 d.set_item("kind", flux.kind.keyword()).ok();
3090 d.set_item("groups", flux.groups).ok();
3091 d.set_item("per_point", flux.per_point).ok();
3092 d.set_item("npoints", flux.npoints()).ok();
3093 d.set_item("values", flux.values.clone()).ok();
3094 d.set_item("total", flux.total()).ok();
3095 Ok(d.into_any().unbind())
3096}
3097
3098fn partisn_deck_from_dict(
3099 deck: &Bound<'_, pyo3::types::PyDict>,
3100) -> PyResult<nucleide_cccc_io::PartisnDeck> {
3101 let title: String = match deck.get_item("title")? {
3102 Some(v) => v
3103 .extract()
3104 .map_err(|_| PyValueError::new_err("partisn deck `title` must be str"))?,
3105 None => return Err(PyValueError::new_err("partisn deck missing `title`")),
3106 };
3107 let dim: u8 = match deck.get_item("dim")? {
3108 Some(v) => v
3109 .extract()
3110 .map_err(|_| PyValueError::new_err("partisn deck `dim` must be 1, 2, or 3"))?,
3111 None => return Err(PyValueError::new_err("partisn deck missing `dim`")),
3112 };
3113 let zones_value = match deck.get_item("zones")? {
3114 Some(v) => v,
3115 None => return Err(PyValueError::new_err("partisn deck missing `zones`")),
3116 };
3117 let zone_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = zones_value
3118 .extract()
3119 .map_err(|_| PyValueError::new_err("partisn deck `zones` must be a list of dicts"))?;
3120 let mut zones = Vec::with_capacity(zone_dicts.len());
3121 for z in &zone_dicts {
3122 let id: u32 = match z.get_item("id")? {
3123 Some(v) => v
3124 .extract()
3125 .map_err(|_| PyValueError::new_err("partisn zone `id` must be int"))?,
3126 None => return Err(PyValueError::new_err("partisn zone missing `id`")),
3127 };
3128 let material: String = match z.get_item("material")? {
3129 Some(v) => v
3130 .extract()
3131 .map_err(|_| PyValueError::new_err("partisn zone `material` must be str"))?,
3132 None => return Err(PyValueError::new_err("partisn zone missing `material`")),
3133 };
3134 let isotxs_labels: Vec<String> = match z.get_item("isotxs_labels")? {
3135 Some(v) => v.extract().map_err(|_| {
3136 PyValueError::new_err("partisn zone `isotxs_labels` must be a list of str")
3137 })?,
3138 None => {
3139 return Err(PyValueError::new_err(
3140 "partisn zone missing `isotxs_labels`",
3141 ))
3142 }
3143 };
3144 let density: f64 = match z.get_item("density")? {
3145 Some(v) => v
3146 .extract()
3147 .map_err(|_| PyValueError::new_err("partisn zone `density` must be float"))?,
3148 None => return Err(PyValueError::new_err("partisn zone missing `density`")),
3149 };
3150 zones.push(nucleide_cccc_io::partisn::PartisnZone {
3151 id,
3152 material,
3153 isotxs_labels,
3154 density,
3155 });
3156 }
3157 let source: Option<String> = match deck.get_item("source")? {
3158 Some(v) if v.is_none() => None,
3159 Some(v) => Some(
3160 v.extract()
3161 .map_err(|_| PyValueError::new_err("partisn deck `source` must be str or None"))?,
3162 ),
3163 None => None,
3164 };
3165 Ok(nucleide_cccc_io::PartisnDeck {
3166 title,
3167 dim,
3168 zones,
3169 source,
3170 })
3171}
3172
3173#[pyfunction]
3178fn partisn_render(py: Python<'_>, deck: &Bound<'_, pyo3::types::PyDict>) -> PyResult<String> {
3179 let rust_deck = partisn_deck_from_dict(deck)?;
3180 Ok(py.detach(move || rust_deck.render()))
3181}
3182
3183#[pyfunction]
3188fn partisn_validate(
3189 py: Python<'_>,
3190 deck: &Bound<'_, pyo3::types::PyDict>,
3191 isotxs_text: &str,
3192) -> PyResult<()> {
3193 let rust_deck = partisn_deck_from_dict(deck)?;
3194 let owned = isotxs_text.to_owned();
3195 let lib = py
3196 .detach(move || nucleide_cccc_io::IsotxsLib::parse(&owned))
3197 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3198 rust_deck
3199 .validate(&lib)
3200 .map_err(|e| PyValueError::new_err(e.to_string()))
3201}
3202
3203fn fispact_row_to_map(
3208 py: Python<'_>,
3209 r: &nucleide_alara_io::output::ResponseRow,
3210) -> BTreeMap<String, Py<PyAny>> {
3211 let mut d = BTreeMap::new();
3212 d.insert(
3213 "time_s".to_string(),
3214 r.time_s.into_pyobject(py).unwrap().unbind().into_any(),
3215 );
3216 d.insert(
3217 "time_label".to_string(),
3218 r.time_label
3219 .clone()
3220 .into_pyobject(py)
3221 .unwrap()
3222 .unbind()
3223 .into_any(),
3224 );
3225 d.insert(
3226 "nuclide".to_string(),
3227 r.nuclide
3228 .clone()
3229 .into_pyobject(py)
3230 .unwrap()
3231 .unbind()
3232 .into_any(),
3233 );
3234 d.insert(
3235 "half_life_s".to_string(),
3236 r.half_life_s.into_pyobject(py).unwrap().unbind().into_any(),
3237 );
3238 d.insert(
3239 "run_lbl".to_string(),
3240 r.run_lbl
3241 .clone()
3242 .into_pyobject(py)
3243 .unwrap()
3244 .unbind()
3245 .into_any(),
3246 );
3247 d.insert(
3248 "block".to_string(),
3249 r.block
3250 .as_str()
3251 .into_pyobject(py)
3252 .unwrap()
3253 .unbind()
3254 .into_any(),
3255 );
3256 d.insert(
3257 "block_name".to_string(),
3258 r.block_name
3259 .clone()
3260 .into_pyobject(py)
3261 .unwrap()
3262 .unbind()
3263 .into_any(),
3264 );
3265 d.insert(
3266 "block_num".to_string(),
3267 r.block_num.into_pyobject(py).unwrap().unbind().into_any(),
3268 );
3269 d.insert(
3270 "variable".to_string(),
3271 r.variable
3272 .as_str()
3273 .into_pyobject(py)
3274 .unwrap()
3275 .unbind()
3276 .into_any(),
3277 );
3278 d.insert(
3279 "var_unit".to_string(),
3280 r.var_unit
3281 .clone()
3282 .into_pyobject(py)
3283 .unwrap()
3284 .unbind()
3285 .into_any(),
3286 );
3287 d.insert(
3288 "value".to_string(),
3289 r.value.into_pyobject(py).unwrap().unbind().into_any(),
3290 );
3291 d
3292}
3293
3294#[pyfunction]
3300fn fispact_parse_output(
3301 py: Python<'_>,
3302 text: &str,
3303 run_lbl: &str,
3304) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
3305 let owned_text = text.to_owned();
3306 let owned_lbl = run_lbl.to_owned();
3307 let rows = py
3308 .detach(move || {
3309 nucleide_fispact_io::parse_to_frame(&owned_text, &owned_lbl).map(|f| f.rows)
3310 })
3311 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3312 Ok(rows.iter().map(|r| fispact_row_to_map(py, r)).collect())
3313}
3314
3315#[pyfunction]
3325fn origen_parse_tape5(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
3326 let owned = text.to_owned();
3327 let tape = py
3328 .detach(move || nucleide_origen_io::Tape5::parse(&owned))
3329 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3330 use pyo3::types::PyDict;
3331 let out = PyDict::new(py);
3332 out.set_item("titles", tape.titles.clone()).ok();
3333 let steps: Vec<Py<PyAny>> = tape
3334 .irradiation_steps
3335 .iter()
3336 .map(|s| {
3337 let d = PyDict::new(py);
3338 d.set_item("flux", s.flux).ok();
3339 d.set_item("days", s.days).ok();
3340 d.into_any().unbind()
3341 })
3342 .collect();
3343 out.set_item("irradiation_steps", steps).ok();
3344 let materials: Vec<Py<PyAny>> = tape
3345 .materials
3346 .iter()
3347 .map(|m| {
3348 let d = PyDict::new(py);
3349 d.set_item("name", &m.name).ok();
3350 let entries: Vec<Py<PyAny>> = m
3351 .grams
3352 .iter()
3353 .map(|(nuclide, grams)| {
3354 let e = PyDict::new(py);
3355 e.set_item("nuclide", nuclide).ok();
3356 e.set_item("grams", *grams).ok();
3357 e.into_any().unbind()
3358 })
3359 .collect();
3360 d.set_item("entries", entries).ok();
3361 d.into_any().unbind()
3362 })
3363 .collect();
3364 out.set_item("materials", materials).ok();
3365 Ok(out.into_any().unbind())
3366}
3367
3368#[pyfunction]
3373fn origen_parse_tape6(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
3374 let owned = text.to_owned();
3375 let tape = py
3376 .detach(move || nucleide_origen_io::Tape6::parse(&owned))
3377 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3378 use pyo3::types::PyDict;
3379 let out = PyDict::new(py);
3380 let records: Vec<Py<PyAny>> = tape
3381 .records
3382 .iter()
3383 .map(|r| {
3384 let d = PyDict::new(py);
3385 d.set_item("nuclide", &r.nuclide).ok();
3386 d.set_item("grams", r.grams).ok();
3387 d.set_item("activity_bq", r.activity_bq).ok();
3388 d.into_any().unbind()
3389 })
3390 .collect();
3391 out.set_item("records", records).ok();
3392 out.set_item("total_activity", tape.total_activity()).ok();
3393 Ok(out.into_any().unbind())
3394}
3395
3396#[pyfunction]
3400fn origen_parse_tape9(py: Python<'_>, text: &str) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
3401 let owned = text.to_owned();
3402 let entries = py
3403 .detach(move || nucleide_origen_io::Tape9Entry::parse(&owned))
3404 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3405 Ok(entries
3406 .iter()
3407 .map(|e| {
3408 let mut d = BTreeMap::new();
3409 d.insert(
3410 "nuclide".to_string(),
3411 e.nuclide
3412 .clone()
3413 .into_pyobject(py)
3414 .unwrap()
3415 .unbind()
3416 .into_any(),
3417 );
3418 d.insert(
3419 "decay_const".to_string(),
3420 e.decay_const.into_pyobject(py).unwrap().unbind().into_any(),
3421 );
3422 d
3423 })
3424 .collect())
3425}
3426
3427fn r2s_workflow_to_py(py: Python<'_>, workflow: &nucleide_r2s::R2sWorkflow) -> Py<PyAny> {
3432 use pyo3::types::PyDict;
3433 let out = PyDict::new(py);
3434 let steps: Vec<Py<PyAny>> = workflow
3435 .steps
3436 .iter()
3437 .map(|s| {
3438 let d = PyDict::new(py);
3439 d.set_item("zone", &s.zone).ok();
3440 d.set_item("flux", &s.flux).ok();
3441 d.into_any().unbind()
3442 })
3443 .collect();
3444 out.set_item("steps", steps).ok();
3445 out.set_item("cooling_s", workflow.cooling_s.clone()).ok();
3446 out.set_item("top_schedule", &workflow.top_schedule).ok();
3447 out.into_any().unbind()
3448}
3449
3450fn r2s_workflow_from_dict(
3451 workflow: &Bound<'_, pyo3::types::PyDict>,
3452) -> PyResult<nucleide_r2s::R2sWorkflow> {
3453 let steps_value = match workflow.get_item("steps")? {
3454 Some(v) => v,
3455 None => return Err(PyValueError::new_err("r2s workflow missing `steps`")),
3456 };
3457 let step_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = steps_value
3458 .extract()
3459 .map_err(|_| PyValueError::new_err("r2s workflow `steps` must be a list of dicts"))?;
3460 let mut steps = Vec::with_capacity(step_dicts.len());
3461 for s in &step_dicts {
3462 let zone: String = match s.get_item("zone")? {
3463 Some(v) => v
3464 .extract()
3465 .map_err(|_| PyValueError::new_err("r2s step `zone` must be str"))?,
3466 None => return Err(PyValueError::new_err("r2s step missing `zone`")),
3467 };
3468 let flux: String = match s.get_item("flux")? {
3469 Some(v) => v
3470 .extract()
3471 .map_err(|_| PyValueError::new_err("r2s step `flux` must be str"))?,
3472 None => return Err(PyValueError::new_err("r2s step missing `flux`")),
3473 };
3474 steps.push(nucleide_r2s::R2sStep { zone, flux });
3475 }
3476 let cooling_s: Vec<f64> = match workflow.get_item("cooling_s")? {
3477 Some(v) => v.extract().map_err(|_| {
3478 PyValueError::new_err("r2s workflow `cooling_s` must be a list of float")
3479 })?,
3480 None => return Err(PyValueError::new_err("r2s workflow missing `cooling_s`")),
3481 };
3482 let top_schedule: String = match workflow.get_item("top_schedule")? {
3483 Some(v) => v
3484 .extract()
3485 .map_err(|_| PyValueError::new_err("r2s workflow `top_schedule` must be str"))?,
3486 None => return Err(PyValueError::new_err("r2s workflow missing `top_schedule`")),
3487 };
3488 Ok(nucleide_r2s::R2sWorkflow {
3489 steps,
3490 cooling_s,
3491 top_schedule,
3492 })
3493}
3494
3495#[pyfunction]
3500fn r2s_from_deck(py: Python<'_>, deck_text: &str) -> PyResult<Py<PyAny>> {
3501 let owned = deck_text.to_owned();
3502 let workflow = py
3503 .detach(move || {
3504 let deck = nucleide_alara_io::AlaraDeck::parse(&owned)
3505 .map_err(|e| nucleide_r2s::Error::Invalid(e.to_string()))?;
3506 nucleide_r2s::R2sWorkflow::from_deck(&deck)
3507 })
3508 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3509 Ok(r2s_workflow_to_py(py, &workflow))
3510}
3511
3512#[pyfunction]
3517fn r2s_validate(
3518 py: Python<'_>,
3519 workflow: &Bound<'_, pyo3::types::PyDict>,
3520 deck_text: &str,
3521) -> PyResult<()> {
3522 let rust_workflow = r2s_workflow_from_dict(workflow)?;
3523 let owned = deck_text.to_owned();
3524 let deck = py
3525 .detach(move || nucleide_alara_io::AlaraDeck::parse(&owned))
3526 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3527 rust_workflow
3528 .validate_against(&deck)
3529 .map_err(|e| PyValueError::new_err(e.to_string()))
3530}
3531
3532#[pyfunction]
3537#[pyo3(signature = (deck_text, top=None))]
3538fn r2s_expand(
3539 py: Python<'_>,
3540 deck_text: &str,
3541 top: Option<&str>,
3542) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
3543 let owned_text = deck_text.to_owned();
3544 let owned_top = top.map(str::to_owned);
3545 let steps = py
3546 .detach(move || {
3547 let deck = nucleide_alara_io::AlaraDeck::parse(&owned_text)
3548 .map_err(|e| nucleide_r2s::Error::Invalid(e.to_string()))?;
3549 let mut workflow = nucleide_r2s::R2sWorkflow::from_deck(&deck)?;
3550 if let Some(top) = owned_top {
3551 workflow.top_schedule = top;
3552 }
3553 workflow.expand(&deck, &[])
3554 })
3555 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3556 Ok(steps
3557 .into_iter()
3558 .map(|s| {
3559 let mut d = BTreeMap::new();
3560 let cooling = s.is_cooling();
3561 d.insert(
3562 "duration_s".to_string(),
3563 s.duration_s.into_pyobject(py).unwrap().unbind().into_any(),
3564 );
3565 d.insert(
3566 "flux".to_string(),
3567 s.flux.into_pyobject(py).unwrap().unbind().into_any(),
3568 );
3569 d.insert(
3570 "is_cooling".to_string(),
3571 pyo3::types::PyBool::new(py, cooling)
3572 .to_owned()
3573 .into_any()
3574 .unbind(),
3575 );
3576 d
3577 })
3578 .collect())
3579}
3580
3581#[pyfunction]
3592fn r2s_assemble(
3593 py: Python<'_>,
3594 output_text: &str,
3595 run_lbl: &str,
3596 zone: &str,
3597 groups: usize,
3598) -> PyResult<Py<PyAny>> {
3599 let owned_text = output_text.to_owned();
3600 let owned_lbl = run_lbl.to_owned();
3601 let owned_zone = zone.to_owned();
3602 let source = py
3603 .detach(move || {
3604 let frame = nucleide_alara_io::output::ResponseFrame::parse(&owned_text, &owned_lbl)
3605 .map_err(|e| nucleide_r2s::Error::Invalid(e.to_string()))?;
3606 Ok::<_, nucleide_r2s::Error>(nucleide_r2s::photon::assemble(
3607 &frame,
3608 &owned_zone,
3609 groups,
3610 ))
3611 })
3612 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3613 use pyo3::types::PyDict;
3614 let out = PyDict::new(py);
3615 out.set_item("zone", source.zone.clone()).ok();
3616 out.set_item("groups", source.groups.clone()).ok();
3617 out.set_item("total", source.total()).ok();
3618 Ok(out.into_any().unbind())
3619}
3620
3621#[pyfunction]
3630#[pyo3(signature = (totals, zone_of_voxel, split=false))]
3631fn r2s_tag_zone_strength(
3632 py: Python<'_>,
3633 totals: Vec<f64>,
3634 zone_of_voxel: Vec<usize>,
3635 split: bool,
3636) -> PyResult<Py<PyAny>> {
3637 let zones: Vec<nucleide_r2s::photon::ZonePhotonSource> = totals
3638 .into_iter()
3639 .enumerate()
3640 .map(|(i, total)| {
3641 let groups = if total == 0.0 {
3642 Vec::new()
3643 } else {
3644 vec![total]
3645 };
3646 nucleide_r2s::photon::ZonePhotonSource {
3647 zone: format!("zone{i}"),
3648 groups,
3649 }
3650 })
3651 .collect();
3652 let tags = if split {
3653 nucleide_r2s::tags::split_zone_totals(&zones, &zone_of_voxel)
3654 } else {
3655 nucleide_r2s::tags::tag_zone_totals(&zones, &zone_of_voxel)
3656 }
3657 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3658 use pyo3::types::PyDict;
3659 let out = PyDict::new(py);
3660 out.set_item("n_zones", tags.n_zones).ok();
3661 out.set_item("zone_of_voxel", tags.zone_of_voxel.clone())
3662 .ok();
3663 out.set_item("source_strength", tags.source_strength.clone())
3664 .ok();
3665 out.set_item("decay_time_s", tags.decay_time_s.clone()).ok();
3666 out.set_item("total", tags.total_strength()).ok();
3667 Ok(out.into_any().unbind())
3668}
3669
3670#[pyfunction]
3679fn r2s_photon_group_sums(
3680 py: Python<'_>,
3681 photon_text: &str,
3682 nuclides: Vec<String>,
3683 time_s: f64,
3684) -> PyResult<Py<PyAny>> {
3685 let source = nucleide_alara_io::photon::PhotonSource::from_str(photon_text)
3686 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3687 let names: Vec<&str> = nuclides.iter().map(String::as_str).collect();
3688 let at = nucleide_r2s::tags::photon_groups_at(&source, &names, time_s);
3689 let sums = nucleide_r2s::tags::sum_group_strengths(&at)
3690 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3691 use pyo3::types::PyDict;
3692 let out = PyDict::new(py);
3693 let rows: Vec<Py<PyAny>> = at
3694 .iter()
3695 .map(|g| {
3696 let d = PyDict::new(py);
3697 d.set_item("nuclide", g.nuclide.clone()).ok();
3698 d.set_item("time_s", g.time_s).ok();
3699 d.set_item("strengths", g.strengths.clone()).ok();
3700 d.into_any().unbind()
3701 })
3702 .collect();
3703 out.set_item("groups", rows).ok();
3704 out.set_item("sums", sums.clone()).ok();
3705 out.set_item("total", sums.iter().sum::<f64>()).ok();
3706 Ok(out.into_any().unbind())
3707}
3708
3709fn snapshot_dict_str(
3710 zone: &Bound<'_, pyo3::types::PyDict>,
3711 key: &str,
3712 what: &str,
3713) -> PyResult<String> {
3714 match zone.get_item(key)? {
3715 Some(v) => v
3716 .extract()
3717 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be str"))),
3718 None => Err(PyValueError::new_err(format!(
3719 "snapshot {what} missing `{key}`"
3720 ))),
3721 }
3722}
3723
3724fn snapshot_dict_opt_str(
3725 zone: &Bound<'_, pyo3::types::PyDict>,
3726 key: &str,
3727 what: &str,
3728) -> PyResult<Option<String>> {
3729 match zone.get_item(key)? {
3730 Some(v) if v.is_none() => Ok(None),
3731 Some(v) => v
3732 .extract::<String>()
3733 .map(Some)
3734 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be str"))),
3735 None => Ok(None),
3736 }
3737}
3738
3739fn snapshot_dict_f64(
3740 zone: &Bound<'_, pyo3::types::PyDict>,
3741 key: &str,
3742 what: &str,
3743) -> PyResult<f64> {
3744 match zone.get_item(key)? {
3745 Some(v) => v
3746 .extract()
3747 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be float"))),
3748 None => Err(PyValueError::new_err(format!(
3749 "snapshot {what} missing `{key}`"
3750 ))),
3751 }
3752}
3753
3754fn snapshot_dict_opt_f64(
3755 zone: &Bound<'_, pyo3::types::PyDict>,
3756 key: &str,
3757 what: &str,
3758) -> PyResult<Option<f64>> {
3759 match zone.get_item(key)? {
3760 Some(v) if v.is_none() => Ok(None),
3761 Some(v) => v
3762 .extract::<f64>()
3763 .map(Some)
3764 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be float"))),
3765 None => Ok(None),
3766 }
3767}
3768
3769fn snapshot_zone_from_dict(
3770 zone: &Bound<'_, pyo3::types::PyDict>,
3771) -> PyResult<nucleide_r2s::snapshot::SnapshotZone> {
3772 let id = snapshot_dict_str(zone, "id", "zone")?;
3773 let volume_cm3 = snapshot_dict_f64(zone, "volume_cm3", "zone")?;
3774 let composition: BTreeMap<String, f64> = match zone.get_item("composition")? {
3775 Some(v) => v.extract().map_err(|_| {
3776 PyValueError::new_err("snapshot zone `composition` must be a dict of str to float")
3777 })?,
3778 None => return Err(PyValueError::new_err("snapshot zone missing `composition`")),
3779 };
3780 Ok(nucleide_r2s::snapshot::SnapshotZone {
3781 zone: id,
3782 volume_cm3,
3783 zbottom_cm: snapshot_dict_opt_f64(zone, "zbottom_cm", "zone")?,
3784 ztop_cm: snapshot_dict_opt_f64(zone, "ztop_cm", "zone")?,
3785 material: snapshot_dict_opt_str(zone, "material", "zone")?,
3786 xs_type: snapshot_dict_opt_str(zone, "xs_type", "zone")?,
3787 temperature_c: snapshot_dict_opt_f64(zone, "temperature_C", "zone")?,
3788 composition: composition.into_iter().collect(),
3789 flux_name: snapshot_dict_opt_str(zone, "flux", "zone")?,
3790 })
3791}
3792
3793fn snapshot_input_from_dict(
3794 snapshot: &Bound<'_, pyo3::types::PyDict>,
3795) -> PyResult<nucleide_r2s::snapshot::SnapshotInput> {
3796 let zone_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = match snapshot.get_item("zones")? {
3797 Some(v) => v
3798 .extract()
3799 .map_err(|_| PyValueError::new_err("snapshot `zones` must be a list of dicts"))?,
3800 None => return Err(PyValueError::new_err("snapshot missing `zones`")),
3801 };
3802 let mut zones = Vec::with_capacity(zone_dicts.len());
3803 for z in &zone_dicts {
3804 zones.push(snapshot_zone_from_dict(z)?);
3805 }
3806 let flux_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = match snapshot.get_item("flux_defs")? {
3807 Some(v) => v
3808 .extract()
3809 .map_err(|_| PyValueError::new_err("snapshot `flux_defs` must be a list of dicts"))?,
3810 None => return Err(PyValueError::new_err("snapshot missing `flux_defs`")),
3811 };
3812 let mut flux_defs = Vec::with_capacity(flux_dicts.len());
3813 for f in &flux_dicts {
3814 flux_defs.push(nucleide_r2s::snapshot::SnapshotFluxDef {
3815 name: snapshot_dict_str(f, "name", "flux")?,
3816 file: snapshot_dict_str(f, "file", "flux")?,
3817 scale: snapshot_dict_f64(f, "scale", "flux")?,
3818 });
3819 }
3820 let cooling_s: Vec<f64> = match snapshot.get_item("cooling_s")? {
3821 Some(v) => v
3822 .extract()
3823 .map_err(|_| PyValueError::new_err("snapshot `cooling_s` must be a list of float"))?,
3824 None => return Err(PyValueError::new_err("snapshot missing `cooling_s`")),
3825 };
3826 Ok(nucleide_r2s::snapshot::SnapshotInput {
3827 zones,
3828 flux_defs,
3829 cooling_s,
3830 schedule_text: snapshot_dict_opt_str(snapshot, "schedule_text", "snapshot")?,
3831 output: snapshot_dict_opt_str(snapshot, "output", "snapshot")?,
3832 })
3833}
3834
3835#[pyfunction]
3851fn r2s_from_snapshot(
3852 py: Python<'_>,
3853 snapshot: &Bound<'_, pyo3::types::PyDict>,
3854) -> PyResult<Py<PyAny>> {
3855 let input = snapshot_input_from_dict(snapshot)?;
3856 let (workflow, template, decks) = py
3857 .detach(move || nucleide_r2s::snapshot::snapshot_workflow(&input))
3858 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3859 use pyo3::types::PyDict;
3860 let out = PyDict::new(py);
3861 out.set_item("workflow", r2s_workflow_to_py(py, &workflow))
3862 .ok();
3863 out.set_item("deck", template.to_string()).ok();
3864 let deck_texts: Vec<String> = decks.iter().map(ToString::to_string).collect();
3865 out.set_item("decks", deck_texts).ok();
3866 Ok(out.into_any().unbind())
3867}
3868
3869fn parse_integrator(name: &str) -> PyResult<nucleide_depletion::Integrator> {
3887 use nucleide_depletion::Integrator as I;
3888 if name.eq_ignore_ascii_case("predictor") {
3889 return Ok(I::Predictor);
3890 }
3891 if name.eq_ignore_ascii_case("cecm") {
3892 return Ok(I::Cecm);
3893 }
3894 if name.eq_ignore_ascii_case("cf4") {
3895 return Ok(I::Cf4);
3896 }
3897 Err(PyValueError::new_err(format!(
3898 "unsupported integrator `{name}` (supported: predictor, cecm, cf4)"
3899 )))
3900}
3901
3902#[pyfunction]
3915#[pyo3(signature = (chain, n0, dts, rates=None, rates_list=None, integrator="predictor", order=48, method="cram48"))]
3916#[allow(clippy::too_many_arguments)]
3917fn deplete_series(
3918 chain: &PyChain,
3919 n0: BTreeMap<String, f64>,
3920 dts: Vec<f64>,
3921 rates: Option<RateMap>,
3922 rates_list: Option<Vec<Option<RateMap>>>,
3923 integrator: &str,
3924 order: u8,
3925 method: &str,
3926) -> PyResult<Py<PyAny>> {
3927 use nucleide_depletion::{DepletionSystem, ReactionRates, Step};
3928 let integrator = parse_integrator(integrator)?;
3929 let method = resolve_method(order, method)?;
3930 if let Some(list) = &rates_list {
3931 if list.len() != dts.len() {
3932 return Err(PyValueError::new_err(format!(
3933 "rates_list has {} entries but dts has {}",
3934 list.len(),
3935 dts.len()
3936 )));
3937 }
3938 }
3939 if dts.is_empty() {
3940 return Err(PyValueError::new_err("dts must not be empty"));
3941 }
3942 let mut n0_vec = vec![0.0; chain.inner.len()];
3944 for (name, value) in &n0 {
3945 let idx = chain.inner.index_of(name).ok_or_else(|| {
3946 PyValueError::new_err(format!("unknown nuclide `{name}` for this chain"))
3947 })?;
3948 n0_vec[idx] = *value;
3949 }
3950 let empty = BTreeMap::new();
3951 let mut steps = Vec::with_capacity(dts.len());
3952 for (i, dt) in dts.iter().enumerate() {
3953 let step_rates = rates_list
3954 .as_ref()
3955 .and_then(|list| list[i].as_ref())
3956 .or(rates.as_ref())
3957 .unwrap_or(&empty);
3958 let rs = split_rates(step_rates, &chain.inner)?;
3959 steps.push(Step::new(*dt, rs));
3960 }
3961 let template = DepletionSystem::build((*chain.inner).clone(), &ReactionRates::new())
3964 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3965 let series =
3968 nucleide_depletion::integrate_with_method(&template, &n0_vec, &steps, integrator, method)
3969 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3970 let names: Vec<&str> = template
3971 .chain
3972 .nuclides
3973 .iter()
3974 .map(|nuc| nuc.name.as_str())
3975 .collect();
3976 let keyed = |rows: &[Vec<f64>]| -> Vec<BTreeMap<String, f64>> {
3977 rows.iter()
3978 .map(|row| {
3979 names
3980 .iter()
3981 .zip(row)
3982 .map(|(name, v)| ((*name).to_string(), *v))
3983 .collect()
3984 })
3985 .collect()
3986 };
3987 let atoms = keyed(&series.atoms[1..]);
3989 let activity = keyed(&series.activity[1..]);
3990 let decay_heat = keyed(&series.decay_heat[1..]);
3991 let times = series.times[1..].to_vec();
3992 Ok(Python::attach(|py| {
3993 use pyo3::types::PyDict;
3994 let out = PyDict::new(py);
3995 out.set_item("times", ×).ok();
3996 out.set_item("atoms", &atoms).ok();
3997 out.set_item("activity", &activity).ok();
3998 out.set_item("decay_heat", &decay_heat).ok();
3999 out.into_any().unbind()
4000 }))
4001}
4002
4003#[pyfunction]
4008fn simple_xs(name: &str) -> PyResult<Option<(f64, f64)>> {
4009 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4010 Ok(nucleide_nuclei::data::simple_xs_by_name(name))
4011}
4012
4013#[pyfunction]
4018fn scattering_length(name: &str) -> PyResult<Option<f64>> {
4019 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4020 Ok(nucleide_nuclei::data::scattering_length_by_name(name).map(|(b_coh, _)| b_coh))
4021}
4022
4023#[pyfunction]
4027fn decay_energy(name: &str) -> PyResult<Option<f64>> {
4028 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4029 Ok(nucleide_nuclei::data::decay_energy_mev_by_name(name))
4030}
4031
4032#[pyfunction]
4038fn decay_branches(name: &str) -> PyResult<Vec<(String, f64, String)>> {
4039 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4040 Ok(nucleide_nuclei::data::decay_branches_by_name(name)
4041 .unwrap_or_default()
4042 .into_iter()
4043 .map(|b| {
4044 (
4045 nucleide_nuclei::NuclideId::from_nucid(b.progeny).to_name(),
4046 b.branching_fraction,
4047 b.mode.as_str().to_string(),
4048 )
4049 })
4050 .collect())
4051}
4052
4053#[pyfunction]
4059fn decay_branch_fraction(parent: &str, progeny: &str) -> PyResult<Option<f64>> {
4060 NuclideId::from_name(parent).map_err(wrap_nucid_err)?;
4061 NuclideId::from_name(progeny).map_err(wrap_nucid_err)?;
4062 Ok(nucleide_nuclei::data::branching_fraction_by_name(
4063 parent, progeny,
4064 ))
4065}
4066
4067#[pyfunction]
4073fn normalize_nuclide(name: &str) -> PyResult<String> {
4074 Ok(nucleide_nuclei::dialects::normalize_nuclide_name(name)
4075 .map_err(|e| PyValueError::new_err(e.to_string()))?
4076 .to_name())
4077}
4078
4079#[pyfunction]
4086fn decay_heat(comp: BTreeMap<String, f64>) -> PyResult<f64> {
4087 let mat = comp_to_material(comp)?;
4088 let analytics = nucleide_material::Analytics {
4089 masses: &nucleide_material::Ame2020,
4090 decays: &nucleide_material::ChainDecays,
4091 };
4092 mat.total_decay_heat(&analytics, &nucleide_material::DecayEnergies)
4093 .map_err(|e| PyValueError::new_err(e.to_string()))
4094}
4095
4096fn parse_dose_pathway(s: &str) -> PyResult<nucleide_material::DosePathway> {
4097 nucleide_material::DosePathway::parse(s).ok_or_else(|| {
4098 PyValueError::new_err(format!(
4099 "unknown dose pathway `{s}` (supported: air, soil, ingest, inhale)"
4100 ))
4101 })
4102}
4103
4104fn parse_dose_source(s: &str) -> PyResult<nucleide_material::DoseSource> {
4105 nucleide_material::DoseSource::parse(s).ok_or_else(|| {
4106 PyValueError::new_err(format!(
4107 "unknown dose source `{s}` (supported: EPA, DOE, GENII)"
4108 ))
4109 })
4110}
4111
4112#[pyfunction]
4119#[pyo3(signature = (name, pathway, source="EPA"))]
4120fn dose_factor(name: &str, pathway: &str, source: &str) -> PyResult<Option<f64>> {
4121 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4122 let p = parse_dose_pathway(pathway)?;
4123 let s = parse_dose_source(source)?;
4124 Ok(nucleide_nuclei::data::dose_factor_by_name(name, p, s))
4125}
4126
4127#[pyfunction]
4139#[pyo3(signature = (comp, pathway, source="EPA"))]
4140fn dose_per_g(comp: BTreeMap<String, f64>, pathway: &str, source: &str) -> PyResult<f64> {
4141 let mat = comp_to_material(comp)?;
4142 let analytics = nucleide_material::Analytics {
4143 masses: &nucleide_material::Ame2020,
4144 decays: &nucleide_material::ChainDecays,
4145 };
4146 let p = parse_dose_pathway(pathway)?;
4147 let s = parse_dose_source(source)?;
4148 mat.total_dose_per_g(&analytics, &nucleide_material::DoseFactors, p, s)
4149 .map_err(|e| PyValueError::new_err(e.to_string()))
4150}
4151
4152#[pyfunction]
4160#[allow(clippy::type_complexity)]
4161fn separate_material(
4162 comp: BTreeMap<String, f64>,
4163 effs: BTreeMap<String, f64>,
4164) -> PyResult<(BTreeMap<String, f64>, BTreeMap<String, f64>)> {
4165 let mat = comp_to_material(comp)?;
4166 let mut table = Vec::with_capacity(effs.len());
4167 for (name, eff) in &effs {
4168 let id = NuclideId::from_name(name)
4169 .map_err(|e| PyValueError::new_err(format!("`{name}`: {e}")))?;
4170 table.push((id, *eff));
4171 }
4172 let (product, tails) = mat
4173 .separate(&table)
4174 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4175 let named =
4176 |m: nucleide_material::Material| m.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect();
4177 Ok((named(product), named(tails)))
4178}
4179
4180#[pyfunction]
4187fn blend_material(parts: Vec<(BTreeMap<String, f64>, f64)>) -> PyResult<BTreeMap<String, f64>> {
4188 let mats: Vec<nucleide_material::Material> = parts
4189 .iter()
4190 .map(|(comp, _)| comp_to_material(comp.clone()))
4191 .collect::<PyResult<_>>()?;
4192 let refs: Vec<(&nucleide_material::Material, f64)> =
4193 mats.iter().zip(parts.iter().map(|(_, r)| *r)).collect();
4194 let out = nucleide_material::Material::blend(&refs)
4195 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4196 Ok(out.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect())
4197}
4198
4199#[pyclass(name = "Cusum")]
4207struct PyCusum {
4208 inner: nucleide_material::Cusum,
4209}
4210
4211#[pymethods]
4212impl PyCusum {
4213 #[new]
4216 #[pyo3(signature = (ref_shift_k=0.5, alarm_h=4.0, startup=10))]
4217 fn new(ref_shift_k: f64, alarm_h: f64, startup: usize) -> PyResult<Self> {
4218 nucleide_material::Cusum::new(ref_shift_k, alarm_h, startup)
4219 .map(|inner| Self { inner })
4220 .map_err(|e| PyValueError::new_err(e.to_string()))
4221 }
4222
4223 fn update(&mut self, x: f64) -> bool {
4225 self.inner.update(x)
4226 }
4227
4228 fn status(&self) -> bool {
4230 self.inner.status()
4231 }
4232
4233 fn statistic(&self) -> f64 {
4235 self.inner.statistic()
4236 }
4237
4238 fn count(&self) -> usize {
4240 self.inner.count()
4241 }
4242
4243 fn mean(&self) -> f64 {
4245 self.inner.mean()
4246 }
4247
4248 fn variance(&self) -> f64 {
4250 self.inner.variance()
4251 }
4252
4253 fn std(&self) -> f64 {
4255 self.inner.std()
4256 }
4257
4258 fn reset(&mut self) {
4260 self.inner.reset();
4261 }
4262}
4263
4264#[pyclass(name = "DeckProblem")]
4270struct PyDeckProblem {
4271 inner: std::sync::Mutex<nucleide_mcnp_io::problem::DeckProblem>,
4272}
4273
4274fn deck_cell_dict(cell: &nucleide_mcnp_io::cell::CellCard) -> BTreeMap<String, String> {
4275 let mut d = BTreeMap::new();
4276 d.insert("num".to_string(), cell.num.to_string());
4277 d.insert("mat".to_string(), cell.mat.to_string());
4278 d.insert(
4279 "dens".to_string(),
4280 cell.dens.map(|v| v.to_string()).unwrap_or_default(),
4281 );
4282 d.insert("geom".to_string(), cell.geom.render());
4283 d.insert("params".to_string(), cell.params.join(" "));
4284 d
4285}
4286
4287#[pymethods]
4288impl PyDeckProblem {
4289 #[staticmethod]
4291 fn loads(text: &str) -> PyResult<Self> {
4292 nucleide_mcnp_io::problem::parse_deck(text)
4293 .map(|inner| Self {
4294 inner: std::sync::Mutex::new(inner),
4295 })
4296 .map_err(|e| PyValueError::new_err(e.to_string()))
4297 }
4298
4299 #[getter]
4301 fn message(&self) -> PyResult<String> {
4302 Ok(self
4303 .inner
4304 .lock()
4305 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4306 .message
4307 .clone())
4308 }
4309
4310 #[getter]
4312 fn title(&self) -> PyResult<String> {
4313 Ok(self
4314 .inner
4315 .lock()
4316 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4317 .title
4318 .clone())
4319 }
4320
4321 #[getter]
4324 fn cells(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4325 Ok(self
4326 .inner
4327 .lock()
4328 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4329 .cells
4330 .iter()
4331 .map(deck_cell_dict)
4332 .collect())
4333 }
4334
4335 #[getter]
4338 fn surfs(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4339 Ok(self
4340 .inner
4341 .lock()
4342 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4343 .surfs
4344 .iter()
4345 .map(|s| {
4346 let mut d = BTreeMap::new();
4347 d.insert("num".to_string(), s.num.to_string());
4348 d.insert("reflecting".to_string(), s.reflecting.to_string());
4349 d.insert(
4350 "transform".to_string(),
4351 s.transform.map(|v| v.to_string()).unwrap_or_default(),
4352 );
4353 d.insert(
4354 "periodic".to_string(),
4355 s.periodic.map(|v| v.to_string()).unwrap_or_default(),
4356 );
4357 d.insert("kind".to_string(), s.kind.keyword().to_string());
4358 d.insert(
4359 "coeffs".to_string(),
4360 s.coeffs
4361 .iter()
4362 .map(|v| v.to_string())
4363 .collect::<Vec<_>>()
4364 .join(" "),
4365 );
4366 d
4367 })
4368 .collect())
4369 }
4370
4371 #[getter]
4373 fn material_numbers(&self) -> PyResult<Vec<u32>> {
4374 Ok(self
4375 .inner
4376 .lock()
4377 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4378 .materials
4379 .iter()
4380 .map(|m| m.number)
4381 .collect())
4382 }
4383
4384 #[getter]
4386 fn data_names(&self) -> PyResult<Vec<String>> {
4387 Ok(self
4388 .inner
4389 .lock()
4390 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4391 .data
4392 .iter()
4393 .map(|d| d.name.clone())
4394 .collect())
4395 }
4396
4397 fn dumps(&self) -> PyResult<String> {
4399 let guard = self
4400 .inner
4401 .lock()
4402 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?;
4403 Ok(nucleide_mcnp_io::problem::write_deck(&guard))
4404 }
4405
4406 fn set_cell_density(&self, cell: u32, dens: f64) -> PyResult<()> {
4408 self.inner
4409 .lock()
4410 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4411 .set_cell_density(cell, dens)
4412 .map_err(|e| PyValueError::new_err(e.to_string()))
4413 }
4414
4415 fn set_cell_material(&self, cell: u32, mat: u32) -> PyResult<()> {
4417 self.inner
4418 .lock()
4419 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4420 .set_cell_material(cell, mat)
4421 .map_err(|e| PyValueError::new_err(e.to_string()))
4422 }
4423
4424 #[getter]
4426 fn mode(&self) -> PyResult<BTreeMap<String, String>> {
4427 let mode = self
4428 .inner
4429 .lock()
4430 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4431 .mode()
4432 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4433 let mut d = BTreeMap::new();
4434 d.insert("particles".to_string(), mode.particles.join(" "));
4435 Ok(d)
4436 }
4437
4438 #[getter]
4441 fn transforms(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4442 let transforms = self
4443 .inner
4444 .lock()
4445 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4446 .transforms()
4447 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4448 Ok(transforms
4449 .iter()
4450 .map(|t| {
4451 let mut d = BTreeMap::new();
4452 d.insert("number".to_string(), t.number.to_string());
4453 d.insert(
4454 "displacement".to_string(),
4455 t.displacement
4456 .iter()
4457 .map(|v| v.to_string())
4458 .collect::<Vec<_>>()
4459 .join(" "),
4460 );
4461 d.insert(
4462 "rotation".to_string(),
4463 t.rotation
4464 .iter()
4465 .map(|v| v.to_string())
4466 .collect::<Vec<_>>()
4467 .join(" "),
4468 );
4469 d.insert("in_degrees".to_string(), t.is_in_degrees.to_string());
4470 d.insert("main_to_aux".to_string(), t.is_main_to_aux.to_string());
4471 d.insert("hidden".to_string(), t.hidden.to_string());
4472 d
4473 })
4474 .collect())
4475 }
4476
4477 #[getter]
4480 fn universes(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4481 let universes = self
4482 .inner
4483 .lock()
4484 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4485 .universes()
4486 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4487 Ok(universes
4488 .iter()
4489 .map(|u| {
4490 let mut d = BTreeMap::new();
4491 d.insert("number".to_string(), u.number.to_string());
4492 d.insert(
4493 "cells".to_string(),
4494 u.cells
4495 .iter()
4496 .map(|v| v.to_string())
4497 .collect::<Vec<_>>()
4498 .join(" "),
4499 );
4500 d.insert(
4501 "not_truncated".to_string(),
4502 u.not_truncated
4503 .iter()
4504 .map(|v| v.to_string())
4505 .collect::<Vec<_>>()
4506 .join(" "),
4507 );
4508 d
4509 })
4510 .collect())
4511 }
4512
4513 #[getter]
4515 fn lattices(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4516 let lattices = self
4517 .inner
4518 .lock()
4519 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4520 .lattices()
4521 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4522 Ok(lattices
4523 .iter()
4524 .map(|l| {
4525 let mut d = BTreeMap::new();
4526 d.insert("cell".to_string(), l.cell.to_string());
4527 d.insert("lattice".to_string(), l.lattice.to_string());
4528 d
4529 })
4530 .collect())
4531 }
4532
4533 #[getter]
4537 fn fills(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4538 use nucleide_mcnp_io::semantic::{FillTarget, FillTransform};
4539 let fills = self
4540 .inner
4541 .lock()
4542 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4543 .fills()
4544 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4545 Ok(fills
4546 .iter()
4547 .map(|f| {
4548 let mut d = BTreeMap::new();
4549 d.insert("cell".to_string(), f.cell.to_string());
4550 match &f.target {
4551 FillTarget::Single(u) => {
4552 d.insert("kind".to_string(), "single".to_string());
4553 d.insert("universe".to_string(), u.to_string());
4554 d.insert("min_index".to_string(), String::new());
4555 d.insert("max_index".to_string(), String::new());
4556 d.insert("universes".to_string(), String::new());
4557 }
4558 FillTarget::Matrix {
4559 min_index,
4560 max_index,
4561 universes,
4562 } => {
4563 d.insert("kind".to_string(), "matrix".to_string());
4564 d.insert("universe".to_string(), String::new());
4565 d.insert(
4566 "min_index".to_string(),
4567 min_index
4568 .iter()
4569 .map(|v| v.to_string())
4570 .collect::<Vec<_>>()
4571 .join(" "),
4572 );
4573 d.insert(
4574 "max_index".to_string(),
4575 max_index
4576 .iter()
4577 .map(|v| v.to_string())
4578 .collect::<Vec<_>>()
4579 .join(" "),
4580 );
4581 d.insert(
4582 "universes".to_string(),
4583 universes
4584 .iter()
4585 .map(|u| {
4586 u.map(|v| v.to_string()).unwrap_or_else(|| "-".to_string())
4587 })
4588 .collect::<Vec<_>>()
4589 .join(" "),
4590 );
4591 }
4592 }
4593 match &f.transform {
4594 None => {
4595 d.insert("transform".to_string(), String::new());
4596 d.insert("hidden_transform".to_string(), String::new());
4597 }
4598 Some(FillTransform::Reference(n)) => {
4599 d.insert("transform".to_string(), n.to_string());
4600 d.insert("hidden_transform".to_string(), String::new());
4601 }
4602 Some(FillTransform::Hidden(t)) => {
4603 d.insert("transform".to_string(), String::new());
4604 let mut coords: Vec<String> =
4605 t.displacement.iter().map(|v| v.to_string()).collect();
4606 coords.extend(t.rotation.iter().map(|v| v.to_string()));
4607 d.insert("hidden_transform".to_string(), coords.join(" "));
4608 }
4609 }
4610 d.insert("in_degrees".to_string(), f.in_degrees.to_string());
4611 d
4612 })
4613 .collect())
4614 }
4615
4616 #[getter]
4618 fn importances(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4619 let importances = self
4620 .inner
4621 .lock()
4622 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4623 .importances()
4624 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4625 Ok(importances
4626 .iter()
4627 .map(|v| {
4628 let mut d = BTreeMap::new();
4629 d.insert("cell".to_string(), v.cell.to_string());
4630 d.insert("particle".to_string(), v.particle.clone());
4631 d.insert("value".to_string(), v.value.to_string());
4632 d
4633 })
4634 .collect())
4635 }
4636
4637 #[getter]
4639 fn volumes(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4640 let volumes = self
4641 .inner
4642 .lock()
4643 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4644 .volumes()
4645 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4646 Ok(volumes
4647 .iter()
4648 .map(|v| {
4649 let mut d = BTreeMap::new();
4650 d.insert("cell".to_string(), v.cell.to_string());
4651 d.insert("volume".to_string(), v.volume.to_string());
4652 d
4653 })
4654 .collect())
4655 }
4656
4657 #[getter]
4660 fn tallies(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
4661 let tallies = self
4662 .inner
4663 .lock()
4664 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4665 .tallies()
4666 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4667 Ok(tallies
4668 .iter()
4669 .map(|t| {
4670 let mut d = BTreeMap::new();
4671 d.insert("number".to_string(), t.number.to_string());
4672 d.insert("type".to_string(), t.tally_type.to_string());
4673 d.insert("particles".to_string(), t.particles.join(","));
4674 d.insert("entries".to_string(), t.entries.join(" "));
4675 d.insert("fm".to_string(), t.fm.clone().unwrap_or_default().join(" "));
4676 d.insert(
4677 "e_bins".to_string(),
4678 t.e_bins.clone().unwrap_or_default().join(" "),
4679 );
4680 d
4681 })
4682 .collect())
4683 }
4684
4685 fn validate(&self) -> PyResult<()> {
4688 self.inner
4689 .lock()
4690 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4691 .validate()
4692 .map_err(|e| PyValueError::new_err(e.to_string()))
4693 }
4694
4695 fn validation_notes(&self) -> PyResult<Vec<String>> {
4697 Ok(self
4698 .inner
4699 .lock()
4700 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4701 .validation_notes())
4702 }
4703
4704 fn set_mode(&self, particles: Vec<String>) -> PyResult<()> {
4706 self.inner
4707 .lock()
4708 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4709 .set_mode(particles)
4710 .map_err(|e| PyValueError::new_err(e.to_string()))
4711 }
4712
4713 fn set_cell_universe(&self, cell: u32, universe: u32, not_truncated: bool) -> PyResult<()> {
4715 self.inner
4716 .lock()
4717 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4718 .set_cell_universe(cell, universe, not_truncated)
4719 .map_err(|e| PyValueError::new_err(e.to_string()))
4720 }
4721
4722 fn set_cell_lattice(&self, cell: u32, lattice: Option<u8>) -> PyResult<()> {
4724 self.inner
4725 .lock()
4726 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4727 .set_cell_lattice(cell, lattice)
4728 .map_err(|e| PyValueError::new_err(e.to_string()))
4729 }
4730
4731 fn set_cell_fill(&self, cell: u32, universe: u32) -> PyResult<()> {
4733 self.inner
4734 .lock()
4735 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4736 .set_cell_fill(cell, universe)
4737 .map_err(|e| PyValueError::new_err(e.to_string()))
4738 }
4739}
4740
4741#[pyfunction]
4743fn read_deck(path: &str) -> PyResult<PyDeckProblem> {
4744 nucleide_mcnp_io::problem::parse_deck_file(path)
4745 .map(|inner| PyDeckProblem {
4746 inner: std::sync::Mutex::new(inner),
4747 })
4748 .map_err(|e| PyValueError::new_err(e.to_string()))
4749}
4750
4751#[pyfunction]
4753fn parse_deck(text: &str) -> PyResult<PyDeckProblem> {
4754 PyDeckProblem::loads(text)
4755}
4756
4757#[pyclass(name = "Inventory")]
4759struct PyInventory {
4760 chain: std::sync::Arc<nucleide_depletion::Chain>,
4761 atoms: BTreeMap<String, f64>,
4762}
4763
4764fn inventory_sys(
4765 chain: &nucleide_depletion::Chain,
4766 rates: &RateMap,
4767) -> PyResult<nucleide_depletion::DepletionSystem> {
4768 let rs = split_rates(rates, chain)?;
4769 nucleide_depletion::DepletionSystem::build(chain.clone(), &rs)
4770 .map_err(|e| PyValueError::new_err(e.to_string()))
4771}
4772
4773fn parse_quantity_unit(unit: &str) -> PyResult<nucleide_depletion::QuantityUnit> {
4774 nucleide_depletion::QuantityUnit::from_str(unit)
4775 .map_err(|e| PyValueError::new_err(format!("{e:?}")))
4776}
4777
4778#[pymethods]
4779impl PyInventory {
4780 #[new]
4783 #[pyo3(signature = (chain, comp, units="atoms"))]
4784 fn new(chain: &PyChain, comp: BTreeMap<String, f64>, units: &str) -> PyResult<Self> {
4785 let unit = parse_quantity_unit(units)?;
4786 let sys = inventory_sys(&chain.inner, &BTreeMap::new())?;
4787 let inv = nucleide_depletion::DecayInventory::from_units(&comp, unit, &sys)
4788 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4789 Ok(Self {
4790 chain: chain.inner.clone(),
4791 atoms: inv.atoms,
4792 })
4793 }
4794
4795 fn numbers(&self) -> BTreeMap<String, f64> {
4797 self.atoms.clone()
4798 }
4799
4800 #[pyo3(signature = (dt, time_unit="s", rates=None, order=48, method="cram48"))]
4807 fn decay(
4808 &self,
4809 dt: f64,
4810 time_unit: &str,
4811 rates: Option<RateMap>,
4812 order: u8,
4813 method: &str,
4814 ) -> PyResult<Self> {
4815 let method = resolve_method(order, method)?;
4816 let unit = nucleide_depletion::inventory::time_unit_from_str(time_unit)
4817 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4818 let seconds = dt * unit.as_seconds();
4819 let empty = BTreeMap::new();
4820 let step_rates = rates.as_ref().unwrap_or(&empty);
4821 let template = inventory_sys(&self.chain, step_rates)?;
4822 let steps = vec![nucleide_depletion::Step::new(
4825 seconds,
4826 split_rates(step_rates, &self.chain)?,
4827 )];
4828 let series = nucleide_depletion::integrate_with_method(
4829 &template,
4830 &chain_vec(&self.chain, &self.atoms)?,
4831 &steps,
4832 nucleide_depletion::Integrator::Predictor,
4833 method,
4834 )
4835 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4836 let names: Vec<String> = self.chain.nuclides.iter().map(|n| n.name.clone()).collect();
4837 let atoms = names
4838 .iter()
4839 .zip(series.atoms.last().cloned().unwrap_or_default())
4840 .map(|(n, v)| (n.clone(), v))
4841 .collect();
4842 Ok(Self {
4843 chain: self.chain.clone(),
4844 atoms,
4845 })
4846 }
4847
4848 fn activities(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
4850 let unit = parse_quantity_unit(units)?;
4851 let sys = inventory_sys(&self.chain, &BTreeMap::new())?;
4852 let inv = nucleide_depletion::DecayInventory {
4853 atoms: self.atoms.clone(),
4854 };
4855 inv.activities(&sys, unit)
4856 .map_err(|e| PyValueError::new_err(e.to_string()))
4857 }
4858
4859 fn masses(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
4861 let unit = parse_quantity_unit(units)?;
4862 let inv = nucleide_depletion::DecayInventory {
4863 atoms: self.atoms.clone(),
4864 };
4865 inv.masses(unit)
4866 .map_err(|e| PyValueError::new_err(e.to_string()))
4867 }
4868
4869 fn moles(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
4871 let unit = parse_quantity_unit(units)?;
4872 let inv = nucleide_depletion::DecayInventory {
4873 atoms: self.atoms.clone(),
4874 };
4875 inv.moles(unit)
4876 .map_err(|e| PyValueError::new_err(e.to_string()))
4877 }
4878
4879 fn activity_fractions(&self) -> PyResult<BTreeMap<String, f64>> {
4881 let sys = inventory_sys(&self.chain, &BTreeMap::new())?;
4882 let inv = nucleide_depletion::DecayInventory {
4883 atoms: self.atoms.clone(),
4884 };
4885 inv.activity_fractions(&sys)
4886 .map_err(|e| PyValueError::new_err(e.to_string()))
4887 }
4888
4889 fn mass_fractions(&self) -> PyResult<BTreeMap<String, f64>> {
4891 let inv = nucleide_depletion::DecayInventory {
4892 atoms: self.atoms.clone(),
4893 };
4894 inv.mass_fractions()
4895 .map_err(|e| PyValueError::new_err(e.to_string()))
4896 }
4897
4898 fn mole_fractions(&self) -> BTreeMap<String, f64> {
4900 nucleide_depletion::DecayInventory {
4901 atoms: self.atoms.clone(),
4902 }
4903 .mole_fractions()
4904 }
4905
4906 fn half_lives_readable(&self) -> BTreeMap<String, String> {
4908 nucleide_depletion::DecayInventory {
4909 atoms: self.atoms.clone(),
4910 }
4911 .half_lives_readable()
4912 }
4913
4914 fn add(&self, other: &Self) -> Self {
4916 let a = nucleide_depletion::DecayInventory {
4917 atoms: self.atoms.clone(),
4918 };
4919 let b = nucleide_depletion::DecayInventory {
4920 atoms: other.atoms.clone(),
4921 };
4922 Self {
4923 chain: self.chain.clone(),
4924 atoms: a.add(&b).atoms,
4925 }
4926 }
4927
4928 fn sub(&self, other: &Self) -> Self {
4930 let a = nucleide_depletion::DecayInventory {
4931 atoms: self.atoms.clone(),
4932 };
4933 let b = nucleide_depletion::DecayInventory {
4934 atoms: other.atoms.clone(),
4935 };
4936 Self {
4937 chain: self.chain.clone(),
4938 atoms: a.sub(&b).atoms,
4939 }
4940 }
4941
4942 fn mul(&self, scalar: f64) -> Self {
4944 let a = nucleide_depletion::DecayInventory {
4945 atoms: self.atoms.clone(),
4946 };
4947 Self {
4948 chain: self.chain.clone(),
4949 atoms: a.mul(scalar).atoms,
4950 }
4951 }
4952
4953 fn div(&self, scalar: f64) -> Self {
4955 let a = nucleide_depletion::DecayInventory {
4956 atoms: self.atoms.clone(),
4957 };
4958 Self {
4959 chain: self.chain.clone(),
4960 atoms: a.div(scalar).atoms,
4961 }
4962 }
4963
4964 fn to_csv(&self) -> String {
4966 nucleide_depletion::DecayInventory {
4967 atoms: self.atoms.clone(),
4968 }
4969 .to_csv()
4970 }
4971
4972 #[staticmethod]
4974 fn from_csv(chain: &PyChain, text: &str) -> PyResult<Self> {
4975 let inv = nucleide_depletion::DecayInventory::from_csv(text)
4977 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4978 for name in inv.atoms.keys() {
4979 if chain.inner.index_of(name).is_none() {
4980 return Err(PyValueError::new_err(format!(
4981 "unknown nuclide `{name}` for this chain"
4982 )));
4983 }
4984 }
4985 Ok(Self {
4986 chain: chain.inner.clone(),
4987 atoms: inv.atoms,
4988 })
4989 }
4990}
4991
4992fn chain_vec(
4994 chain: &nucleide_depletion::Chain,
4995 atoms: &BTreeMap<String, f64>,
4996) -> PyResult<Vec<f64>> {
4997 let mut vec = vec![0.0; chain.len()];
4998 for (name, value) in atoms {
4999 let idx = chain.index_of(name).ok_or_else(|| {
5000 PyValueError::new_err(format!("unknown nuclide `{name}` for this chain"))
5001 })?;
5002 vec[idx] = *value;
5003 }
5004 Ok(vec)
5005}
5006
5007#[pyfunction]
5009#[pyo3(signature = (chain, n0, dt, rates=None))]
5010fn cumulative_decays(
5011 chain: &PyChain,
5012 n0: BTreeMap<String, f64>,
5013 dt: f64,
5014 rates: Option<RateMap>,
5015) -> PyResult<BTreeMap<String, f64>> {
5016 let empty = BTreeMap::new();
5017 let sys = inventory_sys(&chain.inner, rates.as_ref().unwrap_or(&empty))?;
5018 let vec = chain_vec(&chain.inner, &n0)?;
5019 let out = nucleide_depletion::cumulative_decays(&sys, &vec, dt)
5020 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5021 Ok(chain
5022 .inner
5023 .nuclides
5024 .iter()
5025 .zip(out)
5026 .map(|(nuc, v)| (nuc.name.clone(), v))
5027 .collect())
5028}
5029
5030#[pyfunction]
5032fn progeny(chain: &PyChain, name: &str) -> Vec<(String, f64, String)> {
5033 nucleide_depletion::progeny(&chain.inner, name)
5034}
5035
5036#[pyfunction]
5038fn branching_fraction(chain: &PyChain, parent: &str, child: &str) -> Option<f64> {
5039 nucleide_depletion::branching_fraction(&chain.inner, parent, child)
5040}
5041
5042#[pyfunction]
5044fn decay_mode(chain: &PyChain, parent: &str, child: &str) -> Option<String> {
5045 nucleide_depletion::decay_mode(&chain.inner, parent, child)
5046}
5047
5048#[pyfunction]
5050fn chain_edges(chain: &PyChain) -> Vec<(String, String, f64, String)> {
5051 nucleide_depletion::chain_edges(&chain.inner)
5052}
5053
5054#[pyfunction]
5056fn armi_to_nucid(name: &str) -> PyResult<PyNuclide> {
5057 nucleide_nuclei::armi::armi_name_to_nucid(name)
5058 .map(|inner| PyNuclide { inner })
5059 .map_err(|e| PyValueError::new_err(e.to_string()))
5060}
5061
5062#[pyfunction]
5064fn nucid_to_armi(nuclide: &PyNuclide) -> String {
5065 nucleide_nuclei::armi::nucid_to_armi_label(nuclide.inner)
5066}
5067
5068#[pyfunction]
5070fn mcc3_to_nucid(name: &str) -> PyResult<PyNuclide> {
5071 nucleide_nuclei::armi::mcc3_to_nucid(name)
5072 .map(|inner| PyNuclide { inner })
5073 .map_err(|e| PyValueError::new_err(e.to_string()))
5074}
5075
5076#[pyfunction]
5081#[pyo3(signature = (comp, widths=None))]
5082fn check_labels(
5083 comp: BTreeMap<String, f64>,
5084 widths: Option<Vec<usize>>,
5085) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
5086 let mat = comp_to_material(comp)?;
5087 let widths = widths.unwrap_or_else(|| nucleide_material::DEFAULT_WIDTHS.to_vec());
5088 let collisions = nucleide_material::check_labels(&mat, &widths);
5089 Python::attach(|py| {
5090 Ok(collisions
5091 .into_iter()
5092 .map(|c| {
5093 let mut d = BTreeMap::new();
5094 d.insert(
5095 "truncated".to_string(),
5096 c.truncated.into_pyobject(py).unwrap().unbind().into_any(),
5097 );
5098 d.insert(
5099 "width".to_string(),
5100 c.width.into_pyobject(py).unwrap().unbind().into_any(),
5101 );
5102 let members: Vec<String> = c.members.iter().map(|id| id.to_name()).collect();
5103 d.insert(
5104 "members".to_string(),
5105 members.into_pyobject(py).unwrap().unbind().into_any(),
5106 );
5107 d
5108 })
5109 .collect())
5110 })
5111}
5112
5113#[pyfunction]
5115fn audit_material(comp: BTreeMap<String, f64>) -> PyResult<Vec<BTreeMap<String, String>>> {
5116 let mat = comp_to_material(comp)?;
5117 Ok(nucleide_material::audit(&mat, &nucleide_material::Ame2020)
5118 .into_iter()
5119 .map(|issue| {
5120 let mut d = BTreeMap::new();
5121 d.insert("kind".to_string(), format!("{:?}", issue.kind));
5122 d.insert("detail".to_string(), issue.detail);
5123 d
5124 })
5125 .collect())
5126}
5127
5128#[pyfunction]
5135#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
5136#[allow(clippy::too_many_arguments)]
5137fn emit_cards(
5138 comp: BTreeMap<String, f64>,
5139 name: &str,
5140 density: Option<f64>,
5141 mcnp_number: u32,
5142 xs_suffix: &str,
5143 serpent_lib: &str,
5144 fluka_fid: u32,
5145 partisn_zone: u32,
5146) -> PyResult<BTreeMap<String, String>> {
5147 let (emitted, _) = emit_drift_inner(
5148 comp,
5149 name,
5150 density,
5151 mcnp_number,
5152 xs_suffix,
5153 serpent_lib,
5154 fluka_fid,
5155 partisn_zone,
5156 )?;
5157 Ok(emitted
5158 .into_iter()
5159 .map(|e| (e.code.to_string(), e.text))
5160 .collect())
5161}
5162
5163#[pyfunction]
5167#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
5168#[allow(clippy::too_many_arguments)]
5169fn emit_drift_table(
5170 comp: BTreeMap<String, f64>,
5171 name: &str,
5172 density: Option<f64>,
5173 mcnp_number: u32,
5174 xs_suffix: &str,
5175 serpent_lib: &str,
5176 fluka_fid: u32,
5177 partisn_zone: u32,
5178) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
5179 let (_, table) = emit_drift_inner(
5180 comp,
5181 name,
5182 density,
5183 mcnp_number,
5184 xs_suffix,
5185 serpent_lib,
5186 fluka_fid,
5187 partisn_zone,
5188 )?;
5189 drift_table_to_py(table)
5190}
5191
5192fn drift_table_to_py(
5193 table: nucleide_emit::DriftTable,
5194) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
5195 Python::attach(|py| {
5196 Ok(table
5197 .rows
5198 .into_iter()
5199 .map(|r| {
5200 let mut d = BTreeMap::new();
5201 d.insert(
5202 "code".to_string(),
5203 r.code
5204 .to_string()
5205 .into_pyobject(py)
5206 .unwrap()
5207 .unbind()
5208 .into_any(),
5209 );
5210 d.insert(
5211 "mass_in".to_string(),
5212 r.mass_in.into_pyobject(py).unwrap().unbind().into_any(),
5213 );
5214 d.insert(
5215 "mass_out".to_string(),
5216 r.mass_out.into_pyobject(py).unwrap().unbind().into_any(),
5217 );
5218 d.insert(
5219 "rel_drift".to_string(),
5220 r.rel_drift.into_pyobject(py).unwrap().unbind().into_any(),
5221 );
5222 let dropped: Vec<BTreeMap<String, Py<PyAny>>> = r
5223 .dropped
5224 .into_iter()
5225 .map(|x| {
5226 let mut dd = BTreeMap::new();
5227 dd.insert(
5228 "nuclide".to_string(),
5229 x.id.to_name()
5230 .into_pyobject(py)
5231 .unwrap()
5232 .unbind()
5233 .into_any(),
5234 );
5235 dd.insert(
5236 "mass".to_string(),
5237 x.mass.into_pyobject(py).unwrap().unbind().into_any(),
5238 );
5239 dd.insert(
5240 "reason".to_string(),
5241 x.reason.into_pyobject(py).unwrap().unbind().into_any(),
5242 );
5243 dd
5244 })
5245 .collect();
5246 d.insert(
5247 "dropped".to_string(),
5248 dropped.into_pyobject(py).unwrap().unbind().into_any(),
5249 );
5250 d.insert(
5251 "reparsed".to_string(),
5252 pyo3::types::PyBool::new(py, r.reparsed)
5253 .to_owned()
5254 .into_any()
5255 .unbind(),
5256 );
5257 d
5258 })
5259 .collect())
5260 })
5261}
5262
5263#[allow(clippy::too_many_arguments)]
5264fn emit_drift_inner(
5265 comp: BTreeMap<String, f64>,
5266 name: &str,
5267 density: Option<f64>,
5268 mcnp_number: u32,
5269 xs_suffix: &str,
5270 serpent_lib: &str,
5271 fluka_fid: u32,
5272 partisn_zone: u32,
5273) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
5274 let mut mat = comp_to_material(comp)?;
5275 mat.set_density(density);
5276 emit_drift_with_mat(
5277 mat,
5278 name,
5279 mcnp_number,
5280 xs_suffix,
5281 serpent_lib,
5282 fluka_fid,
5283 partisn_zone,
5284 )
5285}
5286
5287#[allow(clippy::too_many_arguments)]
5288fn emit_drift_with_mat(
5289 mat: nucleide_material::Material,
5290 name: &str,
5291 mcnp_number: u32,
5292 xs_suffix: &str,
5293 serpent_lib: &str,
5294 fluka_fid: u32,
5295 partisn_zone: u32,
5296) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
5297 let mut opts = nucleide_emit::EmitOptions::new(name);
5298 opts.mcnp_number = mcnp_number;
5299 opts.xs_suffix = xs_suffix.to_string();
5300 opts.serpent_lib = serpent_lib.to_string();
5301 opts.fluka_fid = fluka_fid;
5302 opts.partisn_zone = partisn_zone;
5303 nucleide_emit::emit_drift(&mat, &opts).map_err(|e| PyValueError::new_err(e.to_string()))
5304}
5305
5306#[allow(clippy::too_many_arguments)]
5307fn emit_armi_drift_inner(
5308 comp: BTreeMap<String, f64>,
5309 name: &str,
5310 density: Option<f64>,
5311 mcnp_number: u32,
5312 xs_suffix: &str,
5313 serpent_lib: &str,
5314 fluka_fid: u32,
5315 partisn_zone: u32,
5316) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
5317 let mat = nucleide_emit::armi::from_armi_mass_fracs(comp, density)
5320 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5321 emit_drift_with_mat(
5322 mat,
5323 name,
5324 mcnp_number,
5325 xs_suffix,
5326 serpent_lib,
5327 fluka_fid,
5328 partisn_zone,
5329 )
5330}
5331
5332#[pyfunction]
5340#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
5341#[allow(clippy::too_many_arguments)]
5342fn emit_armi_cards(
5343 comp: BTreeMap<String, f64>,
5344 name: &str,
5345 density: Option<f64>,
5346 mcnp_number: u32,
5347 xs_suffix: &str,
5348 serpent_lib: &str,
5349 fluka_fid: u32,
5350 partisn_zone: u32,
5351) -> PyResult<BTreeMap<String, String>> {
5352 let (emitted, _) = emit_armi_drift_inner(
5353 comp,
5354 name,
5355 density,
5356 mcnp_number,
5357 xs_suffix,
5358 serpent_lib,
5359 fluka_fid,
5360 partisn_zone,
5361 )?;
5362 Ok(emitted
5363 .into_iter()
5364 .map(|e| (e.code.to_string(), e.text))
5365 .collect())
5366}
5367
5368#[pyfunction]
5372#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
5373#[allow(clippy::too_many_arguments)]
5374fn emit_armi_drift_table(
5375 comp: BTreeMap<String, f64>,
5376 name: &str,
5377 density: Option<f64>,
5378 mcnp_number: u32,
5379 xs_suffix: &str,
5380 serpent_lib: &str,
5381 fluka_fid: u32,
5382 partisn_zone: u32,
5383) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
5384 let (_, table) = emit_armi_drift_inner(
5385 comp,
5386 name,
5387 density,
5388 mcnp_number,
5389 xs_suffix,
5390 serpent_lib,
5391 fluka_fid,
5392 partisn_zone,
5393 )?;
5394 drift_table_to_py(table)
5395}
5396
5397fn parse_reactivity(
5410 spec: &BTreeMap<String, Py<PyAny>>,
5411 py: Python<'_>,
5412) -> PyResult<nucleide_kinetics::Reactivity> {
5413 use nucleide_kinetics::Reactivity as R;
5414 let kind: String = get_str(spec, py, "kind", "reactivity spec needs a `kind`")?;
5415 let num = |key: &str| -> PyResult<f64> { get_num(spec, py, key) };
5416 let vec = |key: &str| -> PyResult<Vec<f64>> { get_vec(spec, py, key) };
5417 let r = match kind.as_str() {
5418 "constant" => R::Constant { rho: num("rho")? },
5419 "step" => R::Step {
5420 t_step: num("t_step")?,
5421 rho_init: num("rho_init")?,
5422 rho_final: num("rho_final")?,
5423 },
5424 "impulse" => R::Impulse {
5425 t_start: num("t_start")?,
5426 t_end: num("t_end")?,
5427 rho_init: num("rho_init")?,
5428 rho_max: num("rho_max")?,
5429 },
5430 "ramp" => R::Ramp {
5431 t_start: num("t_start")?,
5432 t_end: num("t_end")?,
5433 rho_init: num("rho_init")?,
5434 rho_rise: num("rho_rise")?,
5435 rho_final: num("rho_final")?,
5436 },
5437 "polyline" => R::Polyline {
5438 times: vec("times")?,
5439 values: vec("values")?,
5440 },
5441 other => {
5442 return Err(PyValueError::new_err(format!(
5443 "unknown reactivity kind `{other}` (supported: constant, step, impulse, ramp, polyline)"
5444 )))
5445 }
5446 };
5447 r.validate()
5448 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5449 Ok(r)
5450}
5451
5452fn get_str(
5453 spec: &BTreeMap<String, Py<PyAny>>,
5454 py: Python<'_>,
5455 key: &str,
5456 missing: &str,
5457) -> PyResult<String> {
5458 spec.get(key)
5459 .ok_or_else(|| PyValueError::new_err(missing.to_string()))?
5460 .extract::<String>(py)
5461 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a string")))
5462}
5463
5464fn get_num(spec: &BTreeMap<String, Py<PyAny>>, py: Python<'_>, key: &str) -> PyResult<f64> {
5465 spec.get(key)
5466 .ok_or_else(|| PyValueError::new_err(format!("reactivity spec missing `{key}`")))?
5467 .extract::<f64>(py)
5468 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
5469}
5470
5471fn get_vec(spec: &BTreeMap<String, Py<PyAny>>, py: Python<'_>, key: &str) -> PyResult<Vec<f64>> {
5472 spec.get(key)
5473 .ok_or_else(|| PyValueError::new_err(format!("reactivity spec missing `{key}`")))?
5474 .extract::<Vec<f64>>(py)
5475 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a list of numbers")))
5476}
5477
5478fn kinetics_params(
5479 betas: Vec<f64>,
5480 lambdas: Vec<f64>,
5481 lambda_gen: f64,
5482) -> PyResult<nucleide_kinetics::KineticParams> {
5483 nucleide_kinetics::KineticParams::new(betas, lambdas, lambda_gen)
5484 .map_err(|e| PyValueError::new_err(e.to_string()))
5485}
5486
5487#[pyfunction]
5498#[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))]
5499#[allow(clippy::too_many_arguments)]
5500fn kinetics_solve(
5501 py: Python<'_>,
5502 betas: Vec<f64>,
5503 lambdas: Vec<f64>,
5504 lambda_gen: f64,
5505 rho: BTreeMap<String, Py<PyAny>>,
5506 t: Vec<f64>,
5507 n0: f64,
5508 c0: Option<Vec<f64>>,
5509 method: &str,
5510 rtol: f64,
5511 atol: f64,
5512 dt_min: f64,
5513 dt_max: Option<f64>,
5514 max_steps: usize,
5515) -> PyResult<Py<PyAny>> {
5516 use nucleide_kinetics::{Method as M, SolverOptions};
5517 let params = kinetics_params(betas, lambdas, lambda_gen)?;
5518 let rho = parse_reactivity(&rho, py)?;
5519 let grid =
5520 nucleide_kinetics::TimeGrid::new(t).map_err(|e| PyValueError::new_err(e.to_string()))?;
5521 let state = nucleide_kinetics::State::new(¶ms, n0, c0)
5522 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5523 let method = if method.eq_ignore_ascii_case("trapezoidal") {
5524 M::Trapezoidal
5525 } else if method.eq_ignore_ascii_case("backward_euler") {
5526 M::BackwardEuler
5527 } else {
5528 return Err(PyValueError::new_err(format!(
5529 "unknown kinetics method `{method}` (supported: trapezoidal, backward_euler)"
5530 )));
5531 };
5532 let opts = SolverOptions {
5533 method,
5534 rtol,
5535 atol,
5536 dt_min,
5537 dt_max: dt_max.unwrap_or(f64::INFINITY),
5538 max_steps,
5539 };
5540 let sol = nucleide_kinetics::solve(¶ms, &rho, &grid, &state, &opts)
5541 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5542 use pyo3::types::PyDict;
5543 let out = PyDict::new(py);
5544 out.set_item("times", &sol.times).ok();
5545 out.set_item("n", &sol.n).ok();
5546 out.set_item("C", &sol.c).ok();
5547 out.set_item("n0", sol.initial.n0).ok();
5548 out.set_item("C0", &sol.initial.c0).ok();
5549 Ok(out.into_any().unbind())
5550}
5551
5552#[pyfunction]
5554fn kinetics_equilibrium(
5555 betas: Vec<f64>,
5556 lambdas: Vec<f64>,
5557 lambda_gen: f64,
5558 n0: f64,
5559) -> PyResult<Vec<f64>> {
5560 kinetics_params(betas, lambdas, lambda_gen)?
5561 .equilibrium_precursors(n0)
5562 .map_err(|e| PyValueError::new_err(e.to_string()))
5563}
5564
5565#[pyfunction]
5567#[pyo3(signature = (betas, lambdas, lambda_gen, rho, n0, c0=None))]
5568fn kinetics_initial_rate(
5569 py: Python<'_>,
5570 betas: Vec<f64>,
5571 lambdas: Vec<f64>,
5572 lambda_gen: f64,
5573 rho: BTreeMap<String, Py<PyAny>>,
5574 n0: f64,
5575 c0: Option<Vec<f64>>,
5576) -> PyResult<f64> {
5577 let params = kinetics_params(betas, lambdas, lambda_gen)?;
5578 let rho = parse_reactivity(&rho, py)?;
5579 let state = nucleide_kinetics::State::new(¶ms, n0, c0)
5580 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5581 Ok(nucleide_kinetics::solve::initial_rate(
5582 ¶ms, &rho, &state,
5583 ))
5584}
5585
5586#[pyfunction]
5588fn kinetics_inhour_rho(
5589 betas: Vec<f64>,
5590 lambdas: Vec<f64>,
5591 lambda_gen: f64,
5592 omega: f64,
5593) -> PyResult<f64> {
5594 let params = kinetics_params(betas, lambdas, lambda_gen)?;
5595 nucleide_kinetics::rho_of_omega(¶ms, omega)
5596 .map_err(|e| PyValueError::new_err(e.to_string()))
5597}
5598
5599#[pyfunction]
5601fn kinetics_stable_period(
5602 betas: Vec<f64>,
5603 lambdas: Vec<f64>,
5604 lambda_gen: f64,
5605 rho: f64,
5606) -> PyResult<f64> {
5607 let params = kinetics_params(betas, lambdas, lambda_gen)?;
5608 nucleide_kinetics::stable_period(¶ms, rho).map_err(|e| PyValueError::new_err(e.to_string()))
5609}
5610
5611#[pyfunction]
5616fn kinetics_prompt_jump(
5617 n_before: f64,
5618 rho_before: f64,
5619 rho_after: f64,
5620 beta_total: f64,
5621) -> PyResult<f64> {
5622 nucleide_kinetics::prompt_jump(n_before, rho_before, rho_after, beta_total)
5623 .map_err(|e| PyValueError::new_err(e.to_string()))
5624}
5625
5626#[pyfunction]
5632fn spectroscopy_rect_smooth(counts: Vec<f64>, m: i64) -> PyResult<Vec<f64>> {
5633 let w = usize::try_from(m).map_err(|_| {
5634 PyValueError::new_err(format!("spectroscopy: smoothing width {m} is less than 3"))
5635 })?;
5636 nucleide_spectroscopy::rect_smooth(&counts, w).map_err(|e| PyValueError::new_err(e.to_string()))
5637}
5638
5639#[pyfunction]
5641fn spectroscopy_five_point_smooth(counts: Vec<f64>) -> PyResult<Vec<f64>> {
5642 nucleide_spectroscopy::five_point_smooth(&counts)
5643 .map_err(|e| PyValueError::new_err(e.to_string()))
5644}
5645
5646#[pyfunction]
5648fn spectroscopy_calc_bg(
5649 counts: Vec<f64>,
5650 channels: Vec<f64>,
5651 c1: i64,
5652 c2: i64,
5653 m: i64,
5654) -> PyResult<f64> {
5655 nucleide_spectroscopy::calc_bg(&counts, &channels, c1, c2, m)
5656 .map_err(|e| PyValueError::new_err(e.to_string()))
5657}
5658
5659#[pyfunction]
5661fn spectroscopy_gross_count(
5662 counts: Vec<f64>,
5663 channels: Vec<f64>,
5664 c1: i64,
5665 c2: i64,
5666) -> PyResult<f64> {
5667 nucleide_spectroscopy::gross_count(&counts, &channels, c1, c2)
5668 .map_err(|e| PyValueError::new_err(e.to_string()))
5669}
5670
5671#[pyfunction]
5673fn spectroscopy_net_counts(
5674 counts: Vec<f64>,
5675 channels: Vec<f64>,
5676 c1: i64,
5677 c2: i64,
5678 m: i64,
5679) -> PyResult<f64> {
5680 nucleide_spectroscopy::net_counts(&counts, &channels, c1, c2, m)
5681 .map_err(|e| PyValueError::new_err(e.to_string()))
5682}
5683
5684#[pyfunction]
5686fn spectroscopy_energy_bins(channels: Vec<f64>, calib_e_fit: Vec<f64>) -> PyResult<Vec<f64>> {
5687 nucleide_spectroscopy::energy_bins(&channels, &calib_e_fit)
5688 .map_err(|e| PyValueError::new_err(e.to_string()))
5689}
5690
5691#[pyfunction]
5693fn spectroscopy_detector_efficiency(
5694 energy_mev: f64,
5695 eff_coeff: Vec<f64>,
5696 eff_fit: i64,
5697) -> PyResult<f64> {
5698 nucleide_spectroscopy::detector_efficiency(energy_mev, &eff_coeff, eff_fit)
5699 .map_err(|e| PyValueError::new_err(e.to_string()))
5700}
5701
5702fn atomic_key(atomic: &BTreeMap<String, f64>, key: &str) -> PyResult<f64> {
5704 atomic
5705 .get(key)
5706 .copied()
5707 .ok_or_else(|| PyValueError::new_err(format!("atomic constants missing `{key}`")))
5708}
5709
5710#[pyfunction]
5719#[pyo3(signature = (atomic, k_conv=None, l_conv=None))]
5720fn spectroscopy_xray_lines(
5721 atomic: BTreeMap<String, f64>,
5722 k_conv: Option<f64>,
5723 l_conv: Option<f64>,
5724) -> PyResult<Vec<(f64, f64)>> {
5725 let data = nucleide_spectroscopy::AtomicData {
5726 k_shell_fluor: atomic_key(&atomic, "k_shell_fluor")?,
5727 l_shell_fluor: atomic_key(&atomic, "l_shell_fluor")?,
5728 prob: atomic_key(&atomic, "prob")?,
5729 kb_to_ka: atomic_key(&atomic, "kb_to_ka")?,
5730 ka2_to_ka1: atomic_key(&atomic, "ka2_to_ka1")?,
5731 ka1_en_kev: atomic_key(&atomic, "ka1_en_kev")?,
5732 ka2_en_kev: atomic_key(&atomic, "ka2_en_kev")?,
5733 kb_en_kev: atomic_key(&atomic, "kb_en_kev")?,
5734 l_en_kev: atomic_key(&atomic, "l_en_kev")?,
5735 };
5736 let present = |v: Option<f64>| v.filter(|x| !x.is_nan());
5738 Ok(
5739 nucleide_spectroscopy::xray_lines(&data, present(k_conv), present(l_conv))
5740 .iter()
5741 .map(|l| (l.energy_kev, l.intensity))
5742 .collect(),
5743 )
5744}
5745
5746#[pyfunction]
5761#[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))]
5762#[allow(clippy::too_many_arguments)]
5763fn spectroscopy_sdef_decay_source(
5764 lines: Vec<(f64, f64)>,
5765 x: f64,
5766 y: f64,
5767 z: f64,
5768 u: f64,
5769 v: f64,
5770 w: f64,
5771 weight: f64,
5772 particle: &str,
5773 version: u32,
5774) -> PyResult<(Vec<(f64, f64)>, String)> {
5775 let particle = particle
5776 .parse::<nucleide_nuclei::particles::ParticleId>()
5777 .map_err(|e| PyValueError::new_err(format!("spectroscopy: particle {e}")))?;
5778 let source = nucleide_spectroscopy::PointSource {
5779 x,
5780 y,
5781 z,
5782 u,
5783 v,
5784 w,
5785 weight,
5786 particle,
5787 };
5788 nucleide_spectroscopy::sdef_card(&lines, &source, version)
5789 .map_err(|e| PyValueError::new_err(e.to_string()))
5790}
5791
5792fn spectrum_to_py(
5794 py: Python<'_>,
5795 spec: &nucleide_spectroscopy::GammaSpectrum,
5796) -> PyResult<Py<PyAny>> {
5797 use pyo3::types::PyDict;
5798 let d = PyDict::new(py);
5799 let s = &spec.spectrum;
5800 d.set_item("spec_name", &s.spec_name)?;
5801 d.set_item("start_chan_num", s.start_chan_num)?;
5802 d.set_item("num_channels", s.num_channels)?;
5803 d.set_item("channels", &s.channels)?;
5804 d.set_item("counts", &s.counts)?;
5805 d.set_item("ebin", &s.ebin)?;
5806 d.set_item("real_time", spec.real_time)?;
5807 d.set_item("live_time", spec.live_time)?;
5808 d.set_item("dead_time", spec.dead_time())?;
5809 d.set_item("det_id", &spec.det_id)?;
5810 d.set_item("det_descp", &spec.det_descp)?;
5811 d.set_item("start_date", &spec.start_date)?;
5812 d.set_item("start_time", &spec.start_time)?;
5813 d.set_item("calib_e_fit", &spec.calib_e_fit)?;
5814 d.set_item("calib_fwhm_fit", &spec.calib_fwhm_fit)?;
5815 d.set_item("file_name", &spec.file_name)?;
5816 Ok(d.into_any().unbind())
5817}
5818
5819#[pyfunction]
5821fn spectroscopy_parse_dollar_spe(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
5822 let spec = nucleide_spectroscopy::parse_dollar_spe(text, "")
5823 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5824 spectrum_to_py(py, &spec)
5825}
5826
5827#[pyfunction]
5829fn spectroscopy_parse_spe(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
5830 let spec = nucleide_spectroscopy::parse_plain_spe(text, "")
5831 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5832 spectrum_to_py(py, &spec)
5833}
5834
5835#[pyfunction]
5837fn spectroscopy_read_dollar_spe(py: Python<'_>, path: &str) -> PyResult<Py<PyAny>> {
5838 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
5839 let spec = nucleide_spectroscopy::parse_dollar_spe(&text, path)
5840 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5841 spectrum_to_py(py, &spec)
5842}
5843
5844#[pyfunction]
5846fn spectroscopy_read_spe(py: Python<'_>, path: &str) -> PyResult<Py<PyAny>> {
5847 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
5848 let spec = nucleide_spectroscopy::parse_plain_spe(&text, path)
5849 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5850 spectrum_to_py(py, &spec)
5851}
5852
5853#[pyfunction]
5857fn spectroscopy_parse_lines_tsv(text: &str) -> PyResult<Vec<(f64, f64)>> {
5858 nucleide_spectroscopy::parse_lines_tsv(text).map_err(|e| PyValueError::new_err(e.to_string()))
5859}
5860
5861#[pyfunction]
5864fn spectroscopy_read_decay_lines(path: &str) -> PyResult<Vec<(f64, f64)>> {
5865 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
5866 nucleide_spectroscopy::parse_lines_tsv(&text).map_err(|e| PyValueError::new_err(e.to_string()))
5867}
5868
5869fn uq_sample_err(e: nucleide_linalg::sample::SampleError) -> PyErr {
5874 PyValueError::new_err(e.to_string())
5875}
5876
5877fn uq_decay_err(e: nucleide_linalg::decay::DecayError) -> PyErr {
5878 PyValueError::new_err(e.to_string())
5879}
5880
5881#[pyfunction]
5889fn uq_sample_mvn(
5890 py: Python<'_>,
5891 mean: Vec<f64>,
5892 cov: Vec<Vec<f64>>,
5893 n: usize,
5894 seed: u64,
5895) -> PyResult<Py<PyAny>> {
5896 use pyo3::types::PyDict;
5897 let set = nucleide_linalg::sample::sample_mvn(&mean, &cov, n, seed).map_err(uq_sample_err)?;
5898 let d = PyDict::new(py);
5899 d.set_item("samples", set.samples)?;
5900 d.set_item("method", set.method.name())?;
5901 match &set.method {
5902 nucleide_linalg::sample::FactorMethod::Cholesky => {
5903 d.set_item("min_eigen", py.None())?;
5904 d.set_item("max_eigen", py.None())?;
5905 }
5906 nucleide_linalg::sample::FactorMethod::EigenClip {
5907 min_eigen,
5908 max_eigen,
5909 } => {
5910 d.set_item("min_eigen", *min_eigen)?;
5911 d.set_item("max_eigen", *max_eigen)?;
5912 }
5913 }
5914 Ok(d.into_any().unbind())
5915}
5916
5917#[pyfunction]
5919fn uq_sample_mean(samples: Vec<Vec<f64>>) -> PyResult<Vec<f64>> {
5920 nucleide_linalg::sample::sample_mean(&samples).map_err(uq_sample_err)
5921}
5922
5923#[pyfunction]
5925fn uq_sample_cov(samples: Vec<Vec<f64>>) -> PyResult<Vec<Vec<f64>>> {
5926 nucleide_linalg::sample::sample_cov(&samples).map_err(uq_sample_err)
5927}
5928
5929#[pyfunction]
5935fn uq_check_convergence(
5936 py: Python<'_>,
5937 mean: Vec<f64>,
5938 cov: Vec<Vec<f64>>,
5939 samples: Vec<Vec<f64>>,
5940 mean_tol: f64,
5941 cov_tol: f64,
5942) -> PyResult<Py<PyAny>> {
5943 use pyo3::types::PyDict;
5944 let rep = nucleide_linalg::sample::check_convergence(&mean, &cov, &samples, mean_tol, cov_tol)
5945 .map_err(uq_sample_err)?;
5946 let d = PyDict::new(py);
5947 d.set_item("mean_err_max", rep.mean_err_max)?;
5948 d.set_item("cov_err_fro", rep.cov_err_fro)?;
5949 d.set_item("mean_tol", rep.mean_tol)?;
5950 d.set_item("cov_tol", rep.cov_tol)?;
5951 d.set_item("passed", rep.passed)?;
5952 Ok(d.into_any().unbind())
5953}
5954
5955#[pyfunction]
5958fn uq_perturb_branches(base: Vec<f64>, rel: Vec<f64>) -> PyResult<Vec<f64>> {
5959 nucleide_linalg::decay::perturb_branches(&base, &rel).map_err(uq_decay_err)
5960}
5961
5962#[pyfunction]
5965fn uq_perturb_energies(base: Vec<f64>, delta: Vec<f64>, convention: &str) -> PyResult<Vec<f64>> {
5966 let conv = nucleide_linalg::sample::PerturbConvention::parse(convention)
5967 .map_err(PyValueError::new_err)?;
5968 nucleide_linalg::decay::perturb_energies(&base, &delta, conv).map_err(uq_decay_err)
5969}
5970
5971#[pyfunction]
5973fn uq_passthrough(delta: Vec<f64>) -> PyResult<Vec<f64>> {
5974 nucleide_linalg::decay::passthrough(&delta).map_err(uq_decay_err)
5975}
5976
5977#[pyfunction]
5980fn uq_perturb_fission_yields(base: Vec<f64>, rel: Vec<f64>) -> PyResult<Vec<f64>> {
5981 nucleide_linalg::decay::perturb_fission_yields(&base, &rel).map_err(uq_decay_err)
5982}
5983
5984#[pymodule]
5986fn _internal(m: &Bound<'_, PyModule>) -> PyResult<()> {
5987 m.add_function(wrap_pyfunction!(version, m)?)?;
5988 m.add_function(wrap_pyfunction!(from_zaid, m)?)?;
5989 m.add_function(wrap_pyfunction!(atomic_mass, m)?)?;
5990 m.add_function(wrap_pyfunction!(natural_abundance, m)?)?;
5991 m.add_function(wrap_pyfunction!(rxname_id, m)?)?;
5992 m.add_function(wrap_pyfunction!(rxname_name, m)?)?;
5993 m.add_function(wrap_pyfunction!(rxname_mt, m)?)?;
5994 m.add_function(wrap_pyfunction!(read_xsdir, m)?)?;
5995 m.add_function(wrap_pyfunction!(read_meshtal, m)?)?;
5996 m.add_function(wrap_pyfunction!(read_wwinp, m)?)?;
5997 m.add_function(wrap_pyfunction!(read_mctal, m)?)?;
5998 m.add_function(wrap_pyfunction!(read_ssw, m)?)?;
5999 m.add_function(wrap_pyfunction!(read_ptrac, m)?)?;
6000 m.add_function(wrap_pyfunction!(read_mcpl, m)?)?;
6001 m.add_function(wrap_pyfunction!(write_mcpl, m)?)?;
6002 m.add_function(wrap_pyfunction!(ssw2mcpl, m)?)?;
6003 m.add_function(wrap_pyfunction!(mcpl2ssw, m)?)?;
6004 m.add_function(wrap_pyfunction!(read_endl, m)?)?;
6005 m.add_function(wrap_pyfunction!(endl_endftod, m)?)?;
6006 m.add_function(wrap_pyfunction!(combine_ssw_files, m)?)?;
6007 m.add_function(wrap_pyfunction!(read_chain, m)?)?;
6008 m.add_function(wrap_pyfunction!(build_depletion_system, m)?)?;
6009 m.add_function(wrap_pyfunction!(deplete, m)?)?;
6010 m.add_function(wrap_pyfunction!(deplete_series, m)?)?;
6011 m.add_function(wrap_pyfunction!(simple_xs, m)?)?;
6012 m.add_function(wrap_pyfunction!(scattering_length, m)?)?;
6013 m.add_function(wrap_pyfunction!(decay_energy, m)?)?;
6014 m.add_function(wrap_pyfunction!(decay_branches, m)?)?;
6015 m.add_function(wrap_pyfunction!(decay_branch_fraction, m)?)?;
6016 m.add_function(wrap_pyfunction!(normalize_nuclide, m)?)?;
6017 m.add_function(wrap_pyfunction!(decay_heat, m)?)?;
6018 m.add_function(wrap_pyfunction!(dose_factor, m)?)?;
6019 m.add_function(wrap_pyfunction!(dose_per_g, m)?)?;
6020 m.add_function(wrap_pyfunction!(read_serpent, m)?)?;
6021 m.add_function(wrap_pyfunction!(read_usrbin, m)?)?;
6022 m.add_function(wrap_pyfunction!(magic, m)?)?;
6023 m.add_function(wrap_pyfunction!(write_ssw, m)?)?;
6024 m.add_function(wrap_pyfunction!(mesh_to_geom, m)?)?;
6025 m.add_function(wrap_pyfunction!(half_life, m)?)?;
6026 m.add_function(wrap_pyfunction!(decay_constant, m)?)?;
6027 m.add_function(wrap_pyfunction!(q_value_capture, m)?)?;
6028 m.add_function(wrap_pyfunction!(q_value_alpha, m)?)?;
6029 m.add_function(wrap_pyfunction!(read_inp, m)?)?;
6030 m.add_function(wrap_pyfunction!(from_formula, m)?)?;
6031 m.add_function(wrap_pyfunction!(activity, m)?)?;
6032 m.add_function(wrap_pyfunction!(to_xml, m)?)?;
6033 m.add_function(wrap_pyfunction!(alara_parse_deck, m)?)?;
6034 m.add_function(wrap_pyfunction!(alara_parse_flux, m)?)?;
6035 m.add_function(wrap_pyfunction!(alara_parse_output, m)?)?;
6036 m.add_function(wrap_pyfunction!(alara_expand_schedule, m)?)?;
6037 m.add_function(wrap_pyfunction!(isotxs_parse, m)?)?;
6038 m.add_function(wrap_pyfunction!(rtflux_parse, m)?)?;
6039 m.add_function(wrap_pyfunction!(partisn_render, m)?)?;
6040 m.add_function(wrap_pyfunction!(partisn_validate, m)?)?;
6041 m.add_function(wrap_pyfunction!(fispact_parse_output, m)?)?;
6042 m.add_function(wrap_pyfunction!(origen_parse_tape5, m)?)?;
6043 m.add_function(wrap_pyfunction!(origen_parse_tape6, m)?)?;
6044 m.add_function(wrap_pyfunction!(origen_parse_tape9, m)?)?;
6045 m.add_function(wrap_pyfunction!(r2s_from_deck, m)?)?;
6046 m.add_function(wrap_pyfunction!(r2s_from_snapshot, m)?)?;
6047 m.add_function(wrap_pyfunction!(r2s_validate, m)?)?;
6048 m.add_function(wrap_pyfunction!(r2s_expand, m)?)?;
6049 m.add_function(wrap_pyfunction!(r2s_assemble, m)?)?;
6050 m.add_function(wrap_pyfunction!(r2s_tag_zone_strength, m)?)?;
6051 m.add_function(wrap_pyfunction!(r2s_photon_group_sums, m)?)?;
6052 m.add_function(wrap_pyfunction!(kinetics_solve, m)?)?;
6053 m.add_function(wrap_pyfunction!(kinetics_equilibrium, m)?)?;
6054 m.add_function(wrap_pyfunction!(kinetics_initial_rate, m)?)?;
6055 m.add_function(wrap_pyfunction!(kinetics_inhour_rho, m)?)?;
6056 m.add_function(wrap_pyfunction!(kinetics_stable_period, m)?)?;
6057 m.add_function(wrap_pyfunction!(kinetics_prompt_jump, m)?)?;
6058 m.add_function(wrap_pyfunction!(spectroscopy_rect_smooth, m)?)?;
6059 m.add_function(wrap_pyfunction!(spectroscopy_five_point_smooth, m)?)?;
6060 m.add_function(wrap_pyfunction!(spectroscopy_calc_bg, m)?)?;
6061 m.add_function(wrap_pyfunction!(spectroscopy_gross_count, m)?)?;
6062 m.add_function(wrap_pyfunction!(spectroscopy_net_counts, m)?)?;
6063 m.add_function(wrap_pyfunction!(spectroscopy_energy_bins, m)?)?;
6064 m.add_function(wrap_pyfunction!(spectroscopy_detector_efficiency, m)?)?;
6065 m.add_function(wrap_pyfunction!(spectroscopy_xray_lines, m)?)?;
6066 m.add_function(wrap_pyfunction!(spectroscopy_sdef_decay_source, m)?)?;
6067 m.add_function(wrap_pyfunction!(spectroscopy_parse_dollar_spe, m)?)?;
6068 m.add_function(wrap_pyfunction!(spectroscopy_parse_spe, m)?)?;
6069 m.add_function(wrap_pyfunction!(spectroscopy_read_dollar_spe, m)?)?;
6070 m.add_function(wrap_pyfunction!(spectroscopy_read_spe, m)?)?;
6071 m.add_function(wrap_pyfunction!(spectroscopy_parse_lines_tsv, m)?)?;
6072 m.add_function(wrap_pyfunction!(spectroscopy_read_decay_lines, m)?)?;
6073 m.add_function(wrap_pyfunction!(uq_sample_mvn, m)?)?;
6074 m.add_function(wrap_pyfunction!(uq_sample_mean, m)?)?;
6075 m.add_function(wrap_pyfunction!(uq_sample_cov, m)?)?;
6076 m.add_function(wrap_pyfunction!(uq_check_convergence, m)?)?;
6077 m.add_function(wrap_pyfunction!(uq_perturb_branches, m)?)?;
6078 m.add_function(wrap_pyfunction!(uq_perturb_energies, m)?)?;
6079 m.add_function(wrap_pyfunction!(uq_passthrough, m)?)?;
6080 m.add_function(wrap_pyfunction!(uq_perturb_fission_yields, m)?)?;
6081 m.add_function(wrap_pyfunction!(parse_deck, m)?)?;
6082 m.add_function(wrap_pyfunction!(read_deck, m)?)?;
6083 m.add_function(wrap_pyfunction!(cumulative_decays, m)?)?;
6084 m.add_function(wrap_pyfunction!(progeny, m)?)?;
6085 m.add_function(wrap_pyfunction!(branching_fraction, m)?)?;
6086 m.add_function(wrap_pyfunction!(decay_mode, m)?)?;
6087 m.add_function(wrap_pyfunction!(chain_edges, m)?)?;
6088 m.add_function(wrap_pyfunction!(armi_to_nucid, m)?)?;
6089 m.add_function(wrap_pyfunction!(nucid_to_armi, m)?)?;
6090 m.add_function(wrap_pyfunction!(mcc3_to_nucid, m)?)?;
6091 m.add_function(wrap_pyfunction!(check_labels, m)?)?;
6092 m.add_function(wrap_pyfunction!(audit_material, m)?)?;
6093 m.add_function(wrap_pyfunction!(separate_material, m)?)?;
6094 m.add_function(wrap_pyfunction!(blend_material, m)?)?;
6095 m.add_function(wrap_pyfunction!(enrichment_value_func, m)?)?;
6096 m.add_function(wrap_pyfunction!(enrichment_swu_per_feed, m)?)?;
6097 m.add_function(wrap_pyfunction!(enrichment_swu_per_prod, m)?)?;
6098 m.add_function(wrap_pyfunction!(enrichment_swu_per_tail, m)?)?;
6099 m.add_class::<PyCusum>()?;
6100 m.add_function(wrap_pyfunction!(emit_cards, m)?)?;
6101 m.add_function(wrap_pyfunction!(emit_drift_table, m)?)?;
6102 m.add_function(wrap_pyfunction!(emit_armi_cards, m)?)?;
6103 m.add_function(wrap_pyfunction!(emit_armi_drift_table, m)?)?;
6104 m.add_class::<PyNuclide>()?;
6105 m.add_class::<PyParticle>()?;
6106 m.add_class::<PyXsdir>()?;
6107 m.add_class::<PyXsdirTable>()?;
6108 m.add_class::<PyMeshtal>()?;
6109 m.add_class::<PyMeshTally>()?;
6110 m.add_class::<PyWwinp>()?;
6111 m.add_class::<PyMctal>()?;
6112 m.add_class::<PySurfSrc>()?;
6113 m.add_class::<PyPtracFile>()?;
6114 m.add_class::<PyMcplFile>()?;
6115 m.add_class::<PyEndlLibrary>()?;
6116 m.add_class::<PyChain>()?;
6117 m.add_class::<PyDepletionSystem>()?;
6118 m.add_class::<PyUsrbinTally>()?;
6119 m.add_class::<PyMagicOutput>()?;
6120 m.add_class::<PyAliasTable>()?;
6121 m.add_class::<PyMeshSourceSampler>()?;
6122 m.add_class::<PyCascade>()?;
6123 m.add_class::<PyMaterialsCompendium>()?;
6124 m.add_class::<PyDeckProblem>()?;
6125 m.add_class::<PyInventory>()?;
6126 Ok(())
6127}