Skip to main content

lindhard/ion/dynamic/
adapter.rs

1//! The tally-to-delta adapter: turns the transport events of one fluence step
2//! into per-slab, per-element atom **counts**, which the driver scales to
3//! areal inventory deltas.
4//!
5//! The conventions are those of the [parent module](super) ("Conservation and
6//! update semantics"):
7//!
8//! * a recoil is subtracted from the slab it is created in, at the
9//!   [`BcaTally::recoil`] event, and added to the slab where it comes to rest
10//!   at the [`BcaTally::stopped`] event. A recoil that escapes therefore
11//!   shows up as a loss with no further bookkeeping, and the origin slab of
12//!   an escaped atom never has to be recovered from the (unlabelled) event
13//!   sequence, which is what the aggregate exit totals could not give;
14//! * a primary that comes to rest in a slab adds one atom of its species
15//!   there; a primary that escapes adds nothing;
16//! * the substrate is an immutable reservoir: an atom displaced from it
17//!   removes nothing, and any atom that comes to rest in it adds nothing (the
18//!   substrate is not part of the inventory).
19//!
20//! In addition the tally records, per `(origin slab, Z)`, how many of the
21//! recoils that left through the front face were created in that slab
22//! ([`InventoryTally::sputtered_from`]), using `Particle::origin_layer`. The
23//! default conventions above already subtract those atoms at their origin;
24//! the driver's erosion mode uses the count to take them from the front of
25//! the target instead (module docs of [`DynamicRun`](super::DynamicRun)).
26//!
27//! Layer indices of the stack built by
28//! [`CompositionGrid::to_stack`](super::CompositionGrid::to_stack) are the slab
29//! indices of the grid (the substrate, if any, is the one layer after the last
30//! slab), so the adapter uses `Particle::layer` as the slab index directly.
31//!
32//! Counts are integers, so the merged result does not depend on the order in
33//! which chunk tallies are folded, and the driver additionally merges them in
34//! chunk order.
35
36use std::collections::BTreeMap;
37
38use crate::ion::bca::{BcaTally, Face, Particle};
39
40/// Event counts of a block of primaries (one fluence step, or a sum of steps).
41///
42/// All counts are exact integers.
43#[derive(Debug, Clone, Default, PartialEq, Eq)]
44pub struct Yields {
45    /// Primary histories run.
46    pub histories: u64,
47    /// Displaced target atoms (recoils created).
48    pub displaced: u64,
49    /// Primaries that came to rest in a finite slab.
50    pub primaries_in_slabs: u64,
51    /// Primaries that came to rest in the substrate.
52    pub primaries_in_substrate: u64,
53    /// Primaries that left through the front face.
54    pub backscattered: u64,
55    /// Primaries that left through the back face.
56    pub transmitted: u64,
57    /// Recoils that left through the front face (sputtered atoms), by atomic
58    /// number.
59    pub sputtered: BTreeMap<u8, u64>,
60    /// Recoils that left through the back face.
61    pub recoils_transmitted: u64,
62}
63
64impl Yields {
65    /// Total sputtered atoms (all elements).
66    pub fn sputtered_total(&self) -> u64 {
67        self.sputtered.values().sum()
68    }
69
70    /// Add `other` field by field.
71    pub fn add(&mut self, other: &Yields) {
72        self.histories += other.histories;
73        self.displaced += other.displaced;
74        self.primaries_in_slabs += other.primaries_in_slabs;
75        self.primaries_in_substrate += other.primaries_in_substrate;
76        self.backscattered += other.backscattered;
77        self.transmitted += other.transmitted;
78        for (&z, &n) in &other.sputtered {
79            *self.sputtered.entry(z).or_insert(0) += n;
80        }
81        self.recoils_transmitted += other.recoils_transmitted;
82    }
83}
84
85/// A [`BcaTally`] that records the net atom count change per `(slab, Z)` and
86/// the [`Yields`] of a step.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct InventoryTally {
89    n_slabs: usize,
90    counts: BTreeMap<(usize, u8), i64>,
91    sputtered_from: BTreeMap<(usize, u8), u64>,
92    yields: Yields,
93}
94
95impl InventoryTally {
96    /// An empty tally for a grid of `n_slabs` finite slabs (layer indices at
97    /// or above `n_slabs` are the substrate).
98    pub fn new(n_slabs: usize) -> Self {
99        Self {
100            n_slabs,
101            counts: BTreeMap::new(),
102            sputtered_from: BTreeMap::new(),
103            yields: Yields::default(),
104        }
105    }
106
107    /// Net atom count change per `(slab, Z)`, in `(slab, Z)` order. Zero
108    /// entries are omitted.
109    pub fn counts(&self) -> impl Iterator<Item = ((usize, u8), i64)> + '_ {
110        self.counts
111            .iter()
112            .filter(|(_, &n)| n != 0)
113            .map(|(&k, &n)| (k, n))
114    }
115
116    /// Recoils that left through the front face, per `(origin slab, Z)`, in
117    /// that order. Only origins in a finite slab are listed (an atom from the
118    /// substrate was never part of the inventory), so for each `Z` the sum is
119    /// at most the `Z` entry of [`Yields::sputtered`]. Each of these atoms is
120    /// also in [`counts`](Self::counts) as a loss in its origin slab.
121    pub fn sputtered_from(&self) -> impl Iterator<Item = ((usize, u8), u64)> + '_ {
122        self.sputtered_from.iter().map(|(&k, &n)| (k, n))
123    }
124
125    /// The event counts.
126    pub fn yields(&self) -> &Yields {
127        &self.yields
128    }
129
130    fn add(&mut self, layer: usize, z: u8, n: i64) {
131        if layer < self.n_slabs {
132            *self.counts.entry((layer, z)).or_insert(0) += n;
133        }
134    }
135}
136
137impl BcaTally for InventoryTally {
138    fn begin_history(&mut self, _index: u64) {
139        self.yields.histories += 1;
140    }
141
142    fn recoil(&mut self, recoil: &Particle) {
143        self.yields.displaced += 1;
144        self.add(recoil.layer, recoil.z, -1);
145    }
146
147    fn stopped(&mut self, p: &Particle) {
148        self.add(p.layer, p.z, 1);
149        if p.is_primary() {
150            if p.layer < self.n_slabs {
151                self.yields.primaries_in_slabs += 1;
152            } else {
153                self.yields.primaries_in_substrate += 1;
154            }
155        }
156    }
157
158    fn escaped(&mut self, p: &Particle, face: Face) {
159        match (p.is_primary(), face) {
160            (true, Face::Front) => self.yields.backscattered += 1,
161            (true, Face::Back) => self.yields.transmitted += 1,
162            (false, Face::Front) => {
163                *self.yields.sputtered.entry(p.z).or_insert(0) += 1;
164                if p.origin_layer < self.n_slabs {
165                    *self
166                        .sputtered_from
167                        .entry((p.origin_layer, p.z))
168                        .or_insert(0) += 1;
169                }
170            }
171            (false, Face::Back) => self.yields.recoils_transmitted += 1,
172            // A stack has no lateral faces.
173            (_, Face::Side) => {}
174        }
175    }
176
177    fn merge(&mut self, other: Self) {
178        for (k, n) in other.counts {
179            *self.counts.entry(k).or_insert(0) += n;
180        }
181        for (k, n) in other.sputtered_from {
182            *self.sputtered_from.entry(k).or_insert(0) += n;
183        }
184        self.yields.add(&other.yields);
185    }
186}