1use std::collections::BTreeMap;
7use std::str::FromStr;
8
9use pyo3::exceptions::{PyTypeError, PyValueError};
10use pyo3::prelude::*;
11
12use numpy::{IntoPyArray, PyArray1, PyArray2, PyArrayMethods};
13
14use nucleide_nuclei::NuclideId;
15
16#[pyfunction]
18fn version() -> &'static str {
19 env!("CARGO_PKG_VERSION")
20}
21
22fn wrap_nucid_err(e: nucleide_nuclei::Error) -> PyErr {
23 PyValueError::new_err(e.to_string())
24}
25
26#[pyclass(name = "Nuclide")]
32struct PyNuclide {
33 inner: NuclideId,
34}
35
36#[pymethods]
37impl PyNuclide {
38 #[new]
40 fn new(name: &str) -> PyResult<Self> {
41 NuclideId::from_name(name)
42 .map(|inner| Self { inner })
43 .map_err(wrap_nucid_err)
44 }
45
46 #[getter]
48 fn name(&self) -> String {
49 self.inner.to_name()
50 }
51
52 #[getter]
54 fn nucid(&self) -> u32 {
55 self.inner.nucid()
56 }
57
58 #[getter]
60 fn zzaaam(&self) -> u32 {
61 self.inner.zzaaam()
62 }
63
64 #[getter]
66 fn z(&self) -> u32 {
67 self.inner.z()
68 }
69
70 #[getter]
72 fn a(&self) -> u32 {
73 self.inner.a()
74 }
75
76 #[getter]
78 fn state(&self) -> u32 {
79 self.inner.state()
80 }
81
82 #[getter]
84 fn zaid(&self) -> u32 {
85 nucleide_nuclei::dialects::to_zaid(self.inner)
86 }
87
88 #[getter]
90 fn zzllaaam(&self) -> String {
91 nucleide_nuclei::dialects::zzllaaam(self.inner)
92 }
93
94 #[getter]
96 fn serpent(&self) -> String {
97 nucleide_nuclei::dialects::serpent(self.inner)
98 }
99
100 #[getter]
102 fn nist(&self) -> String {
103 nucleide_nuclei::dialects::nist(self.inner)
104 }
105
106 #[getter]
108 fn cinder(&self) -> u32 {
109 nucleide_nuclei::dialects::to_cinder(self.inner)
110 }
111
112 #[getter]
114 fn alara(&self) -> String {
115 nucleide_nuclei::dialects::alara(self.inner)
116 }
117
118 #[getter]
120 fn sza(&self) -> u32 {
121 nucleide_nuclei::dialects::to_sza(self.inner)
122 }
123
124 fn fluka(&self) -> PyResult<&'static str> {
126 nucleide_nuclei::dialects::id_to_fluka(self.inner)
127 .map_err(|e| PyValueError::new_err(e.to_string()))
128 }
129
130 #[getter]
132 fn mass(&self) -> Option<f64> {
133 nucleide_nuclei::data::atomic_mass(self.inner.nucid())
134 }
135
136 #[getter]
138 fn abundance(&self) -> Option<f64> {
139 nucleide_nuclei::data::natural_abundance(self.inner.nucid())
140 }
141
142 fn __repr__(&self) -> String {
143 format!("Nuclide({})", self.inner.to_name())
144 }
145}
146
147#[pyfunction]
149fn from_zaid(zaid: u32) -> PyResult<PyNuclide> {
150 nucleide_nuclei::dialects::from_zaid(zaid)
151 .map(|inner| PyNuclide { inner })
152 .map_err(|e| PyValueError::new_err(e.to_string()))
153}
154
155fn lookup(key: &Bound<'_, PyAny>, f: impl Fn(u32) -> Option<f64>) -> PyResult<Option<f64>> {
156 if let Ok(nucid) = key.extract::<u32>() {
157 return Ok(f(nucid));
158 }
159 if let Ok(name) = key.extract::<&str>() {
160 let id = NuclideId::from_name(name).map_err(wrap_nucid_err)?;
161 return Ok(f(id.nucid()));
162 }
163 Err(PyTypeError::new_err("expected int nucid or str name"))
164}
165
166#[pyfunction]
168fn atomic_mass(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
169 lookup(key, nucleide_nuclei::data::atomic_mass)
170}
171
172#[pyfunction]
174fn natural_abundance(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
175 lookup(key, nucleide_nuclei::data::natural_abundance)
176}
177
178#[pyclass(name = "Particle")]
180struct PyParticle {
181 inner: nucleide_nuclei::particles::ParticleId,
182}
183
184#[pymethods]
185impl PyParticle {
186 #[new]
188 fn new(spec: &Bound<'_, PyAny>) -> PyResult<Self> {
189 let inner = if let Ok(pdc) = spec.extract::<i32>() {
190 nucleide_nuclei::particles::ParticleId::from_pdc(pdc)
191 .ok_or_else(|| PyValueError::new_err(format!("unknown PDC code {pdc}")))?
192 } else if let Ok(s) = spec.extract::<&str>() {
193 s.parse::<nucleide_nuclei::particles::ParticleId>()
194 .map_err(|e| PyValueError::new_err(e.to_string()))?
195 } else {
196 return Err(PyTypeError::new_err("expected str alias or int PDC"));
197 };
198 Ok(Self { inner })
199 }
200
201 #[getter]
202 fn name(&self) -> &'static str {
203 self.inner.name()
204 }
205
206 #[getter]
207 fn describe(&self) -> &'static str {
208 self.inner.describe()
209 }
210
211 fn mcnp(&self) -> Option<&'static str> {
212 self.inner.mcnp()
213 }
214 fn mcnp6(&self) -> Option<&'static str> {
215 self.inner.mcnp6()
216 }
217 fn fluka(&self) -> Option<&'static str> {
218 self.inner.fluka()
219 }
220 fn geant4(&self) -> Option<&'static str> {
221 self.inner.geant4()
222 }
223
224 fn __repr__(&self) -> String {
225 format!("Particle('{}')", self.inner.name())
226 }
227}
228
229#[pyfunction]
231fn rxname_id(name: &str) -> PyResult<u32> {
232 nucleide_nuclei::rxname::name_to_id(name).map_err(|e| PyValueError::new_err(e.to_string()))
233}
234
235#[pyfunction]
237fn rxname_name(id: u32) -> Option<&'static str> {
238 nucleide_nuclei::rxname::id_to_name(id)
239}
240
241#[pyfunction]
243fn rxname_mt(id: u32) -> i32 {
244 nucleide_nuclei::rxname::id_to_mt(id)
245}
246
247fn io_err(e: nucleide_mcnp_io::xsdir::Error) -> PyErr {
252 PyValueError::new_err(e.to_string())
253}
254fn m_err<T>(r: Result<T, impl std::fmt::Display>) -> PyResult<T> {
255 r.map_err(|e| PyValueError::new_err(e.to_string()))
256}
257
258#[pyclass(name = "XsdirTable")]
260struct PyXsdirTable {
261 inner: nucleide_mcnp_io::xsdir::XsdirTable,
262}
263
264#[pymethods]
265impl PyXsdirTable {
266 #[getter]
267 fn name(&self) -> &str {
268 &self.inner.name
269 }
270 #[getter]
271 fn awr(&self) -> f64 {
272 self.inner.awr
273 }
274 #[getter]
275 fn filename(&self) -> &str {
276 &self.inner.filename
277 }
278 #[getter]
279 fn filetype(&self) -> i64 {
280 self.inner.filetype
281 }
282 #[getter]
283 fn address(&self) -> i64 {
284 self.inner.address
285 }
286 #[getter]
287 fn tablelength(&self) -> i64 {
288 self.inner.tablelength
289 }
290 #[getter]
291 fn temperature(&self) -> Option<f64> {
292 self.inner.temperature
293 }
294 #[getter]
295 fn ptable(&self) -> bool {
296 self.inner.ptable
297 }
298 fn zaid(&self) -> &str {
300 self.inner.zaid()
301 }
302 fn to_serpent(&self, directory: &str) -> PyResult<String> {
304 m_err(self.inner.to_serpent(directory))
305 }
306 fn __repr__(&self) -> String {
307 format!("<XsdirTable: {}>", self.inner.name)
308 }
309}
310
311#[pyclass(name = "Xsdir")]
313struct PyXsdir {
314 inner: nucleide_mcnp_io::xsdir::Xsdir,
315}
316
317#[pymethods]
318impl PyXsdir {
319 #[getter]
320 fn datapath(&self) -> Option<&str> {
321 self.inner.datapath.as_deref()
322 }
323 #[getter]
325 fn awr(&self) -> BTreeMap<u32, f64> {
326 self.inner.awr.clone()
327 }
328 #[getter]
330 fn tables(&self) -> Vec<PyXsdirTable> {
331 self.inner
332 .tables
333 .iter()
334 .map(|t| PyXsdirTable { inner: t.clone() })
335 .collect()
336 }
337 fn find_table(&self, name: &str) -> Vec<PyXsdirTable> {
339 self.inner
340 .find_table(name)
341 .into_iter()
342 .map(|t| PyXsdirTable { inner: t.clone() })
343 .collect()
344 }
345 fn nucs(&self) -> Vec<u32> {
347 self.inner.nucs().iter().map(|n| n.nucid()).collect()
348 }
349}
350
351#[pyfunction]
353fn read_xsdir(path: &str) -> PyResult<PyXsdir> {
354 nucleide_mcnp_io::xsdir::Xsdir::from_file(path)
355 .map(|inner| PyXsdir { inner })
356 .map_err(io_err)
357}
358
359#[pyclass(name = "MeshTally")]
361struct PyMeshTally {
362 inner: nucleide_mcnp_io::meshtal::MeshTallyData,
363}
364
365#[pymethods]
366impl PyMeshTally {
367 #[getter]
368 fn tally_number(&self) -> u32 {
369 self.inner.tally_number
370 }
371 #[getter]
373 fn particle(&self) -> char {
374 self.inner.particle.letter()
375 }
376 #[getter]
377 fn dose_response(&self) -> bool {
378 self.inner.dose_response
379 }
380 #[getter]
381 fn x_bounds(&self) -> Vec<f64> {
382 self.inner.x_bounds.clone()
383 }
384 #[getter]
385 fn y_bounds(&self) -> Vec<f64> {
386 self.inner.y_bounds.clone()
387 }
388 #[getter]
389 fn z_bounds(&self) -> Vec<f64> {
390 self.inner.z_bounds.clone()
391 }
392 #[getter]
393 fn e_bounds(&self) -> Vec<f64> {
394 self.inner.e_bounds.clone()
395 }
396 fn dims(&self) -> [usize; 3] {
398 self.inner.dims()
399 }
400 fn num_ves(&self) -> usize {
401 self.inner.num_ves()
402 }
403 fn num_e_groups(&self) -> usize {
404 self.inner.num_e_groups()
405 }
406 fn cell(&self, i: usize, j: usize, k: usize) -> (Vec<f64>, Vec<f64>) {
408 let (r, e) = self.inner.cell(i, j, k);
409 (r.to_vec(), e.to_vec())
410 }
411 fn cell_total(&self, i: usize, j: usize, k: usize) -> (f64, f64) {
413 self.inner.cell_total(i, j, k)
414 }
415 #[getter]
417 fn result(&self) -> Vec<Vec<f64>> {
418 self.inner.result.clone()
419 }
420 #[getter]
422 fn rel_error(&self) -> Vec<Vec<f64>> {
423 self.inner.rel_error.clone()
424 }
425 #[getter]
427 fn total_result(&self) -> Vec<f64> {
428 self.inner.total_result.clone()
429 }
430 #[getter]
432 fn total_rel_error(&self) -> Vec<f64> {
433 self.inner.total_rel_error.clone()
434 }
435 fn to_list(&self) -> (Vec<Vec<f64>>, Vec<Vec<f64>>) {
440 (self.inner.result.clone(), self.inner.rel_error.clone())
441 }
442 fn totals_list(&self) -> (Vec<f64>, Vec<f64>) {
444 (
445 self.inner.total_result.clone(),
446 self.inner.total_rel_error.clone(),
447 )
448 }
449 #[allow(clippy::type_complexity)]
459 fn result_array<'py>(
460 &self,
461 py: Python<'py>,
462 ) -> PyResult<(Bound<'py, PyArray2<f64>>, Bound<'py, PyArray2<f64>>)> {
463 let n_ve = self.inner.num_ves();
464 let n_g = self.inner.num_e_groups();
465 let flatten = |rows: &[Vec<f64>], name: &str| -> PyResult<Vec<f64>> {
466 if rows.len() != n_ve {
467 return Err(PyValueError::new_err(format!(
468 "tally {name}: expected {n_ve} rows, found {}",
469 rows.len()
470 )));
471 }
472 let mut flat = Vec::with_capacity(n_ve * n_g);
473 for (ve, row) in rows.iter().enumerate() {
474 if row.len() != n_g {
475 return Err(PyValueError::new_err(format!(
476 "tally {name}: row {ve} has {} groups, expected {n_g}",
477 row.len()
478 )));
479 }
480 flat.extend_from_slice(row);
481 }
482 Ok(flat)
483 };
484 let flat_r = flatten(&self.inner.result, "result")?;
485 let flat_e = flatten(&self.inner.rel_error, "rel_error")?;
486 let arr_r = m_err(
487 flat_r
488 .into_pyarray(py)
489 .reshape((n_ve, n_g))
490 .map_err(|e| e.to_string()),
491 )?;
492 let arr_e = m_err(
493 flat_e
494 .into_pyarray(py)
495 .reshape((n_ve, n_g))
496 .map_err(|e| e.to_string()),
497 )?;
498 Ok((arr_r, arr_e))
499 }
500 #[allow(clippy::type_complexity)]
506 fn totals_array<'py>(
507 &self,
508 py: Python<'py>,
509 ) -> PyResult<(Bound<'py, PyArray1<f64>>, Bound<'py, PyArray1<f64>>)> {
510 Ok((
511 self.inner.total_result.clone().into_pyarray(py),
512 self.inner.total_rel_error.clone().into_pyarray(py),
513 ))
514 }
515}
516
517#[pyclass(name = "Meshtal")]
519struct PyMeshtal {
520 inner: nucleide_mcnp_io::meshtal::Meshtal,
521}
522
523#[pymethods]
524impl PyMeshtal {
525 #[getter]
526 fn version(&self) -> &str {
527 &self.inner.version
528 }
529 #[getter]
530 fn ld(&self) -> &str {
531 &self.inner.ld
532 }
533 #[getter]
534 fn title(&self) -> &str {
535 &self.inner.title
536 }
537 #[getter]
538 fn histories(&self) -> u64 {
539 self.inner.histories
540 }
541 #[getter]
543 fn tallies(&self) -> BTreeMap<u32, PyMeshTally> {
544 self.inner
545 .tallies
546 .iter()
547 .map(|(k, v)| (*k, PyMeshTally { inner: v.clone() }))
548 .collect()
549 }
550}
551
552#[pyfunction]
554fn read_meshtal(path: &str) -> PyResult<PyMeshtal> {
555 m_err(nucleide_mcnp_io::meshtal::Meshtal::from_file(path).map(|inner| PyMeshtal { inner }))
556}
557
558#[pyclass(name = "Wwinp")]
560struct PyWwinp {
561 inner: nucleide_mcnp_io::wwinp::Wwinp,
562}
563
564#[pymethods]
565impl PyWwinp {
566 #[getter]
567 fn ni(&self) -> u32 {
568 self.inner.ni
569 }
570 #[getter]
571 fn nr(&self) -> u32 {
572 self.inner.nr
573 }
574 #[getter]
575 fn ne(&self) -> Vec<u32> {
576 self.inner.ne.clone()
577 }
578 #[getter]
579 fn nf(&self) -> [u32; 3] {
580 self.inner.nf
581 }
582 #[getter]
583 fn origin(&self) -> [f64; 3] {
584 self.inner.origin
585 }
586 #[getter]
587 fn nc(&self) -> [u32; 3] {
588 self.inner.nc
589 }
590 #[getter]
592 fn cm(&self) -> Vec<Vec<f64>> {
593 self.inner.cm.clone()
594 }
595 #[getter]
597 fn bounds(&self) -> Vec<Vec<f64>> {
598 self.inner.bounds.clone()
599 }
600 #[getter]
602 fn e(&self) -> Vec<Vec<f64>> {
603 self.inner.e.clone()
604 }
605 fn ww_row(&self, particle: usize, group: usize) -> Vec<f64> {
607 self.inner.ww[particle][group].clone()
608 }
609 fn ww_column(&self, particle: usize, ve: usize) -> Vec<f64> {
611 self.inner.ww_column(particle, ve)
612 }
613 fn ww_row_array<'py>(
622 &self,
623 py: Python<'py>,
624 particle: usize,
625 group: usize,
626 ) -> PyResult<Bound<'py, PyArray1<f64>>> {
627 let row = self
628 .inner
629 .ww
630 .get(particle)
631 .and_then(|groups| groups.get(group))
632 .ok_or_else(|| {
633 PyValueError::new_err(format!(
634 "ww_row_array: particle {particle} group {group} out of range"
635 ))
636 })?;
637 Ok(row.clone().into_pyarray(py))
638 }
639 fn ww_column_array<'py>(
646 &self,
647 py: Python<'py>,
648 particle: usize,
649 ve: usize,
650 ) -> PyResult<Bound<'py, PyArray1<f64>>> {
651 let groups = self.inner.ww.get(particle).ok_or_else(|| {
652 PyValueError::new_err(format!("ww_column_array: particle {particle} out of range"))
653 })?;
654 if groups.is_empty() {
655 return Err(PyValueError::new_err(format!(
656 "ww_column_array: particle {particle} has no groups"
657 )));
658 }
659 let nft = groups[0].len();
660 if ve >= nft {
661 return Err(PyValueError::new_err(format!(
662 "ww_column_array: ve {ve} out of range for {nft} volume elements"
663 )));
664 }
665 for (g, row) in groups.iter().enumerate() {
666 if row.len() != nft {
667 return Err(PyValueError::new_err(format!(
668 "ww particle {particle}: group {g} has {} values, expected {nft}",
669 row.len()
670 )));
671 }
672 }
673 let col: Vec<f64> = groups.iter().map(|row| row[ve]).collect();
674 Ok(col.into_pyarray(py))
675 }
676 fn ww_particle_array<'py>(
687 &self,
688 py: Python<'py>,
689 particle: usize,
690 ) -> PyResult<Bound<'py, PyArray2<f64>>> {
691 let groups = self.inner.ww.get(particle).ok_or_else(|| {
692 PyValueError::new_err(format!(
693 "ww_particle_array: particle {particle} out of range"
694 ))
695 })?;
696 if groups.is_empty() {
697 return Err(PyValueError::new_err(format!(
698 "ww_particle_array: particle {particle} has no groups"
699 )));
700 }
701 let nft = groups[0].len();
702 let mut flat = Vec::with_capacity(groups.len() * nft);
703 for (g, row) in groups.iter().enumerate() {
704 if row.len() != nft {
705 return Err(PyValueError::new_err(format!(
706 "ww particle {particle}: group {g} has {} values, expected {nft}",
707 row.len()
708 )));
709 }
710 flat.extend_from_slice(row);
711 }
712 let n_g = groups.len();
713 m_err(
714 flat.into_pyarray(py)
715 .reshape((n_g, nft))
716 .map_err(|e| e.to_string()),
717 )
718 }
719}
720
721#[pyfunction]
723fn read_wwinp(path: &str) -> PyResult<PyWwinp> {
724 m_err(nucleide_mcnp_io::wwinp::Wwinp::from_file(path).map(|inner| PyWwinp { inner }))
725}
726
727fn mctal_card_dict<'py>(
731 py: Python<'py>,
732 card: &nucleide_mcnp_io::mctal::BinCard,
733) -> PyResult<pyo3::Bound<'py, pyo3::types::PyDict>> {
734 let c = pyo3::types::PyDict::new(py);
735 c.set_item("count", card.count)?;
736 c.set_item("values", card.values.clone())?;
737 c.set_item("variant", card.variant.map(|v| v.to_string()))?;
738 c.set_item("flag", card.flag)?;
739 Ok(c)
740}
741
742#[pyclass(name = "Mctal")]
743struct PyMctal {
744 inner: nucleide_mcnp_io::mctal::Mctal,
745}
746
747#[pymethods]
748impl PyMctal {
749 #[getter]
750 fn code_name(&self) -> &str {
751 &self.inner.code_name
752 }
753 #[getter]
754 fn comment(&self) -> &str {
755 &self.inner.comment
756 }
757 #[getter]
758 fn n_histories(&self) -> u64 {
759 self.inner.n_histories
760 }
761 #[getter]
762 fn n_cycles(&self) -> usize {
763 self.inner.n_cycles
764 }
765 #[getter]
766 fn n_inactive(&self) -> usize {
767 self.inner.n_inactive
768 }
769 #[getter]
770 fn vars_per_cycle(&self) -> usize {
771 self.inner.vars_per_cycle
772 }
773 #[getter]
774 fn k_col(&self) -> Vec<f64> {
775 self.inner.k_col.clone()
776 }
777 #[getter]
778 fn k_abs(&self) -> Vec<f64> {
779 self.inner.k_abs.clone()
780 }
781 #[getter]
782 fn k_path(&self) -> Vec<f64> {
783 self.inner.k_path.clone()
784 }
785 #[getter]
786 fn prompt_life_col(&self) -> Vec<f64> {
787 self.inner.prompt_life_col.clone()
788 }
789 #[getter]
790 fn prompt_life_path(&self) -> Vec<f64> {
791 self.inner.prompt_life_path.clone()
792 }
793 #[getter]
796 fn averages(&self) -> Vec<BTreeMap<String, f64>> {
797 self.inner
798 .averages
799 .iter()
800 .map(|a| {
801 let mut m = BTreeMap::new();
802 m.insert("avg_k_col".into(), a.avg_k_col.0);
803 m.insert("avg_k_col_stdev".into(), a.avg_k_col.1);
804 m.insert("avg_k_abs".into(), a.avg_k_abs.0);
805 m.insert("avg_k_abs_stdev".into(), a.avg_k_abs.1);
806 m.insert("avg_k_path".into(), a.avg_k_path.0);
807 m.insert("avg_k_path_stdev".into(), a.avg_k_path.1);
808 m.insert("avg_k_combined".into(), a.avg_k_combined.0);
809 m.insert("avg_k_combined_stdev".into(), a.avg_k_combined.1);
810 m.insert("avg_k_combined_active".into(), a.avg_k_combined_active.0);
811 m.insert(
812 "avg_k_combined_active_stdev".into(),
813 a.avg_k_combined_active.1,
814 );
815 m.insert("prompt_life_combined".into(), a.prompt_life_combined.0);
816 m.insert(
817 "prompt_life_combined_stdev".into(),
818 a.prompt_life_combined.1,
819 );
820 m.insert("cycle_histories".into(), a.cycle_histories);
821 m.insert("fom".into(), a.fom);
822 m
823 })
824 .collect()
825 }
826 #[allow(clippy::type_complexity)]
837 fn k_arrays<'py>(
838 &self,
839 py: Python<'py>,
840 ) -> PyResult<(
841 Bound<'py, PyArray1<f64>>,
842 Bound<'py, PyArray1<f64>>,
843 Bound<'py, PyArray1<f64>>,
844 Bound<'py, PyArray1<f64>>,
845 Bound<'py, PyArray1<f64>>,
846 )> {
847 Ok((
848 self.inner.k_col.clone().into_pyarray(py),
849 self.inner.k_abs.clone().into_pyarray(py),
850 self.inner.k_path.clone().into_pyarray(py),
851 self.inner.prompt_life_col.clone().into_pyarray(py),
852 self.inner.prompt_life_path.clone().into_pyarray(py),
853 ))
854 }
855 fn averages_array<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArray2<f64>>> {
867 let n = self.inner.averages.len();
868 let mut flat = Vec::with_capacity(n * 14);
869 for a in &self.inner.averages {
870 flat.extend_from_slice(&[
871 a.avg_k_col.0,
872 a.avg_k_col.1,
873 a.avg_k_abs.0,
874 a.avg_k_abs.1,
875 a.avg_k_path.0,
876 a.avg_k_path.1,
877 a.avg_k_combined.0,
878 a.avg_k_combined.1,
879 a.avg_k_combined_active.0,
880 a.avg_k_combined_active.1,
881 a.prompt_life_combined.0,
882 a.prompt_life_combined.1,
883 a.cycle_histories,
884 a.fom,
885 ]);
886 }
887 m_err(
888 flat.into_pyarray(py)
889 .reshape((n, 14))
890 .map_err(|e| e.to_string()),
891 )
892 }
893 #[getter]
896 fn npert(&self) -> Option<String> {
897 self.inner.npert.clone()
898 }
899 #[getter]
901 fn tally_nums(&self) -> Vec<u32> {
902 self.inner.tally_nums.clone()
903 }
904 #[getter]
914 fn tallies(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
915 use pyo3::types::PyDict;
916 let mut out = Vec::with_capacity(self.inner.tallies.len());
917 for t in &self.inner.tallies {
918 let d = PyDict::new(py);
919 d.set_item("number", t.number)?;
920 d.set_item("particle_type", t.particle_type)?;
921 d.set_item("detector_type", t.detector_type)?;
922 d.set_item("particle_list", t.particle_list.clone())?;
923 d.set_item("comment", t.comment.clone())?;
924 for (key, card) in [
925 ("f", &t.f),
926 ("d", &t.d),
927 ("u", &t.u),
928 ("s", &t.s),
929 ("m", &t.m),
930 ("c", &t.c),
931 ("e", &t.e),
932 ("t", &t.t),
933 ] {
934 d.set_item(key, mctal_card_dict(py, card)?)?;
935 }
936 let vals: Vec<(f64, f64)> = t.vals.clone();
937 d.set_item("vals", vals)?;
938 let tfc_obj = if let Some(tfc) = &t.tfc {
939 let td = PyDict::new(py);
940 td.set_item("jtf", tfc.jtf.clone())?;
941 let mut rows = Vec::with_capacity(tfc.rows.len());
942 for r in &tfc.rows {
943 let rd = PyDict::new(py);
944 rd.set_item("nps", r.nps)?;
945 rd.set_item("value", r.value)?;
946 rd.set_item("rel_err", r.rel_err)?;
947 rd.set_item("fom", r.fom)?;
948 rows.push(rd.into_any().unbind());
949 }
950 td.set_item("rows", rows)?;
951 td.into_any().unbind()
952 } else {
953 py.None()
954 };
955 d.set_item("tfc", tfc_obj)?;
956 d.set_item("total", t.total_val())?;
957 out.push(d.into_any().unbind());
958 }
959 Ok(out)
960 }
961 #[getter]
967 fn mesh_tallies(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
968 use pyo3::types::PyDict;
969 let mut out = Vec::with_capacity(self.inner.mesh_tallies.len());
970 for t in &self.inner.mesh_tallies {
971 let d = PyDict::new(py);
972 d.set_item("number", t.number)?;
973 d.set_item("particle_type", t.particle_type)?;
974 d.set_item("detector_type", t.detector_type)?;
975 d.set_item("particle_list", t.particle_list.clone())?;
976 d.set_item("comment", t.comment.clone())?;
977 d.set_item("mesh_unknown", t.mesh_unknown)?;
978 d.set_item("ni", t.ni)?;
979 d.set_item("nj", t.nj)?;
980 d.set_item("nk", t.nk)?;
981 d.set_item("dims", t.dims().to_vec())?;
982 d.set_item("num_cells", t.num_cells())?;
983 d.set_item("cora", t.cora.clone())?;
984 d.set_item("corb", t.corb.clone())?;
985 d.set_item("corc", t.corc.clone())?;
986 for (key, card) in [
987 ("d", &t.d),
988 ("u", &t.u),
989 ("s", &t.s),
990 ("m", &t.m),
991 ("c", &t.c),
992 ("e", &t.e),
993 ("t", &t.t),
994 ] {
995 d.set_item(key, mctal_card_dict(py, card)?)?;
996 }
997 let vals: Vec<(f64, f64)> = t.vals.clone();
998 d.set_item("vals", vals)?;
999 d.set_item("total", t.total_val())?;
1000 out.push(d.into_any().unbind());
1001 }
1002 Ok(out)
1003 }
1004 fn tally_vals_array<'py>(
1012 &self,
1013 py: Python<'py>,
1014 number: u32,
1015 ) -> PyResult<Bound<'py, PyArray2<f64>>> {
1016 let tally = self
1017 .inner
1018 .tallies
1019 .iter()
1020 .find(|t| t.number == number)
1021 .ok_or_else(|| {
1022 PyValueError::new_err(format!("mctal has no parsed body for tally {number}"))
1023 })?;
1024 let mut flat = Vec::with_capacity(tally.vals.len() * 2);
1025 for (v, e) in &tally.vals {
1026 flat.push(*v);
1027 flat.push(*e);
1028 }
1029 let n = tally.vals.len();
1030 m_err(
1031 flat.into_pyarray(py)
1032 .reshape((n, 2))
1033 .map_err(|e| e.to_string()),
1034 )
1035 }
1036 fn mesh_tally_vals_array<'py>(
1042 &self,
1043 py: Python<'py>,
1044 number: u32,
1045 ) -> PyResult<Bound<'py, PyArray2<f64>>> {
1046 let tally = self
1047 .inner
1048 .mesh_tallies
1049 .iter()
1050 .find(|t| t.number == number)
1051 .ok_or_else(|| {
1052 PyValueError::new_err(format!("mctal has no parsed mesh body for tally {number}"))
1053 })?;
1054 let mut flat = Vec::with_capacity(tally.vals.len() * 2);
1055 for (v, e) in &tally.vals {
1056 flat.push(*v);
1057 flat.push(*e);
1058 }
1059 let n = tally.vals.len();
1060 m_err(
1061 flat.into_pyarray(py)
1062 .reshape((n, 2))
1063 .map_err(|e| e.to_string()),
1064 )
1065 }
1066}
1067
1068#[pyfunction]
1071fn read_mctal(path: &str) -> PyResult<PyMctal> {
1072 m_err(nucleide_mcnp_io::mctal::Mctal::from_file(path).map(|inner| PyMctal { inner }))
1073}
1074
1075#[pyclass(name = "SurfSrc")]
1077struct PySurfSrc {
1078 inner: nucleide_mcnp_io::surfsrc::SurfSrc,
1079}
1080
1081#[pymethods]
1082impl PySurfSrc {
1083 #[getter]
1084 fn kod(&self) -> String {
1085 self.inner.header.kod.trim_end().to_string()
1086 }
1087 #[getter]
1088 fn ver(&self) -> String {
1089 self.inner.header.ver.trim_end().to_string()
1090 }
1091 #[getter]
1092 fn np1(&self) -> i64 {
1093 self.inner.header.np1
1094 }
1095 #[getter]
1097 fn orignp1(&self) -> i64 {
1098 self.inner.header.orignp1
1099 }
1100 #[getter]
1101 fn nrss(&self) -> i64 {
1102 self.inner.header.nrss
1103 }
1104 #[getter]
1105 fn ncrd(&self) -> i32 {
1106 self.inner.header.ncrd
1107 }
1108 #[getter]
1109 fn njsw(&self) -> i32 {
1110 self.inner.header.njsw
1111 }
1112 #[getter]
1113 fn niss(&self) -> i64 {
1114 self.inner.header.niss
1115 }
1116 fn print_header(&self) -> String {
1118 self.inner.header.print_header()
1119 }
1120 fn tracks(&self) -> PyResult<Vec<BTreeMap<String, f64>>> {
1122 let tracks = self
1123 .inner
1124 .read_tracklist()
1125 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1126 Ok(tracks
1127 .iter()
1128 .map(|t| {
1129 let mut d = BTreeMap::new();
1130 d.insert("nps".into(), t.nps);
1131 d.insert("bitarray".into(), t.bitarray);
1132 d.insert("wgt".into(), t.wgt);
1133 d.insert("erg".into(), t.erg);
1134 d.insert("tme".into(), t.tme);
1135 d.insert("x".into(), t.x);
1136 d.insert("y".into(), t.y);
1137 d.insert("z".into(), t.z);
1138 d.insert("u".into(), t.u);
1139 d.insert("v".into(), t.v);
1140 d.insert("cs".into(), t.cs);
1141 d.insert("w".into(), t.w);
1142 d
1143 })
1144 .collect())
1145 }
1146}
1147
1148#[pyfunction]
1150fn read_ssw(path: &str) -> PyResult<PySurfSrc> {
1151 nucleide_mcnp_io::surfsrc::SurfSrc::open(path)
1152 .map(|inner| PySurfSrc { inner })
1153 .map_err(|e| PyValueError::new_err(e.to_string()))
1154}
1155
1156#[pyclass(name = "PtracFile")]
1158struct PyPtracFile {
1159 inner: nucleide_mcnp_io::ptrac::PtracFile,
1160}
1161
1162#[pymethods]
1163impl PyPtracFile {
1164 #[getter]
1165 fn problem_title(&self) -> &str {
1166 &self.inner.problem_title
1167 }
1168 #[getter]
1170 fn width_code(&self) -> u8 {
1171 match self.inner.format {
1172 nucleide_mcnp_io::ptrac::Format::I4LittleEndian => 0,
1173 nucleide_mcnp_io::ptrac::Format::I8LittleEndian => 1,
1174 }
1175 }
1176 #[getter]
1178 fn variable_nums(&self) -> BTreeMap<String, usize> {
1179 let v = &self.inner.variable_nums;
1180 let mut m = BTreeMap::new();
1181 m.insert("nps".into(), v.nps);
1182 m.insert("src".into(), v.src);
1183 m.insert("bnk".into(), v.bnk);
1184 m.insert("sur".into(), v.sur);
1185 m.insert("col".into(), v.col);
1186 m.insert("ter".into(), v.ter);
1187 m
1188 }
1189 fn events(&self) -> PyResult<Vec<BTreeMap<String, f64>>> {
1191 let events = self
1192 .inner
1193 .events()
1194 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1195 Ok(events
1196 .iter()
1197 .map(|ev| {
1198 let mut d = BTreeMap::new();
1199 d.insert("event_type".to_string(), ev.event_type as f64);
1200 for (n, v) in ev.iter() {
1201 d.insert(n.to_string(), v);
1202 }
1203 d
1204 })
1205 .collect())
1206 }
1207 fn events_array<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyArray2<f64>>> {
1218 let events = self
1219 .inner
1220 .events()
1221 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1222 let n = events.len();
1223 let mut flat = Vec::with_capacity(n * PTRAC_EVENT_COLUMNS.len());
1224 for ev in &events {
1225 flat.push(ev.event_type as f64);
1226 for col in &PTRAC_EVENT_COLUMNS[1..] {
1227 flat.push(ev.get(col).unwrap_or(0.0));
1228 }
1229 }
1230 m_err(
1231 flat.into_pyarray(py)
1232 .reshape((n, PTRAC_EVENT_COLUMNS.len()))
1233 .map_err(|e| e.to_string()),
1234 )
1235 }
1236 fn event_field_array<'py>(
1244 &self,
1245 py: Python<'py>,
1246 field: &str,
1247 ) -> PyResult<Bound<'py, PyArray1<f64>>> {
1248 if !PTRAC_EVENT_COLUMNS.contains(&field) {
1249 return Err(PyValueError::new_err(format!(
1250 "unknown PTRAC field `{field}` (expected one of {})",
1251 PTRAC_EVENT_COLUMNS.join(", ")
1252 )));
1253 }
1254 let events = self
1255 .inner
1256 .events()
1257 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1258 let col: Vec<f64> = events
1259 .iter()
1260 .map(|ev| {
1261 if field == "event_type" {
1262 ev.event_type as f64
1263 } else {
1264 ev.get(field).unwrap_or(0.0)
1265 }
1266 })
1267 .collect();
1268 Ok(col.into_pyarray(py))
1269 }
1270}
1271
1272const PTRAC_EVENT_COLUMNS: [&str; 19] = [
1278 "event_type",
1279 "node",
1280 "nsr",
1281 "nsf",
1282 "nxs",
1283 "ntyn",
1284 "ipt",
1285 "ncl",
1286 "mat",
1287 "ncp",
1288 "xxx",
1289 "yyy",
1290 "zzz",
1291 "uuu",
1292 "vvv",
1293 "www",
1294 "erg",
1295 "wgt",
1296 "tme",
1297];
1298
1299#[pyfunction]
1301fn read_ptrac(path: &str) -> PyResult<PyPtracFile> {
1302 nucleide_mcnp_io::ptrac::PtracFile::open(path)
1303 .map(|inner| PyPtracFile { inner })
1304 .map_err(|e| PyValueError::new_err(e.to_string()))
1305}
1306
1307fn mcpl_particle_to_dict(py: Python<'_>, p: &nucleide_mcpl_io::Particle) -> PyResult<Py<PyAny>> {
1310 use pyo3::types::PyDict;
1311 let d = PyDict::new(py);
1312 d.set_item("ekin", p.ekin)?;
1313 d.set_item("polarisation", p.polarisation.to_vec())?;
1314 d.set_item("position", p.position.to_vec())?;
1315 d.set_item("direction", p.direction.to_vec())?;
1316 d.set_item("time", p.time)?;
1317 d.set_item("weight", p.weight)?;
1318 d.set_item("pdgcode", p.pdgcode)?;
1319 d.set_item("userflags", p.userflags)?;
1320 Ok(d.into_any().unbind())
1321}
1322
1323fn mcpl_particle_from_dict(d: &Bound<'_, PyAny>) -> PyResult<nucleide_mcpl_io::Particle> {
1324 let get_f64 = |key: &str| -> PyResult<f64> {
1325 d.get_item(key)
1326 .map_err(|e| PyValueError::new_err(format!("particle missing `{key}`: {e}")))?
1327 .extract()
1328 .map_err(|_| PyValueError::new_err(format!("particle `{key}` must be a float")))
1329 };
1330 let get_vec3 = |key: &str| -> PyResult<[f64; 3]> {
1331 let v: Vec<f64> = d
1332 .get_item(key)
1333 .map_err(|e| PyValueError::new_err(format!("particle missing `{key}`: {e}")))?
1334 .extract()
1335 .map_err(|_| PyValueError::new_err(format!("particle `{key}` must be a 3-list")))?;
1336 if v.len() != 3 {
1337 return Err(PyValueError::new_err(format!(
1338 "particle `{key}` must have exactly 3 entries"
1339 )));
1340 }
1341 Ok([v[0], v[1], v[2]])
1342 };
1343 let pdgcode: i32 = d
1344 .get_item("pdgcode")
1345 .map_err(|e| PyValueError::new_err(format!("particle missing `pdgcode`: {e}")))?
1346 .extract()
1347 .map_err(|_| PyValueError::new_err("particle `pdgcode` must be an int"))?;
1348 let userflags: u32 = d
1349 .get_item("userflags")
1350 .map_err(|e| PyValueError::new_err(format!("particle missing `userflags`: {e}")))?
1351 .extract()
1352 .map_err(|_| PyValueError::new_err("particle `userflags` must be an int"))?;
1353 Ok(nucleide_mcpl_io::Particle {
1354 ekin: get_f64("ekin")?,
1355 polarisation: get_vec3("polarisation")?,
1356 position: get_vec3("position")?,
1357 direction: get_vec3("direction")?,
1358 time: get_f64("time")?,
1359 weight: get_f64("weight")?,
1360 pdgcode,
1361 userflags,
1362 })
1363}
1364
1365#[pyclass(name = "McplFile")]
1367struct PyMcplFile {
1368 inner: nucleide_mcpl_io::McplFile,
1369}
1370
1371#[pymethods]
1372impl PyMcplFile {
1373 #[getter]
1375 fn version(&self) -> u16 {
1376 self.inner.header.version
1377 }
1378 #[getter]
1380 fn nparticles(&self) -> u64 {
1381 self.inner.header.nparticles
1382 }
1383 #[getter]
1385 fn srcname(&self) -> &str {
1386 &self.inner.header.srcname
1387 }
1388 #[getter]
1390 fn comments(&self) -> Vec<String> {
1391 self.inner.header.comments.clone()
1392 }
1393 #[getter]
1395 fn has_userflags(&self) -> bool {
1396 self.inner.header.has_userflags
1397 }
1398 #[getter]
1400 fn has_polarisation(&self) -> bool {
1401 self.inner.header.has_polarisation
1402 }
1403 #[getter]
1405 fn double_prec(&self) -> bool {
1406 self.inner.header.double_prec
1407 }
1408 #[getter]
1410 fn universal_pdgcode(&self) -> Option<i32> {
1411 self.inner.header.universal_pdgcode
1412 }
1413 #[getter]
1415 fn universal_weight(&self) -> Option<f64> {
1416 self.inner.header.universal_weight
1417 }
1418 #[getter]
1420 fn blobs(&self) -> Vec<(String, Vec<u8>)> {
1421 self.inner
1422 .header
1423 .blobs
1424 .iter()
1425 .map(|b| (b.key.clone(), b.data.clone()))
1426 .collect()
1427 }
1428 fn particles(&self, py: Python<'_>) -> PyResult<Vec<Py<PyAny>>> {
1431 let ps = self
1432 .inner
1433 .particles()
1434 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1435 ps.iter().map(|p| mcpl_particle_to_dict(py, p)).collect()
1436 }
1437}
1438
1439#[pyfunction]
1441fn read_mcpl(path: &str) -> PyResult<PyMcplFile> {
1442 nucleide_mcpl_io::McplFile::open(path)
1443 .map(|inner| PyMcplFile { inner })
1444 .map_err(|e| PyValueError::new_err(e.to_string()))
1445}
1446
1447#[pyfunction]
1457fn write_mcpl(
1458 path: &str,
1459 header: &Bound<'_, PyAny>,
1460 particles: Vec<Bound<'_, PyAny>>,
1461) -> PyResult<()> {
1462 use nucleide_mcpl_io::{Blob, Header};
1463 let get = |key: &str| header.get_item(key);
1464 let srcname: String = get("srcname")
1465 .map_err(|_| PyValueError::new_err("header missing `srcname`"))?
1466 .extract()
1467 .map_err(|_| PyValueError::new_err("header `srcname` must be a str"))?;
1468 let comments: Vec<String> = get("comments")
1469 .map_err(|_| PyValueError::new_err("header missing `comments`"))?
1470 .extract()
1471 .map_err(|_| PyValueError::new_err("header `comments` must be a list of str"))?;
1472 let flag = |key: &str| -> PyResult<bool> {
1473 get(key)
1474 .map_err(|_| PyValueError::new_err(format!("header missing `{key}`")))?
1475 .extract()
1476 .map_err(|_| PyValueError::new_err(format!("header `{key}` must be a bool")))
1477 };
1478 let universal_pdgcode: Option<i32> = get("universal_pdgcode")
1479 .map_err(|_| PyValueError::new_err("header missing `universal_pdgcode`"))?
1480 .extract()
1481 .map_err(|_| PyValueError::new_err("header `universal_pdgcode` must be an int or None"))?;
1482 let universal_weight: Option<f64> = get("universal_weight")
1483 .map_err(|_| PyValueError::new_err("header missing `universal_weight`"))?
1484 .extract()
1485 .map_err(|_| PyValueError::new_err("header `universal_weight` must be a float or None"))?;
1486 let blob_pairs: Vec<(String, Vec<u8>)> = get("blobs")
1487 .map_err(|_| PyValueError::new_err("header missing `blobs`"))?
1488 .extract()
1489 .map_err(|_| {
1490 PyValueError::new_err("header `blobs` must be a list of (key, bytes) pairs")
1491 })?;
1492 let h = Header {
1493 has_userflags: flag("has_userflags")?,
1494 has_polarisation: flag("has_polarisation")?,
1495 double_prec: flag("double_prec")?,
1496 universal_pdgcode,
1497 universal_weight,
1498 srcname,
1499 comments,
1500 blobs: blob_pairs
1501 .into_iter()
1502 .map(|(key, data)| Blob { key, data })
1503 .collect(),
1504 ..Header::default()
1505 };
1506 let ps: Vec<nucleide_mcpl_io::Particle> = particles
1507 .iter()
1508 .map(mcpl_particle_from_dict)
1509 .collect::<PyResult<_>>()?;
1510 nucleide_mcpl_io::write_to_path(path, &h, &ps).map_err(|e| PyValueError::new_err(e.to_string()))
1511}
1512
1513#[pyfunction]
1528#[pyo3(signature = (ssw_path, mcpl_path, surfs, kinds, options=None))]
1529fn ssw2mcpl(
1530 ssw_path: &str,
1531 mcpl_path: &str,
1532 surfs: Vec<u32>,
1533 kinds: Vec<String>,
1534 options: Option<Bound<'_, PyAny>>,
1535) -> PyResult<u64> {
1536 use nucleide_mcnp_io::surfsrc::SurfSrc;
1537 use nucleide_mcpl_io::ssw::{SswParticleKind, SswTrack};
1538 let ssw = SurfSrc::open(ssw_path).map_err(|e| PyValueError::new_err(e.to_string()))?;
1539 let raw = ssw
1540 .read_tracklist()
1541 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1542 if raw.len() != surfs.len() || raw.len() != kinds.len() {
1543 return Err(PyValueError::new_err(format!(
1544 "ssw2mcpl: SSW holds {} tracks but got {} surfs and {} kinds \
1545 (one surf+kind per track required)",
1546 raw.len(),
1547 surfs.len(),
1548 kinds.len()
1549 )));
1550 }
1551 let mut tracks = Vec::with_capacity(raw.len());
1552 for (i, (t, surf, kind)) in raw
1553 .iter()
1554 .zip(surfs)
1555 .zip(kinds.iter())
1556 .map(|((t, s), k)| (t, s, k))
1557 .enumerate()
1558 {
1559 let kind = SswParticleKind::parse(kind).ok_or_else(|| {
1560 PyValueError::new_err(format!(
1561 "track {i} kind `{kind}` unknown (expected one of \
1562 \"neutron\", \"gamma\", \"electron\", \"positron\", \"proton\")"
1563 ))
1564 })?;
1565 tracks.push(SswTrack {
1566 ekin: t.erg,
1567 time_shakes: t.tme,
1568 position: [t.x, t.y, t.z],
1569 direction: [t.u, t.v, t.cs],
1570 weight: t.wgt,
1571 surf,
1572 kind,
1573 });
1574 }
1575 let mut opts = parse_ssw2mcpl_options(options.as_ref())?;
1576 if mcpl_path.ends_with(".gz") {
1577 opts.gzip = true;
1578 }
1579 let bytes = nucleide_mcpl_io::ssw::ssw2mcpl_bytes(&tracks, &opts)
1580 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1581 std::fs::write(mcpl_path, bytes).map_err(|e| PyValueError::new_err(e.to_string()))?;
1582 Ok(tracks.len() as u64)
1583}
1584
1585fn parse_ssw2mcpl_options(
1587 options: Option<&Bound<'_, PyAny>>,
1588) -> PyResult<nucleide_mcpl_io::ssw::Ssw2McplOptions> {
1589 use nucleide_mcpl_io::ssw::{DeckBlob, Ssw2McplOptions};
1590 let mut opts = Ssw2McplOptions::default();
1591 let Some(d) = options else {
1592 return Ok(opts);
1593 };
1594 if !d.is_instance_of::<pyo3::types::PyDict>() {
1595 return Err(PyValueError::new_err("options must be a dict or None"));
1596 }
1597 let flag = |key: &str| -> PyResult<Option<bool>> {
1598 match d.get_item(key) {
1599 Ok(v) => v
1600 .extract()
1601 .map(Some)
1602 .map_err(|_| PyValueError::new_err(format!("options `{key}` must be a bool"))),
1603 Err(_) => Ok(None),
1604 }
1605 };
1606 if let Some(v) = flag("double_prec")? {
1607 opts.double_prec = v;
1608 }
1609 if let Some(v) = flag("surf_to_userflags")? {
1610 opts.surf_to_userflags = v;
1611 }
1612 if let Some(v) = flag("gzip")? {
1613 opts.gzip = v;
1614 }
1615 if let Some(v) = flag("universal_pdg")? {
1616 opts.universal_pdg = v;
1617 }
1618 if let Some(v) = flag("universal_weight")? {
1619 opts.universal_weight = v;
1620 }
1621 if let Ok(v) = d.get_item("polarisation") {
1622 if v.is_none() {
1623 opts.polarisation = None;
1624 } else {
1625 let vec: Vec<f64> = v.extract().map_err(|_| {
1626 PyValueError::new_err("options `polarisation` must be a 3-list or None")
1627 })?;
1628 if vec.len() != 3 {
1629 return Err(PyValueError::new_err(
1630 "options `polarisation` must have exactly 3 entries",
1631 ));
1632 }
1633 opts.polarisation = Some([vec[0], vec[1], vec[2]]);
1634 }
1635 }
1636 if let Ok(v) = d.get_item("srcname") {
1637 opts.srcname = v
1638 .extract()
1639 .map_err(|_| PyValueError::new_err("options `srcname` must be a str"))?;
1640 }
1641 if let Ok(v) = d.get_item("comments") {
1642 opts.comments = v
1643 .extract()
1644 .map_err(|_| PyValueError::new_err("options `comments` must be a list of str"))?;
1645 }
1646 if let Ok(v) = d.get_item("deck_blob") {
1647 if !v.is_none() {
1648 let (key, data): (String, Vec<u8>) = v.extract().map_err(|_| {
1649 PyValueError::new_err("options `deck_blob` must be a (key, bytes) pair or None")
1650 })?;
1651 opts.deck_blob = Some(DeckBlob { key, data });
1652 }
1653 }
1654 Ok(opts)
1655}
1656
1657#[pyfunction]
1671#[pyo3(signature = (mcpl_path, reference_ssw_path, ssw_out_path, surface=None, force_cs_to_one=false, niss=None, allow_polarisation=false))]
1672fn mcpl2ssw(
1673 mcpl_path: &str,
1674 reference_ssw_path: &str,
1675 ssw_out_path: &str,
1676 surface: Option<u32>,
1677 force_cs_to_one: bool,
1678 niss: Option<i64>,
1679 allow_polarisation: bool,
1680) -> PyResult<u64> {
1681 use nucleide_mcnp_io::surfsrc::SurfSrc;
1682 use nucleide_mcpl_io::ssw::Mcpl2SswOptions;
1683 let mcpl = nucleide_mcpl_io::McplFile::open(mcpl_path)
1684 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1685 let particles = mcpl
1686 .particles()
1687 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1688 let reference =
1689 SurfSrc::open(reference_ssw_path).map_err(|e| PyValueError::new_err(e.to_string()))?;
1690 let (header, tracks) = nucleide_mcpl_io::ssw::mcpl2ssw(
1691 &particles,
1692 &reference.header,
1693 &Mcpl2SswOptions {
1694 surface,
1695 force_cs_to_one,
1696 niss_override: niss,
1697 allow_polarisation,
1698 },
1699 )
1700 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1701 nucleide_mcnp_io::surfsrc::write_to_path(ssw_out_path, &header, &tracks)
1702 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1703 Ok(tracks.len() as u64)
1704}
1705
1706#[pyfunction]
1716fn merge_mcpl(paths: Vec<String>, out_path: &str) -> PyResult<u64> {
1717 let mut files = Vec::with_capacity(paths.len());
1718 for (i, p) in paths.iter().enumerate() {
1719 files.push(
1720 nucleide_mcpl_io::McplFile::open(p)
1721 .map_err(|e| PyValueError::new_err(format!("merge_mcpl: input {i} {p}: {e}")))?,
1722 );
1723 }
1724 let (header, particles) =
1725 nucleide_mcpl_io::merge_mcpl(&files).map_err(|e| PyValueError::new_err(e.to_string()))?;
1726 nucleide_mcpl_io::write_to_path(out_path, &header, &particles)
1727 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1728 Ok(particles.len() as u64)
1729}
1730
1731struct ParsedExtract {
1735 spec: nucleide_mcpl_io::ExtractSpec,
1736 pending: Option<std::rc::Rc<std::cell::RefCell<Option<PyErr>>>>,
1737}
1738
1739fn parse_extract_spec(
1741 options: Option<&Bound<'_, PyAny>>,
1742 nparticles: usize,
1743) -> PyResult<ParsedExtract> {
1744 let Some(d) = options else {
1745 return Ok(ParsedExtract {
1746 spec: nucleide_mcpl_io::ExtractSpec::Range(0..nparticles),
1747 pending: None,
1748 });
1749 };
1750 if !d.is_instance_of::<pyo3::types::PyDict>() {
1751 return Err(PyValueError::new_err("options must be a dict or None"));
1752 }
1753 let opt_usize = |key: &str| -> PyResult<Option<usize>> {
1754 match d.get_item(key) {
1755 Err(_) => Ok(None),
1756 Ok(v) if v.is_none() => Ok(None),
1757 Ok(v) => v.extract::<usize>().map(Some).map_err(|_| {
1758 PyValueError::new_err(format!("options `{key}` must be a non-negative int"))
1759 }),
1760 }
1761 };
1762 let start = opt_usize("start")?;
1763 let stop = opt_usize("stop")?;
1764 if let Ok(cb) = d.get_item("predicate") {
1765 if !cb.is_none() {
1766 if start.is_some() || stop.is_some() {
1767 return Err(PyValueError::new_err(
1768 "options `start`/`stop` and `predicate` cannot be combined",
1769 ));
1770 }
1771 if !cb.is_callable() {
1772 return Err(PyValueError::new_err(
1773 "options `predicate` must be callable",
1774 ));
1775 }
1776 let cb = cb.unbind();
1777 let pending: std::rc::Rc<std::cell::RefCell<Option<PyErr>>> =
1778 std::rc::Rc::new(std::cell::RefCell::new(None));
1779 let pending_inner = std::rc::Rc::clone(&pending);
1780 let spec = nucleide_mcpl_io::ExtractSpec::Predicate(Box::new(
1781 move |p: &nucleide_mcpl_io::Particle| -> bool {
1782 if pending_inner.borrow().is_some() {
1783 return false;
1784 }
1785 Python::attach(|py| {
1786 let dict = match mcpl_particle_to_dict(py, p) {
1787 Ok(d) => d,
1788 Err(e) => {
1789 *pending_inner.borrow_mut() = Some(e);
1790 return false;
1791 }
1792 };
1793 match cb.call1(py, (dict,)) {
1794 Ok(v) => match v.is_truthy(py) {
1795 Ok(t) => t,
1796 Err(e) => {
1797 *pending_inner.borrow_mut() = Some(e);
1798 false
1799 }
1800 },
1801 Err(e) => {
1802 *pending_inner.borrow_mut() = Some(e);
1803 false
1804 }
1805 }
1806 })
1807 },
1808 ));
1809 return Ok(ParsedExtract {
1810 spec,
1811 pending: Some(pending),
1812 });
1813 }
1814 }
1815 Ok(ParsedExtract {
1816 spec: nucleide_mcpl_io::ExtractSpec::Range(start.unwrap_or(0)..stop.unwrap_or(nparticles)),
1817 pending: None,
1818 })
1819}
1820
1821#[pyfunction]
1832#[pyo3(signature = (src_path, out_path, options=None))]
1833fn extract_mcpl(
1834 src_path: &str,
1835 out_path: &str,
1836 options: Option<Bound<'_, PyAny>>,
1837) -> PyResult<u64> {
1838 let file = nucleide_mcpl_io::McplFile::open(src_path)
1839 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1840 let nparticles = file.header.nparticles as usize;
1841 let parsed = parse_extract_spec(options.as_ref(), nparticles)?;
1842 let (header, particles) = nucleide_mcpl_io::extract_mcpl(&file, &parsed.spec)
1843 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1844 if let Some(err) = parsed.pending.and_then(|p| p.borrow_mut().take()) {
1846 return Err(err);
1847 }
1848 nucleide_mcpl_io::write_to_path(out_path, &header, &particles)
1849 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1850 Ok(particles.len() as u64)
1851}
1852
1853#[pyfunction]
1860fn mcpl_stats(py: Python<'_>, path: &str) -> PyResult<Py<PyAny>> {
1861 use pyo3::types::PyDict;
1862 let file =
1863 nucleide_mcpl_io::McplFile::open(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
1864 let s =
1865 nucleide_mcpl_io::mcpl_stats(&file).map_err(|e| PyValueError::new_err(e.to_string()))?;
1866 let d = PyDict::new(py);
1867 d.set_item("nparticles", s.nparticles)?;
1868 d.set_item("ekin_sum", s.ekin_sum)?;
1869 d.set_item("ekin_min", s.ekin_min)?;
1870 d.set_item("ekin_max", s.ekin_max)?;
1871 d.set_item("ekin_mean", s.ekin_mean)?;
1872 d.set_item("weight_sum", s.weight_sum)?;
1873 d.set_item("pdg_counts", s.pdg_counts)?;
1874 Ok(d.into_any().unbind())
1875}
1876
1877#[pyfunction]
1885fn repair_mcpl(path: &str) -> PyResult<u64> {
1886 let file =
1887 nucleide_mcpl_io::McplFile::open(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
1888 let repaired = nucleide_mcpl_io::repair_mcpl(&file);
1889 let n = nucleide_mcpl_io::McplFile::from_bytes(repaired.clone())
1890 .map_err(|e| PyValueError::new_err(e.to_string()))?
1891 .header
1892 .nparticles;
1893 nucleide_mcpl_io::write_bytes_to_path(path, &repaired)
1894 .map_err(|e| PyValueError::new_err(e.to_string()))?;
1895 Ok(n)
1896}
1897
1898#[pyclass(name = "EndlLibrary")]
1900struct PyEndlLibrary {
1901 inner: nucleide_mcnp_io::endl::Library,
1902}
1903
1904#[pymethods]
1905impl PyEndlLibrary {
1906 fn nuclides(&self) -> Vec<i64> {
1908 self.inner.nuclides()
1909 }
1910 #[pyo3(signature = (nuc, p_in, rdesc, rprop, x1=None, p_out=None))]
1917 fn get_rx(
1918 &self,
1919 nuc: &Bound<'_, PyAny>,
1920 p_in: i32,
1921 rdesc: i32,
1922 rprop: i32,
1923 x1: Option<i32>,
1924 p_out: Option<i32>,
1925 ) -> PyResult<Vec<Vec<f64>>> {
1926 let id = if let Ok(n) = nuc.extract::<i64>() {
1927 n
1928 } else if let Ok(name) = nuc.extract::<&str>() {
1929 NuclideId::from_name(name).map_err(wrap_nucid_err)?.nucid() as i64
1930 } else {
1931 return Err(PyTypeError::new_err("expected int nucleus id or str name"));
1932 };
1933 self.inner
1934 .get_rx(id, p_in, rdesc, rprop, x1, p_out)
1935 .map(|rows| rows.to_vec())
1936 .map_err(|e| PyValueError::new_err(e.to_string()))
1937 }
1938}
1939
1940#[pyfunction]
1942fn read_endl(path: &str) -> PyResult<PyEndlLibrary> {
1943 nucleide_mcnp_io::endl::Library::open(path)
1944 .map(|inner| PyEndlLibrary { inner })
1945 .map_err(|e| PyValueError::new_err(e.to_string()))
1946}
1947
1948#[pyfunction]
1950fn endl_endftod(field: &str) -> f64 {
1951 nucleide_mcnp_io::endl::endftod(field)
1952}
1953
1954#[pyfunction]
1962fn combine_ssw_files(output: &str, inputs: Vec<String>) -> PyResult<()> {
1963 nucleide_mcnp_io::surfsrc::combine_files(&inputs, output)
1964 .map_err(|e| PyValueError::new_err(e.to_string()))
1965}
1966
1967#[pyclass(name = "Chain")]
1973struct PyChain {
1974 inner: std::sync::Arc<nucleide_depletion::Chain>,
1975}
1976
1977#[pymethods]
1978impl PyChain {
1979 #[getter]
1981 fn nuclides(&self) -> Vec<String> {
1982 self.inner.nuclides.iter().map(|n| n.name.clone()).collect()
1983 }
1984
1985 fn index_of(&self, name: &str) -> Option<usize> {
1986 self.inner.index_of(name)
1987 }
1988}
1989
1990#[pyfunction]
1992fn read_chain(path: &str) -> PyResult<PyChain> {
1993 nucleide_depletion::Chain::from_file(path)
1994 .map(|inner| PyChain {
1995 inner: std::sync::Arc::new(inner),
1996 })
1997 .map_err(|e| PyValueError::new_err(e.to_string()))
1998}
1999
2000type RateMap = BTreeMap<String, f64>;
2002
2003#[pyclass(name = "DepletionSystem")]
2005struct PyDepletionSystem {
2006 inner: std::sync::Arc<nucleide_depletion::DepletionSystem>,
2007}
2008
2009#[pymethods]
2010impl PyDepletionSystem {
2011 #[pyo3(signature = (n0, dt, order=48, method="cram48"))]
2020 fn solve(
2021 &self,
2022 n0: BTreeMap<String, f64>,
2023 dt: f64,
2024 order: u8,
2025 method: &str,
2026 ) -> PyResult<BTreeMap<String, f64>> {
2027 let method = resolve_method(order, method)?;
2028 nucleide_depletion::deplete_with_method(&self.inner, method, &n0, dt)
2029 .map(|r| r.atoms)
2030 .map_err(|e| PyValueError::new_err(e.to_string()))
2031 }
2032
2033 #[pyo3(signature = (n0, dt, order=48, method="cram48"))]
2039 fn solve_vec(&self, n0: Vec<f64>, dt: f64, order: u8, method: &str) -> PyResult<Vec<f64>> {
2040 let method = resolve_method(order, method)?;
2041 nucleide_depletion::solve_with_method(&self.inner, method, &n0, dt)
2042 .map_err(|e| PyValueError::new_err(e.to_string()))
2043 }
2044}
2045
2046#[pyfunction]
2048fn build_depletion_system(chain: &PyChain, rates: RateMap) -> PyResult<PyDepletionSystem> {
2049 let rs = split_rates(&rates, &chain.inner)?;
2050 nucleide_depletion::DepletionSystem::build((*chain.inner).clone(), &rs)
2051 .map(|sys| PyDepletionSystem {
2052 inner: std::sync::Arc::new(sys),
2053 })
2054 .map_err(|e| PyValueError::new_err(e.to_string()))
2055}
2056
2057fn parse_order(order: u8) -> PyResult<nucleide_depletion::Order> {
2058 match order {
2059 16 => Ok(nucleide_depletion::Order::Order16),
2060 48 => Ok(nucleide_depletion::Order::Order48),
2061 other => Err(PyValueError::new_err(format!(
2062 "unsupported CRAM order {other} (supported: 16, 48)"
2063 ))),
2064 }
2065}
2066
2067fn parse_method(name: &str) -> PyResult<nucleide_depletion::Method> {
2070 name.parse().map_err(|e: String| PyValueError::new_err(e))
2071}
2072
2073fn resolve_method(order: u8, method: &str) -> PyResult<nucleide_depletion::Method> {
2078 let parsed = parse_method(method)?;
2079 if parsed == nucleide_depletion::Method::default_cram() {
2080 parse_order(order).map(nucleide_depletion::Method::Cram)
2081 } else {
2082 Ok(parsed)
2083 }
2084}
2085
2086fn split_rates(
2087 rates: &RateMap,
2088 chain: &nucleide_depletion::Chain,
2089) -> PyResult<nucleide_depletion::ReactionRates> {
2090 let mut out = nucleide_depletion::ReactionRates::new();
2091 for (key, v) in rates {
2092 let (nuc, rx) = key.split_once(':').ok_or_else(|| {
2093 PyValueError::new_err(format!("rate key `{key}` must be `Name:reaction`"))
2094 })?;
2095 let idx = chain
2096 .index_of(nuc)
2097 .ok_or_else(|| PyValueError::new_err(format!("rate for unknown nuclide `{nuc}`")))?;
2098 out.entry(idx).or_default().insert(rx.to_string(), *v);
2099 }
2100 Ok(out)
2101}
2102
2103#[pyfunction]
2113#[pyo3(signature = (chain, n0, dt, rates=None, order=48, method="cram48"))]
2114fn deplete(
2115 chain: &PyChain,
2116 n0: BTreeMap<String, f64>,
2117 dt: f64,
2118 rates: Option<RateMap>,
2119 order: u8,
2120 method: &str,
2121) -> PyResult<BTreeMap<String, f64>> {
2122 let method = resolve_method(order, method)?;
2123 let rates = split_rates(rates.as_ref().unwrap_or(&BTreeMap::new()), &chain.inner)?;
2124 let sys = nucleide_depletion::DepletionSystem::build((*chain.inner).clone(), &rates)
2125 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2126 nucleide_depletion::deplete_with_method(&sys, method, &n0, dt)
2127 .map(|r| r.atoms)
2128 .map_err(|e| PyValueError::new_err(e.to_string()))
2129}
2130
2131#[pyfunction]
2140fn read_serpent(path: &str, kind: &str) -> PyResult<Py<PyAny>> {
2141 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
2142 let table = match kind {
2143 "res" => nucleide_serpent_io::parse_res(&text),
2144 "dep" => nucleide_serpent_io::parse_dep(&text),
2145 "det" => nucleide_serpent_io::parse_det(&text),
2146 other => {
2147 return Err(PyValueError::new_err(format!(
2148 "kind must be res|dep|det, got `{other}`"
2149 )))
2150 }
2151 }
2152 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2153 fn entry_to_py(py: Python<'_>, e: &nucleide_serpent_io::Entry) -> PyResult<Py<PyAny>> {
2154 use nucleide_serpent_io::Entry as E;
2155 let value = match e {
2156 E::Scalar(nucleide_serpent_io::Value::Num(n)) => {
2157 n.into_pyobject(py).unwrap().unbind().into_any()
2158 }
2159 E::Scalar(nucleide_serpent_io::Value::Str(s)) => {
2160 s.into_pyobject(py).unwrap().unbind().into_any()
2161 }
2162 E::Vector(vs) => vs
2163 .iter()
2164 .map(|v| match v {
2165 nucleide_serpent_io::Value::Num(n) => {
2166 n.into_pyobject(py).unwrap().unbind().into_any()
2167 }
2168 nucleide_serpent_io::Value::Str(s) => {
2169 s.into_pyobject(py).unwrap().unbind().into_any()
2170 }
2171 })
2172 .collect::<Vec<_>>()
2173 .into_pyobject(py)
2174 .unwrap()
2175 .unbind()
2176 .into_any(),
2177 E::Matrix(m) => m
2178 .to_rows_f64()
2179 .map_err(|err| PyValueError::new_err(err.to_string()))?
2180 .into_pyobject(py)
2181 .unwrap()
2182 .unbind()
2183 .into_any(),
2184 };
2185 Ok(value)
2186 }
2187 Python::attach(|py| {
2188 let dict = pyo3::types::PyDict::new(py);
2189 for (k, e) in table.iter() {
2190 dict.set_item(k, entry_to_py(py, e)?)?;
2191 }
2192 Ok(dict.into_any().unbind())
2193 })
2194}
2195
2196#[pyclass(name = "UsrbinTally")]
2198struct PyUsrbinTally {
2199 inner: nucleide_fluka_io::usrbin::UsrbinTally,
2200}
2201
2202#[pymethods]
2203impl PyUsrbinTally {
2204 #[getter]
2205 fn name(&self) -> &str {
2206 &self.inner.name
2207 }
2208 #[getter]
2209 fn particle(&self) -> &str {
2210 &self.inner.particle
2211 }
2212 #[getter]
2213 fn nx(&self) -> usize {
2214 self.inner.x_info.bins
2215 }
2216 #[getter]
2217 fn ny(&self) -> usize {
2218 self.inner.y_info.bins
2219 }
2220 #[getter]
2221 fn nz(&self) -> usize {
2222 self.inner.z_info.bins
2223 }
2224 #[getter]
2225 fn x_bounds(&self) -> Vec<f64> {
2226 self.inner.x_bounds.clone()
2227 }
2228 #[getter]
2229 fn y_bounds(&self) -> Vec<f64> {
2230 self.inner.y_bounds.clone()
2231 }
2232 #[getter]
2233 fn z_bounds(&self) -> Vec<f64> {
2234 self.inner.z_bounds.clone()
2235 }
2236 #[getter]
2238 fn data(&self) -> Vec<f64> {
2239 self.inner.part_data.clone()
2240 }
2241 #[getter]
2243 fn error(&self) -> Vec<f64> {
2244 self.inner.error_data.clone()
2245 }
2246 fn dims(&self) -> [usize; 3] {
2247 [self.nx(), self.ny(), self.nz()]
2248 }
2249}
2250
2251#[pyfunction]
2253fn read_usrbin(path: &str) -> PyResult<Vec<PyUsrbinTally>> {
2254 let tallies = nucleide_fluka_io::usrbin::read_usrbin_file(path)
2255 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2256 Ok(tallies
2257 .into_iter()
2258 .map(|inner| PyUsrbinTally { inner })
2259 .collect())
2260}
2261
2262#[pyclass(name = "MagicOutput")]
2264struct PyMagicOutput {
2265 inner: nucleide_vr_tools::magic::MagicOutput,
2266}
2267
2268#[pymethods]
2269impl PyMagicOutput {
2270 #[getter]
2272 fn lower_bounds_ww(&self) -> Vec<f64> {
2273 self.inner.lower_bounds_ww.clone()
2274 }
2275 #[getter]
2276 fn groups_per_ve(&self) -> usize {
2277 self.inner.groups_per_ve
2278 }
2279 #[getter]
2280 fn scale_factors(&self) -> Vec<f64> {
2281 self.inner.scale_factors.clone()
2282 }
2283 #[getter]
2284 fn e_upper_bounds(&self) -> Vec<f64> {
2285 self.inner.e_upper_bounds.clone()
2286 }
2287 #[getter]
2288 fn ww_tag_name(&self) -> &str {
2289 &self.inner.ww_tag_name
2290 }
2291}
2292
2293#[pyfunction]
2295#[pyo3(signature = (tally, per_group=false, tolerance=0.5))]
2296fn magic(tally: &PyMeshTally, per_group: bool, tolerance: f64) -> PyResult<PyMagicOutput> {
2297 let selection = if per_group {
2298 nucleide_vr_tools::magic::MagicSelection::PerGroup
2299 } else {
2300 nucleide_vr_tools::magic::MagicSelection::Total
2301 };
2302 let params = nucleide_vr_tools::magic::MagicParams {
2303 tolerance,
2304 ..Default::default()
2305 };
2306 nucleide_vr_tools::magic::magic_with(&tally.inner, selection, params)
2307 .map(|inner| PyMagicOutput { inner })
2308 .map_err(|e| PyValueError::new_err(e.to_string()))
2309}
2310
2311#[pyclass(name = "AliasTable")]
2313struct PyAliasTable {
2314 inner: nucleide_vr_tools::sampling::AliasTable,
2315}
2316
2317#[pymethods]
2318impl PyAliasTable {
2319 #[new]
2321 fn new(pdf: Vec<f64>) -> PyResult<Self> {
2322 nucleide_vr_tools::sampling::AliasTable::new(&pdf)
2323 .map(|inner| PyAliasTable { inner })
2324 .map_err(|e| PyValueError::new_err(e.to_string()))
2325 }
2326 fn sample(&self, r1: f64, r2: f64) -> usize {
2328 self.inner.sample(r1, r2)
2329 }
2330 #[getter]
2331 fn pdf(&self) -> Vec<f64> {
2332 self.inner.pdf().to_vec()
2333 }
2334 fn __len__(&self) -> usize {
2335 self.inner.len()
2336 }
2337}
2338
2339#[pyclass(name = "MeshSourceSampler")]
2341struct PyMeshSourceSampler {
2342 inner: nucleide_vr_tools::sampling::MeshSourceSampler,
2343}
2344
2345#[pymethods]
2346impl PyMeshSourceSampler {
2347 #[new]
2349 #[pyo3(signature = (tally, mode, user_pdf=None))]
2350 fn new(tally: &PyMeshTally, mode: &str, user_pdf: Option<Vec<f64>>) -> PyResult<Self> {
2351 let user = if matches!(mode, "user") {
2352 Some(user_pdf.ok_or_else(|| PyValueError::new_err("user mode needs user_pdf"))?)
2353 } else {
2354 None
2355 };
2356 let m = match mode {
2357 "analog" => nucleide_vr_tools::sampling::Mode::Analog,
2358 "uniform" => nucleide_vr_tools::sampling::Mode::Uniform,
2359 "user" => nucleide_vr_tools::sampling::Mode::User,
2360 other => {
2361 return Err(PyValueError::new_err(format!(
2362 "mode must be analog|uniform|user, got `{other}`"
2363 )))
2364 }
2365 };
2366 nucleide_vr_tools::sampling::MeshSourceSampler::new(&tally.inner, m, user.as_deref())
2367 .map(|inner| PyMeshSourceSampler { inner })
2368 .map_err(|e| PyValueError::new_err(e.to_string()))
2369 }
2370 fn sample(&self, r1: f64, r2: f64) -> BTreeMap<String, f64> {
2372 let s = self.inner.sample(r1, r2);
2373 let mut d = BTreeMap::new();
2374 d.insert("index".into(), s.index as f64);
2375 d.insert("i".into(), s.i as f64);
2376 d.insert("j".into(), s.j as f64);
2377 d.insert("k".into(), s.k as f64);
2378 d.insert("weight".into(), s.weight);
2379 d
2380 }
2381 fn mode(&self) -> &'static str {
2383 match self.inner.mode() {
2384 nucleide_vr_tools::sampling::Mode::Analog => "analog",
2385 nucleide_vr_tools::sampling::Mode::Uniform => "uniform",
2386 nucleide_vr_tools::sampling::Mode::User => "user",
2387 }
2388 }
2389 fn num_voxels(&self) -> usize {
2391 self.inner.num_voxels()
2392 }
2393 fn table_len(&self) -> usize {
2395 self.inner.table().len()
2396 }
2397}
2398
2399#[pyclass(name = "KdeSampler")]
2401struct PyKdeSampler {
2402 inner: nucleide_vr_tools::kde::KdeSampler,
2403}
2404
2405#[pymethods]
2406impl PyKdeSampler {
2407 #[new]
2410 #[pyo3(signature = (samples, bandwidth=None))]
2411 fn new(samples: Vec<Vec<f64>>, bandwidth: Option<&Bound<'_, PyAny>>) -> PyResult<Self> {
2412 let rule = match bandwidth {
2413 None => nucleide_vr_tools::kde::Bandwidth::Silverman,
2414 Some(b) => {
2415 if let Ok(name) = b.extract::<String>() {
2416 match name.as_str() {
2417 "silverman" => nucleide_vr_tools::kde::Bandwidth::Silverman,
2418 other => {
2419 return Err(PyValueError::new_err(format!(
2420 "bandwidth must be silverman or a width list, got `{other}`"
2421 )))
2422 }
2423 }
2424 } else {
2425 let widths = b.extract::<Vec<f64>>().map_err(|_| {
2426 PyValueError::new_err("bandwidth must be silverman or a width list")
2427 })?;
2428 nucleide_vr_tools::kde::Bandwidth::Fixed(widths)
2429 }
2430 }
2431 };
2432 nucleide_vr_tools::kde::KdeSampler::fit(&samples, rule)
2433 .map(|inner| PyKdeSampler { inner })
2434 .map_err(|e| PyValueError::new_err(e.to_string()))
2435 }
2436 fn pdf(&self, point: Vec<f64>) -> PyResult<f64> {
2438 self.inner
2439 .pdf(&point)
2440 .map_err(|e| PyValueError::new_err(e.to_string()))
2441 }
2442 fn draw(&self, u: f64, normals: Vec<f64>) -> PyResult<Vec<f64>> {
2444 self.inner
2445 .draw(u, &normals)
2446 .map_err(|e| PyValueError::new_err(e.to_string()))
2447 }
2448 fn bandwidths(&self) -> Vec<f64> {
2450 self.inner.bandwidths().to_vec()
2451 }
2452 fn n_samples(&self) -> usize {
2454 self.inner.n_samples()
2455 }
2456}
2457
2458#[pyfunction]
2461#[pyo3(signature = (ssw, path, tracks=None))]
2462fn write_ssw(
2463 ssw: &PySurfSrc,
2464 path: &str,
2465 tracks: Option<Vec<BTreeMap<String, f64>>>,
2466) -> PyResult<()> {
2467 let header = ssw.inner.header.clone();
2468 let track_data: Vec<nucleide_mcnp_io::surfsrc::TrackData> = match tracks {
2469 Some(dict_tracks) => dict_tracks
2470 .iter()
2471 .map(|d| {
2472 let g = |k: &str| d.get(k).copied().unwrap_or(0.0);
2473 let mut record = vec![0.0f64; nucleide_mcnp_io::surfsrc::TrackData::RECORD_WIDTH];
2474 record[0] = g("nps");
2475 record[1] = g("bitarray");
2476 record[2] = g("wgt");
2477 record[3] = g("erg");
2478 record[4] = g("tme");
2479 record[5] = g("x");
2480 record[6] = g("y");
2481 record[7] = g("z");
2482 record[8] = g("u");
2483 record[9] = g("v");
2484 record[10] = g("cs");
2485 nucleide_mcnp_io::surfsrc::TrackData::from_record(record)
2486 })
2487 .collect(),
2488 None => ssw
2489 .inner
2490 .read_tracklist()
2491 .map_err(|e| PyValueError::new_err(e.to_string()))?,
2492 };
2493 let mut f = std::fs::File::create(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
2494 nucleide_mcnp_io::surfsrc::write_to(&mut f, &header, &track_data)
2495 .map_err(|e| PyValueError::new_err(e.to_string()))
2496}
2497
2498#[pyfunction]
2500fn mesh_to_geom(
2501 x_bounds: Vec<f64>,
2502 y_bounds: Vec<f64>,
2503 z_bounds: Vec<f64>,
2504 cell_materials: Vec<Option<(String, f64)>>,
2505 title_card: &str,
2506) -> String {
2507 let opts = nucleide_mcnp_io::deck::DeckOptions {
2508 title_card: title_card.to_string(),
2509 frac_type: nucleide_mcnp_io::deck::FracType::Mass,
2510 };
2511 nucleide_mcnp_io::deck::mesh_to_geom(&x_bounds, &y_bounds, &z_bounds, &cell_materials, &opts)
2512}
2513
2514#[pyfunction]
2525fn alara_parse_deck(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
2526 let owned = text.to_owned();
2527 let deck = py
2528 .detach(move || nucleide_alara_io::AlaraDeck::parse(&owned))
2529 .map_err(ala_err)?;
2530 Ok(deck_to_py(py, &deck))
2531}
2532
2533fn ala_err(e: nucleide_alara_io::Error) -> PyErr {
2534 PyValueError::new_err(e.to_string())
2535}
2536
2537fn deck_to_py(py: Python<'_>, deck: &nucleide_alara_io::AlaraDeck) -> Py<PyAny> {
2538 use pyo3::types::PyDict;
2539 let out = PyDict::new(py);
2540 let block_kinds: Vec<&str> = deck.block_kinds();
2541 out.set_item("block_kinds", block_kinds).ok();
2542 out.set_item("geometry", deck.geometry.as_ref().map(|g| g.kind.clone()))
2543 .ok();
2544 let mixtures: Vec<Py<PyAny>> = deck.mixtures.iter().map(|m| mixture_to_py(py, m)).collect();
2545 out.set_item("mixtures", mixtures).ok();
2546 let fluxes: Vec<Py<PyAny>> = deck.fluxes.iter().map(|f| fluxdef_to_py(py, f)).collect();
2547 out.set_item("fluxes", fluxes).ok();
2548 out.set_item(
2549 "cooling_times_s",
2550 deck.cooling
2551 .as_ref()
2552 .map(|c| c.times_s.clone())
2553 .unwrap_or_default(),
2554 )
2555 .ok();
2556 let schedules: Vec<Py<PyAny>> = deck
2557 .schedules
2558 .iter()
2559 .map(|s| {
2560 let d = PyDict::new(py);
2561 let items: Vec<Vec<String>> = s.items.iter().map(|it| it.tokens.clone()).collect();
2562 d.set_item("name", &s.name).ok();
2563 d.set_item("items", items).ok();
2564 d.into_any().unbind()
2565 })
2566 .collect();
2567 out.set_item("schedules", schedules).ok();
2568 let histories: Vec<Py<PyAny>> = deck
2569 .pulse_histories
2570 .iter()
2571 .map(|h| {
2572 let d = PyDict::new(py);
2573 let levels: Vec<Py<PyAny>> = h
2574 .levels
2575 .iter()
2576 .map(|l| {
2577 let e = PyDict::new(py);
2578 e.set_item("pulses", l.pulses).ok();
2579 e.set_item("delay_s", l.delay_s).ok();
2580 e.into_any().unbind()
2581 })
2582 .collect();
2583 d.set_item("name", &h.name).ok();
2584 d.set_item("levels", levels).ok();
2585 d.into_any().unbind()
2586 })
2587 .collect();
2588 out.set_item("pulse_histories", histories).ok();
2589 let outputs: Vec<Py<PyAny>> = deck
2590 .outputs
2591 .iter()
2592 .map(|o| {
2593 let d = PyDict::new(py);
2594 d.set_item("resolution", &o.resolution).ok();
2595 d.set_item("entries", o.entries.clone()).ok();
2596 d.into_any().unbind()
2597 })
2598 .collect();
2599 out.set_item("outputs", outputs).ok();
2600 out.set_item("truncation", deck.truncation.as_ref().map(|t| t.tolerance))
2601 .ok();
2602 out.into_any().unbind()
2603}
2604
2605fn mixture_to_py(py: Python<'_>, mix: &nucleide_alara_io::deck::Mixture) -> Py<PyAny> {
2606 use pyo3::types::PyDict;
2607 let entries: Vec<Py<PyAny>> = mix
2608 .entries
2609 .iter()
2610 .map(|e| mixture_entry_to_py(py, e))
2611 .collect();
2612 let d = PyDict::new(py);
2613 d.set_item("name", &mix.name).ok();
2614 d.set_item("entries", entries).ok();
2615 d.into_any().unbind()
2616}
2617
2618fn mixture_entry_to_py(py: Python<'_>, entry: &nucleide_alara_io::deck::MixtureEntry) -> Py<PyAny> {
2619 use nucleide_alara_io::deck::MixtureEntry as E;
2620 use pyo3::types::PyDict;
2621 let d = PyDict::new(py);
2622 match entry {
2623 E::Material {
2624 name,
2625 rel_density,
2626 vol_fraction,
2627 } => {
2628 d.set_item("kind", "material").ok();
2629 d.set_item("name", name).ok();
2630 d.set_item("rel_density", *rel_density).ok();
2631 d.set_item("vol_fraction", *vol_fraction).ok();
2632 }
2633 E::Element {
2634 symbol,
2635 rel_density,
2636 vol_fraction,
2637 } => {
2638 d.set_item("kind", "element").ok();
2639 d.set_item("symbol", symbol).ok();
2640 d.set_item("rel_density", *rel_density).ok();
2641 d.set_item("vol_fraction", *vol_fraction).ok();
2642 }
2643 E::Like {
2644 mixture,
2645 rel_density,
2646 } => {
2647 d.set_item("kind", "like").ok();
2648 d.set_item("mixture", mixture).ok();
2649 d.set_item("rel_density", *rel_density).ok();
2650 }
2651 E::Target { target_kind, name } => {
2652 d.set_item("kind", "target").ok();
2653 d.set_item("target_kind", target_kind).ok();
2654 d.set_item("name", name).ok();
2655 }
2656 }
2657 d.into_any().unbind()
2658}
2659
2660fn fluxdef_to_py(py: Python<'_>, flux: &nucleide_alara_io::deck::FluxDef) -> Py<PyAny> {
2661 use pyo3::types::PyDict;
2662 let d = PyDict::new(py);
2663 d.set_item("name", &flux.name).ok();
2664 d.set_item("file", &flux.file).ok();
2665 d.set_item("scale", flux.scale).ok();
2666 d.set_item("skip", flux.skip).ok();
2667 d.set_item("format", &flux.format).ok();
2668 d.into_any().unbind()
2669}
2670
2671#[pyfunction]
2676fn alara_parse_flux(py: Python<'_>, text: &str, name: &str) -> PyResult<Py<PyAny>> {
2677 let owned_text = text.to_owned();
2678 let owned_name = name.to_owned();
2679 let spectra = py
2680 .detach(move || nucleide_alara_io::FluxSpectra::parse(&owned_name, &owned_text))
2681 .map_err(ala_err)?;
2682 use pyo3::types::PyDict;
2683 let d = PyDict::new(py);
2684 d.set_item("name", spectra.name.clone()).ok();
2685 d.set_item("groups_per_interval", spectra.groups_per_interval)
2686 .ok();
2687 d.set_item("num_intervals", spectra.num_intervals()).ok();
2688 let totals: Vec<f64> = spectra.intervals.iter().map(|iv| iv.iter().sum()).collect();
2689 d.set_item("totals", totals).ok();
2690 d.set_item("total", spectra.total()).ok();
2691 d.set_item("intervals", spectra.intervals.clone()).ok();
2692 Ok(d.into_any().unbind())
2693}
2694
2695#[pyfunction]
2701fn alara_parse_output(
2702 py: Python<'_>,
2703 text: &str,
2704 run_lbl: &str,
2705) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
2706 let owned_text = text.to_owned();
2707 let owned_lbl = run_lbl.to_owned();
2708 let rows = py
2709 .detach(move || {
2710 nucleide_alara_io::output::ResponseFrame::parse(&owned_text, &owned_lbl).map(|f| f.rows)
2711 })
2712 .map_err(ala_err)?;
2713 Ok(rows
2714 .iter()
2715 .map(|r| {
2716 let mut d = BTreeMap::new();
2717 d.insert(
2718 "time_s".to_string(),
2719 r.time_s.into_pyobject(py).unwrap().unbind().into_any(),
2720 );
2721 d.insert(
2722 "time_label".to_string(),
2723 r.time_label
2724 .clone()
2725 .into_pyobject(py)
2726 .unwrap()
2727 .unbind()
2728 .into_any(),
2729 );
2730 d.insert(
2731 "nuclide".to_string(),
2732 r.nuclide
2733 .clone()
2734 .into_pyobject(py)
2735 .unwrap()
2736 .unbind()
2737 .into_any(),
2738 );
2739 d.insert(
2740 "half_life_s".to_string(),
2741 r.half_life_s.into_pyobject(py).unwrap().unbind().into_any(),
2742 );
2743 d.insert(
2744 "run_lbl".to_string(),
2745 r.run_lbl
2746 .clone()
2747 .into_pyobject(py)
2748 .unwrap()
2749 .unbind()
2750 .into_any(),
2751 );
2752 d.insert(
2753 "block".to_string(),
2754 r.block
2755 .as_str()
2756 .into_pyobject(py)
2757 .unwrap()
2758 .unbind()
2759 .into_any(),
2760 );
2761 d.insert(
2762 "block_name".to_string(),
2763 r.block_name
2764 .clone()
2765 .into_pyobject(py)
2766 .unwrap()
2767 .unbind()
2768 .into_any(),
2769 );
2770 d.insert(
2771 "block_num".to_string(),
2772 r.block_num.into_pyobject(py).unwrap().unbind().into_any(),
2773 );
2774 d.insert(
2775 "variable".to_string(),
2776 r.variable
2777 .as_str()
2778 .into_pyobject(py)
2779 .unwrap()
2780 .unbind()
2781 .into_any(),
2782 );
2783 d.insert(
2784 "var_unit".to_string(),
2785 r.var_unit
2786 .clone()
2787 .into_pyobject(py)
2788 .unwrap()
2789 .unbind()
2790 .into_any(),
2791 );
2792 d.insert(
2793 "value".to_string(),
2794 r.value.into_pyobject(py).unwrap().unbind().into_any(),
2795 );
2796 d
2797 })
2798 .collect())
2799}
2800
2801#[pyfunction]
2808#[pyo3(signature = (deck_text, top=None))]
2809fn alara_expand_schedule(
2810 py: Python<'_>,
2811 deck_text: &str,
2812 top: Option<&str>,
2813) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
2814 let owned_text = deck_text.to_owned();
2815 let owned_top = top.map(str::to_owned);
2816 let steps = py
2817 .detach(move || expand_deck_schedules(&owned_text, owned_top.as_deref()))
2818 .map_err(PyValueError::new_err)?;
2819 Ok(steps
2820 .into_iter()
2821 .map(|s| {
2822 let mut d = BTreeMap::new();
2823 let cooling = s.is_cooling();
2824 d.insert(
2825 "duration_s".to_string(),
2826 s.duration_s.into_pyobject(py).unwrap().unbind().into_any(),
2827 );
2828 d.insert(
2829 "flux".to_string(),
2830 s.flux
2831 .clone()
2832 .into_pyobject(py)
2833 .unwrap()
2834 .unbind()
2835 .into_any(),
2836 );
2837 d.insert(
2838 "is_cooling".to_string(),
2839 pyo3::types::PyBool::new(py, cooling)
2840 .to_owned()
2841 .into_any()
2842 .unbind(),
2843 );
2844 d
2845 })
2846 .collect())
2847}
2848
2849fn expand_deck_schedules(
2850 deck_text: &str,
2851 top: Option<&str>,
2852) -> Result<Vec<nucleide_alara_io::FlatStep>, String> {
2853 let deck = nucleide_alara_io::AlaraDeck::parse(deck_text).map_err(|e| e.to_string())?;
2854 let mut scheds = Vec::with_capacity(deck.schedules.len());
2855 for raw in &deck.schedules {
2856 let mut items = Vec::with_capacity(raw.items.len());
2857 for entry in &raw.items {
2858 items.push(
2859 parse_deck_sched_item(&entry.tokens)
2860 .map_err(|m| format!("schedule `{}` line {}: {m}", raw.name, entry.line))?,
2861 );
2862 }
2863 scheds.push(nucleide_alara_io::schedule::ScheduleDef {
2864 name: raw.name.clone(),
2865 items,
2866 });
2867 }
2868 let histories: Vec<nucleide_alara_io::schedule::PulseHistory> = deck
2869 .pulse_histories
2870 .iter()
2871 .map(|h| nucleide_alara_io::schedule::PulseHistory {
2872 name: h.name.clone(),
2873 levels: h
2874 .levels
2875 .iter()
2876 .map(|l| nucleide_alara_io::schedule::PulseLevel {
2877 count: l.pulses,
2878 delay_s: l.delay_s,
2879 })
2880 .collect(),
2881 })
2882 .collect();
2883 match top {
2884 Some(name) => {
2885 nucleide_alara_io::expand_from(name, &scheds, &histories).map_err(|e| e.to_string())
2886 }
2887 None => nucleide_alara_io::expand(&scheds, &histories).map_err(|e| e.to_string()),
2888 }
2889}
2890
2891fn parse_deck_sched_item(tokens: &[String]) -> Result<nucleide_alara_io::SchedItem, String> {
2892 match tokens {
2893 [op_text, op_unit, flux, history, delay_text, delay_unit] => {
2894 let op: f64 = op_text
2895 .parse()
2896 .map_err(|_| format!("expected operating time, found `{op_text}`"))?;
2897 let delay: f64 = delay_text
2898 .parse()
2899 .map_err(|_| format!("expected delay, found `{delay_text}`"))?;
2900 let op_time_s =
2901 nucleide_alara_io::parse_time_to_seconds(op, op_unit).map_err(|e| e.to_string())?;
2902 let delay_s = nucleide_alara_io::parse_time_to_seconds(delay, delay_unit)
2903 .map_err(|e| e.to_string())?;
2904 Ok(nucleide_alara_io::SchedItem::Pulse {
2905 op_time_s,
2906 flux: flux.clone(),
2907 history: history.clone(),
2908 delay_s,
2909 })
2910 }
2911 [name, history, delay_text, delay_unit] => {
2912 let delay: f64 = delay_text
2913 .parse()
2914 .map_err(|_| format!("expected delay, found `{delay_text}`"))?;
2915 let delay_s = nucleide_alara_io::parse_time_to_seconds(delay, delay_unit)
2916 .map_err(|e| e.to_string())?;
2917 Ok(nucleide_alara_io::SchedItem::SubSchedule {
2918 name: name.clone(),
2919 history: history.clone(),
2920 delay_s,
2921 })
2922 }
2923 _ => Err(format!(
2924 "expected 4- or 6-token schedule item, found {}",
2925 tokens.join(" ")
2926 )),
2927 }
2928}
2929
2930#[pyfunction]
2936fn half_life(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2937 lookup(key, nucleide_nuclei::data::half_life)
2938}
2939
2940#[pyfunction]
2942fn decay_constant(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2943 lookup(key, nucleide_nuclei::data::decay_constant)
2944}
2945
2946#[pyfunction]
2948fn q_value_capture(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2949 lookup(key, nucleide_nuclei::data::q_value_neutron_capture)
2950}
2951
2952#[pyfunction]
2954fn q_value_alpha(key: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
2955 lookup(key, nucleide_nuclei::data::q_value_alpha)
2956}
2957
2958#[pyfunction]
2962fn read_inp(path: &str) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
2963 let mats = nucleide_mcnp_io::inp::materials_from_file(path)
2964 .map_err(|e| PyValueError::new_err(e.to_string()))?;
2965 Python::attach(|py| {
2966 Ok(mats
2967 .into_iter()
2968 .map(|m| {
2969 let mut d = BTreeMap::new();
2970 d.insert(
2971 "number".to_string(),
2972 m.number.into_pyobject(py).unwrap().unbind().into_any(),
2973 );
2974 let fr: BTreeMap<String, f64> = m
2975 .fractions
2976 .iter()
2977 .map(|(id, f)| (id.to_name(), *f))
2978 .collect();
2979 d.insert(
2980 "fractions".to_string(),
2981 fr.into_pyobject(py).unwrap().unbind().into_any(),
2982 );
2983 d.insert(
2984 "fraction_type".to_string(),
2985 match m.fraction_type {
2986 nucleide_mcnp_io::inp::FracKind::Atom => "atom",
2987 nucleide_mcnp_io::inp::FracKind::Mass => "mass",
2988 }
2989 .into_pyobject(py)
2990 .unwrap()
2991 .unbind()
2992 .into_any(),
2993 );
2994 d.insert(
2995 "density".to_string(),
2996 m.density.into_pyobject(py).unwrap().unbind().into_any(),
2997 );
2998 d.insert(
2999 "comments".to_string(),
3000 m.comments
3001 .join(" ")
3002 .into_pyobject(py)
3003 .unwrap()
3004 .unbind()
3005 .into_any(),
3006 );
3007 d
3008 })
3009 .collect())
3010 })
3011}
3012
3013fn comp_to_material(comp: BTreeMap<String, f64>) -> PyResult<nucleide_material::Material> {
3014 let mut mat = nucleide_material::Material::new();
3015 for (name, grams) in &comp {
3016 let id = nucleide_nuclei::NuclideId::from_name(name)
3017 .map_err(|e| PyValueError::new_err(format!("`{name}`: {e}")))?;
3018 mat.add_nuclide(id, *grams);
3019 }
3020 Ok(mat)
3021}
3022
3023#[pyfunction]
3026fn from_formula(formula: &str) -> PyResult<BTreeMap<String, f64>> {
3027 use nucleide_material::AbundanceProvider;
3028 let parsed = nucleide_material::parse_formula(formula)
3029 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3030 let mut nat = Vec::new();
3032 for (z, count) in &parsed {
3033 if let Some(isotopes) = nucleide_material::NaturalAbundances.natural_isotopes(*z) {
3034 for (id, frac) in isotopes {
3035 nat.push((id, frac * count));
3036 }
3037 }
3038 }
3039 let total: f64 = nat.iter().map(|(_, c)| c).sum();
3040 if total <= 0.0 {
3041 return Err(PyValueError::new_err("empty formula expansion"));
3042 }
3043 let mut out: BTreeMap<String, f64> = BTreeMap::new();
3044 for (id, atoms) in nat {
3045 *out.entry(id.to_name()).or_insert(0.0) += atoms / total;
3046 }
3047 Ok(out)
3048}
3049
3050#[pyfunction]
3053fn activity(comp: BTreeMap<String, f64>) -> PyResult<BTreeMap<String, f64>> {
3054 let mat = comp_to_material(comp)?;
3055 let analytics = nucleide_material::Analytics {
3056 masses: &nucleide_material::Ame2020,
3057 decays: &nucleide_material::ChainDecays,
3058 };
3059 let per_nuc = mat
3060 .activity(&analytics)
3061 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3062 let specific = mat
3063 .specific_activity(&analytics)
3064 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3065 let mut out: BTreeMap<String, f64> = per_nuc
3066 .into_iter()
3067 .map(|(id, v)| (id.to_name(), v))
3068 .collect();
3069 out.insert("specific".to_string(), specific);
3070 Ok(out)
3071}
3072
3073#[pyfunction]
3075fn to_xml(comp: BTreeMap<String, f64>, name: &str, density: f64, units: &str) -> PyResult<String> {
3076 let mat = comp_to_material(comp)?;
3077 mat.to_xml(name, density, units)
3078 .map_err(|e| PyValueError::new_err(e.to_string()))
3079}
3080
3081#[pyclass(name = "Cascade")]
3083struct PyCascade {
3084 inner: std::sync::Mutex<nucleide_enrichment::Cascade>,
3085}
3086
3087#[pymethods]
3088impl PyCascade {
3089 #[staticmethod]
3091 fn default_uranium() -> Self {
3092 Self {
3093 inner: std::sync::Mutex::new(nucleide_enrichment::default_uranium_cascade()),
3094 }
3095 }
3096
3097 #[new]
3100 #[allow(non_snake_case)]
3101 #[allow(clippy::too_many_arguments)]
3102 fn new(
3103 alpha: f64,
3104 Mstar: f64,
3105 j: u32,
3106 k: u32,
3107 N: f64,
3108 M: f64,
3109 x_feed_j: f64,
3110 x_prod_j: f64,
3111 x_tail_j: f64,
3112 mat_feed: BTreeMap<String, f64>,
3113 ) -> PyResult<Self> {
3114 let mut feed = BTreeMap::new();
3115 for (name, frac) in mat_feed {
3116 let id = NuclideId::from_name(&name).map_err(wrap_nucid_err)?;
3117 feed.insert(id, frac);
3118 }
3119 let casc = nucleide_enrichment::Cascade {
3120 alpha,
3121 Mstar,
3122 j: NuclideId::from_nucid(j),
3123 k: NuclideId::from_nucid(k),
3124 N,
3125 M,
3126 x_feed_j,
3127 x_prod_j,
3128 x_tail_j,
3129 mat_feed: nucleide_enrichment::Stream::with_total_mass(feed, 1.0),
3130 mat_prod: nucleide_enrichment::Stream::new(),
3131 mat_tail: nucleide_enrichment::Stream::new(),
3132 l_t_per_feed: 0.0,
3133 swu_per_feed: 0.0,
3134 swu_per_prod: 0.0,
3135 };
3136 Ok(Self {
3137 inner: std::sync::Mutex::new(casc),
3138 })
3139 }
3140
3141 #[pyo3(signature = (tolerance=None, max_iterations=None))]
3143 fn solve(&self, tolerance: Option<f64>, max_iterations: Option<u32>) -> PyResult<()> {
3144 let tol = tolerance.unwrap_or(nucleide_enrichment::DEFAULT_TOLERANCE);
3145 let iters = max_iterations.unwrap_or(nucleide_enrichment::DEFAULT_MAX_ITER);
3146 let mut c = self
3147 .inner
3148 .lock()
3149 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
3150 *c = nucleide_enrichment::solve_numeric(&c, tol, iters)
3151 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3152 Ok(())
3153 }
3154
3155 #[pyo3(signature = (tolerance=None, max_iterations=None))]
3157 fn solve_multicomponent(
3158 &self,
3159 tolerance: Option<f64>,
3160 max_iterations: Option<u32>,
3161 ) -> PyResult<()> {
3162 let tol = tolerance.unwrap_or(nucleide_enrichment::DEFAULT_TOLERANCE);
3163 let iters = max_iterations.unwrap_or(nucleide_enrichment::DEFAULT_MAX_ITER);
3164 let mut c = self
3165 .inner
3166 .lock()
3167 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
3168 *c = nucleide_enrichment::multicomponent(&c, tol, iters)
3169 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3170 Ok(())
3171 }
3172
3173 #[getter]
3174 fn alpha(&self) -> PyResult<f64> {
3175 Ok(self
3176 .inner
3177 .lock()
3178 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3179 .alpha)
3180 }
3181 #[getter]
3182 #[allow(non_snake_case)]
3183 fn Mstar(&self) -> PyResult<f64> {
3184 Ok(self
3185 .inner
3186 .lock()
3187 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3188 .Mstar)
3189 }
3190 #[getter]
3191 #[allow(non_snake_case)]
3192 fn N(&self) -> PyResult<f64> {
3193 Ok(self
3194 .inner
3195 .lock()
3196 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3197 .N)
3198 }
3199 #[getter]
3200 #[allow(non_snake_case)]
3201 fn M(&self) -> PyResult<f64> {
3202 Ok(self
3203 .inner
3204 .lock()
3205 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3206 .M)
3207 }
3208 #[getter]
3209 fn x_feed_j(&self) -> PyResult<f64> {
3210 Ok(self
3211 .inner
3212 .lock()
3213 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3214 .x_feed_j)
3215 }
3216 #[getter]
3217 fn x_prod_j(&self) -> PyResult<f64> {
3218 Ok(self
3219 .inner
3220 .lock()
3221 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3222 .x_prod_j)
3223 }
3224 #[getter]
3225 fn x_tail_j(&self) -> PyResult<f64> {
3226 Ok(self
3227 .inner
3228 .lock()
3229 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3230 .x_tail_j)
3231 }
3232 #[getter]
3233 fn l_t_per_feed(&self) -> PyResult<f64> {
3234 Ok(self
3235 .inner
3236 .lock()
3237 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3238 .l_t_per_feed)
3239 }
3240 #[getter]
3241 fn swu_per_feed(&self) -> PyResult<f64> {
3242 Ok(self
3243 .inner
3244 .lock()
3245 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3246 .swu_per_feed)
3247 }
3248 #[getter]
3249 fn swu_per_prod(&self) -> PyResult<f64> {
3250 Ok(self
3251 .inner
3252 .lock()
3253 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3254 .swu_per_prod)
3255 }
3256 #[getter]
3258 fn mat_feed(&self) -> PyResult<BTreeMap<String, f64>> {
3259 Ok(self
3260 .inner
3261 .lock()
3262 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3263 .mat_feed
3264 .comp
3265 .iter()
3266 .map(|(id, frac)| (id.to_name(), *frac))
3267 .collect())
3268 }
3269 #[getter]
3271 fn mat_prod(&self) -> PyResult<BTreeMap<String, f64>> {
3272 Ok(self
3273 .inner
3274 .lock()
3275 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3276 .mat_prod
3277 .comp
3278 .iter()
3279 .map(|(id, frac)| (id.to_name(), *frac))
3280 .collect())
3281 }
3282 #[getter]
3284 fn mat_tail(&self) -> PyResult<BTreeMap<String, f64>> {
3285 Ok(self
3286 .inner
3287 .lock()
3288 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?
3289 .mat_tail
3290 .comp
3291 .iter()
3292 .map(|(id, frac)| (id.to_name(), *frac))
3293 .collect())
3294 }
3295 fn separative_work_per_product(&self) -> PyResult<f64> {
3297 let c = self
3298 .inner
3299 .lock()
3300 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
3301 Ok(nucleide_enrichment::swu_per_prod(
3302 c.x_feed_j, c.x_prod_j, c.x_tail_j,
3303 ))
3304 }
3305
3306 fn __repr__(&self) -> PyResult<String> {
3307 let c = self
3308 .inner
3309 .lock()
3310 .map_err(|_| PyValueError::new_err("cascade lock poisoned"))?;
3311 Ok(format!(
3312 "Cascade(alpha={}, Mstar={}, x_prod_j={:.5})",
3313 c.alpha, c.Mstar, c.x_prod_j
3314 ))
3315 }
3316}
3317
3318#[pyfunction]
3322fn enrichment_value_func(x: f64) -> f64 {
3323 nucleide_enrichment::value_func(x)
3324}
3325
3326#[pyfunction]
3330fn enrichment_swu_per_feed(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
3331 nucleide_enrichment::swu_per_feed(x_feed, x_prod, x_tail)
3332}
3333
3334#[pyfunction]
3338fn enrichment_swu_per_prod(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
3339 nucleide_enrichment::swu_per_prod(x_feed, x_prod, x_tail)
3340}
3341
3342#[pyfunction]
3346fn enrichment_swu_per_tail(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
3347 nucleide_enrichment::swu_per_tail(x_feed, x_prod, x_tail)
3348}
3349
3350#[pyclass(name = "MaterialsCompendium")]
3352struct PyMaterialsCompendium {
3353 inner: nucleide_material::MaterialsLibrary,
3354}
3355
3356#[pymethods]
3357impl PyMaterialsCompendium {
3358 #[staticmethod]
3360 fn load(path: &str) -> PyResult<Self> {
3361 nucleide_material::MaterialsLibrary::from_file(path)
3362 .map(|inner| PyMaterialsCompendium { inner })
3363 .map_err(|e| PyValueError::new_err(e.to_string()))
3364 }
3365
3366 fn __len__(&self) -> usize {
3367 self.inner.len()
3368 }
3369
3370 fn names(&self) -> Vec<String> {
3372 self.inner.names().into_iter().map(String::from).collect()
3373 }
3374
3375 #[pyo3(signature = (name, as_material=false))]
3379 #[allow(clippy::type_complexity)]
3380 fn get(&self, name: &str, as_material: bool) -> PyResult<Option<BTreeMap<String, Py<PyAny>>>> {
3381 let entry = match self.inner.get(name) {
3382 Some(e) => e,
3383 None => return Ok(None),
3384 };
3385 let named_fractions = if as_material {
3387 Some(
3388 entry
3389 .to_material()
3390 .map_err(|e| PyValueError::new_err(e.to_string()))?,
3391 )
3392 } else {
3393 None
3394 };
3395
3396 Ok(Python::attach(|py| {
3397 let mut d: BTreeMap<String, Py<PyAny>> = BTreeMap::new();
3398 d.insert(
3399 "name".into(),
3400 entry
3401 .name
3402 .as_str()
3403 .into_pyobject(py)
3404 .unwrap()
3405 .unbind()
3406 .into_any(),
3407 );
3408 d.insert(
3409 "mat_num".into(),
3410 entry.mat_num.into_pyobject(py).unwrap().unbind().into_any(),
3411 );
3412 d.insert(
3413 "density".into(),
3414 entry.density.into_pyobject(py).unwrap().unbind().into_any(),
3415 );
3416 match &named_fractions {
3417 Some(mat) => {
3418 let fr: BTreeMap<String, f64> =
3419 mat.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect();
3420 d.insert(
3421 "fractions".into(),
3422 fr.into_pyobject(py).unwrap().unbind().into_any(),
3423 );
3424 }
3425 None => {
3426 let fr = entry.weight_fractions();
3427 d.insert(
3428 "fractions".into(),
3429 fr.into_pyobject(py).unwrap().unbind().into_any(),
3430 );
3431 }
3432 }
3433 Some(d)
3434 }))
3435 }
3436}
3437
3438#[pyfunction]
3447fn isotxs_parse(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
3448 let owned = text.to_owned();
3449 let lib = py
3450 .detach(move || nucleide_cccc_io::IsotxsLib::parse(&owned))
3451 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3452 Ok(isotxs_to_py(py, &lib))
3453}
3454
3455fn isotxs_to_py(py: Python<'_>, lib: &nucleide_cccc_io::IsotxsLib) -> Py<PyAny> {
3456 use pyo3::types::PyDict;
3457 let out = PyDict::new(py);
3458 let nuclides: Vec<Py<PyAny>> = lib
3459 .nuclides
3460 .iter()
3461 .map(|n| {
3462 let d = PyDict::new(py);
3463 d.set_item("label", &n.label).ok();
3464 d.set_item("zaid", &n.zaid).ok();
3465 d.set_item("groups", n.groups).ok();
3466 d.set_item("total_xs", n.total_xs.clone()).ok();
3467 d.into_any().unbind()
3468 })
3469 .collect();
3470 out.set_item("nuclides", nuclides).ok();
3471 out.into_any().unbind()
3472}
3473
3474#[pyfunction]
3480#[pyo3(signature = (text, kind="rtflux"))]
3481fn rtflux_parse(py: Python<'_>, text: &str, kind: &str) -> PyResult<Py<PyAny>> {
3482 let flux_kind = match kind.to_ascii_lowercase().as_str() {
3483 "rtflux" => nucleide_cccc_io::rtflux::FluxKind::Rtflux,
3484 "atflux" => nucleide_cccc_io::rtflux::FluxKind::Atflux,
3485 "rzflux" => nucleide_cccc_io::rtflux::FluxKind::Rzflux,
3486 other => {
3487 return Err(PyValueError::new_err(format!(
3488 "kind must be rtflux|atflux|rzflux, got `{other}`"
3489 )))
3490 }
3491 };
3492 let owned = text.to_owned();
3493 let flux = py
3494 .detach(move || nucleide_cccc_io::FluxFile::parse(flux_kind, &owned))
3495 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3496 use pyo3::types::PyDict;
3497 let d = PyDict::new(py);
3498 d.set_item("kind", flux.kind.keyword()).ok();
3499 d.set_item("groups", flux.groups).ok();
3500 d.set_item("per_point", flux.per_point).ok();
3501 d.set_item("npoints", flux.npoints()).ok();
3502 d.set_item("values", flux.values.clone()).ok();
3503 d.set_item("total", flux.total()).ok();
3504 Ok(d.into_any().unbind())
3505}
3506
3507fn partisn_deck_from_dict(
3508 deck: &Bound<'_, pyo3::types::PyDict>,
3509) -> PyResult<nucleide_cccc_io::PartisnDeck> {
3510 let title: String = match deck.get_item("title")? {
3511 Some(v) => v
3512 .extract()
3513 .map_err(|_| PyValueError::new_err("partisn deck `title` must be str"))?,
3514 None => return Err(PyValueError::new_err("partisn deck missing `title`")),
3515 };
3516 let dim: u8 = match deck.get_item("dim")? {
3517 Some(v) => v
3518 .extract()
3519 .map_err(|_| PyValueError::new_err("partisn deck `dim` must be 1, 2, or 3"))?,
3520 None => return Err(PyValueError::new_err("partisn deck missing `dim`")),
3521 };
3522 let zones_value = match deck.get_item("zones")? {
3523 Some(v) => v,
3524 None => return Err(PyValueError::new_err("partisn deck missing `zones`")),
3525 };
3526 let zone_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = zones_value
3527 .extract()
3528 .map_err(|_| PyValueError::new_err("partisn deck `zones` must be a list of dicts"))?;
3529 let mut zones = Vec::with_capacity(zone_dicts.len());
3530 for z in &zone_dicts {
3531 let id: u32 = match z.get_item("id")? {
3532 Some(v) => v
3533 .extract()
3534 .map_err(|_| PyValueError::new_err("partisn zone `id` must be int"))?,
3535 None => return Err(PyValueError::new_err("partisn zone missing `id`")),
3536 };
3537 let material: String = match z.get_item("material")? {
3538 Some(v) => v
3539 .extract()
3540 .map_err(|_| PyValueError::new_err("partisn zone `material` must be str"))?,
3541 None => return Err(PyValueError::new_err("partisn zone missing `material`")),
3542 };
3543 let isotxs_labels: Vec<String> = match z.get_item("isotxs_labels")? {
3544 Some(v) => v.extract().map_err(|_| {
3545 PyValueError::new_err("partisn zone `isotxs_labels` must be a list of str")
3546 })?,
3547 None => {
3548 return Err(PyValueError::new_err(
3549 "partisn zone missing `isotxs_labels`",
3550 ))
3551 }
3552 };
3553 let density: f64 = match z.get_item("density")? {
3554 Some(v) => v
3555 .extract()
3556 .map_err(|_| PyValueError::new_err("partisn zone `density` must be float"))?,
3557 None => return Err(PyValueError::new_err("partisn zone missing `density`")),
3558 };
3559 zones.push(nucleide_cccc_io::partisn::PartisnZone {
3560 id,
3561 material,
3562 isotxs_labels,
3563 density,
3564 });
3565 }
3566 let source: Option<String> = match deck.get_item("source")? {
3567 Some(v) if v.is_none() => None,
3568 Some(v) => Some(
3569 v.extract()
3570 .map_err(|_| PyValueError::new_err("partisn deck `source` must be str or None"))?,
3571 ),
3572 None => None,
3573 };
3574 Ok(nucleide_cccc_io::PartisnDeck {
3575 title,
3576 dim,
3577 zones,
3578 source,
3579 })
3580}
3581
3582#[pyfunction]
3587fn partisn_render(py: Python<'_>, deck: &Bound<'_, pyo3::types::PyDict>) -> PyResult<String> {
3588 let rust_deck = partisn_deck_from_dict(deck)?;
3589 Ok(py.detach(move || rust_deck.render()))
3590}
3591
3592#[pyfunction]
3597fn partisn_validate(
3598 py: Python<'_>,
3599 deck: &Bound<'_, pyo3::types::PyDict>,
3600 isotxs_text: &str,
3601) -> PyResult<()> {
3602 let rust_deck = partisn_deck_from_dict(deck)?;
3603 let owned = isotxs_text.to_owned();
3604 let lib = py
3605 .detach(move || nucleide_cccc_io::IsotxsLib::parse(&owned))
3606 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3607 rust_deck
3608 .validate(&lib)
3609 .map_err(|e| PyValueError::new_err(e.to_string()))
3610}
3611
3612fn fispact_row_to_map(
3617 py: Python<'_>,
3618 r: &nucleide_alara_io::output::ResponseRow,
3619) -> BTreeMap<String, Py<PyAny>> {
3620 let mut d = BTreeMap::new();
3621 d.insert(
3622 "time_s".to_string(),
3623 r.time_s.into_pyobject(py).unwrap().unbind().into_any(),
3624 );
3625 d.insert(
3626 "time_label".to_string(),
3627 r.time_label
3628 .clone()
3629 .into_pyobject(py)
3630 .unwrap()
3631 .unbind()
3632 .into_any(),
3633 );
3634 d.insert(
3635 "nuclide".to_string(),
3636 r.nuclide
3637 .clone()
3638 .into_pyobject(py)
3639 .unwrap()
3640 .unbind()
3641 .into_any(),
3642 );
3643 d.insert(
3644 "half_life_s".to_string(),
3645 r.half_life_s.into_pyobject(py).unwrap().unbind().into_any(),
3646 );
3647 d.insert(
3648 "run_lbl".to_string(),
3649 r.run_lbl
3650 .clone()
3651 .into_pyobject(py)
3652 .unwrap()
3653 .unbind()
3654 .into_any(),
3655 );
3656 d.insert(
3657 "block".to_string(),
3658 r.block
3659 .as_str()
3660 .into_pyobject(py)
3661 .unwrap()
3662 .unbind()
3663 .into_any(),
3664 );
3665 d.insert(
3666 "block_name".to_string(),
3667 r.block_name
3668 .clone()
3669 .into_pyobject(py)
3670 .unwrap()
3671 .unbind()
3672 .into_any(),
3673 );
3674 d.insert(
3675 "block_num".to_string(),
3676 r.block_num.into_pyobject(py).unwrap().unbind().into_any(),
3677 );
3678 d.insert(
3679 "variable".to_string(),
3680 r.variable
3681 .as_str()
3682 .into_pyobject(py)
3683 .unwrap()
3684 .unbind()
3685 .into_any(),
3686 );
3687 d.insert(
3688 "var_unit".to_string(),
3689 r.var_unit
3690 .clone()
3691 .into_pyobject(py)
3692 .unwrap()
3693 .unbind()
3694 .into_any(),
3695 );
3696 d.insert(
3697 "value".to_string(),
3698 r.value.into_pyobject(py).unwrap().unbind().into_any(),
3699 );
3700 d
3701}
3702
3703#[pyfunction]
3709fn fispact_parse_output(
3710 py: Python<'_>,
3711 text: &str,
3712 run_lbl: &str,
3713) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
3714 let owned_text = text.to_owned();
3715 let owned_lbl = run_lbl.to_owned();
3716 let rows = py
3717 .detach(move || {
3718 nucleide_fispact_io::parse_to_frame(&owned_text, &owned_lbl).map(|f| f.rows)
3719 })
3720 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3721 Ok(rows.iter().map(|r| fispact_row_to_map(py, r)).collect())
3722}
3723
3724#[pyfunction]
3734fn fispact_parse_clearance(
3735 py: Python<'_>,
3736 text: &str,
3737) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
3738 let owned_text = text.to_owned();
3739 let scan = py
3740 .detach(move || nucleide_fispact_io::parse_clearance(&owned_text))
3741 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3742 Ok(scan
3743 .rows
3744 .iter()
3745 .map(|row| {
3746 let mut d = BTreeMap::new();
3747 d.insert(
3748 "interval".to_string(),
3749 row.interval.into_pyobject(py).unwrap().unbind().into_any(),
3750 );
3751 d.insert(
3752 "time_s".to_string(),
3753 row.time_s.into_pyobject(py).unwrap().unbind().into_any(),
3754 );
3755 d.insert(
3756 "time_label".to_string(),
3757 row.time_label
3758 .clone()
3759 .into_pyobject(py)
3760 .unwrap()
3761 .unbind()
3762 .into_any(),
3763 );
3764 d.insert(
3765 "cooling".to_string(),
3766 pyo3::types::PyBool::new(py, row.cooling)
3767 .to_owned()
3768 .into_any()
3769 .unbind(),
3770 );
3771 d.insert(
3772 "nuclide".to_string(),
3773 row.nuclide
3774 .clone()
3775 .into_pyobject(py)
3776 .unwrap()
3777 .unbind()
3778 .into_any(),
3779 );
3780 d.insert(
3781 "flags".to_string(),
3782 row.flags
3783 .clone()
3784 .into_pyobject(py)
3785 .unwrap()
3786 .unbind()
3787 .into_any(),
3788 );
3789 d.insert(
3790 "activity_bq".to_string(),
3791 row.activity_bq
3792 .into_pyobject(py)
3793 .unwrap()
3794 .unbind()
3795 .into_any(),
3796 );
3797 d.insert(
3798 "clearance_index".to_string(),
3799 row.clearance_index
3800 .into_pyobject(py)
3801 .unwrap()
3802 .unbind()
3803 .into_any(),
3804 );
3805 d.insert(
3806 "half_life_s".to_string(),
3807 row.half_life_s
3808 .into_pyobject(py)
3809 .unwrap()
3810 .unbind()
3811 .into_any(),
3812 );
3813 d
3814 })
3815 .collect())
3816}
3817
3818#[pyfunction]
3828fn origen_parse_tape5(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
3829 let owned = text.to_owned();
3830 let tape = py
3831 .detach(move || nucleide_origen_io::Tape5::parse(&owned))
3832 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3833 use pyo3::types::PyDict;
3834 let out = PyDict::new(py);
3835 out.set_item("titles", tape.titles.clone()).ok();
3836 let steps: Vec<Py<PyAny>> = tape
3837 .irradiation_steps
3838 .iter()
3839 .map(|s| {
3840 let d = PyDict::new(py);
3841 d.set_item("flux", s.flux).ok();
3842 d.set_item("days", s.days).ok();
3843 d.into_any().unbind()
3844 })
3845 .collect();
3846 out.set_item("irradiation_steps", steps).ok();
3847 let materials: Vec<Py<PyAny>> = tape
3848 .materials
3849 .iter()
3850 .map(|m| {
3851 let d = PyDict::new(py);
3852 d.set_item("name", &m.name).ok();
3853 let entries: Vec<Py<PyAny>> = m
3854 .grams
3855 .iter()
3856 .map(|(nuclide, grams)| {
3857 let e = PyDict::new(py);
3858 e.set_item("nuclide", nuclide).ok();
3859 e.set_item("grams", *grams).ok();
3860 e.into_any().unbind()
3861 })
3862 .collect();
3863 d.set_item("entries", entries).ok();
3864 d.into_any().unbind()
3865 })
3866 .collect();
3867 out.set_item("materials", materials).ok();
3868 Ok(out.into_any().unbind())
3869}
3870
3871#[pyfunction]
3876fn origen_parse_tape6(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
3877 let owned = text.to_owned();
3878 let tape = py
3879 .detach(move || nucleide_origen_io::Tape6::parse(&owned))
3880 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3881 use pyo3::types::PyDict;
3882 let out = PyDict::new(py);
3883 let records: Vec<Py<PyAny>> = tape
3884 .records
3885 .iter()
3886 .map(|r| {
3887 let d = PyDict::new(py);
3888 d.set_item("nuclide", &r.nuclide).ok();
3889 d.set_item("grams", r.grams).ok();
3890 d.set_item("activity_bq", r.activity_bq).ok();
3891 d.into_any().unbind()
3892 })
3893 .collect();
3894 out.set_item("records", records).ok();
3895 out.set_item("total_activity", tape.total_activity()).ok();
3896 Ok(out.into_any().unbind())
3897}
3898
3899#[pyfunction]
3903fn origen_parse_tape9(py: Python<'_>, text: &str) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
3904 let owned = text.to_owned();
3905 let entries = py
3906 .detach(move || nucleide_origen_io::Tape9Entry::parse(&owned))
3907 .map_err(|e| PyValueError::new_err(e.to_string()))?;
3908 Ok(entries
3909 .iter()
3910 .map(|e| {
3911 let mut d = BTreeMap::new();
3912 d.insert(
3913 "nuclide".to_string(),
3914 e.nuclide
3915 .clone()
3916 .into_pyobject(py)
3917 .unwrap()
3918 .unbind()
3919 .into_any(),
3920 );
3921 d.insert(
3922 "decay_const".to_string(),
3923 e.decay_const.into_pyobject(py).unwrap().unbind().into_any(),
3924 );
3925 d
3926 })
3927 .collect())
3928}
3929
3930fn r2s_workflow_to_py(py: Python<'_>, workflow: &nucleide_r2s::R2sWorkflow) -> Py<PyAny> {
3935 use pyo3::types::PyDict;
3936 let out = PyDict::new(py);
3937 let steps: Vec<Py<PyAny>> = workflow
3938 .steps
3939 .iter()
3940 .map(|s| {
3941 let d = PyDict::new(py);
3942 d.set_item("zone", &s.zone).ok();
3943 d.set_item("flux", &s.flux).ok();
3944 d.into_any().unbind()
3945 })
3946 .collect();
3947 out.set_item("steps", steps).ok();
3948 out.set_item("cooling_s", workflow.cooling_s.clone()).ok();
3949 out.set_item("top_schedule", &workflow.top_schedule).ok();
3950 out.into_any().unbind()
3951}
3952
3953fn r2s_workflow_from_dict(
3954 workflow: &Bound<'_, pyo3::types::PyDict>,
3955) -> PyResult<nucleide_r2s::R2sWorkflow> {
3956 let steps_value = match workflow.get_item("steps")? {
3957 Some(v) => v,
3958 None => return Err(PyValueError::new_err("r2s workflow missing `steps`")),
3959 };
3960 let step_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = steps_value
3961 .extract()
3962 .map_err(|_| PyValueError::new_err("r2s workflow `steps` must be a list of dicts"))?;
3963 let mut steps = Vec::with_capacity(step_dicts.len());
3964 for s in &step_dicts {
3965 let zone: String = match s.get_item("zone")? {
3966 Some(v) => v
3967 .extract()
3968 .map_err(|_| PyValueError::new_err("r2s step `zone` must be str"))?,
3969 None => return Err(PyValueError::new_err("r2s step missing `zone`")),
3970 };
3971 let flux: String = match s.get_item("flux")? {
3972 Some(v) => v
3973 .extract()
3974 .map_err(|_| PyValueError::new_err("r2s step `flux` must be str"))?,
3975 None => return Err(PyValueError::new_err("r2s step missing `flux`")),
3976 };
3977 steps.push(nucleide_r2s::R2sStep { zone, flux });
3978 }
3979 let cooling_s: Vec<f64> = match workflow.get_item("cooling_s")? {
3980 Some(v) => v.extract().map_err(|_| {
3981 PyValueError::new_err("r2s workflow `cooling_s` must be a list of float")
3982 })?,
3983 None => return Err(PyValueError::new_err("r2s workflow missing `cooling_s`")),
3984 };
3985 let top_schedule: String = match workflow.get_item("top_schedule")? {
3986 Some(v) => v
3987 .extract()
3988 .map_err(|_| PyValueError::new_err("r2s workflow `top_schedule` must be str"))?,
3989 None => return Err(PyValueError::new_err("r2s workflow missing `top_schedule`")),
3990 };
3991 Ok(nucleide_r2s::R2sWorkflow {
3992 steps,
3993 cooling_s,
3994 top_schedule,
3995 })
3996}
3997
3998#[pyfunction]
4003fn r2s_from_deck(py: Python<'_>, deck_text: &str) -> PyResult<Py<PyAny>> {
4004 let owned = deck_text.to_owned();
4005 let workflow = py
4006 .detach(move || {
4007 let deck = nucleide_alara_io::AlaraDeck::parse(&owned)
4008 .map_err(|e| nucleide_r2s::Error::Invalid(e.to_string()))?;
4009 nucleide_r2s::R2sWorkflow::from_deck(&deck)
4010 })
4011 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4012 Ok(r2s_workflow_to_py(py, &workflow))
4013}
4014
4015#[pyfunction]
4020fn r2s_validate(
4021 py: Python<'_>,
4022 workflow: &Bound<'_, pyo3::types::PyDict>,
4023 deck_text: &str,
4024) -> PyResult<()> {
4025 let rust_workflow = r2s_workflow_from_dict(workflow)?;
4026 let owned = deck_text.to_owned();
4027 let deck = py
4028 .detach(move || nucleide_alara_io::AlaraDeck::parse(&owned))
4029 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4030 rust_workflow
4031 .validate_against(&deck)
4032 .map_err(|e| PyValueError::new_err(e.to_string()))
4033}
4034
4035#[pyfunction]
4040#[pyo3(signature = (deck_text, top=None))]
4041fn r2s_expand(
4042 py: Python<'_>,
4043 deck_text: &str,
4044 top: Option<&str>,
4045) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
4046 let owned_text = deck_text.to_owned();
4047 let owned_top = top.map(str::to_owned);
4048 let steps = py
4049 .detach(move || {
4050 let deck = nucleide_alara_io::AlaraDeck::parse(&owned_text)
4051 .map_err(|e| nucleide_r2s::Error::Invalid(e.to_string()))?;
4052 let mut workflow = nucleide_r2s::R2sWorkflow::from_deck(&deck)?;
4053 if let Some(top) = owned_top {
4054 workflow.top_schedule = top;
4055 }
4056 workflow.expand(&deck, &[])
4057 })
4058 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4059 Ok(steps
4060 .into_iter()
4061 .map(|s| {
4062 let mut d = BTreeMap::new();
4063 let cooling = s.is_cooling();
4064 d.insert(
4065 "duration_s".to_string(),
4066 s.duration_s.into_pyobject(py).unwrap().unbind().into_any(),
4067 );
4068 d.insert(
4069 "flux".to_string(),
4070 s.flux.into_pyobject(py).unwrap().unbind().into_any(),
4071 );
4072 d.insert(
4073 "is_cooling".to_string(),
4074 pyo3::types::PyBool::new(py, cooling)
4075 .to_owned()
4076 .into_any()
4077 .unbind(),
4078 );
4079 d
4080 })
4081 .collect())
4082}
4083
4084#[pyfunction]
4095fn r2s_assemble(
4096 py: Python<'_>,
4097 output_text: &str,
4098 run_lbl: &str,
4099 zone: &str,
4100 groups: usize,
4101) -> PyResult<Py<PyAny>> {
4102 let owned_text = output_text.to_owned();
4103 let owned_lbl = run_lbl.to_owned();
4104 let owned_zone = zone.to_owned();
4105 let source = py
4106 .detach(move || {
4107 let frame = nucleide_alara_io::output::ResponseFrame::parse(&owned_text, &owned_lbl)
4108 .map_err(|e| nucleide_r2s::Error::Invalid(e.to_string()))?;
4109 Ok::<_, nucleide_r2s::Error>(nucleide_r2s::photon::assemble(
4110 &frame,
4111 &owned_zone,
4112 groups,
4113 ))
4114 })
4115 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4116 use pyo3::types::PyDict;
4117 let out = PyDict::new(py);
4118 out.set_item("zone", source.zone.clone()).ok();
4119 out.set_item("groups", source.groups.clone()).ok();
4120 out.set_item("total", source.total()).ok();
4121 Ok(out.into_any().unbind())
4122}
4123
4124#[pyfunction]
4133#[pyo3(signature = (totals, zone_of_voxel, split=false))]
4134fn r2s_tag_zone_strength(
4135 py: Python<'_>,
4136 totals: Vec<f64>,
4137 zone_of_voxel: Vec<usize>,
4138 split: bool,
4139) -> PyResult<Py<PyAny>> {
4140 let zones: Vec<nucleide_r2s::photon::ZonePhotonSource> = totals
4141 .into_iter()
4142 .enumerate()
4143 .map(|(i, total)| {
4144 let groups = if total == 0.0 {
4145 Vec::new()
4146 } else {
4147 vec![total]
4148 };
4149 nucleide_r2s::photon::ZonePhotonSource {
4150 zone: format!("zone{i}"),
4151 groups,
4152 }
4153 })
4154 .collect();
4155 let tags = if split {
4156 nucleide_r2s::tags::split_zone_totals(&zones, &zone_of_voxel)
4157 } else {
4158 nucleide_r2s::tags::tag_zone_totals(&zones, &zone_of_voxel)
4159 }
4160 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4161 use pyo3::types::PyDict;
4162 let out = PyDict::new(py);
4163 out.set_item("n_zones", tags.n_zones).ok();
4164 out.set_item("zone_of_voxel", tags.zone_of_voxel.clone())
4165 .ok();
4166 out.set_item("source_strength", tags.source_strength.clone())
4167 .ok();
4168 out.set_item("decay_time_s", tags.decay_time_s.clone()).ok();
4169 out.set_item("total", tags.total_strength()).ok();
4170 Ok(out.into_any().unbind())
4171}
4172
4173#[pyfunction]
4182fn r2s_photon_group_sums(
4183 py: Python<'_>,
4184 photon_text: &str,
4185 nuclides: Vec<String>,
4186 time_s: f64,
4187) -> PyResult<Py<PyAny>> {
4188 let source = nucleide_alara_io::photon::PhotonSource::from_str(photon_text)
4189 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4190 let names: Vec<&str> = nuclides.iter().map(String::as_str).collect();
4191 let at = nucleide_r2s::tags::photon_groups_at(&source, &names, time_s);
4192 let sums = nucleide_r2s::tags::sum_group_strengths(&at)
4193 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4194 use pyo3::types::PyDict;
4195 let out = PyDict::new(py);
4196 let rows: Vec<Py<PyAny>> = at
4197 .iter()
4198 .map(|g| {
4199 let d = PyDict::new(py);
4200 d.set_item("nuclide", g.nuclide.clone()).ok();
4201 d.set_item("time_s", g.time_s).ok();
4202 d.set_item("strengths", g.strengths.clone()).ok();
4203 d.into_any().unbind()
4204 })
4205 .collect();
4206 out.set_item("groups", rows).ok();
4207 out.set_item("sums", sums.clone()).ok();
4208 out.set_item("total", sums.iter().sum::<f64>()).ok();
4209 Ok(out.into_any().unbind())
4210}
4211
4212fn snapshot_dict_str(
4213 zone: &Bound<'_, pyo3::types::PyDict>,
4214 key: &str,
4215 what: &str,
4216) -> PyResult<String> {
4217 match zone.get_item(key)? {
4218 Some(v) => v
4219 .extract()
4220 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be str"))),
4221 None => Err(PyValueError::new_err(format!(
4222 "snapshot {what} missing `{key}`"
4223 ))),
4224 }
4225}
4226
4227fn snapshot_dict_opt_str(
4228 zone: &Bound<'_, pyo3::types::PyDict>,
4229 key: &str,
4230 what: &str,
4231) -> PyResult<Option<String>> {
4232 match zone.get_item(key)? {
4233 Some(v) if v.is_none() => Ok(None),
4234 Some(v) => v
4235 .extract::<String>()
4236 .map(Some)
4237 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be str"))),
4238 None => Ok(None),
4239 }
4240}
4241
4242fn snapshot_dict_f64(
4243 zone: &Bound<'_, pyo3::types::PyDict>,
4244 key: &str,
4245 what: &str,
4246) -> PyResult<f64> {
4247 match zone.get_item(key)? {
4248 Some(v) => v
4249 .extract()
4250 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be float"))),
4251 None => Err(PyValueError::new_err(format!(
4252 "snapshot {what} missing `{key}`"
4253 ))),
4254 }
4255}
4256
4257fn snapshot_dict_opt_f64(
4258 zone: &Bound<'_, pyo3::types::PyDict>,
4259 key: &str,
4260 what: &str,
4261) -> PyResult<Option<f64>> {
4262 match zone.get_item(key)? {
4263 Some(v) if v.is_none() => Ok(None),
4264 Some(v) => v
4265 .extract::<f64>()
4266 .map(Some)
4267 .map_err(|_| PyValueError::new_err(format!("snapshot {what} `{key}` must be float"))),
4268 None => Ok(None),
4269 }
4270}
4271
4272fn snapshot_zone_from_dict(
4273 zone: &Bound<'_, pyo3::types::PyDict>,
4274) -> PyResult<nucleide_r2s::snapshot::SnapshotZone> {
4275 let id = snapshot_dict_str(zone, "id", "zone")?;
4276 let volume_cm3 = snapshot_dict_f64(zone, "volume_cm3", "zone")?;
4277 let composition: BTreeMap<String, f64> = match zone.get_item("composition")? {
4278 Some(v) => v.extract().map_err(|_| {
4279 PyValueError::new_err("snapshot zone `composition` must be a dict of str to float")
4280 })?,
4281 None => return Err(PyValueError::new_err("snapshot zone missing `composition`")),
4282 };
4283 Ok(nucleide_r2s::snapshot::SnapshotZone {
4284 zone: id,
4285 volume_cm3,
4286 zbottom_cm: snapshot_dict_opt_f64(zone, "zbottom_cm", "zone")?,
4287 ztop_cm: snapshot_dict_opt_f64(zone, "ztop_cm", "zone")?,
4288 material: snapshot_dict_opt_str(zone, "material", "zone")?,
4289 xs_type: snapshot_dict_opt_str(zone, "xs_type", "zone")?,
4290 temperature_c: snapshot_dict_opt_f64(zone, "temperature_C", "zone")?,
4291 composition: composition.into_iter().collect(),
4292 flux_name: snapshot_dict_opt_str(zone, "flux", "zone")?,
4293 })
4294}
4295
4296fn snapshot_input_from_dict(
4297 snapshot: &Bound<'_, pyo3::types::PyDict>,
4298) -> PyResult<nucleide_r2s::snapshot::SnapshotInput> {
4299 let zone_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = match snapshot.get_item("zones")? {
4300 Some(v) => v
4301 .extract()
4302 .map_err(|_| PyValueError::new_err("snapshot `zones` must be a list of dicts"))?,
4303 None => return Err(PyValueError::new_err("snapshot missing `zones`")),
4304 };
4305 let mut zones = Vec::with_capacity(zone_dicts.len());
4306 for z in &zone_dicts {
4307 zones.push(snapshot_zone_from_dict(z)?);
4308 }
4309 let flux_dicts: Vec<Bound<'_, pyo3::types::PyDict>> = match snapshot.get_item("flux_defs")? {
4310 Some(v) => v
4311 .extract()
4312 .map_err(|_| PyValueError::new_err("snapshot `flux_defs` must be a list of dicts"))?,
4313 None => return Err(PyValueError::new_err("snapshot missing `flux_defs`")),
4314 };
4315 let mut flux_defs = Vec::with_capacity(flux_dicts.len());
4316 for f in &flux_dicts {
4317 flux_defs.push(nucleide_r2s::snapshot::SnapshotFluxDef {
4318 name: snapshot_dict_str(f, "name", "flux")?,
4319 file: snapshot_dict_str(f, "file", "flux")?,
4320 scale: snapshot_dict_f64(f, "scale", "flux")?,
4321 });
4322 }
4323 let cooling_s: Vec<f64> = match snapshot.get_item("cooling_s")? {
4324 Some(v) => v
4325 .extract()
4326 .map_err(|_| PyValueError::new_err("snapshot `cooling_s` must be a list of float"))?,
4327 None => return Err(PyValueError::new_err("snapshot missing `cooling_s`")),
4328 };
4329 Ok(nucleide_r2s::snapshot::SnapshotInput {
4330 zones,
4331 flux_defs,
4332 cooling_s,
4333 schedule_text: snapshot_dict_opt_str(snapshot, "schedule_text", "snapshot")?,
4334 output: snapshot_dict_opt_str(snapshot, "output", "snapshot")?,
4335 })
4336}
4337
4338#[pyfunction]
4344fn r2s_snapshot_inventory(
4345 snapshot: &Bound<'_, pyo3::types::PyDict>,
4346) -> PyResult<BTreeMap<String, f64>> {
4347 let input = snapshot_input_from_dict(snapshot)?;
4348 nucleide_r2s::snapshot::snapshot_inventory(&input)
4349 .map(|totals| totals.into_iter().collect())
4350 .map_err(|e| PyValueError::new_err(e.to_string()))
4351}
4352
4353#[pyfunction]
4359fn r2s_expand_sweep(
4360 axes: Vec<BTreeMap<String, Bound<'_, pyo3::types::PyAny>>>,
4361) -> PyResult<Vec<BTreeMap<String, String>>> {
4362 use pyo3::types::PyAnyMethods;
4363 let mut parsed = Vec::with_capacity(axes.len());
4364 for axis in &axes {
4365 let name: String = axis
4366 .get("name")
4367 .and_then(|v| v.extract().ok())
4368 .ok_or_else(|| PyValueError::new_err("sweep axis needs a `name` string"))?;
4369 let values: Vec<f64> = axis
4370 .get("values")
4371 .and_then(|v| v.extract().ok())
4372 .ok_or_else(|| PyValueError::new_err("sweep axis needs a `values` float list"))?;
4373 parsed.push(
4374 nucleide_r2s::sweep::SweepAxis::new(&name, values)
4375 .map_err(|e| PyValueError::new_err(e.to_string()))?,
4376 );
4377 }
4378 let cases = nucleide_r2s::sweep::expand_sweep(&parsed)
4379 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4380 Ok(cases
4381 .into_iter()
4382 .map(|c| {
4383 let mut d = BTreeMap::new();
4384 d.insert("name".to_string(), c.name);
4385 d.insert(
4386 "params".to_string(),
4387 c.params
4388 .iter()
4389 .map(|(k, v)| format!("{k}={v}"))
4390 .collect::<Vec<_>>()
4391 .join(","),
4392 );
4393 d
4394 })
4395 .collect())
4396}
4397#[pyfunction]
4414fn r2s_from_snapshot(
4415 py: Python<'_>,
4416 snapshot: &Bound<'_, pyo3::types::PyDict>,
4417) -> PyResult<Py<PyAny>> {
4418 let input = snapshot_input_from_dict(snapshot)?;
4419 let (workflow, template, decks) = py
4420 .detach(move || nucleide_r2s::snapshot::snapshot_workflow(&input))
4421 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4422 use pyo3::types::PyDict;
4423 let out = PyDict::new(py);
4424 out.set_item("workflow", r2s_workflow_to_py(py, &workflow))
4425 .ok();
4426 out.set_item("deck", template.to_string()).ok();
4427 let deck_texts: Vec<String> = decks.iter().map(ToString::to_string).collect();
4428 out.set_item("decks", deck_texts).ok();
4429 Ok(out.into_any().unbind())
4430}
4431
4432fn parse_integrator(name: &str) -> PyResult<nucleide_depletion::Integrator> {
4450 use nucleide_depletion::Integrator as I;
4451 if name.eq_ignore_ascii_case("predictor") {
4452 return Ok(I::Predictor);
4453 }
4454 if name.eq_ignore_ascii_case("cecm") {
4455 return Ok(I::Cecm);
4456 }
4457 if name.eq_ignore_ascii_case("cf4") {
4458 return Ok(I::Cf4);
4459 }
4460 Err(PyValueError::new_err(format!(
4461 "unsupported integrator `{name}` (supported: predictor, cecm, cf4)"
4462 )))
4463}
4464
4465#[pyfunction]
4478#[pyo3(signature = (chain, n0, dts, rates=None, rates_list=None, integrator="predictor", order=48, method="cram48"))]
4479#[allow(clippy::too_many_arguments)]
4480fn deplete_series(
4481 chain: &PyChain,
4482 n0: BTreeMap<String, f64>,
4483 dts: Vec<f64>,
4484 rates: Option<RateMap>,
4485 rates_list: Option<Vec<Option<RateMap>>>,
4486 integrator: &str,
4487 order: u8,
4488 method: &str,
4489) -> PyResult<Py<PyAny>> {
4490 use nucleide_depletion::{DepletionSystem, ReactionRates, Step};
4491 let integrator = parse_integrator(integrator)?;
4492 let method = resolve_method(order, method)?;
4493 if let Some(list) = &rates_list {
4494 if list.len() != dts.len() {
4495 return Err(PyValueError::new_err(format!(
4496 "rates_list has {} entries but dts has {}",
4497 list.len(),
4498 dts.len()
4499 )));
4500 }
4501 }
4502 if dts.is_empty() {
4503 return Err(PyValueError::new_err("dts must not be empty"));
4504 }
4505 let mut n0_vec = vec![0.0; chain.inner.len()];
4507 for (name, value) in &n0 {
4508 let idx = chain.inner.index_of(name).ok_or_else(|| {
4509 PyValueError::new_err(format!("unknown nuclide `{name}` for this chain"))
4510 })?;
4511 n0_vec[idx] = *value;
4512 }
4513 let empty = BTreeMap::new();
4514 let mut steps = Vec::with_capacity(dts.len());
4515 for (i, dt) in dts.iter().enumerate() {
4516 let step_rates = rates_list
4517 .as_ref()
4518 .and_then(|list| list[i].as_ref())
4519 .or(rates.as_ref())
4520 .unwrap_or(&empty);
4521 let rs = split_rates(step_rates, &chain.inner)?;
4522 steps.push(Step::new(*dt, rs));
4523 }
4524 let template = DepletionSystem::build((*chain.inner).clone(), &ReactionRates::new())
4527 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4528 let series =
4531 nucleide_depletion::integrate_with_method(&template, &n0_vec, &steps, integrator, method)
4532 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4533 let names: Vec<&str> = template
4534 .chain
4535 .nuclides
4536 .iter()
4537 .map(|nuc| nuc.name.as_str())
4538 .collect();
4539 let keyed = |rows: &[Vec<f64>]| -> Vec<BTreeMap<String, f64>> {
4540 rows.iter()
4541 .map(|row| {
4542 names
4543 .iter()
4544 .zip(row)
4545 .map(|(name, v)| ((*name).to_string(), *v))
4546 .collect()
4547 })
4548 .collect()
4549 };
4550 let atoms = keyed(&series.atoms[1..]);
4552 let activity = keyed(&series.activity[1..]);
4553 let decay_heat = keyed(&series.decay_heat[1..]);
4554 let times = series.times[1..].to_vec();
4555 Ok(Python::attach(|py| {
4556 use pyo3::types::PyDict;
4557 let out = PyDict::new(py);
4558 out.set_item("times", ×).ok();
4559 out.set_item("atoms", &atoms).ok();
4560 out.set_item("activity", &activity).ok();
4561 out.set_item("decay_heat", &decay_heat).ok();
4562 out.into_any().unbind()
4563 }))
4564}
4565
4566#[pyfunction]
4571fn simple_xs(name: &str) -> PyResult<Option<(f64, f64)>> {
4572 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4573 Ok(nucleide_nuclei::data::simple_xs_by_name(name))
4574}
4575
4576#[pyfunction]
4581fn scattering_length(name: &str) -> PyResult<Option<f64>> {
4582 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4583 Ok(nucleide_nuclei::data::scattering_length_by_name(name).map(|(b_coh, _)| b_coh))
4584}
4585
4586#[pyfunction]
4590fn decay_energy(name: &str) -> PyResult<Option<f64>> {
4591 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4592 Ok(nucleide_nuclei::data::decay_energy_mev_by_name(name))
4593}
4594
4595#[pyfunction]
4601fn decay_branches(name: &str) -> PyResult<Vec<(String, f64, String)>> {
4602 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4603 Ok(nucleide_nuclei::data::decay_branches_by_name(name)
4604 .unwrap_or_default()
4605 .into_iter()
4606 .map(|b| {
4607 (
4608 nucleide_nuclei::NuclideId::from_nucid(b.progeny).to_name(),
4609 b.branching_fraction,
4610 b.mode.as_str().to_string(),
4611 )
4612 })
4613 .collect())
4614}
4615
4616#[pyfunction]
4622fn decay_branch_fraction(parent: &str, progeny: &str) -> PyResult<Option<f64>> {
4623 NuclideId::from_name(parent).map_err(wrap_nucid_err)?;
4624 NuclideId::from_name(progeny).map_err(wrap_nucid_err)?;
4625 Ok(nucleide_nuclei::data::branching_fraction_by_name(
4626 parent, progeny,
4627 ))
4628}
4629
4630type PyFissionYieldSets = Vec<(f64, Vec<(String, f64, f64)>)>;
4640
4641#[pyfunction]
4642#[pyo3(signature = (parent, origin="n", kind="independent"))]
4643fn fission_yields(parent: &str, origin: &str, kind: &str) -> PyResult<PyFissionYieldSets> {
4644 NuclideId::from_name(parent).map_err(wrap_nucid_err)?;
4645 let origin = nucleide_nuclei::data::FissionYieldOrigin::parse(origin).ok_or_else(|| {
4646 PyValueError::new_err(format!(
4647 "unknown fission-yield origin `{origin}` (expected `n` or `sf`)"
4648 ))
4649 })?;
4650 let kind = nucleide_nuclei::data::FissionYieldKind::parse(kind).ok_or_else(|| {
4651 PyValueError::new_err(format!(
4652 "unknown fission-yield kind `{kind}` (expected `independent` or `cumulative`)"
4653 ))
4654 })?;
4655 Ok(
4656 nucleide_nuclei::data::fission_yields_by_name(parent, origin, kind)
4657 .unwrap_or_default()
4658 .into_iter()
4659 .map(|set| {
4660 (
4661 set.energy_ev,
4662 set.products
4663 .into_iter()
4664 .map(|p| {
4665 (
4666 nucleide_nuclei::NuclideId::from_nucid(p.progeny).to_name(),
4667 p.yield_fraction,
4668 p.uncertainty,
4669 )
4670 })
4671 .collect(),
4672 )
4673 })
4674 .collect(),
4675 )
4676}
4677
4678#[pyfunction]
4685fn fission_yield(parent: &str, progeny: &str) -> PyResult<Option<f64>> {
4686 NuclideId::from_name(parent).map_err(wrap_nucid_err)?;
4687 NuclideId::from_name(progeny).map_err(wrap_nucid_err)?;
4688 Ok(nucleide_nuclei::data::fission_yield_by_name(
4689 parent, progeny,
4690 ))
4691}
4692
4693#[pyfunction]
4699fn normalize_nuclide(name: &str) -> PyResult<String> {
4700 Ok(nucleide_nuclei::dialects::normalize_nuclide_name(name)
4701 .map_err(|e| PyValueError::new_err(e.to_string()))?
4702 .to_name())
4703}
4704
4705#[pyfunction]
4712fn decay_heat(comp: BTreeMap<String, f64>) -> PyResult<f64> {
4713 let mat = comp_to_material(comp)?;
4714 let analytics = nucleide_material::Analytics {
4715 masses: &nucleide_material::Ame2020,
4716 decays: &nucleide_material::ChainDecays,
4717 };
4718 mat.total_decay_heat(&analytics, &nucleide_material::DecayEnergies)
4719 .map_err(|e| PyValueError::new_err(e.to_string()))
4720}
4721
4722fn parse_dose_pathway(s: &str) -> PyResult<nucleide_material::DosePathway> {
4723 nucleide_material::DosePathway::parse(s).ok_or_else(|| {
4724 PyValueError::new_err(format!(
4725 "unknown dose pathway `{s}` (supported: air, soil, ingest, inhale)"
4726 ))
4727 })
4728}
4729
4730fn parse_dose_source(s: &str) -> PyResult<nucleide_material::DoseSource> {
4731 nucleide_material::DoseSource::parse(s).ok_or_else(|| {
4732 PyValueError::new_err(format!(
4733 "unknown dose source `{s}` (supported: EPA, DOE, GENII)"
4734 ))
4735 })
4736}
4737
4738#[pyfunction]
4745#[pyo3(signature = (name, pathway, source="EPA"))]
4746fn dose_factor(name: &str, pathway: &str, source: &str) -> PyResult<Option<f64>> {
4747 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
4748 let p = parse_dose_pathway(pathway)?;
4749 let s = parse_dose_source(source)?;
4750 Ok(nucleide_nuclei::data::dose_factor_by_name(name, p, s))
4751}
4752
4753fn wrap_fgr15_err(e: nucleide_nuclei::fgr15::Error) -> PyErr {
4754 PyValueError::new_err(e.to_string())
4755}
4756
4757#[pyfunction]
4768#[pyo3(signature = (text, expected_rows))]
4769fn parse_fgr15_table<'py>(
4770 py: Python<'py>,
4771 text: &str,
4772 expected_rows: usize,
4773) -> PyResult<pyo3::Bound<'py, pyo3::types::PyDict>> {
4774 let table = nucleide_nuclei::fgr15::parse_table(text, expected_rows).map_err(wrap_fgr15_err)?;
4775 let out = pyo3::types::PyDict::new(py);
4776 out.set_item("scenario", table.scenario().as_str())?;
4777 out.set_item("units", table.units())?;
4778 let coefficients = pyo3::types::PyDict::new(py);
4779 for (nucid, row) in table.iter() {
4780 coefficients.set_item(
4781 nucleide_nuclei::fgr15::name_of(NuclideId::from_nucid(nucid)),
4782 row.to_vec(),
4783 )?;
4784 }
4785 out.set_item("coefficients", coefficients)?;
4786 Ok(out)
4787}
4788
4789#[pyfunction]
4795fn fgr15_age_index(age: &str) -> PyResult<usize> {
4796 nucleide_nuclei::fgr15::Fgr15Age::parse(age)
4797 .map(|a| a.index())
4798 .ok_or_else(|| {
4799 PyValueError::new_err(format!(
4800 "unknown FGR 15 age group `{age}` (supported: newborn, 1, 5, 10, 15, adult)"
4801 ))
4802 })
4803}
4804
4805#[pyfunction]
4817#[pyo3(signature = (comp, pathway, source="EPA"))]
4818fn dose_per_g(comp: BTreeMap<String, f64>, pathway: &str, source: &str) -> PyResult<f64> {
4819 let mat = comp_to_material(comp)?;
4820 let analytics = nucleide_material::Analytics {
4821 masses: &nucleide_material::Ame2020,
4822 decays: &nucleide_material::ChainDecays,
4823 };
4824 let p = parse_dose_pathway(pathway)?;
4825 let s = parse_dose_source(source)?;
4826 mat.total_dose_per_g(&analytics, &nucleide_material::DoseFactors, p, s)
4827 .map_err(|e| PyValueError::new_err(e.to_string()))
4828}
4829
4830#[pyfunction]
4838#[allow(clippy::type_complexity)]
4839fn separate_material(
4840 comp: BTreeMap<String, f64>,
4841 effs: BTreeMap<String, f64>,
4842) -> PyResult<(BTreeMap<String, f64>, BTreeMap<String, f64>)> {
4843 let mat = comp_to_material(comp)?;
4844 let mut table = Vec::with_capacity(effs.len());
4845 for (name, eff) in &effs {
4846 let id = NuclideId::from_name(name)
4847 .map_err(|e| PyValueError::new_err(format!("`{name}`: {e}")))?;
4848 table.push((id, *eff));
4849 }
4850 let (product, tails) = mat
4851 .separate(&table)
4852 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4853 let named =
4854 |m: nucleide_material::Material| m.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect();
4855 Ok((named(product), named(tails)))
4856}
4857
4858#[pyfunction]
4865fn blend_material(parts: Vec<(BTreeMap<String, f64>, f64)>) -> PyResult<BTreeMap<String, f64>> {
4866 let mats: Vec<nucleide_material::Material> = parts
4867 .iter()
4868 .map(|(comp, _)| comp_to_material(comp.clone()))
4869 .collect::<PyResult<_>>()?;
4870 let refs: Vec<(&nucleide_material::Material, f64)> =
4871 mats.iter().zip(parts.iter().map(|(_, r)| *r)).collect();
4872 let out = nucleide_material::Material::blend(&refs)
4873 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4874 Ok(out.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect())
4875}
4876
4877#[pyclass(name = "Cusum")]
4885struct PyCusum {
4886 inner: nucleide_material::Cusum,
4887}
4888
4889#[pymethods]
4890impl PyCusum {
4891 #[new]
4894 #[pyo3(signature = (ref_shift_k=0.5, alarm_h=4.0, startup=10))]
4895 fn new(ref_shift_k: f64, alarm_h: f64, startup: usize) -> PyResult<Self> {
4896 nucleide_material::Cusum::new(ref_shift_k, alarm_h, startup)
4897 .map(|inner| Self { inner })
4898 .map_err(|e| PyValueError::new_err(e.to_string()))
4899 }
4900
4901 fn update(&mut self, x: f64) -> bool {
4903 self.inner.update(x)
4904 }
4905
4906 fn status(&self) -> bool {
4908 self.inner.status()
4909 }
4910
4911 fn statistic(&self) -> f64 {
4913 self.inner.statistic()
4914 }
4915
4916 fn count(&self) -> usize {
4918 self.inner.count()
4919 }
4920
4921 fn mean(&self) -> f64 {
4923 self.inner.mean()
4924 }
4925
4926 fn variance(&self) -> f64 {
4928 self.inner.variance()
4929 }
4930
4931 fn std(&self) -> f64 {
4933 self.inner.std()
4934 }
4935
4936 fn reset(&mut self) {
4938 self.inner.reset();
4939 }
4940}
4941
4942#[pyclass(name = "DeckProblem")]
4948struct PyDeckProblem {
4949 inner: std::sync::Mutex<nucleide_mcnp_io::problem::DeckProblem>,
4950}
4951
4952fn deck_cell_dict(cell: &nucleide_mcnp_io::cell::CellCard) -> BTreeMap<String, String> {
4953 let mut d = BTreeMap::new();
4954 d.insert("num".to_string(), cell.num.to_string());
4955 d.insert("mat".to_string(), cell.mat.to_string());
4956 d.insert(
4957 "dens".to_string(),
4958 cell.dens.map(|v| v.to_string()).unwrap_or_default(),
4959 );
4960 d.insert("geom".to_string(), cell.geom.render());
4961 d.insert("params".to_string(), cell.params.join(" "));
4962 d
4963}
4964
4965#[pymethods]
4966impl PyDeckProblem {
4967 #[staticmethod]
4969 fn loads(text: &str) -> PyResult<Self> {
4970 nucleide_mcnp_io::problem::parse_deck(text)
4971 .map(|inner| Self {
4972 inner: std::sync::Mutex::new(inner),
4973 })
4974 .map_err(|e| PyValueError::new_err(e.to_string()))
4975 }
4976
4977 #[getter]
4979 fn message(&self) -> PyResult<String> {
4980 Ok(self
4981 .inner
4982 .lock()
4983 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4984 .message
4985 .clone())
4986 }
4987
4988 #[getter]
4990 fn title(&self) -> PyResult<String> {
4991 Ok(self
4992 .inner
4993 .lock()
4994 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
4995 .title
4996 .clone())
4997 }
4998
4999 #[getter]
5002 fn cells(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5003 Ok(self
5004 .inner
5005 .lock()
5006 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5007 .cells
5008 .iter()
5009 .map(deck_cell_dict)
5010 .collect())
5011 }
5012
5013 #[getter]
5016 fn surfs(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5017 Ok(self
5018 .inner
5019 .lock()
5020 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5021 .surfs
5022 .iter()
5023 .map(|s| {
5024 let mut d = BTreeMap::new();
5025 d.insert("num".to_string(), s.num.to_string());
5026 d.insert("reflecting".to_string(), s.reflecting.to_string());
5027 d.insert(
5028 "transform".to_string(),
5029 s.transform.map(|v| v.to_string()).unwrap_or_default(),
5030 );
5031 d.insert(
5032 "periodic".to_string(),
5033 s.periodic.map(|v| v.to_string()).unwrap_or_default(),
5034 );
5035 d.insert("kind".to_string(), s.kind.keyword().to_string());
5036 d.insert(
5037 "coeffs".to_string(),
5038 s.coeffs
5039 .iter()
5040 .map(|v| v.to_string())
5041 .collect::<Vec<_>>()
5042 .join(" "),
5043 );
5044 d
5045 })
5046 .collect())
5047 }
5048
5049 #[getter]
5051 fn material_numbers(&self) -> PyResult<Vec<u32>> {
5052 Ok(self
5053 .inner
5054 .lock()
5055 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5056 .materials
5057 .iter()
5058 .map(|m| m.number)
5059 .collect())
5060 }
5061
5062 #[getter]
5064 fn data_names(&self) -> PyResult<Vec<String>> {
5065 Ok(self
5066 .inner
5067 .lock()
5068 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5069 .data
5070 .iter()
5071 .map(|d| d.name.clone())
5072 .collect())
5073 }
5074
5075 fn dumps(&self) -> PyResult<String> {
5077 let guard = self
5078 .inner
5079 .lock()
5080 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?;
5081 Ok(nucleide_mcnp_io::problem::write_deck(&guard))
5082 }
5083
5084 fn set_cell_density(&self, cell: u32, dens: f64) -> PyResult<()> {
5086 self.inner
5087 .lock()
5088 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5089 .set_cell_density(cell, dens)
5090 .map_err(|e| PyValueError::new_err(e.to_string()))
5091 }
5092
5093 fn set_cell_material(&self, cell: u32, mat: u32) -> PyResult<()> {
5095 self.inner
5096 .lock()
5097 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5098 .set_cell_material(cell, mat)
5099 .map_err(|e| PyValueError::new_err(e.to_string()))
5100 }
5101
5102 #[getter]
5104 fn mode(&self) -> PyResult<BTreeMap<String, String>> {
5105 let mode = self
5106 .inner
5107 .lock()
5108 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5109 .mode()
5110 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5111 let mut d = BTreeMap::new();
5112 d.insert("particles".to_string(), mode.particles.join(" "));
5113 Ok(d)
5114 }
5115
5116 #[getter]
5119 fn transforms(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5120 let transforms = self
5121 .inner
5122 .lock()
5123 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5124 .transforms()
5125 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5126 Ok(transforms
5127 .iter()
5128 .map(|t| {
5129 let mut d = BTreeMap::new();
5130 d.insert("number".to_string(), t.number.to_string());
5131 d.insert(
5132 "displacement".to_string(),
5133 t.displacement
5134 .iter()
5135 .map(|v| v.to_string())
5136 .collect::<Vec<_>>()
5137 .join(" "),
5138 );
5139 d.insert(
5140 "rotation".to_string(),
5141 t.rotation
5142 .iter()
5143 .map(|v| v.to_string())
5144 .collect::<Vec<_>>()
5145 .join(" "),
5146 );
5147 d.insert("in_degrees".to_string(), t.is_in_degrees.to_string());
5148 d.insert("main_to_aux".to_string(), t.is_main_to_aux.to_string());
5149 d.insert("hidden".to_string(), t.hidden.to_string());
5150 d
5151 })
5152 .collect())
5153 }
5154
5155 #[getter]
5158 fn universes(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5159 let universes = self
5160 .inner
5161 .lock()
5162 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5163 .universes()
5164 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5165 Ok(universes
5166 .iter()
5167 .map(|u| {
5168 let mut d = BTreeMap::new();
5169 d.insert("number".to_string(), u.number.to_string());
5170 d.insert(
5171 "cells".to_string(),
5172 u.cells
5173 .iter()
5174 .map(|v| v.to_string())
5175 .collect::<Vec<_>>()
5176 .join(" "),
5177 );
5178 d.insert(
5179 "not_truncated".to_string(),
5180 u.not_truncated
5181 .iter()
5182 .map(|v| v.to_string())
5183 .collect::<Vec<_>>()
5184 .join(" "),
5185 );
5186 d
5187 })
5188 .collect())
5189 }
5190
5191 #[getter]
5193 fn lattices(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5194 let lattices = self
5195 .inner
5196 .lock()
5197 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5198 .lattices()
5199 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5200 Ok(lattices
5201 .iter()
5202 .map(|l| {
5203 let mut d = BTreeMap::new();
5204 d.insert("cell".to_string(), l.cell.to_string());
5205 d.insert("lattice".to_string(), l.lattice.to_string());
5206 d
5207 })
5208 .collect())
5209 }
5210
5211 #[getter]
5215 fn fills(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5216 use nucleide_mcnp_io::semantic::{FillTarget, FillTransform};
5217 let fills = self
5218 .inner
5219 .lock()
5220 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5221 .fills()
5222 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5223 Ok(fills
5224 .iter()
5225 .map(|f| {
5226 let mut d = BTreeMap::new();
5227 d.insert("cell".to_string(), f.cell.to_string());
5228 match &f.target {
5229 FillTarget::Single(u) => {
5230 d.insert("kind".to_string(), "single".to_string());
5231 d.insert("universe".to_string(), u.to_string());
5232 d.insert("min_index".to_string(), String::new());
5233 d.insert("max_index".to_string(), String::new());
5234 d.insert("universes".to_string(), String::new());
5235 }
5236 FillTarget::Matrix {
5237 min_index,
5238 max_index,
5239 universes,
5240 } => {
5241 d.insert("kind".to_string(), "matrix".to_string());
5242 d.insert("universe".to_string(), String::new());
5243 d.insert(
5244 "min_index".to_string(),
5245 min_index
5246 .iter()
5247 .map(|v| v.to_string())
5248 .collect::<Vec<_>>()
5249 .join(" "),
5250 );
5251 d.insert(
5252 "max_index".to_string(),
5253 max_index
5254 .iter()
5255 .map(|v| v.to_string())
5256 .collect::<Vec<_>>()
5257 .join(" "),
5258 );
5259 d.insert(
5260 "universes".to_string(),
5261 universes
5262 .iter()
5263 .map(|u| {
5264 u.map(|v| v.to_string()).unwrap_or_else(|| "-".to_string())
5265 })
5266 .collect::<Vec<_>>()
5267 .join(" "),
5268 );
5269 }
5270 }
5271 match &f.transform {
5272 None => {
5273 d.insert("transform".to_string(), String::new());
5274 d.insert("hidden_transform".to_string(), String::new());
5275 }
5276 Some(FillTransform::Reference(n)) => {
5277 d.insert("transform".to_string(), n.to_string());
5278 d.insert("hidden_transform".to_string(), String::new());
5279 }
5280 Some(FillTransform::Hidden(t)) => {
5281 d.insert("transform".to_string(), String::new());
5282 let mut coords: Vec<String> =
5283 t.displacement.iter().map(|v| v.to_string()).collect();
5284 coords.extend(t.rotation.iter().map(|v| v.to_string()));
5285 d.insert("hidden_transform".to_string(), coords.join(" "));
5286 }
5287 }
5288 d.insert("in_degrees".to_string(), f.in_degrees.to_string());
5289 d
5290 })
5291 .collect())
5292 }
5293
5294 #[getter]
5296 fn importances(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5297 let importances = self
5298 .inner
5299 .lock()
5300 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5301 .importances()
5302 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5303 Ok(importances
5304 .iter()
5305 .map(|v| {
5306 let mut d = BTreeMap::new();
5307 d.insert("cell".to_string(), v.cell.to_string());
5308 d.insert("particle".to_string(), v.particle.clone());
5309 d.insert("value".to_string(), v.value.to_string());
5310 d
5311 })
5312 .collect())
5313 }
5314
5315 #[getter]
5317 fn volumes(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5318 let volumes = self
5319 .inner
5320 .lock()
5321 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5322 .volumes()
5323 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5324 Ok(volumes
5325 .iter()
5326 .map(|v| {
5327 let mut d = BTreeMap::new();
5328 d.insert("cell".to_string(), v.cell.to_string());
5329 d.insert("volume".to_string(), v.volume.to_string());
5330 d
5331 })
5332 .collect())
5333 }
5334
5335 #[getter]
5338 fn tallies(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5339 let tallies = self
5340 .inner
5341 .lock()
5342 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5343 .tallies()
5344 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5345 Ok(tallies
5346 .iter()
5347 .map(|t| {
5348 let mut d = BTreeMap::new();
5349 d.insert("number".to_string(), t.number.to_string());
5350 d.insert("type".to_string(), t.tally_type.to_string());
5351 d.insert("particles".to_string(), t.particles.join(","));
5352 d.insert("entries".to_string(), t.entries.join(" "));
5353 d.insert("fm".to_string(), t.fm.clone().unwrap_or_default().join(" "));
5354 d.insert(
5355 "e_bins".to_string(),
5356 t.e_bins.clone().unwrap_or_default().join(" "),
5357 );
5358 d
5359 })
5360 .collect())
5361 }
5362
5363 #[getter]
5366 fn sdef(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
5367 let guard = self
5368 .inner
5369 .lock()
5370 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?;
5371 let sdef = guard
5372 .sdef()
5373 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5374 sdef.map(|s| sdef_to_py(py, &s)).transpose()
5375 }
5376
5377 fn validate(&self) -> PyResult<()> {
5380 self.inner
5381 .lock()
5382 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5383 .validate()
5384 .map_err(|e| PyValueError::new_err(e.to_string()))
5385 }
5386
5387 fn validation_notes(&self) -> PyResult<Vec<String>> {
5389 Ok(self
5390 .inner
5391 .lock()
5392 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5393 .validation_notes())
5394 }
5395
5396 fn set_mode(&self, particles: Vec<String>) -> PyResult<()> {
5398 self.inner
5399 .lock()
5400 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5401 .set_mode(particles)
5402 .map_err(|e| PyValueError::new_err(e.to_string()))
5403 }
5404
5405 fn set_cell_universe(&self, cell: u32, universe: u32, not_truncated: bool) -> PyResult<()> {
5407 self.inner
5408 .lock()
5409 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5410 .set_cell_universe(cell, universe, not_truncated)
5411 .map_err(|e| PyValueError::new_err(e.to_string()))
5412 }
5413
5414 fn set_cell_lattice(&self, cell: u32, lattice: Option<u8>) -> PyResult<()> {
5416 self.inner
5417 .lock()
5418 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5419 .set_cell_lattice(cell, lattice)
5420 .map_err(|e| PyValueError::new_err(e.to_string()))
5421 }
5422
5423 fn set_cell_fill(&self, cell: u32, universe: u32) -> PyResult<()> {
5425 self.inner
5426 .lock()
5427 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5428 .set_cell_fill(cell, universe)
5429 .map_err(|e| PyValueError::new_err(e.to_string()))
5430 }
5431}
5432
5433#[pyfunction]
5435fn read_deck(path: &str) -> PyResult<PyDeckProblem> {
5436 nucleide_mcnp_io::problem::parse_deck_file(path)
5437 .map(|inner| PyDeckProblem {
5438 inner: std::sync::Mutex::new(inner),
5439 })
5440 .map_err(|e| PyValueError::new_err(e.to_string()))
5441}
5442
5443#[pyfunction]
5445fn parse_deck(text: &str) -> PyResult<PyDeckProblem> {
5446 PyDeckProblem::loads(text)
5447}
5448
5449fn sdef_to_py(py: Python<'_>, sdef: &nucleide_mcnp_io::sdef::SdefProblem) -> PyResult<Py<PyAny>> {
5458 use pyo3::types::PyDict;
5459 let opt3 = |v: &Option<nucleide_mcnp_io::sdef::SdefRef<[f64; 3]>>| {
5460 v.as_ref().map(|r| r.render()).unwrap_or_default()
5461 };
5462 let opt1 = |v: &Option<nucleide_mcnp_io::sdef::SdefRef<f64>>| {
5463 v.as_ref().map(|r| r.render()).unwrap_or_default()
5464 };
5465 let optu = |v: &Option<nucleide_mcnp_io::sdef::SdefRef<u32>>| {
5466 v.as_ref().map(|r| r.render()).unwrap_or_default()
5467 };
5468 let d = PyDict::new(py);
5469 d.set_item("pos", opt3(&sdef.card.pos))?;
5470 d.set_item("cell", optu(&sdef.card.cell))?;
5471 d.set_item("surf", optu(&sdef.card.surf))?;
5472 d.set_item("vec", opt3(&sdef.card.vec))?;
5473 d.set_item("dir", opt1(&sdef.card.dir))?;
5474 d.set_item("axs", opt3(&sdef.card.axs))?;
5475 d.set_item("rad", opt1(&sdef.card.rad))?;
5476 d.set_item("ext", opt1(&sdef.card.ext))?;
5477 d.set_item("erg", opt1(&sdef.card.erg))?;
5478 d.set_item("nrm", opt1(&sdef.card.nrm))?;
5479 d.set_item(
5480 "par",
5481 sdef.card
5482 .par
5483 .as_ref()
5484 .map(|r| r.render())
5485 .unwrap_or_default(),
5486 )?;
5487 d.set_item("wgt", opt1(&sdef.card.wgt))?;
5488 d.set_item("tme", opt1(&sdef.card.tme))?;
5489 d.set_item("ignored", sdef.card.ignored.clone())?;
5490 let dists: Vec<Py<PyAny>> = sdef
5491 .dists
5492 .iter()
5493 .map(|dist| {
5494 let m = PyDict::new(py);
5495 m.set_item("number", dist.number.to_string())?;
5496 m.set_item("si_option", "L")?;
5497 m.set_item("si", dist.si_text())?;
5498 m.set_item(
5499 "sp_option",
5500 dist.sp.as_ref().map(|_| "D").unwrap_or_default(),
5501 )?;
5502 m.set_item("sp", dist.sp_text())?;
5503 m.set_item(
5504 "sb_option",
5505 dist.sb.as_ref().map(|_| "D").unwrap_or_default(),
5506 )?;
5507 m.set_item("sb", dist.sb_text())?;
5508 Ok(m.into_any().unbind())
5509 })
5510 .collect::<PyResult<Vec<_>>>()?;
5511 d.set_item("distributions", dists)?;
5512 d.set_item("card", sdef.emit())?;
5513 Ok(d.into_any().unbind())
5514}
5515
5516#[pyfunction]
5522fn parse_sdef(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
5523 let sdef = nucleide_mcnp_io::sdef::parse_sdef_text(text)
5524 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5525 sdef_to_py(py, &sdef)
5526}
5527
5528fn csg_to_openmc_inner(
5535 deck: &nucleide_mcnp_io::problem::DeckProblem,
5536) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5537 let (xml, table) = nucleide_csg_xlate::deck_csg_to_openmc_xml(deck)
5538 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5539 Ok((
5540 xml,
5541 table
5542 .entries
5543 .into_iter()
5544 .map(|e| {
5545 let mut d = BTreeMap::new();
5546 d.insert("scope".to_string(), e.scope.to_string());
5547 d.insert("target".to_string(), e.target.to_string());
5548 d.insert("action".to_string(), e.action);
5549 d.insert("reason".to_string(), e.reason);
5550 d
5551 })
5552 .collect(),
5553 ))
5554}
5555
5556#[pyfunction]
5559fn parse_csg_to_openmc(text: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5560 let deck = nucleide_mcnp_io::problem::parse_deck(text)
5561 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5562 csg_to_openmc_inner(&deck)
5563}
5564
5565#[pyfunction]
5568fn read_csg_to_openmc(path: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5569 let deck = nucleide_mcnp_io::problem::parse_deck_file(path)
5570 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5571 csg_to_openmc_inner(&deck)
5572}
5573
5574fn csg_to_serpent_inner(
5581 deck: &nucleide_mcnp_io::problem::DeckProblem,
5582) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5583 let (text, table) = nucleide_csg_xlate::deck_csg_to_serpent_input(deck)
5584 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5585 Ok((
5586 text,
5587 table
5588 .entries
5589 .into_iter()
5590 .map(|e| {
5591 let mut d = BTreeMap::new();
5592 d.insert("scope".to_string(), e.scope.to_string());
5593 d.insert("target".to_string(), e.target.to_string());
5594 d.insert("action".to_string(), e.action);
5595 d.insert("reason".to_string(), e.reason);
5596 d
5597 })
5598 .collect(),
5599 ))
5600}
5601
5602#[pyfunction]
5605fn parse_csg_to_serpent(text: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5606 let deck = nucleide_mcnp_io::problem::parse_deck(text)
5607 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5608 csg_to_serpent_inner(&deck)
5609}
5610
5611#[pyfunction]
5614fn read_csg_to_serpent(path: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5615 let deck = nucleide_mcnp_io::problem::parse_deck_file(path)
5616 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5617 csg_to_serpent_inner(&deck)
5618}
5619
5620fn csg_to_phits_inner(
5628 deck: &nucleide_mcnp_io::problem::DeckProblem,
5629) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5630 let (text, table) = nucleide_csg_xlate::deck_csg_to_phits_input(deck)
5631 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5632 Ok((
5633 text,
5634 table
5635 .entries
5636 .into_iter()
5637 .map(|e| {
5638 let mut d = BTreeMap::new();
5639 d.insert("scope".to_string(), e.scope.to_string());
5640 d.insert("target".to_string(), e.target.to_string());
5641 d.insert("action".to_string(), e.action);
5642 d.insert("reason".to_string(), e.reason);
5643 d
5644 })
5645 .collect(),
5646 ))
5647}
5648
5649#[pyfunction]
5652fn parse_csg_to_phits(text: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5653 let deck = nucleide_mcnp_io::problem::parse_deck(text)
5654 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5655 csg_to_phits_inner(&deck)
5656}
5657
5658#[pyfunction]
5661fn read_csg_to_phits(path: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5662 let deck = nucleide_mcnp_io::problem::parse_deck_file(path)
5663 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5664 csg_to_phits_inner(&deck)
5665}
5666
5667fn csg_to_gdml_inner(
5676 deck: &nucleide_mcnp_io::problem::DeckProblem,
5677) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5678 let (xml, table) = nucleide_csg_xlate::deck_csg_to_gdml(deck)
5679 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5680 Ok((
5681 xml,
5682 table
5683 .entries
5684 .into_iter()
5685 .map(|e| {
5686 let mut d = BTreeMap::new();
5687 d.insert("scope".to_string(), e.scope.to_string());
5688 d.insert("target".to_string(), e.target.to_string());
5689 d.insert("action".to_string(), e.action);
5690 d.insert("reason".to_string(), e.reason);
5691 d
5692 })
5693 .collect(),
5694 ))
5695}
5696
5697#[pyfunction]
5700fn parse_csg_to_gdml(text: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5701 let deck = nucleide_mcnp_io::problem::parse_deck(text)
5702 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5703 csg_to_gdml_inner(&deck)
5704}
5705
5706#[pyfunction]
5709fn read_csg_to_gdml(path: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5710 let deck = nucleide_mcnp_io::problem::parse_deck_file(path)
5711 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5712 csg_to_gdml_inner(&deck)
5713}
5714
5715#[pyclass(name = "Inventory")]
5717struct PyInventory {
5718 chain: std::sync::Arc<nucleide_depletion::Chain>,
5719 atoms: BTreeMap<String, f64>,
5720}
5721
5722fn inventory_sys(
5723 chain: &nucleide_depletion::Chain,
5724 rates: &RateMap,
5725) -> PyResult<nucleide_depletion::DepletionSystem> {
5726 let rs = split_rates(rates, chain)?;
5727 nucleide_depletion::DepletionSystem::build(chain.clone(), &rs)
5728 .map_err(|e| PyValueError::new_err(e.to_string()))
5729}
5730
5731fn parse_quantity_unit(unit: &str) -> PyResult<nucleide_depletion::QuantityUnit> {
5732 nucleide_depletion::QuantityUnit::from_str(unit)
5733 .map_err(|e| PyValueError::new_err(format!("{e:?}")))
5734}
5735
5736#[pymethods]
5737impl PyInventory {
5738 #[new]
5741 #[pyo3(signature = (chain, comp, units="atoms"))]
5742 fn new(chain: &PyChain, comp: BTreeMap<String, f64>, units: &str) -> PyResult<Self> {
5743 let unit = parse_quantity_unit(units)?;
5744 let sys = inventory_sys(&chain.inner, &BTreeMap::new())?;
5745 let inv = nucleide_depletion::DecayInventory::from_units(&comp, unit, &sys)
5746 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5747 Ok(Self {
5748 chain: chain.inner.clone(),
5749 atoms: inv.atoms,
5750 })
5751 }
5752
5753 fn numbers(&self) -> BTreeMap<String, f64> {
5755 self.atoms.clone()
5756 }
5757
5758 #[pyo3(signature = (dt, time_unit="s", rates=None, order=48, method="cram48"))]
5765 fn decay(
5766 &self,
5767 dt: f64,
5768 time_unit: &str,
5769 rates: Option<RateMap>,
5770 order: u8,
5771 method: &str,
5772 ) -> PyResult<Self> {
5773 let method = resolve_method(order, method)?;
5774 let unit = nucleide_depletion::inventory::time_unit_from_str(time_unit)
5775 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5776 let seconds = dt * unit.as_seconds();
5777 let empty = BTreeMap::new();
5778 let step_rates = rates.as_ref().unwrap_or(&empty);
5779 let template = inventory_sys(&self.chain, step_rates)?;
5780 let steps = vec![nucleide_depletion::Step::new(
5783 seconds,
5784 split_rates(step_rates, &self.chain)?,
5785 )];
5786 let series = nucleide_depletion::integrate_with_method(
5787 &template,
5788 &chain_vec(&self.chain, &self.atoms)?,
5789 &steps,
5790 nucleide_depletion::Integrator::Predictor,
5791 method,
5792 )
5793 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5794 let names: Vec<String> = self.chain.nuclides.iter().map(|n| n.name.clone()).collect();
5795 let atoms = names
5796 .iter()
5797 .zip(series.atoms.last().cloned().unwrap_or_default())
5798 .map(|(n, v)| (n.clone(), v))
5799 .collect();
5800 Ok(Self {
5801 chain: self.chain.clone(),
5802 atoms,
5803 })
5804 }
5805
5806 fn activities(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
5808 let unit = parse_quantity_unit(units)?;
5809 let sys = inventory_sys(&self.chain, &BTreeMap::new())?;
5810 let inv = nucleide_depletion::DecayInventory {
5811 atoms: self.atoms.clone(),
5812 };
5813 inv.activities(&sys, unit)
5814 .map_err(|e| PyValueError::new_err(e.to_string()))
5815 }
5816
5817 fn masses(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
5819 let unit = parse_quantity_unit(units)?;
5820 let inv = nucleide_depletion::DecayInventory {
5821 atoms: self.atoms.clone(),
5822 };
5823 inv.masses(unit)
5824 .map_err(|e| PyValueError::new_err(e.to_string()))
5825 }
5826
5827 fn moles(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
5829 let unit = parse_quantity_unit(units)?;
5830 let inv = nucleide_depletion::DecayInventory {
5831 atoms: self.atoms.clone(),
5832 };
5833 inv.moles(unit)
5834 .map_err(|e| PyValueError::new_err(e.to_string()))
5835 }
5836
5837 fn activity_fractions(&self) -> PyResult<BTreeMap<String, f64>> {
5839 let sys = inventory_sys(&self.chain, &BTreeMap::new())?;
5840 let inv = nucleide_depletion::DecayInventory {
5841 atoms: self.atoms.clone(),
5842 };
5843 inv.activity_fractions(&sys)
5844 .map_err(|e| PyValueError::new_err(e.to_string()))
5845 }
5846
5847 fn mass_fractions(&self) -> PyResult<BTreeMap<String, f64>> {
5849 let inv = nucleide_depletion::DecayInventory {
5850 atoms: self.atoms.clone(),
5851 };
5852 inv.mass_fractions()
5853 .map_err(|e| PyValueError::new_err(e.to_string()))
5854 }
5855
5856 fn mole_fractions(&self) -> BTreeMap<String, f64> {
5858 nucleide_depletion::DecayInventory {
5859 atoms: self.atoms.clone(),
5860 }
5861 .mole_fractions()
5862 }
5863
5864 fn half_lives_readable(&self) -> BTreeMap<String, String> {
5866 nucleide_depletion::DecayInventory {
5867 atoms: self.atoms.clone(),
5868 }
5869 .half_lives_readable()
5870 }
5871
5872 fn add(&self, other: &Self) -> Self {
5874 let a = nucleide_depletion::DecayInventory {
5875 atoms: self.atoms.clone(),
5876 };
5877 let b = nucleide_depletion::DecayInventory {
5878 atoms: other.atoms.clone(),
5879 };
5880 Self {
5881 chain: self.chain.clone(),
5882 atoms: a.add(&b).atoms,
5883 }
5884 }
5885
5886 fn sub(&self, other: &Self) -> Self {
5888 let a = nucleide_depletion::DecayInventory {
5889 atoms: self.atoms.clone(),
5890 };
5891 let b = nucleide_depletion::DecayInventory {
5892 atoms: other.atoms.clone(),
5893 };
5894 Self {
5895 chain: self.chain.clone(),
5896 atoms: a.sub(&b).atoms,
5897 }
5898 }
5899
5900 fn mul(&self, scalar: f64) -> Self {
5902 let a = nucleide_depletion::DecayInventory {
5903 atoms: self.atoms.clone(),
5904 };
5905 Self {
5906 chain: self.chain.clone(),
5907 atoms: a.mul(scalar).atoms,
5908 }
5909 }
5910
5911 fn div(&self, scalar: f64) -> Self {
5913 let a = nucleide_depletion::DecayInventory {
5914 atoms: self.atoms.clone(),
5915 };
5916 Self {
5917 chain: self.chain.clone(),
5918 atoms: a.div(scalar).atoms,
5919 }
5920 }
5921
5922 fn to_csv(&self) -> String {
5924 nucleide_depletion::DecayInventory {
5925 atoms: self.atoms.clone(),
5926 }
5927 .to_csv()
5928 }
5929
5930 #[staticmethod]
5932 fn from_csv(chain: &PyChain, text: &str) -> PyResult<Self> {
5933 let inv = nucleide_depletion::DecayInventory::from_csv(text)
5935 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5936 for name in inv.atoms.keys() {
5937 if chain.inner.index_of(name).is_none() {
5938 return Err(PyValueError::new_err(format!(
5939 "unknown nuclide `{name}` for this chain"
5940 )));
5941 }
5942 }
5943 Ok(Self {
5944 chain: chain.inner.clone(),
5945 atoms: inv.atoms,
5946 })
5947 }
5948}
5949
5950fn chain_vec(
5952 chain: &nucleide_depletion::Chain,
5953 atoms: &BTreeMap<String, f64>,
5954) -> PyResult<Vec<f64>> {
5955 let mut vec = vec![0.0; chain.len()];
5956 for (name, value) in atoms {
5957 let idx = chain.index_of(name).ok_or_else(|| {
5958 PyValueError::new_err(format!("unknown nuclide `{name}` for this chain"))
5959 })?;
5960 vec[idx] = *value;
5961 }
5962 Ok(vec)
5963}
5964
5965#[pyfunction]
5967#[pyo3(signature = (chain, n0, dt, rates=None))]
5968fn cumulative_decays(
5969 chain: &PyChain,
5970 n0: BTreeMap<String, f64>,
5971 dt: f64,
5972 rates: Option<RateMap>,
5973) -> PyResult<BTreeMap<String, f64>> {
5974 let empty = BTreeMap::new();
5975 let sys = inventory_sys(&chain.inner, rates.as_ref().unwrap_or(&empty))?;
5976 let vec = chain_vec(&chain.inner, &n0)?;
5977 let out = nucleide_depletion::cumulative_decays(&sys, &vec, dt)
5978 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5979 Ok(chain
5980 .inner
5981 .nuclides
5982 .iter()
5983 .zip(out)
5984 .map(|(nuc, v)| (nuc.name.clone(), v))
5985 .collect())
5986}
5987
5988#[pyfunction]
5990fn progeny(chain: &PyChain, name: &str) -> Vec<(String, f64, String)> {
5991 nucleide_depletion::progeny(&chain.inner, name)
5992}
5993
5994#[pyfunction]
5996fn branching_fraction(chain: &PyChain, parent: &str, child: &str) -> Option<f64> {
5997 nucleide_depletion::branching_fraction(&chain.inner, parent, child)
5998}
5999
6000#[pyfunction]
6002fn decay_mode(chain: &PyChain, parent: &str, child: &str) -> Option<String> {
6003 nucleide_depletion::decay_mode(&chain.inner, parent, child)
6004}
6005
6006#[pyfunction]
6008fn chain_edges(chain: &PyChain) -> Vec<(String, String, f64, String)> {
6009 nucleide_depletion::chain_edges(&chain.inner)
6010}
6011
6012#[pyfunction]
6014fn armi_to_nucid(name: &str) -> PyResult<PyNuclide> {
6015 nucleide_nuclei::armi::armi_name_to_nucid(name)
6016 .map(|inner| PyNuclide { inner })
6017 .map_err(|e| PyValueError::new_err(e.to_string()))
6018}
6019
6020#[pyfunction]
6022fn nucid_to_armi(nuclide: &PyNuclide) -> String {
6023 nucleide_nuclei::armi::nucid_to_armi_label(nuclide.inner)
6024}
6025
6026#[pyfunction]
6028fn mcc3_to_nucid(name: &str) -> PyResult<PyNuclide> {
6029 nucleide_nuclei::armi::mcc3_to_nucid(name)
6030 .map(|inner| PyNuclide { inner })
6031 .map_err(|e| PyValueError::new_err(e.to_string()))
6032}
6033
6034#[pyfunction]
6039#[pyo3(signature = (comp, widths=None))]
6040fn check_labels(
6041 comp: BTreeMap<String, f64>,
6042 widths: Option<Vec<usize>>,
6043) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
6044 let mat = comp_to_material(comp)?;
6045 let widths = widths.unwrap_or_else(|| nucleide_material::DEFAULT_WIDTHS.to_vec());
6046 let collisions = nucleide_material::check_labels(&mat, &widths);
6047 Python::attach(|py| {
6048 Ok(collisions
6049 .into_iter()
6050 .map(|c| {
6051 let mut d = BTreeMap::new();
6052 d.insert(
6053 "truncated".to_string(),
6054 c.truncated.into_pyobject(py).unwrap().unbind().into_any(),
6055 );
6056 d.insert(
6057 "width".to_string(),
6058 c.width.into_pyobject(py).unwrap().unbind().into_any(),
6059 );
6060 let members: Vec<String> = c.members.iter().map(|id| id.to_name()).collect();
6061 d.insert(
6062 "members".to_string(),
6063 members.into_pyobject(py).unwrap().unbind().into_any(),
6064 );
6065 d
6066 })
6067 .collect())
6068 })
6069}
6070
6071#[pyfunction]
6073fn audit_material(comp: BTreeMap<String, f64>) -> PyResult<Vec<BTreeMap<String, String>>> {
6074 let mat = comp_to_material(comp)?;
6075 Ok(nucleide_material::audit(&mat, &nucleide_material::Ame2020)
6076 .into_iter()
6077 .map(|issue| {
6078 let mut d = BTreeMap::new();
6079 d.insert("kind".to_string(), format!("{:?}", issue.kind));
6080 d.insert("detail".to_string(), issue.detail);
6081 d
6082 })
6083 .collect())
6084}
6085
6086#[pyfunction]
6093#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
6094#[allow(clippy::too_many_arguments)]
6095fn emit_cards(
6096 comp: BTreeMap<String, f64>,
6097 name: &str,
6098 density: Option<f64>,
6099 mcnp_number: u32,
6100 xs_suffix: &str,
6101 serpent_lib: &str,
6102 fluka_fid: u32,
6103 partisn_zone: u32,
6104) -> PyResult<BTreeMap<String, String>> {
6105 let (emitted, _) = emit_drift_inner(
6106 comp,
6107 name,
6108 density,
6109 mcnp_number,
6110 xs_suffix,
6111 serpent_lib,
6112 fluka_fid,
6113 partisn_zone,
6114 )?;
6115 Ok(emitted
6116 .into_iter()
6117 .map(|e| (e.code.to_string(), e.text))
6118 .collect())
6119}
6120
6121#[pyfunction]
6125#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
6126#[allow(clippy::too_many_arguments)]
6127fn emit_drift_table(
6128 comp: BTreeMap<String, f64>,
6129 name: &str,
6130 density: Option<f64>,
6131 mcnp_number: u32,
6132 xs_suffix: &str,
6133 serpent_lib: &str,
6134 fluka_fid: u32,
6135 partisn_zone: u32,
6136) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
6137 let (_, table) = emit_drift_inner(
6138 comp,
6139 name,
6140 density,
6141 mcnp_number,
6142 xs_suffix,
6143 serpent_lib,
6144 fluka_fid,
6145 partisn_zone,
6146 )?;
6147 drift_table_to_py(table)
6148}
6149
6150fn drift_table_to_py(
6151 table: nucleide_emit::DriftTable,
6152) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
6153 Python::attach(|py| {
6154 Ok(table
6155 .rows
6156 .into_iter()
6157 .map(|r| {
6158 let mut d = BTreeMap::new();
6159 d.insert(
6160 "code".to_string(),
6161 r.code
6162 .to_string()
6163 .into_pyobject(py)
6164 .unwrap()
6165 .unbind()
6166 .into_any(),
6167 );
6168 d.insert(
6169 "mass_in".to_string(),
6170 r.mass_in.into_pyobject(py).unwrap().unbind().into_any(),
6171 );
6172 d.insert(
6173 "mass_out".to_string(),
6174 r.mass_out.into_pyobject(py).unwrap().unbind().into_any(),
6175 );
6176 d.insert(
6177 "rel_drift".to_string(),
6178 r.rel_drift.into_pyobject(py).unwrap().unbind().into_any(),
6179 );
6180 let dropped: Vec<BTreeMap<String, Py<PyAny>>> = r
6181 .dropped
6182 .into_iter()
6183 .map(|x| {
6184 let mut dd = BTreeMap::new();
6185 dd.insert(
6186 "nuclide".to_string(),
6187 x.id.to_name()
6188 .into_pyobject(py)
6189 .unwrap()
6190 .unbind()
6191 .into_any(),
6192 );
6193 dd.insert(
6194 "mass".to_string(),
6195 x.mass.into_pyobject(py).unwrap().unbind().into_any(),
6196 );
6197 dd.insert(
6198 "reason".to_string(),
6199 x.reason.into_pyobject(py).unwrap().unbind().into_any(),
6200 );
6201 dd
6202 })
6203 .collect();
6204 d.insert(
6205 "dropped".to_string(),
6206 dropped.into_pyobject(py).unwrap().unbind().into_any(),
6207 );
6208 d.insert(
6209 "reparsed".to_string(),
6210 pyo3::types::PyBool::new(py, r.reparsed)
6211 .to_owned()
6212 .into_any()
6213 .unbind(),
6214 );
6215 d
6216 })
6217 .collect())
6218 })
6219}
6220
6221#[allow(clippy::too_many_arguments)]
6222fn emit_drift_inner(
6223 comp: BTreeMap<String, f64>,
6224 name: &str,
6225 density: Option<f64>,
6226 mcnp_number: u32,
6227 xs_suffix: &str,
6228 serpent_lib: &str,
6229 fluka_fid: u32,
6230 partisn_zone: u32,
6231) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
6232 let mut mat = comp_to_material(comp)?;
6233 mat.set_density(density);
6234 emit_drift_with_mat(
6235 mat,
6236 name,
6237 mcnp_number,
6238 xs_suffix,
6239 serpent_lib,
6240 fluka_fid,
6241 partisn_zone,
6242 )
6243}
6244
6245#[allow(clippy::too_many_arguments)]
6246fn emit_drift_with_mat(
6247 mat: nucleide_material::Material,
6248 name: &str,
6249 mcnp_number: u32,
6250 xs_suffix: &str,
6251 serpent_lib: &str,
6252 fluka_fid: u32,
6253 partisn_zone: u32,
6254) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
6255 let mut opts = nucleide_emit::EmitOptions::new(name);
6256 opts.mcnp_number = mcnp_number;
6257 opts.xs_suffix = xs_suffix.to_string();
6258 opts.serpent_lib = serpent_lib.to_string();
6259 opts.fluka_fid = fluka_fid;
6260 opts.partisn_zone = partisn_zone;
6261 nucleide_emit::emit_drift(&mat, &opts).map_err(|e| PyValueError::new_err(e.to_string()))
6262}
6263
6264#[allow(clippy::too_many_arguments)]
6265fn emit_armi_drift_inner(
6266 comp: BTreeMap<String, f64>,
6267 name: &str,
6268 density: Option<f64>,
6269 mcnp_number: u32,
6270 xs_suffix: &str,
6271 serpent_lib: &str,
6272 fluka_fid: u32,
6273 partisn_zone: u32,
6274) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
6275 let mat = nucleide_emit::armi::from_armi_mass_fracs(comp, density)
6278 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6279 emit_drift_with_mat(
6280 mat,
6281 name,
6282 mcnp_number,
6283 xs_suffix,
6284 serpent_lib,
6285 fluka_fid,
6286 partisn_zone,
6287 )
6288}
6289
6290#[pyfunction]
6298#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
6299#[allow(clippy::too_many_arguments)]
6300fn emit_armi_cards(
6301 comp: BTreeMap<String, f64>,
6302 name: &str,
6303 density: Option<f64>,
6304 mcnp_number: u32,
6305 xs_suffix: &str,
6306 serpent_lib: &str,
6307 fluka_fid: u32,
6308 partisn_zone: u32,
6309) -> PyResult<BTreeMap<String, String>> {
6310 let (emitted, _) = emit_armi_drift_inner(
6311 comp,
6312 name,
6313 density,
6314 mcnp_number,
6315 xs_suffix,
6316 serpent_lib,
6317 fluka_fid,
6318 partisn_zone,
6319 )?;
6320 Ok(emitted
6321 .into_iter()
6322 .map(|e| (e.code.to_string(), e.text))
6323 .collect())
6324}
6325
6326#[pyfunction]
6330#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
6331#[allow(clippy::too_many_arguments)]
6332fn emit_armi_drift_table(
6333 comp: BTreeMap<String, f64>,
6334 name: &str,
6335 density: Option<f64>,
6336 mcnp_number: u32,
6337 xs_suffix: &str,
6338 serpent_lib: &str,
6339 fluka_fid: u32,
6340 partisn_zone: u32,
6341) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
6342 let (_, table) = emit_armi_drift_inner(
6343 comp,
6344 name,
6345 density,
6346 mcnp_number,
6347 xs_suffix,
6348 serpent_lib,
6349 fluka_fid,
6350 partisn_zone,
6351 )?;
6352 drift_table_to_py(table)
6353}
6354
6355fn parse_reactivity(
6368 spec: &BTreeMap<String, Py<PyAny>>,
6369 py: Python<'_>,
6370) -> PyResult<nucleide_kinetics::Reactivity> {
6371 use nucleide_kinetics::Reactivity as R;
6372 let kind: String = get_str(spec, py, "kind", "reactivity spec needs a `kind`")?;
6373 let num = |key: &str| -> PyResult<f64> { get_num(spec, py, key) };
6374 let vec = |key: &str| -> PyResult<Vec<f64>> { get_vec(spec, py, key) };
6375 let r = match kind.as_str() {
6376 "constant" => R::Constant { rho: num("rho")? },
6377 "step" => R::Step {
6378 t_step: num("t_step")?,
6379 rho_init: num("rho_init")?,
6380 rho_final: num("rho_final")?,
6381 },
6382 "impulse" => R::Impulse {
6383 t_start: num("t_start")?,
6384 t_end: num("t_end")?,
6385 rho_init: num("rho_init")?,
6386 rho_max: num("rho_max")?,
6387 },
6388 "ramp" => R::Ramp {
6389 t_start: num("t_start")?,
6390 t_end: num("t_end")?,
6391 rho_init: num("rho_init")?,
6392 rho_rise: num("rho_rise")?,
6393 rho_final: num("rho_final")?,
6394 },
6395 "polyline" => R::Polyline {
6396 times: vec("times")?,
6397 values: vec("values")?,
6398 },
6399 other => {
6400 return Err(PyValueError::new_err(format!(
6401 "unknown reactivity kind `{other}` (supported: constant, step, impulse, ramp, polyline)"
6402 )))
6403 }
6404 };
6405 r.validate()
6406 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6407 Ok(r)
6408}
6409
6410fn get_str(
6411 spec: &BTreeMap<String, Py<PyAny>>,
6412 py: Python<'_>,
6413 key: &str,
6414 missing: &str,
6415) -> PyResult<String> {
6416 spec.get(key)
6417 .ok_or_else(|| PyValueError::new_err(missing.to_string()))?
6418 .extract::<String>(py)
6419 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a string")))
6420}
6421
6422fn get_num(spec: &BTreeMap<String, Py<PyAny>>, py: Python<'_>, key: &str) -> PyResult<f64> {
6423 spec.get(key)
6424 .ok_or_else(|| PyValueError::new_err(format!("reactivity spec missing `{key}`")))?
6425 .extract::<f64>(py)
6426 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
6427}
6428
6429fn get_vec(spec: &BTreeMap<String, Py<PyAny>>, py: Python<'_>, key: &str) -> PyResult<Vec<f64>> {
6430 spec.get(key)
6431 .ok_or_else(|| PyValueError::new_err(format!("reactivity spec missing `{key}`")))?
6432 .extract::<Vec<f64>>(py)
6433 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a list of numbers")))
6434}
6435
6436fn kinetics_params(
6437 betas: Vec<f64>,
6438 lambdas: Vec<f64>,
6439 lambda_gen: f64,
6440) -> PyResult<nucleide_kinetics::KineticParams> {
6441 nucleide_kinetics::KineticParams::new(betas, lambdas, lambda_gen)
6442 .map_err(|e| PyValueError::new_err(e.to_string()))
6443}
6444
6445#[pyfunction]
6456#[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))]
6457#[allow(clippy::too_many_arguments)]
6458fn kinetics_solve(
6459 py: Python<'_>,
6460 betas: Vec<f64>,
6461 lambdas: Vec<f64>,
6462 lambda_gen: f64,
6463 rho: BTreeMap<String, Py<PyAny>>,
6464 t: Vec<f64>,
6465 n0: f64,
6466 c0: Option<Vec<f64>>,
6467 method: &str,
6468 rtol: f64,
6469 atol: f64,
6470 dt_min: f64,
6471 dt_max: Option<f64>,
6472 max_steps: usize,
6473) -> PyResult<Py<PyAny>> {
6474 use nucleide_kinetics::{Method as M, SolverOptions};
6475 let params = kinetics_params(betas, lambdas, lambda_gen)?;
6476 let rho = parse_reactivity(&rho, py)?;
6477 let grid =
6478 nucleide_kinetics::TimeGrid::new(t).map_err(|e| PyValueError::new_err(e.to_string()))?;
6479 let state = nucleide_kinetics::State::new(¶ms, n0, c0)
6480 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6481 let method = if method.eq_ignore_ascii_case("trapezoidal") {
6482 M::Trapezoidal
6483 } else if method.eq_ignore_ascii_case("backward_euler") {
6484 M::BackwardEuler
6485 } else {
6486 return Err(PyValueError::new_err(format!(
6487 "unknown kinetics method `{method}` (supported: trapezoidal, backward_euler)"
6488 )));
6489 };
6490 let opts = SolverOptions {
6491 method,
6492 rtol,
6493 atol,
6494 dt_min,
6495 dt_max: dt_max.unwrap_or(f64::INFINITY),
6496 max_steps,
6497 };
6498 let sol = nucleide_kinetics::solve(¶ms, &rho, &grid, &state, &opts)
6499 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6500 use pyo3::types::PyDict;
6501 let out = PyDict::new(py);
6502 out.set_item("times", &sol.times).ok();
6503 out.set_item("n", &sol.n).ok();
6504 out.set_item("C", &sol.c).ok();
6505 out.set_item("n0", sol.initial.n0).ok();
6506 out.set_item("C0", &sol.initial.c0).ok();
6507 Ok(out.into_any().unbind())
6508}
6509
6510#[pyfunction]
6512fn kinetics_equilibrium(
6513 betas: Vec<f64>,
6514 lambdas: Vec<f64>,
6515 lambda_gen: f64,
6516 n0: f64,
6517) -> PyResult<Vec<f64>> {
6518 kinetics_params(betas, lambdas, lambda_gen)?
6519 .equilibrium_precursors(n0)
6520 .map_err(|e| PyValueError::new_err(e.to_string()))
6521}
6522
6523#[pyfunction]
6525#[pyo3(signature = (betas, lambdas, lambda_gen, rho, n0, c0=None))]
6526fn kinetics_initial_rate(
6527 py: Python<'_>,
6528 betas: Vec<f64>,
6529 lambdas: Vec<f64>,
6530 lambda_gen: f64,
6531 rho: BTreeMap<String, Py<PyAny>>,
6532 n0: f64,
6533 c0: Option<Vec<f64>>,
6534) -> PyResult<f64> {
6535 let params = kinetics_params(betas, lambdas, lambda_gen)?;
6536 let rho = parse_reactivity(&rho, py)?;
6537 let state = nucleide_kinetics::State::new(¶ms, n0, c0)
6538 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6539 Ok(nucleide_kinetics::solve::initial_rate(
6540 ¶ms, &rho, &state,
6541 ))
6542}
6543
6544#[pyfunction]
6546fn kinetics_inhour_rho(
6547 betas: Vec<f64>,
6548 lambdas: Vec<f64>,
6549 lambda_gen: f64,
6550 omega: f64,
6551) -> PyResult<f64> {
6552 let params = kinetics_params(betas, lambdas, lambda_gen)?;
6553 nucleide_kinetics::rho_of_omega(¶ms, omega)
6554 .map_err(|e| PyValueError::new_err(e.to_string()))
6555}
6556
6557#[pyfunction]
6559fn kinetics_stable_period(
6560 betas: Vec<f64>,
6561 lambdas: Vec<f64>,
6562 lambda_gen: f64,
6563 rho: f64,
6564) -> PyResult<f64> {
6565 let params = kinetics_params(betas, lambdas, lambda_gen)?;
6566 nucleide_kinetics::stable_period(¶ms, rho).map_err(|e| PyValueError::new_err(e.to_string()))
6567}
6568
6569#[pyfunction]
6574fn kinetics_prompt_jump(
6575 n_before: f64,
6576 rho_before: f64,
6577 rho_after: f64,
6578 beta_total: f64,
6579) -> PyResult<f64> {
6580 nucleide_kinetics::prompt_jump(n_before, rho_before, rho_after, beta_total)
6581 .map_err(|e| PyValueError::new_err(e.to_string()))
6582}
6583
6584#[pyfunction]
6601#[pyo3(signature = (response, rates, guess, tolerance=1e-3, max_iterations=200))]
6602fn unfold_sandii(
6603 py: Python<'_>,
6604 response: Vec<Vec<f64>>,
6605 rates: Vec<f64>,
6606 guess: Vec<f64>,
6607 tolerance: f64,
6608 max_iterations: usize,
6609) -> PyResult<Py<PyAny>> {
6610 let sol = nucleide_unfold::sandii::unfold(&response, &rates, &guess, tolerance, max_iterations)
6611 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6612 use pyo3::types::PyDict;
6613 let out = PyDict::new(py);
6614 out.set_item("spectrum", &sol.spectrum).ok();
6615 out.set_item("rates", &sol.rates).ok();
6616 out.set_item("rate_factors", &sol.rate_factors).ok();
6617 out.set_item("iterations", sol.iterations).ok();
6618 out.set_item("tolerance", sol.tolerance).ok();
6619 out.set_item("max_rel_change", sol.max_rel_change).ok();
6620 Ok(out.into_any().unbind())
6621}
6622
6623#[pyfunction]
6627fn unfold_forward_fold(response: Vec<Vec<f64>>, spectrum: Vec<f64>) -> PyResult<Vec<f64>> {
6628 nucleide_unfold::forward_fold(&response, &spectrum)
6629 .map_err(|e| PyValueError::new_err(e.to_string()))
6630}
6631
6632fn parse_plasma_reaction(name: &str) -> PyResult<nucleide_plasma_source::FusionReaction> {
6638 use nucleide_plasma_source::FusionReaction as R;
6639 match name
6640 .to_ascii_lowercase()
6641 .replace(['-', '_', ' '], "")
6642 .as_str()
6643 {
6644 "dt" => Ok(R::Dt),
6645 "dd" => Ok(R::Dd),
6646 other => Err(PyValueError::new_err(format!(
6647 "unknown fusion reaction `{other}` (supported: dt, dd)"
6648 ))),
6649 }
6650}
6651
6652enum PyPlasmaSource {
6654 Basic(nucleide_plasma_source::PlasmaSourceConfig),
6655 Parametric(nucleide_plasma_source::ParametricPlasmaConfig),
6656}
6657
6658fn parse_plasma_basic_spec(
6660 spec: &BTreeMap<String, Py<PyAny>>,
6661 py: Python<'_>,
6662 kind: &str,
6663) -> PyResult<nucleide_plasma_source::PlasmaSourceConfig> {
6664 use nucleide_plasma_source as ps;
6665 let num = |key: &str| -> PyResult<f64> {
6666 spec.get(key)
6667 .ok_or_else(|| PyValueError::new_err(format!("source spec missing `{key}`")))?
6668 .extract::<f64>(py)
6669 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
6670 };
6671 let reaction = parse_plasma_reaction(&get_str(
6672 spec,
6673 py,
6674 "reaction",
6675 "source spec missing `reaction`",
6676 )?)?;
6677 let model = match kind {
6678 "point" => {
6679 let position: Vec<f64> = spec
6680 .get("position")
6681 .ok_or_else(|| PyValueError::new_err("point source needs `position` [cm]"))?
6682 .extract::<Vec<f64>>(py)
6683 .map_err(|_| PyValueError::new_err("`position` must be a list of numbers"))?;
6684 if position.len() != 3 {
6685 return Err(PyValueError::new_err(
6686 "`position` must have exactly three entries",
6687 ));
6688 }
6689 ps::SourceModel::Point(ps::PointSource {
6690 x_cm: position[0],
6691 y_cm: position[1],
6692 z_cm: position[2],
6693 })
6694 }
6695 "ring" => ps::SourceModel::Ring(ps::RingSource {
6696 radius_cm: num("radius")?,
6697 height_cm: num("height")?,
6698 }),
6699 other => {
6700 return Err(PyValueError::new_err(format!(
6701 "unknown source kind `{other}` (supported: point, ring, parametric)"
6702 )))
6703 }
6704 };
6705 let mut config = ps::PlasmaSourceConfig {
6706 model,
6707 reaction,
6708 ion_temperature_kev: num("ion_temperature_kev")?,
6709 weight: 1.0,
6710 };
6711 if let Some(weight) = spec.get("weight") {
6712 let weight = weight
6713 .extract::<f64>(py)
6714 .map_err(|_| PyValueError::new_err("`weight` must be a number"))?;
6715 config = config.with_weight(weight);
6716 }
6717 config
6718 .validate()
6719 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6720 Ok(config)
6721}
6722
6723fn parse_plasma_parametric_spec(
6736 spec: &BTreeMap<String, Py<PyAny>>,
6737 py: Python<'_>,
6738) -> PyResult<nucleide_plasma_source::ParametricPlasmaConfig> {
6739 use nucleide_plasma_source as ps;
6740 let num = |key: &str| -> PyResult<f64> {
6741 spec.get(key)
6742 .ok_or_else(|| PyValueError::new_err(format!("parametric spec missing `{key}`")))?
6743 .extract::<f64>(py)
6744 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
6745 };
6746 for key in ["fuel", "start_angle", "rotation_angle"] {
6747 if spec.contains_key(key) {
6748 return Err(PyValueError::new_err(format!(
6749 "plasma-source: not yet supported: `{key}` (fuel mixtures are \
6750 Eriksson-weighted reactant distributions; sectors need a \
6751 toroidal-angle distribution — both outside the parametric model)"
6752 )));
6753 }
6754 }
6755 let mode = ps::ProfileMode::parse(&get_str(
6756 spec,
6757 py,
6758 "mode",
6759 "parametric spec missing `mode`",
6760 )?)
6761 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6762 let fuel = parse_plasma_reaction(&get_str(
6763 spec,
6764 py,
6765 "reaction",
6766 "parametric spec missing `reaction`",
6767 )?)?;
6768 let mut config = ps::ParametricPlasmaConfig {
6769 geometry: ps::MillerGeometry {
6770 major_radius_cm: num("major_radius")?,
6771 minor_radius_cm: num("minor_radius")?,
6772 elongation: num("elongation")?,
6773 triangularity: num("triangularity")?,
6774 shafranov_factor_cm: num("shafranov_factor")?,
6775 },
6776 mode,
6777 ion_density: ps::DensityProfile {
6778 centre_m3: num("ion_density_centre")?,
6779 peaking_factor: num("ion_density_peaking_factor")?,
6780 pedestal_m3: num("ion_density_pedestal")?,
6781 separatrix_m3: num("ion_density_separatrix")?,
6782 },
6783 ion_temperature: ps::TemperatureProfile {
6784 centre_kev: num("ion_temperature_centre")?,
6785 peaking_factor: num("ion_temperature_peaking_factor")?,
6786 beta: num("ion_temperature_beta")?,
6787 pedestal_kev: num("ion_temperature_pedestal")?,
6788 separatrix_kev: num("ion_temperature_separatrix")?,
6789 },
6790 pedestal_radius_cm: num("pedestal_radius")?,
6791 fuel,
6792 weight: 1.0,
6793 };
6794 if let Some(weight) = spec.get("weight") {
6795 let weight = weight
6796 .extract::<f64>(py)
6797 .map_err(|_| PyValueError::new_err("`weight` must be a number"))?;
6798 config.weight = weight;
6799 }
6800 config
6801 .validate()
6802 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6803 Ok(config)
6804}
6805
6806fn parse_plasma_source_spec(
6809 spec: &BTreeMap<String, Py<PyAny>>,
6810 py: Python<'_>,
6811) -> PyResult<PyPlasmaSource> {
6812 let kind: String = spec
6813 .get("kind")
6814 .ok_or_else(|| PyValueError::new_err("source spec needs a `kind`"))?
6815 .extract::<String>(py)
6816 .map_err(|_| PyValueError::new_err("`kind` must be a string"))?;
6817 match kind.as_str() {
6818 "point" | "ring" => Ok(PyPlasmaSource::Basic(parse_plasma_basic_spec(
6819 spec, py, &kind,
6820 )?)),
6821 "parametric" => Ok(PyPlasmaSource::Parametric(parse_plasma_parametric_spec(
6822 spec, py,
6823 )?)),
6824 other => Err(PyValueError::new_err(format!(
6825 "unknown source kind `{other}` (supported: point, ring, parametric)"
6826 ))),
6827 }
6828}
6829
6830fn plasma_drift_rows(
6833 py: Python<'_>,
6834 report: &nucleide_plasma_source::DriftReport,
6835) -> PyResult<Vec<Py<pyo3::types::PyDict>>> {
6836 use pyo3::types::PyDict;
6837 let mut rows = Vec::with_capacity(report.rows.len());
6838 for row in &report.rows {
6839 let d = PyDict::new(py);
6840 d.set_item("quantity", &row.quantity)?;
6841 d.set_item("accounted", row.accounted)?;
6842 d.set_item("rel_drift", row.rel_drift)?;
6843 d.set_item("reparsed", row.reparsed)?;
6844 d.set_item("note", &row.note)?;
6845 rows.push(d.unbind());
6846 }
6847 Ok(rows)
6848}
6849
6850#[pyfunction]
6860#[pyo3(signature = (spec, n, seed))]
6861fn plasma_source_particles(
6862 py: Python<'_>,
6863 spec: BTreeMap<String, Py<PyAny>>,
6864 n: usize,
6865 seed: u64,
6866) -> PyResult<Py<PyAny>> {
6867 use nucleide_plasma_source as ps;
6868 let particles = match parse_plasma_source_spec(&spec, py)? {
6869 PyPlasmaSource::Basic(config) => ps::SourceSampler::new(config, seed)
6870 .map_err(|e| PyValueError::new_err(e.to_string()))?
6871 .sample_n(n),
6872 PyPlasmaSource::Parametric(config) => ps::ParametricSampler::new(config, seed)
6873 .map_err(|e| PyValueError::new_err(e.to_string()))?
6874 .sample_n(n),
6875 };
6876 let mut x = Vec::with_capacity(n);
6877 let mut y = Vec::with_capacity(n);
6878 let mut z = Vec::with_capacity(n);
6879 let mut u = Vec::with_capacity(n);
6880 let mut v = Vec::with_capacity(n);
6881 let mut w = Vec::with_capacity(n);
6882 let mut energy = Vec::with_capacity(n);
6883 let mut weight = Vec::with_capacity(n);
6884 for p in &particles {
6885 x.push(p.position_cm[0]);
6886 y.push(p.position_cm[1]);
6887 z.push(p.position_cm[2]);
6888 u.push(p.direction[0]);
6889 v.push(p.direction[1]);
6890 w.push(p.direction[2]);
6891 energy.push(p.energy_mev);
6892 weight.push(p.weight);
6893 }
6894 use pyo3::types::PyDict;
6895 let out = PyDict::new(py);
6896 out.set_item("x", x.into_pyarray(py))?;
6897 out.set_item("y", y.into_pyarray(py))?;
6898 out.set_item("z", z.into_pyarray(py))?;
6899 out.set_item("u", u.into_pyarray(py))?;
6900 out.set_item("v", v.into_pyarray(py))?;
6901 out.set_item("w", w.into_pyarray(py))?;
6902 out.set_item("energy", energy.into_pyarray(py))?;
6903 out.set_item("weight", weight.into_pyarray(py))?;
6904 Ok(out.into_any().unbind())
6905}
6906
6907#[pyfunction]
6919#[pyo3(signature = (spec, bins=21))]
6920fn plasma_source_emit_cards(
6921 py: Python<'_>,
6922 spec: BTreeMap<String, Py<PyAny>>,
6923 bins: usize,
6924) -> PyResult<Py<PyAny>> {
6925 use nucleide_plasma_source as ps;
6926 let source = parse_plasma_source_spec(&spec, py)?;
6927 let version = match spec.get("mcnp_version") {
6928 Some(v) => v
6929 .extract::<u32>(py)
6930 .map_err(|_| PyValueError::new_err("`mcnp_version` must be an integer (5 or 6)"))?,
6931 None => 5,
6932 };
6933 let emit = |card: ps::EmittedCard| -> PyResult<Py<pyo3::types::PyDict>> {
6934 use pyo3::types::PyDict;
6935 let d = PyDict::new(py);
6936 d.set_item("card", card.text)?;
6937 d.set_item("drift", plasma_drift_rows(py, &card.drift)?)?;
6938 Ok(d.unbind())
6939 };
6940 use pyo3::types::PyDict;
6941 let out = PyDict::new(py);
6942 let (nominal, mean, sigma, mono) = match &source {
6943 PyPlasmaSource::Basic(config) => {
6944 let sdef = ps::emit_sdef(config, version, bins)
6945 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6946 let serpent =
6947 ps::emit_serpent(config, bins).map_err(|e| PyValueError::new_err(e.to_string()))?;
6948 let spectrum = config
6949 .spectrum()
6950 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6951 let sigma = match spectrum {
6952 ps::SpectrumSpec::Gaussian { sigma_mev, .. } => sigma_mev,
6953 ps::SpectrumSpec::Mono { .. } => 0.0,
6954 };
6955 out.set_item("sdef", emit(sdef)?)?;
6956 out.set_item("serpent", emit(serpent)?)?;
6957 (
6958 config.reaction.nominal_energy_mev(),
6959 spectrum.mean_mev(),
6960 sigma,
6961 spectrum.is_mono(),
6962 )
6963 }
6964 PyPlasmaSource::Parametric(config) => {
6965 let sdef = ps::emit_sdef_parametric(config, version, bins)
6966 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6967 let serpent = ps::emit_serpent_parametric(config, bins)
6968 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6969 let (mean, sigma) = config
6971 .fuel
6972 .moments_mev(config.temperature_kev(0.0))
6973 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6974 out.set_item("sdef", emit(sdef)?)?;
6975 out.set_item("serpent", emit(serpent)?)?;
6976 (config.fuel.nominal_energy_mev(), mean, sigma, sigma == 0.0)
6977 }
6978 };
6979 let spec_out = PyDict::new(py);
6980 spec_out.set_item("nominal_mev", nominal)?;
6981 spec_out.set_item("mean_mev", mean)?;
6982 spec_out.set_item("sigma_mev", sigma)?;
6983 spec_out.set_item("mono", mono)?;
6984 out.set_item("spectrum", spec_out)?;
6985 Ok(out.into_any().unbind())
6986}
6987
6988#[pyfunction]
6995fn plasma_source_spectrum_moments(
6996 py: Python<'_>,
6997 reaction: &str,
6998 ion_temperature_kev: f64,
6999) -> PyResult<Py<PyAny>> {
7000 use nucleide_plasma_source::FusionReaction as R;
7001 let reaction = parse_plasma_reaction(reaction)?;
7002 let (mean, sigma) = reaction
7003 .moments_mev(ion_temperature_kev)
7004 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7005 use pyo3::types::PyDict;
7006 let out = PyDict::new(py);
7007 out.set_item(
7008 "reaction",
7009 match reaction {
7010 R::Dt => "dt",
7011 R::Dd => "dd",
7012 },
7013 )?;
7014 out.set_item("label", reaction.label())?;
7015 out.set_item("nominal_mev", reaction.nominal_energy_mev())?;
7016 out.set_item("mean_mev", mean)?;
7017 out.set_item("sigma_mev", sigma)?;
7018 Ok(out.into_any().unbind())
7019}
7020
7021#[pyfunction]
7025fn plasma_source_reactivity(reaction: &str, ion_temperature_kev: f64) -> PyResult<f64> {
7026 parse_plasma_reaction(reaction)?
7027 .reactivity_m3_per_s(ion_temperature_kev)
7028 .map_err(|e| PyValueError::new_err(e.to_string()))
7029}
7030
7031fn parse_damage_nuclide(key: &Bound<'_, PyAny>) -> PyResult<NuclideId> {
7037 if let Ok(nucid) = key.extract::<u32>() {
7038 return NuclideId::try_from_nucid(nucid).map_err(wrap_nucid_err);
7039 }
7040 if let Ok(name) = key.extract::<&str>() {
7041 return NuclideId::from_name(name).map_err(wrap_nucid_err);
7042 }
7043 Err(PyTypeError::new_err("expected int nucid or str name"))
7044}
7045
7046#[pyfunction]
7052#[pyo3(signature = (flux, response, bounds, seconds))]
7053fn damage_nrt_dpa(
7054 flux: Vec<f64>,
7055 response: Vec<f64>,
7056 bounds: Vec<f64>,
7057 seconds: f64,
7058) -> PyResult<f64> {
7059 nucleide_damage::nrt_dpa(&flux, &response, &bounds, seconds)
7060 .map_err(|e| PyValueError::new_err(e.to_string()))
7061}
7062
7063#[pyfunction]
7065#[pyo3(signature = (flux, response, bounds, seconds))]
7066fn damage_arc_dpa(
7067 flux: Vec<f64>,
7068 response: Vec<f64>,
7069 bounds: Vec<f64>,
7070 seconds: f64,
7071) -> PyResult<f64> {
7072 nucleide_damage::arc_dpa(&flux, &response, &bounds, seconds)
7073 .map_err(|e| PyValueError::new_err(e.to_string()))
7074}
7075
7076#[pyfunction]
7079#[pyo3(signature = (flux, response, bounds, seconds))]
7080fn damage_gas_appm(
7081 flux: Vec<f64>,
7082 response: Vec<f64>,
7083 bounds: Vec<f64>,
7084 seconds: f64,
7085) -> PyResult<f64> {
7086 nucleide_damage::gas_appm(&flux, &response, &bounds, seconds)
7087 .map_err(|e| PyValueError::new_err(e.to_string()))
7088}
7089
7090#[pyfunction]
7094#[pyo3(signature = (flux, he_response, damage_response, bounds, seconds))]
7095fn damage_he_dpa_ratio(
7096 flux: Vec<f64>,
7097 he_response: Vec<f64>,
7098 damage_response: Vec<f64>,
7099 bounds: Vec<f64>,
7100 seconds: f64,
7101) -> PyResult<f64> {
7102 nucleide_damage::he_dpa_ratio(&flux, &he_response, &damage_response, &bounds, seconds)
7103 .map_err(|e| PyValueError::new_err(e.to_string()))
7104}
7105
7106#[pyfunction]
7110#[pyo3(signature = (t_ev, recoil, lattice))]
7111fn damage_lindhard_partition(
7112 t_ev: f64,
7113 recoil: &Bound<'_, PyAny>,
7114 lattice: &Bound<'_, PyAny>,
7115) -> PyResult<f64> {
7116 let recoil = parse_damage_nuclide(recoil)?;
7117 let lattice = parse_damage_nuclide(lattice)?;
7118 nucleide_damage::lindhard_partition(t_ev, &recoil, &lattice)
7119 .map_err(|e| PyValueError::new_err(e.to_string()))
7120}
7121
7122#[pyfunction]
7124#[pyo3(signature = (t_ev, recoil, lattice))]
7125fn damage_damage_energy(
7126 t_ev: f64,
7127 recoil: &Bound<'_, PyAny>,
7128 lattice: &Bound<'_, PyAny>,
7129) -> PyResult<f64> {
7130 let recoil = parse_damage_nuclide(recoil)?;
7131 let lattice = parse_damage_nuclide(lattice)?;
7132 nucleide_damage::damage_energy(t_ev, &recoil, &lattice)
7133 .map_err(|e| PyValueError::new_err(e.to_string()))
7134}
7135
7136#[pyfunction]
7139#[pyo3(signature = (t_ev, ed_ev, target))]
7140fn damage_nrt_displacements(t_ev: f64, ed_ev: f64, target: &Bound<'_, PyAny>) -> PyResult<f64> {
7141 let target = parse_damage_nuclide(target)?;
7142 nucleide_damage::nrt_displacements(t_ev, ed_ev, &target)
7143 .map_err(|e| PyValueError::new_err(e.to_string()))
7144}
7145
7146#[pyfunction]
7149#[pyo3(signature = (t_dam_ev, ed_ev, b_arc, c_arc))]
7150fn damage_arc_efficiency(t_dam_ev: f64, ed_ev: f64, b_arc: f64, c_arc: f64) -> PyResult<f64> {
7151 let params = nucleide_damage::ArcParams::new(b_arc, c_arc)
7152 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7153 nucleide_damage::arc_efficiency(t_dam_ev, ed_ev, ¶ms)
7154 .map_err(|e| PyValueError::new_err(e.to_string()))
7155}
7156
7157#[pyfunction]
7160#[pyo3(signature = (t_ev, ed_ev, target, b_arc, c_arc))]
7161fn damage_arc_displacements(
7162 t_ev: f64,
7163 ed_ev: f64,
7164 target: &Bound<'_, PyAny>,
7165 b_arc: f64,
7166 c_arc: f64,
7167) -> PyResult<f64> {
7168 let target = parse_damage_nuclide(target)?;
7169 let params = nucleide_damage::ArcParams::new(b_arc, c_arc)
7170 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7171 nucleide_damage::arc_displacements(t_ev, ed_ev, &target, ¶ms)
7172 .map_err(|e| PyValueError::new_err(e.to_string()))
7173}
7174
7175#[pyfunction]
7182#[pyo3(signature = (metric, flux, response, bounds, seconds, mean, cov, n, seed, k))]
7183#[allow(clippy::too_many_arguments)] fn damage_fold_uq(
7185 py: Python<'_>,
7186 metric: &str,
7187 flux: Vec<f64>,
7188 response: Vec<f64>,
7189 bounds: Vec<f64>,
7190 seconds: f64,
7191 mean: Vec<f64>,
7192 cov: Vec<Vec<f64>>,
7193 n: usize,
7194 seed: u64,
7195 k: f64,
7196) -> PyResult<Py<PyAny>> {
7197 use nucleide_damage::FoldMetric as M;
7198 let metric = match metric
7199 .to_ascii_lowercase()
7200 .replace(['-', ' '], "_")
7201 .as_str()
7202 {
7203 "nrt_dpa" => M::NrtDpa,
7204 "arc_dpa" => M::ArcDpa,
7205 "gas_appm" => M::GasAppm,
7206 "he_dpa_ratio" => M::HeDpaRatio,
7207 other => {
7208 return Err(PyValueError::new_err(format!(
7209 "unknown fold metric `{other}` (supported: nrt_dpa, arc_dpa, gas_appm)"
7210 )))
7211 }
7212 };
7213 let s = nucleide_damage::fold_uq(
7214 metric, &flux, &response, &bounds, seconds, &mean, &cov, n, seed, k,
7215 )
7216 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7217 use pyo3::types::PyDict;
7218 let out = PyDict::new(py);
7219 out.set_item("metric", s.metric.name())?;
7220 out.set_item("nominal", s.nominal)?;
7221 out.set_item("mean", s.mean)?;
7222 out.set_item("std", s.std)?;
7223 out.set_item("expected", s.expected)?;
7224 out.set_item("analytic_std", s.analytic_std)?;
7225 out.set_item("k", s.k)?;
7226 out.set_item("n", s.n)?;
7227 out.set_item("seed", s.seed)?;
7228 out.set_item("passed", s.passed)?;
7229 Ok(out.into_any().unbind())
7230}
7231
7232fn parse_tritium_boundary(
7244 spec: &BTreeMap<String, Py<PyAny>>,
7245 py: Python<'_>,
7246) -> PyResult<nucleide_tritium::Boundary> {
7247 use nucleide_tritium::Boundary as B;
7248 let kind: String = spec
7249 .get("kind")
7250 .ok_or_else(|| PyValueError::new_err("boundary spec needs a `kind`"))?
7251 .extract::<String>(py)
7252 .map_err(|_| PyValueError::new_err("`kind` must be a string"))?;
7253 let num = |key: &str| -> PyResult<f64> {
7254 spec.get(key)
7255 .ok_or_else(|| PyValueError::new_err(format!("boundary spec missing `{key}`")))?
7256 .extract::<f64>(py)
7257 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
7258 };
7259 let b = match kind.as_str() {
7260 "dirichlet" => B::dirichlet(num("value")?),
7261 "sieverts" => B::sieverts(num("solubility")?, num("pressure")?),
7262 "henry" => B::henry(num("solubility")?, num("pressure")?),
7263 "recombination" => B::recombination(num("rate")?),
7264 "zero_flux" => Ok(B::ZeroFlux),
7265 other => {
7266 return Err(PyValueError::new_err(format!(
7267 "unknown boundary kind `{other}` (supported: dirichlet, sieverts, henry, recombination, zero_flux)"
7268 )))
7269 }
7270 };
7271 b.map_err(|e| PyValueError::new_err(e.to_string()))
7272}
7273
7274fn parse_tritium_trap(
7279 spec: &BTreeMap<String, Py<PyAny>>,
7280 py: Python<'_>,
7281) -> PyResult<nucleide_tritium::TrapSpec> {
7282 let num = |key: &str| -> PyResult<f64> {
7283 spec.get(key)
7284 .ok_or_else(|| PyValueError::new_err(format!("trap spec missing `{key}`")))?
7285 .extract::<f64>(py)
7286 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
7287 };
7288 let opt = |key: &str| -> PyResult<f64> {
7289 match spec.get(key) {
7290 None => Ok(0.0),
7291 Some(v) => v
7292 .extract::<f64>(py)
7293 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number"))),
7294 }
7295 };
7296 nucleide_tritium::TrapSpec::new(
7297 num("k0")?,
7298 opt("e_k")?,
7299 num("p0")?,
7300 opt("e_p")?,
7301 num("site_density")?,
7302 )
7303 .map_err(|e| PyValueError::new_err(e.to_string()))
7304}
7305
7306#[allow(clippy::too_many_arguments)]
7307fn tritium_params(
7308 py: Python<'_>,
7309 length: f64,
7310 cells: usize,
7311 d0: f64,
7312 e_d: f64,
7313 traps: Vec<BTreeMap<String, Py<PyAny>>>,
7314 temperature: Vec<f64>,
7315 source: Option<Vec<f64>>,
7316) -> PyResult<nucleide_tritium::TransportParams> {
7317 let parsed: Vec<nucleide_tritium::TrapSpec> = traps
7318 .iter()
7319 .map(|s| parse_tritium_trap(s, py))
7320 .collect::<PyResult<_>>()?;
7321 nucleide_tritium::TransportParams::new(
7322 length,
7323 cells,
7324 d0,
7325 e_d,
7326 parsed,
7327 temperature,
7328 source.unwrap_or_default(),
7329 )
7330 .map_err(|e| PyValueError::new_err(e.to_string()))
7331}
7332
7333#[pyfunction]
7343#[pyo3(signature = (length, cells, d0, e_d, traps, temperature, source, left, right))]
7344#[allow(clippy::too_many_arguments)]
7345fn tritium_steady(
7346 py: Python<'_>,
7347 length: f64,
7348 cells: usize,
7349 d0: f64,
7350 e_d: f64,
7351 traps: Vec<BTreeMap<String, Py<PyAny>>>,
7352 temperature: Vec<f64>,
7353 source: Option<Vec<f64>>,
7354 left: BTreeMap<String, Py<PyAny>>,
7355 right: BTreeMap<String, Py<PyAny>>,
7356) -> PyResult<Py<PyAny>> {
7357 let params = tritium_params(py, length, cells, d0, e_d, traps, temperature, source)?;
7358 let left = parse_tritium_boundary(&left, py)?;
7359 let right = parse_tritium_boundary(&right, py)?;
7360 let s = nucleide_tritium::steady_state(¶ms, &left, &right)
7361 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7362 use pyo3::types::PyDict;
7363 let out = PyDict::new(py);
7364 out.set_item("centres", &s.centres).ok();
7365 out.set_item("mobile", &s.mobile).ok();
7366 out.set_item("trapped", &s.trapped).ok();
7367 out.set_item("flux_left", s.flux_left).ok();
7368 out.set_item("flux_right", s.flux_right).ok();
7369 out.set_item("inventory_mobile", s.inventory_mobile).ok();
7370 out.set_item("inventory_trapped", s.inventory_trapped).ok();
7371 Ok(out.into_any().unbind())
7372}
7373
7374#[pyfunction]
7384#[pyo3(signature = (length, cells, d0, e_d, traps, temperature, source, left, right, t, mobile0=None, trapped0=None, method="crank_nicolson", rtol=1e-9, atol=1e-12, dt_min=1e-14, dt_max=None, max_steps=1000000))]
7385#[allow(clippy::too_many_arguments)]
7386fn tritium_transient(
7387 py: Python<'_>,
7388 length: f64,
7389 cells: usize,
7390 d0: f64,
7391 e_d: f64,
7392 traps: Vec<BTreeMap<String, Py<PyAny>>>,
7393 temperature: Vec<f64>,
7394 source: Option<Vec<f64>>,
7395 left: BTreeMap<String, Py<PyAny>>,
7396 right: BTreeMap<String, Py<PyAny>>,
7397 t: Vec<f64>,
7398 mobile0: Option<Vec<f64>>,
7399 trapped0: Option<Vec<Vec<f64>>>,
7400 method: &str,
7401 rtol: f64,
7402 atol: f64,
7403 dt_min: f64,
7404 dt_max: Option<f64>,
7405 max_steps: usize,
7406) -> PyResult<Py<PyAny>> {
7407 use nucleide_tritium::{SolverOptions, Theta};
7408 let params = tritium_params(py, length, cells, d0, e_d, traps, temperature, source)?;
7409 let left = parse_tritium_boundary(&left, py)?;
7410 let right = parse_tritium_boundary(&right, py)?;
7411 let grid =
7412 nucleide_tritium::TimeGrid::new(t).map_err(|e| PyValueError::new_err(e.to_string()))?;
7413 let ntraps = params.traps.len();
7414 let mobile = mobile0.unwrap_or_else(|| vec![0.0; params.cells]);
7415 let trapped = trapped0.unwrap_or_else(|| vec![vec![0.0; ntraps]; params.cells]);
7416 let initial = nucleide_tritium::InitialState::new(¶ms, mobile, trapped)
7417 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7418 let theta = if method.eq_ignore_ascii_case("crank_nicolson") {
7419 Theta::CrankNicolson
7420 } else if method.eq_ignore_ascii_case("backward_euler") {
7421 Theta::BackwardEuler
7422 } else {
7423 return Err(PyValueError::new_err(format!(
7424 "unknown tritium method `{method}` (supported: crank_nicolson, backward_euler)"
7425 )));
7426 };
7427 let opts = SolverOptions {
7428 theta,
7429 rtol,
7430 atol,
7431 dt_min,
7432 dt_max: dt_max.unwrap_or(f64::INFINITY),
7433 max_steps,
7434 };
7435 let sol = nucleide_tritium::solve(¶ms, &left, &right, &grid, &initial, &opts)
7436 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7437 use pyo3::types::PyDict;
7438 let out = PyDict::new(py);
7439 out.set_item("times", &sol.times).ok();
7440 out.set_item("mobile", &sol.mobile).ok();
7441 out.set_item("trapped", &sol.trapped).ok();
7442 out.set_item("flux_left", &sol.flux_left).ok();
7443 out.set_item("flux_right", &sol.flux_right).ok();
7444 Ok(out.into_any().unbind())
7445}
7446
7447#[pyfunction]
7449fn tritium_time_lag(length: f64, diffusivity: f64) -> PyResult<f64> {
7450 nucleide_tritium::time_lag(length, diffusivity)
7451 .map_err(|e| PyValueError::new_err(e.to_string()))
7452}
7453
7454#[pyfunction]
7456fn tritium_breakthrough(diffusivity: f64, length: f64, times: Vec<f64>) -> PyResult<Vec<f64>> {
7457 times
7458 .iter()
7459 .map(|t| {
7460 nucleide_tritium::breakthrough_ratio(diffusivity, length, *t)
7461 .map_err(|e| PyValueError::new_err(e.to_string()))
7462 })
7463 .collect()
7464}
7465
7466#[pyfunction]
7468fn tritium_oriani(diffusivity: f64, equilibrium_constant: f64, site_density: f64) -> PyResult<f64> {
7469 nucleide_tritium::effective_diffusivity(diffusivity, equilibrium_constant, site_density)
7470 .map_err(|e| PyValueError::new_err(e.to_string()))
7471}
7472
7473#[pyfunction]
7475fn tritium_langmuir(site_density: f64, equilibrium_constant: f64, c_mobile: f64) -> PyResult<f64> {
7476 nucleide_tritium::equilibrium_trapped(site_density, equilibrium_constant, c_mobile)
7477 .map_err(|e| PyValueError::new_err(e.to_string()))
7478}
7479
7480#[pyfunction]
7482fn tritium_irreversible_fill(
7483 rate_k: f64,
7484 c_mobile: f64,
7485 site_density: f64,
7486 times: Vec<f64>,
7487) -> PyResult<Vec<f64>> {
7488 times
7489 .iter()
7490 .map(|t| {
7491 nucleide_tritium::irreversible_fill(rate_k, c_mobile, site_density, *t)
7492 .map_err(|e| PyValueError::new_err(e.to_string()))
7493 })
7494 .collect()
7495}
7496
7497#[pyfunction]
7499fn tritium_sieverts(solubility: f64, pressure: f64) -> PyResult<f64> {
7500 nucleide_tritium::sieverts_concentration(solubility, pressure)
7501 .map_err(|e| PyValueError::new_err(e.to_string()))
7502}
7503
7504#[pyfunction]
7506fn tritium_recombination_rate(kr0: f64, e_r: f64, temp: f64) -> PyResult<f64> {
7507 nucleide_tritium::recombination_rate_arrhenius(kr0, e_r, temp)
7508 .map_err(|e| PyValueError::new_err(e.to_string()))
7509}
7510
7511fn parse_tritium_layer(
7521 spec: &BTreeMap<String, Py<PyAny>>,
7522 py: Python<'_>,
7523) -> PyResult<nucleide_tritium::LayerSpec> {
7524 let num = |key: &str| -> PyResult<f64> {
7525 spec.get(key)
7526 .ok_or_else(|| PyValueError::new_err(format!("layer spec missing `{key}`")))?
7527 .extract::<f64>(py)
7528 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
7529 };
7530 let opt = |key: &str| -> PyResult<f64> {
7531 match spec.get(key) {
7532 None => Ok(0.0),
7533 Some(v) => v
7534 .extract::<f64>(py)
7535 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number"))),
7536 }
7537 };
7538 let traps = match spec.get("traps") {
7539 None => Vec::new(),
7540 Some(v) => v
7541 .extract::<Vec<BTreeMap<String, Py<PyAny>>>>(py)
7542 .map_err(|_| PyValueError::new_err("`traps` must be a list of dicts"))?
7543 .iter()
7544 .map(|s| parse_tritium_trap(s, py))
7545 .collect::<PyResult<_>>()?,
7546 };
7547 let temperature: Vec<f64> = match spec.get("temperature") {
7548 None => vec![500.0],
7549 Some(v) => v
7550 .extract::<Vec<f64>>(py)
7551 .map_err(|_| PyValueError::new_err("`temperature` must be a list of numbers"))?,
7552 };
7553 let source: Vec<f64> = match spec.get("source") {
7554 None => Vec::new(),
7555 Some(v) => v
7556 .extract::<Vec<f64>>(py)
7557 .map_err(|_| PyValueError::new_err("`source` must be a list of numbers"))?,
7558 };
7559 let cells: usize = spec
7560 .get("cells")
7561 .ok_or_else(|| PyValueError::new_err("layer spec missing `cells`"))?
7562 .extract::<usize>(py)
7563 .map_err(|_| PyValueError::new_err("`cells` must be an integer"))?;
7564 nucleide_tritium::LayerSpec::new(
7565 num("thickness")?,
7566 cells,
7567 num("D")?,
7568 opt("E_D")?,
7569 num("solubility")?,
7570 traps,
7571 temperature,
7572 source,
7573 )
7574 .map_err(|e| PyValueError::new_err(e.to_string()))
7575}
7576
7577fn tritium_layer_stack(
7578 py: Python<'_>,
7579 layers: Vec<BTreeMap<String, Py<PyAny>>>,
7580) -> PyResult<nucleide_tritium::LayerStack> {
7581 let parsed: Vec<nucleide_tritium::LayerSpec> = layers
7582 .iter()
7583 .map(|s| parse_tritium_layer(s, py))
7584 .collect::<PyResult<_>>()?;
7585 let interfaces = vec![nucleide_tritium::Interface::Sieverts; parsed.len().saturating_sub(1)];
7586 nucleide_tritium::LayerStack::new(parsed, interfaces)
7587 .map_err(|e| PyValueError::new_err(e.to_string()))
7588}
7589
7590#[pyfunction]
7600fn tritium_layers_steady(
7601 py: Python<'_>,
7602 layers: Vec<BTreeMap<String, Py<PyAny>>>,
7603 left: BTreeMap<String, Py<PyAny>>,
7604 right: BTreeMap<String, Py<PyAny>>,
7605) -> PyResult<Py<PyAny>> {
7606 let stack = tritium_layer_stack(py, layers)?;
7607 let left = parse_tritium_boundary(&left, py)?;
7608 let right = parse_tritium_boundary(&right, py)?;
7609 let s = nucleide_tritium::steady_layers(&stack, &left, &right)
7610 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7611 use pyo3::types::PyDict;
7612 let out = PyDict::new(py);
7613 out.set_item("centres", &s.centres).ok();
7614 out.set_item("mobile", &s.mobile).ok();
7615 out.set_item("trapped", &s.trapped).ok();
7616 out.set_item("flux_left", s.flux_left).ok();
7617 out.set_item("flux_right", s.flux_right).ok();
7618 out.set_item("inventory_mobile", s.inventory_mobile).ok();
7619 out.set_item("inventory_trapped", s.inventory_trapped).ok();
7620 Ok(out.into_any().unbind())
7621}
7622
7623#[pyfunction]
7634#[pyo3(signature = (layers, left, right, t, mobile0=None, trapped0=None, method="crank_nicolson", rtol=1e-9, atol=1e-12, dt_min=1e-14, dt_max=None, max_steps=1000000))]
7635#[allow(clippy::too_many_arguments)]
7636fn tritium_layers_transient(
7637 py: Python<'_>,
7638 layers: Vec<BTreeMap<String, Py<PyAny>>>,
7639 left: BTreeMap<String, Py<PyAny>>,
7640 right: BTreeMap<String, Py<PyAny>>,
7641 t: Vec<f64>,
7642 mobile0: Option<Vec<f64>>,
7643 trapped0: Option<Vec<Vec<f64>>>,
7644 method: &str,
7645 rtol: f64,
7646 atol: f64,
7647 dt_min: f64,
7648 dt_max: Option<f64>,
7649 max_steps: usize,
7650) -> PyResult<Py<PyAny>> {
7651 use nucleide_tritium::{SolverOptions, Theta};
7652 let stack = tritium_layer_stack(py, layers)?;
7653 let left = parse_tritium_boundary(&left, py)?;
7654 let right = parse_tritium_boundary(&right, py)?;
7655 let grid =
7656 nucleide_tritium::TimeGrid::new(t).map_err(|e| PyValueError::new_err(e.to_string()))?;
7657 let mobile = mobile0.unwrap_or_else(|| vec![0.0; stack.total_cells()]);
7658 let trapped = trapped0.unwrap_or_else(|| stack.zero_state().trapped);
7659 let initial = nucleide_tritium::InitialState { mobile, trapped };
7660 let theta = if method.eq_ignore_ascii_case("crank_nicolson") {
7661 Theta::CrankNicolson
7662 } else if method.eq_ignore_ascii_case("backward_euler") {
7663 Theta::BackwardEuler
7664 } else {
7665 return Err(PyValueError::new_err(format!(
7666 "unknown tritium method `{method}` (supported: crank_nicolson, backward_euler)"
7667 )));
7668 };
7669 let opts = SolverOptions {
7670 theta,
7671 rtol,
7672 atol,
7673 dt_min,
7674 dt_max: dt_max.unwrap_or(f64::INFINITY),
7675 max_steps,
7676 };
7677 let sol = nucleide_tritium::solve_layers(&stack, &left, &right, &grid, &initial, &opts)
7678 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7679 use pyo3::types::PyDict;
7680 let out = PyDict::new(py);
7681 out.set_item("times", &sol.times).ok();
7682 out.set_item("mobile", &sol.mobile).ok();
7683 out.set_item("trapped", &sol.trapped).ok();
7684 out.set_item("flux_left", &sol.flux_left).ok();
7685 out.set_item("flux_right", &sol.flux_right).ok();
7686 Ok(out.into_any().unbind())
7687}
7688
7689#[pyfunction]
7695fn spectroscopy_rect_smooth(counts: Vec<f64>, m: i64) -> PyResult<Vec<f64>> {
7696 let w = usize::try_from(m).map_err(|_| {
7697 PyValueError::new_err(format!("spectroscopy: smoothing width {m} is less than 3"))
7698 })?;
7699 nucleide_spectroscopy::rect_smooth(&counts, w).map_err(|e| PyValueError::new_err(e.to_string()))
7700}
7701
7702#[pyfunction]
7704fn spectroscopy_five_point_smooth(counts: Vec<f64>) -> PyResult<Vec<f64>> {
7705 nucleide_spectroscopy::five_point_smooth(&counts)
7706 .map_err(|e| PyValueError::new_err(e.to_string()))
7707}
7708
7709#[pyfunction]
7711fn spectroscopy_calc_bg(
7712 counts: Vec<f64>,
7713 channels: Vec<f64>,
7714 c1: i64,
7715 c2: i64,
7716 m: i64,
7717) -> PyResult<f64> {
7718 nucleide_spectroscopy::calc_bg(&counts, &channels, c1, c2, m)
7719 .map_err(|e| PyValueError::new_err(e.to_string()))
7720}
7721
7722#[pyfunction]
7724fn spectroscopy_gross_count(
7725 counts: Vec<f64>,
7726 channels: Vec<f64>,
7727 c1: i64,
7728 c2: i64,
7729) -> PyResult<f64> {
7730 nucleide_spectroscopy::gross_count(&counts, &channels, c1, c2)
7731 .map_err(|e| PyValueError::new_err(e.to_string()))
7732}
7733
7734#[pyfunction]
7736fn spectroscopy_net_counts(
7737 counts: Vec<f64>,
7738 channels: Vec<f64>,
7739 c1: i64,
7740 c2: i64,
7741 m: i64,
7742) -> PyResult<f64> {
7743 nucleide_spectroscopy::net_counts(&counts, &channels, c1, c2, m)
7744 .map_err(|e| PyValueError::new_err(e.to_string()))
7745}
7746
7747#[pyfunction]
7749fn spectroscopy_energy_bins(channels: Vec<f64>, calib_e_fit: Vec<f64>) -> PyResult<Vec<f64>> {
7750 nucleide_spectroscopy::energy_bins(&channels, &calib_e_fit)
7751 .map_err(|e| PyValueError::new_err(e.to_string()))
7752}
7753
7754#[pyfunction]
7756fn spectroscopy_detector_efficiency(
7757 energy_mev: f64,
7758 eff_coeff: Vec<f64>,
7759 eff_fit: i64,
7760) -> PyResult<f64> {
7761 nucleide_spectroscopy::detector_efficiency(energy_mev, &eff_coeff, eff_fit)
7762 .map_err(|e| PyValueError::new_err(e.to_string()))
7763}
7764
7765#[pyfunction]
7771#[pyo3(signature = (energies, effs, weights, order, eff_fit=1))]
7772fn spectroscopy_fit_efficiency(
7773 energies: Vec<f64>,
7774 effs: Vec<f64>,
7775 weights: Vec<f64>,
7776 order: usize,
7777 eff_fit: i64,
7778) -> PyResult<Vec<f64>> {
7779 nucleide_spectroscopy::fit_efficiency(&energies, &effs, &weights, order, eff_fit)
7780 .map_err(|e| PyValueError::new_err(e.to_string()))
7781}
7782
7783fn atomic_key(atomic: &BTreeMap<String, f64>, key: &str) -> PyResult<f64> {
7785 atomic
7786 .get(key)
7787 .copied()
7788 .ok_or_else(|| PyValueError::new_err(format!("atomic constants missing `{key}`")))
7789}
7790
7791#[pyfunction]
7800#[pyo3(signature = (atomic, k_conv=None, l_conv=None))]
7801fn spectroscopy_xray_lines(
7802 atomic: BTreeMap<String, f64>,
7803 k_conv: Option<f64>,
7804 l_conv: Option<f64>,
7805) -> PyResult<Vec<(f64, f64)>> {
7806 let data = nucleide_spectroscopy::AtomicData {
7807 k_shell_fluor: atomic_key(&atomic, "k_shell_fluor")?,
7808 l_shell_fluor: atomic_key(&atomic, "l_shell_fluor")?,
7809 prob: atomic_key(&atomic, "prob")?,
7810 kb_to_ka: atomic_key(&atomic, "kb_to_ka")?,
7811 ka2_to_ka1: atomic_key(&atomic, "ka2_to_ka1")?,
7812 ka1_en_kev: atomic_key(&atomic, "ka1_en_kev")?,
7813 ka2_en_kev: atomic_key(&atomic, "ka2_en_kev")?,
7814 kb_en_kev: atomic_key(&atomic, "kb_en_kev")?,
7815 l_en_kev: atomic_key(&atomic, "l_en_kev")?,
7816 };
7817 let present = |v: Option<f64>| v.filter(|x| !x.is_nan());
7819 Ok(
7820 nucleide_spectroscopy::xray_lines(&data, present(k_conv), present(l_conv))
7821 .iter()
7822 .map(|l| (l.energy_kev, l.intensity))
7823 .collect(),
7824 )
7825}
7826
7827#[pyfunction]
7842#[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))]
7843#[allow(clippy::too_many_arguments)]
7844fn spectroscopy_sdef_decay_source(
7845 lines: Vec<(f64, f64)>,
7846 x: f64,
7847 y: f64,
7848 z: f64,
7849 u: f64,
7850 v: f64,
7851 w: f64,
7852 weight: f64,
7853 particle: &str,
7854 version: u32,
7855) -> PyResult<(Vec<(f64, f64)>, String)> {
7856 let particle = particle
7857 .parse::<nucleide_nuclei::particles::ParticleId>()
7858 .map_err(|e| PyValueError::new_err(format!("spectroscopy: particle {e}")))?;
7859 let source = nucleide_spectroscopy::PointSource {
7860 x,
7861 y,
7862 z,
7863 u,
7864 v,
7865 w,
7866 weight,
7867 particle,
7868 };
7869 nucleide_spectroscopy::sdef_card(&lines, &source, version)
7870 .map_err(|e| PyValueError::new_err(e.to_string()))
7871}
7872
7873fn spectrum_to_py(
7875 py: Python<'_>,
7876 spec: &nucleide_spectroscopy::GammaSpectrum,
7877) -> PyResult<Py<PyAny>> {
7878 use pyo3::types::PyDict;
7879 let d = PyDict::new(py);
7880 let s = &spec.spectrum;
7881 d.set_item("spec_name", &s.spec_name)?;
7882 d.set_item("start_chan_num", s.start_chan_num)?;
7883 d.set_item("num_channels", s.num_channels)?;
7884 d.set_item("channels", &s.channels)?;
7885 d.set_item("counts", &s.counts)?;
7886 d.set_item("ebin", &s.ebin)?;
7887 d.set_item("real_time", spec.real_time)?;
7888 d.set_item("live_time", spec.live_time)?;
7889 d.set_item("dead_time", spec.dead_time())?;
7890 d.set_item("det_id", &spec.det_id)?;
7891 d.set_item("det_descp", &spec.det_descp)?;
7892 d.set_item("start_date", &spec.start_date)?;
7893 d.set_item("start_time", &spec.start_time)?;
7894 d.set_item("calib_e_fit", &spec.calib_e_fit)?;
7895 d.set_item("calib_fwhm_fit", &spec.calib_fwhm_fit)?;
7896 d.set_item("file_name", &spec.file_name)?;
7897 Ok(d.into_any().unbind())
7898}
7899
7900#[pyfunction]
7902fn spectroscopy_parse_dollar_spe(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
7903 let spec = nucleide_spectroscopy::parse_dollar_spe(text, "")
7904 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7905 spectrum_to_py(py, &spec)
7906}
7907
7908#[pyfunction]
7910fn spectroscopy_parse_spe(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
7911 let spec = nucleide_spectroscopy::parse_plain_spe(text, "")
7912 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7913 spectrum_to_py(py, &spec)
7914}
7915
7916#[pyfunction]
7918fn spectroscopy_read_dollar_spe(py: Python<'_>, path: &str) -> PyResult<Py<PyAny>> {
7919 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
7920 let spec = nucleide_spectroscopy::parse_dollar_spe(&text, path)
7921 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7922 spectrum_to_py(py, &spec)
7923}
7924
7925#[pyfunction]
7927fn spectroscopy_read_spe(py: Python<'_>, path: &str) -> PyResult<Py<PyAny>> {
7928 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
7929 let spec = nucleide_spectroscopy::parse_plain_spe(&text, path)
7930 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7931 spectrum_to_py(py, &spec)
7932}
7933
7934#[pyfunction]
7938fn spectroscopy_parse_lines_tsv(text: &str) -> PyResult<Vec<(f64, f64)>> {
7939 nucleide_spectroscopy::parse_lines_tsv(text).map_err(|e| PyValueError::new_err(e.to_string()))
7940}
7941
7942#[pyfunction]
7945fn spectroscopy_read_decay_lines(path: &str) -> PyResult<Vec<(f64, f64)>> {
7946 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
7947 nucleide_spectroscopy::parse_lines_tsv(&text).map_err(|e| PyValueError::new_err(e.to_string()))
7948}
7949
7950fn uq_sample_err(e: nucleide_linalg::sample::SampleError) -> PyErr {
7955 PyValueError::new_err(e.to_string())
7956}
7957
7958fn uq_decay_err(e: nucleide_linalg::decay::DecayError) -> PyErr {
7959 PyValueError::new_err(e.to_string())
7960}
7961
7962#[pyfunction]
7970fn uq_sample_mvn(
7971 py: Python<'_>,
7972 mean: Vec<f64>,
7973 cov: Vec<Vec<f64>>,
7974 n: usize,
7975 seed: u64,
7976) -> PyResult<Py<PyAny>> {
7977 use pyo3::types::PyDict;
7978 let set = nucleide_linalg::sample::sample_mvn(&mean, &cov, n, seed).map_err(uq_sample_err)?;
7979 let d = PyDict::new(py);
7980 d.set_item("samples", set.samples)?;
7981 d.set_item("method", set.method.name())?;
7982 match &set.method {
7983 nucleide_linalg::sample::FactorMethod::Cholesky => {
7984 d.set_item("min_eigen", py.None())?;
7985 d.set_item("max_eigen", py.None())?;
7986 }
7987 nucleide_linalg::sample::FactorMethod::EigenClip {
7988 min_eigen,
7989 max_eigen,
7990 } => {
7991 d.set_item("min_eigen", *min_eigen)?;
7992 d.set_item("max_eigen", *max_eigen)?;
7993 }
7994 }
7995 Ok(d.into_any().unbind())
7996}
7997
7998#[pyfunction]
8000fn uq_sample_mean(samples: Vec<Vec<f64>>) -> PyResult<Vec<f64>> {
8001 nucleide_linalg::sample::sample_mean(&samples).map_err(uq_sample_err)
8002}
8003
8004#[pyfunction]
8006fn uq_sample_cov(samples: Vec<Vec<f64>>) -> PyResult<Vec<Vec<f64>>> {
8007 nucleide_linalg::sample::sample_cov(&samples).map_err(uq_sample_err)
8008}
8009
8010#[pyfunction]
8016fn uq_check_convergence(
8017 py: Python<'_>,
8018 mean: Vec<f64>,
8019 cov: Vec<Vec<f64>>,
8020 samples: Vec<Vec<f64>>,
8021 mean_tol: f64,
8022 cov_tol: f64,
8023) -> PyResult<Py<PyAny>> {
8024 use pyo3::types::PyDict;
8025 let rep = nucleide_linalg::sample::check_convergence(&mean, &cov, &samples, mean_tol, cov_tol)
8026 .map_err(uq_sample_err)?;
8027 let d = PyDict::new(py);
8028 d.set_item("mean_err_max", rep.mean_err_max)?;
8029 d.set_item("cov_err_fro", rep.cov_err_fro)?;
8030 d.set_item("mean_tol", rep.mean_tol)?;
8031 d.set_item("cov_tol", rep.cov_tol)?;
8032 d.set_item("passed", rep.passed)?;
8033 Ok(d.into_any().unbind())
8034}
8035
8036#[pyfunction]
8039fn uq_perturb_branches(base: Vec<f64>, rel: Vec<f64>) -> PyResult<Vec<f64>> {
8040 nucleide_linalg::decay::perturb_branches(&base, &rel).map_err(uq_decay_err)
8041}
8042
8043#[pyfunction]
8047fn uq_perturb_energies(base: Vec<f64>, delta: Vec<f64>, convention: &str) -> PyResult<Vec<f64>> {
8048 let conv = nucleide_linalg::sample::PerturbConvention::parse(convention)
8049 .map_err(PyValueError::new_err)?;
8050 nucleide_linalg::decay::perturb_energies(&base, &delta, conv).map_err(uq_decay_err)
8051}
8052
8053#[pyfunction]
8060fn uq_sample_lognormal(
8061 py: Python<'_>,
8062 mean_log: Vec<f64>,
8063 cov: Vec<Vec<f64>>,
8064 n: usize,
8065 seed: u64,
8066) -> PyResult<Py<PyAny>> {
8067 use pyo3::types::PyDict;
8068 let set = nucleide_linalg::sample::sample_lognormal(&mean_log, &cov, n, seed)
8069 .map_err(uq_sample_err)?;
8070 let d = PyDict::new(py);
8071 d.set_item("samples", set.samples)?;
8072 d.set_item("method", set.method.name())?;
8073 match &set.method {
8074 nucleide_linalg::sample::FactorMethod::Cholesky => {
8075 d.set_item("min_eigen", py.None())?;
8076 d.set_item("max_eigen", py.None())?;
8077 }
8078 nucleide_linalg::sample::FactorMethod::EigenClip {
8079 min_eigen,
8080 max_eigen,
8081 } => {
8082 d.set_item("min_eigen", *min_eigen)?;
8083 d.set_item("max_eigen", *max_eigen)?;
8084 }
8085 }
8086 Ok(d.into_any().unbind())
8087}
8088
8089#[pyfunction]
8096fn uq_sample_lhs(
8097 py: Python<'_>,
8098 mean: Vec<f64>,
8099 cov: Vec<Vec<f64>>,
8100 n: usize,
8101 seed: u64,
8102) -> PyResult<Py<PyAny>> {
8103 use pyo3::types::PyDict;
8104 let set = nucleide_linalg::sample::sample_lhs(&mean, &cov, n, seed).map_err(uq_sample_err)?;
8105 let d = PyDict::new(py);
8106 d.set_item("samples", set.samples)?;
8107 d.set_item("method", set.method.name())?;
8108 match &set.method {
8109 nucleide_linalg::sample::FactorMethod::Cholesky => {
8110 d.set_item("min_eigen", py.None())?;
8111 d.set_item("max_eigen", py.None())?;
8112 }
8113 nucleide_linalg::sample::FactorMethod::EigenClip {
8114 min_eigen,
8115 max_eigen,
8116 } => {
8117 d.set_item("min_eigen", *min_eigen)?;
8118 d.set_item("max_eigen", *max_eigen)?;
8119 }
8120 }
8121 Ok(d.into_any().unbind())
8122}
8123
8124#[pyfunction]
8127fn uq_lognormal_mean(mean_log: Vec<f64>, cov: Vec<Vec<f64>>) -> PyResult<Vec<f64>> {
8128 nucleide_linalg::sample::lognormal_mean(&mean_log, &cov).map_err(uq_sample_err)
8129}
8130
8131#[pyfunction]
8134fn uq_lognormal_cov(mean_log: Vec<f64>, cov: Vec<Vec<f64>>) -> PyResult<Vec<Vec<f64>>> {
8135 nucleide_linalg::sample::lognormal_cov(&mean_log, &cov).map_err(uq_sample_err)
8136}
8137
8138#[pyfunction]
8140fn uq_passthrough(delta: Vec<f64>) -> PyResult<Vec<f64>> {
8141 nucleide_linalg::decay::passthrough(&delta).map_err(uq_decay_err)
8142}
8143
8144#[pyfunction]
8147fn uq_perturb_fission_yields(base: Vec<f64>, rel: Vec<f64>) -> PyResult<Vec<f64>> {
8148 nucleide_linalg::decay::perturb_fission_yields(&base, &rel).map_err(uq_decay_err)
8149}
8150
8151fn parse_projectile(flag: &str) -> PyResult<nucleide_nuclei::rxname::Projectile> {
8156 flag.parse::<nucleide_nuclei::rxname::Projectile>()
8157 .map_err(|e| PyValueError::new_err(e.to_string()))
8158}
8159
8160fn resolve_rx_id(spec: &Bound<'_, PyAny>) -> PyResult<u32> {
8161 if let Ok(id) = spec.extract::<u32>() {
8162 return Ok(id);
8163 }
8164 if let Ok(s) = spec.extract::<&str>() {
8165 return nucleide_nuclei::rxname::name_to_id(s)
8166 .map_err(|e| PyValueError::new_err(e.to_string()));
8167 }
8168 Err(PyTypeError::new_err(
8169 "expected reaction id (int) or name (str)",
8170 ))
8171}
8172
8173#[pyfunction]
8175fn rxname_label(id: u32) -> &'static str {
8176 nucleide_nuclei::rxname::label(id)
8177}
8178
8179#[pyfunction]
8181fn rxname_doc(id: u32) -> &'static str {
8182 nucleide_nuclei::rxname::doc(id)
8183}
8184
8185#[pyfunction]
8187fn rxname_reaction(py: Python<'_>, id: u32) -> PyResult<Option<Py<PyAny>>> {
8188 use pyo3::types::PyDict;
8189 Ok(nucleide_nuclei::rxname::reaction(id).map(|r| {
8190 let d = PyDict::new(py);
8191 d.set_item("id", r.id).ok();
8192 d.set_item("name", r.name).ok();
8193 d.set_item("mt", r.mt).ok();
8194 d.set_item("label", r.label).ok();
8195 d.set_item("doc", r.doc).ok();
8196 d.into_any().unbind()
8197 }))
8198}
8199
8200#[pyfunction]
8202#[pyo3(signature = (from_nucid, to_nucid, projectile="n"))]
8203fn rxname_id_from_nucdelta(from_nucid: u32, to_nucid: u32, projectile: &str) -> PyResult<u32> {
8204 let p = parse_projectile(projectile)?;
8205 nucleide_nuclei::rxname::id_from_nucdelta(from_nucid, to_nucid, p)
8206 .map_err(|e| PyValueError::new_err(e.to_string()))
8207}
8208
8209#[pyfunction]
8211#[pyo3(signature = (parent, rx, projectile="n"))]
8212fn rxname_child(parent: &str, rx: &Bound<'_, PyAny>, projectile: &str) -> PyResult<String> {
8213 let p = parse_projectile(projectile)?;
8214 let rx = resolve_rx_id(rx)?;
8215 let parent_id = NuclideId::from_name(parent)
8216 .map_err(|e| PyValueError::new_err(format!("`{parent}`: {e}")))?;
8217 nucleide_nuclei::rxname::child(parent_id, rx, p)
8218 .map(|id| id.to_name())
8219 .map_err(|e| PyValueError::new_err(e.to_string()))
8220}
8221
8222#[pyfunction]
8224#[pyo3(signature = (child, rx, projectile="n"))]
8225fn rxname_parent(child: &str, rx: &Bound<'_, PyAny>, projectile: &str) -> PyResult<String> {
8226 let p = parse_projectile(projectile)?;
8227 let rx = resolve_rx_id(rx)?;
8228 let child_id = NuclideId::from_name(child)
8229 .map_err(|e| PyValueError::new_err(format!("`{child}`: {e}")))?;
8230 nucleide_nuclei::rxname::parent(child_id, rx, p)
8231 .map(|id| id.to_name())
8232 .map_err(|e| PyValueError::new_err(e.to_string()))
8233}
8234
8235#[pyfunction]
8237fn particle_is_valid(spec: &str) -> bool {
8238 nucleide_nuclei::particles::is_valid(spec)
8239}
8240
8241#[pyfunction]
8243fn particle_is_valid_pdc(n: i32) -> bool {
8244 nucleide_nuclei::particles::is_valid_pdc(n)
8245}
8246
8247#[pyfunction]
8249fn particle_is_hydrogen(spec: &str) -> bool {
8250 nucleide_nuclei::particles::is_hydrogen(spec)
8251}
8252
8253#[pyfunction]
8255fn particle_is_heavy_ion(spec: &str) -> bool {
8256 nucleide_nuclei::particles::is_heavy_ion(spec)
8257}
8258
8259#[pyfunction]
8261#[pyo3(signature = (name, source="EPA"))]
8262fn dose_f1(name: &str, source: &str) -> PyResult<Option<f64>> {
8263 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
8264 let s = parse_dose_source(source)?;
8265 Ok(nucleide_nuclei::data::dose_f1_by_name(name, s))
8266}
8267
8268#[pyfunction]
8270#[pyo3(signature = (name, source="EPA"))]
8271fn dose_lung_model(name: &str, source: &str) -> PyResult<Option<char>> {
8272 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
8273 let s = parse_dose_source(source)?;
8274 Ok(nucleide_nuclei::data::dose_lung_model_by_name(name, s))
8275}
8276
8277fn bare_element_z(name: &str) -> Option<u32> {
8279 let t = name.trim();
8280 if t.is_empty() {
8281 return None;
8282 }
8283 let mut chars = t.chars();
8284 let first = chars.next()?.to_uppercase().next()?;
8285 let rest: String = chars.collect::<String>().to_lowercase();
8286 let canon = format!("{first}{rest}");
8287 nucleide_nuclei::element_z(&canon)
8288}
8289
8290fn mat_from_comp_elements(comp: BTreeMap<String, f64>) -> PyResult<nucleide_material::Material> {
8291 let mut mat = nucleide_material::Material::new();
8292 for (name, grams) in &comp {
8293 let id = match NuclideId::from_name(name) {
8294 Ok(id) => id,
8295 Err(_) => match bare_element_z(name) {
8296 Some(z) => NuclideId::from_nucid(z * 10_000_000),
8297 None => {
8298 return Err(PyValueError::new_err(format!(
8299 "`{name}`: unknown nuclide or element"
8300 )));
8301 }
8302 },
8303 };
8304 mat.add_nuclide(id, *grams);
8305 }
8306 Ok(mat)
8307}
8308
8309fn mat_to_comp_elements(mat: &nucleide_material::Material) -> BTreeMap<String, f64> {
8310 let mut out = BTreeMap::new();
8311 for (&id, &grams) in &mat.comp {
8312 let key = if id.a() == 0 && id.state() == 0 {
8313 nucleide_nuclei::element_symbol(id.z())
8314 .unwrap_or("X")
8315 .to_string()
8316 } else {
8317 id.to_name()
8318 };
8319 *out.entry(key).or_insert(0.0) += grams;
8320 }
8321 out
8322}
8323
8324#[pyfunction]
8328fn mix_by_mass(parts: Vec<(BTreeMap<String, f64>, f64)>) -> PyResult<BTreeMap<String, f64>> {
8329 let mats: Vec<nucleide_material::Material> = parts
8330 .iter()
8331 .map(|(comp, _)| mat_from_comp_elements(comp.clone()))
8332 .collect::<PyResult<_>>()?;
8333 let refs: Vec<(&nucleide_material::Material, f64)> =
8334 mats.iter().zip(parts.iter().map(|(_, w)| *w)).collect();
8335 let out = nucleide_material::Material::mix_by_mass(&refs)
8336 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8337 Ok(mat_to_comp_elements(&out))
8338}
8339
8340#[pyfunction]
8344fn mix_by_volume(parts: Vec<(BTreeMap<String, f64>, f64, f64)>) -> PyResult<BTreeMap<String, f64>> {
8345 let mut mats: Vec<nucleide_material::Material> = Vec::with_capacity(parts.len());
8346 for (comp, _, density) in &parts {
8347 let mut m = mat_from_comp_elements(comp.clone())?;
8348 m.set_density(Some(*density));
8349 mats.push(m);
8350 }
8351 let refs: Vec<(&nucleide_material::Material, f64)> =
8352 mats.iter().zip(parts.iter().map(|(_, v, _)| *v)).collect();
8353 let out = nucleide_material::Material::mix_by_volume(&refs)
8354 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8355 Ok(mat_to_comp_elements(&out))
8356}
8357
8358#[pyfunction]
8360fn specific_activity(comp: BTreeMap<String, f64>) -> PyResult<f64> {
8361 let mat = mat_from_comp_elements(comp)?;
8362 let analytics = nucleide_material::Analytics {
8363 masses: &nucleide_material::Ame2020,
8364 decays: &nucleide_material::ChainDecays,
8365 };
8366 mat.specific_activity(&analytics)
8367 .map_err(|e| PyValueError::new_err(e.to_string()))
8368}
8369
8370#[pyfunction]
8374#[pyo3(signature = (entries, cross_sections=None))]
8375fn materials_doc_to_xml(
8376 entries: Vec<(String, BTreeMap<String, f64>, f64)>,
8377 cross_sections: Option<String>,
8378) -> PyResult<String> {
8379 let mut doc = nucleide_material::MaterialsDoc::new();
8380 if let Some(path) = cross_sections {
8381 doc = doc.cross_sections(path);
8382 }
8383 for (name, comp, density) in entries {
8384 let mut mat = mat_from_comp_elements(comp)?;
8385 mat.set_density(Some(density));
8386 doc = doc.push(name, mat);
8387 }
8388 doc.to_xml()
8389 .map_err(|e| PyValueError::new_err(e.to_string()))
8390}
8391
8392#[pyfunction]
8396fn expand_elements(comp: BTreeMap<String, f64>) -> PyResult<BTreeMap<String, f64>> {
8397 let mut mat = mat_from_comp_elements(comp)?;
8398 mat.expand_elements(
8399 &nucleide_material::Ame2020,
8400 &nucleide_material::NaturalAbundances,
8401 )
8402 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8403 Ok(mat_to_comp_elements(&mat))
8404}
8405
8406#[pyfunction]
8408fn collapse_elements(comp: BTreeMap<String, f64>) -> PyResult<BTreeMap<String, f64>> {
8409 let mat = mat_from_comp_elements(comp)?;
8410 Ok(mat_to_comp_elements(&mat.collapse_elements()))
8411}
8412
8413fn parse_fluka_nuc(spec: &str) -> PyResult<nucleide_fluka_io::material::FlukaNuc> {
8414 use nucleide_fluka_io::material::FlukaNuc;
8415 if let Ok(id) = NuclideId::from_name(spec) {
8416 return Ok(FlukaNuc::Nuclide(id));
8417 }
8418 if let Some(z) = bare_element_z(spec) {
8419 return Ok(FlukaNuc::Element(z));
8420 }
8421 if let Ok(z) = spec.trim().parse::<u32>() {
8422 if nucleide_nuclei::element_symbol(z).is_some() {
8423 return Ok(FlukaNuc::Element(z));
8424 }
8425 }
8426 Err(PyValueError::new_err(format!(
8427 "`{spec}`: unknown nuclide or element"
8428 )))
8429}
8430
8431#[pyfunction]
8433fn fluka_material_str(fid: u32, nuc: &str, density: f64) -> PyResult<String> {
8434 let parsed = parse_fluka_nuc(nuc)?;
8435 nucleide_fluka_io::material::material_str(fid, parsed, density)
8436 .map_err(|e| PyValueError::new_err(e.to_string()))
8437}
8438
8439#[pyfunction]
8443#[pyo3(signature = (fid, compound_name, density, frac_type="mass", components=None))]
8444fn fluka_compound_str(
8445 fid: u32,
8446 compound_name: &str,
8447 density: f64,
8448 frac_type: &str,
8449 components: Option<Vec<(String, f64)>>,
8450) -> PyResult<String> {
8451 use nucleide_fluka_io::material::{Component, FracType};
8452 let frac = match frac_type.trim().to_ascii_lowercase().as_str() {
8453 "mass" => FracType::Mass,
8454 "atom" => FracType::Atom,
8455 other => {
8456 return Err(PyValueError::new_err(format!(
8457 "frac_type must be mass|atom, got `{other}`"
8458 )));
8459 }
8460 };
8461 let pairs = components.unwrap_or_default();
8462 let comps: Vec<Component> = pairs
8463 .iter()
8464 .map(|(nuc, frac)| parse_fluka_nuc(nuc).map(|n| Component::new(n, *frac)))
8465 .collect::<PyResult<_>>()?;
8466 nucleide_fluka_io::material::compound_str(fid, compound_name, density, frac, &comps)
8467 .map_err(|e| PyValueError::new_err(e.to_string()))
8468}
8469
8470#[pyfunction]
8472fn fluka_builtin_set() -> Vec<String> {
8473 let mut out: Vec<String> = nucleide_fluka_io::material::builtin_set()
8474 .into_iter()
8475 .map(str::to_string)
8476 .collect();
8477 out.sort();
8478 out
8479}
8480
8481#[pyfunction]
8483fn alara_validate_deck(text: &str) -> PyResult<()> {
8484 let deck = nucleide_alara_io::AlaraDeck::parse(text)
8485 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8486 deck.validate()
8487 .map_err(|e| PyValueError::new_err(e.to_string()))
8488}
8489
8490#[pyfunction]
8492fn alara_check_block(block: &str, line: usize) -> PyResult<()> {
8493 nucleide_alara_io::AlaraDeck::check_block(block, line)
8494 .map_err(|e| PyValueError::new_err(e.to_string()))
8495}
8496
8497#[pyfunction]
8499fn alara_flux_total(name: &str, text: &str) -> PyResult<f64> {
8500 nucleide_alara_io::FluxSpec::parse(name, text)
8501 .map(|f| f.total())
8502 .map_err(|e| PyValueError::new_err(e.to_string()))
8503}
8504
8505#[pyfunction]
8507fn alara_flux_len(name: &str, text: &str) -> PyResult<usize> {
8508 nucleide_alara_io::FluxSpec::parse(name, text)
8509 .map(|f| f.len())
8510 .map_err(|e| PyValueError::new_err(e.to_string()))
8511}
8512
8513#[pyfunction]
8515fn alara_output_totals(
8516 py: Python<'_>,
8517 text: &str,
8518 run_lbl: &str,
8519) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
8520 let owned_text = text.to_owned();
8521 let owned_lbl = run_lbl.to_owned();
8522 let frame = py
8523 .detach(move || {
8524 nucleide_alara_io::output::ResponseFrame::parse(&owned_text, &owned_lbl)
8525 .map(|f| f.totals())
8526 })
8527 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8528 Ok(frame
8529 .rows
8530 .iter()
8531 .map(|r| fispact_row_to_map(py, r))
8532 .collect())
8533}
8534
8535#[pyfunction]
8537fn alara_output_total_activity(text: &str, run_lbl: &str) -> PyResult<f64> {
8538 nucleide_alara_io::output::ResponseFrame::parse(text, run_lbl)
8539 .map(|f| f.total_activity())
8540 .map_err(|e| PyValueError::new_err(e.to_string()))
8541}
8542
8543#[pyfunction]
8545fn alara_photon_total_strength(text: &str) -> PyResult<f64> {
8546 nucleide_alara_io::PhotonSource::from_str(text)
8547 .map(|p| p.total_strength())
8548 .map_err(|e| PyValueError::new_err(e.to_string()))
8549}
8550
8551#[pyfunction]
8553#[pyo3(signature = (deck_text, top=None))]
8554fn alara_schedule_total_time(deck_text: &str, top: Option<&str>) -> PyResult<f64> {
8555 let owned = deck_text.to_owned();
8556 let owned_top = top.map(str::to_owned);
8557 let steps =
8558 expand_deck_schedules(&owned, owned_top.as_deref()).map_err(PyValueError::new_err)?;
8559 Ok(nucleide_alara_io::schedule::total_time(&steps))
8560}
8561
8562#[pyfunction]
8571fn alara_clearance_eu_table() -> BTreeMap<String, f64> {
8572 nucleide_alara_io::ClearanceTable::eu_annex_vii()
8573 .iter()
8574 .map(|(nuc, limit)| (nucleide_nuclei::dialects::serpent(nuc), limit))
8575 .collect()
8576}
8577
8578fn clearance_key(key: &str) -> PyResult<NuclideId> {
8580 nucleide_nuclei::dialects::normalize_nuclide_name(key)
8581 .map_err(|e| PyValueError::new_err(format!("bad nuclide name `{key}`: {e}")))
8582}
8583
8584fn clearance_pairs(map: &BTreeMap<String, f64>, what: &str) -> PyResult<Vec<(NuclideId, f64)>> {
8586 map.iter()
8587 .map(|(name, value)| Ok((clearance_key(name)?, *value)))
8588 .collect::<PyResult<_>>()
8589 .map_err(|e| PyValueError::new_err(format!("{what}: {e}")))
8590}
8591
8592fn clearance_table_from(
8594 map: &BTreeMap<String, f64>,
8595) -> PyResult<nucleide_alara_io::ClearanceTable> {
8596 let mut table = nucleide_alara_io::ClearanceTable::new();
8597 for (nuc, limit) in clearance_pairs(map, "limits")? {
8598 table
8599 .insert(nuc, limit)
8600 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8601 }
8602 Ok(table)
8603}
8604
8605#[pyfunction]
8613#[pyo3(signature = (inventory, limits=None))]
8614fn alara_clearance_index(
8615 inventory: BTreeMap<String, f64>,
8616 limits: Option<BTreeMap<String, f64>>,
8617) -> PyResult<f64> {
8618 let table = match limits {
8619 Some(map) => clearance_table_from(&map)?,
8620 None => nucleide_alara_io::ClearanceTable::eu_annex_vii(),
8621 };
8622 let pairs = clearance_pairs(&inventory, "inventory")?;
8623 nucleide_alara_io::clearance_index(&pairs, &table)
8624 .map_err(|e| PyValueError::new_err(e.to_string()))
8625}
8626
8627#[pyfunction]
8636#[pyo3(signature = (inventory, limits=None))]
8637fn alara_sum_of_fractions(
8638 py: Python<'_>,
8639 inventory: BTreeMap<String, f64>,
8640 limits: Option<BTreeMap<String, f64>>,
8641) -> PyResult<BTreeMap<String, Py<PyAny>>> {
8642 let table = match limits {
8643 Some(map) => clearance_table_from(&map)?,
8644 None => nucleide_alara_io::ClearanceTable::eu_annex_vii(),
8645 };
8646 let pairs = clearance_pairs(&inventory, "inventory")?;
8647 let out = nucleide_alara_io::sum_of_fractions(&pairs, &table)
8648 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8649 let mut d = BTreeMap::new();
8650 d.insert(
8651 "sum".to_string(),
8652 out.sum.into_pyobject(py).unwrap().unbind().into_any(),
8653 );
8654 d.insert(
8655 "class".to_string(),
8656 out.class
8657 .to_string()
8658 .into_pyobject(py)
8659 .unwrap()
8660 .unbind()
8661 .into_any(),
8662 );
8663 d.insert(
8664 "max_fraction".to_string(),
8665 out.max_fraction
8666 .into_pyobject(py)
8667 .unwrap()
8668 .unbind()
8669 .into_any(),
8670 );
8671 d.insert(
8672 "max_nuclide".to_string(),
8673 out.max_nuclide
8674 .map(nucleide_nuclei::dialects::serpent)
8675 .into_pyobject(py)
8676 .unwrap()
8677 .unbind()
8678 .into_any(),
8679 );
8680 Ok(d)
8681}
8682
8683#[pyfunction]
8685fn origen_tape6_find(py: Python<'_>, text: &str, nuclide: &str) -> PyResult<Option<Py<PyAny>>> {
8686 use pyo3::types::PyDict;
8687 let owned = text.to_owned();
8688 let query = nuclide.to_owned();
8689 let found = py
8690 .detach(move || nucleide_origen_io::Tape6::parse(&owned).map(|t| t.find(&query).cloned()))
8691 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8692 Ok(found.map(|r| {
8693 let d = PyDict::new(py);
8694 d.set_item("nuclide", &r.nuclide).ok();
8695 d.set_item("grams", r.grams).ok();
8696 d.set_item("activity_bq", r.activity_bq).ok();
8697 d.into_any().unbind()
8698 }))
8699}
8700
8701#[pyfunction]
8703fn origen_tape6_total_activity(text: &str) -> PyResult<f64> {
8704 nucleide_origen_io::Tape6::parse(text)
8705 .map(|t| t.total_activity())
8706 .map_err(|e| PyValueError::new_err(e.to_string()))
8707}
8708
8709#[pyfunction]
8711fn origen_tape9_find(py: Python<'_>, text: &str, nuclide: &str) -> PyResult<Option<Py<PyAny>>> {
8712 use pyo3::types::PyDict;
8713 let owned = text.to_owned();
8714 let query = nuclide.to_owned();
8715 let found = py
8716 .detach(move || {
8717 nucleide_origen_io::Tape9Entry::parse(&owned)
8718 .map(|entries| nucleide_origen_io::Tape9Entry::find(&entries, &query).cloned())
8719 })
8720 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8721 Ok(found.map(|e| {
8722 let d = PyDict::new(py);
8723 d.set_item("nuclide", &e.nuclide).ok();
8724 d.set_item("decay_const", e.decay_const).ok();
8725 d.into_any().unbind()
8726 }))
8727}
8728
8729#[pyfunction]
8731#[pyo3(signature = (text, kind="rtflux"))]
8732fn cccc_rtflux_npoints(text: &str, kind: &str) -> PyResult<usize> {
8733 let flux_kind = parse_flux_kind(kind)?;
8734 nucleide_cccc_io::FluxFile::parse(flux_kind, text)
8735 .map(|f| f.npoints())
8736 .map_err(|e| PyValueError::new_err(e.to_string()))
8737}
8738
8739#[pyfunction]
8741#[pyo3(signature = (text, kind="rtflux", index=0))]
8742fn cccc_rtflux_point(text: &str, kind: &str, index: usize) -> PyResult<Option<Vec<f64>>> {
8743 let flux_kind = parse_flux_kind(kind)?;
8744 let flux = nucleide_cccc_io::FluxFile::parse(flux_kind, text)
8745 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8746 Ok(flux.point(index).map(<[f64]>::to_vec))
8747}
8748
8749#[pyfunction]
8751#[pyo3(signature = (text, kind="rtflux"))]
8752fn cccc_rtflux_total(text: &str, kind: &str) -> PyResult<f64> {
8753 let flux_kind = parse_flux_kind(kind)?;
8754 nucleide_cccc_io::FluxFile::parse(flux_kind, text)
8755 .map(|f| f.total())
8756 .map_err(|e| PyValueError::new_err(e.to_string()))
8757}
8758
8759fn parse_flux_kind(kind: &str) -> PyResult<nucleide_cccc_io::rtflux::FluxKind> {
8760 match kind.to_ascii_lowercase().as_str() {
8761 "rtflux" => Ok(nucleide_cccc_io::rtflux::FluxKind::Rtflux),
8762 "atflux" => Ok(nucleide_cccc_io::rtflux::FluxKind::Atflux),
8763 "rzflux" => Ok(nucleide_cccc_io::rtflux::FluxKind::Rzflux),
8764 other => Err(PyValueError::new_err(format!(
8765 "kind must be rtflux|atflux|rzflux, got `{other}`"
8766 ))),
8767 }
8768}
8769
8770#[pyfunction]
8772fn cccc_isotxs_find(py: Python<'_>, text: &str, label: &str) -> PyResult<Option<Py<PyAny>>> {
8773 use pyo3::types::PyDict;
8774 let owned = text.to_owned();
8775 let query = label.to_owned();
8776 let found = py
8777 .detach(move || {
8778 nucleide_cccc_io::IsotxsLib::parse(&owned).map(|lib| lib.find(&query).cloned())
8779 })
8780 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8781 Ok(found.map(|n| {
8782 let d = PyDict::new(py);
8783 d.set_item("label", &n.label).ok();
8784 d.set_item("zaid", &n.zaid).ok();
8785 d.set_item("groups", n.groups).ok();
8786 d.set_item("total_xs", n.total_xs.clone()).ok();
8787 d.into_any().unbind()
8788 }))
8789}
8790
8791#[pyfunction]
8793fn cccc_isotxs_len(text: &str) -> PyResult<usize> {
8794 nucleide_cccc_io::IsotxsLib::parse(text)
8795 .map(|lib| lib.len())
8796 .map_err(|e| PyValueError::new_err(e.to_string()))
8797}
8798
8799#[pyfunction]
8801fn fispact_is_output(path: &str) -> bool {
8802 nucleide_fispact_io::is_fispact_output(path)
8803}
8804
8805#[pyfunction]
8807fn enrichment_prod_per_feed(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
8808 nucleide_enrichment::prod_per_feed(x_feed, x_prod, x_tail)
8809}
8810
8811#[pyfunction]
8813fn enrichment_tail_per_feed(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
8814 nucleide_enrichment::tail_per_feed(x_feed, x_prod, x_tail)
8815}
8816
8817#[pyfunction]
8819fn enrichment_tail_per_prod(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
8820 nucleide_enrichment::tail_per_prod(x_feed, x_prod, x_tail)
8821}
8822
8823#[pyfunction]
8825fn enrichment_feed_per_prod(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
8826 nucleide_enrichment::feed_per_prod(x_feed, x_prod, x_tail)
8827}
8828
8829#[pyfunction]
8831fn enrichment_feed_per_tail(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
8832 nucleide_enrichment::feed_per_tail(x_feed, x_prod, x_tail)
8833}
8834
8835#[pyfunction]
8837fn enrichment_prod_per_tail(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
8838 nucleide_enrichment::prod_per_tail(x_feed, x_prod, x_tail)
8839}
8840
8841#[pyfunction]
8843#[allow(non_snake_case)]
8844fn enrichment_alphastar_i(alpha: f64, Mstar: f64, M_i: f64) -> f64 {
8845 nucleide_enrichment::alphastar_i(alpha, Mstar, M_i)
8846}
8847
8848#[pyfunction]
8855fn kinetics_from_ifp(
8856 py: Python<'_>,
8857 betas: Vec<f64>,
8858 lambda_gen: f64,
8859 lambdas: Vec<f64>,
8860) -> PyResult<Py<PyAny>> {
8861 use pyo3::types::PyDict;
8862 let params = nucleide_kinetics::KineticParams::from_ifp(betas, lambda_gen, lambdas)
8863 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8864 let d = PyDict::new(py);
8865 d.set_item("betas", params.betas()).ok();
8866 d.set_item("lambdas", params.lambdas()).ok();
8867 d.set_item("lambda_gen", params.lambda_gen()).ok();
8868 d.set_item("beta_total", params.beta_total()).ok();
8869 d.set_item("groups", params.groups()).ok();
8870 Ok(d.into_any().unbind())
8871}
8872
8873#[pyfunction]
8876#[pyo3(signature = (tally, selection="total", tolerance=0.5, null_value=0.0))]
8877fn magic_with(
8878 tally: &PyMeshTally,
8879 selection: &str,
8880 tolerance: f64,
8881 null_value: f64,
8882) -> PyResult<PyMagicOutput> {
8883 let sel = match selection.trim().to_ascii_lowercase().as_str() {
8884 "total" => nucleide_vr_tools::magic::MagicSelection::Total,
8885 "per_group" | "pergroup" | "per-group" => {
8886 nucleide_vr_tools::magic::MagicSelection::PerGroup
8887 }
8888 other => {
8889 return Err(PyValueError::new_err(format!(
8890 "selection must be total|per_group, got `{other}`"
8891 )));
8892 }
8893 };
8894 let params = nucleide_vr_tools::magic::MagicParams {
8895 tolerance,
8896 null_value,
8897 };
8898 nucleide_vr_tools::magic::magic_with(&tally.inner, sel, params)
8899 .map(|inner| PyMagicOutput { inner })
8900 .map_err(|e| PyValueError::new_err(e.to_string()))
8901}
8902
8903#[pyfunction]
8907#[pyo3(signature = (tally, output, mesh_id=1, window_id=1, upper_bound_ratio=5.0, survival_ratio=3.0, max_split=10, weight_cutoff=1e-38))]
8908#[allow(clippy::too_many_arguments)]
8909fn emit_openmc_weight_windows(
8910 py: Python<'_>,
8911 tally: &PyMeshTally,
8912 output: &PyMagicOutput,
8913 mesh_id: u32,
8914 window_id: u32,
8915 upper_bound_ratio: f64,
8916 survival_ratio: f64,
8917 max_split: u32,
8918 weight_cutoff: f64,
8919) -> PyResult<Py<PyAny>> {
8920 use pyo3::types::PyDict;
8921 let options = nucleide_vr_tools::windows::OpenMcOptions {
8922 mesh_id,
8923 window_id,
8924 upper_bound_ratio,
8925 survival_ratio,
8926 max_split,
8927 weight_cutoff,
8928 };
8929 let out = nucleide_vr_tools::windows::emit_openmc_weight_windows(
8930 &output.inner,
8931 &tally.inner,
8932 &options,
8933 )
8934 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8935 let d = PyDict::new(py);
8936 d.set_item("xml", out.xml)?;
8937 d.set_item("notes", out.notes)?;
8938 Ok(d.into_any().unbind())
8939}
8940
8941#[pyfunction]
8945#[pyo3(signature = (tally, output, name="ww1", file="wwindows.wwd"))]
8946fn emit_serpent_wwin(
8947 py: Python<'_>,
8948 tally: &PyMeshTally,
8949 output: &PyMagicOutput,
8950 name: &str,
8951 file: &str,
8952) -> PyResult<Py<PyAny>> {
8953 use pyo3::types::PyDict;
8954 let out =
8955 nucleide_vr_tools::windows::emit_serpent_wwin(&output.inner, &tally.inner, name, file)
8956 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8957 let d = PyDict::new(py);
8958 d.set_item("text", out.text)?;
8959 d.set_item("card", out.card)?;
8960 d.set_item("notes", out.notes)?;
8961 Ok(d.into_any().unbind())
8962}
8963
8964#[pyfunction]
8966fn mcpl_statsum_validate(comment: &str) -> PyResult<String> {
8967 nucleide_mcpl_io::statsum_validate(comment)
8968 .map(str::to_string)
8969 .map_err(PyValueError::new_err)
8970}
8971
8972#[pyfunction]
8974fn mcpl_statsum_comment(key: &str, value: f64) -> PyResult<String> {
8975 nucleide_mcpl_io::statsum_comment(key, value).map_err(|e| PyValueError::new_err(e.to_string()))
8976}
8977
8978#[pymodule]
8980fn _internal(m: &Bound<'_, PyModule>) -> PyResult<()> {
8981 m.add_function(wrap_pyfunction!(version, m)?)?;
8982 m.add_function(wrap_pyfunction!(from_zaid, m)?)?;
8983 m.add_function(wrap_pyfunction!(atomic_mass, m)?)?;
8984 m.add_function(wrap_pyfunction!(natural_abundance, m)?)?;
8985 m.add_function(wrap_pyfunction!(rxname_id, m)?)?;
8986 m.add_function(wrap_pyfunction!(rxname_name, m)?)?;
8987 m.add_function(wrap_pyfunction!(rxname_mt, m)?)?;
8988 m.add_function(wrap_pyfunction!(rxname_label, m)?)?;
8989 m.add_function(wrap_pyfunction!(rxname_doc, m)?)?;
8990 m.add_function(wrap_pyfunction!(rxname_reaction, m)?)?;
8991 m.add_function(wrap_pyfunction!(rxname_id_from_nucdelta, m)?)?;
8992 m.add_function(wrap_pyfunction!(rxname_child, m)?)?;
8993 m.add_function(wrap_pyfunction!(rxname_parent, m)?)?;
8994 m.add_function(wrap_pyfunction!(particle_is_valid, m)?)?;
8995 m.add_function(wrap_pyfunction!(particle_is_valid_pdc, m)?)?;
8996 m.add_function(wrap_pyfunction!(particle_is_hydrogen, m)?)?;
8997 m.add_function(wrap_pyfunction!(particle_is_heavy_ion, m)?)?;
8998 m.add_function(wrap_pyfunction!(dose_f1, m)?)?;
8999 m.add_function(wrap_pyfunction!(dose_lung_model, m)?)?;
9000 m.add_function(wrap_pyfunction!(read_xsdir, m)?)?;
9001 m.add_function(wrap_pyfunction!(read_meshtal, m)?)?;
9002 m.add_function(wrap_pyfunction!(read_wwinp, m)?)?;
9003 m.add_function(wrap_pyfunction!(read_mctal, m)?)?;
9004 m.add_function(wrap_pyfunction!(read_ssw, m)?)?;
9005 m.add_function(wrap_pyfunction!(read_ptrac, m)?)?;
9006 m.add_function(wrap_pyfunction!(read_mcpl, m)?)?;
9007 m.add_function(wrap_pyfunction!(write_mcpl, m)?)?;
9008 m.add_function(wrap_pyfunction!(ssw2mcpl, m)?)?;
9009 m.add_function(wrap_pyfunction!(mcpl2ssw, m)?)?;
9010 m.add_function(wrap_pyfunction!(merge_mcpl, m)?)?;
9011 m.add_function(wrap_pyfunction!(extract_mcpl, m)?)?;
9012 m.add_function(wrap_pyfunction!(mcpl_stats, m)?)?;
9013 m.add_function(wrap_pyfunction!(repair_mcpl, m)?)?;
9014 m.add_function(wrap_pyfunction!(read_endl, m)?)?;
9015 m.add_function(wrap_pyfunction!(endl_endftod, m)?)?;
9016 m.add_function(wrap_pyfunction!(combine_ssw_files, m)?)?;
9017 m.add_function(wrap_pyfunction!(read_chain, m)?)?;
9018 m.add_function(wrap_pyfunction!(build_depletion_system, m)?)?;
9019 m.add_function(wrap_pyfunction!(deplete, m)?)?;
9020 m.add_function(wrap_pyfunction!(deplete_series, m)?)?;
9021 m.add_function(wrap_pyfunction!(simple_xs, m)?)?;
9022 m.add_function(wrap_pyfunction!(scattering_length, m)?)?;
9023 m.add_function(wrap_pyfunction!(decay_energy, m)?)?;
9024 m.add_function(wrap_pyfunction!(decay_branches, m)?)?;
9025 m.add_function(wrap_pyfunction!(decay_branch_fraction, m)?)?;
9026 m.add_function(wrap_pyfunction!(fission_yields, m)?)?;
9027 m.add_function(wrap_pyfunction!(fission_yield, m)?)?;
9028 m.add_function(wrap_pyfunction!(normalize_nuclide, m)?)?;
9029 m.add_function(wrap_pyfunction!(decay_heat, m)?)?;
9030 m.add_function(wrap_pyfunction!(dose_factor, m)?)?;
9031 m.add_function(wrap_pyfunction!(dose_per_g, m)?)?;
9032 m.add_function(wrap_pyfunction!(parse_fgr15_table, m)?)?;
9033 m.add_function(wrap_pyfunction!(fgr15_age_index, m)?)?;
9034 m.add_function(wrap_pyfunction!(mix_by_mass, m)?)?;
9035 m.add_function(wrap_pyfunction!(mix_by_volume, m)?)?;
9036 m.add_function(wrap_pyfunction!(specific_activity, m)?)?;
9037 m.add_function(wrap_pyfunction!(materials_doc_to_xml, m)?)?;
9038 m.add_function(wrap_pyfunction!(expand_elements, m)?)?;
9039 m.add_function(wrap_pyfunction!(collapse_elements, m)?)?;
9040 m.add_function(wrap_pyfunction!(read_serpent, m)?)?;
9041 m.add_function(wrap_pyfunction!(read_usrbin, m)?)?;
9042 m.add_function(wrap_pyfunction!(fluka_material_str, m)?)?;
9043 m.add_function(wrap_pyfunction!(fluka_compound_str, m)?)?;
9044 m.add_function(wrap_pyfunction!(fluka_builtin_set, m)?)?;
9045 m.add_function(wrap_pyfunction!(magic, m)?)?;
9046 m.add_function(wrap_pyfunction!(magic_with, m)?)?;
9047 m.add_function(wrap_pyfunction!(emit_openmc_weight_windows, m)?)?;
9048 m.add_function(wrap_pyfunction!(emit_serpent_wwin, m)?)?;
9049 m.add_function(wrap_pyfunction!(write_ssw, m)?)?;
9050 m.add_function(wrap_pyfunction!(mesh_to_geom, m)?)?;
9051 m.add_function(wrap_pyfunction!(half_life, m)?)?;
9052 m.add_function(wrap_pyfunction!(decay_constant, m)?)?;
9053 m.add_function(wrap_pyfunction!(q_value_capture, m)?)?;
9054 m.add_function(wrap_pyfunction!(q_value_alpha, m)?)?;
9055 m.add_function(wrap_pyfunction!(read_inp, m)?)?;
9056 m.add_function(wrap_pyfunction!(from_formula, m)?)?;
9057 m.add_function(wrap_pyfunction!(activity, m)?)?;
9058 m.add_function(wrap_pyfunction!(to_xml, m)?)?;
9059 m.add_function(wrap_pyfunction!(alara_parse_deck, m)?)?;
9060 m.add_function(wrap_pyfunction!(alara_parse_flux, m)?)?;
9061 m.add_function(wrap_pyfunction!(alara_parse_output, m)?)?;
9062 m.add_function(wrap_pyfunction!(alara_expand_schedule, m)?)?;
9063 m.add_function(wrap_pyfunction!(alara_validate_deck, m)?)?;
9064 m.add_function(wrap_pyfunction!(alara_check_block, m)?)?;
9065 m.add_function(wrap_pyfunction!(alara_flux_total, m)?)?;
9066 m.add_function(wrap_pyfunction!(alara_flux_len, m)?)?;
9067 m.add_function(wrap_pyfunction!(alara_output_totals, m)?)?;
9068 m.add_function(wrap_pyfunction!(alara_output_total_activity, m)?)?;
9069 m.add_function(wrap_pyfunction!(alara_photon_total_strength, m)?)?;
9070 m.add_function(wrap_pyfunction!(alara_schedule_total_time, m)?)?;
9071 m.add_function(wrap_pyfunction!(alara_clearance_eu_table, m)?)?;
9072 m.add_function(wrap_pyfunction!(alara_clearance_index, m)?)?;
9073 m.add_function(wrap_pyfunction!(alara_sum_of_fractions, m)?)?;
9074 m.add_function(wrap_pyfunction!(isotxs_parse, m)?)?;
9075 m.add_function(wrap_pyfunction!(rtflux_parse, m)?)?;
9076 m.add_function(wrap_pyfunction!(cccc_rtflux_npoints, m)?)?;
9077 m.add_function(wrap_pyfunction!(cccc_rtflux_point, m)?)?;
9078 m.add_function(wrap_pyfunction!(cccc_rtflux_total, m)?)?;
9079 m.add_function(wrap_pyfunction!(cccc_isotxs_find, m)?)?;
9080 m.add_function(wrap_pyfunction!(cccc_isotxs_len, m)?)?;
9081 m.add_function(wrap_pyfunction!(partisn_render, m)?)?;
9082 m.add_function(wrap_pyfunction!(partisn_validate, m)?)?;
9083 m.add_function(wrap_pyfunction!(fispact_parse_output, m)?)?;
9084 m.add_function(wrap_pyfunction!(fispact_parse_clearance, m)?)?;
9085 m.add_function(wrap_pyfunction!(fispact_is_output, m)?)?;
9086 m.add_function(wrap_pyfunction!(origen_parse_tape5, m)?)?;
9087 m.add_function(wrap_pyfunction!(origen_parse_tape6, m)?)?;
9088 m.add_function(wrap_pyfunction!(origen_parse_tape9, m)?)?;
9089 m.add_function(wrap_pyfunction!(origen_tape6_find, m)?)?;
9090 m.add_function(wrap_pyfunction!(origen_tape6_total_activity, m)?)?;
9091 m.add_function(wrap_pyfunction!(origen_tape9_find, m)?)?;
9092 m.add_function(wrap_pyfunction!(r2s_from_deck, m)?)?;
9093 m.add_function(wrap_pyfunction!(r2s_from_snapshot, m)?)?;
9094 m.add_function(wrap_pyfunction!(r2s_validate, m)?)?;
9095 m.add_function(wrap_pyfunction!(r2s_expand, m)?)?;
9096 m.add_function(wrap_pyfunction!(r2s_assemble, m)?)?;
9097 m.add_function(wrap_pyfunction!(r2s_tag_zone_strength, m)?)?;
9098 m.add_function(wrap_pyfunction!(r2s_photon_group_sums, m)?)?;
9099 m.add_function(wrap_pyfunction!(r2s_snapshot_inventory, m)?)?;
9100 m.add_function(wrap_pyfunction!(r2s_expand_sweep, m)?)?;
9101 m.add_function(wrap_pyfunction!(kinetics_solve, m)?)?;
9102 m.add_function(wrap_pyfunction!(kinetics_equilibrium, m)?)?;
9103 m.add_function(wrap_pyfunction!(kinetics_initial_rate, m)?)?;
9104 m.add_function(wrap_pyfunction!(kinetics_inhour_rho, m)?)?;
9105 m.add_function(wrap_pyfunction!(kinetics_stable_period, m)?)?;
9106 m.add_function(wrap_pyfunction!(kinetics_prompt_jump, m)?)?;
9107 m.add_function(wrap_pyfunction!(kinetics_from_ifp, m)?)?;
9108 m.add_function(wrap_pyfunction!(unfold_sandii, m)?)?;
9109 m.add_function(wrap_pyfunction!(unfold_forward_fold, m)?)?;
9110 m.add_function(wrap_pyfunction!(plasma_source_particles, m)?)?;
9111 m.add_function(wrap_pyfunction!(plasma_source_emit_cards, m)?)?;
9112 m.add_function(wrap_pyfunction!(plasma_source_spectrum_moments, m)?)?;
9113 m.add_function(wrap_pyfunction!(plasma_source_reactivity, m)?)?;
9114 m.add_function(wrap_pyfunction!(damage_nrt_dpa, m)?)?;
9115 m.add_function(wrap_pyfunction!(damage_arc_dpa, m)?)?;
9116 m.add_function(wrap_pyfunction!(damage_gas_appm, m)?)?;
9117 m.add_function(wrap_pyfunction!(damage_he_dpa_ratio, m)?)?;
9118 m.add_function(wrap_pyfunction!(damage_lindhard_partition, m)?)?;
9119 m.add_function(wrap_pyfunction!(damage_damage_energy, m)?)?;
9120 m.add_function(wrap_pyfunction!(damage_nrt_displacements, m)?)?;
9121 m.add_function(wrap_pyfunction!(damage_arc_efficiency, m)?)?;
9122 m.add_function(wrap_pyfunction!(damage_arc_displacements, m)?)?;
9123 m.add_function(wrap_pyfunction!(damage_fold_uq, m)?)?;
9124 m.add_function(wrap_pyfunction!(tritium_steady, m)?)?;
9125 m.add_function(wrap_pyfunction!(tritium_transient, m)?)?;
9126 m.add_function(wrap_pyfunction!(tritium_time_lag, m)?)?;
9127 m.add_function(wrap_pyfunction!(tritium_breakthrough, m)?)?;
9128 m.add_function(wrap_pyfunction!(tritium_oriani, m)?)?;
9129 m.add_function(wrap_pyfunction!(tritium_langmuir, m)?)?;
9130 m.add_function(wrap_pyfunction!(tritium_irreversible_fill, m)?)?;
9131 m.add_function(wrap_pyfunction!(tritium_sieverts, m)?)?;
9132 m.add_function(wrap_pyfunction!(tritium_recombination_rate, m)?)?;
9133 m.add_function(wrap_pyfunction!(tritium_layers_steady, m)?)?;
9134 m.add_function(wrap_pyfunction!(tritium_layers_transient, m)?)?;
9135 m.add_function(wrap_pyfunction!(spectroscopy_rect_smooth, m)?)?;
9136 m.add_function(wrap_pyfunction!(spectroscopy_five_point_smooth, m)?)?;
9137 m.add_function(wrap_pyfunction!(spectroscopy_calc_bg, m)?)?;
9138 m.add_function(wrap_pyfunction!(spectroscopy_gross_count, m)?)?;
9139 m.add_function(wrap_pyfunction!(spectroscopy_net_counts, m)?)?;
9140 m.add_function(wrap_pyfunction!(spectroscopy_energy_bins, m)?)?;
9141 m.add_function(wrap_pyfunction!(spectroscopy_detector_efficiency, m)?)?;
9142 m.add_function(wrap_pyfunction!(spectroscopy_fit_efficiency, m)?)?;
9143 m.add_function(wrap_pyfunction!(spectroscopy_xray_lines, m)?)?;
9144 m.add_function(wrap_pyfunction!(spectroscopy_sdef_decay_source, m)?)?;
9145 m.add_function(wrap_pyfunction!(spectroscopy_parse_dollar_spe, m)?)?;
9146 m.add_function(wrap_pyfunction!(spectroscopy_parse_spe, m)?)?;
9147 m.add_function(wrap_pyfunction!(spectroscopy_read_dollar_spe, m)?)?;
9148 m.add_function(wrap_pyfunction!(spectroscopy_read_spe, m)?)?;
9149 m.add_function(wrap_pyfunction!(spectroscopy_parse_lines_tsv, m)?)?;
9150 m.add_function(wrap_pyfunction!(spectroscopy_read_decay_lines, m)?)?;
9151 m.add_function(wrap_pyfunction!(uq_sample_mvn, m)?)?;
9152 m.add_function(wrap_pyfunction!(uq_sample_lhs, m)?)?;
9153 m.add_function(wrap_pyfunction!(uq_sample_lognormal, m)?)?;
9154 m.add_function(wrap_pyfunction!(uq_lognormal_mean, m)?)?;
9155 m.add_function(wrap_pyfunction!(uq_lognormal_cov, m)?)?;
9156 m.add_function(wrap_pyfunction!(uq_sample_mean, m)?)?;
9157 m.add_function(wrap_pyfunction!(uq_sample_cov, m)?)?;
9158 m.add_function(wrap_pyfunction!(uq_check_convergence, m)?)?;
9159 m.add_function(wrap_pyfunction!(uq_perturb_branches, m)?)?;
9160 m.add_function(wrap_pyfunction!(uq_perturb_energies, m)?)?;
9161 m.add_function(wrap_pyfunction!(uq_passthrough, m)?)?;
9162 m.add_function(wrap_pyfunction!(uq_perturb_fission_yields, m)?)?;
9163 m.add_function(wrap_pyfunction!(parse_deck, m)?)?;
9164 m.add_function(wrap_pyfunction!(read_deck, m)?)?;
9165 m.add_function(wrap_pyfunction!(parse_sdef, m)?)?;
9166 m.add_function(wrap_pyfunction!(parse_csg_to_openmc, m)?)?;
9167 m.add_function(wrap_pyfunction!(read_csg_to_openmc, m)?)?;
9168 m.add_function(wrap_pyfunction!(parse_csg_to_serpent, m)?)?;
9169 m.add_function(wrap_pyfunction!(read_csg_to_serpent, m)?)?;
9170 m.add_function(wrap_pyfunction!(parse_csg_to_phits, m)?)?;
9171 m.add_function(wrap_pyfunction!(read_csg_to_phits, m)?)?;
9172 m.add_function(wrap_pyfunction!(parse_csg_to_gdml, m)?)?;
9173 m.add_function(wrap_pyfunction!(read_csg_to_gdml, m)?)?;
9174 m.add_function(wrap_pyfunction!(cumulative_decays, m)?)?;
9175 m.add_function(wrap_pyfunction!(progeny, m)?)?;
9176 m.add_function(wrap_pyfunction!(branching_fraction, m)?)?;
9177 m.add_function(wrap_pyfunction!(decay_mode, m)?)?;
9178 m.add_function(wrap_pyfunction!(chain_edges, m)?)?;
9179 m.add_function(wrap_pyfunction!(armi_to_nucid, m)?)?;
9180 m.add_function(wrap_pyfunction!(nucid_to_armi, m)?)?;
9181 m.add_function(wrap_pyfunction!(mcc3_to_nucid, m)?)?;
9182 m.add_function(wrap_pyfunction!(check_labels, m)?)?;
9183 m.add_function(wrap_pyfunction!(audit_material, m)?)?;
9184 m.add_function(wrap_pyfunction!(separate_material, m)?)?;
9185 m.add_function(wrap_pyfunction!(blend_material, m)?)?;
9186 m.add_function(wrap_pyfunction!(enrichment_value_func, m)?)?;
9187 m.add_function(wrap_pyfunction!(enrichment_swu_per_feed, m)?)?;
9188 m.add_function(wrap_pyfunction!(enrichment_swu_per_prod, m)?)?;
9189 m.add_function(wrap_pyfunction!(enrichment_swu_per_tail, m)?)?;
9190 m.add_function(wrap_pyfunction!(enrichment_prod_per_feed, m)?)?;
9191 m.add_function(wrap_pyfunction!(enrichment_tail_per_feed, m)?)?;
9192 m.add_function(wrap_pyfunction!(enrichment_tail_per_prod, m)?)?;
9193 m.add_function(wrap_pyfunction!(enrichment_feed_per_prod, m)?)?;
9194 m.add_function(wrap_pyfunction!(enrichment_feed_per_tail, m)?)?;
9195 m.add_function(wrap_pyfunction!(enrichment_prod_per_tail, m)?)?;
9196 m.add_function(wrap_pyfunction!(enrichment_alphastar_i, m)?)?;
9197 m.add_function(wrap_pyfunction!(mcpl_statsum_validate, m)?)?;
9198 m.add_function(wrap_pyfunction!(mcpl_statsum_comment, m)?)?;
9199 m.add_class::<PyCusum>()?;
9200 m.add_function(wrap_pyfunction!(emit_cards, m)?)?;
9201 m.add_function(wrap_pyfunction!(emit_drift_table, m)?)?;
9202 m.add_function(wrap_pyfunction!(emit_armi_cards, m)?)?;
9203 m.add_function(wrap_pyfunction!(emit_armi_drift_table, m)?)?;
9204 m.add_class::<PyNuclide>()?;
9205 m.add_class::<PyParticle>()?;
9206 m.add_class::<PyXsdir>()?;
9207 m.add_class::<PyXsdirTable>()?;
9208 m.add_class::<PyMeshtal>()?;
9209 m.add_class::<PyMeshTally>()?;
9210 m.add_class::<PyWwinp>()?;
9211 m.add_class::<PyMctal>()?;
9212 m.add_class::<PySurfSrc>()?;
9213 m.add_class::<PyPtracFile>()?;
9214 m.add_class::<PyMcplFile>()?;
9215 m.add_class::<PyEndlLibrary>()?;
9216 m.add_class::<PyChain>()?;
9217 m.add_class::<PyDepletionSystem>()?;
9218 m.add_class::<PyUsrbinTally>()?;
9219 m.add_class::<PyMagicOutput>()?;
9220 m.add_class::<PyAliasTable>()?;
9221 m.add_class::<PyMeshSourceSampler>()?;
9222 m.add_class::<PyKdeSampler>()?;
9223 m.add_class::<PyCascade>()?;
9224 m.add_class::<PyMaterialsCompendium>()?;
9225 m.add_class::<PyDeckProblem>()?;
9226 m.add_class::<PyInventory>()?;
9227 Ok(())
9228}