Skip to main content

nucleide_vr_tools/
lib.rs

1#![warn(missing_docs)]
2//! Monte Carlo variance-reduction utilities built on [`mcnp_io`] meshtal data.
3//!
4//! - [`magic`] — MAGIC weight-window generation operating on native
5//!   [`nucleide_mcnp_io::meshtal::MeshTallyData`] instead of MOAB-tagged meshes.
6//! - [`windows`] — emission of MAGIC weight windows for OpenMC
7//!   (`settings.xml` `<mesh>` + `<weight_windows>` text) and Serpent
8//!   (`wwin ... wf FILE 2`, the MCNP WWINP spelling Serpent reads).
9//! - [`sampling`] — Walker/Vose alias-table source sampling plus a
10//!   voxel-level `MeshSourceSampler` with ANALOG / UNIFORM / USER bias modes.
11//! - [`kde`] — Gaussian kernel-density source sampling (KDSource-class,
12//!   clean-room): fit over caller particle vectors, deterministic resampling.
13
14pub mod kde;
15pub mod magic;
16pub mod sampling;
17pub mod windows;
18
19pub use kde::{Bandwidth, KdeSampler};
20
21pub use magic::{magic, magic_with, MagicOutput, MagicParams, MagicSelection};
22pub use sampling::{AliasTable, MeshSourceSampler, Mode, SampledVoxel};
23pub use windows::{
24    emit_openmc_weight_windows, emit_serpent_wwin, OpenMcOptions, OpenMcWeightWindows, SerpentWwin,
25};
26
27/// Result alias for the `vr-tools` crate.
28pub type Result<T> = std::result::Result<T, Error>;
29
30/// Errors raised by variance-reduction tools.
31#[derive(Debug, Clone, PartialEq)]
32#[non_exhaustive]
33pub enum Error {
34    /// Tally carries no volume elements.
35    EmptyTally,
36    /// A requested array has the wrong length.
37    LengthMismatch {
38        /// Expected element count.
39        expected: usize,
40        /// Actual element count.
41        got: usize,
42    },
43    /// Every flux value feeding one energy bin is non-positive, so the
44    /// MAGIC normalization `value / (2 * max)` would divide by zero.
45    /// (Rather than silently emitting `inf`/`nan`, this is an error.)
46    ZeroMaxFlux {
47        /// Index of the energy bin with no positive flux.
48        energy_group: usize,
49    },
50    /// PDF input is empty.
51    EmptyPdf,
52    /// PDF contains a negative entry.
53    NegativePdf {
54        /// Index of the negative entry.
55        index: usize,
56        /// The offending value.
57        value: f64,
58    },
59    /// PDF contains a non-finite (NaN/infinite) entry.
60    NonFinitePdf {
61        /// Index of the non-finite entry.
62        index: usize,
63    },
64    /// PDF sums to zero (or negatively); cannot normalize.
65    ZeroSumPdf,
66    /// Tally or user density contains a negative value.
67    NegativeTally {
68        /// Index of the negative value.
69        index: usize,
70        /// The offending value.
71        value: f64,
72    },
73    /// Tally array contains a non-finite (NaN/infinite) value.
74    NonFiniteTally {
75        /// Name of the tally field holding the value.
76        field: &'static str,
77        /// Index of the non-finite entry.
78        index: usize,
79    },
80    /// A KDE Silverman dimension has zero variance (a zero bandwidth is a
81    /// delta spike, never a density); use an explicit fixed width instead.
82    ZeroVarianceDim {
83        /// Index of the zero-variance dimension.
84        dim: usize,
85    },
86    /// A KDE draw uniform falls outside `[0, 1)`.
87    BadDraw {
88        /// The offending uniform value.
89        value: f64,
90    },
91    /// A weight-window lower bound is negative and has no spelling in either
92    /// target format (non-positive windows are inert; negative never is).
93    NegativeWindow {
94        /// Flat index into `MagicOutput::lower_bounds_ww`.
95        index: usize,
96        /// The offending value.
97        value: f64,
98    },
99    /// A weight-window lower bound is NaN or infinite.
100    NonFiniteWindow {
101        /// Flat index into `MagicOutput::lower_bounds_ww`.
102        index: usize,
103    },
104    /// Energy upper bounds are empty, non-finite, non-positive, or not
105    /// strictly increasing, so the window energy grid is ill-defined.
106    BadEnergyBounds {
107        /// Index of the offending bound.
108        index: usize,
109    },
110    /// Mesh bounds have fewer than two entries, are non-finite, or are not
111    /// strictly increasing.
112    BadMeshBounds {
113        /// Axis index (0 = x, 1 = y, 2 = z).
114        axis: usize,
115        /// Index of the offending bound.
116        index: usize,
117    },
118    /// An emission tuning parameter or card token falls outside the range the
119    /// target code enforces when reading the file, or cannot be interpolated
120    /// into the emitted card safely.
121    BadEmissionOption {
122        /// Parameter name (`upper_bound_ratio`, `survival_ratio`,
123        /// `max_split`, `weight_cutoff`, `name`, `file`, `energy_bounds`,
124        /// `upper_ww_bounds`).
125        option: &'static str,
126        /// The offending value.
127        value: String,
128        /// Why it was rejected.
129        detail: &'static str,
130    },
131    /// The WWINP writer in `nucleide-mcnp-io` refused the assembled file.
132    Wwinp(String),
133}
134
135impl std::fmt::Display for Error {
136    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137        match self {
138            Error::EmptyTally => write!(f, "tally contains no volume elements"),
139            Error::LengthMismatch { expected, got } => {
140                write!(f, "length mismatch: expected {expected}, got {got}")
141            }
142            Error::ZeroMaxFlux { energy_group } => write!(
143                f,
144                "energy group {energy_group} has no positive flux; \
145                 weight-window normalization would divide by zero"
146            ),
147            Error::EmptyPdf => write!(f, "pdf must contain at least one value"),
148            Error::NegativePdf { index, value } => {
149                write!(f, "pdf[{index}] = {value} is negative")
150            }
151            Error::NonFinitePdf { index } => write!(f, "pdf[{index}] is not finite"),
152            Error::ZeroSumPdf => write!(f, "pdf sums to zero; cannot normalize"),
153            Error::NegativeTally { index, value } => write!(
154                f,
155                "tally/density value at index {index} = {value} is negative"
156            ),
157            Error::NonFiniteTally { field, index } => {
158                write!(f, "{field}[{index}] is not finite")
159            }
160            Error::ZeroVarianceDim { dim } => {
161                write!(
162                    f,
163                    "kde dimension {dim} has zero variance; use a fixed bandwidth"
164                )
165            }
166            Error::BadDraw { value } => {
167                write!(f, "kde draw uniform {value} is outside [0, 1)")
168            }
169            Error::NegativeWindow { index, value } => {
170                write!(
171                    f,
172                    "weight-window lower bound at index {index} = {value} is negative"
173                )
174            }
175            Error::NonFiniteWindow { index } => {
176                write!(
177                    f,
178                    "weight-window lower bound at index {index} is not finite"
179                )
180            }
181            Error::BadEnergyBounds { index } => {
182                write!(
183                    f,
184                    "energy upper bound at index {index} is not positive and increasing"
185                )
186            }
187            Error::BadMeshBounds { axis, index } => {
188                write!(
189                    f,
190                    "mesh bound at axis {axis} index {index} is not finite and increasing"
191                )
192            }
193            Error::BadEmissionOption {
194                option,
195                value,
196                detail,
197            } => write!(f, "emission option {option} = {value}: {detail}"),
198            Error::Wwinp(m) => write!(f, "wwinp writer error: {m}"),
199        }
200    }
201}
202
203impl std::error::Error for Error {}
204
205#[cfg(test)]
206mod tests {
207    use super::Error;
208
209    #[test]
210    fn error_display_covers_all_variants() {
211        let cases: Vec<(Error, &str)> = vec![
212            (Error::EmptyTally, "no volume elements"),
213            (
214                Error::LengthMismatch {
215                    expected: 3,
216                    got: 5,
217                },
218                "expected 3, got 5",
219            ),
220            (Error::ZeroMaxFlux { energy_group: 2 }, "energy group 2"),
221            (Error::EmptyPdf, "at least one value"),
222            (
223                Error::NegativePdf {
224                    index: 1,
225                    value: -0.5,
226                },
227                "pdf[1]",
228            ),
229            (Error::NonFinitePdf { index: 0 }, "pdf[0]"),
230            (Error::ZeroSumPdf, "sums to zero"),
231            (
232                Error::NegativeTally {
233                    index: 4,
234                    value: -1.0,
235                },
236                "index 4",
237            ),
238            (
239                Error::NonFiniteTally {
240                    field: "flux",
241                    index: 7,
242                },
243                "flux[7]",
244            ),
245            (Error::ZeroVarianceDim { dim: 1 }, "dimension 1"),
246            (Error::BadDraw { value: 1.5 }, "outside [0, 1)"),
247            (
248                Error::NegativeWindow {
249                    index: 2,
250                    value: -0.1,
251                },
252                "index 2",
253            ),
254            (Error::NonFiniteWindow { index: 3 }, "index 3"),
255            (Error::BadEnergyBounds { index: 1 }, "index 1"),
256            (Error::BadMeshBounds { axis: 2, index: 4 }, "axis 2"),
257            (
258                Error::BadEmissionOption {
259                    option: "survival_ratio",
260                    value: "1".to_string(),
261                    detail: "must be greater than 1",
262                },
263                "survival_ratio",
264            ),
265            (Error::Wwinp("disk".into()), "wwinp writer error"),
266        ];
267        for (err, needle) in cases {
268            assert!(
269                format!("{err}").contains(needle),
270                "display of {err:?} should contain {needle:?}"
271            );
272            // Ensure the std::error::Error impl is linked.
273            let _: &dyn std::error::Error = &err;
274        }
275    }
276}