Skip to main content

lindhard/tally/
trajectory.rs

1//! Opt-in recorder of ion paths: the collision points of chosen histories.
2//!
3//! [`TrajectoryRecorder`] is a [`BcaTally`] like any other, so the engine pays
4//! nothing for it unless a run uses it: no field on [`Particle`], no flag and
5//! no branch in the transport loop. [`Recorded`] forwards every hook to an
6//! ordinary tally as well, so the usual results come out of the same run and
7//! are identical with recording on or off (the recorder draws no random
8//! numbers and never touches the engine state).
9//!
10//! # Convention
11//!
12//! A history is a list of *tracks*: the primary (generation 0) and every
13//! displaced atom (a recoil, generation = parent + 1). A track is an ordered
14//! list of points, positions in metres (`x` the depth) and energies in eV:
15//!
16//! * a recoil starts with its state at creation;
17//! * every collision partner adds the particle's **incoming** state at the
18//!   collision (the [`BcaTally::partner`] convention). A crystal step with
19//!   several simultaneous partners adds one point. The primary's own track
20//!   therefore starts at its first collision, not at the surface;
21//! * a track that stops or leaves the target ends with that state (for an
22//!   escape, the state outside after refraction by the surface barrier), the
23//!   same position the other tallies see. A recoil that is created below the
24//!   recoil cutoff, or with cascades off, is stopped at creation and has that
25//!   one point.
26//!
27//! Each non-primary track names its `parent` track in the same history. The
28//! engine reports no track-start event; the recorder rebuilds the link by
29//! mirroring the engine's last-in first-out queue of recoils: a recoil
30//! reported and then stopped with the identical state was never queued, any
31//! other is followed after its parent ends, the most recent first.
32//!
33//! # Bounds
34//!
35//! Memory is bounded by two caps. Only histories with index below
36//! [`TrajectoryConfig::max_histories`] are recorded, so the lowest indices win
37//! whatever the thread count; the others are counted in
38//! [`TrajectoryRecorder::histories_skipped`]. A history keeps at most
39//! [`TrajectoryConfig::max_points`] points over all its tracks; further points
40//! (and the tracks of further recoils) are dropped and the history is flagged
41//! [`History::truncated`]. Histories are merged in chunk order, so the output
42//! is bit-identical at any thread count.
43
44use crate::ion::bca::{BcaTally, ElectronicChannel, EnergyBudget, Face, LatticeDeposit, Particle};
45
46/// Caps on what a [`TrajectoryRecorder`] keeps.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct TrajectoryConfig {
49    /// Record histories with index `< max_histories`.
50    pub max_histories: u64,
51    /// Keep at most this many points per history, over all its tracks.
52    pub max_points: usize,
53}
54
55/// One point of a track.
56#[derive(Debug, Clone, Copy, PartialEq)]
57pub struct TrajectoryPoint {
58    /// Position `[x, y, z]`, m.
59    pub pos: [f64; 3],
60    /// Kinetic energy, eV.
61    pub energy_ev: f64,
62}
63
64/// How a track ended.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum TrackEnd {
67    /// Came to rest in the target.
68    Stopped,
69    /// Left the target through a face.
70    Escaped(Face),
71}
72
73/// The path of one particle.
74#[derive(Debug, Clone, PartialEq)]
75pub struct Track {
76    /// Species index, as [`Particle::species`].
77    pub species: usize,
78    /// 0 for the primary, parent's generation + 1 for a recoil.
79    pub generation: u32,
80    /// Index in [`History::tracks`] of the track that displaced this atom;
81    /// `None` for the primary.
82    pub parent: Option<usize>,
83    /// How the track ended; `None` if the end was dropped by the point cap.
84    pub end: Option<TrackEnd>,
85    /// The points, in order of occurrence.
86    pub points: Vec<TrajectoryPoint>,
87}
88
89/// The recorded tracks of one primary history. `tracks[0]` is the primary;
90/// every `parent` index is smaller than the track's own.
91#[derive(Debug, Clone, PartialEq)]
92pub struct History {
93    /// Primary index.
94    pub index: u64,
95    /// Tracks in order of creation.
96    pub tracks: Vec<Track>,
97    /// True if the point cap dropped points or tracks of this history.
98    pub truncated: bool,
99}
100
101/// A recoil reported but not yet followed, as the engine's queue holds it.
102#[derive(Debug, Clone, Copy)]
103struct Queued {
104    track: Option<usize>,
105    generation: u32,
106    pos: [f64; 3],
107    energy_ev: f64,
108}
109
110/// The track being followed. `track` is `None` once the point cap stopped
111/// the recorder from keeping it (its events are still followed so the queue
112/// mirror stays in step).
113#[derive(Debug, Clone, Copy)]
114struct Current {
115    track: Option<usize>,
116    generation: u32,
117}
118
119#[derive(Debug, Clone)]
120struct Active {
121    history: History,
122    points: usize,
123    queue: Vec<Queued>,
124    cur: Current,
125}
126
127/// A [`BcaTally`] that records ion and recoil paths. See the module
128/// documentation for the convention and the caps.
129#[derive(Debug, Clone)]
130pub struct TrajectoryRecorder {
131    cfg: TrajectoryConfig,
132    /// Recorded histories, by increasing index.
133    pub histories: Vec<History>,
134    /// Histories seen but not recorded because of `max_histories`.
135    pub histories_skipped: u64,
136    active: Option<Active>,
137}
138
139impl TrajectoryRecorder {
140    /// Empty recorder with the given caps.
141    pub fn new(cfg: TrajectoryConfig) -> Self {
142        Self {
143            cfg,
144            histories: Vec::new(),
145            histories_skipped: 0,
146            active: None,
147        }
148    }
149
150    /// The caps in force.
151    pub fn config(&self) -> TrajectoryConfig {
152        self.cfg
153    }
154}
155
156impl Active {
157    fn push_point(&mut self, max_points: usize, pos: [f64; 3], energy_ev: f64) {
158        let Some(t) = self.cur.track else { return };
159        let track = &mut self.history.tracks[t];
160        let pt = TrajectoryPoint { pos, energy_ev };
161        // The partners of one crystal step share the incoming state.
162        if track.points.last() == Some(&pt) {
163            return;
164        }
165        if self.points >= max_points {
166            self.history.truncated = true;
167            return;
168        }
169        self.points += 1;
170        track.points.push(pt);
171    }
172
173    /// The current track ends; follow the most recently queued recoil next.
174    fn finish(&mut self, max_points: usize, p: &Particle, end: TrackEnd) {
175        if let Some(t) = self.cur.track {
176            let before = self.history.tracks[t].points.len();
177            self.push_point(max_points, p.pos, p.energy_ev);
178            let track = &mut self.history.tracks[t];
179            // A dropped end point leaves the end unknown.
180            if track.points.len() > before || track.points.last().map(|l| l.pos) == Some(p.pos) {
181                track.end = Some(end);
182            }
183        }
184        self.cur = match self.queue.pop() {
185            Some(q) => Current {
186                track: q.track,
187                generation: q.generation,
188            },
189            None => Current {
190                track: None,
191                generation: 0,
192            },
193        };
194    }
195
196    /// A recoil that is stopped right after being reported was never queued.
197    fn take_unqueued(&mut self, p: &Particle) -> bool {
198        match self.queue.last() {
199            Some(q)
200                if q.generation == p.generation
201                    && q.generation == self.cur.generation + 1
202                    && q.pos == p.pos
203                    && q.energy_ev == p.energy_ev =>
204            {
205                let q = self.queue.pop().expect("checked non-empty");
206                if let Some(t) = q.track {
207                    self.history.tracks[t].end = Some(TrackEnd::Stopped);
208                }
209                true
210            }
211            _ => false,
212        }
213    }
214}
215
216impl BcaTally for TrajectoryRecorder {
217    fn begin_history(&mut self, index: u64) {
218        if index >= self.cfg.max_histories {
219            self.histories_skipped += 1;
220            self.active = None;
221            return;
222        }
223        self.active = Some(Active {
224            history: History {
225                index,
226                tracks: vec![Track {
227                    species: 0,
228                    generation: 0,
229                    parent: None,
230                    end: None,
231                    points: Vec::new(),
232                }],
233                truncated: false,
234            },
235            points: 0,
236            queue: Vec::new(),
237            cur: Current {
238                track: Some(0),
239                generation: 0,
240            },
241        });
242    }
243
244    fn partner(&mut self, p: &Particle, _impact_parameter_m: f64, _partners: usize) {
245        let max = self.cfg.max_points;
246        if let Some(a) = &mut self.active {
247            a.push_point(max, p.pos, p.energy_ev);
248        }
249    }
250
251    fn recoil(&mut self, r: &Particle) {
252        let max = self.cfg.max_points;
253        let Some(a) = &mut self.active else { return };
254        let mut track = None;
255        if let Some(parent) = a.cur.track {
256            if a.points < max {
257                a.history.tracks.push(Track {
258                    species: r.species,
259                    generation: r.generation,
260                    parent: Some(parent),
261                    end: None,
262                    points: Vec::new(),
263                });
264                track = Some(a.history.tracks.len() - 1);
265                // The new track is current only for the creation point.
266                let saved = a.cur;
267                a.cur.track = track;
268                a.push_point(max, r.pos, r.energy_ev);
269                a.cur = saved;
270            } else {
271                a.history.truncated = true;
272            }
273        }
274        a.queue.push(Queued {
275            track,
276            generation: r.generation,
277            pos: r.pos,
278            energy_ev: r.energy_ev,
279        });
280    }
281
282    fn stopped(&mut self, p: &Particle) {
283        let max = self.cfg.max_points;
284        let Some(a) = &mut self.active else { return };
285        if !a.take_unqueued(p) {
286            a.finish(max, p, TrackEnd::Stopped);
287        }
288    }
289
290    fn escaped(&mut self, p: &Particle, face: Face) {
291        let max = self.cfg.max_points;
292        if let Some(a) = &mut self.active {
293            a.finish(max, p, TrackEnd::Escaped(face));
294        }
295    }
296
297    fn end_history(&mut self, _index: u64, _budget: &EnergyBudget) {
298        if let Some(a) = self.active.take() {
299            self.histories.push(a.history);
300        }
301    }
302
303    fn merge(&mut self, other: Self) {
304        self.histories.extend(other.histories);
305        self.histories_skipped += other.histories_skipped;
306    }
307}
308
309/// An ordinary tally and a [`TrajectoryRecorder`] fed by the same events of
310/// one run. `inner` sees exactly what it would see alone.
311#[derive(Debug, Clone)]
312pub struct Recorded<T> {
313    /// The tally whose results the run is for.
314    pub inner: T,
315    /// The recorder.
316    pub recorder: TrajectoryRecorder,
317}
318
319impl<T: BcaTally> BcaTally for Recorded<T> {
320    fn begin_history(&mut self, index: u64) {
321        self.inner.begin_history(index);
322        self.recorder.begin_history(index);
323    }
324    fn electronic(&mut self, p: &Particle, from: [f64; 3], c: ElectronicChannel, e: f64) {
325        self.inner.electronic(p, from, c, e);
326    }
327    fn partner(&mut self, p: &Particle, b: f64, n: usize) {
328        self.inner.partner(p, b, n);
329        self.recorder.partner(p, b, n);
330    }
331    fn lattice(&mut self, at: [f64; 3], layer: usize, kind: LatticeDeposit, e: f64) {
332        self.inner.lattice(at, layer, kind, e);
333    }
334    fn recoil(&mut self, r: &Particle) {
335        self.inner.recoil(r);
336        self.recorder.recoil(r);
337    }
338    fn stopped(&mut self, p: &Particle) {
339        self.inner.stopped(p);
340        self.recorder.stopped(p);
341    }
342    fn escaped(&mut self, p: &Particle, face: Face) {
343        self.inner.escaped(p, face);
344        self.recorder.escaped(p, face);
345    }
346    fn end_history(&mut self, index: u64, budget: &EnergyBudget) {
347        self.inner.end_history(index, budget);
348        self.recorder.end_history(index, budget);
349    }
350    fn merge(&mut self, other: Self) {
351        self.inner.merge(other.inner);
352        self.recorder.merge(other.recorder);
353    }
354}