Skip to main content

lindhard/tally/
ion.rs

1//! The full ion tally: ranges, damage and escapes, wired to the BCA engine.
2//!
3//! [`IonTally`] implements [`BcaTally`]. Build one prototype with
4//! [`IonTally::new`] and hand clones to [`crate::ion::bca::Bca::run`]; the
5//! merged tally turns into a plain-data [`IonReport`] with
6//! [`IonTally::report`].
7//!
8//! Units: positions and lengths in metres, energies in eV, angles in radians.
9//! Totals are summed over all histories; divide by
10//! [`IonReport::histories`] for per-ion values (the escape coefficients are
11//! already per ion).
12//!
13//! # Determinism
14//!
15//! Counts are integers, moments use the mergeable accumulator of
16//! [`super::moments`], and energy sums are plain `f64` sums; all are merged
17//! field by field in the order [`crate::rng::run_particles`] calls `merge`
18//! (chunk order). For a fixed `chunk_size` the result is bit-identical at any
19//! thread count (`lindhard/tests/tally.rs`). Nothing is stored in a hash map.
20
21use serde::{Deserialize, Serialize};
22
23use super::hist::{Binning, Histogram};
24use super::moments::{MomentSummary, Moments};
25use super::pearson::{DualPearson, DualPearsonFit, PearsonError, PearsonIv};
26use crate::geometry::Stack;
27use crate::ion::bca::{BcaTally, EnergyBudget, Face, LatticeDeposit, Particle};
28use crate::ion::damage::{damage_energy_ev, kinchin_pease, nrt_displacements};
29use crate::material::MaterialError;
30
31/// Binnings for an [`IonTally`].
32#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
33pub struct IonTallyConfig {
34    /// Depth `x`, m: range histograms (overall and per layer) and the
35    /// defect depth histograms.
36    pub depth: Binning,
37    /// Lateral position `y` and `z`, m, of stopped primaries.
38    pub lateral: Binning,
39    /// Radial distance `(y^2 + z^2)^(1/2)`, m, of stopped primaries.
40    pub radial: Binning,
41    /// Kinetic energy outside the target of escaping particles, eV.
42    pub escape_energy: Binning,
43    /// Polar angle of escaping particles from the outward surface normal,
44    /// rad (`0` is along the normal, `π/2` grazing).
45    pub escape_polar: Binning,
46}
47
48/// Displacement-model estimates (NRT and Kinchin-Pease) from the PKA damage
49/// energies. **Not** a count of simulated defects: see [`CascadeDefects`]
50/// and [`crate::ion::damage`] for why the two are kept apart.
51///
52/// In a layer with more than one element, the Lindhard partition uses the
53/// PKA's own `Z1`/`A1` and the layer's atom-fraction mean `Z2`/`A2`. NRT is
54/// defined for monatomic targets, so this is an approximation and this
55/// crate's own convention: see
56/// [the compound-target section](crate::ion::damage#compound-and-multi-element-targets-an-approximation)
57/// of [`crate::ion::damage`].
58#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
59pub struct NrtDamage {
60    /// Primary knock-on atoms: target atoms displaced by the beam particle.
61    pub pka_count: u64,
62    /// Sum of the PKA energies `T` (energy transferred, before the lattice
63    /// binding is subtracted), eV.
64    pub pka_energy_ev: f64,
65    /// Sum of the PKA damage energies `T_dam` (Lindhard partition), eV.
66    pub damage_energy_ev: f64,
67    /// Sum of the NRT displacements `N_NRT(T_dam)` over PKAs.
68    pub nrt_displacements: f64,
69    /// Sum of the Kinchin-Pease displacements `N_KP(T_dam)` over PKAs.
70    pub kinchin_pease_displacements: f64,
71}
72
73impl NrtDamage {
74    fn merge(&mut self, o: &NrtDamage) {
75        self.pka_count += o.pka_count;
76        self.pka_energy_ev += o.pka_energy_ev;
77        self.damage_energy_ev += o.damage_energy_ev;
78        self.nrt_displacements += o.nrt_displacements;
79        self.kinchin_pease_displacements += o.kinchin_pease_displacements;
80    }
81}
82
83/// Defects counted event by event in the simulated cascades.
84///
85/// * `displacements`: target atoms given more than `E_d` (recoils created).
86/// * `replacements`: a moving atom came to rest, immediately after the
87///   collision in which it displaced an atom of its own element, at that
88///   atom's site, so it fills the site. In this engine a particle stops when
89///   its energy falls below its cutoff, so this count depends on the cutoffs
90///   (a recoil cutoff near `E_d` gives the most replacements). When a recoil
91///   is not followed (below its cutoff, or with
92///   [`BcaConfig::follow_recoils`](crate::ion::bca::BcaConfig::follow_recoils)
93///   `= false`), the engine reports `stopped(&r)` straight after
94///   `recoil(&r)`, which clears the pending site, so the displacer cannot
95///   score a replacement in that collision. With `follow_recoils = false` the
96///   replacement count is therefore always 0.
97/// * `vacancies = displacements - replacements`.
98/// * `interstitials`: recoils that came to rest in the target and did not
99///   fill a site. Implanted beam particles are not counted here (see the
100///   range tally). Recoils that leave the target create a vacancy and no
101///   interstitial, so the two counts differ.
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
103pub struct CascadeDefects {
104    /// Atoms displaced.
105    pub displacements: u64,
106    /// Replacement events.
107    pub replacements: u64,
108    /// Vacancies left.
109    pub vacancies: u64,
110    /// Interstitials left.
111    pub interstitials: u64,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Default)]
115struct DefectCounts {
116    displacements: u64,
117    replacements: u64,
118    interstitials: u64,
119}
120
121impl DefectCounts {
122    fn merge(&mut self, o: &DefectCounts) {
123        self.displacements += o.displacements;
124        self.replacements += o.replacements;
125        self.interstitials += o.interstitials;
126    }
127
128    fn report(&self) -> CascadeDefects {
129        CascadeDefects {
130            displacements: self.displacements,
131            replacements: self.replacements,
132            vacancies: self.displacements - self.replacements,
133            interstitials: self.interstitials,
134        }
135    }
136}
137
138/// Per-layer constants for the damage models (not merged).
139#[derive(Debug, Clone, PartialEq)]
140struct LayerConsts {
141    /// `(Z, E_d, E_b)` of each element, eV.
142    elems: Vec<(u8, f64, f64)>,
143    /// Atom-fraction mean atomic number.
144    mean_z: f64,
145    /// Mean atomic weight, u.
146    mean_a: f64,
147}
148
149#[derive(Debug, Clone, PartialEq)]
150struct LayerAcc {
151    stopped: Moments,
152    depth_hist: Histogram,
153    nrt: NrtDamage,
154    defects: DefectCounts,
155}
156
157#[derive(Debug, Clone, PartialEq)]
158struct FaceAcc {
159    count: u64,
160    energy_ev: f64,
161    energy_hist: Histogram,
162    polar_hist: Histogram,
163}
164
165impl FaceAcc {
166    fn new(c: &IonTallyConfig) -> Self {
167        Self {
168            count: 0,
169            energy_ev: 0.0,
170            energy_hist: Histogram::new(c.escape_energy),
171            polar_hist: Histogram::new(c.escape_polar),
172        }
173    }
174
175    fn merge(&mut self, o: &FaceAcc) {
176        self.count += o.count;
177        self.energy_ev += o.energy_ev;
178        self.energy_hist.merge(&o.energy_hist);
179        self.polar_hist.merge(&o.polar_hist);
180    }
181
182    fn report(&self, histories: u64) -> FaceEscape {
183        FaceEscape {
184            count: self.count,
185            per_ion: per(self.count as f64, histories),
186            energy_ev: self.energy_ev,
187            mean_energy_ev: if self.count > 0 {
188                self.energy_ev / self.count as f64
189            } else {
190                0.0
191            },
192            energy_histogram: self.energy_hist.clone(),
193            polar_histogram: self.polar_hist.clone(),
194        }
195    }
196}
197
198fn per(x: f64, histories: u64) -> f64 {
199    if histories == 0 {
200        0.0
201    } else {
202        x / histories as f64
203    }
204}
205
206/// The site of the most recent displacement, for replacement detection.
207#[derive(Debug, Clone, Copy, PartialEq)]
208struct Site {
209    pos: [f64; 3],
210    z: u8,
211    species: usize,
212    displacer_generation: u32,
213}
214
215/// The replacement rule of [`CascadeDefects`], shared with the per-element
216/// voxel tally ([`crate::tally::VoxelTally`]) so both count the same events.
217///
218/// Feed it the engine's hooks: [`Self::recoil`] on every `recoil`,
219/// [`Self::clear`] on `begin_history`, `escaped`, `end_history` and on a
220/// `lattice` deposit of kind [`LatticeDeposit::Subthreshold`], and
221/// [`Self::stopped`] on every `stopped`.
222#[derive(Debug, Clone, Copy, PartialEq, Default)]
223pub(crate) struct ReplacementTracker {
224    site: Option<Site>,
225}
226
227impl ReplacementTracker {
228    /// Forget the pending displacement site.
229    pub(crate) fn clear(&mut self) {
230        self.site = None;
231    }
232
233    /// A target atom was displaced: remember its site.
234    pub(crate) fn recoil(&mut self, r: &Particle) {
235        self.site = Some(Site {
236            pos: r.pos,
237            z: r.z,
238            species: r.species,
239            displacer_generation: r.generation.saturating_sub(1),
240        });
241    }
242
243    /// A particle stopped. If it fills the pending site, returns the species
244    /// of the displaced atom; either way the pending site is cleared.
245    pub(crate) fn stopped(&mut self, p: &Particle) -> Option<usize> {
246        let hit = match self.site {
247            Some(s) if s.pos == p.pos && s.z == p.z && s.displacer_generation == p.generation => {
248                Some(s.species)
249            }
250            _ => None,
251        };
252        self.site = None;
253        hit
254    }
255}
256
257/// Range, damage and escape tally for the BCA engine. See the module docs.
258#[derive(Debug, Clone, PartialEq)]
259pub struct IonTally {
260    config: IonTallyConfig,
261    species_z: Vec<u8>,
262    consts: Vec<LayerConsts>,
263    histories: u64,
264    budget: EnergyBudget,
265    depth: Moments,
266    lateral_y: Moments,
267    lateral_z: Moments,
268    radial: Moments,
269    depth_hist: Histogram,
270    lateral_y_hist: Histogram,
271    lateral_z_hist: Histogram,
272    radial_hist: Histogram,
273    layers: Vec<LayerAcc>,
274    displacement_hist: Histogram,
275    replacement_hist: Histogram,
276    interstitial_hist: Histogram,
277    /// `[species] -> (front, back)`.
278    escapes: Vec<(FaceAcc, FaceAcc)>,
279    /// Per-history scratch: not part of the result, reset every history.
280    last_site: ReplacementTracker,
281}
282
283impl IonTally {
284    /// Empty tally for `stack`, with the particle species of the engine
285    /// (`species_z`, as returned by [`crate::ion::bca::Bca::species_z`]:
286    /// index 0 is the beam).
287    ///
288    /// Every element of every layer must have `E_d` and `E_b` set (the engine
289    /// requires the same).
290    pub fn new(
291        stack: &Stack,
292        species_z: &[u8],
293        config: IonTallyConfig,
294    ) -> Result<Self, MaterialError> {
295        let mut consts = Vec::with_capacity(stack.layers().len());
296        for l in stack.layers() {
297            let m = l.material();
298            let mut elems = Vec::new();
299            let mut mean_z = 0.0;
300            for (z, x) in m.atom_fractions() {
301                elems.push((
302                    z,
303                    m.displacement_energy_ev(z)?,
304                    m.lattice_binding_energy_ev(z)?,
305                ));
306                mean_z += x * f64::from(z);
307            }
308            consts.push(LayerConsts {
309                elems,
310                mean_z,
311                mean_a: m.mean_atomic_weight(),
312            });
313        }
314        let layers = (0..consts.len())
315            .map(|_| LayerAcc {
316                stopped: Moments::new(),
317                depth_hist: Histogram::new(config.depth),
318                nrt: NrtDamage::default(),
319                defects: DefectCounts::default(),
320            })
321            .collect();
322        Ok(Self {
323            config,
324            species_z: species_z.to_vec(),
325            consts,
326            histories: 0,
327            budget: EnergyBudget::default(),
328            depth: Moments::new(),
329            lateral_y: Moments::new(),
330            lateral_z: Moments::new(),
331            radial: Moments::new(),
332            depth_hist: Histogram::new(config.depth),
333            lateral_y_hist: Histogram::new(config.lateral),
334            lateral_z_hist: Histogram::new(config.lateral),
335            radial_hist: Histogram::new(config.radial),
336            layers,
337            displacement_hist: Histogram::new(config.depth),
338            replacement_hist: Histogram::new(config.depth),
339            interstitial_hist: Histogram::new(config.depth),
340            escapes: species_z
341                .iter()
342                .map(|_| (FaceAcc::new(&config), FaceAcc::new(&config)))
343                .collect(),
344            last_site: ReplacementTracker::default(),
345        })
346    }
347
348    /// Histories tallied so far.
349    pub fn histories(&self) -> u64 {
350        self.histories
351    }
352
353    /// `E_d` and `E_b` of element `z` in layer `layer`.
354    fn energies(&self, layer: usize, z: u8) -> (f64, f64) {
355        self.consts[layer]
356            .elems
357            .iter()
358            .find(|e| e.0 == z)
359            .map(|e| (e.1, e.2))
360            .expect("recoils are elements of their layer")
361    }
362
363    /// The plain-data result. With `fit_dual_pearson` the depth histogram is
364    /// also fitted with [`DualPearson::fit`] (slower: a few thousand
365    /// evaluations over the histogram).
366    pub fn report(&self, fit_dual_pearson: bool) -> IonReport {
367        let h = self.histories;
368        let depth = self.depth.summary();
369        let (pearson_iv, pearson_iv_error) = match depth {
370            None => (None, None),
371            Some(s) => match PearsonIv::from_moments(s.mean, s.std_dev, s.skewness, s.kurtosis) {
372                Ok(p) => (Some(p), None),
373                Err(e) => (None, Some(e)),
374            },
375        };
376        let (dual_pearson, dual_pearson_error) = if fit_dual_pearson {
377            match DualPearson::fit(&self.depth_hist) {
378                Ok(f) => (Some(f), None),
379                Err(e) => (None, Some(e)),
380            }
381        } else {
382            (None, None)
383        };
384        let range = RangeReport {
385            stopped: self.depth.n,
386            depth,
387            pearson_iv,
388            pearson_iv_error,
389            dual_pearson,
390            dual_pearson_error,
391            depth_histogram: self.depth_hist.clone(),
392            lateral_y: self.lateral_y.summary(),
393            lateral_z: self.lateral_z.summary(),
394            radial: self.radial.summary(),
395            lateral_y_histogram: self.lateral_y_hist.clone(),
396            lateral_z_histogram: self.lateral_z_hist.clone(),
397            radial_histogram: self.radial_hist.clone(),
398            layers: self
399                .layers
400                .iter()
401                .enumerate()
402                .map(|(i, l)| LayerRange {
403                    layer: i,
404                    stopped: l.stopped.n,
405                    depth: l.stopped.summary(),
406                    depth_histogram: l.depth_hist.clone(),
407                })
408                .collect(),
409        };
410        let mut nrt = NrtDamage::default();
411        let mut defects = DefectCounts::default();
412        for l in &self.layers {
413            nrt.merge(&l.nrt);
414            defects.merge(&l.defects);
415        }
416        let mut vacancy_hist = self.displacement_hist.clone();
417        for (v, r) in vacancy_hist
418            .counts
419            .iter_mut()
420            .zip(&self.replacement_hist.counts)
421        {
422            *v -= r;
423        }
424        vacancy_hist.underflow -= self.replacement_hist.underflow;
425        vacancy_hist.overflow -= self.replacement_hist.overflow;
426        let damage = DamageReport {
427            nrt,
428            cascade: defects.report(),
429            layers: self
430                .layers
431                .iter()
432                .enumerate()
433                .map(|(i, l)| LayerDamage {
434                    layer: i,
435                    nrt: l.nrt,
436                    cascade: l.defects.report(),
437                })
438                .collect(),
439            vacancy_histogram: vacancy_hist,
440            interstitial_histogram: self.interstitial_hist.clone(),
441            replacement_histogram: self.replacement_hist.clone(),
442        };
443        let species: Vec<SpeciesEscape> = self
444            .escapes
445            .iter()
446            .enumerate()
447            .map(|(i, (f, b))| SpeciesEscape {
448                species: i,
449                z: self.species_z[i],
450                beam: i == 0,
451                front: f.report(h),
452                back: b.report(h),
453            })
454            .collect();
455        let sputtered: u64 = self.escapes.iter().skip(1).map(|e| e.0.count).sum();
456        let escapes = EscapeReport {
457            backscatter_coefficient: per(self.escapes[0].0.count as f64, h),
458            transmission_coefficient: per(self.escapes[0].1.count as f64, h),
459            energy_reflection_coefficient: if self.budget.incident > 0.0 {
460                self.budget.backscattered / self.budget.incident
461            } else {
462                0.0
463            },
464            sputter_yield: per(sputtered as f64, h),
465            species,
466        };
467        IonReport {
468            histories: h,
469            species_z: self.species_z.clone(),
470            budget: self.budget,
471            range,
472            damage,
473            escapes,
474        }
475    }
476}
477
478impl BcaTally for IonTally {
479    fn begin_history(&mut self, _index: u64) {
480        self.last_site.clear();
481    }
482
483    fn lattice(&mut self, _at: [f64; 3], _layer: usize, kind: LatticeDeposit, _energy_ev: f64) {
484        if kind == LatticeDeposit::Subthreshold {
485            self.last_site.clear();
486        }
487    }
488
489    fn recoil(&mut self, r: &Particle) {
490        let layer = r.layer;
491        let (e_d, e_b) = self.energies(layer, r.z);
492        let acc = &mut self.layers[layer];
493        acc.defects.displacements += 1;
494        self.displacement_hist.fill(r.pos[0]);
495        if r.generation == 1 {
496            // A PKA: the transfer T is the recoil energy plus the binding
497            // energy the engine left in the lattice.
498            let c = &self.consts[layer];
499            let t = r.energy_ev + e_b;
500            let t_dam = damage_energy_ev(t, f64::from(r.z), r.mass_amu, c.mean_z, c.mean_a);
501            acc.nrt.pka_count += 1;
502            acc.nrt.pka_energy_ev += t;
503            acc.nrt.damage_energy_ev += t_dam;
504            acc.nrt.nrt_displacements += nrt_displacements(t_dam, e_d);
505            acc.nrt.kinchin_pease_displacements += kinchin_pease(t_dam, e_d);
506        }
507        self.last_site.recoil(r);
508    }
509
510    fn stopped(&mut self, p: &Particle) {
511        let replaced = self.last_site.stopped(p).is_some();
512        let x = p.pos[0];
513        if replaced {
514            self.layers[p.layer].defects.replacements += 1;
515            self.replacement_hist.fill(x);
516        } else if !p.is_primary() {
517            self.layers[p.layer].defects.interstitials += 1;
518            self.interstitial_hist.fill(x);
519        }
520        if p.is_primary() {
521            let (y, z) = (p.pos[1], p.pos[2]);
522            let r = y.hypot(z);
523            self.depth.push(x);
524            self.lateral_y.push(y);
525            self.lateral_z.push(z);
526            self.radial.push(r);
527            self.depth_hist.fill(x);
528            self.lateral_y_hist.fill(y);
529            self.lateral_z_hist.fill(z);
530            self.radial_hist.fill(r);
531            let l = &mut self.layers[p.layer];
532            l.stopped.push(x);
533            l.depth_hist.fill(x);
534        }
535    }
536
537    fn escaped(&mut self, p: &Particle, face: Face) {
538        self.last_site.clear();
539        let polar = p.dir[0].abs().min(1.0).acos();
540        let pair = &mut self.escapes[p.species];
541        let acc = match face {
542            Face::Front => &mut pair.0,
543            Face::Back => &mut pair.1,
544            // Lateral faces exist only on voxel grids, which this stack
545            // tally does not describe.
546            Face::Side => return,
547        };
548        acc.count += 1;
549        acc.energy_ev += p.energy_ev;
550        acc.energy_hist.fill(p.energy_ev);
551        acc.polar_hist.fill(polar);
552    }
553
554    fn end_history(&mut self, _index: u64, budget: &EnergyBudget) {
555        self.histories += 1;
556        self.budget.add(budget);
557        self.last_site.clear();
558    }
559
560    /// Field-wise merge.
561    ///
562    /// # Panics
563    /// If `other` was built with a different stack, species list or
564    /// configuration.
565    fn merge(&mut self, o: Self) {
566        assert!(
567            self.config == o.config && self.species_z == o.species_z && self.consts == o.consts,
568            "merging ion tallies built for different runs"
569        );
570        self.histories += o.histories;
571        self.budget.add(&o.budget);
572        self.depth.merge(&o.depth);
573        self.lateral_y.merge(&o.lateral_y);
574        self.lateral_z.merge(&o.lateral_z);
575        self.radial.merge(&o.radial);
576        self.depth_hist.merge(&o.depth_hist);
577        self.lateral_y_hist.merge(&o.lateral_y_hist);
578        self.lateral_z_hist.merge(&o.lateral_z_hist);
579        self.radial_hist.merge(&o.radial_hist);
580        for (a, b) in self.layers.iter_mut().zip(&o.layers) {
581            a.stopped.merge(&b.stopped);
582            a.depth_hist.merge(&b.depth_hist);
583            a.nrt.merge(&b.nrt);
584            a.defects.merge(&b.defects);
585        }
586        self.displacement_hist.merge(&o.displacement_hist);
587        self.replacement_hist.merge(&o.replacement_hist);
588        self.interstitial_hist.merge(&o.interstitial_hist);
589        for (a, b) in self.escapes.iter_mut().zip(&o.escapes) {
590            a.0.merge(&b.0);
591            a.1.merge(&b.1);
592        }
593    }
594}
595
596/// Everything an [`IonTally`] measured, as plain data.
597#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
598pub struct IonReport {
599    /// Primary histories.
600    pub histories: u64,
601    /// Atomic number by species index (0 is the beam).
602    pub species_z: Vec<u8>,
603    /// Summed energy budget of all histories, eV.
604    pub budget: EnergyBudget,
605    /// Range distributions of the stopped beam particles.
606    pub range: RangeReport,
607    /// Damage: model estimates and cascade counts, side by side.
608    pub damage: DamageReport,
609    /// Backscattering, transmission and sputtering.
610    pub escapes: EscapeReport,
611}
612
613/// Range distributions of beam particles that came to rest in the target.
614#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
615pub struct RangeReport {
616    /// Beam particles at rest in the target.
617    pub stopped: u64,
618    /// Depth moments (`Rp`, `ΔRp`, `γ`, `β`), m. `None` with fewer than two
619    /// stopped particles.
620    pub depth: Option<MomentSummary>,
621    /// Pearson IV with the depth moments, if they are in the type IV region.
622    pub pearson_iv: Option<PearsonIv>,
623    /// Why `pearson_iv` is `None`, when there were moments to try.
624    pub pearson_iv_error: Option<PearsonError>,
625    /// Dual-Pearson fit to the depth histogram, if requested and possible.
626    pub dual_pearson: Option<DualPearsonFit>,
627    /// Why the requested dual-Pearson fit failed.
628    pub dual_pearson_error: Option<PearsonError>,
629    /// Depth histogram, m.
630    pub depth_histogram: Histogram,
631    /// Moments of the lateral coordinate `y`, m.
632    pub lateral_y: Option<MomentSummary>,
633    /// Moments of the lateral coordinate `z`, m.
634    pub lateral_z: Option<MomentSummary>,
635    /// Moments of the radial distance from the beam axis through the entry
636    /// point, m.
637    pub radial: Option<MomentSummary>,
638    /// Histogram of `y`, m.
639    pub lateral_y_histogram: Histogram,
640    /// Histogram of `z`, m.
641    pub lateral_z_histogram: Histogram,
642    /// Histogram of the radial distance, m.
643    pub radial_histogram: Histogram,
644    /// The same depth statistics per layer (by where the particle stopped).
645    pub layers: Vec<LayerRange>,
646}
647
648/// Depth statistics of the beam particles that stopped in one layer.
649#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
650pub struct LayerRange {
651    /// Layer index (0 is the front layer).
652    pub layer: usize,
653    /// Beam particles at rest in this layer.
654    pub stopped: u64,
655    /// Their depth moments, m.
656    pub depth: Option<MomentSummary>,
657    /// Their depth histogram, m (overall binning).
658    pub depth_histogram: Histogram,
659}
660
661/// Damage, as two separate kinds of result. See [`crate::ion::damage`].
662#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
663pub struct DamageReport {
664    /// NRT / Kinchin-Pease model estimates, all layers.
665    pub nrt: NrtDamage,
666    /// Simulated cascade defect counts, all layers.
667    pub cascade: CascadeDefects,
668    /// Both, per layer (by the layer of the event).
669    pub layers: Vec<LayerDamage>,
670    /// Depth histogram of vacancies, m.
671    pub vacancy_histogram: Histogram,
672    /// Depth histogram of interstitials, m.
673    pub interstitial_histogram: Histogram,
674    /// Depth histogram of replacements, m.
675    pub replacement_histogram: Histogram,
676}
677
678/// Damage in one layer.
679#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
680pub struct LayerDamage {
681    /// Layer index.
682    pub layer: usize,
683    /// Model estimates for PKAs created in this layer.
684    pub nrt: NrtDamage,
685    /// Cascade counts for events in this layer.
686    pub cascade: CascadeDefects,
687}
688
689/// Particles leaving the target.
690#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
691pub struct EscapeReport {
692    /// Beam particles leaving the front face, per incident ion.
693    pub backscatter_coefficient: f64,
694    /// Beam particles leaving the back face, per incident ion.
695    pub transmission_coefficient: f64,
696    /// Energy carried out of the front face by beam particles, as a fraction
697    /// of the incident energy.
698    pub energy_reflection_coefficient: f64,
699    /// Target atoms leaving the front face, all species, per incident ion.
700    pub sputter_yield: f64,
701    /// Per species (index 0 is the beam).
702    pub species: Vec<SpeciesEscape>,
703}
704
705/// Escapes of one species through each face.
706#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
707pub struct SpeciesEscape {
708    /// Species index.
709    pub species: usize,
710    /// Atomic number.
711    pub z: u8,
712    /// True for the beam species (front escapes are backscattering), false
713    /// for a target element (front escapes are sputtering).
714    pub beam: bool,
715    /// Through the front face (`x = 0`).
716    pub front: FaceEscape,
717    /// Through the back face of a finite stack.
718    pub back: FaceEscape,
719}
720
721/// Count and spectra of particles leaving through one face.
722#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
723pub struct FaceEscape {
724    /// Particles.
725    pub count: u64,
726    /// Particles per incident ion (backscatter or transmission coefficient,
727    /// or partial sputter yield).
728    pub per_ion: f64,
729    /// Sum of their energies outside the target, eV.
730    pub energy_ev: f64,
731    /// Mean energy outside the target, eV (0 if none escaped).
732    pub mean_energy_ev: f64,
733    /// Energy spectrum, eV.
734    pub energy_histogram: Histogram,
735    /// Polar angle from the outward normal, rad.
736    pub polar_histogram: Histogram,
737}