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}