1use serde::{Deserialize, Serialize};
45
46use super::hist::{Binning, Histogram};
47use crate::geometry::Stack;
48use crate::units::J_PER_EV;
49
50#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
52pub struct InterfaceRefine {
53 pub width_m: f64,
57 pub bins: usize,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
63pub struct DepthDoseConfig {
64 pub bins_per_layer: usize,
67 #[serde(default)]
69 pub refine: Option<InterfaceRefine>,
70 #[serde(default)]
72 pub substrate_depth_m: f64,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
77pub struct DoseConfig {
78 #[serde(default)]
80 pub depth: Option<DepthDoseConfig>,
81 #[serde(default)]
84 pub transmission: bool,
85}
86
87#[derive(Debug, Clone, PartialEq, thiserror::Error)]
89#[error("invalid dose configuration: {0}")]
90pub struct DoseError(pub String);
91
92#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
94pub struct Estimate {
95 pub mean: f64,
97 pub std_err: f64,
99}
100
101impl Estimate {
102 fn scaled(self, k: f64) -> Self {
103 Self {
104 mean: self.mean * k,
105 std_err: self.std_err * k.abs(),
106 }
107 }
108}
109
110#[derive(Debug, Clone, PartialEq)]
113struct HistorySums {
114 histories: u64,
115 sum: Vec<f64>,
116 sum_sq: Vec<f64>,
117 scratch: Vec<f64>,
118 touched: Vec<usize>,
119}
120
121impl HistorySums {
122 fn new(n: usize) -> Self {
123 Self {
124 histories: 0,
125 sum: vec![0.0; n],
126 sum_sq: vec![0.0; n],
127 scratch: vec![0.0; n],
128 touched: Vec::new(),
129 }
130 }
131
132 fn add(&mut self, i: usize, x: f64) {
133 if x <= 0.0 {
134 return;
135 }
136 if self.scratch[i] == 0.0 {
137 self.touched.push(i);
138 }
139 self.scratch[i] += x;
140 }
141
142 fn end_history(&mut self) {
143 self.histories += 1;
144 for &i in &self.touched {
145 let s = self.scratch[i];
146 self.sum[i] += s;
147 self.sum_sq[i] += s * s;
148 self.scratch[i] = 0.0;
149 }
150 self.touched.clear();
151 }
152
153 fn merge(&mut self, o: &HistorySums) {
154 self.histories += o.histories;
155 for (a, b) in self.sum.iter_mut().zip(&o.sum) {
156 *a += b;
157 }
158 for (a, b) in self.sum_sq.iter_mut().zip(&o.sum_sq) {
159 *a += b;
160 }
161 }
162
163 fn estimate(&self, i: usize) -> Estimate {
164 let n = self.histories as f64;
165 if self.histories == 0 {
166 return Estimate {
167 mean: 0.0,
168 std_err: f64::NAN,
169 };
170 }
171 let mean = self.sum[i] / n;
172 let std_err = if self.histories < 2 {
173 f64::NAN
174 } else {
175 let var = (self.sum_sq[i] - self.sum[i] * self.sum[i] / n).max(0.0);
176 (var / (n * (n - 1.0))).sqrt()
177 };
178 Estimate { mean, std_err }
179 }
180}
181
182#[derive(Debug, Clone, PartialEq)]
184struct LayerInfo {
185 name: Option<String>,
186 front_m: f64,
187 back_m: f64,
188 density_kg_m3: f64,
189 edges: Vec<f64>,
192 first_bin: usize,
193}
194
195impl LayerInfo {
196 fn areal_mass(&self) -> Option<f64> {
197 let t = self.back_m - self.front_m;
198 t.is_finite().then_some(self.density_kg_m3 * t)
199 }
200}
201
202fn layer_edges(front: f64, back: f64, finite: bool, cfg: &DepthDoseConfig) -> Vec<f64> {
203 let t = back - front;
204 let mut edges = Vec::new();
205 let band = cfg
206 .refine
207 .map(|r| r.width_m.min(0.5 * t))
208 .unwrap_or(0.0)
209 .max(0.0);
210 let fine = cfg.refine.map(|r| r.bins).unwrap_or(0);
211 let refine_front = band > 0.0 && fine > 0;
212 let refine_back = refine_front && finite;
214 let mid_lo = if refine_front { front + band } else { front };
215 let mid_hi = if refine_back { back - band } else { back };
216 if refine_front {
217 for i in 0..fine {
218 edges.push(front + band * i as f64 / fine as f64);
219 }
220 }
221 for i in 0..cfg.bins_per_layer {
222 edges.push(mid_lo + (mid_hi - mid_lo) * i as f64 / cfg.bins_per_layer as f64);
223 }
224 if refine_back {
225 for i in 0..fine {
226 edges.push(mid_hi + band * i as f64 / fine as f64);
227 }
228 }
229 edges.push(back);
230 edges.dedup();
232 edges
233}
234
235#[derive(Debug, Clone, PartialEq)]
237pub(crate) struct DoseAcc {
238 config: DoseConfig,
239 layers: Vec<LayerInfo>,
240 layer_sums: HistorySums,
242 depth_sums: Option<HistorySums>,
244 n_bins: usize,
245 transmission_sums: Option<HistorySums>,
247 spectra: Vec<Histogram>,
248}
249
250impl DoseAcc {
251 pub(crate) fn new(
252 stack: &Stack,
253 config: DoseConfig,
254 escape_energy: Binning,
255 ) -> Result<Self, DoseError> {
256 if let Some(d) = &config.depth {
257 if d.bins_per_layer == 0 {
258 return Err(DoseError("bins_per_layer must be at least 1".into()));
259 }
260 if let Some(r) = &d.refine {
261 if !(r.width_m.is_finite() && r.width_m >= 0.0) {
262 return Err(DoseError(format!(
263 "refine.width_m must be finite and non-negative, got {}",
264 r.width_m
265 )));
266 }
267 }
268 let last_infinite = stack
269 .layers()
270 .last()
271 .is_some_and(|l| !l.back_m().is_finite());
272 if last_infinite && !(d.substrate_depth_m.is_finite() && d.substrate_depth_m > 0.0) {
273 return Err(DoseError(
274 "substrate_depth_m must be finite and positive for a semi-infinite last layer"
275 .into(),
276 ));
277 }
278 }
279 let mut n_bins = 0usize;
280 let mut layers = Vec::new();
281 for l in stack.layers() {
282 let (edges, first_bin) = match &config.depth {
283 Some(d) => {
284 let back = if l.back_m().is_finite() {
285 l.back_m()
286 } else {
287 l.front_m() + d.substrate_depth_m
288 };
289 let e = layer_edges(l.front_m(), back, l.back_m().is_finite(), d);
290 let first = n_bins;
291 n_bins += e.len() - 1;
292 (e, first)
293 }
294 None => (Vec::new(), 0),
295 };
296 layers.push(LayerInfo {
297 name: l.material().name().map(str::to_string),
298 front_m: l.front_m(),
299 back_m: l.back_m(),
300 density_kg_m3: l.material().mass_density(),
301 edges,
302 first_bin,
303 });
304 }
305 let n_if = layers.len().saturating_sub(1);
306 Ok(Self {
307 config,
308 layer_sums: HistorySums::new(layers.len()),
309 depth_sums: config.depth.map(|_| HistorySums::new(n_bins + 1)),
310 n_bins,
311 transmission_sums: config.transmission.then(|| HistorySums::new(n_if * 4 * 2)),
312 spectra: if config.transmission {
313 (0..n_if * 4)
314 .map(|_| Histogram::new(escape_energy))
315 .collect()
316 } else {
317 Vec::new()
318 },
319 layers,
320 })
321 }
322
323 pub(crate) fn deposit(&mut self, layer: usize, x: f64, e: f64) {
324 self.layer_sums.add(layer, e);
325 if let Some(d) = &mut self.depth_sums {
326 let info = &self.layers[layer];
327 let last = *info.edges.last().expect("depth edges built");
328 if x >= last && !info.back_m.is_finite() {
329 d.add(self.n_bins, e);
330 return;
331 }
332 let xc = x.clamp(info.edges[0], last);
333 let nb = info.edges.len() - 1;
334 let i = info.edges[1..nb].partition_point(|&edge| edge <= xc);
335 d.add(info.first_bin + i, e);
336 }
337 }
338
339 pub(crate) fn crossing(&mut self, from: usize, to: usize, energy_ev: f64, secondary: bool) {
341 let Some(t) = &mut self.transmission_sums else {
342 return;
343 };
344 let (interface, backward) = if to > from { (from, 0) } else { (to, 1) };
345 let k = (interface * 2 + backward) * 2 + usize::from(secondary);
346 t.add(2 * k, 1.0);
347 t.add(2 * k + 1, energy_ev);
348 self.spectra[k].fill(energy_ev);
349 }
350
351 pub(crate) fn end_history(&mut self) {
352 self.layer_sums.end_history();
353 if let Some(d) = &mut self.depth_sums {
354 d.end_history();
355 }
356 if let Some(t) = &mut self.transmission_sums {
357 t.end_history();
358 }
359 }
360
361 pub(crate) fn merge(&mut self, o: &DoseAcc) {
362 self.layer_sums.merge(&o.layer_sums);
363 if let (Some(a), Some(b)) = (&mut self.depth_sums, &o.depth_sums) {
364 a.merge(b);
365 }
366 if let (Some(a), Some(b)) = (&mut self.transmission_sums, &o.transmission_sums) {
367 a.merge(b);
368 }
369 for (a, b) in self.spectra.iter_mut().zip(&o.spectra) {
370 for (x, y) in a.counts.iter_mut().zip(&b.counts) {
371 *x += y;
372 }
373 a.underflow += b.underflow;
374 a.overflow += b.overflow;
375 }
376 }
377
378 pub(crate) fn report(&self, incident_ev: f64) -> DoseReport {
379 let layers = self
380 .layers
381 .iter()
382 .enumerate()
383 .map(|(i, l)| {
384 let e = self.layer_sums.estimate(i);
385 LayerDose {
386 index: i,
387 name: l.name.clone(),
388 thickness_m: (l.back_m - l.front_m)
389 .is_finite()
390 .then_some(l.back_m - l.front_m),
391 areal_mass_kg_m2: l.areal_mass(),
392 energy_ev: e,
393 dose_gy_m2: l.areal_mass().map(|m| e.scaled(J_PER_EV / m)),
394 }
395 })
396 .collect();
397 let depth = self.depth_sums.as_ref().map(|d| {
398 let mut edges_m = Vec::new();
399 let mut layer_of_bin = Vec::new();
400 let mut energy_ev = Vec::new();
401 let mut ev_per_nm = Vec::new();
402 let mut dose = Vec::new();
403 for (li, l) in self.layers.iter().enumerate() {
404 for b in 0..l.edges.len() - 1 {
405 let (lo, hi) = (l.edges[b], l.edges[b + 1]);
406 let e = d.estimate(l.first_bin + b);
407 let w = hi - lo;
408 edges_m.push(lo);
409 layer_of_bin.push(li);
410 energy_ev.push(e);
411 ev_per_nm.push(e.scaled(1e-9 / w));
412 dose.push(
413 l.back_m
414 .is_finite()
415 .then(|| e.scaled(J_PER_EV / (l.density_kg_m3 * w))),
416 );
417 }
418 }
419 edges_m.push(
420 self.layers
421 .last()
422 .and_then(|l| l.edges.last().copied())
423 .unwrap_or(0.0),
424 );
425 DepthDose {
426 edges_m,
427 layer_of_bin,
428 energy_ev,
429 ev_per_nm,
430 dose_gy_m2: dose,
431 outside_ev: d.estimate(self.n_bins),
432 }
433 });
434 let transmission = self
435 .transmission_sums
436 .as_ref()
437 .map(|t| {
438 let n_if = self.layers.len().saturating_sub(1);
439 let mut out = Vec::new();
440 for interface in 0..n_if {
441 for backward in 0..2 {
442 for secondary in 0..2 {
443 let k = (interface * 2 + backward) * 2 + secondary;
444 let energy = t.estimate(2 * k + 1);
445 let total_energy = t.sum[2 * k + 1];
446 out.push(InterfaceTransmission {
447 interface,
448 from_layer: if backward == 0 {
449 interface
450 } else {
451 interface + 1
452 },
453 to_layer: if backward == 0 {
454 interface + 1
455 } else {
456 interface
457 },
458 secondary: secondary == 1,
459 count: t.estimate(2 * k),
460 energy_ev: energy,
461 energy_fraction: if incident_ev > 0.0 {
462 total_energy / incident_ev
463 } else {
464 0.0
465 },
466 spectrum: self.spectra[k].clone(),
467 });
468 }
469 }
470 }
471 out
472 })
473 .unwrap_or_default();
474 DoseReport {
475 layers,
476 depth,
477 transmission,
478 }
479 }
480}
481
482#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
484pub struct LayerDose {
485 pub index: usize,
487 pub name: Option<String>,
489 pub thickness_m: Option<f64>,
491 pub areal_mass_kg_m2: Option<f64>,
493 pub energy_ev: Estimate,
495 pub dose_gy_m2: Option<Estimate>,
498}
499
500#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
502pub struct DepthDose {
503 pub edges_m: Vec<f64>,
506 pub layer_of_bin: Vec<usize>,
508 pub energy_ev: Vec<Estimate>,
510 pub ev_per_nm: Vec<Estimate>,
512 pub dose_gy_m2: Vec<Option<Estimate>>,
515 pub outside_ev: Estimate,
518}
519
520#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
522pub struct InterfaceTransmission {
523 pub interface: usize,
525 pub from_layer: usize,
527 pub to_layer: usize,
529 pub secondary: bool,
531 pub count: Estimate,
533 pub energy_ev: Estimate,
535 pub energy_fraction: f64,
537 pub spectrum: Histogram,
540}
541
542#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
544pub struct DoseReport {
545 pub layers: Vec<LayerDose>,
547 pub depth: Option<DepthDose>,
549 pub transmission: Vec<InterfaceTransmission>,
552}