lindhard/ion/dynamic/driver.rs
1//! The fluence stepping loop.
2//!
3//! [`DynamicRun`] drives a [`CompositionGrid`] through a sequence of fluence
4//! steps: run `n` primaries on the current target, turn the transport events
5//! into inventory deltas ([`InventoryTally`]), apply them, and repeat until
6//! all primaries of the beam are used.
7//!
8//! # Fluence
9//!
10//! The beam's `count` is the total number of primaries of the run and
11//! [`DynamicConfig::fluence_m2`] the total fluence they represent, so each
12//! primary stands for `fluence_m2 / count` atoms/m². A step of `n` primaries
13//! scales its integer atom counts by that, giving the inventory deltas in
14//! atoms/m². (The delta of a step is a mean over the `n` histories; the
15//! target is held fixed during the step, so the step must be small enough
16//! that the composition does not change much within it. That is what the
17//! adaptive policy bounds.)
18//!
19//! # Determinism
20//!
21//! The primaries of a run are numbered `0..count` globally and primary `i`
22//! always draws from [`stream`](crate::rng::stream)`(seed, i)`. A step that
23//! covers global primaries `start..start + n` runs exactly those histories
24//! ([`Bca::run_range`]), so a run is reproducible end to end, a step never
25//! replays indices of an earlier one, and the thread count never matters:
26//! per-chunk tallies are integer counts merged in chunk order and the deltas
27//! are applied in `(slab, Z)` order.
28//!
29//! # Adaptive steps
30//!
31//! With [`StepPolicy::Adaptive`] a step is accepted only if its largest
32//! relative composition change is at most `max_change`, where the change of a
33//! slab is `|delta_Z| / (total atoms/m² of the slab)` maximised over the
34//! elements `Z` (a slab emptied completely has change 1). A step that is too
35//! large is **discarded** (the grid is not touched), shrunk to
36//! `floor(n * max_change / change)` primaries (at least `min_ions_per_step`
37//! and fewer than before) and retried from the **same** first index, so
38//! rejected attempts consume no indices: the sequence of accepted
39//! `(first_index, n)` pairs is decided from merged step results only, hence
40//! deterministic. A step that fails [`CompositionGrid::apply`] (for example
41//! it would remove more of an element than a slab holds) is treated the same
42//! way, halving `n`. At `min_ions_per_step` a step is accepted whatever its
43//! change; if it still removes more of an element than a slab holds (the last
44//! atoms of an eroding slab), that removal is capped at what the slab holds
45//! and counted in [`StepRecord::clamped`] (the step then creates the atoms the
46//! cap refused to remove, so inventory is not conserved for that step by up to
47//! one step's worth of removal). A [`StepPolicy::Fixed`] run does not cap: an
48//! `apply` error is returned, with the grid unchanged, and the cure is a
49//! smaller step or an adaptive policy. After an accepted step with
50//! change below `max_change / 2` the next step size doubles, up to
51//! `max_ions_per_step`. These rules are this crate's own design, not taken
52//! from a published code.
53//!
54//! # Coordinates
55//!
56//! The front surface is `x = 0` of the grid at every step ([parent
57//! module](super), "Coordinates"). Per-step outputs report the thickness of
58//! the finite slabs and the interface depths measured from the surface of that
59//! moment. With [`DynamicConfig::erosion`] off the surface never moves in the
60//! sample frame either, and the depths are the depths of the original frame.
61//! With erosion on, the surface recedes ("Erosion" below) and a depth `x`
62//! at some step is `x + R` in the original frame, with `R` the cumulative
63//! recession ([`StepRecord::recession_total_m`]) at that step.
64//!
65//! # Erosion
66//!
67//! With [`DynamicConfig::erosion`] on, sputtered atoms leave the target from
68//! its front, not from the slab where they were displaced, as in the
69//! dynamic-composition codes TRIDYN (Moller and Eckstein, Nucl. Instrum.
70//! Methods B 2 (1984) 814, and the later TRIDYN papers: the target is updated
71//! by removing the sputtered atoms from the surface layers and letting the
72//! remaining material move up) and SDTrimSP (Mutzke et al., IPP report
73//! 2019-02, the sections on the dynamic target and sputter erosion). Only the
74//! published descriptions were used. The update, per accepted step:
75//!
76//! 1. The default tally subtracts every recoil at its origin slab, so an
77//! escaped recoil is already a loss there. Erosion adds those losses back
78//! (the tally counts them per origin slab, as integers, from
79//! `Particle::origin_layer`) and instead removes the same number of atoms
80//! of the same element, `Y_Z` per element, from the front of the target:
81//! slab 0 first, then deeper slabs when the element is used up (counting
82//! what the slab holds after the step's other deltas). Nothing is removed
83//! twice and no atom is created or lost: for each `Z` the net change of
84//! [`CompositionGrid::total_inventory`](super::CompositionGrid::total_inventory)
85//! is the implanted atoms minus the sputtered ones, as with erosion off.
86//! Atoms that came from the substrate were never in the inventory and
87//! remove nothing. Counts are scaled to atoms/m² like all other deltas.
88//! 2. The step recession `dR` is the thickness the removed inventory occupied
89//! under the grid's [`Relaxation`](super::Relaxation):
90//! `sum_Z removed_Z v_Z` (ideal mixing) or `sum_Z removed_Z / n_mix` (fixed
91//! number density). It is tracked explicitly and summed over accepted steps
92//! into `R`; it cannot be recovered from the thickness, which also grows
93//! with implantation.
94//! 3. Removal is part of the deltas the step is checked and applied with, so
95//! `max_change` and the adaptive policy see it. Removal never exceeds what
96//! the slabs hold, so it is never the cause of the `clamped` cap.
97//! 4. The grid is rebuilt from `x = 0` as usual, which is the re-anchoring.
98//!
99//! Erosion is a pure function of the step's merged integer tally and draws no
100//! random numbers, so it keeps the determinism of the run. With erosion off
101//! none of this code runs and results are unchanged.
102//!
103//! # Retention limit
104//!
105//! [`DynamicRun::with_retention`] sets, per implanted species, a saturation
106//! limit: the largest atomic fraction `f` of that species a slab may hold. The
107//! dynamic-composition codes TRIDYN (Moller and Eckstein, Nucl. Instrum.
108//! Methods B 2 (1984) 814) and SDTrimSP (Mutzke et al., IPP report 2019-02)
109//! describe a saturation concentration of the implanted species above which
110//! further atoms are not retained and are released again; only those published
111//! descriptions were used. The update below is this crate's own statement of
112//! that idea, and no numeric default is supplied: the limit is always given by
113//! the caller.
114//!
115//! Per accepted step, after the transport deltas (and erosion) are formed and
116//! before they are checked or applied, each slab is looked at with the
117//! composition it would have after the step. For a limited species `Z` with
118//! amount `a_Z` and `o` atoms/m² of all other elements, the slab may hold at
119//! most `a_max = f o / (1 - f)` of `Z` (that is `a_Z / (a_Z + o) <= f`). Any
120//! excess `a_Z - a_max` is **re-emitted**: it is added to the step's deltas as
121//! a removal from that slab and leaves the target, so it is not inventory any
122//! more and is reported in [`StepRecord::re_emitted_m2`]. Species are taken in
123//! ascending `Z` and each sees the amounts left by the earlier ones, so with
124//! several limited species in one slab the fractions are met in that order
125//! (removing one raises the fraction of the others slightly); with one limited
126//! species, the usual case, the limit is met exactly. A slab holding only the
127//! limited species has `o = 0` and loses all of it. Because the removal is part
128//! of the deltas, `max_change` and the adaptive policy see it. It is a pure
129//! function of the merged step tally and the grid, draws no random numbers and
130//! so keeps the thread-count invariance. The re-emitted atoms are not placed
131//! anywhere: this is a bookkeeping limit, not a model of diffusion or of where
132//! the released gas goes. With no limits set none of this code runs and
133//! results are unchanged.
134//!
135//! The atom balance of an element `Z` over a step is
136//! `Δ total_inventory(Z) = transport net - re_emitted`, where the transport net
137//! counts retained primaries and moved, sputtered and transmitted recoils per
138//! the conventions of the [parent module](super).
139
140use std::collections::BTreeMap;
141
142use crate::ion::bca::{Bca, BcaConfig, BcaError, Beam};
143use crate::ion::scattering::ScatteringTable;
144use crate::ion::stopping::ElectronicStopping;
145
146use super::adapter::{InventoryTally, Yields};
147use super::{CompositionGrid, DynamicError, InventoryDelta};
148
149/// How the primaries are divided into fluence steps.
150#[derive(Debug, Clone, Copy, PartialEq)]
151pub enum StepPolicy {
152 /// Every step has `ions_per_step` primaries (the last may have fewer).
153 Fixed {
154 /// Primaries per step, at least 1.
155 ions_per_step: u64,
156 },
157 /// Steps start at `max_ions_per_step`, shrink when the composition change
158 /// exceeds `max_change` and grow back (see the [module docs](super)).
159 Adaptive {
160 /// Largest and initial step, at least `min_ions_per_step`.
161 max_ions_per_step: u64,
162 /// Smallest step, at least 1.
163 min_ions_per_step: u64,
164 /// Largest accepted relative composition change per step, `> 0`.
165 max_change: f64,
166 },
167}
168
169/// Fluence-loop settings.
170#[derive(Debug, Clone, Copy, PartialEq)]
171pub struct DynamicConfig {
172 /// Total fluence of the whole run, ions/m², represented by the beam's
173 /// `count` primaries.
174 pub fluence_m2: f64,
175 /// Step sizes.
176 pub policy: StepPolicy,
177 /// Sputter erosion and surface recession (see the [`DynamicRun`] docs,
178 /// "Erosion"). Off reproduces the fixed-front behaviour exactly.
179 pub erosion: bool,
180}
181
182/// Errors from the fluence loop.
183#[derive(Debug, thiserror::Error)]
184pub enum DynamicRunError {
185 /// A setting is out of range.
186 #[error("invalid dynamic run setting: {0}")]
187 Invalid(String),
188 /// The grid rejected an update (or could not be turned into a stack) and
189 /// no smaller step is allowed.
190 #[error("composition update failed: {0}")]
191 Grid(#[from] DynamicError),
192 /// The transport engine could not be set up or failed.
193 #[error("transport failed: {0}")]
194 Transport(#[from] BcaError),
195}
196
197/// What one accepted step did.
198#[derive(Debug, Clone, PartialEq)]
199pub struct StepRecord {
200 /// Step number, from 1.
201 pub step: u64,
202 /// Global index of the first primary of the step.
203 pub first_index: u64,
204 /// Primaries in the step.
205 pub ions: u64,
206 /// Attempts made at this `first_index` (1 if never rejected).
207 pub attempts: u32,
208 /// Largest relative composition change of the step (see the
209 /// [module docs](super)).
210 pub max_change: f64,
211 /// `(slab, element)` removals that were capped at what the slab held (see
212 /// the [module docs](super)); 0 for a step that needed no capping.
213 pub clamped: u32,
214 /// Event counts of the step.
215 pub yields: Yields,
216 /// Slabs (indices before the step) that emptied and were removed.
217 pub removed_slabs: Vec<usize>,
218 /// Surface recession of this step, m (0 with erosion off).
219 pub recession_m: f64,
220 /// Cumulative recession up to and including this step, m (0 with erosion
221 /// off): add it to a depth of this step to get the depth in the original
222 /// frame.
223 pub recession_total_m: f64,
224 /// Atoms/m² re-emitted by the retention limit in this step, by atomic
225 /// number (see the module docs, "Retention limit"); empty with no
226 /// limit set or nothing in excess.
227 pub re_emitted_m2: BTreeMap<u8, f64>,
228}
229
230/// A fluence-stepped run. See the [module docs](super).
231pub struct DynamicRun<'a> {
232 grid: CompositionGrid,
233 beam: Beam,
234 config: BcaConfig,
235 stopping: &'a (dyn ElectronicStopping + Sync),
236 table: &'a ScatteringTable,
237 cfg: DynamicConfig,
238 next_index: u64,
239 steps: u64,
240 current_n: u64,
241 cumulative: Yields,
242 recession_m: f64,
243 retention: BTreeMap<u8, f64>,
244 re_emitted_m2: BTreeMap<u8, f64>,
245}
246
247// Manual: the borrowed stopping model is a trait object without `Debug`, and
248// the scattering table is large.
249impl std::fmt::Debug for DynamicRun<'_> {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 f.debug_struct("DynamicRun")
252 .field("beam", &self.beam)
253 .field("config", &self.config)
254 .field("cfg", &self.cfg)
255 .field("next_index", &self.next_index)
256 .field("steps", &self.steps)
257 .field("current_n", &self.current_n)
258 .field("cumulative", &self.cumulative)
259 .field("recession_m", &self.recession_m)
260 .field("retention", &self.retention)
261 .finish_non_exhaustive()
262 }
263}
264
265impl<'a> DynamicRun<'a> {
266 /// Set up a run of `beam.count` primaries on `grid`.
267 pub fn new(
268 grid: CompositionGrid,
269 beam: Beam,
270 config: BcaConfig,
271 stopping: &'a (dyn ElectronicStopping + Sync),
272 table: &'a ScatteringTable,
273 cfg: DynamicConfig,
274 ) -> Result<Self, DynamicRunError> {
275 let bad = |m: &str| Err(DynamicRunError::Invalid(m.to_string()));
276 if beam.count == 0 {
277 return bad("the beam needs at least one primary");
278 }
279 if !(cfg.fluence_m2.is_finite() && cfg.fluence_m2 > 0.0) {
280 return bad("fluence must be finite and positive");
281 }
282 let current_n = match cfg.policy {
283 StepPolicy::Fixed { ions_per_step } => {
284 if ions_per_step == 0 {
285 return bad("ions_per_step must be at least 1");
286 }
287 ions_per_step
288 }
289 StepPolicy::Adaptive {
290 max_ions_per_step,
291 min_ions_per_step,
292 max_change,
293 } => {
294 if min_ions_per_step == 0 || max_ions_per_step < min_ions_per_step {
295 return bad("need 1 <= min_ions_per_step <= max_ions_per_step");
296 }
297 if !(max_change.is_finite() && max_change > 0.0) {
298 return bad("max_change must be finite and positive");
299 }
300 max_ions_per_step
301 }
302 };
303 Ok(Self {
304 grid,
305 beam,
306 config,
307 stopping,
308 table,
309 cfg,
310 next_index: 0,
311 steps: 0,
312 current_n,
313 cumulative: Yields::default(),
314 recession_m: 0.0,
315 retention: BTreeMap::new(),
316 re_emitted_m2: BTreeMap::new(),
317 })
318 }
319
320 /// Set the retention limits (see the module docs, "Retention
321 /// limit"): for each `(Z, f)` the largest atomic fraction `f`, with
322 /// `0 < f < 1`, of element `Z` a slab may hold. Replaces any earlier
323 /// limits; an empty map switches the model off (the default).
324 pub fn with_retention(
325 mut self,
326 limits: impl IntoIterator<Item = (u8, f64)>,
327 ) -> Result<Self, DynamicRunError> {
328 let mut map = BTreeMap::new();
329 for (z, f) in limits {
330 if !(f.is_finite() && f > 0.0 && f < 1.0) {
331 return Err(DynamicRunError::Invalid(format!(
332 "the retention limit of Z = {z} must be an atomic fraction in (0, 1)"
333 )));
334 }
335 if map.insert(z, f).is_some() {
336 return Err(DynamicRunError::Invalid(format!(
337 "the retention limit of Z = {z} is given twice"
338 )));
339 }
340 }
341 self.retention = map;
342 Ok(self)
343 }
344
345 /// Atoms/m² re-emitted by the retention limit over the accepted steps, by
346 /// atomic number.
347 pub fn re_emitted_m2(&self) -> &BTreeMap<u8, f64> {
348 &self.re_emitted_m2
349 }
350
351 /// The current target.
352 pub fn grid(&self) -> &CompositionGrid {
353 &self.grid
354 }
355
356 /// Primaries used so far (accepted steps only).
357 pub fn ions_done(&self) -> u64 {
358 self.next_index
359 }
360
361 /// Total primaries of the run.
362 pub fn ions_total(&self) -> u64 {
363 self.beam.count
364 }
365
366 /// Fluence delivered so far, ions/m².
367 pub fn fluence_done_m2(&self) -> f64 {
368 self.next_index as f64 * self.fluence_per_ion_m2()
369 }
370
371 /// Fluence one primary stands for, ions/m².
372 pub fn fluence_per_ion_m2(&self) -> f64 {
373 self.cfg.fluence_m2 / self.beam.count as f64
374 }
375
376 /// Accepted steps so far.
377 pub fn steps_done(&self) -> u64 {
378 self.steps
379 }
380
381 /// Event counts summed over the accepted steps.
382 pub fn cumulative(&self) -> &Yields {
383 &self.cumulative
384 }
385
386 /// Cumulative surface recession of the accepted steps, m (0 with erosion
387 /// off).
388 pub fn recession_m(&self) -> f64 {
389 self.recession_m
390 }
391
392 /// Run the next step. `Ok(None)` when all primaries are used. On error the
393 /// grid and the run state are unchanged.
394 pub fn step(&mut self) -> Result<Option<StepRecord>, DynamicRunError> {
395 let remaining = self.beam.count - self.next_index;
396 if remaining == 0 {
397 return Ok(None);
398 }
399 let first = self.next_index;
400 let mut n = self.current_n.min(remaining);
401 let mut attempts = 0u32;
402 loop {
403 attempts += 1;
404 let n_slabs = self.grid.n_slabs();
405 let stack = self.grid.to_stack()?;
406 let beam = Beam {
407 count: n,
408 ..self.beam
409 };
410 let bca = Bca::new(beam, &stack, self.config, self.stopping, self.table)?;
411 let tally = bca.run_range(first, n, || InventoryTally::new(n_slabs))?;
412
413 let per_ion = self.fluence_per_ion_m2();
414 let (deltas, step_recession, leftover) = if self.cfg.erosion {
415 self.erosion_deltas(&tally, per_ion)?
416 } else {
417 let d = tally
418 .counts()
419 .map(|((slab, z), c)| InventoryDelta {
420 slab,
421 z,
422 delta_atoms_m2: c as f64 * per_ion,
423 })
424 .collect();
425 (d, 0.0, 0)
426 };
427 let (deltas, re_emitted) = self.retention_deltas(deltas);
428 let change = self.max_change(&deltas);
429
430 let shrink = match self.cfg.policy {
431 StepPolicy::Adaptive {
432 min_ions_per_step,
433 max_change,
434 ..
435 } if n > min_ions_per_step => {
436 if change > max_change {
437 let scaled = (n as f64 * max_change / change).floor() as u64;
438 Some(scaled.clamp(min_ions_per_step, n - 1))
439 } else {
440 None
441 }
442 }
443 _ => None,
444 };
445 if let Some(smaller) = shrink {
446 n = smaller;
447 continue;
448 }
449
450 let mut clamped = leftover;
451 let outcome = match self.grid.apply(&deltas) {
452 Ok(o) => o,
453 Err(e) => {
454 let StepPolicy::Adaptive {
455 min_ions_per_step, ..
456 } = self.cfg.policy
457 else {
458 return Err(e.into());
459 };
460 if n > min_ions_per_step {
461 n = (n / 2).max(min_ions_per_step);
462 continue;
463 }
464 // Smallest step: a removal beyond what a slab holds is
465 // capped at what it holds.
466 if !matches!(e, DynamicError::NegativeInventory { .. }) {
467 return Err(e.into());
468 }
469 let capped: Vec<InventoryDelta> = deltas
470 .iter()
471 .map(|d| {
472 let have = self
473 .grid
474 .inventory(d.slab)
475 .and_then(|v| v.iter().find(|&&(z, _)| z == d.z).map(|&(_, a)| a))
476 .unwrap_or(0.0);
477 if d.delta_atoms_m2 < -have {
478 clamped += 1;
479 InventoryDelta {
480 delta_atoms_m2: -have,
481 ..*d
482 }
483 } else {
484 *d
485 }
486 })
487 .collect();
488 self.grid.apply(&capped)?
489 }
490 };
491 self.steps += 1;
492 self.next_index += n;
493 self.cumulative.add(tally.yields());
494 self.recession_m += step_recession;
495 for (&z, &a) in &re_emitted {
496 *self.re_emitted_m2.entry(z).or_insert(0.0) += a;
497 }
498 if let StepPolicy::Adaptive {
499 max_ions_per_step,
500 max_change,
501 ..
502 } = self.cfg.policy
503 {
504 self.current_n = if change < 0.5 * max_change {
505 n.saturating_mul(2).min(max_ions_per_step)
506 } else {
507 n
508 };
509 }
510 return Ok(Some(StepRecord {
511 step: self.steps,
512 first_index: first,
513 ions: n,
514 attempts,
515 max_change: change,
516 clamped,
517 yields: tally.yields().clone(),
518 removed_slabs: outcome.removed_slabs,
519 recession_m: step_recession,
520 recession_total_m: self.recession_m,
521 re_emitted_m2: re_emitted,
522 }));
523 }
524 }
525
526 /// The deltas of a step with erosion on, its recession in m, and the
527 /// number of elements whose sputtered count exceeded what the slabs hold
528 /// (that excess is not removed; the step reports it in `clamped`). See the
529 /// `DynamicRun` docs, "Erosion".
530 fn erosion_deltas(
531 &self,
532 tally: &InventoryTally,
533 per_ion: f64,
534 ) -> Result<(Vec<InventoryDelta>, f64, u32), DynamicRunError> {
535 // Integer net counts with the origin-slab loss of sputtered atoms
536 // cancelled; sputtered atoms per element to take from the front.
537 let mut net: BTreeMap<(usize, u8), i64> = tally.counts().collect();
538 let mut sputtered: BTreeMap<u8, u64> = BTreeMap::new();
539 for ((slab, z), n) in tally.sputtered_from() {
540 *net.entry((slab, z)).or_insert(0) += n as i64;
541 *sputtered.entry(z).or_insert(0) += n;
542 }
543 let mut delta: BTreeMap<(usize, u8), f64> = net
544 .into_iter()
545 .map(|(k, c)| (k, c as f64 * per_ion))
546 .collect();
547 let inventories: Vec<Vec<(u8, f64)>> = (0..self.grid.n_slabs())
548 .map(|i| self.grid.inventory(i).unwrap_or_default())
549 .collect();
550 let mut removed: BTreeMap<u8, f64> = BTreeMap::new();
551 let mut leftover = 0u32;
552 for (&z, &y) in &sputtered {
553 let mut rem = y as f64 * per_ion;
554 for (slab, inv) in inventories.iter().enumerate() {
555 if rem <= 0.0 {
556 break;
557 }
558 let have = inv
559 .iter()
560 .find(|&&(zz, _)| zz == z)
561 .map_or(0.0, |&(_, a)| a);
562 let d = delta.entry((slab, z)).or_insert(0.0);
563 let avail = (have + *d).max(0.0);
564 let take = avail.min(rem);
565 if take > 0.0 {
566 *d -= take;
567 rem -= take;
568 *removed.entry(z).or_insert(0.0) += take;
569 }
570 }
571 if rem > 0.0 {
572 leftover += 1;
573 }
574 }
575 let recession = if removed.is_empty() {
576 0.0
577 } else {
578 self.grid.relaxation().thickness_m(&removed)?
579 };
580 let deltas = delta
581 .into_iter()
582 .filter(|&(_, d)| d != 0.0)
583 .map(|((slab, z), d)| InventoryDelta {
584 slab,
585 z,
586 delta_atoms_m2: d,
587 })
588 .collect();
589 Ok((deltas, recession, leftover))
590 }
591
592 /// Add the removals the retention limits require to the step's `deltas`;
593 /// returns them with the atoms/m² re-emitted per element. See the module
594 /// docs, "Retention limit". Returns the deltas unchanged with no limits.
595 fn retention_deltas(
596 &self,
597 deltas: Vec<InventoryDelta>,
598 ) -> (Vec<InventoryDelta>, BTreeMap<u8, f64>) {
599 let mut re_emitted = BTreeMap::new();
600 if self.retention.is_empty() {
601 return (deltas, re_emitted);
602 }
603 let mut delta: BTreeMap<(usize, u8), f64> = BTreeMap::new();
604 for d in &deltas {
605 *delta.entry((d.slab, d.z)).or_insert(0.0) += d.delta_atoms_m2;
606 }
607 for slab in 0..self.grid.n_slabs() {
608 let inv = self.grid.inventory(slab).unwrap_or_default();
609 let have = |z: u8| {
610 inv.iter()
611 .find(|&&(zz, _)| zz == z)
612 .map_or(0.0, |&(_, a)| a)
613 };
614 // Composition after the step, in Z order.
615 let mut after: BTreeMap<u8, f64> = inv.iter().map(|&(z, a)| (z, a)).collect();
616 for (&(s, z), &d) in delta.range((slab, 0)..=(slab, u8::MAX)) {
617 debug_assert_eq!(s, slab);
618 *after.entry(z).or_insert(0.0) += d;
619 }
620 for (&z, &f) in &self.retention {
621 let a = after.get(&z).copied().unwrap_or(0.0);
622 let others: f64 = after
623 .iter()
624 .filter(|(&zz, _)| zz != z)
625 .map(|(_, &x)| x)
626 .sum();
627 let a_max = f * others / (1.0 - f);
628 if a <= a_max {
629 continue;
630 }
631 let excess = a - a_max;
632 let d = delta.entry((slab, z)).or_insert(0.0);
633 // A slab with nothing else loses the species exactly.
634 *d = if a_max == 0.0 { -have(z) } else { *d - excess };
635 after.insert(z, a_max);
636 *re_emitted.entry(z).or_insert(0.0) += excess;
637 }
638 }
639 let deltas = delta
640 .into_iter()
641 .filter(|&(_, d)| d != 0.0)
642 .map(|((slab, z), d)| InventoryDelta {
643 slab,
644 z,
645 delta_atoms_m2: d,
646 })
647 .collect();
648 (deltas, re_emitted)
649 }
650
651 /// Largest `|delta_Z| / (atoms/m^2 of the slab)` over the deltas.
652 fn max_change(&self, deltas: &[InventoryDelta]) -> f64 {
653 let totals: Vec<f64> = (0..self.grid.n_slabs())
654 .map(|i| {
655 self.grid
656 .inventory(i)
657 .map_or(0.0, |v| v.iter().map(|&(_, a)| a).sum())
658 })
659 .collect();
660 deltas
661 .iter()
662 .map(|d| d.delta_atoms_m2.abs() / totals[d.slab])
663 .fold(0.0, f64::max)
664 }
665
666 /// Run all remaining steps, calling `on_step` after each accepted step.
667 pub fn run_all(
668 &mut self,
669 mut on_step: impl FnMut(&StepRecord, &CompositionGrid),
670 ) -> Result<(), DynamicRunError> {
671 while let Some(rec) = self.step()? {
672 on_step(&rec, &self.grid);
673 }
674 Ok(())
675 }
676}