1use std::collections::BTreeMap;
147
148use crate::geometry::{Boundary, Exit, ExitOutcome, Face, Flight, Geometry};
149use crate::ion::bca::{Bca, BcaConfig, BcaTally, Beam, Particle};
150use crate::ion::scattering::ScatteringTable;
151use crate::ion::stopping::ElectronicStopping;
152use crate::material::Material;
153use crate::tally::voxel::{VoxelTally, VoxelTallyGrid};
154
155use super::adapter::Yields;
156use super::driver::{DynamicRunError, StepPolicy};
157use super::profiles::{CutThrough, ProfileAccumulator, ProfileSnapshot};
158use super::{CompositionGrid, DynamicError, InventoryDelta};
159
160pub const PROFILE_CUTOFF: f64 = 1.0e-4;
162
163#[derive(Debug, Clone, Copy, PartialEq)]
165pub enum BeamProfile {
166 Uniform,
168 Gaussian {
171 center_m: f64,
173 fwhm_m: f64,
175 },
176}
177
178#[derive(Debug, Clone, Copy, PartialEq)]
180pub struct Dynamic2dConfig {
181 pub fluence_m2: f64,
184 pub policy: StepPolicy,
186 pub profile: BeamProfile,
188 pub lateral: Boundary,
191}
192
193#[derive(Debug, Clone, PartialEq)]
195pub struct Step2d {
196 pub step: u64,
198 pub first_index: u64,
200 pub ions: u64,
202 pub attempts: u32,
204 pub max_change: f64,
206 pub clamped: u32,
209 pub yields: Yields,
211 pub surface_m: Vec<f64>,
215 pub incidence_rad: Vec<f64>,
218 pub column_yield: Vec<f64>,
221 pub sputtered_atoms_per_m: f64,
223 pub redeposited_atoms_per_m: f64,
225 pub implanted_atoms_per_m: f64,
227 pub lateral_escapes: u64,
230}
231
232impl Step2d {
233 pub fn redeposited_fraction(&self) -> f64 {
236 if self.sputtered_atoms_per_m > 0.0 {
237 self.redeposited_atoms_per_m / self.sputtered_atoms_per_m
238 } else {
239 0.0
240 }
241 }
242}
243
244#[derive(Debug)]
251struct ColumnTarget {
252 h: f64,
253 n_cols: usize,
254 boundary: Boundary,
255 surface: Vec<f64>,
257 materials: Vec<Material>,
259 offset: Vec<usize>,
261 n_slabs: Vec<usize>,
264 tops: Vec<Vec<f64>>,
267 back: Vec<Option<f64>>,
269 min_surface: f64,
270}
271
272impl ColumnTarget {
273 fn new(
274 grids: &[CompositionGrid],
275 surface: &[f64],
276 h: f64,
277 boundary: Boundary,
278 ) -> Result<Self, DynamicError> {
279 let n_cols = grids.len();
280 let mut materials = Vec::new();
281 let mut offset = Vec::with_capacity(n_cols + 1);
282 let mut n_slabs = Vec::with_capacity(n_cols);
283 let mut tops = Vec::with_capacity(n_cols);
284 let mut back = Vec::with_capacity(n_cols);
285 for (j, g) in grids.iter().enumerate() {
286 offset.push(materials.len());
287 let stack = g.to_stack()?;
288 let mut x = surface[j];
289 let mut t = Vec::new();
290 let mut ns = 0;
291 let mut bk = None;
292 for (k, layer) in stack.layers().iter().enumerate() {
293 t.push(x);
294 materials.push(layer.material().clone());
295 if layer.thickness_m().is_finite() {
296 x += layer.thickness_m();
297 ns += 1;
298 } else {
299 debug_assert_eq!(k + 1, stack.layers().len());
300 }
301 }
302 if ns == stack.layers().len() {
303 bk = Some(x);
304 }
305 n_slabs.push(ns);
306 tops.push(t);
307 back.push(bk);
308 }
309 offset.push(materials.len());
310 let min_surface = surface.iter().copied().fold(f64::INFINITY, f64::min);
311 Ok(Self {
312 h,
313 n_cols,
314 boundary,
315 surface: surface.to_vec(),
316 materials,
317 offset,
318 n_slabs,
319 tops,
320 back,
321 min_surface,
322 })
323 }
324
325 fn width(&self) -> f64 {
326 self.n_cols as f64 * self.h
327 }
328
329 fn split(&self, region: usize) -> (usize, usize) {
331 let j = self.offset.partition_point(|&o| o <= region) - 1;
332 (j, region - self.offset[j])
333 }
334
335 fn col_of(&self, y: f64) -> Option<usize> {
336 let l = self.width();
337 if !(y >= 0.0 && y <= l) {
338 return None;
339 }
340 if y == l {
341 return match self.boundary {
342 Boundary::Vacuum => None,
343 Boundary::Periodic => Some(0),
344 };
345 }
346 Some(((y / self.h) as usize).min(self.n_cols - 1))
347 }
348
349 fn layer_of(&self, j: usize, x: f64) -> Option<usize> {
352 if let Some(b) = self.back[j] {
353 if x >= b {
354 return None;
355 }
356 }
357 let t = &self.tops[j];
358 Some(t[1..].partition_point(|&b| b <= x))
359 }
360
361 fn neighbours(&self, j: usize) -> (f64, f64) {
363 let n = self.n_cols;
364 let left = if j > 0 {
365 self.surface[j - 1]
366 } else if self.boundary == Boundary::Periodic {
367 self.surface[n - 1]
368 } else {
369 self.surface[0]
370 };
371 let right = if j + 1 < n {
372 self.surface[j + 1]
373 } else if self.boundary == Boundary::Periodic {
374 self.surface[0]
375 } else {
376 self.surface[n - 1]
377 };
378 (left, right)
379 }
380
381 fn surface_at(&self, j: usize, y: f64) -> (f64, f64) {
383 let yc = (j as f64 + 0.5) * self.h;
384 let (l, r) = self.neighbours(j);
385 let rj = self.surface[j];
386 let m = if y < yc {
387 (rj - l) / self.h
388 } else {
389 (r - rj) / self.h
390 };
391 (rj + (y - yc) * m, m)
392 }
393
394 fn surface_crossing(
397 &self,
398 j: usize,
399 pos: [f64; 3],
400 dir: [f64; 3],
401 tmax: f64,
402 ) -> Option<(f64, [f64; 3])> {
403 let (x0, y0) = (pos[0], pos[1]);
404 let (dx, dy) = (dir[0], dir[1]);
405 let yc = (j as f64 + 0.5) * self.h;
406 let (left, right) = self.neighbours(j);
407 let rj = self.surface[j];
408 let t_c = if dy != 0.0 {
409 (yc - y0) / dy
410 } else {
411 f64::INFINITY
412 };
413 let mut lower = y0 < yc || (y0 == yc && dy < 0.0);
414 let mut t0 = 0.0;
415 loop {
416 let m = if lower {
417 (rj - left) / self.h
418 } else {
419 (right - rj) / self.h
420 };
421 let switches = (lower && dy > 0.0) || (!lower && dy < 0.0);
422 let t1 = if switches && t_c > t0 && t_c < tmax {
423 t_c
424 } else {
425 tmax
426 };
427 let g0 = x0 + t0 * dx - (rj + (y0 + t0 * dy - yc) * m);
428 let slope = dx - m * dy;
429 let g1 = g0 + slope * (t1 - t0);
430 if slope < 0.0 && g1 < 0.0 {
431 let t = if g0 <= 0.0 { t0 } else { t0 - g0 / slope };
432 let norm = (1.0 + m * m).sqrt();
433 return Some((t.min(tmax), [-1.0 / norm, m / norm, 0.0]));
434 }
435 if t1 >= tmax {
436 return None;
437 }
438 t0 = t1;
439 lower = !lower;
440 }
441 }
442
443 fn trace_to_surface(&self, pos: [f64; 3], dir: [f64; 3]) -> Option<usize> {
446 let (x0, dx, dy) = (pos[0], dir[0], dir[1]);
447 if !(x0.is_finite() && dx.is_finite() && dy.is_finite()) {
448 return None;
449 }
450 let mut j = self.col_of(pos[1].clamp(0.0, self.width()))?;
451 let y_start = pos[1];
452 let mut t0 = 0.0;
453 let mut y = y_start;
455 let mut first = true;
456 for _ in 0..(16 * self.n_cols + 16) {
457 let yc = (j as f64 + 0.5) * self.h;
458 let lower = y < yc || (y == yc && dy < 0.0);
459 let (l, r) = self.neighbours(j);
460 let rj = self.surface[j];
461 let m = if lower {
462 (rj - l) / self.h
463 } else {
464 (r - rj) / self.h
465 };
466 let (y_edge, at_centre) = if dy > 0.0 {
467 if lower {
468 (yc, true)
469 } else {
470 ((j as f64 + 1.0) * self.h, false)
471 }
472 } else if dy < 0.0 {
473 if lower {
474 (j as f64 * self.h, false)
475 } else {
476 (yc, true)
477 }
478 } else {
479 (y, true)
480 };
481 let t1 = if dy == 0.0 {
482 f64::INFINITY
483 } else {
484 t0 + ((y_edge - y) / dy).max(0.0)
485 };
486 let mut g0 = x0 + t0 * dx - (rj + (y - yc) * m);
487 if first {
488 g0 = g0.min(0.0);
489 first = false;
490 }
491 let slope = dx - m * dy;
492 if slope > 0.0 {
493 let t_hit = t0 + (-g0).max(0.0) / slope;
494 if t_hit <= t1 {
495 return Some(j);
496 }
497 }
498 if !t1.is_finite() {
499 return None;
500 }
501 if dx < 0.0 && x0 + t1 * dx < self.min_surface {
503 return None;
504 }
505 t0 = t1;
506 y = y_edge;
507 if !at_centre {
508 if dy > 0.0 {
509 if j + 1 == self.n_cols {
510 match self.boundary {
511 Boundary::Vacuum => return None,
512 Boundary::Periodic => {
513 j = 0;
514 y = 0.0;
515 }
516 }
517 } else {
518 j += 1;
519 }
520 } else if j == 0 {
521 match self.boundary {
522 Boundary::Vacuum => return None,
523 Boundary::Periodic => {
524 j = self.n_cols - 1;
525 y = self.width();
526 }
527 }
528 } else {
529 j -= 1;
530 }
531 }
532 }
533 None
534 }
535}
536
537impl Geometry for ColumnTarget {
538 fn n_materials(&self) -> usize {
539 self.materials.len()
540 }
541
542 fn material(&self, index: usize) -> &Material {
543 &self.materials[index]
544 }
545
546 fn n_regions(&self) -> usize {
547 self.materials.len()
548 }
549
550 fn material_index(&self, region: usize) -> usize {
551 region
552 }
553
554 fn locate(&self, pos: [f64; 3]) -> Option<usize> {
555 if !pos.iter().all(|c| c.is_finite()) {
556 return None;
557 }
558 let j = self.col_of(pos[1])?;
559 let (s, _) = self.surface_at(j, pos[1].min(self.width()));
560 if pos[0] < s {
561 return None;
562 }
563 let k = self.layer_of(j, pos[0])?;
564 Some(self.offset[j] + k)
565 }
566
567 fn entry_point(&self) -> [f64; 3] {
568 [self.surface[0] + 1.0e-6 * self.h, 0.5 * self.h, 0.0]
569 }
570
571 fn exit(&self, region: usize, pos: [f64; 3], dir: [f64; 3], limit: f64) -> Option<Exit> {
572 match self.flight(region, pos, dir, limit) {
573 Flight::Event(e) => Some(e),
574 Flight::Clear { .. } => None,
575 }
576 }
577
578 fn flight(&self, region: usize, pos: [f64; 3], dir: [f64; 3], limit: f64) -> Flight {
579 if !dir.iter().all(|d| d.is_finite()) || dir == [0.0; 3] {
580 return Flight::Clear { region };
581 }
582 #[derive(Clone, Copy)]
583 enum Kind {
584 Lateral(bool),
585 Interface(bool),
586 Back,
587 }
588 let (j, k) = self.split(region);
589 let (dx, dy) = (dir[0], dir[1]);
590 let mut t_end = f64::INFINITY;
591 let mut kind = None;
592 let mut consider = |t: f64, kd: Kind| {
593 if t < t_end {
594 t_end = t;
595 kind = Some(kd);
596 }
597 };
598 if dy > 0.0 {
599 consider(
600 (((j as f64 + 1.0) * self.h - pos[1]) / dy).max(0.0),
601 Kind::Lateral(true),
602 );
603 } else if dy < 0.0 {
604 consider(
605 ((j as f64 * self.h - pos[1]) / dy).max(0.0),
606 Kind::Lateral(false),
607 );
608 }
609 if dx > 0.0 {
610 let n_layers = self.offset[j + 1] - self.offset[j];
611 if k + 1 < n_layers {
612 consider(
613 ((self.tops[j][k + 1] - pos[0]) / dx).max(0.0),
614 Kind::Interface(true),
615 );
616 } else if let Some(b) = self.back[j] {
617 consider(((b - pos[0]) / dx).max(0.0), Kind::Back);
618 }
619 } else if dx < 0.0 && k > 0 {
620 consider(
621 ((self.tops[j][k] - pos[0]) / dx).max(0.0),
622 Kind::Interface(false),
623 );
624 }
625 let tmax = t_end.min(limit);
626 let point = |t: f64| [pos[0] + t * dx, pos[1] + t * dy, pos[2] + t * dir[2]];
627 if let Some((t, normal)) = self.surface_crossing(j, pos, dir, tmax) {
628 return Flight::Event(Exit {
629 distance: t,
630 at: point(t),
631 outcome: ExitOutcome::Escape {
632 face: Face::Front,
633 normal,
634 },
635 });
636 }
637 let Some(kind) = kind.filter(|_| t_end <= limit) else {
638 return Flight::Clear { region };
639 };
640 let mut at = point(t_end);
641 let outcome = match kind {
642 Kind::Interface(down) => ExitOutcome::Enter {
643 region: if down { region + 1 } else { region - 1 },
644 pos: at,
645 },
646 Kind::Back => ExitOutcome::Escape {
647 face: Face::Back,
648 normal: [1.0, 0.0, 0.0],
649 },
650 Kind::Lateral(up) => {
651 at[1] = if up {
652 (j as f64 + 1.0) * self.h
653 } else {
654 j as f64 * self.h
655 };
656 let mut new_pos = at;
657 let next = if up {
658 if j + 1 == self.n_cols {
659 (self.boundary == Boundary::Periodic).then(|| {
660 new_pos[1] = 0.0;
661 0
662 })
663 } else {
664 Some(j + 1)
665 }
666 } else if j == 0 {
667 (self.boundary == Boundary::Periodic).then(|| {
668 new_pos[1] = self.width();
669 self.n_cols - 1
670 })
671 } else {
672 Some(j - 1)
673 };
674 let normal = [0.0, if up { 1.0 } else { -1.0 }, 0.0];
675 match next {
676 None => ExitOutcome::Escape {
677 face: Face::Side,
678 normal,
679 },
680 Some(nj) => match self.layer_of(nj, at[0]) {
681 Some(nk) => ExitOutcome::Enter {
682 region: self.offset[nj] + nk,
683 pos: new_pos,
684 },
685 None => ExitOutcome::Escape {
687 face: Face::Back,
688 normal,
689 },
690 },
691 }
692 }
693 };
694 Flight::Event(Exit {
695 distance: t_end,
696 at,
697 outcome,
698 })
699 }
700
701 fn in_vacuum(&self, pos: [f64; 3]) -> bool {
702 match self.col_of(pos[1]) {
703 None => true,
704 Some(j) => pos[0] < self.surface_at(j, pos[1].min(self.width())).0,
705 }
706 }
707}
708
709struct Tally2d<'t> {
716 target: &'t ColumnTarget,
717 counts: BTreeMap<(usize, usize, u8), i64>,
718 sputtered_from: BTreeMap<(usize, usize, u8), u64>,
719 redeposited: BTreeMap<(usize, u8), u64>,
720 lateral: u64,
721 yields: Yields,
722 vox: Option<(VoxelTally, VoxelTally)>,
725}
726
727impl<'t> Tally2d<'t> {
728 fn new(target: &'t ColumnTarget, proto: Option<&VoxelTally>) -> Self {
729 Self {
730 vox: proto.map(|p| (p.clone(), p.clone())),
731 target,
732 counts: BTreeMap::new(),
733 sputtered_from: BTreeMap::new(),
734 redeposited: BTreeMap::new(),
735 lateral: 0,
736 yields: Yields::default(),
737 }
738 }
739
740 fn slab(&self, region: usize) -> Option<(usize, usize)> {
742 let (j, k) = self.target.split(region);
743 (k < self.target.n_slabs[j]).then_some((j, k))
744 }
745}
746
747impl BcaTally for Tally2d<'_> {
748 fn begin_history(&mut self, index: u64) {
749 self.yields.histories += 1;
750 if let Some((a, b)) = &mut self.vox {
751 a.begin_history(index);
752 b.begin_history(index);
753 }
754 }
755
756 fn end_history(&mut self, index: u64, budget: &crate::ion::bca::EnergyBudget) {
757 if let Some((a, b)) = &mut self.vox {
758 a.end_history(index, budget);
759 b.end_history(index, budget);
760 }
761 }
762
763 fn recoil(&mut self, recoil: &Particle) {
764 self.yields.displaced += 1;
765 if self.vox.is_some() {
766 let in_substrate = self.slab(recoil.layer).is_none();
767 if let Some((a, b)) = &mut self.vox {
768 a.recoil(recoil);
769 if in_substrate {
770 b.recoil(recoil);
771 }
772 }
773 }
774 if let Some((j, k)) = self.slab(recoil.layer) {
775 *self.counts.entry((j, k, recoil.z)).or_insert(0) -= 1;
776 }
777 }
778
779 fn stopped(&mut self, p: &Particle) {
780 let slab = self.slab(p.layer);
781 if let Some((a, b)) = &mut self.vox {
782 a.stopped(p);
783 if slab.is_none() {
784 b.stopped(p);
785 }
786 }
787 if let Some((j, k)) = slab {
788 *self.counts.entry((j, k, p.z)).or_insert(0) += 1;
789 }
790 if p.is_primary() {
791 if slab.is_some() {
792 self.yields.primaries_in_slabs += 1;
793 } else {
794 self.yields.primaries_in_substrate += 1;
795 }
796 }
797 }
798
799 fn escaped(&mut self, p: &Particle, face: Face) {
800 if let Some((a, b)) = &mut self.vox {
801 a.escaped(p, face);
802 b.escaped(p, face);
803 }
804 match (p.is_primary(), face) {
805 (true, Face::Front) => self.yields.backscattered += 1,
806 (true, Face::Back) => self.yields.transmitted += 1,
807 (false, Face::Front) => {
808 *self.yields.sputtered.entry(p.z).or_insert(0) += 1;
809 if let Some((j, k)) = self.slab(p.origin_layer) {
810 *self.sputtered_from.entry((j, k, p.z)).or_insert(0) += 1;
811 if let Some(c) = self.target.trace_to_surface(p.pos, p.dir) {
812 if self.target.n_slabs[c] > 0 {
813 *self.redeposited.entry((c, p.z)).or_insert(0) += 1;
814 }
815 }
816 }
817 }
818 (false, Face::Back) => self.yields.recoils_transmitted += 1,
819 (_, Face::Side) => self.lateral += 1,
820 }
821 }
822
823 fn merge(&mut self, other: Self) {
824 for (k, n) in other.counts {
825 *self.counts.entry(k).or_insert(0) += n;
826 }
827 for (k, n) in other.sputtered_from {
828 *self.sputtered_from.entry(k).or_insert(0) += n;
829 }
830 for (k, n) in other.redeposited {
831 *self.redeposited.entry(k).or_insert(0) += n;
832 }
833 self.lateral += other.lateral;
834 self.yields.add(&other.yields);
835 if let (Some((a, b)), Some((oa, ob))) = (&mut self.vox, other.vox) {
836 a.merge(oa);
837 b.merge(ob);
838 }
839 }
840}
841
842pub struct DynamicRun2d<'a> {
848 grids: Vec<CompositionGrid>,
849 surface: Vec<f64>,
850 h: f64,
851 beam: Beam,
852 config: BcaConfig,
853 stopping: &'a (dyn ElectronicStopping + Sync),
854 table: &'a ScatteringTable,
855 cfg: Dynamic2dConfig,
856 weights: Vec<f64>,
858 next_index: u64,
859 steps: u64,
860 current_n: u64,
861 cumulative: Yields,
862 sputtered_atoms_per_m: f64,
863 redeposited_atoms_per_m: f64,
864 profiles: Option<Profiles>,
865}
866
867struct Profiles {
869 proto: VoxelTally,
870 acc: ProfileAccumulator,
871 at: Vec<u64>,
873 snapshots: Vec<ProfileSnapshot>,
874}
875
876impl std::fmt::Debug for DynamicRun2d<'_> {
877 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
878 f.debug_struct("DynamicRun2d")
879 .field("columns", &self.grids.len())
880 .field("column_width_m", &self.h)
881 .field("cfg", &self.cfg)
882 .field("next_index", &self.next_index)
883 .field("steps", &self.steps)
884 .finish_non_exhaustive()
885 }
886}
887
888impl<'a> DynamicRun2d<'a> {
889 pub fn new(
893 columns: Vec<CompositionGrid>,
894 column_width_m: f64,
895 beam: Beam,
896 config: BcaConfig,
897 stopping: &'a (dyn ElectronicStopping + Sync),
898 table: &'a ScatteringTable,
899 cfg: Dynamic2dConfig,
900 ) -> Result<Self, DynamicRunError> {
901 let bad = |m: &str| Err(DynamicRunError::Invalid(m.to_string()));
902 if columns.is_empty() {
903 return bad("at least one column is needed");
904 }
905 if !(column_width_m.is_finite() && column_width_m > 0.0) {
906 return bad("the column width must be finite and positive");
907 }
908 if beam.count == 0 {
909 return bad("the beam needs at least one primary per column");
910 }
911 if (columns.len() as u64).checked_mul(beam.count).is_none() {
912 return bad("columns times primaries overflows the index space");
913 }
914 if !(cfg.fluence_m2.is_finite() && cfg.fluence_m2 > 0.0) {
915 return bad("fluence must be finite and positive");
916 }
917 let n = columns.len();
918 let weights: Vec<f64> = match cfg.profile {
919 BeamProfile::Uniform => vec![1.0; n],
920 BeamProfile::Gaussian { center_m, fwhm_m } => {
921 if !(center_m.is_finite() && fwhm_m.is_finite() && fwhm_m > 0.0) {
922 return bad("the Gaussian profile needs a finite centre and positive FWHM");
923 }
924 let sigma = fwhm_m / (2.0 * (2.0 * std::f64::consts::LN_2).sqrt());
925 let l = n as f64 * column_width_m;
926 (0..n)
927 .map(|j| {
928 let mut d = (j as f64 + 0.5) * column_width_m - center_m;
929 if cfg.lateral == Boundary::Periodic {
930 d -= l * (d / l).round();
931 }
932 let p = (-d * d / (2.0 * sigma * sigma)).exp();
933 if p >= PROFILE_CUTOFF {
934 p
935 } else {
936 0.0
937 }
938 })
939 .collect()
940 }
941 };
942 if weights.iter().all(|&w| w == 0.0) {
943 return bad("the beam illuminates no column");
944 }
945 let current_n = match cfg.policy {
946 StepPolicy::Fixed { ions_per_step } => {
947 if ions_per_step == 0 {
948 return bad("ions_per_step must be at least 1");
949 }
950 ions_per_step
951 }
952 StepPolicy::Adaptive {
953 max_ions_per_step,
954 min_ions_per_step,
955 max_change,
956 } => {
957 if min_ions_per_step == 0 || max_ions_per_step < min_ions_per_step {
958 return bad("need 1 <= min_ions_per_step <= max_ions_per_step");
959 }
960 if !(max_change.is_finite() && max_change > 0.0) {
961 return bad("max_change must be finite and positive");
962 }
963 max_ions_per_step
964 }
965 };
966 Ok(Self {
967 surface: vec![0.0; n],
968 grids: columns,
969 h: column_width_m,
970 beam,
971 config,
972 stopping,
973 table,
974 cfg,
975 weights,
976 next_index: 0,
977 steps: 0,
978 current_n,
979 cumulative: Yields::default(),
980 sputtered_atoms_per_m: 0.0,
981 redeposited_atoms_per_m: 0.0,
982 profiles: None,
983 })
984 }
985
986 pub fn with_profiles(
996 mut self,
997 grid: VoxelTallyGrid,
998 snapshot_fluences_m2: &[f64],
999 ) -> Result<Self, DynamicRunError> {
1000 let mut at = Vec::new();
1001 for &f in snapshot_fluences_m2 {
1002 if !(f.is_finite() && f > 0.0 && f <= self.cfg.fluence_m2) {
1003 return Err(DynamicRunError::Invalid(
1004 "a snapshot fluence must be in (0, the run fluence]".to_string(),
1005 ));
1006 }
1007 let k = (f / self.cfg.fluence_m2 * self.beam.count as f64).round() as u64;
1008 at.push(k.clamp(1, self.beam.count));
1009 }
1010 at.sort_unstable();
1011 at.dedup();
1012 self.profiles = Some(Profiles {
1013 proto: VoxelTally::new(grid),
1014 acc: ProfileAccumulator::new(grid),
1015 at,
1016 snapshots: Vec::new(),
1017 });
1018 Ok(self)
1019 }
1020
1021 pub fn snapshots(&self) -> &[ProfileSnapshot] {
1023 self.profiles.as_ref().map_or(&[], |p| &p.snapshots)
1024 }
1025
1026 pub fn profile_snapshot(&self) -> Option<ProfileSnapshot> {
1029 self.profiles.as_ref().map(|p| {
1030 p.acc
1031 .snapshot(self.beam.ion.z(), self.fluence_done_m2(), self.next_index)
1032 })
1033 }
1034
1035 fn cut_column(&self) -> Option<usize> {
1038 (0..self.grids.len()).find(|&j| self.weights[j] > 0.0 && self.grids[j].n_slabs() == 0)
1039 }
1040
1041 pub fn run_until_cut_through(
1050 &mut self,
1051 mut on_step: impl FnMut(&Step2d, &[CompositionGrid]),
1052 ) -> Result<CutThrough, DynamicRunError> {
1053 if let Some(j) =
1054 (0..self.grids.len()).find(|&j| self.weights[j] > 0.0 && !self.grids[j].has_substrate())
1055 {
1056 return Err(DynamicRunError::Invalid(format!(
1057 "column {j} has no substrate: there is nothing to cut through to"
1058 )));
1059 }
1060 let mut before = self.fluence_done_m2();
1061 while self.cut_column().is_none() {
1062 before = self.fluence_done_m2();
1063 let Some(rec) = self.step()? else {
1064 break;
1065 };
1066 on_step(&rec, &self.grids);
1067 }
1068 let column = self.cut_column();
1069 Ok(CutThrough {
1070 reached: column.is_some(),
1071 column,
1072 fluence_before_m2: before,
1073 fluence_m2: self.fluence_done_m2(),
1074 ions_done: self.next_index,
1075 profiles: self.profile_snapshot(),
1076 })
1077 }
1078
1079 pub fn columns(&self) -> &[CompositionGrid] {
1081 &self.grids
1082 }
1083
1084 pub fn surface_m(&self) -> &[f64] {
1086 &self.surface
1087 }
1088
1089 pub fn profile_weights(&self) -> &[f64] {
1091 &self.weights
1092 }
1093
1094 pub fn ions_total_per_column(&self) -> u64 {
1096 self.beam.count
1097 }
1098
1099 pub fn ions_done(&self) -> u64 {
1101 self.next_index
1102 }
1103
1104 pub fn fluence_done_m2(&self) -> f64 {
1106 self.cfg.fluence_m2 * self.next_index as f64 / self.beam.count as f64
1107 }
1108
1109 pub fn steps_done(&self) -> u64 {
1111 self.steps
1112 }
1113
1114 pub fn cumulative(&self) -> &Yields {
1116 &self.cumulative
1117 }
1118
1119 pub fn redeposited_fraction(&self) -> f64 {
1121 if self.sputtered_atoms_per_m > 0.0 {
1122 self.redeposited_atoms_per_m / self.sputtered_atoms_per_m
1123 } else {
1124 0.0
1125 }
1126 }
1127
1128 pub fn incidence_rad(&self) -> Vec<f64> {
1131 let n = self.surface.len();
1132 let d = self.beam.direction();
1133 (0..n)
1134 .map(|j| {
1135 let periodic = self.cfg.lateral == Boundary::Periodic;
1136 let lo = if j > 0 {
1137 Some(j - 1)
1138 } else if periodic {
1139 Some(n - 1)
1140 } else {
1141 None
1142 };
1143 let hi = if j + 1 < n {
1144 Some(j + 1)
1145 } else if periodic {
1146 Some(0)
1147 } else {
1148 None
1149 };
1150 let (a, b, span) = match (lo, hi) {
1151 (Some(l), Some(r)) => (self.surface[l], self.surface[r], 2.0),
1152 (None, Some(r)) => (self.surface[j], self.surface[r], 1.0),
1153 (Some(l), None) => (self.surface[l], self.surface[j], 1.0),
1154 (None, None) => (0.0, 0.0, 1.0),
1155 };
1156 let m = (b - a) / (span * self.h);
1157 ((d[0] - m * d[1]) / (1.0 + m * m).sqrt())
1158 .clamp(-1.0, 1.0)
1159 .acos()
1160 })
1161 .collect()
1162 }
1163
1164 fn max_change(&self, deltas: &BTreeMap<(usize, usize, u8), f64>) -> f64 {
1165 let totals: Vec<Vec<f64>> = self
1166 .grids
1167 .iter()
1168 .map(|g| {
1169 (0..g.n_slabs())
1170 .map(|k| {
1171 g.inventory(k)
1172 .map_or(0.0, |v| v.iter().map(|&(_, a)| a).sum())
1173 })
1174 .collect()
1175 })
1176 .collect();
1177 deltas
1178 .iter()
1179 .map(|(&(j, k, _), d)| d.abs() / totals[j][k])
1180 .fold(0.0, f64::max)
1181 }
1182
1183 fn apply_all(
1185 &self,
1186 delta: &BTreeMap<(usize, usize, u8), f64>,
1187 ) -> Result<Vec<CompositionGrid>, DynamicError> {
1188 let mut grids = self.grids.clone();
1189 for (j, g) in grids.iter_mut().enumerate() {
1190 let ds: Vec<InventoryDelta> = delta
1191 .range((j, 0, 0)..=(j, usize::MAX, u8::MAX))
1192 .map(|(&(_, slab, z), &d)| InventoryDelta {
1193 slab,
1194 z,
1195 delta_atoms_m2: d,
1196 })
1197 .collect();
1198 g.apply(&ds)?;
1199 }
1200 Ok(grids)
1201 }
1202
1203 pub fn step(&mut self) -> Result<Option<Step2d>, DynamicRunError> {
1206 let remaining = self.beam.count - self.next_index;
1207 if remaining == 0 {
1208 return Ok(None);
1209 }
1210 let first = self.next_index;
1211 let mut n = self.current_n.min(remaining);
1212 if let Some(p) = &self.profiles {
1213 if let Some(&next) = p.at.iter().find(|&&k| k > first) {
1214 n = n.min(next - first);
1215 }
1216 }
1217 let mut attempts = 0u32;
1218 let incidence = self.incidence_rad();
1219 let n_cols = self.grids.len();
1220 loop {
1221 attempts += 1;
1222 let target = ColumnTarget::new(&self.grids, &self.surface, self.h, self.cfg.lateral)?;
1223
1224 let mut delta: BTreeMap<(usize, usize, u8), f64> = BTreeMap::new();
1225 let mut sputtered: BTreeMap<(usize, u8), f64> = BTreeMap::new();
1226 let mut deposited: BTreeMap<(usize, u8), f64> = BTreeMap::new();
1227 let mut yields = Yields::default();
1228 let mut column_yield = vec![0.0; n_cols];
1229 let mut lateral = 0u64;
1230 let mut implanted = 0.0;
1231 let mut step_profiles = self
1232 .profiles
1233 .as_ref()
1234 .map(|p| ProfileAccumulator::new(*p.proto.grid()));
1235 #[allow(clippy::needless_range_loop)]
1236 for c in 0..n_cols {
1237 if self.weights[c] == 0.0 {
1238 continue;
1239 }
1240 let beam = Beam {
1241 count: n,
1242 ..self.beam
1243 };
1244 let entry = [
1245 self.surface[c] + 1.0e-6 * self.h,
1246 (c as f64 + 0.5) * self.h,
1247 0.0,
1248 ];
1249 let bca = Bca::new(beam, &target, self.config, self.stopping, self.table)?
1250 .with_entry_point(entry)?;
1251 let tally: Tally2d<'_> =
1252 bca.run_range(c as u64 * self.beam.count + first, n, || {
1253 Tally2d::new(&target, self.profiles.as_ref().map(|p| &p.proto))
1254 })?;
1255 let w = self.cfg.fluence_m2 * self.weights[c] / self.beam.count as f64;
1257 if let (Some(acc), Some((all, sub))) = (&mut step_profiles, &tally.vox) {
1258 acc.add_counts(&all.report(), &sub.report(), w * self.h);
1259 }
1260 for (&k, &cnt) in &tally.counts {
1261 if cnt != 0 {
1262 *delta.entry(k).or_insert(0.0) += cnt as f64 * w;
1263 }
1264 }
1265 for (&(j, k, z), &cnt) in &tally.sputtered_from {
1266 *delta.entry((j, k, z)).or_insert(0.0) += cnt as f64 * w;
1268 *sputtered.entry((j, z)).or_insert(0.0) += cnt as f64 * w;
1269 }
1270 for (&k, &cnt) in &tally.redeposited {
1271 *deposited.entry(k).or_insert(0.0) += cnt as f64 * w;
1272 }
1273 column_yield[c] = tally.yields.sputtered_total() as f64 / n as f64;
1274 lateral += tally.lateral;
1275 implanted += tally.yields.primaries_in_slabs as f64 * w * self.h;
1276 yields.add(&tally.yields);
1277 }
1278
1279 let mut removed: Vec<BTreeMap<u8, f64>> = vec![BTreeMap::new(); n_cols];
1282 for (&(j, z), &y) in &sputtered {
1283 let mut rem = y;
1284 for slab in 0..self.grids[j].n_slabs() {
1285 if rem <= 0.0 {
1286 break;
1287 }
1288 let inv = self.grids[j].inventory(slab).unwrap_or_default();
1289 let have = inv
1290 .iter()
1291 .find(|&&(zz, _)| zz == z)
1292 .map_or(0.0, |&(_, a)| a);
1293 let d = delta.entry((j, slab, z)).or_insert(0.0);
1294 let take = (have + *d).max(0.0).min(rem);
1295 if take > 0.0 {
1296 *d -= take;
1297 rem -= take;
1298 *removed[j].entry(z).or_insert(0.0) += take;
1299 }
1300 }
1301 }
1302 let mut dep_inv: Vec<BTreeMap<u8, f64>> = vec![BTreeMap::new(); n_cols];
1304 for (&(j, z), &a) in &deposited {
1305 *delta.entry((j, 0, z)).or_insert(0.0) += a;
1306 *dep_inv[j].entry(z).or_insert(0.0) += a;
1307 }
1308 let mut step_recession = vec![0.0; n_cols];
1309 for j in 0..n_cols {
1310 let relax = self.grids[j].relaxation();
1311 if !removed[j].is_empty() {
1312 step_recession[j] += relax.thickness_m(&removed[j])?;
1313 }
1314 if !dep_inv[j].is_empty() {
1315 step_recession[j] -= relax.thickness_m(&dep_inv[j])?;
1316 }
1317 }
1318 delta.retain(|_, d| *d != 0.0);
1319 let change = self.max_change(&delta);
1320
1321 if let StepPolicy::Adaptive {
1322 min_ions_per_step,
1323 max_change,
1324 ..
1325 } = self.cfg.policy
1326 {
1327 if n > min_ions_per_step && change > max_change {
1328 let scaled = (n as f64 * max_change / change).floor() as u64;
1329 n = scaled.clamp(min_ions_per_step, n - 1);
1330 continue;
1331 }
1332 }
1333
1334 let mut clamped = 0u32;
1335 let new_grids = match self.apply_all(&delta) {
1336 Ok(g) => g,
1337 Err(e) => {
1338 let StepPolicy::Adaptive {
1339 min_ions_per_step, ..
1340 } = self.cfg.policy
1341 else {
1342 return Err(e.into());
1343 };
1344 if n > min_ions_per_step {
1345 n = (n / 2).max(min_ions_per_step);
1346 continue;
1347 }
1348 if !matches!(e, DynamicError::NegativeInventory { .. }) {
1351 return Err(e.into());
1352 }
1353 let capped: BTreeMap<(usize, usize, u8), f64> = delta
1354 .iter()
1355 .map(|(&(j, k, z), &d)| {
1356 let have = self.grids[j]
1357 .inventory(k)
1358 .and_then(|v| v.iter().find(|&&(zz, _)| zz == z).map(|&(_, a)| a))
1359 .unwrap_or(0.0);
1360 if d < -have {
1361 clamped += 1;
1362 ((j, k, z), -have)
1363 } else {
1364 ((j, k, z), d)
1365 }
1366 })
1367 .collect();
1368 self.apply_all(&capped)?
1369 }
1370 };
1371 self.grids = new_grids;
1372 for (r, d) in self.surface.iter_mut().zip(&step_recession) {
1373 *r += d;
1374 }
1375 self.steps += 1;
1376 self.next_index += n;
1377 self.cumulative.add(&yields);
1378 if let (Some(p), Some(acc)) = (&mut self.profiles, &step_profiles) {
1379 p.acc.merge(acc);
1380 }
1381 let per_m = |m: &BTreeMap<(usize, u8), f64>| (0.0 + m.values().sum::<f64>()) * self.h;
1382 let sputtered_apm = per_m(&sputtered);
1383 let redeposited_apm = per_m(&deposited);
1384 self.sputtered_atoms_per_m += sputtered_apm;
1385 self.redeposited_atoms_per_m += redeposited_apm;
1386 if let StepPolicy::Adaptive {
1387 max_ions_per_step,
1388 max_change,
1389 ..
1390 } = self.cfg.policy
1391 {
1392 self.current_n = if change < 0.5 * max_change {
1393 n.saturating_mul(2).min(max_ions_per_step)
1394 } else {
1395 n
1396 };
1397 }
1398 if let Some(p) = &mut self.profiles {
1399 if p.at.binary_search(&self.next_index).is_ok() {
1400 let f = self.cfg.fluence_m2 * self.next_index as f64 / self.beam.count as f64;
1401 p.snapshots
1402 .push(p.acc.snapshot(self.beam.ion.z(), f, self.next_index));
1403 }
1404 }
1405 return Ok(Some(Step2d {
1406 step: self.steps,
1407 first_index: first,
1408 ions: n,
1409 attempts,
1410 max_change: change,
1411 clamped,
1412 yields,
1413 surface_m: self.surface.clone(),
1414 incidence_rad: incidence,
1415 column_yield,
1416 sputtered_atoms_per_m: sputtered_apm,
1417 redeposited_atoms_per_m: redeposited_apm,
1418 implanted_atoms_per_m: implanted,
1419 lateral_escapes: lateral,
1420 }));
1421 }
1422 }
1423
1424 pub fn run_all(
1426 &mut self,
1427 mut on_step: impl FnMut(&Step2d, &[CompositionGrid]),
1428 ) -> Result<(), DynamicRunError> {
1429 while let Some(rec) = self.step()? {
1430 on_step(&rec, &self.grids);
1431 }
1432 Ok(())
1433 }
1434}
1435
1436#[cfg(test)]
1437mod tests {
1438 use super::*;
1439 use crate::geometry::Stack;
1440 use crate::ion::dynamic::Relaxation;
1441
1442 const NM: f64 = 1e-9;
1443
1444 fn si() -> Material {
1445 let mut m = Material::from_atom_fractions(&[(14, 1.0)], None).unwrap();
1446 m.set_displacement_energy_ev(14, 15.0).unwrap();
1447 m.set_lattice_binding_energy_ev(14, 2.0).unwrap();
1448 m.set_surface_binding_energy_ev(14, 2.0).unwrap();
1449 m
1450 }
1451
1452 fn target(surface: &[f64], boundary: Boundary) -> ColumnTarget {
1453 let stack = Stack::new(vec![(si(), 10.0 * NM)], Some(si())).unwrap();
1454 let g =
1455 CompositionGrid::from_stack(&stack, Relaxation::ideal_mixing_elemental(&[14]).unwrap())
1456 .unwrap();
1457 let grids = vec![g; surface.len()];
1458 ColumnTarget::new(&grids, surface, 10.0 * NM, boundary).unwrap()
1459 }
1460
1461 #[test]
1462 fn flat_surface_escape_is_vertical_and_exact() {
1463 let t = target(&[0.0; 3], Boundary::Periodic);
1464 let r = t.locate([2.0 * NM, 5.0 * NM, 0.0]).unwrap();
1465 let f = t.flight(r, [2.0 * NM, 5.0 * NM, 0.0], [-1.0, 0.0, 0.0], 1.0);
1467 let Flight::Event(e) = f else { panic!("{f:?}") };
1468 assert!((e.distance - 2.0 * NM).abs() < 1e-18);
1469 match e.outcome {
1470 ExitOutcome::Escape { face, normal } => {
1471 assert_eq!(face, Face::Front);
1472 assert_eq!(normal, [-1.0, 0.0, 0.0]);
1473 }
1474 o => panic!("{o:?}"),
1475 }
1476 let f = t.flight(r, [2.0 * NM, 5.0 * NM, 0.0], [1.0, 0.0, 0.0], 1.0);
1478 let Flight::Event(e) = f else { panic!("{f:?}") };
1479 assert!((e.distance - 8.0 * NM).abs() < 1e-18);
1480 assert!(matches!(e.outcome, ExitOutcome::Enter { region, .. } if region == r + 1));
1481 }
1482
1483 #[test]
1484 fn sloped_surface_normal_follows_the_neighbours() {
1485 let t = target(&[0.0, 10.0 * NM, 20.0 * NM], Boundary::Vacuum);
1487 let pos = [12.0 * NM, 10.0 * NM, 0.0];
1489 let r = t.locate(pos).unwrap();
1490 let f = t.flight(r, pos, [-1.0, 0.0, 0.0], 1.0);
1491 let Flight::Event(e) = f else { panic!("{f:?}") };
1492 assert!((e.distance - 7.0 * NM).abs() < 1e-18, "{}", e.distance);
1493 let ExitOutcome::Escape { normal, .. } = e.outcome else {
1494 panic!()
1495 };
1496 let s = std::f64::consts::FRAC_1_SQRT_2;
1497 assert!((normal[0] + s).abs() < 1e-12 && (normal[1] - s).abs() < 1e-12);
1498 assert_eq!(t.locate([2.0 * NM, 10.0 * NM, 0.0]), None);
1500 assert!(t.in_vacuum([2.0 * NM, 10.0 * NM, 0.0]));
1501 }
1502
1503 #[test]
1504 fn periodic_wrap_and_vacuum_side() {
1505 let p = target(&[0.0; 2], Boundary::Periodic);
1506 let pos = [3.0 * NM, 19.0 * NM, 0.0];
1507 let r = p.locate(pos).unwrap();
1508 let Flight::Event(e) = p.flight(r, pos, [0.0, 1.0, 0.0], 1.0) else {
1509 panic!()
1510 };
1511 let ExitOutcome::Enter { region, pos } = e.outcome else {
1512 panic!()
1513 };
1514 assert_eq!(p.split(region).0, 0);
1515 assert_eq!(pos[1], 0.0);
1516 let v = target(&[0.0; 2], Boundary::Vacuum);
1517 let r = v.locate([3.0 * NM, 19.0 * NM, 0.0]).unwrap();
1518 let Flight::Event(e) = v.flight(r, [3.0 * NM, 19.0 * NM, 0.0], [0.0, 1.0, 0.0], 1.0) else {
1519 panic!()
1520 };
1521 assert!(matches!(
1522 e.outcome,
1523 ExitOutcome::Escape {
1524 face: Face::Side,
1525 ..
1526 }
1527 ));
1528 }
1529
1530 #[test]
1531 fn redeposition_trace_hits_the_far_wall_of_a_groove() {
1532 let t = target(&[0.0, 40.0 * NM, 0.0], Boundary::Vacuum);
1537 let hit = t.trace_to_surface([36.0 * NM, 15.0 * NM, 0.0], [-0.6, 0.8, 0.0]);
1538 assert_eq!(hit, Some(1));
1539 assert_eq!(
1541 t.trace_to_surface([40.0 * NM, 15.0 * NM, 0.0], [-1.0, 0.0, 0.0]),
1542 None
1543 );
1544 assert_eq!(
1546 t.trace_to_surface([0.0, 25.0 * NM, 0.0], [-0.5, 0.866, 0.0]),
1547 None
1548 );
1549 }
1550}