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]
4822#[pyo3(signature = (text, reactions=None))]
4823fn parse_irdff_g725<'py>(
4824 py: Python<'py>,
4825 text: &str,
4826 reactions: Option<Vec<String>>,
4827) -> PyResult<pyo3::Bound<'py, pyo3::types::PyDict>> {
4828 use nucleide_nuclei::irdff::{parse_g725, V1_REACTIONS};
4829 let wanted: Vec<nucleide_nuclei::irdff::IrdffReaction> = match &reactions {
4830 None => V1_REACTIONS.to_vec(),
4831 Some(names) => {
4832 let mut out = Vec::with_capacity(names.len());
4833 for name in names {
4834 match V1_REACTIONS.iter().find(|r| r.name == name) {
4835 Some(r) => out.push(*r),
4836 None => {
4837 let supported = V1_REACTIONS
4838 .iter()
4839 .map(|r| r.name)
4840 .collect::<Vec<_>>()
4841 .join(", ");
4842 return Err(PyValueError::new_err(format!(
4843 "unknown IRDFF-II reaction `{name}` (supported: {supported})"
4844 )));
4845 }
4846 }
4847 }
4848 out
4849 }
4850 };
4851 let pack = parse_g725(text, &wanted).map_err(|e| PyValueError::new_err(e.to_string()))?;
4852 let out = pyo3::types::PyDict::new(py);
4853 out.set_item("groups", pack.bounds().to_vec())?;
4854 out.set_item(
4855 "reactions",
4856 pack.rows()
4857 .iter()
4858 .map(|r| r.reaction().name)
4859 .collect::<Vec<_>>(),
4860 )?;
4861 out.set_item("response", pack.response())?;
4862 Ok(out)
4863}
4864
4865#[pyfunction]
4877#[pyo3(signature = (comp, pathway, source="EPA"))]
4878fn dose_per_g(comp: BTreeMap<String, f64>, pathway: &str, source: &str) -> PyResult<f64> {
4879 let mat = comp_to_material(comp)?;
4880 let analytics = nucleide_material::Analytics {
4881 masses: &nucleide_material::Ame2020,
4882 decays: &nucleide_material::ChainDecays,
4883 };
4884 let p = parse_dose_pathway(pathway)?;
4885 let s = parse_dose_source(source)?;
4886 mat.total_dose_per_g(&analytics, &nucleide_material::DoseFactors, p, s)
4887 .map_err(|e| PyValueError::new_err(e.to_string()))
4888}
4889
4890#[pyfunction]
4898#[allow(clippy::type_complexity)]
4899fn separate_material(
4900 comp: BTreeMap<String, f64>,
4901 effs: BTreeMap<String, f64>,
4902) -> PyResult<(BTreeMap<String, f64>, BTreeMap<String, f64>)> {
4903 let mat = comp_to_material(comp)?;
4904 let mut table = Vec::with_capacity(effs.len());
4905 for (name, eff) in &effs {
4906 let id = NuclideId::from_name(name)
4907 .map_err(|e| PyValueError::new_err(format!("`{name}`: {e}")))?;
4908 table.push((id, *eff));
4909 }
4910 let (product, tails) = mat
4911 .separate(&table)
4912 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4913 let named =
4914 |m: nucleide_material::Material| m.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect();
4915 Ok((named(product), named(tails)))
4916}
4917
4918#[pyfunction]
4925fn blend_material(parts: Vec<(BTreeMap<String, f64>, f64)>) -> PyResult<BTreeMap<String, f64>> {
4926 let mats: Vec<nucleide_material::Material> = parts
4927 .iter()
4928 .map(|(comp, _)| comp_to_material(comp.clone()))
4929 .collect::<PyResult<_>>()?;
4930 let refs: Vec<(&nucleide_material::Material, f64)> =
4931 mats.iter().zip(parts.iter().map(|(_, r)| *r)).collect();
4932 let out = nucleide_material::Material::blend(&refs)
4933 .map_err(|e| PyValueError::new_err(e.to_string()))?;
4934 Ok(out.comp.iter().map(|(id, g)| (id.to_name(), *g)).collect())
4935}
4936
4937#[pyclass(name = "Cusum")]
4945struct PyCusum {
4946 inner: nucleide_material::Cusum,
4947}
4948
4949#[pymethods]
4950impl PyCusum {
4951 #[new]
4954 #[pyo3(signature = (ref_shift_k=0.5, alarm_h=4.0, startup=10))]
4955 fn new(ref_shift_k: f64, alarm_h: f64, startup: usize) -> PyResult<Self> {
4956 nucleide_material::Cusum::new(ref_shift_k, alarm_h, startup)
4957 .map(|inner| Self { inner })
4958 .map_err(|e| PyValueError::new_err(e.to_string()))
4959 }
4960
4961 fn update(&mut self, x: f64) -> bool {
4963 self.inner.update(x)
4964 }
4965
4966 fn status(&self) -> bool {
4968 self.inner.status()
4969 }
4970
4971 fn statistic(&self) -> f64 {
4973 self.inner.statistic()
4974 }
4975
4976 fn count(&self) -> usize {
4978 self.inner.count()
4979 }
4980
4981 fn mean(&self) -> f64 {
4983 self.inner.mean()
4984 }
4985
4986 fn variance(&self) -> f64 {
4988 self.inner.variance()
4989 }
4990
4991 fn std(&self) -> f64 {
4993 self.inner.std()
4994 }
4995
4996 fn reset(&mut self) {
4998 self.inner.reset();
4999 }
5000}
5001
5002#[pyclass(name = "DeckProblem")]
5008struct PyDeckProblem {
5009 inner: std::sync::Mutex<nucleide_mcnp_io::problem::DeckProblem>,
5010}
5011
5012fn deck_cell_dict(cell: &nucleide_mcnp_io::cell::CellCard) -> BTreeMap<String, String> {
5013 let mut d = BTreeMap::new();
5014 d.insert("num".to_string(), cell.num.to_string());
5015 d.insert("mat".to_string(), cell.mat.to_string());
5016 d.insert(
5017 "dens".to_string(),
5018 cell.dens.map(|v| v.to_string()).unwrap_or_default(),
5019 );
5020 d.insert("geom".to_string(), cell.geom.render());
5021 d.insert("params".to_string(), cell.params.join(" "));
5022 d
5023}
5024
5025#[pymethods]
5026impl PyDeckProblem {
5027 #[staticmethod]
5029 fn loads(text: &str) -> PyResult<Self> {
5030 nucleide_mcnp_io::problem::parse_deck(text)
5031 .map(|inner| Self {
5032 inner: std::sync::Mutex::new(inner),
5033 })
5034 .map_err(|e| PyValueError::new_err(e.to_string()))
5035 }
5036
5037 #[getter]
5039 fn message(&self) -> PyResult<String> {
5040 Ok(self
5041 .inner
5042 .lock()
5043 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5044 .message
5045 .clone())
5046 }
5047
5048 #[getter]
5050 fn title(&self) -> PyResult<String> {
5051 Ok(self
5052 .inner
5053 .lock()
5054 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5055 .title
5056 .clone())
5057 }
5058
5059 #[getter]
5062 fn cells(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5063 Ok(self
5064 .inner
5065 .lock()
5066 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5067 .cells
5068 .iter()
5069 .map(deck_cell_dict)
5070 .collect())
5071 }
5072
5073 #[getter]
5076 fn surfs(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5077 Ok(self
5078 .inner
5079 .lock()
5080 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5081 .surfs
5082 .iter()
5083 .map(|s| {
5084 let mut d = BTreeMap::new();
5085 d.insert("num".to_string(), s.num.to_string());
5086 d.insert("reflecting".to_string(), s.reflecting.to_string());
5087 d.insert(
5088 "transform".to_string(),
5089 s.transform.map(|v| v.to_string()).unwrap_or_default(),
5090 );
5091 d.insert(
5092 "periodic".to_string(),
5093 s.periodic.map(|v| v.to_string()).unwrap_or_default(),
5094 );
5095 d.insert("kind".to_string(), s.kind.keyword().to_string());
5096 d.insert(
5097 "coeffs".to_string(),
5098 s.coeffs
5099 .iter()
5100 .map(|v| v.to_string())
5101 .collect::<Vec<_>>()
5102 .join(" "),
5103 );
5104 d
5105 })
5106 .collect())
5107 }
5108
5109 #[getter]
5111 fn material_numbers(&self) -> PyResult<Vec<u32>> {
5112 Ok(self
5113 .inner
5114 .lock()
5115 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5116 .materials
5117 .iter()
5118 .map(|m| m.number)
5119 .collect())
5120 }
5121
5122 #[getter]
5124 fn data_names(&self) -> PyResult<Vec<String>> {
5125 Ok(self
5126 .inner
5127 .lock()
5128 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5129 .data
5130 .iter()
5131 .map(|d| d.name.clone())
5132 .collect())
5133 }
5134
5135 fn dumps(&self) -> PyResult<String> {
5137 let guard = self
5138 .inner
5139 .lock()
5140 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?;
5141 Ok(nucleide_mcnp_io::problem::write_deck(&guard))
5142 }
5143
5144 fn set_cell_density(&self, cell: u32, dens: f64) -> PyResult<()> {
5146 self.inner
5147 .lock()
5148 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5149 .set_cell_density(cell, dens)
5150 .map_err(|e| PyValueError::new_err(e.to_string()))
5151 }
5152
5153 fn set_cell_material(&self, cell: u32, mat: u32) -> PyResult<()> {
5155 self.inner
5156 .lock()
5157 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5158 .set_cell_material(cell, mat)
5159 .map_err(|e| PyValueError::new_err(e.to_string()))
5160 }
5161
5162 #[getter]
5164 fn mode(&self) -> PyResult<BTreeMap<String, String>> {
5165 let mode = self
5166 .inner
5167 .lock()
5168 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5169 .mode()
5170 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5171 let mut d = BTreeMap::new();
5172 d.insert("particles".to_string(), mode.particles.join(" "));
5173 Ok(d)
5174 }
5175
5176 #[getter]
5179 fn transforms(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5180 let transforms = self
5181 .inner
5182 .lock()
5183 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5184 .transforms()
5185 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5186 Ok(transforms
5187 .iter()
5188 .map(|t| {
5189 let mut d = BTreeMap::new();
5190 d.insert("number".to_string(), t.number.to_string());
5191 d.insert(
5192 "displacement".to_string(),
5193 t.displacement
5194 .iter()
5195 .map(|v| v.to_string())
5196 .collect::<Vec<_>>()
5197 .join(" "),
5198 );
5199 d.insert(
5200 "rotation".to_string(),
5201 t.rotation
5202 .iter()
5203 .map(|v| v.to_string())
5204 .collect::<Vec<_>>()
5205 .join(" "),
5206 );
5207 d.insert("in_degrees".to_string(), t.is_in_degrees.to_string());
5208 d.insert("main_to_aux".to_string(), t.is_main_to_aux.to_string());
5209 d.insert("hidden".to_string(), t.hidden.to_string());
5210 d
5211 })
5212 .collect())
5213 }
5214
5215 #[getter]
5218 fn universes(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5219 let universes = self
5220 .inner
5221 .lock()
5222 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5223 .universes()
5224 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5225 Ok(universes
5226 .iter()
5227 .map(|u| {
5228 let mut d = BTreeMap::new();
5229 d.insert("number".to_string(), u.number.to_string());
5230 d.insert(
5231 "cells".to_string(),
5232 u.cells
5233 .iter()
5234 .map(|v| v.to_string())
5235 .collect::<Vec<_>>()
5236 .join(" "),
5237 );
5238 d.insert(
5239 "not_truncated".to_string(),
5240 u.not_truncated
5241 .iter()
5242 .map(|v| v.to_string())
5243 .collect::<Vec<_>>()
5244 .join(" "),
5245 );
5246 d
5247 })
5248 .collect())
5249 }
5250
5251 #[getter]
5253 fn lattices(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5254 let lattices = self
5255 .inner
5256 .lock()
5257 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5258 .lattices()
5259 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5260 Ok(lattices
5261 .iter()
5262 .map(|l| {
5263 let mut d = BTreeMap::new();
5264 d.insert("cell".to_string(), l.cell.to_string());
5265 d.insert("lattice".to_string(), l.lattice.to_string());
5266 d
5267 })
5268 .collect())
5269 }
5270
5271 #[getter]
5275 fn fills(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5276 use nucleide_mcnp_io::semantic::{FillTarget, FillTransform};
5277 let fills = self
5278 .inner
5279 .lock()
5280 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5281 .fills()
5282 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5283 Ok(fills
5284 .iter()
5285 .map(|f| {
5286 let mut d = BTreeMap::new();
5287 d.insert("cell".to_string(), f.cell.to_string());
5288 match &f.target {
5289 FillTarget::Single(u) => {
5290 d.insert("kind".to_string(), "single".to_string());
5291 d.insert("universe".to_string(), u.to_string());
5292 d.insert("min_index".to_string(), String::new());
5293 d.insert("max_index".to_string(), String::new());
5294 d.insert("universes".to_string(), String::new());
5295 }
5296 FillTarget::Matrix {
5297 min_index,
5298 max_index,
5299 universes,
5300 } => {
5301 d.insert("kind".to_string(), "matrix".to_string());
5302 d.insert("universe".to_string(), String::new());
5303 d.insert(
5304 "min_index".to_string(),
5305 min_index
5306 .iter()
5307 .map(|v| v.to_string())
5308 .collect::<Vec<_>>()
5309 .join(" "),
5310 );
5311 d.insert(
5312 "max_index".to_string(),
5313 max_index
5314 .iter()
5315 .map(|v| v.to_string())
5316 .collect::<Vec<_>>()
5317 .join(" "),
5318 );
5319 d.insert(
5320 "universes".to_string(),
5321 universes
5322 .iter()
5323 .map(|u| {
5324 u.map(|v| v.to_string()).unwrap_or_else(|| "-".to_string())
5325 })
5326 .collect::<Vec<_>>()
5327 .join(" "),
5328 );
5329 }
5330 }
5331 match &f.transform {
5332 None => {
5333 d.insert("transform".to_string(), String::new());
5334 d.insert("hidden_transform".to_string(), String::new());
5335 }
5336 Some(FillTransform::Reference(n)) => {
5337 d.insert("transform".to_string(), n.to_string());
5338 d.insert("hidden_transform".to_string(), String::new());
5339 }
5340 Some(FillTransform::Hidden(t)) => {
5341 d.insert("transform".to_string(), String::new());
5342 let mut coords: Vec<String> =
5343 t.displacement.iter().map(|v| v.to_string()).collect();
5344 coords.extend(t.rotation.iter().map(|v| v.to_string()));
5345 d.insert("hidden_transform".to_string(), coords.join(" "));
5346 }
5347 }
5348 d.insert("in_degrees".to_string(), f.in_degrees.to_string());
5349 d
5350 })
5351 .collect())
5352 }
5353
5354 #[getter]
5356 fn importances(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5357 let importances = self
5358 .inner
5359 .lock()
5360 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5361 .importances()
5362 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5363 Ok(importances
5364 .iter()
5365 .map(|v| {
5366 let mut d = BTreeMap::new();
5367 d.insert("cell".to_string(), v.cell.to_string());
5368 d.insert("particle".to_string(), v.particle.clone());
5369 d.insert("value".to_string(), v.value.to_string());
5370 d
5371 })
5372 .collect())
5373 }
5374
5375 #[getter]
5377 fn volumes(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5378 let volumes = self
5379 .inner
5380 .lock()
5381 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5382 .volumes()
5383 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5384 Ok(volumes
5385 .iter()
5386 .map(|v| {
5387 let mut d = BTreeMap::new();
5388 d.insert("cell".to_string(), v.cell.to_string());
5389 d.insert("volume".to_string(), v.volume.to_string());
5390 d
5391 })
5392 .collect())
5393 }
5394
5395 #[getter]
5398 fn tallies(&self) -> PyResult<Vec<BTreeMap<String, String>>> {
5399 let tallies = self
5400 .inner
5401 .lock()
5402 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5403 .tallies()
5404 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5405 Ok(tallies
5406 .iter()
5407 .map(|t| {
5408 let mut d = BTreeMap::new();
5409 d.insert("number".to_string(), t.number.to_string());
5410 d.insert("type".to_string(), t.tally_type.to_string());
5411 d.insert("particles".to_string(), t.particles.join(","));
5412 d.insert("entries".to_string(), t.entries.join(" "));
5413 d.insert("fm".to_string(), t.fm.clone().unwrap_or_default().join(" "));
5414 d.insert(
5415 "e_bins".to_string(),
5416 t.e_bins.clone().unwrap_or_default().join(" "),
5417 );
5418 d
5419 })
5420 .collect())
5421 }
5422
5423 #[getter]
5426 fn sdef(&self, py: Python<'_>) -> PyResult<Option<Py<PyAny>>> {
5427 let guard = self
5428 .inner
5429 .lock()
5430 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?;
5431 let sdef = guard
5432 .sdef()
5433 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5434 sdef.map(|s| sdef_to_py(py, &s)).transpose()
5435 }
5436
5437 fn validate(&self) -> PyResult<()> {
5440 self.inner
5441 .lock()
5442 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5443 .validate()
5444 .map_err(|e| PyValueError::new_err(e.to_string()))
5445 }
5446
5447 fn validation_notes(&self) -> PyResult<Vec<String>> {
5449 Ok(self
5450 .inner
5451 .lock()
5452 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5453 .validation_notes())
5454 }
5455
5456 fn set_mode(&self, particles: Vec<String>) -> PyResult<()> {
5458 self.inner
5459 .lock()
5460 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5461 .set_mode(particles)
5462 .map_err(|e| PyValueError::new_err(e.to_string()))
5463 }
5464
5465 fn set_cell_universe(&self, cell: u32, universe: u32, not_truncated: bool) -> PyResult<()> {
5467 self.inner
5468 .lock()
5469 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5470 .set_cell_universe(cell, universe, not_truncated)
5471 .map_err(|e| PyValueError::new_err(e.to_string()))
5472 }
5473
5474 fn set_cell_lattice(&self, cell: u32, lattice: Option<u8>) -> PyResult<()> {
5476 self.inner
5477 .lock()
5478 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5479 .set_cell_lattice(cell, lattice)
5480 .map_err(|e| PyValueError::new_err(e.to_string()))
5481 }
5482
5483 fn set_cell_fill(&self, cell: u32, universe: u32) -> PyResult<()> {
5485 self.inner
5486 .lock()
5487 .map_err(|_| PyValueError::new_err("deck lock poisoned"))?
5488 .set_cell_fill(cell, universe)
5489 .map_err(|e| PyValueError::new_err(e.to_string()))
5490 }
5491}
5492
5493#[pyfunction]
5495fn read_deck(path: &str) -> PyResult<PyDeckProblem> {
5496 nucleide_mcnp_io::problem::parse_deck_file(path)
5497 .map(|inner| PyDeckProblem {
5498 inner: std::sync::Mutex::new(inner),
5499 })
5500 .map_err(|e| PyValueError::new_err(e.to_string()))
5501}
5502
5503#[pyfunction]
5505fn parse_deck(text: &str) -> PyResult<PyDeckProblem> {
5506 PyDeckProblem::loads(text)
5507}
5508
5509fn sdef_to_py(py: Python<'_>, sdef: &nucleide_mcnp_io::sdef::SdefProblem) -> PyResult<Py<PyAny>> {
5518 use pyo3::types::PyDict;
5519 let opt3 = |v: &Option<nucleide_mcnp_io::sdef::SdefRef<[f64; 3]>>| {
5520 v.as_ref().map(|r| r.render()).unwrap_or_default()
5521 };
5522 let opt1 = |v: &Option<nucleide_mcnp_io::sdef::SdefRef<f64>>| {
5523 v.as_ref().map(|r| r.render()).unwrap_or_default()
5524 };
5525 let optu = |v: &Option<nucleide_mcnp_io::sdef::SdefRef<u32>>| {
5526 v.as_ref().map(|r| r.render()).unwrap_or_default()
5527 };
5528 let d = PyDict::new(py);
5529 d.set_item("pos", opt3(&sdef.card.pos))?;
5530 d.set_item("cell", optu(&sdef.card.cell))?;
5531 d.set_item("surf", optu(&sdef.card.surf))?;
5532 d.set_item("vec", opt3(&sdef.card.vec))?;
5533 d.set_item("dir", opt1(&sdef.card.dir))?;
5534 d.set_item("axs", opt3(&sdef.card.axs))?;
5535 d.set_item("rad", opt1(&sdef.card.rad))?;
5536 d.set_item("ext", opt1(&sdef.card.ext))?;
5537 d.set_item("erg", opt1(&sdef.card.erg))?;
5538 d.set_item("nrm", opt1(&sdef.card.nrm))?;
5539 d.set_item(
5540 "par",
5541 sdef.card
5542 .par
5543 .as_ref()
5544 .map(|r| r.render())
5545 .unwrap_or_default(),
5546 )?;
5547 d.set_item("wgt", opt1(&sdef.card.wgt))?;
5548 d.set_item("tme", opt1(&sdef.card.tme))?;
5549 d.set_item("ignored", sdef.card.ignored.clone())?;
5550 let dists: Vec<Py<PyAny>> = sdef
5551 .dists
5552 .iter()
5553 .map(|dist| {
5554 let m = PyDict::new(py);
5555 m.set_item("number", dist.number.to_string())?;
5556 m.set_item("si_option", "L")?;
5557 m.set_item("si", dist.si_text())?;
5558 m.set_item(
5559 "sp_option",
5560 dist.sp.as_ref().map(|_| "D").unwrap_or_default(),
5561 )?;
5562 m.set_item("sp", dist.sp_text())?;
5563 m.set_item(
5564 "sb_option",
5565 dist.sb.as_ref().map(|_| "D").unwrap_or_default(),
5566 )?;
5567 m.set_item("sb", dist.sb_text())?;
5568 Ok(m.into_any().unbind())
5569 })
5570 .collect::<PyResult<Vec<_>>>()?;
5571 d.set_item("distributions", dists)?;
5572 d.set_item("card", sdef.emit())?;
5573 Ok(d.into_any().unbind())
5574}
5575
5576#[pyfunction]
5582fn parse_sdef(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
5583 let sdef = nucleide_mcnp_io::sdef::parse_sdef_text(text)
5584 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5585 sdef_to_py(py, &sdef)
5586}
5587
5588fn csg_to_openmc_inner(
5595 deck: &nucleide_mcnp_io::problem::DeckProblem,
5596) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5597 let (xml, table) = nucleide_csg_xlate::deck_csg_to_openmc_xml(deck)
5598 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5599 Ok((
5600 xml,
5601 table
5602 .entries
5603 .into_iter()
5604 .map(|e| {
5605 let mut d = BTreeMap::new();
5606 d.insert("scope".to_string(), e.scope.to_string());
5607 d.insert("target".to_string(), e.target.to_string());
5608 d.insert("action".to_string(), e.action);
5609 d.insert("reason".to_string(), e.reason);
5610 d
5611 })
5612 .collect(),
5613 ))
5614}
5615
5616#[pyfunction]
5619fn parse_csg_to_openmc(text: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5620 let deck = nucleide_mcnp_io::problem::parse_deck(text)
5621 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5622 csg_to_openmc_inner(&deck)
5623}
5624
5625#[pyfunction]
5628fn read_csg_to_openmc(path: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5629 let deck = nucleide_mcnp_io::problem::parse_deck_file(path)
5630 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5631 csg_to_openmc_inner(&deck)
5632}
5633
5634fn csg_to_serpent_inner(
5641 deck: &nucleide_mcnp_io::problem::DeckProblem,
5642) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5643 let (text, table) = nucleide_csg_xlate::deck_csg_to_serpent_input(deck)
5644 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5645 Ok((
5646 text,
5647 table
5648 .entries
5649 .into_iter()
5650 .map(|e| {
5651 let mut d = BTreeMap::new();
5652 d.insert("scope".to_string(), e.scope.to_string());
5653 d.insert("target".to_string(), e.target.to_string());
5654 d.insert("action".to_string(), e.action);
5655 d.insert("reason".to_string(), e.reason);
5656 d
5657 })
5658 .collect(),
5659 ))
5660}
5661
5662#[pyfunction]
5665fn parse_csg_to_serpent(text: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5666 let deck = nucleide_mcnp_io::problem::parse_deck(text)
5667 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5668 csg_to_serpent_inner(&deck)
5669}
5670
5671#[pyfunction]
5674fn read_csg_to_serpent(path: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5675 let deck = nucleide_mcnp_io::problem::parse_deck_file(path)
5676 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5677 csg_to_serpent_inner(&deck)
5678}
5679
5680fn csg_to_phits_inner(
5688 deck: &nucleide_mcnp_io::problem::DeckProblem,
5689) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5690 let (text, table) = nucleide_csg_xlate::deck_csg_to_phits_input(deck)
5691 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5692 Ok((
5693 text,
5694 table
5695 .entries
5696 .into_iter()
5697 .map(|e| {
5698 let mut d = BTreeMap::new();
5699 d.insert("scope".to_string(), e.scope.to_string());
5700 d.insert("target".to_string(), e.target.to_string());
5701 d.insert("action".to_string(), e.action);
5702 d.insert("reason".to_string(), e.reason);
5703 d
5704 })
5705 .collect(),
5706 ))
5707}
5708
5709#[pyfunction]
5712fn parse_csg_to_phits(text: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5713 let deck = nucleide_mcnp_io::problem::parse_deck(text)
5714 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5715 csg_to_phits_inner(&deck)
5716}
5717
5718#[pyfunction]
5721fn read_csg_to_phits(path: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5722 let deck = nucleide_mcnp_io::problem::parse_deck_file(path)
5723 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5724 csg_to_phits_inner(&deck)
5725}
5726
5727fn csg_to_gdml_inner(
5736 deck: &nucleide_mcnp_io::problem::DeckProblem,
5737) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5738 let (xml, table) = nucleide_csg_xlate::deck_csg_to_gdml(deck)
5739 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5740 Ok((
5741 xml,
5742 table
5743 .entries
5744 .into_iter()
5745 .map(|e| {
5746 let mut d = BTreeMap::new();
5747 d.insert("scope".to_string(), e.scope.to_string());
5748 d.insert("target".to_string(), e.target.to_string());
5749 d.insert("action".to_string(), e.action);
5750 d.insert("reason".to_string(), e.reason);
5751 d
5752 })
5753 .collect(),
5754 ))
5755}
5756
5757#[pyfunction]
5760fn parse_csg_to_gdml(text: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5761 let deck = nucleide_mcnp_io::problem::parse_deck(text)
5762 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5763 csg_to_gdml_inner(&deck)
5764}
5765
5766#[pyfunction]
5769fn read_csg_to_gdml(path: &str) -> PyResult<(String, Vec<BTreeMap<String, String>>)> {
5770 let deck = nucleide_mcnp_io::problem::parse_deck_file(path)
5771 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5772 csg_to_gdml_inner(&deck)
5773}
5774
5775#[pyclass(name = "Inventory")]
5777struct PyInventory {
5778 chain: std::sync::Arc<nucleide_depletion::Chain>,
5779 atoms: BTreeMap<String, f64>,
5780}
5781
5782fn inventory_sys(
5783 chain: &nucleide_depletion::Chain,
5784 rates: &RateMap,
5785) -> PyResult<nucleide_depletion::DepletionSystem> {
5786 let rs = split_rates(rates, chain)?;
5787 nucleide_depletion::DepletionSystem::build(chain.clone(), &rs)
5788 .map_err(|e| PyValueError::new_err(e.to_string()))
5789}
5790
5791fn parse_quantity_unit(unit: &str) -> PyResult<nucleide_depletion::QuantityUnit> {
5792 nucleide_depletion::QuantityUnit::from_str(unit)
5793 .map_err(|e| PyValueError::new_err(format!("{e:?}")))
5794}
5795
5796#[pymethods]
5797impl PyInventory {
5798 #[new]
5801 #[pyo3(signature = (chain, comp, units="atoms"))]
5802 fn new(chain: &PyChain, comp: BTreeMap<String, f64>, units: &str) -> PyResult<Self> {
5803 let unit = parse_quantity_unit(units)?;
5804 let sys = inventory_sys(&chain.inner, &BTreeMap::new())?;
5805 let inv = nucleide_depletion::DecayInventory::from_units(&comp, unit, &sys)
5806 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5807 Ok(Self {
5808 chain: chain.inner.clone(),
5809 atoms: inv.atoms,
5810 })
5811 }
5812
5813 fn numbers(&self) -> BTreeMap<String, f64> {
5815 self.atoms.clone()
5816 }
5817
5818 #[pyo3(signature = (dt, time_unit="s", rates=None, order=48, method="cram48"))]
5825 fn decay(
5826 &self,
5827 dt: f64,
5828 time_unit: &str,
5829 rates: Option<RateMap>,
5830 order: u8,
5831 method: &str,
5832 ) -> PyResult<Self> {
5833 let method = resolve_method(order, method)?;
5834 let unit = nucleide_depletion::inventory::time_unit_from_str(time_unit)
5835 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5836 let seconds = dt * unit.as_seconds();
5837 let empty = BTreeMap::new();
5838 let step_rates = rates.as_ref().unwrap_or(&empty);
5839 let template = inventory_sys(&self.chain, step_rates)?;
5840 let steps = vec![nucleide_depletion::Step::new(
5843 seconds,
5844 split_rates(step_rates, &self.chain)?,
5845 )];
5846 let series = nucleide_depletion::integrate_with_method(
5847 &template,
5848 &chain_vec(&self.chain, &self.atoms)?,
5849 &steps,
5850 nucleide_depletion::Integrator::Predictor,
5851 method,
5852 )
5853 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5854 let names: Vec<String> = self.chain.nuclides.iter().map(|n| n.name.clone()).collect();
5855 let atoms = names
5856 .iter()
5857 .zip(series.atoms.last().cloned().unwrap_or_default())
5858 .map(|(n, v)| (n.clone(), v))
5859 .collect();
5860 Ok(Self {
5861 chain: self.chain.clone(),
5862 atoms,
5863 })
5864 }
5865
5866 fn activities(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
5868 let unit = parse_quantity_unit(units)?;
5869 let sys = inventory_sys(&self.chain, &BTreeMap::new())?;
5870 let inv = nucleide_depletion::DecayInventory {
5871 atoms: self.atoms.clone(),
5872 };
5873 inv.activities(&sys, unit)
5874 .map_err(|e| PyValueError::new_err(e.to_string()))
5875 }
5876
5877 fn masses(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
5879 let unit = parse_quantity_unit(units)?;
5880 let inv = nucleide_depletion::DecayInventory {
5881 atoms: self.atoms.clone(),
5882 };
5883 inv.masses(unit)
5884 .map_err(|e| PyValueError::new_err(e.to_string()))
5885 }
5886
5887 fn moles(&self, units: &str) -> PyResult<BTreeMap<String, f64>> {
5889 let unit = parse_quantity_unit(units)?;
5890 let inv = nucleide_depletion::DecayInventory {
5891 atoms: self.atoms.clone(),
5892 };
5893 inv.moles(unit)
5894 .map_err(|e| PyValueError::new_err(e.to_string()))
5895 }
5896
5897 fn activity_fractions(&self) -> PyResult<BTreeMap<String, f64>> {
5899 let sys = inventory_sys(&self.chain, &BTreeMap::new())?;
5900 let inv = nucleide_depletion::DecayInventory {
5901 atoms: self.atoms.clone(),
5902 };
5903 inv.activity_fractions(&sys)
5904 .map_err(|e| PyValueError::new_err(e.to_string()))
5905 }
5906
5907 fn mass_fractions(&self) -> PyResult<BTreeMap<String, f64>> {
5909 let inv = nucleide_depletion::DecayInventory {
5910 atoms: self.atoms.clone(),
5911 };
5912 inv.mass_fractions()
5913 .map_err(|e| PyValueError::new_err(e.to_string()))
5914 }
5915
5916 fn mole_fractions(&self) -> BTreeMap<String, f64> {
5918 nucleide_depletion::DecayInventory {
5919 atoms: self.atoms.clone(),
5920 }
5921 .mole_fractions()
5922 }
5923
5924 fn half_lives_readable(&self) -> BTreeMap<String, String> {
5926 nucleide_depletion::DecayInventory {
5927 atoms: self.atoms.clone(),
5928 }
5929 .half_lives_readable()
5930 }
5931
5932 fn add(&self, other: &Self) -> Self {
5934 let a = nucleide_depletion::DecayInventory {
5935 atoms: self.atoms.clone(),
5936 };
5937 let b = nucleide_depletion::DecayInventory {
5938 atoms: other.atoms.clone(),
5939 };
5940 Self {
5941 chain: self.chain.clone(),
5942 atoms: a.add(&b).atoms,
5943 }
5944 }
5945
5946 fn sub(&self, other: &Self) -> Self {
5948 let a = nucleide_depletion::DecayInventory {
5949 atoms: self.atoms.clone(),
5950 };
5951 let b = nucleide_depletion::DecayInventory {
5952 atoms: other.atoms.clone(),
5953 };
5954 Self {
5955 chain: self.chain.clone(),
5956 atoms: a.sub(&b).atoms,
5957 }
5958 }
5959
5960 fn mul(&self, scalar: f64) -> Self {
5962 let a = nucleide_depletion::DecayInventory {
5963 atoms: self.atoms.clone(),
5964 };
5965 Self {
5966 chain: self.chain.clone(),
5967 atoms: a.mul(scalar).atoms,
5968 }
5969 }
5970
5971 fn div(&self, scalar: f64) -> Self {
5973 let a = nucleide_depletion::DecayInventory {
5974 atoms: self.atoms.clone(),
5975 };
5976 Self {
5977 chain: self.chain.clone(),
5978 atoms: a.div(scalar).atoms,
5979 }
5980 }
5981
5982 fn to_csv(&self) -> String {
5984 nucleide_depletion::DecayInventory {
5985 atoms: self.atoms.clone(),
5986 }
5987 .to_csv()
5988 }
5989
5990 #[staticmethod]
5992 fn from_csv(chain: &PyChain, text: &str) -> PyResult<Self> {
5993 let inv = nucleide_depletion::DecayInventory::from_csv(text)
5995 .map_err(|e| PyValueError::new_err(e.to_string()))?;
5996 for name in inv.atoms.keys() {
5997 if chain.inner.index_of(name).is_none() {
5998 return Err(PyValueError::new_err(format!(
5999 "unknown nuclide `{name}` for this chain"
6000 )));
6001 }
6002 }
6003 Ok(Self {
6004 chain: chain.inner.clone(),
6005 atoms: inv.atoms,
6006 })
6007 }
6008}
6009
6010fn chain_vec(
6012 chain: &nucleide_depletion::Chain,
6013 atoms: &BTreeMap<String, f64>,
6014) -> PyResult<Vec<f64>> {
6015 let mut vec = vec![0.0; chain.len()];
6016 for (name, value) in atoms {
6017 let idx = chain.index_of(name).ok_or_else(|| {
6018 PyValueError::new_err(format!("unknown nuclide `{name}` for this chain"))
6019 })?;
6020 vec[idx] = *value;
6021 }
6022 Ok(vec)
6023}
6024
6025#[pyfunction]
6027#[pyo3(signature = (chain, n0, dt, rates=None))]
6028fn cumulative_decays(
6029 chain: &PyChain,
6030 n0: BTreeMap<String, f64>,
6031 dt: f64,
6032 rates: Option<RateMap>,
6033) -> PyResult<BTreeMap<String, f64>> {
6034 let empty = BTreeMap::new();
6035 let sys = inventory_sys(&chain.inner, rates.as_ref().unwrap_or(&empty))?;
6036 let vec = chain_vec(&chain.inner, &n0)?;
6037 let out = nucleide_depletion::cumulative_decays(&sys, &vec, dt)
6038 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6039 Ok(chain
6040 .inner
6041 .nuclides
6042 .iter()
6043 .zip(out)
6044 .map(|(nuc, v)| (nuc.name.clone(), v))
6045 .collect())
6046}
6047
6048#[pyfunction]
6050fn progeny(chain: &PyChain, name: &str) -> Vec<(String, f64, String)> {
6051 nucleide_depletion::progeny(&chain.inner, name)
6052}
6053
6054#[pyfunction]
6056fn branching_fraction(chain: &PyChain, parent: &str, child: &str) -> Option<f64> {
6057 nucleide_depletion::branching_fraction(&chain.inner, parent, child)
6058}
6059
6060#[pyfunction]
6062fn decay_mode(chain: &PyChain, parent: &str, child: &str) -> Option<String> {
6063 nucleide_depletion::decay_mode(&chain.inner, parent, child)
6064}
6065
6066#[pyfunction]
6068fn chain_edges(chain: &PyChain) -> Vec<(String, String, f64, String)> {
6069 nucleide_depletion::chain_edges(&chain.inner)
6070}
6071
6072#[pyfunction]
6074fn armi_to_nucid(name: &str) -> PyResult<PyNuclide> {
6075 nucleide_nuclei::armi::armi_name_to_nucid(name)
6076 .map(|inner| PyNuclide { inner })
6077 .map_err(|e| PyValueError::new_err(e.to_string()))
6078}
6079
6080#[pyfunction]
6082fn nucid_to_armi(nuclide: &PyNuclide) -> String {
6083 nucleide_nuclei::armi::nucid_to_armi_label(nuclide.inner)
6084}
6085
6086#[pyfunction]
6088fn mcc3_to_nucid(name: &str) -> PyResult<PyNuclide> {
6089 nucleide_nuclei::armi::mcc3_to_nucid(name)
6090 .map(|inner| PyNuclide { inner })
6091 .map_err(|e| PyValueError::new_err(e.to_string()))
6092}
6093
6094#[pyfunction]
6099#[pyo3(signature = (comp, widths=None))]
6100fn check_labels(
6101 comp: BTreeMap<String, f64>,
6102 widths: Option<Vec<usize>>,
6103) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
6104 let mat = comp_to_material(comp)?;
6105 let widths = widths.unwrap_or_else(|| nucleide_material::DEFAULT_WIDTHS.to_vec());
6106 let collisions = nucleide_material::check_labels(&mat, &widths);
6107 Python::attach(|py| {
6108 Ok(collisions
6109 .into_iter()
6110 .map(|c| {
6111 let mut d = BTreeMap::new();
6112 d.insert(
6113 "truncated".to_string(),
6114 c.truncated.into_pyobject(py).unwrap().unbind().into_any(),
6115 );
6116 d.insert(
6117 "width".to_string(),
6118 c.width.into_pyobject(py).unwrap().unbind().into_any(),
6119 );
6120 let members: Vec<String> = c.members.iter().map(|id| id.to_name()).collect();
6121 d.insert(
6122 "members".to_string(),
6123 members.into_pyobject(py).unwrap().unbind().into_any(),
6124 );
6125 d
6126 })
6127 .collect())
6128 })
6129}
6130
6131#[pyfunction]
6133fn audit_material(comp: BTreeMap<String, f64>) -> PyResult<Vec<BTreeMap<String, String>>> {
6134 let mat = comp_to_material(comp)?;
6135 Ok(nucleide_material::audit(&mat, &nucleide_material::Ame2020)
6136 .into_iter()
6137 .map(|issue| {
6138 let mut d = BTreeMap::new();
6139 d.insert("kind".to_string(), format!("{:?}", issue.kind));
6140 d.insert("detail".to_string(), issue.detail);
6141 d
6142 })
6143 .collect())
6144}
6145
6146#[pyfunction]
6153#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
6154#[allow(clippy::too_many_arguments)]
6155fn emit_cards(
6156 comp: BTreeMap<String, f64>,
6157 name: &str,
6158 density: Option<f64>,
6159 mcnp_number: u32,
6160 xs_suffix: &str,
6161 serpent_lib: &str,
6162 fluka_fid: u32,
6163 partisn_zone: u32,
6164) -> PyResult<BTreeMap<String, String>> {
6165 let (emitted, _) = emit_drift_inner(
6166 comp,
6167 name,
6168 density,
6169 mcnp_number,
6170 xs_suffix,
6171 serpent_lib,
6172 fluka_fid,
6173 partisn_zone,
6174 )?;
6175 Ok(emitted
6176 .into_iter()
6177 .map(|e| (e.code.to_string(), e.text))
6178 .collect())
6179}
6180
6181#[pyfunction]
6185#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
6186#[allow(clippy::too_many_arguments)]
6187fn emit_drift_table(
6188 comp: BTreeMap<String, f64>,
6189 name: &str,
6190 density: Option<f64>,
6191 mcnp_number: u32,
6192 xs_suffix: &str,
6193 serpent_lib: &str,
6194 fluka_fid: u32,
6195 partisn_zone: u32,
6196) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
6197 let (_, table) = emit_drift_inner(
6198 comp,
6199 name,
6200 density,
6201 mcnp_number,
6202 xs_suffix,
6203 serpent_lib,
6204 fluka_fid,
6205 partisn_zone,
6206 )?;
6207 drift_table_to_py(table)
6208}
6209
6210fn drift_table_to_py(
6211 table: nucleide_emit::DriftTable,
6212) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
6213 Python::attach(|py| {
6214 Ok(table
6215 .rows
6216 .into_iter()
6217 .map(|r| {
6218 let mut d = BTreeMap::new();
6219 d.insert(
6220 "code".to_string(),
6221 r.code
6222 .to_string()
6223 .into_pyobject(py)
6224 .unwrap()
6225 .unbind()
6226 .into_any(),
6227 );
6228 d.insert(
6229 "mass_in".to_string(),
6230 r.mass_in.into_pyobject(py).unwrap().unbind().into_any(),
6231 );
6232 d.insert(
6233 "mass_out".to_string(),
6234 r.mass_out.into_pyobject(py).unwrap().unbind().into_any(),
6235 );
6236 d.insert(
6237 "rel_drift".to_string(),
6238 r.rel_drift.into_pyobject(py).unwrap().unbind().into_any(),
6239 );
6240 let dropped: Vec<BTreeMap<String, Py<PyAny>>> = r
6241 .dropped
6242 .into_iter()
6243 .map(|x| {
6244 let mut dd = BTreeMap::new();
6245 dd.insert(
6246 "nuclide".to_string(),
6247 x.id.to_name()
6248 .into_pyobject(py)
6249 .unwrap()
6250 .unbind()
6251 .into_any(),
6252 );
6253 dd.insert(
6254 "mass".to_string(),
6255 x.mass.into_pyobject(py).unwrap().unbind().into_any(),
6256 );
6257 dd.insert(
6258 "reason".to_string(),
6259 x.reason.into_pyobject(py).unwrap().unbind().into_any(),
6260 );
6261 dd
6262 })
6263 .collect();
6264 d.insert(
6265 "dropped".to_string(),
6266 dropped.into_pyobject(py).unwrap().unbind().into_any(),
6267 );
6268 d.insert(
6269 "reparsed".to_string(),
6270 pyo3::types::PyBool::new(py, r.reparsed)
6271 .to_owned()
6272 .into_any()
6273 .unbind(),
6274 );
6275 d
6276 })
6277 .collect())
6278 })
6279}
6280
6281#[allow(clippy::too_many_arguments)]
6282fn emit_drift_inner(
6283 comp: BTreeMap<String, f64>,
6284 name: &str,
6285 density: Option<f64>,
6286 mcnp_number: u32,
6287 xs_suffix: &str,
6288 serpent_lib: &str,
6289 fluka_fid: u32,
6290 partisn_zone: u32,
6291) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
6292 let mut mat = comp_to_material(comp)?;
6293 mat.set_density(density);
6294 emit_drift_with_mat(
6295 mat,
6296 name,
6297 mcnp_number,
6298 xs_suffix,
6299 serpent_lib,
6300 fluka_fid,
6301 partisn_zone,
6302 )
6303}
6304
6305#[allow(clippy::too_many_arguments)]
6306fn emit_drift_with_mat(
6307 mat: nucleide_material::Material,
6308 name: &str,
6309 mcnp_number: u32,
6310 xs_suffix: &str,
6311 serpent_lib: &str,
6312 fluka_fid: u32,
6313 partisn_zone: u32,
6314) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
6315 let mut opts = nucleide_emit::EmitOptions::new(name);
6316 opts.mcnp_number = mcnp_number;
6317 opts.xs_suffix = xs_suffix.to_string();
6318 opts.serpent_lib = serpent_lib.to_string();
6319 opts.fluka_fid = fluka_fid;
6320 opts.partisn_zone = partisn_zone;
6321 nucleide_emit::emit_drift(&mat, &opts).map_err(|e| PyValueError::new_err(e.to_string()))
6322}
6323
6324#[allow(clippy::too_many_arguments)]
6325fn emit_armi_drift_inner(
6326 comp: BTreeMap<String, f64>,
6327 name: &str,
6328 density: Option<f64>,
6329 mcnp_number: u32,
6330 xs_suffix: &str,
6331 serpent_lib: &str,
6332 fluka_fid: u32,
6333 partisn_zone: u32,
6334) -> PyResult<(Vec<nucleide_emit::Emitted>, nucleide_emit::DriftTable)> {
6335 let mat = nucleide_emit::armi::from_armi_mass_fracs(comp, density)
6338 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6339 emit_drift_with_mat(
6340 mat,
6341 name,
6342 mcnp_number,
6343 xs_suffix,
6344 serpent_lib,
6345 fluka_fid,
6346 partisn_zone,
6347 )
6348}
6349
6350#[pyfunction]
6358#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
6359#[allow(clippy::too_many_arguments)]
6360fn emit_armi_cards(
6361 comp: BTreeMap<String, f64>,
6362 name: &str,
6363 density: Option<f64>,
6364 mcnp_number: u32,
6365 xs_suffix: &str,
6366 serpent_lib: &str,
6367 fluka_fid: u32,
6368 partisn_zone: u32,
6369) -> PyResult<BTreeMap<String, String>> {
6370 let (emitted, _) = emit_armi_drift_inner(
6371 comp,
6372 name,
6373 density,
6374 mcnp_number,
6375 xs_suffix,
6376 serpent_lib,
6377 fluka_fid,
6378 partisn_zone,
6379 )?;
6380 Ok(emitted
6381 .into_iter()
6382 .map(|e| (e.code.to_string(), e.text))
6383 .collect())
6384}
6385
6386#[pyfunction]
6390#[pyo3(signature = (comp, name, density=None, mcnp_number=1, xs_suffix="80c", serpent_lib="03c", fluka_fid=1, partisn_zone=1))]
6391#[allow(clippy::too_many_arguments)]
6392fn emit_armi_drift_table(
6393 comp: BTreeMap<String, f64>,
6394 name: &str,
6395 density: Option<f64>,
6396 mcnp_number: u32,
6397 xs_suffix: &str,
6398 serpent_lib: &str,
6399 fluka_fid: u32,
6400 partisn_zone: u32,
6401) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
6402 let (_, table) = emit_armi_drift_inner(
6403 comp,
6404 name,
6405 density,
6406 mcnp_number,
6407 xs_suffix,
6408 serpent_lib,
6409 fluka_fid,
6410 partisn_zone,
6411 )?;
6412 drift_table_to_py(table)
6413}
6414
6415fn parse_reactivity(
6428 spec: &BTreeMap<String, Py<PyAny>>,
6429 py: Python<'_>,
6430) -> PyResult<nucleide_kinetics::Reactivity> {
6431 use nucleide_kinetics::Reactivity as R;
6432 let kind: String = get_str(spec, py, "kind", "reactivity spec needs a `kind`")?;
6433 let num = |key: &str| -> PyResult<f64> { get_num(spec, py, key) };
6434 let vec = |key: &str| -> PyResult<Vec<f64>> { get_vec(spec, py, key) };
6435 let r = match kind.as_str() {
6436 "constant" => R::Constant { rho: num("rho")? },
6437 "step" => R::Step {
6438 t_step: num("t_step")?,
6439 rho_init: num("rho_init")?,
6440 rho_final: num("rho_final")?,
6441 },
6442 "impulse" => R::Impulse {
6443 t_start: num("t_start")?,
6444 t_end: num("t_end")?,
6445 rho_init: num("rho_init")?,
6446 rho_max: num("rho_max")?,
6447 },
6448 "ramp" => R::Ramp {
6449 t_start: num("t_start")?,
6450 t_end: num("t_end")?,
6451 rho_init: num("rho_init")?,
6452 rho_rise: num("rho_rise")?,
6453 rho_final: num("rho_final")?,
6454 },
6455 "polyline" => R::Polyline {
6456 times: vec("times")?,
6457 values: vec("values")?,
6458 },
6459 other => {
6460 return Err(PyValueError::new_err(format!(
6461 "unknown reactivity kind `{other}` (supported: constant, step, impulse, ramp, polyline)"
6462 )))
6463 }
6464 };
6465 r.validate()
6466 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6467 Ok(r)
6468}
6469
6470fn get_str(
6471 spec: &BTreeMap<String, Py<PyAny>>,
6472 py: Python<'_>,
6473 key: &str,
6474 missing: &str,
6475) -> PyResult<String> {
6476 spec.get(key)
6477 .ok_or_else(|| PyValueError::new_err(missing.to_string()))?
6478 .extract::<String>(py)
6479 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a string")))
6480}
6481
6482fn get_num(spec: &BTreeMap<String, Py<PyAny>>, py: Python<'_>, key: &str) -> PyResult<f64> {
6483 spec.get(key)
6484 .ok_or_else(|| PyValueError::new_err(format!("reactivity spec missing `{key}`")))?
6485 .extract::<f64>(py)
6486 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
6487}
6488
6489fn get_vec(spec: &BTreeMap<String, Py<PyAny>>, py: Python<'_>, key: &str) -> PyResult<Vec<f64>> {
6490 spec.get(key)
6491 .ok_or_else(|| PyValueError::new_err(format!("reactivity spec missing `{key}`")))?
6492 .extract::<Vec<f64>>(py)
6493 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a list of numbers")))
6494}
6495
6496fn kinetics_params(
6497 betas: Vec<f64>,
6498 lambdas: Vec<f64>,
6499 lambda_gen: f64,
6500) -> PyResult<nucleide_kinetics::KineticParams> {
6501 nucleide_kinetics::KineticParams::new(betas, lambdas, lambda_gen)
6502 .map_err(|e| PyValueError::new_err(e.to_string()))
6503}
6504
6505#[pyfunction]
6516#[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))]
6517#[allow(clippy::too_many_arguments)]
6518fn kinetics_solve(
6519 py: Python<'_>,
6520 betas: Vec<f64>,
6521 lambdas: Vec<f64>,
6522 lambda_gen: f64,
6523 rho: BTreeMap<String, Py<PyAny>>,
6524 t: Vec<f64>,
6525 n0: f64,
6526 c0: Option<Vec<f64>>,
6527 method: &str,
6528 rtol: f64,
6529 atol: f64,
6530 dt_min: f64,
6531 dt_max: Option<f64>,
6532 max_steps: usize,
6533) -> PyResult<Py<PyAny>> {
6534 use nucleide_kinetics::{Method as M, SolverOptions};
6535 let params = kinetics_params(betas, lambdas, lambda_gen)?;
6536 let rho = parse_reactivity(&rho, py)?;
6537 let grid =
6538 nucleide_kinetics::TimeGrid::new(t).map_err(|e| PyValueError::new_err(e.to_string()))?;
6539 let state = nucleide_kinetics::State::new(¶ms, n0, c0)
6540 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6541 let method = if method.eq_ignore_ascii_case("trapezoidal") {
6542 M::Trapezoidal
6543 } else if method.eq_ignore_ascii_case("backward_euler") {
6544 M::BackwardEuler
6545 } else {
6546 return Err(PyValueError::new_err(format!(
6547 "unknown kinetics method `{method}` (supported: trapezoidal, backward_euler)"
6548 )));
6549 };
6550 let opts = SolverOptions {
6551 method,
6552 rtol,
6553 atol,
6554 dt_min,
6555 dt_max: dt_max.unwrap_or(f64::INFINITY),
6556 max_steps,
6557 };
6558 let sol = nucleide_kinetics::solve(¶ms, &rho, &grid, &state, &opts)
6559 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6560 use pyo3::types::PyDict;
6561 let out = PyDict::new(py);
6562 out.set_item("times", &sol.times).ok();
6563 out.set_item("n", &sol.n).ok();
6564 out.set_item("C", &sol.c).ok();
6565 out.set_item("n0", sol.initial.n0).ok();
6566 out.set_item("C0", &sol.initial.c0).ok();
6567 Ok(out.into_any().unbind())
6568}
6569
6570#[pyfunction]
6572fn kinetics_equilibrium(
6573 betas: Vec<f64>,
6574 lambdas: Vec<f64>,
6575 lambda_gen: f64,
6576 n0: f64,
6577) -> PyResult<Vec<f64>> {
6578 kinetics_params(betas, lambdas, lambda_gen)?
6579 .equilibrium_precursors(n0)
6580 .map_err(|e| PyValueError::new_err(e.to_string()))
6581}
6582
6583#[pyfunction]
6585#[pyo3(signature = (betas, lambdas, lambda_gen, rho, n0, c0=None))]
6586fn kinetics_initial_rate(
6587 py: Python<'_>,
6588 betas: Vec<f64>,
6589 lambdas: Vec<f64>,
6590 lambda_gen: f64,
6591 rho: BTreeMap<String, Py<PyAny>>,
6592 n0: f64,
6593 c0: Option<Vec<f64>>,
6594) -> PyResult<f64> {
6595 let params = kinetics_params(betas, lambdas, lambda_gen)?;
6596 let rho = parse_reactivity(&rho, py)?;
6597 let state = nucleide_kinetics::State::new(¶ms, n0, c0)
6598 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6599 Ok(nucleide_kinetics::solve::initial_rate(
6600 ¶ms, &rho, &state,
6601 ))
6602}
6603
6604#[pyfunction]
6606fn kinetics_inhour_rho(
6607 betas: Vec<f64>,
6608 lambdas: Vec<f64>,
6609 lambda_gen: f64,
6610 omega: f64,
6611) -> PyResult<f64> {
6612 let params = kinetics_params(betas, lambdas, lambda_gen)?;
6613 nucleide_kinetics::rho_of_omega(¶ms, omega)
6614 .map_err(|e| PyValueError::new_err(e.to_string()))
6615}
6616
6617#[pyfunction]
6619fn kinetics_stable_period(
6620 betas: Vec<f64>,
6621 lambdas: Vec<f64>,
6622 lambda_gen: f64,
6623 rho: f64,
6624) -> PyResult<f64> {
6625 let params = kinetics_params(betas, lambdas, lambda_gen)?;
6626 nucleide_kinetics::stable_period(¶ms, rho).map_err(|e| PyValueError::new_err(e.to_string()))
6627}
6628
6629#[pyfunction]
6634fn kinetics_prompt_jump(
6635 n_before: f64,
6636 rho_before: f64,
6637 rho_after: f64,
6638 beta_total: f64,
6639) -> PyResult<f64> {
6640 nucleide_kinetics::prompt_jump(n_before, rho_before, rho_after, beta_total)
6641 .map_err(|e| PyValueError::new_err(e.to_string()))
6642}
6643
6644#[pyfunction]
6661#[pyo3(signature = (response, rates, guess, tolerance=1e-3, max_iterations=200))]
6662fn unfold_sandii(
6663 py: Python<'_>,
6664 response: Vec<Vec<f64>>,
6665 rates: Vec<f64>,
6666 guess: Vec<f64>,
6667 tolerance: f64,
6668 max_iterations: usize,
6669) -> PyResult<Py<PyAny>> {
6670 let sol = nucleide_unfold::sandii::unfold(&response, &rates, &guess, tolerance, max_iterations)
6671 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6672 use pyo3::types::PyDict;
6673 let out = PyDict::new(py);
6674 out.set_item("spectrum", &sol.spectrum).ok();
6675 out.set_item("rates", &sol.rates).ok();
6676 out.set_item("rate_factors", &sol.rate_factors).ok();
6677 out.set_item("iterations", sol.iterations).ok();
6678 out.set_item("tolerance", sol.tolerance).ok();
6679 out.set_item("max_rel_change", sol.max_rel_change).ok();
6680 Ok(out.into_any().unbind())
6681}
6682
6683#[pyfunction]
6701#[pyo3(signature = (response, rates, sigmas, guess, tolerance=1e-3, max_iterations=200, damping=1e-3))]
6702#[allow(clippy::too_many_arguments)]
6703fn unfold_staysl(
6704 py: Python<'_>,
6705 response: Vec<Vec<f64>>,
6706 rates: Vec<f64>,
6707 sigmas: Vec<f64>,
6708 guess: Vec<f64>,
6709 tolerance: f64,
6710 max_iterations: usize,
6711 damping: f64,
6712) -> PyResult<Py<PyAny>> {
6713 let sol = nucleide_unfold::staysl::unfold(
6714 &response,
6715 &rates,
6716 &sigmas,
6717 &guess,
6718 tolerance,
6719 max_iterations,
6720 damping,
6721 )
6722 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6723 use pyo3::types::PyDict;
6724 let out = PyDict::new(py);
6725 out.set_item("spectrum", &sol.spectrum).ok();
6726 out.set_item("rates", &sol.rates).ok();
6727 out.set_item("rate_factors", &sol.rate_factors).ok();
6728 out.set_item("iterations", sol.iterations).ok();
6729 out.set_item("tolerance", sol.tolerance).ok();
6730 out.set_item("max_rel_change", sol.max_rel_change).ok();
6731 Ok(out.into_any().unbind())
6732}
6733
6734#[pyfunction]
6751#[pyo3(signature = (response, rates, sigmas, guess, tolerance=1e-3, max_iterations=200))]
6752fn unfold_gravel(
6753 py: Python<'_>,
6754 response: Vec<Vec<f64>>,
6755 rates: Vec<f64>,
6756 sigmas: Vec<f64>,
6757 guess: Vec<f64>,
6758 tolerance: f64,
6759 max_iterations: usize,
6760) -> PyResult<Py<PyAny>> {
6761 let sol = nucleide_unfold::gravel::unfold(
6762 &response,
6763 &rates,
6764 &sigmas,
6765 &guess,
6766 tolerance,
6767 max_iterations,
6768 )
6769 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6770 use pyo3::types::PyDict;
6771 let out = PyDict::new(py);
6772 out.set_item("spectrum", &sol.spectrum).ok();
6773 out.set_item("rates", &sol.rates).ok();
6774 out.set_item("rate_factors", &sol.rate_factors).ok();
6775 out.set_item("iterations", sol.iterations).ok();
6776 out.set_item("tolerance", sol.tolerance).ok();
6777 out.set_item("max_rel_change", sol.max_rel_change).ok();
6778 Ok(out.into_any().unbind())
6779}
6780
6781#[pyfunction]
6785fn unfold_forward_fold(response: Vec<Vec<f64>>, spectrum: Vec<f64>) -> PyResult<Vec<f64>> {
6786 nucleide_unfold::forward_fold(&response, &spectrum)
6787 .map_err(|e| PyValueError::new_err(e.to_string()))
6788}
6789
6790fn parse_plasma_reaction(name: &str) -> PyResult<nucleide_plasma_source::FusionReaction> {
6796 use nucleide_plasma_source::FusionReaction as R;
6797 match name
6798 .to_ascii_lowercase()
6799 .replace(['-', '_', ' '], "")
6800 .as_str()
6801 {
6802 "dt" => Ok(R::Dt),
6803 "dd" => Ok(R::Dd),
6804 other => Err(PyValueError::new_err(format!(
6805 "unknown fusion reaction `{other}` (supported: dt, dd)"
6806 ))),
6807 }
6808}
6809
6810enum PyPlasmaSource {
6812 Basic(nucleide_plasma_source::PlasmaSourceConfig),
6813 Parametric(nucleide_plasma_source::ParametricPlasmaConfig),
6814}
6815
6816fn parse_plasma_basic_spec(
6818 spec: &BTreeMap<String, Py<PyAny>>,
6819 py: Python<'_>,
6820 kind: &str,
6821) -> PyResult<nucleide_plasma_source::PlasmaSourceConfig> {
6822 use nucleide_plasma_source as ps;
6823 let num = |key: &str| -> PyResult<f64> {
6824 spec.get(key)
6825 .ok_or_else(|| PyValueError::new_err(format!("source spec missing `{key}`")))?
6826 .extract::<f64>(py)
6827 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
6828 };
6829 let reaction = parse_plasma_reaction(&get_str(
6830 spec,
6831 py,
6832 "reaction",
6833 "source spec missing `reaction`",
6834 )?)?;
6835 let model = match kind {
6836 "point" => {
6837 let position: Vec<f64> = spec
6838 .get("position")
6839 .ok_or_else(|| PyValueError::new_err("point source needs `position` [cm]"))?
6840 .extract::<Vec<f64>>(py)
6841 .map_err(|_| PyValueError::new_err("`position` must be a list of numbers"))?;
6842 if position.len() != 3 {
6843 return Err(PyValueError::new_err(
6844 "`position` must have exactly three entries",
6845 ));
6846 }
6847 ps::SourceModel::Point(ps::PointSource {
6848 x_cm: position[0],
6849 y_cm: position[1],
6850 z_cm: position[2],
6851 })
6852 }
6853 "ring" => ps::SourceModel::Ring(ps::RingSource {
6854 radius_cm: num("radius")?,
6855 height_cm: num("height")?,
6856 }),
6857 other => {
6858 return Err(PyValueError::new_err(format!(
6859 "unknown source kind `{other}` (supported: point, ring, parametric)"
6860 )))
6861 }
6862 };
6863 let mut config = ps::PlasmaSourceConfig {
6864 model,
6865 reaction,
6866 ion_temperature_kev: num("ion_temperature_kev")?,
6867 weight: 1.0,
6868 };
6869 if let Some(weight) = spec.get("weight") {
6870 let weight = weight
6871 .extract::<f64>(py)
6872 .map_err(|_| PyValueError::new_err("`weight` must be a number"))?;
6873 config = config.with_weight(weight);
6874 }
6875 config
6876 .validate()
6877 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6878 Ok(config)
6879}
6880
6881fn parse_plasma_parametric_spec(
6898 spec: &BTreeMap<String, Py<PyAny>>,
6899 py: Python<'_>,
6900) -> PyResult<nucleide_plasma_source::ParametricPlasmaConfig> {
6901 use nucleide_plasma_source as ps;
6902 let num = |key: &str| -> PyResult<f64> {
6903 spec.get(key)
6904 .ok_or_else(|| PyValueError::new_err(format!("parametric spec missing `{key}`")))?
6905 .extract::<f64>(py)
6906 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
6907 };
6908 for key in ["start_angle", "rotation_angle"] {
6909 if spec.contains_key(key) {
6910 return Err(PyValueError::new_err(format!(
6911 "plasma-source: not yet supported: `{key}` (sectors need a \
6912 toroidal-angle distribution — outside the parametric model)"
6913 )));
6914 }
6915 }
6916 let fuel_mixture = match spec.get("fuel") {
6919 Some(value) => {
6920 let fractions: BTreeMap<String, f64> = value.extract(py).map_err(|_| {
6921 PyValueError::new_err(
6922 "`fuel` must be a dict of fractions like {\"D\": 0.7, \"T\": 0.3}",
6923 )
6924 })?;
6925 for key in fractions.keys() {
6926 if !matches!(key.as_str(), "D" | "T") {
6927 return Err(PyValueError::new_err(format!(
6928 "unsupported fuel fraction `{key}` (supported keys: D, T)"
6929 )));
6930 }
6931 }
6932 let missing = |key: &str| {
6933 PyValueError::new_err(format!(
6934 "`fuel` dict needs both `D` and `T` fractions (missing `{key}`)"
6935 ))
6936 };
6937 let f_d = fractions.get("D").copied().ok_or_else(|| missing("D"))?;
6938 let f_t = fractions.get("T").copied().ok_or_else(|| missing("T"))?;
6939 Some(ps::FuelMixture::new(f_d, f_t).map_err(|e| PyValueError::new_err(e.to_string()))?)
6940 }
6941 None => None,
6942 };
6943 let mode = ps::ProfileMode::parse(&get_str(
6944 spec,
6945 py,
6946 "mode",
6947 "parametric spec missing `mode`",
6948 )?)
6949 .map_err(|e| PyValueError::new_err(e.to_string()))?;
6950 let fuel = match spec.get("reaction") {
6953 Some(_) => parse_plasma_reaction(&get_str(
6954 spec,
6955 py,
6956 "reaction",
6957 "parametric spec missing `reaction`",
6958 )?)?,
6959 None if fuel_mixture.is_some() => ps::FusionReaction::Dt,
6960 None => {
6961 return Err(PyValueError::new_err("parametric spec missing `reaction`"));
6962 }
6963 };
6964 let mut config = ps::ParametricPlasmaConfig {
6965 geometry: ps::MillerGeometry {
6966 major_radius_cm: num("major_radius")?,
6967 minor_radius_cm: num("minor_radius")?,
6968 elongation: num("elongation")?,
6969 triangularity: num("triangularity")?,
6970 shafranov_factor_cm: num("shafranov_factor")?,
6971 },
6972 mode,
6973 ion_density: ps::DensityProfile {
6974 centre_m3: num("ion_density_centre")?,
6975 peaking_factor: num("ion_density_peaking_factor")?,
6976 pedestal_m3: num("ion_density_pedestal")?,
6977 separatrix_m3: num("ion_density_separatrix")?,
6978 },
6979 ion_temperature: ps::TemperatureProfile {
6980 centre_kev: num("ion_temperature_centre")?,
6981 peaking_factor: num("ion_temperature_peaking_factor")?,
6982 beta: num("ion_temperature_beta")?,
6983 pedestal_kev: num("ion_temperature_pedestal")?,
6984 separatrix_kev: num("ion_temperature_separatrix")?,
6985 },
6986 pedestal_radius_cm: num("pedestal_radius")?,
6987 fuel,
6988 fuel_mixture,
6989 weight: 1.0,
6990 };
6991 if let Some(weight) = spec.get("weight") {
6992 let weight = weight
6993 .extract::<f64>(py)
6994 .map_err(|_| PyValueError::new_err("`weight` must be a number"))?;
6995 config.weight = weight;
6996 }
6997 config
6998 .validate()
6999 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7000 Ok(config)
7001}
7002
7003fn parse_plasma_source_spec(
7006 spec: &BTreeMap<String, Py<PyAny>>,
7007 py: Python<'_>,
7008) -> PyResult<PyPlasmaSource> {
7009 let kind: String = spec
7010 .get("kind")
7011 .ok_or_else(|| PyValueError::new_err("source spec needs a `kind`"))?
7012 .extract::<String>(py)
7013 .map_err(|_| PyValueError::new_err("`kind` must be a string"))?;
7014 match kind.as_str() {
7015 "point" | "ring" => Ok(PyPlasmaSource::Basic(parse_plasma_basic_spec(
7016 spec, py, &kind,
7017 )?)),
7018 "parametric" => Ok(PyPlasmaSource::Parametric(parse_plasma_parametric_spec(
7019 spec, py,
7020 )?)),
7021 other => Err(PyValueError::new_err(format!(
7022 "unknown source kind `{other}` (supported: point, ring, parametric)"
7023 ))),
7024 }
7025}
7026
7027fn plasma_drift_rows(
7030 py: Python<'_>,
7031 report: &nucleide_plasma_source::DriftReport,
7032) -> PyResult<Vec<Py<pyo3::types::PyDict>>> {
7033 use pyo3::types::PyDict;
7034 let mut rows = Vec::with_capacity(report.rows.len());
7035 for row in &report.rows {
7036 let d = PyDict::new(py);
7037 d.set_item("quantity", &row.quantity)?;
7038 d.set_item("accounted", row.accounted)?;
7039 d.set_item("rel_drift", row.rel_drift)?;
7040 d.set_item("reparsed", row.reparsed)?;
7041 d.set_item("note", &row.note)?;
7042 rows.push(d.unbind());
7043 }
7044 Ok(rows)
7045}
7046
7047#[pyfunction]
7057#[pyo3(signature = (spec, n, seed))]
7058fn plasma_source_particles(
7059 py: Python<'_>,
7060 spec: BTreeMap<String, Py<PyAny>>,
7061 n: usize,
7062 seed: u64,
7063) -> PyResult<Py<PyAny>> {
7064 use nucleide_plasma_source as ps;
7065 let particles = match parse_plasma_source_spec(&spec, py)? {
7066 PyPlasmaSource::Basic(config) => ps::SourceSampler::new(config, seed)
7067 .map_err(|e| PyValueError::new_err(e.to_string()))?
7068 .sample_n(n),
7069 PyPlasmaSource::Parametric(config) => ps::ParametricSampler::new(config, seed)
7070 .map_err(|e| PyValueError::new_err(e.to_string()))?
7071 .sample_n(n),
7072 };
7073 let mut x = Vec::with_capacity(n);
7074 let mut y = Vec::with_capacity(n);
7075 let mut z = Vec::with_capacity(n);
7076 let mut u = Vec::with_capacity(n);
7077 let mut v = Vec::with_capacity(n);
7078 let mut w = Vec::with_capacity(n);
7079 let mut energy = Vec::with_capacity(n);
7080 let mut weight = Vec::with_capacity(n);
7081 for p in &particles {
7082 x.push(p.position_cm[0]);
7083 y.push(p.position_cm[1]);
7084 z.push(p.position_cm[2]);
7085 u.push(p.direction[0]);
7086 v.push(p.direction[1]);
7087 w.push(p.direction[2]);
7088 energy.push(p.energy_mev);
7089 weight.push(p.weight);
7090 }
7091 use pyo3::types::PyDict;
7092 let out = PyDict::new(py);
7093 out.set_item("x", x.into_pyarray(py))?;
7094 out.set_item("y", y.into_pyarray(py))?;
7095 out.set_item("z", z.into_pyarray(py))?;
7096 out.set_item("u", u.into_pyarray(py))?;
7097 out.set_item("v", v.into_pyarray(py))?;
7098 out.set_item("w", w.into_pyarray(py))?;
7099 out.set_item("energy", energy.into_pyarray(py))?;
7100 out.set_item("weight", weight.into_pyarray(py))?;
7101 Ok(out.into_any().unbind())
7102}
7103
7104#[pyfunction]
7116#[pyo3(signature = (spec, bins=21))]
7117fn plasma_source_emit_cards(
7118 py: Python<'_>,
7119 spec: BTreeMap<String, Py<PyAny>>,
7120 bins: usize,
7121) -> PyResult<Py<PyAny>> {
7122 use nucleide_plasma_source as ps;
7123 let source = parse_plasma_source_spec(&spec, py)?;
7124 let version = match spec.get("mcnp_version") {
7125 Some(v) => v
7126 .extract::<u32>(py)
7127 .map_err(|_| PyValueError::new_err("`mcnp_version` must be an integer (5 or 6)"))?,
7128 None => 5,
7129 };
7130 let emit = |card: ps::EmittedCard| -> PyResult<Py<pyo3::types::PyDict>> {
7131 use pyo3::types::PyDict;
7132 let d = PyDict::new(py);
7133 d.set_item("card", card.text)?;
7134 d.set_item("drift", plasma_drift_rows(py, &card.drift)?)?;
7135 Ok(d.unbind())
7136 };
7137 use pyo3::types::PyDict;
7138 let out = PyDict::new(py);
7139 let (nominal, mean, sigma, mono) = match &source {
7140 PyPlasmaSource::Basic(config) => {
7141 let sdef = ps::emit_sdef(config, version, bins)
7142 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7143 let serpent =
7144 ps::emit_serpent(config, bins).map_err(|e| PyValueError::new_err(e.to_string()))?;
7145 let spectrum = config
7146 .spectrum()
7147 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7148 let sigma = match spectrum {
7149 ps::SpectrumSpec::Gaussian { sigma_mev, .. } => sigma_mev,
7150 ps::SpectrumSpec::Mono { .. } => 0.0,
7151 };
7152 out.set_item("sdef", emit(sdef)?)?;
7153 out.set_item("serpent", emit(serpent)?)?;
7154 (
7155 config.reaction.nominal_energy_mev(),
7156 spectrum.mean_mev(),
7157 sigma,
7158 spectrum.is_mono(),
7159 )
7160 }
7161 PyPlasmaSource::Parametric(config) => {
7162 let sdef = ps::emit_sdef_parametric(config, version, bins)
7163 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7164 let serpent = ps::emit_serpent_parametric(config, bins)
7165 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7166 let (nominal, mean, sigma) = config
7169 .axis_spectrum_summary()
7170 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7171 out.set_item("sdef", emit(sdef)?)?;
7172 out.set_item("serpent", emit(serpent)?)?;
7173 (nominal, mean, sigma, sigma == 0.0)
7174 }
7175 };
7176 let spec_out = PyDict::new(py);
7177 spec_out.set_item("nominal_mev", nominal)?;
7178 spec_out.set_item("mean_mev", mean)?;
7179 spec_out.set_item("sigma_mev", sigma)?;
7180 spec_out.set_item("mono", mono)?;
7181 out.set_item("spectrum", spec_out)?;
7182 Ok(out.into_any().unbind())
7183}
7184
7185#[pyfunction]
7192fn plasma_source_spectrum_moments(
7193 py: Python<'_>,
7194 reaction: &str,
7195 ion_temperature_kev: f64,
7196) -> PyResult<Py<PyAny>> {
7197 use nucleide_plasma_source::FusionReaction as R;
7198 let reaction = parse_plasma_reaction(reaction)?;
7199 let (mean, sigma) = reaction
7200 .moments_mev(ion_temperature_kev)
7201 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7202 use pyo3::types::PyDict;
7203 let out = PyDict::new(py);
7204 out.set_item(
7205 "reaction",
7206 match reaction {
7207 R::Dt => "dt",
7208 R::Dd => "dd",
7209 },
7210 )?;
7211 out.set_item("label", reaction.label())?;
7212 out.set_item("nominal_mev", reaction.nominal_energy_mev())?;
7213 out.set_item("mean_mev", mean)?;
7214 out.set_item("sigma_mev", sigma)?;
7215 Ok(out.into_any().unbind())
7216}
7217
7218#[pyfunction]
7222fn plasma_source_reactivity(reaction: &str, ion_temperature_kev: f64) -> PyResult<f64> {
7223 parse_plasma_reaction(reaction)?
7224 .reactivity_m3_per_s(ion_temperature_kev)
7225 .map_err(|e| PyValueError::new_err(e.to_string()))
7226}
7227
7228fn parse_damage_nuclide(key: &Bound<'_, PyAny>) -> PyResult<NuclideId> {
7234 if let Ok(nucid) = key.extract::<u32>() {
7235 return NuclideId::try_from_nucid(nucid).map_err(wrap_nucid_err);
7236 }
7237 if let Ok(name) = key.extract::<&str>() {
7238 return NuclideId::from_name(name).map_err(wrap_nucid_err);
7239 }
7240 Err(PyTypeError::new_err("expected int nucid or str name"))
7241}
7242
7243#[pyfunction]
7249#[pyo3(signature = (flux, response, bounds, seconds))]
7250fn damage_nrt_dpa(
7251 flux: Vec<f64>,
7252 response: Vec<f64>,
7253 bounds: Vec<f64>,
7254 seconds: f64,
7255) -> PyResult<f64> {
7256 nucleide_damage::nrt_dpa(&flux, &response, &bounds, seconds)
7257 .map_err(|e| PyValueError::new_err(e.to_string()))
7258}
7259
7260#[pyfunction]
7262#[pyo3(signature = (flux, response, bounds, seconds))]
7263fn damage_arc_dpa(
7264 flux: Vec<f64>,
7265 response: Vec<f64>,
7266 bounds: Vec<f64>,
7267 seconds: f64,
7268) -> PyResult<f64> {
7269 nucleide_damage::arc_dpa(&flux, &response, &bounds, seconds)
7270 .map_err(|e| PyValueError::new_err(e.to_string()))
7271}
7272
7273#[pyfunction]
7276#[pyo3(signature = (flux, response, bounds, seconds))]
7277fn damage_gas_appm(
7278 flux: Vec<f64>,
7279 response: Vec<f64>,
7280 bounds: Vec<f64>,
7281 seconds: f64,
7282) -> PyResult<f64> {
7283 nucleide_damage::gas_appm(&flux, &response, &bounds, seconds)
7284 .map_err(|e| PyValueError::new_err(e.to_string()))
7285}
7286
7287#[pyfunction]
7291#[pyo3(signature = (flux, he_response, damage_response, bounds, seconds))]
7292fn damage_he_dpa_ratio(
7293 flux: Vec<f64>,
7294 he_response: Vec<f64>,
7295 damage_response: Vec<f64>,
7296 bounds: Vec<f64>,
7297 seconds: f64,
7298) -> PyResult<f64> {
7299 nucleide_damage::he_dpa_ratio(&flux, &he_response, &damage_response, &bounds, seconds)
7300 .map_err(|e| PyValueError::new_err(e.to_string()))
7301}
7302
7303#[pyfunction]
7307#[pyo3(signature = (t_ev, recoil, lattice))]
7308fn damage_lindhard_partition(
7309 t_ev: f64,
7310 recoil: &Bound<'_, PyAny>,
7311 lattice: &Bound<'_, PyAny>,
7312) -> PyResult<f64> {
7313 let recoil = parse_damage_nuclide(recoil)?;
7314 let lattice = parse_damage_nuclide(lattice)?;
7315 nucleide_damage::lindhard_partition(t_ev, &recoil, &lattice)
7316 .map_err(|e| PyValueError::new_err(e.to_string()))
7317}
7318
7319#[pyfunction]
7321#[pyo3(signature = (t_ev, recoil, lattice))]
7322fn damage_damage_energy(
7323 t_ev: f64,
7324 recoil: &Bound<'_, PyAny>,
7325 lattice: &Bound<'_, PyAny>,
7326) -> PyResult<f64> {
7327 let recoil = parse_damage_nuclide(recoil)?;
7328 let lattice = parse_damage_nuclide(lattice)?;
7329 nucleide_damage::damage_energy(t_ev, &recoil, &lattice)
7330 .map_err(|e| PyValueError::new_err(e.to_string()))
7331}
7332
7333#[pyfunction]
7336#[pyo3(signature = (t_ev, ed_ev, target))]
7337fn damage_nrt_displacements(t_ev: f64, ed_ev: f64, target: &Bound<'_, PyAny>) -> PyResult<f64> {
7338 let target = parse_damage_nuclide(target)?;
7339 nucleide_damage::nrt_displacements(t_ev, ed_ev, &target)
7340 .map_err(|e| PyValueError::new_err(e.to_string()))
7341}
7342
7343#[pyfunction]
7346#[pyo3(signature = (t_dam_ev, ed_ev, b_arc, c_arc))]
7347fn damage_arc_efficiency(t_dam_ev: f64, ed_ev: f64, b_arc: f64, c_arc: f64) -> PyResult<f64> {
7348 let params = nucleide_damage::ArcParams::new(b_arc, c_arc)
7349 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7350 nucleide_damage::arc_efficiency(t_dam_ev, ed_ev, ¶ms)
7351 .map_err(|e| PyValueError::new_err(e.to_string()))
7352}
7353
7354#[pyfunction]
7357#[pyo3(signature = (t_ev, ed_ev, target, b_arc, c_arc))]
7358fn damage_arc_displacements(
7359 t_ev: f64,
7360 ed_ev: f64,
7361 target: &Bound<'_, PyAny>,
7362 b_arc: f64,
7363 c_arc: f64,
7364) -> PyResult<f64> {
7365 let target = parse_damage_nuclide(target)?;
7366 let params = nucleide_damage::ArcParams::new(b_arc, c_arc)
7367 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7368 nucleide_damage::arc_displacements(t_ev, ed_ev, &target, ¶ms)
7369 .map_err(|e| PyValueError::new_err(e.to_string()))
7370}
7371
7372#[pyfunction]
7379#[pyo3(signature = (metric, flux, response, bounds, seconds, mean, cov, n, seed, k))]
7380#[allow(clippy::too_many_arguments)] fn damage_fold_uq(
7382 py: Python<'_>,
7383 metric: &str,
7384 flux: Vec<f64>,
7385 response: Vec<f64>,
7386 bounds: Vec<f64>,
7387 seconds: f64,
7388 mean: Vec<f64>,
7389 cov: Vec<Vec<f64>>,
7390 n: usize,
7391 seed: u64,
7392 k: f64,
7393) -> PyResult<Py<PyAny>> {
7394 use nucleide_damage::FoldMetric as M;
7395 let metric = match metric
7396 .to_ascii_lowercase()
7397 .replace(['-', ' '], "_")
7398 .as_str()
7399 {
7400 "nrt_dpa" => M::NrtDpa,
7401 "arc_dpa" => M::ArcDpa,
7402 "gas_appm" => M::GasAppm,
7403 "he_dpa_ratio" => M::HeDpaRatio,
7404 other => {
7405 return Err(PyValueError::new_err(format!(
7406 "unknown fold metric `{other}` (supported: nrt_dpa, arc_dpa, gas_appm)"
7407 )))
7408 }
7409 };
7410 let s = nucleide_damage::fold_uq(
7411 metric, &flux, &response, &bounds, seconds, &mean, &cov, n, seed, k,
7412 )
7413 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7414 use pyo3::types::PyDict;
7415 let out = PyDict::new(py);
7416 out.set_item("metric", s.metric.name())?;
7417 out.set_item("nominal", s.nominal)?;
7418 out.set_item("mean", s.mean)?;
7419 out.set_item("std", s.std)?;
7420 out.set_item("expected", s.expected)?;
7421 out.set_item("analytic_std", s.analytic_std)?;
7422 out.set_item("k", s.k)?;
7423 out.set_item("n", s.n)?;
7424 out.set_item("seed", s.seed)?;
7425 out.set_item("passed", s.passed)?;
7426 Ok(out.into_any().unbind())
7427}
7428
7429#[pyfunction]
7439#[pyo3(signature = (spectrum,))]
7440fn damage_specter_table(spectrum: &str) -> PyResult<BTreeMap<String, f64>> {
7441 let spectrum = nucleide_damage::SpecterSpectrum::parse(spectrum)
7442 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7443 Ok(nucleide_damage::SpecterTable::for_spectrum(spectrum)
7444 .iter()
7445 .map(|(element, entry)| (element.clone(), entry.dpa_xs_barns()))
7446 .collect())
7447}
7448
7449#[pyfunction]
7455#[pyo3(signature = (spectrum,))]
7456fn damage_specter_damage_energy(spectrum: &str) -> PyResult<BTreeMap<String, f64>> {
7457 let spectrum = nucleide_damage::SpecterSpectrum::parse(spectrum)
7458 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7459 Ok(nucleide_damage::SpecterTable::for_spectrum(spectrum)
7460 .iter()
7461 .map(|(element, entry)| (element.clone(), entry.damage_energy_kev_b))
7462 .collect())
7463}
7464
7465#[pyfunction]
7468#[pyo3(signature = (element,))]
7469fn damage_specter_ed(element: &str) -> PyResult<f64> {
7470 nucleide_damage::specter_ed_ev(element).map_err(|e| PyValueError::new_err(e.to_string()))
7471}
7472
7473#[pyfunction]
7475fn damage_specter_spectra() -> Vec<String> {
7476 nucleide_damage::SpecterSpectrum::ALL
7477 .iter()
7478 .map(|s| s.name().to_string())
7479 .collect()
7480}
7481
7482fn parse_tritium_boundary(
7494 spec: &BTreeMap<String, Py<PyAny>>,
7495 py: Python<'_>,
7496) -> PyResult<nucleide_tritium::Boundary> {
7497 use nucleide_tritium::Boundary as B;
7498 let kind: String = spec
7499 .get("kind")
7500 .ok_or_else(|| PyValueError::new_err("boundary spec needs a `kind`"))?
7501 .extract::<String>(py)
7502 .map_err(|_| PyValueError::new_err("`kind` must be a string"))?;
7503 let num = |key: &str| -> PyResult<f64> {
7504 spec.get(key)
7505 .ok_or_else(|| PyValueError::new_err(format!("boundary spec missing `{key}`")))?
7506 .extract::<f64>(py)
7507 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
7508 };
7509 let b = match kind.as_str() {
7510 "dirichlet" => B::dirichlet(num("value")?),
7511 "sieverts" => B::sieverts(num("solubility")?, num("pressure")?),
7512 "henry" => B::henry(num("solubility")?, num("pressure")?),
7513 "recombination" => B::recombination(num("rate")?),
7514 "zero_flux" => Ok(B::ZeroFlux),
7515 other => {
7516 return Err(PyValueError::new_err(format!(
7517 "unknown boundary kind `{other}` (supported: dirichlet, sieverts, henry, recombination, zero_flux)"
7518 )))
7519 }
7520 };
7521 b.map_err(|e| PyValueError::new_err(e.to_string()))
7522}
7523
7524fn parse_tritium_trap(
7529 spec: &BTreeMap<String, Py<PyAny>>,
7530 py: Python<'_>,
7531) -> PyResult<nucleide_tritium::TrapSpec> {
7532 let num = |key: &str| -> PyResult<f64> {
7533 spec.get(key)
7534 .ok_or_else(|| PyValueError::new_err(format!("trap spec missing `{key}`")))?
7535 .extract::<f64>(py)
7536 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
7537 };
7538 let opt = |key: &str| -> PyResult<f64> {
7539 match spec.get(key) {
7540 None => Ok(0.0),
7541 Some(v) => v
7542 .extract::<f64>(py)
7543 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number"))),
7544 }
7545 };
7546 nucleide_tritium::TrapSpec::new(
7547 num("k0")?,
7548 opt("e_k")?,
7549 num("p0")?,
7550 opt("e_p")?,
7551 num("site_density")?,
7552 )
7553 .map_err(|e| PyValueError::new_err(e.to_string()))
7554}
7555
7556#[allow(clippy::too_many_arguments)]
7557fn tritium_params(
7558 py: Python<'_>,
7559 length: f64,
7560 cells: usize,
7561 d0: f64,
7562 e_d: f64,
7563 traps: Vec<BTreeMap<String, Py<PyAny>>>,
7564 temperature: Vec<f64>,
7565 source: Option<Vec<f64>>,
7566) -> PyResult<nucleide_tritium::TransportParams> {
7567 let parsed: Vec<nucleide_tritium::TrapSpec> = traps
7568 .iter()
7569 .map(|s| parse_tritium_trap(s, py))
7570 .collect::<PyResult<_>>()?;
7571 nucleide_tritium::TransportParams::new(
7572 length,
7573 cells,
7574 d0,
7575 e_d,
7576 parsed,
7577 temperature,
7578 source.unwrap_or_default(),
7579 )
7580 .map_err(|e| PyValueError::new_err(e.to_string()))
7581}
7582
7583#[pyfunction]
7593#[pyo3(signature = (length, cells, d0, e_d, traps, temperature, source, left, right))]
7594#[allow(clippy::too_many_arguments)]
7595fn tritium_steady(
7596 py: Python<'_>,
7597 length: f64,
7598 cells: usize,
7599 d0: f64,
7600 e_d: f64,
7601 traps: Vec<BTreeMap<String, Py<PyAny>>>,
7602 temperature: Vec<f64>,
7603 source: Option<Vec<f64>>,
7604 left: BTreeMap<String, Py<PyAny>>,
7605 right: BTreeMap<String, Py<PyAny>>,
7606) -> PyResult<Py<PyAny>> {
7607 let params = tritium_params(py, length, cells, d0, e_d, traps, temperature, source)?;
7608 let left = parse_tritium_boundary(&left, py)?;
7609 let right = parse_tritium_boundary(&right, py)?;
7610 let s = nucleide_tritium::steady_state(¶ms, &left, &right)
7611 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7612 use pyo3::types::PyDict;
7613 let out = PyDict::new(py);
7614 out.set_item("centres", &s.centres).ok();
7615 out.set_item("mobile", &s.mobile).ok();
7616 out.set_item("trapped", &s.trapped).ok();
7617 out.set_item("flux_left", s.flux_left).ok();
7618 out.set_item("flux_right", s.flux_right).ok();
7619 out.set_item("inventory_mobile", s.inventory_mobile).ok();
7620 out.set_item("inventory_trapped", s.inventory_trapped).ok();
7621 Ok(out.into_any().unbind())
7622}
7623
7624#[pyfunction]
7634#[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))]
7635#[allow(clippy::too_many_arguments)]
7636fn tritium_transient(
7637 py: Python<'_>,
7638 length: f64,
7639 cells: usize,
7640 d0: f64,
7641 e_d: f64,
7642 traps: Vec<BTreeMap<String, Py<PyAny>>>,
7643 temperature: Vec<f64>,
7644 source: Option<Vec<f64>>,
7645 left: BTreeMap<String, Py<PyAny>>,
7646 right: BTreeMap<String, Py<PyAny>>,
7647 t: Vec<f64>,
7648 mobile0: Option<Vec<f64>>,
7649 trapped0: Option<Vec<Vec<f64>>>,
7650 method: &str,
7651 rtol: f64,
7652 atol: f64,
7653 dt_min: f64,
7654 dt_max: Option<f64>,
7655 max_steps: usize,
7656) -> PyResult<Py<PyAny>> {
7657 use nucleide_tritium::{SolverOptions, Theta};
7658 let params = tritium_params(py, length, cells, d0, e_d, traps, temperature, source)?;
7659 let left = parse_tritium_boundary(&left, py)?;
7660 let right = parse_tritium_boundary(&right, py)?;
7661 let grid =
7662 nucleide_tritium::TimeGrid::new(t).map_err(|e| PyValueError::new_err(e.to_string()))?;
7663 let ntraps = params.traps.len();
7664 let mobile = mobile0.unwrap_or_else(|| vec![0.0; params.cells]);
7665 let trapped = trapped0.unwrap_or_else(|| vec![vec![0.0; ntraps]; params.cells]);
7666 let initial = nucleide_tritium::InitialState::new(¶ms, mobile, trapped)
7667 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7668 let theta = if method.eq_ignore_ascii_case("crank_nicolson") {
7669 Theta::CrankNicolson
7670 } else if method.eq_ignore_ascii_case("backward_euler") {
7671 Theta::BackwardEuler
7672 } else {
7673 return Err(PyValueError::new_err(format!(
7674 "unknown tritium method `{method}` (supported: crank_nicolson, backward_euler)"
7675 )));
7676 };
7677 let opts = SolverOptions {
7678 theta,
7679 rtol,
7680 atol,
7681 dt_min,
7682 dt_max: dt_max.unwrap_or(f64::INFINITY),
7683 max_steps,
7684 };
7685 let sol = nucleide_tritium::solve(¶ms, &left, &right, &grid, &initial, &opts)
7686 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7687 use pyo3::types::PyDict;
7688 let out = PyDict::new(py);
7689 out.set_item("times", &sol.times).ok();
7690 out.set_item("mobile", &sol.mobile).ok();
7691 out.set_item("trapped", &sol.trapped).ok();
7692 out.set_item("flux_left", &sol.flux_left).ok();
7693 out.set_item("flux_right", &sol.flux_right).ok();
7694 Ok(out.into_any().unbind())
7695}
7696
7697#[pyfunction]
7699fn tritium_time_lag(length: f64, diffusivity: f64) -> PyResult<f64> {
7700 nucleide_tritium::time_lag(length, diffusivity)
7701 .map_err(|e| PyValueError::new_err(e.to_string()))
7702}
7703
7704#[pyfunction]
7706fn tritium_breakthrough(diffusivity: f64, length: f64, times: Vec<f64>) -> PyResult<Vec<f64>> {
7707 times
7708 .iter()
7709 .map(|t| {
7710 nucleide_tritium::breakthrough_ratio(diffusivity, length, *t)
7711 .map_err(|e| PyValueError::new_err(e.to_string()))
7712 })
7713 .collect()
7714}
7715
7716#[pyfunction]
7718fn tritium_oriani(diffusivity: f64, equilibrium_constant: f64, site_density: f64) -> PyResult<f64> {
7719 nucleide_tritium::effective_diffusivity(diffusivity, equilibrium_constant, site_density)
7720 .map_err(|e| PyValueError::new_err(e.to_string()))
7721}
7722
7723#[pyfunction]
7725fn tritium_langmuir(site_density: f64, equilibrium_constant: f64, c_mobile: f64) -> PyResult<f64> {
7726 nucleide_tritium::equilibrium_trapped(site_density, equilibrium_constant, c_mobile)
7727 .map_err(|e| PyValueError::new_err(e.to_string()))
7728}
7729
7730#[pyfunction]
7732fn tritium_irreversible_fill(
7733 rate_k: f64,
7734 c_mobile: f64,
7735 site_density: f64,
7736 times: Vec<f64>,
7737) -> PyResult<Vec<f64>> {
7738 times
7739 .iter()
7740 .map(|t| {
7741 nucleide_tritium::irreversible_fill(rate_k, c_mobile, site_density, *t)
7742 .map_err(|e| PyValueError::new_err(e.to_string()))
7743 })
7744 .collect()
7745}
7746
7747#[pyfunction]
7749fn tritium_sieverts(solubility: f64, pressure: f64) -> PyResult<f64> {
7750 nucleide_tritium::sieverts_concentration(solubility, pressure)
7751 .map_err(|e| PyValueError::new_err(e.to_string()))
7752}
7753
7754#[pyfunction]
7756fn tritium_recombination_rate(kr0: f64, e_r: f64, temp: f64) -> PyResult<f64> {
7757 nucleide_tritium::recombination_rate_arrhenius(kr0, e_r, temp)
7758 .map_err(|e| PyValueError::new_err(e.to_string()))
7759}
7760
7761fn parse_tritium_layer(
7772 spec: &BTreeMap<String, Py<PyAny>>,
7773 py: Python<'_>,
7774) -> PyResult<nucleide_tritium::LayerSpec> {
7775 let num = |key: &str| -> PyResult<f64> {
7776 spec.get(key)
7777 .ok_or_else(|| PyValueError::new_err(format!("layer spec missing `{key}`")))?
7778 .extract::<f64>(py)
7779 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number")))
7780 };
7781 let opt = |key: &str| -> PyResult<f64> {
7782 match spec.get(key) {
7783 None => Ok(0.0),
7784 Some(v) => v
7785 .extract::<f64>(py)
7786 .map_err(|_| PyValueError::new_err(format!("`{key}` must be a number"))),
7787 }
7788 };
7789 let traps = match spec.get("traps") {
7790 None => Vec::new(),
7791 Some(v) => v
7792 .extract::<Vec<BTreeMap<String, Py<PyAny>>>>(py)
7793 .map_err(|_| PyValueError::new_err("`traps` must be a list of dicts"))?
7794 .iter()
7795 .map(|s| parse_tritium_trap(s, py))
7796 .collect::<PyResult<_>>()?,
7797 };
7798 let temperature: Vec<f64> = match spec.get("temperature") {
7799 None => vec![500.0],
7800 Some(v) => v
7801 .extract::<Vec<f64>>(py)
7802 .map_err(|_| PyValueError::new_err("`temperature` must be a list of numbers"))?,
7803 };
7804 let source: Vec<f64> = match spec.get("source") {
7805 None => Vec::new(),
7806 Some(v) => v
7807 .extract::<Vec<f64>>(py)
7808 .map_err(|_| PyValueError::new_err("`source` must be a list of numbers"))?,
7809 };
7810 let cells: usize = spec
7811 .get("cells")
7812 .ok_or_else(|| PyValueError::new_err("layer spec missing `cells`"))?
7813 .extract::<usize>(py)
7814 .map_err(|_| PyValueError::new_err("`cells` must be an integer"))?;
7815 nucleide_tritium::LayerSpec::new(
7816 num("thickness")?,
7817 cells,
7818 num("D")?,
7819 opt("E_D")?,
7820 num("solubility")?,
7821 traps,
7822 temperature,
7823 source,
7824 )
7825 .map_err(|e| PyValueError::new_err(e.to_string()))
7826}
7827
7828fn parse_tritium_interfaces(
7839 specs: Option<Vec<String>>,
7840 n_layers: usize,
7841) -> PyResult<Vec<nucleide_tritium::Interface>> {
7842 use nucleide_tritium::Interface as I;
7843 match specs {
7844 None => Ok(vec![I::Sieverts; n_layers.saturating_sub(1)]),
7845 Some(list) => {
7846 if list.len() + 1 != n_layers {
7847 return Err(PyValueError::new_err(format!(
7848 "need exactly one interface per gap ({n_layers} layers -> {} interfaces, got {})",
7849 n_layers.saturating_sub(1),
7850 list.len()
7851 )));
7852 }
7853 list.iter()
7854 .map(|s| match s.to_ascii_lowercase().as_str() {
7855 "sieverts" => Ok(I::Sieverts),
7856 "henry" => Ok(I::Henry),
7857 "recombination" => Ok(I::Recombination),
7858 other => Err(PyValueError::new_err(format!(
7859 "unknown tritium interface kind `{other}` (supported: sieverts, henry; recombination interfaces are not supported)"
7860 ))),
7861 })
7862 .collect()
7863 }
7864 }
7865}
7866
7867fn tritium_layer_stack(
7868 py: Python<'_>,
7869 layers: Vec<BTreeMap<String, Py<PyAny>>>,
7870 interfaces: Option<Vec<String>>,
7871) -> PyResult<nucleide_tritium::LayerStack> {
7872 let parsed: Vec<nucleide_tritium::LayerSpec> = layers
7873 .iter()
7874 .map(|s| parse_tritium_layer(s, py))
7875 .collect::<PyResult<_>>()?;
7876 let interfaces = parse_tritium_interfaces(interfaces, parsed.len())?;
7877 nucleide_tritium::LayerStack::new(parsed, interfaces)
7878 .map_err(|e| PyValueError::new_err(e.to_string()))
7879}
7880
7881#[pyfunction]
7892#[pyo3(signature = (layers, left, right, interfaces=None))]
7893fn tritium_layers_steady(
7894 py: Python<'_>,
7895 layers: Vec<BTreeMap<String, Py<PyAny>>>,
7896 left: BTreeMap<String, Py<PyAny>>,
7897 right: BTreeMap<String, Py<PyAny>>,
7898 interfaces: Option<Vec<String>>,
7899) -> PyResult<Py<PyAny>> {
7900 let stack = tritium_layer_stack(py, layers, interfaces)?;
7901 let left = parse_tritium_boundary(&left, py)?;
7902 let right = parse_tritium_boundary(&right, py)?;
7903 let s = nucleide_tritium::steady_layers(&stack, &left, &right)
7904 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7905 use pyo3::types::PyDict;
7906 let out = PyDict::new(py);
7907 out.set_item("centres", &s.centres).ok();
7908 out.set_item("mobile", &s.mobile).ok();
7909 out.set_item("trapped", &s.trapped).ok();
7910 out.set_item("flux_left", s.flux_left).ok();
7911 out.set_item("flux_right", s.flux_right).ok();
7912 out.set_item("inventory_mobile", s.inventory_mobile).ok();
7913 out.set_item("inventory_trapped", s.inventory_trapped).ok();
7914 Ok(out.into_any().unbind())
7915}
7916
7917#[pyfunction]
7929#[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, interfaces=None))]
7930#[allow(clippy::too_many_arguments)]
7931fn tritium_layers_transient(
7932 py: Python<'_>,
7933 layers: Vec<BTreeMap<String, Py<PyAny>>>,
7934 left: BTreeMap<String, Py<PyAny>>,
7935 right: BTreeMap<String, Py<PyAny>>,
7936 t: Vec<f64>,
7937 mobile0: Option<Vec<f64>>,
7938 trapped0: Option<Vec<Vec<f64>>>,
7939 method: &str,
7940 rtol: f64,
7941 atol: f64,
7942 dt_min: f64,
7943 dt_max: Option<f64>,
7944 max_steps: usize,
7945 interfaces: Option<Vec<String>>,
7946) -> PyResult<Py<PyAny>> {
7947 use nucleide_tritium::{SolverOptions, Theta};
7948 let stack = tritium_layer_stack(py, layers, interfaces)?;
7949 let left = parse_tritium_boundary(&left, py)?;
7950 let right = parse_tritium_boundary(&right, py)?;
7951 let grid =
7952 nucleide_tritium::TimeGrid::new(t).map_err(|e| PyValueError::new_err(e.to_string()))?;
7953 let mobile = mobile0.unwrap_or_else(|| vec![0.0; stack.total_cells()]);
7954 let trapped = trapped0.unwrap_or_else(|| stack.zero_state().trapped);
7955 let initial = nucleide_tritium::InitialState { mobile, trapped };
7956 let theta = if method.eq_ignore_ascii_case("crank_nicolson") {
7957 Theta::CrankNicolson
7958 } else if method.eq_ignore_ascii_case("backward_euler") {
7959 Theta::BackwardEuler
7960 } else {
7961 return Err(PyValueError::new_err(format!(
7962 "unknown tritium method `{method}` (supported: crank_nicolson, backward_euler)"
7963 )));
7964 };
7965 let opts = SolverOptions {
7966 theta,
7967 rtol,
7968 atol,
7969 dt_min,
7970 dt_max: dt_max.unwrap_or(f64::INFINITY),
7971 max_steps,
7972 };
7973 let sol = nucleide_tritium::solve_layers(&stack, &left, &right, &grid, &initial, &opts)
7974 .map_err(|e| PyValueError::new_err(e.to_string()))?;
7975 use pyo3::types::PyDict;
7976 let out = PyDict::new(py);
7977 out.set_item("times", &sol.times).ok();
7978 out.set_item("mobile", &sol.mobile).ok();
7979 out.set_item("trapped", &sol.trapped).ok();
7980 out.set_item("flux_left", &sol.flux_left).ok();
7981 out.set_item("flux_right", &sol.flux_right).ok();
7982 Ok(out.into_any().unbind())
7983}
7984
7985#[pyfunction]
7991fn spectroscopy_rect_smooth(counts: Vec<f64>, m: i64) -> PyResult<Vec<f64>> {
7992 let w = usize::try_from(m).map_err(|_| {
7993 PyValueError::new_err(format!("spectroscopy: smoothing width {m} is less than 3"))
7994 })?;
7995 nucleide_spectroscopy::rect_smooth(&counts, w).map_err(|e| PyValueError::new_err(e.to_string()))
7996}
7997
7998#[pyfunction]
8000fn spectroscopy_five_point_smooth(counts: Vec<f64>) -> PyResult<Vec<f64>> {
8001 nucleide_spectroscopy::five_point_smooth(&counts)
8002 .map_err(|e| PyValueError::new_err(e.to_string()))
8003}
8004
8005#[pyfunction]
8007fn spectroscopy_calc_bg(
8008 counts: Vec<f64>,
8009 channels: Vec<f64>,
8010 c1: i64,
8011 c2: i64,
8012 m: i64,
8013) -> PyResult<f64> {
8014 nucleide_spectroscopy::calc_bg(&counts, &channels, c1, c2, m)
8015 .map_err(|e| PyValueError::new_err(e.to_string()))
8016}
8017
8018#[pyfunction]
8020fn spectroscopy_gross_count(
8021 counts: Vec<f64>,
8022 channels: Vec<f64>,
8023 c1: i64,
8024 c2: i64,
8025) -> PyResult<f64> {
8026 nucleide_spectroscopy::gross_count(&counts, &channels, c1, c2)
8027 .map_err(|e| PyValueError::new_err(e.to_string()))
8028}
8029
8030#[pyfunction]
8032fn spectroscopy_net_counts(
8033 counts: Vec<f64>,
8034 channels: Vec<f64>,
8035 c1: i64,
8036 c2: i64,
8037 m: i64,
8038) -> PyResult<f64> {
8039 nucleide_spectroscopy::net_counts(&counts, &channels, c1, c2, m)
8040 .map_err(|e| PyValueError::new_err(e.to_string()))
8041}
8042
8043#[pyfunction]
8045fn spectroscopy_energy_bins(channels: Vec<f64>, calib_e_fit: Vec<f64>) -> PyResult<Vec<f64>> {
8046 nucleide_spectroscopy::energy_bins(&channels, &calib_e_fit)
8047 .map_err(|e| PyValueError::new_err(e.to_string()))
8048}
8049
8050#[pyfunction]
8052fn spectroscopy_detector_efficiency(
8053 energy_mev: f64,
8054 eff_coeff: Vec<f64>,
8055 eff_fit: i64,
8056) -> PyResult<f64> {
8057 nucleide_spectroscopy::detector_efficiency(energy_mev, &eff_coeff, eff_fit)
8058 .map_err(|e| PyValueError::new_err(e.to_string()))
8059}
8060
8061#[pyfunction]
8067#[pyo3(signature = (energies, effs, weights, order, eff_fit=1))]
8068fn spectroscopy_fit_efficiency(
8069 energies: Vec<f64>,
8070 effs: Vec<f64>,
8071 weights: Vec<f64>,
8072 order: usize,
8073 eff_fit: i64,
8074) -> PyResult<Vec<f64>> {
8075 nucleide_spectroscopy::fit_efficiency(&energies, &effs, &weights, order, eff_fit)
8076 .map_err(|e| PyValueError::new_err(e.to_string()))
8077}
8078
8079fn atomic_key(atomic: &BTreeMap<String, f64>, key: &str) -> PyResult<f64> {
8081 atomic
8082 .get(key)
8083 .copied()
8084 .ok_or_else(|| PyValueError::new_err(format!("atomic constants missing `{key}`")))
8085}
8086
8087#[pyfunction]
8096#[pyo3(signature = (atomic, k_conv=None, l_conv=None))]
8097fn spectroscopy_xray_lines(
8098 atomic: BTreeMap<String, f64>,
8099 k_conv: Option<f64>,
8100 l_conv: Option<f64>,
8101) -> PyResult<Vec<(f64, f64)>> {
8102 let data = nucleide_spectroscopy::AtomicData {
8103 k_shell_fluor: atomic_key(&atomic, "k_shell_fluor")?,
8104 l_shell_fluor: atomic_key(&atomic, "l_shell_fluor")?,
8105 prob: atomic_key(&atomic, "prob")?,
8106 kb_to_ka: atomic_key(&atomic, "kb_to_ka")?,
8107 ka2_to_ka1: atomic_key(&atomic, "ka2_to_ka1")?,
8108 ka1_en_kev: atomic_key(&atomic, "ka1_en_kev")?,
8109 ka2_en_kev: atomic_key(&atomic, "ka2_en_kev")?,
8110 kb_en_kev: atomic_key(&atomic, "kb_en_kev")?,
8111 l_en_kev: atomic_key(&atomic, "l_en_kev")?,
8112 };
8113 let present = |v: Option<f64>| v.filter(|x| !x.is_nan());
8115 Ok(
8116 nucleide_spectroscopy::xray_lines(&data, present(k_conv), present(l_conv))
8117 .iter()
8118 .map(|l| (l.energy_kev, l.intensity))
8119 .collect(),
8120 )
8121}
8122
8123#[pyfunction]
8138#[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))]
8139#[allow(clippy::too_many_arguments)]
8140fn spectroscopy_sdef_decay_source(
8141 lines: Vec<(f64, f64)>,
8142 x: f64,
8143 y: f64,
8144 z: f64,
8145 u: f64,
8146 v: f64,
8147 w: f64,
8148 weight: f64,
8149 particle: &str,
8150 version: u32,
8151) -> PyResult<(Vec<(f64, f64)>, String)> {
8152 let particle = particle
8153 .parse::<nucleide_nuclei::particles::ParticleId>()
8154 .map_err(|e| PyValueError::new_err(format!("spectroscopy: particle {e}")))?;
8155 let source = nucleide_spectroscopy::PointSource {
8156 x,
8157 y,
8158 z,
8159 u,
8160 v,
8161 w,
8162 weight,
8163 particle,
8164 };
8165 nucleide_spectroscopy::sdef_card(&lines, &source, version)
8166 .map_err(|e| PyValueError::new_err(e.to_string()))
8167}
8168
8169fn spectrum_to_py(
8171 py: Python<'_>,
8172 spec: &nucleide_spectroscopy::GammaSpectrum,
8173) -> PyResult<Py<PyAny>> {
8174 use pyo3::types::PyDict;
8175 let d = PyDict::new(py);
8176 let s = &spec.spectrum;
8177 d.set_item("spec_name", &s.spec_name)?;
8178 d.set_item("start_chan_num", s.start_chan_num)?;
8179 d.set_item("num_channels", s.num_channels)?;
8180 d.set_item("channels", &s.channels)?;
8181 d.set_item("counts", &s.counts)?;
8182 d.set_item("ebin", &s.ebin)?;
8183 d.set_item("real_time", spec.real_time)?;
8184 d.set_item("live_time", spec.live_time)?;
8185 d.set_item("dead_time", spec.dead_time())?;
8186 d.set_item("det_id", &spec.det_id)?;
8187 d.set_item("det_descp", &spec.det_descp)?;
8188 d.set_item("start_date", &spec.start_date)?;
8189 d.set_item("start_time", &spec.start_time)?;
8190 d.set_item("calib_e_fit", &spec.calib_e_fit)?;
8191 d.set_item("calib_fwhm_fit", &spec.calib_fwhm_fit)?;
8192 d.set_item("file_name", &spec.file_name)?;
8193 Ok(d.into_any().unbind())
8194}
8195
8196#[pyfunction]
8198fn spectroscopy_parse_dollar_spe(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
8199 let spec = nucleide_spectroscopy::parse_dollar_spe(text, "")
8200 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8201 spectrum_to_py(py, &spec)
8202}
8203
8204#[pyfunction]
8206fn spectroscopy_parse_spe(py: Python<'_>, text: &str) -> PyResult<Py<PyAny>> {
8207 let spec = nucleide_spectroscopy::parse_plain_spe(text, "")
8208 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8209 spectrum_to_py(py, &spec)
8210}
8211
8212#[pyfunction]
8214fn spectroscopy_read_dollar_spe(py: Python<'_>, path: &str) -> PyResult<Py<PyAny>> {
8215 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
8216 let spec = nucleide_spectroscopy::parse_dollar_spe(&text, path)
8217 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8218 spectrum_to_py(py, &spec)
8219}
8220
8221#[pyfunction]
8223fn spectroscopy_read_spe(py: Python<'_>, path: &str) -> PyResult<Py<PyAny>> {
8224 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
8225 let spec = nucleide_spectroscopy::parse_plain_spe(&text, path)
8226 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8227 spectrum_to_py(py, &spec)
8228}
8229
8230#[pyfunction]
8234fn spectroscopy_parse_lines_tsv(text: &str) -> PyResult<Vec<(f64, f64)>> {
8235 nucleide_spectroscopy::parse_lines_tsv(text).map_err(|e| PyValueError::new_err(e.to_string()))
8236}
8237
8238#[pyfunction]
8241fn spectroscopy_read_decay_lines(path: &str) -> PyResult<Vec<(f64, f64)>> {
8242 let text = std::fs::read_to_string(path).map_err(|e| PyValueError::new_err(e.to_string()))?;
8243 nucleide_spectroscopy::parse_lines_tsv(&text).map_err(|e| PyValueError::new_err(e.to_string()))
8244}
8245
8246fn uq_sample_err(e: nucleide_linalg::sample::SampleError) -> PyErr {
8251 PyValueError::new_err(e.to_string())
8252}
8253
8254fn uq_decay_err(e: nucleide_linalg::decay::DecayError) -> PyErr {
8255 PyValueError::new_err(e.to_string())
8256}
8257
8258#[pyfunction]
8266fn uq_sample_mvn(
8267 py: Python<'_>,
8268 mean: Vec<f64>,
8269 cov: Vec<Vec<f64>>,
8270 n: usize,
8271 seed: u64,
8272) -> PyResult<Py<PyAny>> {
8273 use pyo3::types::PyDict;
8274 let set = nucleide_linalg::sample::sample_mvn(&mean, &cov, n, seed).map_err(uq_sample_err)?;
8275 let d = PyDict::new(py);
8276 d.set_item("samples", set.samples)?;
8277 d.set_item("method", set.method.name())?;
8278 match &set.method {
8279 nucleide_linalg::sample::FactorMethod::Cholesky => {
8280 d.set_item("min_eigen", py.None())?;
8281 d.set_item("max_eigen", py.None())?;
8282 }
8283 nucleide_linalg::sample::FactorMethod::EigenClip {
8284 min_eigen,
8285 max_eigen,
8286 } => {
8287 d.set_item("min_eigen", *min_eigen)?;
8288 d.set_item("max_eigen", *max_eigen)?;
8289 }
8290 }
8291 Ok(d.into_any().unbind())
8292}
8293
8294#[pyfunction]
8296fn uq_sample_mean(samples: Vec<Vec<f64>>) -> PyResult<Vec<f64>> {
8297 nucleide_linalg::sample::sample_mean(&samples).map_err(uq_sample_err)
8298}
8299
8300#[pyfunction]
8302fn uq_sample_cov(samples: Vec<Vec<f64>>) -> PyResult<Vec<Vec<f64>>> {
8303 nucleide_linalg::sample::sample_cov(&samples).map_err(uq_sample_err)
8304}
8305
8306#[pyfunction]
8312fn uq_check_convergence(
8313 py: Python<'_>,
8314 mean: Vec<f64>,
8315 cov: Vec<Vec<f64>>,
8316 samples: Vec<Vec<f64>>,
8317 mean_tol: f64,
8318 cov_tol: f64,
8319) -> PyResult<Py<PyAny>> {
8320 use pyo3::types::PyDict;
8321 let rep = nucleide_linalg::sample::check_convergence(&mean, &cov, &samples, mean_tol, cov_tol)
8322 .map_err(uq_sample_err)?;
8323 let d = PyDict::new(py);
8324 d.set_item("mean_err_max", rep.mean_err_max)?;
8325 d.set_item("cov_err_fro", rep.cov_err_fro)?;
8326 d.set_item("mean_tol", rep.mean_tol)?;
8327 d.set_item("cov_tol", rep.cov_tol)?;
8328 d.set_item("passed", rep.passed)?;
8329 Ok(d.into_any().unbind())
8330}
8331
8332#[pyfunction]
8335fn uq_perturb_branches(base: Vec<f64>, rel: Vec<f64>) -> PyResult<Vec<f64>> {
8336 nucleide_linalg::decay::perturb_branches(&base, &rel).map_err(uq_decay_err)
8337}
8338
8339#[pyfunction]
8343fn uq_perturb_energies(base: Vec<f64>, delta: Vec<f64>, convention: &str) -> PyResult<Vec<f64>> {
8344 let conv = nucleide_linalg::sample::PerturbConvention::parse(convention)
8345 .map_err(PyValueError::new_err)?;
8346 nucleide_linalg::decay::perturb_energies(&base, &delta, conv).map_err(uq_decay_err)
8347}
8348
8349#[pyfunction]
8356fn uq_sample_lognormal(
8357 py: Python<'_>,
8358 mean_log: Vec<f64>,
8359 cov: Vec<Vec<f64>>,
8360 n: usize,
8361 seed: u64,
8362) -> PyResult<Py<PyAny>> {
8363 use pyo3::types::PyDict;
8364 let set = nucleide_linalg::sample::sample_lognormal(&mean_log, &cov, n, seed)
8365 .map_err(uq_sample_err)?;
8366 let d = PyDict::new(py);
8367 d.set_item("samples", set.samples)?;
8368 d.set_item("method", set.method.name())?;
8369 match &set.method {
8370 nucleide_linalg::sample::FactorMethod::Cholesky => {
8371 d.set_item("min_eigen", py.None())?;
8372 d.set_item("max_eigen", py.None())?;
8373 }
8374 nucleide_linalg::sample::FactorMethod::EigenClip {
8375 min_eigen,
8376 max_eigen,
8377 } => {
8378 d.set_item("min_eigen", *min_eigen)?;
8379 d.set_item("max_eigen", *max_eigen)?;
8380 }
8381 }
8382 Ok(d.into_any().unbind())
8383}
8384
8385#[pyfunction]
8392fn uq_sample_lhs(
8393 py: Python<'_>,
8394 mean: Vec<f64>,
8395 cov: Vec<Vec<f64>>,
8396 n: usize,
8397 seed: u64,
8398) -> PyResult<Py<PyAny>> {
8399 use pyo3::types::PyDict;
8400 let set = nucleide_linalg::sample::sample_lhs(&mean, &cov, n, seed).map_err(uq_sample_err)?;
8401 let d = PyDict::new(py);
8402 d.set_item("samples", set.samples)?;
8403 d.set_item("method", set.method.name())?;
8404 match &set.method {
8405 nucleide_linalg::sample::FactorMethod::Cholesky => {
8406 d.set_item("min_eigen", py.None())?;
8407 d.set_item("max_eigen", py.None())?;
8408 }
8409 nucleide_linalg::sample::FactorMethod::EigenClip {
8410 min_eigen,
8411 max_eigen,
8412 } => {
8413 d.set_item("min_eigen", *min_eigen)?;
8414 d.set_item("max_eigen", *max_eigen)?;
8415 }
8416 }
8417 Ok(d.into_any().unbind())
8418}
8419
8420#[pyfunction]
8423fn uq_lognormal_mean(mean_log: Vec<f64>, cov: Vec<Vec<f64>>) -> PyResult<Vec<f64>> {
8424 nucleide_linalg::sample::lognormal_mean(&mean_log, &cov).map_err(uq_sample_err)
8425}
8426
8427#[pyfunction]
8430fn uq_lognormal_cov(mean_log: Vec<f64>, cov: Vec<Vec<f64>>) -> PyResult<Vec<Vec<f64>>> {
8431 nucleide_linalg::sample::lognormal_cov(&mean_log, &cov).map_err(uq_sample_err)
8432}
8433
8434#[pyfunction]
8436fn uq_passthrough(delta: Vec<f64>) -> PyResult<Vec<f64>> {
8437 nucleide_linalg::decay::passthrough(&delta).map_err(uq_decay_err)
8438}
8439
8440#[pyfunction]
8443fn uq_perturb_fission_yields(base: Vec<f64>, rel: Vec<f64>) -> PyResult<Vec<f64>> {
8444 nucleide_linalg::decay::perturb_fission_yields(&base, &rel).map_err(uq_decay_err)
8445}
8446
8447fn parse_projectile(flag: &str) -> PyResult<nucleide_nuclei::rxname::Projectile> {
8452 flag.parse::<nucleide_nuclei::rxname::Projectile>()
8453 .map_err(|e| PyValueError::new_err(e.to_string()))
8454}
8455
8456fn resolve_rx_id(spec: &Bound<'_, PyAny>) -> PyResult<u32> {
8457 if let Ok(id) = spec.extract::<u32>() {
8458 return Ok(id);
8459 }
8460 if let Ok(s) = spec.extract::<&str>() {
8461 return nucleide_nuclei::rxname::name_to_id(s)
8462 .map_err(|e| PyValueError::new_err(e.to_string()));
8463 }
8464 Err(PyTypeError::new_err(
8465 "expected reaction id (int) or name (str)",
8466 ))
8467}
8468
8469#[pyfunction]
8471fn rxname_label(id: u32) -> &'static str {
8472 nucleide_nuclei::rxname::label(id)
8473}
8474
8475#[pyfunction]
8477fn rxname_doc(id: u32) -> &'static str {
8478 nucleide_nuclei::rxname::doc(id)
8479}
8480
8481#[pyfunction]
8483fn rxname_reaction(py: Python<'_>, id: u32) -> PyResult<Option<Py<PyAny>>> {
8484 use pyo3::types::PyDict;
8485 Ok(nucleide_nuclei::rxname::reaction(id).map(|r| {
8486 let d = PyDict::new(py);
8487 d.set_item("id", r.id).ok();
8488 d.set_item("name", r.name).ok();
8489 d.set_item("mt", r.mt).ok();
8490 d.set_item("label", r.label).ok();
8491 d.set_item("doc", r.doc).ok();
8492 d.into_any().unbind()
8493 }))
8494}
8495
8496#[pyfunction]
8498#[pyo3(signature = (from_nucid, to_nucid, projectile="n"))]
8499fn rxname_id_from_nucdelta(from_nucid: u32, to_nucid: u32, projectile: &str) -> PyResult<u32> {
8500 let p = parse_projectile(projectile)?;
8501 nucleide_nuclei::rxname::id_from_nucdelta(from_nucid, to_nucid, p)
8502 .map_err(|e| PyValueError::new_err(e.to_string()))
8503}
8504
8505#[pyfunction]
8507#[pyo3(signature = (parent, rx, projectile="n"))]
8508fn rxname_child(parent: &str, rx: &Bound<'_, PyAny>, projectile: &str) -> PyResult<String> {
8509 let p = parse_projectile(projectile)?;
8510 let rx = resolve_rx_id(rx)?;
8511 let parent_id = NuclideId::from_name(parent)
8512 .map_err(|e| PyValueError::new_err(format!("`{parent}`: {e}")))?;
8513 nucleide_nuclei::rxname::child(parent_id, rx, p)
8514 .map(|id| id.to_name())
8515 .map_err(|e| PyValueError::new_err(e.to_string()))
8516}
8517
8518#[pyfunction]
8520#[pyo3(signature = (child, rx, projectile="n"))]
8521fn rxname_parent(child: &str, rx: &Bound<'_, PyAny>, projectile: &str) -> PyResult<String> {
8522 let p = parse_projectile(projectile)?;
8523 let rx = resolve_rx_id(rx)?;
8524 let child_id = NuclideId::from_name(child)
8525 .map_err(|e| PyValueError::new_err(format!("`{child}`: {e}")))?;
8526 nucleide_nuclei::rxname::parent(child_id, rx, p)
8527 .map(|id| id.to_name())
8528 .map_err(|e| PyValueError::new_err(e.to_string()))
8529}
8530
8531#[pyfunction]
8533fn particle_is_valid(spec: &str) -> bool {
8534 nucleide_nuclei::particles::is_valid(spec)
8535}
8536
8537#[pyfunction]
8539fn particle_is_valid_pdc(n: i32) -> bool {
8540 nucleide_nuclei::particles::is_valid_pdc(n)
8541}
8542
8543#[pyfunction]
8545fn particle_is_hydrogen(spec: &str) -> bool {
8546 nucleide_nuclei::particles::is_hydrogen(spec)
8547}
8548
8549#[pyfunction]
8551fn particle_is_heavy_ion(spec: &str) -> bool {
8552 nucleide_nuclei::particles::is_heavy_ion(spec)
8553}
8554
8555#[pyfunction]
8557#[pyo3(signature = (name, source="EPA"))]
8558fn dose_f1(name: &str, source: &str) -> PyResult<Option<f64>> {
8559 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
8560 let s = parse_dose_source(source)?;
8561 Ok(nucleide_nuclei::data::dose_f1_by_name(name, s))
8562}
8563
8564#[pyfunction]
8566#[pyo3(signature = (name, source="EPA"))]
8567fn dose_lung_model(name: &str, source: &str) -> PyResult<Option<char>> {
8568 NuclideId::from_name(name).map_err(wrap_nucid_err)?;
8569 let s = parse_dose_source(source)?;
8570 Ok(nucleide_nuclei::data::dose_lung_model_by_name(name, s))
8571}
8572
8573fn bare_element_z(name: &str) -> Option<u32> {
8575 let t = name.trim();
8576 if t.is_empty() {
8577 return None;
8578 }
8579 let mut chars = t.chars();
8580 let first = chars.next()?.to_uppercase().next()?;
8581 let rest: String = chars.collect::<String>().to_lowercase();
8582 let canon = format!("{first}{rest}");
8583 nucleide_nuclei::element_z(&canon)
8584}
8585
8586fn mat_from_comp_elements(comp: BTreeMap<String, f64>) -> PyResult<nucleide_material::Material> {
8587 let mut mat = nucleide_material::Material::new();
8588 for (name, grams) in &comp {
8589 let id = match NuclideId::from_name(name) {
8590 Ok(id) => id,
8591 Err(_) => match bare_element_z(name) {
8592 Some(z) => NuclideId::from_nucid(z * 10_000_000),
8593 None => {
8594 return Err(PyValueError::new_err(format!(
8595 "`{name}`: unknown nuclide or element"
8596 )));
8597 }
8598 },
8599 };
8600 mat.add_nuclide(id, *grams);
8601 }
8602 Ok(mat)
8603}
8604
8605fn mat_to_comp_elements(mat: &nucleide_material::Material) -> BTreeMap<String, f64> {
8606 let mut out = BTreeMap::new();
8607 for (&id, &grams) in &mat.comp {
8608 let key = if id.a() == 0 && id.state() == 0 {
8609 nucleide_nuclei::element_symbol(id.z())
8610 .unwrap_or("X")
8611 .to_string()
8612 } else {
8613 id.to_name()
8614 };
8615 *out.entry(key).or_insert(0.0) += grams;
8616 }
8617 out
8618}
8619
8620#[pyfunction]
8624fn mix_by_mass(parts: Vec<(BTreeMap<String, f64>, f64)>) -> PyResult<BTreeMap<String, f64>> {
8625 let mats: Vec<nucleide_material::Material> = parts
8626 .iter()
8627 .map(|(comp, _)| mat_from_comp_elements(comp.clone()))
8628 .collect::<PyResult<_>>()?;
8629 let refs: Vec<(&nucleide_material::Material, f64)> =
8630 mats.iter().zip(parts.iter().map(|(_, w)| *w)).collect();
8631 let out = nucleide_material::Material::mix_by_mass(&refs)
8632 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8633 Ok(mat_to_comp_elements(&out))
8634}
8635
8636#[pyfunction]
8640fn mix_by_volume(parts: Vec<(BTreeMap<String, f64>, f64, f64)>) -> PyResult<BTreeMap<String, f64>> {
8641 let mut mats: Vec<nucleide_material::Material> = Vec::with_capacity(parts.len());
8642 for (comp, _, density) in &parts {
8643 let mut m = mat_from_comp_elements(comp.clone())?;
8644 m.set_density(Some(*density));
8645 mats.push(m);
8646 }
8647 let refs: Vec<(&nucleide_material::Material, f64)> =
8648 mats.iter().zip(parts.iter().map(|(_, v, _)| *v)).collect();
8649 let out = nucleide_material::Material::mix_by_volume(&refs)
8650 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8651 Ok(mat_to_comp_elements(&out))
8652}
8653
8654#[pyfunction]
8656fn specific_activity(comp: BTreeMap<String, f64>) -> PyResult<f64> {
8657 let mat = mat_from_comp_elements(comp)?;
8658 let analytics = nucleide_material::Analytics {
8659 masses: &nucleide_material::Ame2020,
8660 decays: &nucleide_material::ChainDecays,
8661 };
8662 mat.specific_activity(&analytics)
8663 .map_err(|e| PyValueError::new_err(e.to_string()))
8664}
8665
8666#[pyfunction]
8670#[pyo3(signature = (entries, cross_sections=None))]
8671fn materials_doc_to_xml(
8672 entries: Vec<(String, BTreeMap<String, f64>, f64)>,
8673 cross_sections: Option<String>,
8674) -> PyResult<String> {
8675 let mut doc = nucleide_material::MaterialsDoc::new();
8676 if let Some(path) = cross_sections {
8677 doc = doc.cross_sections(path);
8678 }
8679 for (name, comp, density) in entries {
8680 let mut mat = mat_from_comp_elements(comp)?;
8681 mat.set_density(Some(density));
8682 doc = doc.push(name, mat);
8683 }
8684 doc.to_xml()
8685 .map_err(|e| PyValueError::new_err(e.to_string()))
8686}
8687
8688#[pyfunction]
8692fn expand_elements(comp: BTreeMap<String, f64>) -> PyResult<BTreeMap<String, f64>> {
8693 let mut mat = mat_from_comp_elements(comp)?;
8694 mat.expand_elements(
8695 &nucleide_material::Ame2020,
8696 &nucleide_material::NaturalAbundances,
8697 )
8698 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8699 Ok(mat_to_comp_elements(&mat))
8700}
8701
8702#[pyfunction]
8704fn collapse_elements(comp: BTreeMap<String, f64>) -> PyResult<BTreeMap<String, f64>> {
8705 let mat = mat_from_comp_elements(comp)?;
8706 Ok(mat_to_comp_elements(&mat.collapse_elements()))
8707}
8708
8709fn parse_fluka_nuc(spec: &str) -> PyResult<nucleide_fluka_io::material::FlukaNuc> {
8710 use nucleide_fluka_io::material::FlukaNuc;
8711 if let Ok(id) = NuclideId::from_name(spec) {
8712 return Ok(FlukaNuc::Nuclide(id));
8713 }
8714 if let Some(z) = bare_element_z(spec) {
8715 return Ok(FlukaNuc::Element(z));
8716 }
8717 if let Ok(z) = spec.trim().parse::<u32>() {
8718 if nucleide_nuclei::element_symbol(z).is_some() {
8719 return Ok(FlukaNuc::Element(z));
8720 }
8721 }
8722 Err(PyValueError::new_err(format!(
8723 "`{spec}`: unknown nuclide or element"
8724 )))
8725}
8726
8727#[pyfunction]
8729fn fluka_material_str(fid: u32, nuc: &str, density: f64) -> PyResult<String> {
8730 let parsed = parse_fluka_nuc(nuc)?;
8731 nucleide_fluka_io::material::material_str(fid, parsed, density)
8732 .map_err(|e| PyValueError::new_err(e.to_string()))
8733}
8734
8735#[pyfunction]
8739#[pyo3(signature = (fid, compound_name, density, frac_type="mass", components=None))]
8740fn fluka_compound_str(
8741 fid: u32,
8742 compound_name: &str,
8743 density: f64,
8744 frac_type: &str,
8745 components: Option<Vec<(String, f64)>>,
8746) -> PyResult<String> {
8747 use nucleide_fluka_io::material::{Component, FracType};
8748 let frac = match frac_type.trim().to_ascii_lowercase().as_str() {
8749 "mass" => FracType::Mass,
8750 "atom" => FracType::Atom,
8751 other => {
8752 return Err(PyValueError::new_err(format!(
8753 "frac_type must be mass|atom, got `{other}`"
8754 )));
8755 }
8756 };
8757 let pairs = components.unwrap_or_default();
8758 let comps: Vec<Component> = pairs
8759 .iter()
8760 .map(|(nuc, frac)| parse_fluka_nuc(nuc).map(|n| Component::new(n, *frac)))
8761 .collect::<PyResult<_>>()?;
8762 nucleide_fluka_io::material::compound_str(fid, compound_name, density, frac, &comps)
8763 .map_err(|e| PyValueError::new_err(e.to_string()))
8764}
8765
8766#[pyfunction]
8768fn fluka_builtin_set() -> Vec<String> {
8769 let mut out: Vec<String> = nucleide_fluka_io::material::builtin_set()
8770 .into_iter()
8771 .map(str::to_string)
8772 .collect();
8773 out.sort();
8774 out
8775}
8776
8777#[pyfunction]
8779fn alara_validate_deck(text: &str) -> PyResult<()> {
8780 let deck = nucleide_alara_io::AlaraDeck::parse(text)
8781 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8782 deck.validate()
8783 .map_err(|e| PyValueError::new_err(e.to_string()))
8784}
8785
8786#[pyfunction]
8788fn alara_check_block(block: &str, line: usize) -> PyResult<()> {
8789 nucleide_alara_io::AlaraDeck::check_block(block, line)
8790 .map_err(|e| PyValueError::new_err(e.to_string()))
8791}
8792
8793#[pyfunction]
8795fn alara_flux_total(name: &str, text: &str) -> PyResult<f64> {
8796 nucleide_alara_io::FluxSpec::parse(name, text)
8797 .map(|f| f.total())
8798 .map_err(|e| PyValueError::new_err(e.to_string()))
8799}
8800
8801#[pyfunction]
8803fn alara_flux_len(name: &str, text: &str) -> PyResult<usize> {
8804 nucleide_alara_io::FluxSpec::parse(name, text)
8805 .map(|f| f.len())
8806 .map_err(|e| PyValueError::new_err(e.to_string()))
8807}
8808
8809#[pyfunction]
8811fn alara_output_totals(
8812 py: Python<'_>,
8813 text: &str,
8814 run_lbl: &str,
8815) -> PyResult<Vec<BTreeMap<String, Py<PyAny>>>> {
8816 let owned_text = text.to_owned();
8817 let owned_lbl = run_lbl.to_owned();
8818 let frame = py
8819 .detach(move || {
8820 nucleide_alara_io::output::ResponseFrame::parse(&owned_text, &owned_lbl)
8821 .map(|f| f.totals())
8822 })
8823 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8824 Ok(frame
8825 .rows
8826 .iter()
8827 .map(|r| fispact_row_to_map(py, r))
8828 .collect())
8829}
8830
8831#[pyfunction]
8833fn alara_output_total_activity(text: &str, run_lbl: &str) -> PyResult<f64> {
8834 nucleide_alara_io::output::ResponseFrame::parse(text, run_lbl)
8835 .map(|f| f.total_activity())
8836 .map_err(|e| PyValueError::new_err(e.to_string()))
8837}
8838
8839#[pyfunction]
8841fn alara_photon_total_strength(text: &str) -> PyResult<f64> {
8842 nucleide_alara_io::PhotonSource::from_str(text)
8843 .map(|p| p.total_strength())
8844 .map_err(|e| PyValueError::new_err(e.to_string()))
8845}
8846
8847#[pyfunction]
8849#[pyo3(signature = (deck_text, top=None))]
8850fn alara_schedule_total_time(deck_text: &str, top: Option<&str>) -> PyResult<f64> {
8851 let owned = deck_text.to_owned();
8852 let owned_top = top.map(str::to_owned);
8853 let steps =
8854 expand_deck_schedules(&owned, owned_top.as_deref()).map_err(PyValueError::new_err)?;
8855 Ok(nucleide_alara_io::schedule::total_time(&steps))
8856}
8857
8858#[pyfunction]
8867fn alara_clearance_eu_table() -> BTreeMap<String, f64> {
8868 nucleide_alara_io::ClearanceTable::eu_annex_vii()
8869 .iter()
8870 .map(|(nuc, limit)| (nucleide_nuclei::dialects::serpent(nuc), limit))
8871 .collect()
8872}
8873
8874#[pyfunction]
8888fn alara_clearance_es_table(landfill: &str, material: &str) -> PyResult<BTreeMap<String, f64>> {
8889 use nucleide_alara_io::EsNormMaterial;
8890 let material = match material {
8891 "rocks" => EsNormMaterial::Rocks,
8892 "ashes" => EsNormMaterial::Ashes,
8893 "sands" => EsNormMaterial::Sands,
8894 "slags" => EsNormMaterial::Slags,
8895 "oil_gas" => EsNormMaterial::OilGas,
8896 other => {
8897 return Err(PyValueError::new_err(format!(
8898 "bad material `{other}` (expected one of: rocks, ashes, sands, slags, oil_gas)"
8899 )));
8900 }
8901 };
8902 let table = match landfill {
8903 "inert" => nucleide_alara_io::ClearanceTable::es_conditional_inert(material),
8904 "non_hazardous" => {
8905 nucleide_alara_io::ClearanceTable::es_conditional_non_hazardous(material)
8906 }
8907 "hazardous" => nucleide_alara_io::ClearanceTable::es_conditional_hazardous(material),
8908 other => {
8909 return Err(PyValueError::new_err(format!(
8910 "bad landfill `{other}` (expected one of: inert, non_hazardous, hazardous)"
8911 )));
8912 }
8913 };
8914 Ok(table
8915 .iter()
8916 .map(|(nuc, limit)| (nucleide_nuclei::dialects::serpent(nuc), limit))
8917 .collect())
8918}
8919
8920fn clearance_key(key: &str) -> PyResult<NuclideId> {
8922 nucleide_nuclei::dialects::normalize_nuclide_name(key)
8923 .map_err(|e| PyValueError::new_err(format!("bad nuclide name `{key}`: {e}")))
8924}
8925
8926fn clearance_pairs(map: &BTreeMap<String, f64>, what: &str) -> PyResult<Vec<(NuclideId, f64)>> {
8928 map.iter()
8929 .map(|(name, value)| Ok((clearance_key(name)?, *value)))
8930 .collect::<PyResult<_>>()
8931 .map_err(|e| PyValueError::new_err(format!("{what}: {e}")))
8932}
8933
8934fn clearance_table_from(
8936 map: &BTreeMap<String, f64>,
8937) -> PyResult<nucleide_alara_io::ClearanceTable> {
8938 let mut table = nucleide_alara_io::ClearanceTable::new();
8939 for (nuc, limit) in clearance_pairs(map, "limits")? {
8940 table
8941 .insert(nuc, limit)
8942 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8943 }
8944 Ok(table)
8945}
8946
8947#[pyfunction]
8955#[pyo3(signature = (inventory, limits=None))]
8956fn alara_clearance_index(
8957 inventory: BTreeMap<String, f64>,
8958 limits: Option<BTreeMap<String, f64>>,
8959) -> PyResult<f64> {
8960 let table = match limits {
8961 Some(map) => clearance_table_from(&map)?,
8962 None => nucleide_alara_io::ClearanceTable::eu_annex_vii(),
8963 };
8964 let pairs = clearance_pairs(&inventory, "inventory")?;
8965 nucleide_alara_io::clearance_index(&pairs, &table)
8966 .map_err(|e| PyValueError::new_err(e.to_string()))
8967}
8968
8969#[pyfunction]
8978#[pyo3(signature = (inventory, limits=None))]
8979fn alara_sum_of_fractions(
8980 py: Python<'_>,
8981 inventory: BTreeMap<String, f64>,
8982 limits: Option<BTreeMap<String, f64>>,
8983) -> PyResult<BTreeMap<String, Py<PyAny>>> {
8984 let table = match limits {
8985 Some(map) => clearance_table_from(&map)?,
8986 None => nucleide_alara_io::ClearanceTable::eu_annex_vii(),
8987 };
8988 let pairs = clearance_pairs(&inventory, "inventory")?;
8989 let out = nucleide_alara_io::sum_of_fractions(&pairs, &table)
8990 .map_err(|e| PyValueError::new_err(e.to_string()))?;
8991 let mut d = BTreeMap::new();
8992 d.insert(
8993 "sum".to_string(),
8994 out.sum.into_pyobject(py).unwrap().unbind().into_any(),
8995 );
8996 d.insert(
8997 "class".to_string(),
8998 out.class
8999 .to_string()
9000 .into_pyobject(py)
9001 .unwrap()
9002 .unbind()
9003 .into_any(),
9004 );
9005 d.insert(
9006 "max_fraction".to_string(),
9007 out.max_fraction
9008 .into_pyobject(py)
9009 .unwrap()
9010 .unbind()
9011 .into_any(),
9012 );
9013 d.insert(
9014 "max_nuclide".to_string(),
9015 out.max_nuclide
9016 .map(nucleide_nuclei::dialects::serpent)
9017 .into_pyobject(py)
9018 .unwrap()
9019 .unbind()
9020 .into_any(),
9021 );
9022 Ok(d)
9023}
9024
9025#[pyfunction]
9027fn origen_tape6_find(py: Python<'_>, text: &str, nuclide: &str) -> PyResult<Option<Py<PyAny>>> {
9028 use pyo3::types::PyDict;
9029 let owned = text.to_owned();
9030 let query = nuclide.to_owned();
9031 let found = py
9032 .detach(move || nucleide_origen_io::Tape6::parse(&owned).map(|t| t.find(&query).cloned()))
9033 .map_err(|e| PyValueError::new_err(e.to_string()))?;
9034 Ok(found.map(|r| {
9035 let d = PyDict::new(py);
9036 d.set_item("nuclide", &r.nuclide).ok();
9037 d.set_item("grams", r.grams).ok();
9038 d.set_item("activity_bq", r.activity_bq).ok();
9039 d.into_any().unbind()
9040 }))
9041}
9042
9043#[pyfunction]
9045fn origen_tape6_total_activity(text: &str) -> PyResult<f64> {
9046 nucleide_origen_io::Tape6::parse(text)
9047 .map(|t| t.total_activity())
9048 .map_err(|e| PyValueError::new_err(e.to_string()))
9049}
9050
9051#[pyfunction]
9053fn origen_tape9_find(py: Python<'_>, text: &str, nuclide: &str) -> PyResult<Option<Py<PyAny>>> {
9054 use pyo3::types::PyDict;
9055 let owned = text.to_owned();
9056 let query = nuclide.to_owned();
9057 let found = py
9058 .detach(move || {
9059 nucleide_origen_io::Tape9Entry::parse(&owned)
9060 .map(|entries| nucleide_origen_io::Tape9Entry::find(&entries, &query).cloned())
9061 })
9062 .map_err(|e| PyValueError::new_err(e.to_string()))?;
9063 Ok(found.map(|e| {
9064 let d = PyDict::new(py);
9065 d.set_item("nuclide", &e.nuclide).ok();
9066 d.set_item("decay_const", e.decay_const).ok();
9067 d.into_any().unbind()
9068 }))
9069}
9070
9071#[pyfunction]
9073#[pyo3(signature = (text, kind="rtflux"))]
9074fn cccc_rtflux_npoints(text: &str, kind: &str) -> PyResult<usize> {
9075 let flux_kind = parse_flux_kind(kind)?;
9076 nucleide_cccc_io::FluxFile::parse(flux_kind, text)
9077 .map(|f| f.npoints())
9078 .map_err(|e| PyValueError::new_err(e.to_string()))
9079}
9080
9081#[pyfunction]
9083#[pyo3(signature = (text, kind="rtflux", index=0))]
9084fn cccc_rtflux_point(text: &str, kind: &str, index: usize) -> PyResult<Option<Vec<f64>>> {
9085 let flux_kind = parse_flux_kind(kind)?;
9086 let flux = nucleide_cccc_io::FluxFile::parse(flux_kind, text)
9087 .map_err(|e| PyValueError::new_err(e.to_string()))?;
9088 Ok(flux.point(index).map(<[f64]>::to_vec))
9089}
9090
9091#[pyfunction]
9093#[pyo3(signature = (text, kind="rtflux"))]
9094fn cccc_rtflux_total(text: &str, kind: &str) -> PyResult<f64> {
9095 let flux_kind = parse_flux_kind(kind)?;
9096 nucleide_cccc_io::FluxFile::parse(flux_kind, text)
9097 .map(|f| f.total())
9098 .map_err(|e| PyValueError::new_err(e.to_string()))
9099}
9100
9101fn parse_flux_kind(kind: &str) -> PyResult<nucleide_cccc_io::rtflux::FluxKind> {
9102 match kind.to_ascii_lowercase().as_str() {
9103 "rtflux" => Ok(nucleide_cccc_io::rtflux::FluxKind::Rtflux),
9104 "atflux" => Ok(nucleide_cccc_io::rtflux::FluxKind::Atflux),
9105 "rzflux" => Ok(nucleide_cccc_io::rtflux::FluxKind::Rzflux),
9106 other => Err(PyValueError::new_err(format!(
9107 "kind must be rtflux|atflux|rzflux, got `{other}`"
9108 ))),
9109 }
9110}
9111
9112#[pyfunction]
9114fn cccc_isotxs_find(py: Python<'_>, text: &str, label: &str) -> PyResult<Option<Py<PyAny>>> {
9115 use pyo3::types::PyDict;
9116 let owned = text.to_owned();
9117 let query = label.to_owned();
9118 let found = py
9119 .detach(move || {
9120 nucleide_cccc_io::IsotxsLib::parse(&owned).map(|lib| lib.find(&query).cloned())
9121 })
9122 .map_err(|e| PyValueError::new_err(e.to_string()))?;
9123 Ok(found.map(|n| {
9124 let d = PyDict::new(py);
9125 d.set_item("label", &n.label).ok();
9126 d.set_item("zaid", &n.zaid).ok();
9127 d.set_item("groups", n.groups).ok();
9128 d.set_item("total_xs", n.total_xs.clone()).ok();
9129 d.into_any().unbind()
9130 }))
9131}
9132
9133#[pyfunction]
9135fn cccc_isotxs_len(text: &str) -> PyResult<usize> {
9136 nucleide_cccc_io::IsotxsLib::parse(text)
9137 .map(|lib| lib.len())
9138 .map_err(|e| PyValueError::new_err(e.to_string()))
9139}
9140
9141#[pyfunction]
9143fn fispact_is_output(path: &str) -> bool {
9144 nucleide_fispact_io::is_fispact_output(path)
9145}
9146
9147#[pyfunction]
9149fn enrichment_prod_per_feed(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
9150 nucleide_enrichment::prod_per_feed(x_feed, x_prod, x_tail)
9151}
9152
9153#[pyfunction]
9155fn enrichment_tail_per_feed(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
9156 nucleide_enrichment::tail_per_feed(x_feed, x_prod, x_tail)
9157}
9158
9159#[pyfunction]
9161fn enrichment_tail_per_prod(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
9162 nucleide_enrichment::tail_per_prod(x_feed, x_prod, x_tail)
9163}
9164
9165#[pyfunction]
9167fn enrichment_feed_per_prod(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
9168 nucleide_enrichment::feed_per_prod(x_feed, x_prod, x_tail)
9169}
9170
9171#[pyfunction]
9173fn enrichment_feed_per_tail(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
9174 nucleide_enrichment::feed_per_tail(x_feed, x_prod, x_tail)
9175}
9176
9177#[pyfunction]
9179fn enrichment_prod_per_tail(x_feed: f64, x_prod: f64, x_tail: f64) -> f64 {
9180 nucleide_enrichment::prod_per_tail(x_feed, x_prod, x_tail)
9181}
9182
9183#[pyfunction]
9185#[allow(non_snake_case)]
9186fn enrichment_alphastar_i(alpha: f64, Mstar: f64, M_i: f64) -> f64 {
9187 nucleide_enrichment::alphastar_i(alpha, Mstar, M_i)
9188}
9189
9190#[pyfunction]
9197fn kinetics_from_ifp(
9198 py: Python<'_>,
9199 betas: Vec<f64>,
9200 lambda_gen: f64,
9201 lambdas: Vec<f64>,
9202) -> PyResult<Py<PyAny>> {
9203 use pyo3::types::PyDict;
9204 let params = nucleide_kinetics::KineticParams::from_ifp(betas, lambda_gen, lambdas)
9205 .map_err(|e| PyValueError::new_err(e.to_string()))?;
9206 let d = PyDict::new(py);
9207 d.set_item("betas", params.betas()).ok();
9208 d.set_item("lambdas", params.lambdas()).ok();
9209 d.set_item("lambda_gen", params.lambda_gen()).ok();
9210 d.set_item("beta_total", params.beta_total()).ok();
9211 d.set_item("groups", params.groups()).ok();
9212 Ok(d.into_any().unbind())
9213}
9214
9215#[pyfunction]
9218#[pyo3(signature = (tally, selection="total", tolerance=0.5, null_value=0.0))]
9219fn magic_with(
9220 tally: &PyMeshTally,
9221 selection: &str,
9222 tolerance: f64,
9223 null_value: f64,
9224) -> PyResult<PyMagicOutput> {
9225 let sel = match selection.trim().to_ascii_lowercase().as_str() {
9226 "total" => nucleide_vr_tools::magic::MagicSelection::Total,
9227 "per_group" | "pergroup" | "per-group" => {
9228 nucleide_vr_tools::magic::MagicSelection::PerGroup
9229 }
9230 other => {
9231 return Err(PyValueError::new_err(format!(
9232 "selection must be total|per_group, got `{other}`"
9233 )));
9234 }
9235 };
9236 let params = nucleide_vr_tools::magic::MagicParams {
9237 tolerance,
9238 null_value,
9239 };
9240 nucleide_vr_tools::magic::magic_with(&tally.inner, sel, params)
9241 .map(|inner| PyMagicOutput { inner })
9242 .map_err(|e| PyValueError::new_err(e.to_string()))
9243}
9244
9245#[pyfunction]
9249#[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))]
9250#[allow(clippy::too_many_arguments)]
9251fn emit_openmc_weight_windows(
9252 py: Python<'_>,
9253 tally: &PyMeshTally,
9254 output: &PyMagicOutput,
9255 mesh_id: u32,
9256 window_id: u32,
9257 upper_bound_ratio: f64,
9258 survival_ratio: f64,
9259 max_split: u32,
9260 weight_cutoff: f64,
9261) -> PyResult<Py<PyAny>> {
9262 use pyo3::types::PyDict;
9263 let options = nucleide_vr_tools::windows::OpenMcOptions {
9264 mesh_id,
9265 window_id,
9266 upper_bound_ratio,
9267 survival_ratio,
9268 max_split,
9269 weight_cutoff,
9270 };
9271 let out = nucleide_vr_tools::windows::emit_openmc_weight_windows(
9272 &output.inner,
9273 &tally.inner,
9274 &options,
9275 )
9276 .map_err(|e| PyValueError::new_err(e.to_string()))?;
9277 let d = PyDict::new(py);
9278 d.set_item("xml", out.xml)?;
9279 d.set_item("notes", out.notes)?;
9280 Ok(d.into_any().unbind())
9281}
9282
9283#[pyfunction]
9287#[pyo3(signature = (tally, output, name="ww1", file="wwindows.wwd"))]
9288fn emit_serpent_wwin(
9289 py: Python<'_>,
9290 tally: &PyMeshTally,
9291 output: &PyMagicOutput,
9292 name: &str,
9293 file: &str,
9294) -> PyResult<Py<PyAny>> {
9295 use pyo3::types::PyDict;
9296 let out =
9297 nucleide_vr_tools::windows::emit_serpent_wwin(&output.inner, &tally.inner, name, file)
9298 .map_err(|e| PyValueError::new_err(e.to_string()))?;
9299 let d = PyDict::new(py);
9300 d.set_item("text", out.text)?;
9301 d.set_item("card", out.card)?;
9302 d.set_item("notes", out.notes)?;
9303 Ok(d.into_any().unbind())
9304}
9305
9306#[pyfunction]
9308fn mcpl_statsum_validate(comment: &str) -> PyResult<String> {
9309 nucleide_mcpl_io::statsum_validate(comment)
9310 .map(str::to_string)
9311 .map_err(PyValueError::new_err)
9312}
9313
9314#[pyfunction]
9316fn mcpl_statsum_comment(key: &str, value: f64) -> PyResult<String> {
9317 nucleide_mcpl_io::statsum_comment(key, value).map_err(|e| PyValueError::new_err(e.to_string()))
9318}
9319
9320#[pymodule]
9322fn _internal(m: &Bound<'_, PyModule>) -> PyResult<()> {
9323 m.add_function(wrap_pyfunction!(version, m)?)?;
9324 m.add_function(wrap_pyfunction!(from_zaid, m)?)?;
9325 m.add_function(wrap_pyfunction!(atomic_mass, m)?)?;
9326 m.add_function(wrap_pyfunction!(natural_abundance, m)?)?;
9327 m.add_function(wrap_pyfunction!(rxname_id, m)?)?;
9328 m.add_function(wrap_pyfunction!(rxname_name, m)?)?;
9329 m.add_function(wrap_pyfunction!(rxname_mt, m)?)?;
9330 m.add_function(wrap_pyfunction!(rxname_label, m)?)?;
9331 m.add_function(wrap_pyfunction!(rxname_doc, m)?)?;
9332 m.add_function(wrap_pyfunction!(rxname_reaction, m)?)?;
9333 m.add_function(wrap_pyfunction!(rxname_id_from_nucdelta, m)?)?;
9334 m.add_function(wrap_pyfunction!(rxname_child, m)?)?;
9335 m.add_function(wrap_pyfunction!(rxname_parent, m)?)?;
9336 m.add_function(wrap_pyfunction!(particle_is_valid, m)?)?;
9337 m.add_function(wrap_pyfunction!(particle_is_valid_pdc, m)?)?;
9338 m.add_function(wrap_pyfunction!(particle_is_hydrogen, m)?)?;
9339 m.add_function(wrap_pyfunction!(particle_is_heavy_ion, m)?)?;
9340 m.add_function(wrap_pyfunction!(dose_f1, m)?)?;
9341 m.add_function(wrap_pyfunction!(dose_lung_model, m)?)?;
9342 m.add_function(wrap_pyfunction!(read_xsdir, m)?)?;
9343 m.add_function(wrap_pyfunction!(read_meshtal, m)?)?;
9344 m.add_function(wrap_pyfunction!(read_wwinp, m)?)?;
9345 m.add_function(wrap_pyfunction!(read_mctal, m)?)?;
9346 m.add_function(wrap_pyfunction!(read_ssw, m)?)?;
9347 m.add_function(wrap_pyfunction!(read_ptrac, m)?)?;
9348 m.add_function(wrap_pyfunction!(read_mcpl, m)?)?;
9349 m.add_function(wrap_pyfunction!(write_mcpl, m)?)?;
9350 m.add_function(wrap_pyfunction!(ssw2mcpl, m)?)?;
9351 m.add_function(wrap_pyfunction!(mcpl2ssw, m)?)?;
9352 m.add_function(wrap_pyfunction!(merge_mcpl, m)?)?;
9353 m.add_function(wrap_pyfunction!(extract_mcpl, m)?)?;
9354 m.add_function(wrap_pyfunction!(mcpl_stats, m)?)?;
9355 m.add_function(wrap_pyfunction!(repair_mcpl, m)?)?;
9356 m.add_function(wrap_pyfunction!(read_endl, m)?)?;
9357 m.add_function(wrap_pyfunction!(endl_endftod, m)?)?;
9358 m.add_function(wrap_pyfunction!(combine_ssw_files, m)?)?;
9359 m.add_function(wrap_pyfunction!(read_chain, m)?)?;
9360 m.add_function(wrap_pyfunction!(build_depletion_system, m)?)?;
9361 m.add_function(wrap_pyfunction!(deplete, m)?)?;
9362 m.add_function(wrap_pyfunction!(deplete_series, m)?)?;
9363 m.add_function(wrap_pyfunction!(simple_xs, m)?)?;
9364 m.add_function(wrap_pyfunction!(scattering_length, m)?)?;
9365 m.add_function(wrap_pyfunction!(decay_energy, m)?)?;
9366 m.add_function(wrap_pyfunction!(decay_branches, m)?)?;
9367 m.add_function(wrap_pyfunction!(decay_branch_fraction, m)?)?;
9368 m.add_function(wrap_pyfunction!(fission_yields, m)?)?;
9369 m.add_function(wrap_pyfunction!(fission_yield, m)?)?;
9370 m.add_function(wrap_pyfunction!(normalize_nuclide, m)?)?;
9371 m.add_function(wrap_pyfunction!(decay_heat, m)?)?;
9372 m.add_function(wrap_pyfunction!(dose_factor, m)?)?;
9373 m.add_function(wrap_pyfunction!(dose_per_g, m)?)?;
9374 m.add_function(wrap_pyfunction!(parse_fgr15_table, m)?)?;
9375 m.add_function(wrap_pyfunction!(fgr15_age_index, m)?)?;
9376 m.add_function(wrap_pyfunction!(parse_irdff_g725, m)?)?;
9377 m.add_function(wrap_pyfunction!(mix_by_mass, m)?)?;
9378 m.add_function(wrap_pyfunction!(mix_by_volume, m)?)?;
9379 m.add_function(wrap_pyfunction!(specific_activity, m)?)?;
9380 m.add_function(wrap_pyfunction!(materials_doc_to_xml, m)?)?;
9381 m.add_function(wrap_pyfunction!(expand_elements, m)?)?;
9382 m.add_function(wrap_pyfunction!(collapse_elements, m)?)?;
9383 m.add_function(wrap_pyfunction!(read_serpent, m)?)?;
9384 m.add_function(wrap_pyfunction!(read_usrbin, m)?)?;
9385 m.add_function(wrap_pyfunction!(fluka_material_str, m)?)?;
9386 m.add_function(wrap_pyfunction!(fluka_compound_str, m)?)?;
9387 m.add_function(wrap_pyfunction!(fluka_builtin_set, m)?)?;
9388 m.add_function(wrap_pyfunction!(magic, m)?)?;
9389 m.add_function(wrap_pyfunction!(magic_with, m)?)?;
9390 m.add_function(wrap_pyfunction!(emit_openmc_weight_windows, m)?)?;
9391 m.add_function(wrap_pyfunction!(emit_serpent_wwin, m)?)?;
9392 m.add_function(wrap_pyfunction!(write_ssw, m)?)?;
9393 m.add_function(wrap_pyfunction!(mesh_to_geom, m)?)?;
9394 m.add_function(wrap_pyfunction!(half_life, m)?)?;
9395 m.add_function(wrap_pyfunction!(decay_constant, m)?)?;
9396 m.add_function(wrap_pyfunction!(q_value_capture, m)?)?;
9397 m.add_function(wrap_pyfunction!(q_value_alpha, m)?)?;
9398 m.add_function(wrap_pyfunction!(read_inp, m)?)?;
9399 m.add_function(wrap_pyfunction!(from_formula, m)?)?;
9400 m.add_function(wrap_pyfunction!(activity, m)?)?;
9401 m.add_function(wrap_pyfunction!(to_xml, m)?)?;
9402 m.add_function(wrap_pyfunction!(alara_parse_deck, m)?)?;
9403 m.add_function(wrap_pyfunction!(alara_parse_flux, m)?)?;
9404 m.add_function(wrap_pyfunction!(alara_parse_output, m)?)?;
9405 m.add_function(wrap_pyfunction!(alara_expand_schedule, m)?)?;
9406 m.add_function(wrap_pyfunction!(alara_validate_deck, m)?)?;
9407 m.add_function(wrap_pyfunction!(alara_check_block, m)?)?;
9408 m.add_function(wrap_pyfunction!(alara_flux_total, m)?)?;
9409 m.add_function(wrap_pyfunction!(alara_flux_len, m)?)?;
9410 m.add_function(wrap_pyfunction!(alara_output_totals, m)?)?;
9411 m.add_function(wrap_pyfunction!(alara_output_total_activity, m)?)?;
9412 m.add_function(wrap_pyfunction!(alara_photon_total_strength, m)?)?;
9413 m.add_function(wrap_pyfunction!(alara_schedule_total_time, m)?)?;
9414 m.add_function(wrap_pyfunction!(alara_clearance_eu_table, m)?)?;
9415 m.add_function(wrap_pyfunction!(alara_clearance_es_table, m)?)?;
9416 m.add_function(wrap_pyfunction!(alara_clearance_index, m)?)?;
9417 m.add_function(wrap_pyfunction!(alara_sum_of_fractions, m)?)?;
9418 m.add_function(wrap_pyfunction!(isotxs_parse, m)?)?;
9419 m.add_function(wrap_pyfunction!(rtflux_parse, m)?)?;
9420 m.add_function(wrap_pyfunction!(cccc_rtflux_npoints, m)?)?;
9421 m.add_function(wrap_pyfunction!(cccc_rtflux_point, m)?)?;
9422 m.add_function(wrap_pyfunction!(cccc_rtflux_total, m)?)?;
9423 m.add_function(wrap_pyfunction!(cccc_isotxs_find, m)?)?;
9424 m.add_function(wrap_pyfunction!(cccc_isotxs_len, m)?)?;
9425 m.add_function(wrap_pyfunction!(partisn_render, m)?)?;
9426 m.add_function(wrap_pyfunction!(partisn_validate, m)?)?;
9427 m.add_function(wrap_pyfunction!(fispact_parse_output, m)?)?;
9428 m.add_function(wrap_pyfunction!(fispact_parse_clearance, m)?)?;
9429 m.add_function(wrap_pyfunction!(fispact_is_output, m)?)?;
9430 m.add_function(wrap_pyfunction!(origen_parse_tape5, m)?)?;
9431 m.add_function(wrap_pyfunction!(origen_parse_tape6, m)?)?;
9432 m.add_function(wrap_pyfunction!(origen_parse_tape9, m)?)?;
9433 m.add_function(wrap_pyfunction!(origen_tape6_find, m)?)?;
9434 m.add_function(wrap_pyfunction!(origen_tape6_total_activity, m)?)?;
9435 m.add_function(wrap_pyfunction!(origen_tape9_find, m)?)?;
9436 m.add_function(wrap_pyfunction!(r2s_from_deck, m)?)?;
9437 m.add_function(wrap_pyfunction!(r2s_from_snapshot, m)?)?;
9438 m.add_function(wrap_pyfunction!(r2s_validate, m)?)?;
9439 m.add_function(wrap_pyfunction!(r2s_expand, m)?)?;
9440 m.add_function(wrap_pyfunction!(r2s_assemble, m)?)?;
9441 m.add_function(wrap_pyfunction!(r2s_tag_zone_strength, m)?)?;
9442 m.add_function(wrap_pyfunction!(r2s_photon_group_sums, m)?)?;
9443 m.add_function(wrap_pyfunction!(r2s_snapshot_inventory, m)?)?;
9444 m.add_function(wrap_pyfunction!(r2s_expand_sweep, m)?)?;
9445 m.add_function(wrap_pyfunction!(kinetics_solve, m)?)?;
9446 m.add_function(wrap_pyfunction!(kinetics_equilibrium, m)?)?;
9447 m.add_function(wrap_pyfunction!(kinetics_initial_rate, m)?)?;
9448 m.add_function(wrap_pyfunction!(kinetics_inhour_rho, m)?)?;
9449 m.add_function(wrap_pyfunction!(kinetics_stable_period, m)?)?;
9450 m.add_function(wrap_pyfunction!(kinetics_prompt_jump, m)?)?;
9451 m.add_function(wrap_pyfunction!(kinetics_from_ifp, m)?)?;
9452 m.add_function(wrap_pyfunction!(unfold_sandii, m)?)?;
9453 m.add_function(wrap_pyfunction!(unfold_staysl, m)?)?;
9454 m.add_function(wrap_pyfunction!(unfold_gravel, m)?)?;
9455 m.add_function(wrap_pyfunction!(unfold_forward_fold, m)?)?;
9456 m.add_function(wrap_pyfunction!(plasma_source_particles, m)?)?;
9457 m.add_function(wrap_pyfunction!(plasma_source_emit_cards, m)?)?;
9458 m.add_function(wrap_pyfunction!(plasma_source_spectrum_moments, m)?)?;
9459 m.add_function(wrap_pyfunction!(plasma_source_reactivity, m)?)?;
9460 m.add_function(wrap_pyfunction!(damage_nrt_dpa, m)?)?;
9461 m.add_function(wrap_pyfunction!(damage_arc_dpa, m)?)?;
9462 m.add_function(wrap_pyfunction!(damage_gas_appm, m)?)?;
9463 m.add_function(wrap_pyfunction!(damage_he_dpa_ratio, m)?)?;
9464 m.add_function(wrap_pyfunction!(damage_lindhard_partition, m)?)?;
9465 m.add_function(wrap_pyfunction!(damage_damage_energy, m)?)?;
9466 m.add_function(wrap_pyfunction!(damage_nrt_displacements, m)?)?;
9467 m.add_function(wrap_pyfunction!(damage_arc_efficiency, m)?)?;
9468 m.add_function(wrap_pyfunction!(damage_arc_displacements, m)?)?;
9469 m.add_function(wrap_pyfunction!(damage_fold_uq, m)?)?;
9470 m.add_function(wrap_pyfunction!(damage_specter_table, m)?)?;
9471 m.add_function(wrap_pyfunction!(damage_specter_damage_energy, m)?)?;
9472 m.add_function(wrap_pyfunction!(damage_specter_ed, m)?)?;
9473 m.add_function(wrap_pyfunction!(damage_specter_spectra, m)?)?;
9474 m.add_function(wrap_pyfunction!(tritium_steady, m)?)?;
9475 m.add_function(wrap_pyfunction!(tritium_transient, m)?)?;
9476 m.add_function(wrap_pyfunction!(tritium_time_lag, m)?)?;
9477 m.add_function(wrap_pyfunction!(tritium_breakthrough, m)?)?;
9478 m.add_function(wrap_pyfunction!(tritium_oriani, m)?)?;
9479 m.add_function(wrap_pyfunction!(tritium_langmuir, m)?)?;
9480 m.add_function(wrap_pyfunction!(tritium_irreversible_fill, m)?)?;
9481 m.add_function(wrap_pyfunction!(tritium_sieverts, m)?)?;
9482 m.add_function(wrap_pyfunction!(tritium_recombination_rate, m)?)?;
9483 m.add_function(wrap_pyfunction!(tritium_layers_steady, m)?)?;
9484 m.add_function(wrap_pyfunction!(tritium_layers_transient, m)?)?;
9485 m.add_function(wrap_pyfunction!(spectroscopy_rect_smooth, m)?)?;
9486 m.add_function(wrap_pyfunction!(spectroscopy_five_point_smooth, m)?)?;
9487 m.add_function(wrap_pyfunction!(spectroscopy_calc_bg, m)?)?;
9488 m.add_function(wrap_pyfunction!(spectroscopy_gross_count, m)?)?;
9489 m.add_function(wrap_pyfunction!(spectroscopy_net_counts, m)?)?;
9490 m.add_function(wrap_pyfunction!(spectroscopy_energy_bins, m)?)?;
9491 m.add_function(wrap_pyfunction!(spectroscopy_detector_efficiency, m)?)?;
9492 m.add_function(wrap_pyfunction!(spectroscopy_fit_efficiency, m)?)?;
9493 m.add_function(wrap_pyfunction!(spectroscopy_xray_lines, m)?)?;
9494 m.add_function(wrap_pyfunction!(spectroscopy_sdef_decay_source, m)?)?;
9495 m.add_function(wrap_pyfunction!(spectroscopy_parse_dollar_spe, m)?)?;
9496 m.add_function(wrap_pyfunction!(spectroscopy_parse_spe, m)?)?;
9497 m.add_function(wrap_pyfunction!(spectroscopy_read_dollar_spe, m)?)?;
9498 m.add_function(wrap_pyfunction!(spectroscopy_read_spe, m)?)?;
9499 m.add_function(wrap_pyfunction!(spectroscopy_parse_lines_tsv, m)?)?;
9500 m.add_function(wrap_pyfunction!(spectroscopy_read_decay_lines, m)?)?;
9501 m.add_function(wrap_pyfunction!(uq_sample_mvn, m)?)?;
9502 m.add_function(wrap_pyfunction!(uq_sample_lhs, m)?)?;
9503 m.add_function(wrap_pyfunction!(uq_sample_lognormal, m)?)?;
9504 m.add_function(wrap_pyfunction!(uq_lognormal_mean, m)?)?;
9505 m.add_function(wrap_pyfunction!(uq_lognormal_cov, m)?)?;
9506 m.add_function(wrap_pyfunction!(uq_sample_mean, m)?)?;
9507 m.add_function(wrap_pyfunction!(uq_sample_cov, m)?)?;
9508 m.add_function(wrap_pyfunction!(uq_check_convergence, m)?)?;
9509 m.add_function(wrap_pyfunction!(uq_perturb_branches, m)?)?;
9510 m.add_function(wrap_pyfunction!(uq_perturb_energies, m)?)?;
9511 m.add_function(wrap_pyfunction!(uq_passthrough, m)?)?;
9512 m.add_function(wrap_pyfunction!(uq_perturb_fission_yields, m)?)?;
9513 m.add_function(wrap_pyfunction!(parse_deck, m)?)?;
9514 m.add_function(wrap_pyfunction!(read_deck, m)?)?;
9515 m.add_function(wrap_pyfunction!(parse_sdef, m)?)?;
9516 m.add_function(wrap_pyfunction!(parse_csg_to_openmc, m)?)?;
9517 m.add_function(wrap_pyfunction!(read_csg_to_openmc, m)?)?;
9518 m.add_function(wrap_pyfunction!(parse_csg_to_serpent, m)?)?;
9519 m.add_function(wrap_pyfunction!(read_csg_to_serpent, m)?)?;
9520 m.add_function(wrap_pyfunction!(parse_csg_to_phits, m)?)?;
9521 m.add_function(wrap_pyfunction!(read_csg_to_phits, m)?)?;
9522 m.add_function(wrap_pyfunction!(parse_csg_to_gdml, m)?)?;
9523 m.add_function(wrap_pyfunction!(read_csg_to_gdml, m)?)?;
9524 m.add_function(wrap_pyfunction!(cumulative_decays, m)?)?;
9525 m.add_function(wrap_pyfunction!(progeny, m)?)?;
9526 m.add_function(wrap_pyfunction!(branching_fraction, m)?)?;
9527 m.add_function(wrap_pyfunction!(decay_mode, m)?)?;
9528 m.add_function(wrap_pyfunction!(chain_edges, m)?)?;
9529 m.add_function(wrap_pyfunction!(armi_to_nucid, m)?)?;
9530 m.add_function(wrap_pyfunction!(nucid_to_armi, m)?)?;
9531 m.add_function(wrap_pyfunction!(mcc3_to_nucid, m)?)?;
9532 m.add_function(wrap_pyfunction!(check_labels, m)?)?;
9533 m.add_function(wrap_pyfunction!(audit_material, m)?)?;
9534 m.add_function(wrap_pyfunction!(separate_material, m)?)?;
9535 m.add_function(wrap_pyfunction!(blend_material, m)?)?;
9536 m.add_function(wrap_pyfunction!(enrichment_value_func, m)?)?;
9537 m.add_function(wrap_pyfunction!(enrichment_swu_per_feed, m)?)?;
9538 m.add_function(wrap_pyfunction!(enrichment_swu_per_prod, m)?)?;
9539 m.add_function(wrap_pyfunction!(enrichment_swu_per_tail, m)?)?;
9540 m.add_function(wrap_pyfunction!(enrichment_prod_per_feed, m)?)?;
9541 m.add_function(wrap_pyfunction!(enrichment_tail_per_feed, m)?)?;
9542 m.add_function(wrap_pyfunction!(enrichment_tail_per_prod, m)?)?;
9543 m.add_function(wrap_pyfunction!(enrichment_feed_per_prod, m)?)?;
9544 m.add_function(wrap_pyfunction!(enrichment_feed_per_tail, m)?)?;
9545 m.add_function(wrap_pyfunction!(enrichment_prod_per_tail, m)?)?;
9546 m.add_function(wrap_pyfunction!(enrichment_alphastar_i, m)?)?;
9547 m.add_function(wrap_pyfunction!(mcpl_statsum_validate, m)?)?;
9548 m.add_function(wrap_pyfunction!(mcpl_statsum_comment, m)?)?;
9549 m.add_class::<PyCusum>()?;
9550 m.add_function(wrap_pyfunction!(emit_cards, m)?)?;
9551 m.add_function(wrap_pyfunction!(emit_drift_table, m)?)?;
9552 m.add_function(wrap_pyfunction!(emit_armi_cards, m)?)?;
9553 m.add_function(wrap_pyfunction!(emit_armi_drift_table, m)?)?;
9554 m.add_class::<PyNuclide>()?;
9555 m.add_class::<PyParticle>()?;
9556 m.add_class::<PyXsdir>()?;
9557 m.add_class::<PyXsdirTable>()?;
9558 m.add_class::<PyMeshtal>()?;
9559 m.add_class::<PyMeshTally>()?;
9560 m.add_class::<PyWwinp>()?;
9561 m.add_class::<PyMctal>()?;
9562 m.add_class::<PySurfSrc>()?;
9563 m.add_class::<PyPtracFile>()?;
9564 m.add_class::<PyMcplFile>()?;
9565 m.add_class::<PyEndlLibrary>()?;
9566 m.add_class::<PyChain>()?;
9567 m.add_class::<PyDepletionSystem>()?;
9568 m.add_class::<PyUsrbinTally>()?;
9569 m.add_class::<PyMagicOutput>()?;
9570 m.add_class::<PyAliasTable>()?;
9571 m.add_class::<PyMeshSourceSampler>()?;
9572 m.add_class::<PyKdeSampler>()?;
9573 m.add_class::<PyCascade>()?;
9574 m.add_class::<PyMaterialsCompendium>()?;
9575 m.add_class::<PyDeckProblem>()?;
9576 m.add_class::<PyInventory>()?;
9577 Ok(())
9578}