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}