1use std::collections::BTreeMap;
40use std::io::{self, Write};
41
42use serde::{Deserialize, Serialize};
43
44use crate::tally::voxel::{VoxelReport, VoxelTallyGrid};
45
46#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
48pub struct ProfileCell {
49 pub implanted_per_m: f64,
51 pub displacements_per_m: f64,
53 pub substrate_displacements_per_m: f64,
55}
56
57impl ProfileCell {
58 fn add(&mut self, o: &ProfileCell) {
59 self.implanted_per_m += o.implanted_per_m;
60 self.displacements_per_m += o.displacements_per_m;
61 self.substrate_displacements_per_m += o.substrate_displacements_per_m;
62 }
63}
64
65#[derive(Debug, Clone, PartialEq)]
67pub(crate) struct ProfileAccumulator {
68 grid: VoxelTallyGrid,
69 cells: BTreeMap<u64, ProfileCell>,
70 outside: ProfileCell,
71}
72
73impl ProfileAccumulator {
74 pub(crate) fn new(grid: VoxelTallyGrid) -> Self {
75 Self {
76 grid,
77 cells: BTreeMap::new(),
78 outside: ProfileCell::default(),
79 }
80 }
81
82 pub(crate) fn add_counts(&mut self, all: &VoxelReport, substrate: &VoxelReport, scale: f64) {
85 for v in &all.voxels {
86 let c = self.cells.entry(self.grid.flat_index(v.index)).or_default();
87 c.implanted_per_m += v.cell.implanted as f64 * scale;
88 c.displacements_per_m += v.cell.displacements as f64 * scale;
89 }
90 self.outside.implanted_per_m += all.outside.implanted as f64 * scale;
91 self.outside.displacements_per_m += all.outside.displacements as f64 * scale;
92 for v in &substrate.voxels {
93 let c = self.cells.entry(self.grid.flat_index(v.index)).or_default();
94 c.substrate_displacements_per_m += v.cell.displacements as f64 * scale;
95 }
96 self.outside.substrate_displacements_per_m +=
97 substrate.outside.displacements as f64 * scale;
98 }
99
100 pub(crate) fn merge(&mut self, o: &ProfileAccumulator) {
101 for (&k, c) in &o.cells {
102 self.cells.entry(k).or_default().add(c);
103 }
104 self.outside.add(&o.outside);
105 }
106
107 pub(crate) fn snapshot(&self, beam_z: u8, fluence_m2: f64, ions: u64) -> ProfileSnapshot {
108 ProfileSnapshot {
109 beam_z,
110 peak_fluence_m2: fluence_m2,
111 ions_per_column: ions,
112 grid: self.grid,
113 voxels: self
114 .cells
115 .iter()
116 .map(|(&k, &cell)| (self.grid.unflatten(k), cell))
117 .collect(),
118 outside: self.outside,
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
125pub struct ProfileSnapshot {
126 pub beam_z: u8,
128 pub peak_fluence_m2: f64,
130 pub ions_per_column: u64,
132 pub grid: VoxelTallyGrid,
134 pub voxels: Vec<([usize; 3], ProfileCell)>,
136 pub outside: ProfileCell,
138}
139
140impl ProfileSnapshot {
141 pub fn grand_total(&self) -> ProfileCell {
143 let mut t = self.outside;
144 for (_, c) in &self.voxels {
145 t.add(c);
146 }
147 t
148 }
149
150 pub fn voxel_area_m2(&self) -> f64 {
153 let s = self.grid.spacing_m();
154 s[0] * s[1]
155 }
156
157 pub fn depth_profile(&self) -> Vec<ProfileCell> {
160 let mut out = vec![ProfileCell::default(); self.grid.dims()[0]];
161 for (ijk, c) in &self.voxels {
162 out[ijk[0]].add(c);
163 }
164 out
165 }
166
167 pub fn lateral_profile(&self) -> Vec<ProfileCell> {
170 let mut out = vec![ProfileCell::default(); self.grid.dims()[1]];
171 for (ijk, c) in &self.voxels {
172 out[ijk[1]].add(c);
173 }
174 out
175 }
176
177 pub fn write_csv<W: Write>(&self, mut w: W) -> io::Result<()> {
180 let g = &self.grid;
181 let j3 = |a: [f64; 3]| format!("{:e},{:e},{:e}", a[0], a[1], a[2]);
182 writeln!(w, "# lindhard 2d profile v1")?;
183 writeln!(w, "# beam_z = {}", self.beam_z)?;
184 writeln!(w, "# peak_fluence_m2 = {:e}", self.peak_fluence_m2)?;
185 writeln!(w, "# ions_per_column = {}", self.ions_per_column)?;
186 writeln!(w, "# origin_m = {}", j3(g.origin_m()))?;
187 writeln!(w, "# spacing_m = {}", j3(g.spacing_m()))?;
188 let d = g.dims();
189 writeln!(w, "# dims = {},{},{}", d[0], d[1], d[2])?;
190 let o = &self.outside;
191 writeln!(
192 w,
193 "# outside_per_m = {:e},{:e},{:e}",
194 o.implanted_per_m, o.displacements_per_m, o.substrate_displacements_per_m
195 )?;
196 writeln!(
197 w,
198 "i,j,k,x_m,y_m,z_m,implanted_per_m,displacements_per_m,\
199 substrate_displacements_per_m,implanted_per_m3,displacements_per_m3,\
200 substrate_displacements_per_m3"
201 )?;
202 let a = self.voxel_area_m2();
203 for (ijk, c) in &self.voxels {
204 let m = g.center_m(*ijk);
205 writeln!(
206 w,
207 "{},{},{},{:e},{:e},{:e},{:e},{:e},{:e},{:e},{:e},{:e}",
208 ijk[0],
209 ijk[1],
210 ijk[2],
211 m[0],
212 m[1],
213 m[2],
214 c.implanted_per_m,
215 c.displacements_per_m,
216 c.substrate_displacements_per_m,
217 c.implanted_per_m / a,
218 c.displacements_per_m / a,
219 c.substrate_displacements_per_m / a
220 )?;
221 }
222 Ok(())
223 }
224
225 pub fn to_csv(&self) -> String {
227 let mut buf = Vec::new();
228 self.write_csv(&mut buf).expect("writing to a Vec");
229 String::from_utf8(buf).expect("the CSV is ASCII")
230 }
231}
232
233#[derive(Debug, Clone, PartialEq)]
235pub struct CutThrough {
236 pub reached: bool,
238 pub column: Option<usize>,
240 pub fluence_before_m2: f64,
245 pub fluence_m2: f64,
248 pub ions_done: u64,
250 pub profiles: Option<ProfileSnapshot>,
253}
254
255#[derive(Debug, Clone, Copy, PartialEq)]
257pub struct PointDifference {
258 pub y_m: f64,
261 pub published_m: f64,
263 pub simulated_m: f64,
265}
266
267#[derive(Debug, Clone, PartialEq)]
269pub struct ProfileComparison {
270 pub points: Vec<PointDifference>,
272 pub rms_m: f64,
274 pub max_abs_m: f64,
276 pub relative_rms: f64,
278 pub tolerance: f64,
280 pub within_tolerance: bool,
282}
283
284pub fn compare_profile(
294 sim_y_m: &[f64],
295 sim_depth_m: &[f64],
296 published: &[(f64, f64)],
297 tolerance: f64,
298) -> Result<ProfileComparison, String> {
299 if sim_y_m.len() != sim_depth_m.len() || sim_y_m.len() < 2 {
300 return Err("the simulated profile needs at least two matching points".into());
301 }
302 if published.is_empty() {
303 return Err("no published points".into());
304 }
305 if !(tolerance.is_finite() && tolerance > 0.0) {
306 return Err("the tolerance must be finite and positive".into());
307 }
308 if sim_y_m.windows(2).any(|w| w[1] <= w[0]) {
309 return Err("the simulated lateral coordinates must increase".into());
310 }
311 let mut points = Vec::with_capacity(published.len());
312 for &(y, d) in published {
313 if !(y.is_finite() && d.is_finite()) {
314 return Err("published points must be finite".into());
315 }
316 let (lo, hi) = (sim_y_m[0], sim_y_m[sim_y_m.len() - 1]);
317 if y < lo || y > hi {
318 return Err(format!(
319 "published point y = {y:e} m is outside [{lo:e}, {hi:e}]"
320 ));
321 }
322 let i = sim_y_m
323 .partition_point(|&v| v <= y)
324 .clamp(1, sim_y_m.len() - 1);
325 let t = (y - sim_y_m[i - 1]) / (sim_y_m[i] - sim_y_m[i - 1]);
326 let s = sim_depth_m[i - 1] + t * (sim_depth_m[i] - sim_depth_m[i - 1]);
327 points.push(PointDifference {
328 y_m: y,
329 published_m: d,
330 simulated_m: s,
331 });
332 }
333 let n = points.len() as f64;
334 let rms = (points
335 .iter()
336 .map(|p| (p.simulated_m - p.published_m).powi(2))
337 .sum::<f64>()
338 / n)
339 .sqrt();
340 let max_abs = points
341 .iter()
342 .map(|p| (p.simulated_m - p.published_m).abs())
343 .fold(0.0, f64::max);
344 let scale = points
345 .iter()
346 .map(|p| p.published_m.abs())
347 .fold(0.0, f64::max);
348 if scale == 0.0 {
349 return Err("the largest published depth is zero".into());
350 }
351 let relative_rms = rms / scale;
352 Ok(ProfileComparison {
353 points,
354 rms_m: rms,
355 max_abs_m: max_abs,
356 relative_rms,
357 tolerance,
358 within_tolerance: relative_rms <= tolerance,
359 })
360}