Skip to main content

lindhard/ion/bca/
crystal.rs

1//! The crystal flight model: collision partners taken from explicit lattice
2//! sites instead of a random impact parameter (step 20b of the M2 crystal
3//! plan), optionally displaced by thermal vibration (step 21b).
4//!
5//! # Choosing the model per region
6//!
7//! [`Geometry`] is unchanged. A [`Bca`] is amorphous everywhere unless
8//! [`Bca::with_crystal`] names a [`CrystalTarget`] (lattice and orientation)
9//! and the list of **regions** (layers of a stack, voxels of a grid) it
10//! fills. A particle whose current region is in that list uses the crystal
11//! model; in every other region, and for a [`Bca`] without `with_crystal`, the
12//! amorphous model is used unchanged (and bit for bit: the amorphous code path
13//! draws the same random numbers and does the same arithmetic as before; the
14//! test `tests/crystal_amorphous_identity.rs` compares complete reports).
15//! Several calls give several crystals (different lattices or orientations)
16//! in different regions.
17//!
18//! The material of a crystal region still supplies the displacement and
19//! binding energies, the stopping and the surface barrier; it must contain
20//! every element of the lattice and its atom density must agree with the
21//! lattice's to 5 %. The crystal model needs the constant-path convention and
22//! no weak collisions (the lattice replaces both). Beam direction and surface
23//! stay in the lab frame of the geometry; the orientation's lab-to-crystal
24//! rotation ([`Orientation::to_crystal`]) is the only thing read from it (the
25//! beam is the [`Beam`](super::Beam)'s, so give the `Beam` the orientation's tilt and
26//! twist, as `Orientation::beam_lab` does).
27//!
28//! # Lattice frame of a history
29//!
30//! The crystal position of a lab point `r` is `R r + u`, with `R` the
31//! orientation's rotation and `u` a translation drawn uniformly from the
32//! rectangular cell that tiles the crystal, `[0, e_x) x [0, e_y) x [0, e_z)`
33//! with the edges of [`Lattice::orthogonal_cell`] (the cube `[0, a)^3` for a
34//! cubic lattice), once per primary history (three uniform numbers per crystal,
35//! drawn from a copy of the history's stream positioned at word 2^67, so the
36//! transport draws are the same with or without crystals), shared by the
37//! primary and all its recoils. A beam of finite
38//! width samples all positions of the unit cell, so this is the static
39//! equivalent of a random entry point; it also makes the surface termination
40//! random. Lattice sites that lie outside the target (in front of the surface)
41//! or in a region that is not part of this crystal are not partners.
42//!
43//! # Collision sequence (after DISPLATH, Tier A)
44//!
45//! The ion moves in straight segments. For a segment from position `r0` along
46//! the unit direction `d` the partners are the lattice sites within `p_max`
47//! of the line, ordered by the path distance `s = (r - r0) . d` of their
48//! point of closest approach ([`crate::ion::crystal::LatticeSearch`]). The
49//! scheme is the one of DISPLATH (<https://github.com/permissionx/DISPLATH>,
50//! MIT, commit `7f461141c518305e6a5de8ce7e37dd314cd20ccb`, `src/dynamics.jl`:
51//! `GetTargetsFromNeighbor` and `Collision!`, and `SimultaneousCriteria` in
52//! `src/geometry.jl`), re-implemented here on [`LatticeSearch`] with
53//! the lattice-site list of this crate, and with the project's own scattering
54//! tables, kinematics and energy bookkeeping. DISPLATH is Copyright (c) 2024
55//! 裴幂許Permission (MIT); the notice is in `THIRD_PARTY_LICENSES.md`.
56//!
57//! * **Free path.** The ion flies straight to the closest-approach point of
58//!   the nearest partner (smallest `s`, ties broken by the search's documented
59//!   order). There is no free-path length parameter: the path is the crystal's.
60//!   The nonlocal electronic loss of the model chosen in the [`BcaConfig`](super::BcaConfig) is
61//!   taken over that length at the energy at the start of the segment, as in
62//!   the amorphous model. Under
63//!   [`ElectronicLoss::EquipartitionLsOr`](super::ElectronicLoss::EquipartitionLsOr)
64//!   that is half of the Lindhard-Scharff loss, and the other half is local
65//!   (next bullets and "Local electronic loss"). When no partner lies within
66//!   `search_length` the ion flies that distance and searches again (a
67//!   channel). The geometry is asked about every segment, so surfaces and
68//!   interfaces cut it exactly like an amorphous flight.
69//! * **Simultaneous collisions.** Further sites join the nearest one if, for
70//!   every partner `t` already accepted, `Δ = s - s_t` (≥ 0) satisfies
71//!   `Δ <= q_max`, `p_t² + Δ² <= p_max²` and `p² + Δ² <= p_max²`
72//!   (DISPLATH's `SimultaneousCriteria`: sites at similar path distance and
73//!   inside a sphere of radius `p_max` around the collision point). DISPLATH
74//!   sets `q_max` to the sum of the two atomic radii (about the
75//!   nearest-neighbour distance); here the default is a small fraction of it,
76//!   see the next section.
77//! * **Collision.** Each partner `i` is scattered from the incoming state at
78//!   its own impact parameter `p_i` (the distance from the site to the line)
79//!   with the scattering table: transfer `T_i = γ E sin²(Θ_i/2)`, the recoil
80//!   leaving along `sin(Θ_i/2) d + cos(Θ_i/2) n_i`, with `n_i` the unit vector
81//!   from the line to the site (the half-angle form of
82//!   [`kinematics::lab_angles`]: the recoil angle is `(π - Θ)/2`). The
83//!   projectile takes the momentum that is left, `P = P0 - Σ sqrt(2 m_i T_i)
84//!   r_i`, and the energies are scaled by the common factor
85//!   `λ = (E - Σ Q_i) / (|P|²/2m + Σ T_i)` so that energy is conserved with
86//!   the local electronic losses `Q_i` (DISPLATH's `Collision_!`). For one
87//!   partner and no local loss this is the ordinary binary collision
88//!   (`λ = 1`). The projectile stays at the closest-approach point of the
89//!   nearest partner; the time-delay offsets of Robinson and Torrens (1974)
90//!   are not applied (DISPLATH does not apply them either).
91//! * **Recoils** start at their lattice site, with the usual displacement
92//!   criterion ([`BcaConfig::follow_recoils`](super::BcaConfig::follow_recoils)) and follow the same model. The
93//!   sites that were just hit are excluded from the next search (so is the
94//!   site the recoil starts on): the ion cannot collide twice in a row with
95//!   the same atom.
96//!
97//! # Choice of the search parameters
98//!
99//! Measured on this engine (B 5 keV into Si, 4000 ions, ZBL, Lindhard-Scharff,
100//! 7 degrees tilt, 22 degrees twist, static lattice; ratios of Rp to the
101//! amorphous result), with `nn` the nearest-neighbour distance (2.35 Å):
102//!
103//! * With `q_max = nn` (DISPLATH's rule) the result depends strongly on
104//!   `p_max`: `p_max` = 1.53 Å gives Rp ratio 1.11, but `p_max` = 2.3 Å gives
105//!   4.4. The merged collisions of distant sites cancel the kicks of a
106//!   channel's walls, so almost every ion channels, at 20 and 30 degrees tilt
107//!   as well. That is an artefact of merging over a sphere as large as the
108//!   channel, not physics.
109//! * With `q_max <= 0.1 nn` the result is independent of `q_max` and, for
110//!   `p_max >= 1.5 * 1.53 Å = nn`, of `p_max` within the statistics (Rp ratio
111//!   1.08-1.09 for `p_max` = 2.3, 3.1 Å and `q_max` = 0.01-0.1 nn; at
112//!   `q_max` = 0.2 nn the artefact starts).
113//!
114//! So the defaults are `p_max = nn` and `q_max = 0.05 nn`: converged in both,
115//! and the cost grows with `p_max`. These are properties of this engine's
116//! implementation of the criterion, checked on silicon only; other lattices
117//! need the same convergence check.
118//!
119//! # Measured checks (static lattice, 4000 ions, seed 1, Si, ZBL, Lindhard-Scharff)
120//!
121//! `tests/crystal_bca.rs` (the statistical ones are `#[ignore]`d; release
122//! build). Ratios are crystal over amorphous:
123//!
124//! | Case | Rp | dRp | Notes |
125//! |---|---|---|---|
126//! | B 5 keV, 30° tilt, 17° twist | 0.97 | 1.09 | within 10 % / 15 % |
127//! | As 30 keV, 30° tilt, 17° twist | 0.93 | 1.13 | within 10 % / 15 % |
128//! | B 5 keV, 7° tilt, 22° twist | 1.06 | 1.24 | median 23.8 vs 23.8 nm, 99th percentile 76.2 vs 56.8 nm |
129//! | As 30 keV, 7° tilt, 22° twist | 1.01 | 1.88 | median 22.2 vs 25.2 nm, 99th percentile 112.2 vs 50.8 nm |
130//! | B 5 keV along <110> | **6.1** (121 vs 20 nm) | 3.2 | 91 % of ions deeper than twice the amorphous Rp (amorphous 5 %) |
131//!
132//! The random-direction check (Rp within 10 %, dRp within 15 % of the
133//! amorphous result) is made at 30°/17° only. That beam is at least 8.0° from
134//! every {100}, {110} and {111} plane. It passes within 1.3° of a {211} and
135//! 0.9° of a {311} plane, which are higher-index planes with a lower atomic
136//! areal density. The 7°/22° rows are not a random-direction check; see "The
137//! 7°/22° criterion" below.
138//!
139//! # Measured checks with thermal vibration (same set-up, `THETA_D_SI`, zero-point term on)
140//!
141//! `tests/crystal_thermal.rs` (statistical ones `#[ignore]`d; release build).
142//! "Tail" is the fraction of primaries deeper than twice the amorphous Rp
143//! (amorphous: 0.034 at 7°/22°, 0.041 at 30°/17°), with its binomial error.
144//!
145//! | Case | Static | 0 K | 300 K | 600 K |
146//! |---|---|---|---|---|
147//! | B 5 keV along <110>: Rp, nm | 121.0 | 111.7 | 94.3 | 75.8 |
148//! | B 5 keV along <110>: tail | 0.910 | 0.876 ± 0.005 | 0.833 ± 0.006 | 0.758 ± 0.007 |
149//! | B 5 keV, 30°/17°: Rp / dRp ratio | 0.97 / 1.09 | 0.99 / 1.15 | 1.00 / 1.18 | 0.99 / 1.15 |
150//! | As 30 keV, 30°/17°: Rp / dRp ratio | 0.93 / 1.13 | | 0.95 / 1.17 | |
151//! | B 5 keV, 7°/22°: Rp / dRp ratio | 1.06 / 1.24 | 1.13 / 1.41 | 1.14 / 1.43 | 1.10 / 1.29 |
152//! | As 30 keV, 7°/22°: Rp / dRp ratio | 1.01 / 1.88 | | 1.05 / 1.80 | |
153//!
154//! The static and 300 K entries of the 30°/17° and 7°/22° rows were measured
155//! again for #225 (aarch64 macOS, commit `8513cfe`). The 0 K and 600 K
156//! entries and the <110> rows are from PR #224. The crystal path is not bit
157//! identical across platforms (`docs/architecture.md`). The re-measured
158//! values differ from those of PR #224 by at most 0.02 in a dRp ratio. That
159//! is within the seed-to-seed spread below.
160//!
161//! Along <110> the channeled tail falls with temperature, each step by more
162//! than five binomial standard errors (the #181 criterion asks for a
163//! monotonic decrease beyond the statistics). At the random direction the Rp
164//! ratio stays within 10 % at 300 K.
165//!
166//! **Off-axis directions get a longer tail with vibration, not a shorter
167//! one.** At 7°/22° (B) and 30°/17°, the tail beyond twice the amorphous Rp
168//! grows from the static value as the amplitude rises from zero. It peaks
169//! near `u1` ≈ 0.045-0.065 Å (0-300 K for Si) and falls again at higher
170//! amplitudes. PR #224 measured, for B at 7°/22° with 4000 ions: tail 0.080
171//! static, 0.075 at `u1` ≈ 0.0004 Å, 0.108 at 0 K, 0.115 at 300 K, 0.094 at
172//! 600 K, and 0.076 at 1500 K. The dependence is smooth in the amplitude. It
173//! is converged in the search parameters at 300 K: `q_max` = 0.01, 0.05 and
174//! 0.1 nn and `p_max` = 1.5 nn spread the dRp ratio over 1.39-1.45 and change
175//! Rp by under 1 %. So it is a property of the model as built, not of the
176//! thermal code path.
177//!
178//! #225 measured how stable this is. The rise is significant at 16000 ions
179//! (B 5 keV), under both electronic-loss treatments. The amorphous reference
180//! uses the same loss as the crystal run
181//! (`off_axis_tail_grows_with_vibration_under_both_losses`):
182//!
183//! | B 5 keV, 16000 ions | Amorphous tail | Static tail | 300 K tail | Rise | 300 K Rp / dRp ratio |
184//! |---|---|---|---|---|---|
185//! | 7°/22°, `NonLocal` | 0.0350 | 0.0806 ± 0.0022 | 0.1089 ± 0.0025 | 8.5 σ | 1.13 / 1.40 |
186//! | 7°/22°, `EquipartitionLsOr` | 0.0395 | 0.0787 ± 0.0022 | 0.1059 ± 0.0025 | 8.2 σ | 1.12 / 1.39 |
187//! | 30°/17°, `NonLocal` | 0.0452 | 0.0565 ± 0.0019 | 0.0714 ± 0.0021 | 5.2 σ | 1.00 / 1.20 |
188//! | 30°/17°, `EquipartitionLsOr` | 0.0484 | 0.0523 ± 0.0018 | 0.0683 ± 0.0021 | 5.8 σ | 0.99 / 1.18 |
189//!
190//! With 4000 ions under `EquipartitionLsOr`, As 30 keV gives a tail of 0.057
191//! static and 0.069 at 300 K at 7°/22° (amorphous 0.009). At 30°/17° it gives
192//! 0.020 static and 0.028 at 300 K (amorphous 0.015).
193//!
194//! Over seeds 1 to 5 (4000 ions, `NonLocal`, 7°/22°), the tail is larger at
195//! 300 K than static for every seed and both ions:
196//!
197//! * B: 0.075-0.085 static, 0.105-0.118 at 300 K.
198//! * As: 0.057-0.060 static, 0.065-0.072 at 300 K.
199//!
200//! The dRp ratios have a seed-to-seed standard deviation of:
201//!
202//! * B: 0.018 static, 0.037 at 300 K.
203//! * As: 0.052 static, 0.047 at 300 K.
204//!
205//! For As the dRp ratio does not rise with vibration (1.88 static, 1.80 at
206//! 300 K). More ions reach the tail, but the deepest ones stop shallower:
207//! the 99th percentile is 112 nm static and 98 nm at 300 K.
208//!
209//! The local Oen-Robinson half does not remove the rise, so the rise is
210//! robust to this change of loss treatment (#225, part 3). The cause is
211//! not established: `EquipartitionLsOr` still keeps half of the nonlocal
212//! loss, so both treatments share the amorphous-average nonlocal
213//! approximation, and this comparison cannot rule it out as a contributor.
214//! Correlated vibration is absent: neighbouring atoms are displaced
215//! independently ("Thermal vibration"). No source read for #225 gives the
216//! sign of the effect of correlations on this tail, so its expected
217//! direction is not stated.
218//!
219//! # Comparison with the literature (#225)
220//!
221//! Searched: measured depth profiles of B or As implanted into crystalline
222//! (100) Si near 7° tilt at room temperature, and measurements against tilt,
223//! twist or wafer temperature.
224//!
225//! **Not opened.** The leads from the issue could not be read:
226//!
227//! * Miyake et al., J. Electrochem. Soc. 130, 716 (1983),
228//!   doi:10.1149/1.2119789. Paywalled at the publisher.
229//! * Tian et al., Mikrochim. Acta (1992), doi:10.1007/BF01244469. Closed.
230//! * Klein et al., IEEE Trans. Electron Devices 39 (1992),
231//!   doi:10.1109/16.141226. Closed. It is a UT-MARLOWE paper, Tier C.
232//!
233//! Several further candidates were not readable either:
234//!
235//! * Semicond. Sci. Technol. 5 (1990), doi:10.1088/0268-1242/5/10/001,
236//!   "Boron implants in <100> silicon at tilt angles of 0 degrees and 7
237//!   degrees". The publisher served a bot check.
238//! * J. Electrochem. Soc. papers on B channeling against tilt angle:
239//!   doi:10.1149/1.2108785 and doi:10.1149/1.2085934. The publisher served a
240//!   bot check.
241//! * K. Nordlund, F. Djurabekova and G. Hobler, Phys. Rev. B 94, 214109
242//!   (2016), doi:10.1103/PhysRevB.94.214109. The publisher refused access and
243//!   the repository copy did not respond.
244//!
245//! None of these is cited for content here.
246//!
247//! **Read: measured profiles, not usable at matched conditions.** D. Cai,
248//! N. Grønbech-Jensen, C. M. Snell and K. M. Beardmore, Phys. Rev. B 54,
249//! 17147 (1996), arXiv:physics/9901056 (read in full). The paper shows SIMS
250//! profiles of B in (100) Si:
251//!
252//! * at 15, 35 and 80 keV, at tilt 7° and rotation 30° (Fig. 3);
253//! * at 5 keV, at tilt 7° and rotation 7° (Fig. 4; the SIMS data are from
254//!   K. Gärtner, M. Nitschke and W. Eckstein, Nucl. Instrum. Methods B 83, 87
255//!   (1993), which was not found openly readable).
256//!
257//! It also shows As at 8°/30° (Fig. 6). These profiles were not digitized,
258//! for three reasons:
259//!
260//! 1. The paper does not give the in-plane reference direction of
261//!    "rotation", so the orientation cannot be mapped through
262//!    `docs/crystal-orientation.md`.
263//! 2. It does not give the beam divergence, and it does not give the oxide
264//!    or dose of the 5 keV measurement. Its 16 Å native oxide is a
265//!    simulation setting.
266//! 3. Each measured line overlaps the UT-MARLOWE curves (Tier C) in the same
267//!    vector figure.
268//!
269//! A single room-temperature profile would also only show the model at
270//! 300 K. It could not tell the static and the vibrating lattice apart.
271//!
272//! **Read: a published simulation of the same question.** M. I. Bratchenko,
273//! A. S. Bakai and S. V. Dyuldya, J. Phys. Stud. 13, 1601 (2009),
274//! doi:10.30970/jps.13.1601 (read in full). This is molecular dynamics with
275//! the authors' own code, not a measurement. It simulates 15 keV B and As
276//! into (001) Si at 7° tilt and 30° rotation, for a static lattice and at
277//! 300 K, with uncorrelated Debye displacements. The paper reports:
278//!
279//! * In the static lattice, the deeply channeled states are almost empty.
280//!   The authors attribute this to blocking, which forbids capture into
281//!   them by a single strong collision.
282//! * At 300 K "thermal vibrations facilitate the volume capture of ions into
283//!   the stable channeling mode", that is, feeding-in.
284//! * For As this lengthens the channeling tail substantially compared with
285//!   the static lattice. For B the temperature effect on the tail length is
286//!   "much weaker".
287//! * "As long as a target is ordered there is no chance to obtain truly
288//!   random-equivalent doping profiles".
289//!
290//! Its introduction also states, citing SIMS work not read here, that
291//! off-axis SIMS profiles show long tails. It says the fraction of ions
292//! beyond the amorphous Gaussian reaches about 20-30 % below 20 keV.
293//!
294//! The sign of the population effect here agrees with that paper: vibration
295//! feeds ions into channels at an off-axis direction. But the energy (5 and
296//! 30 keV here, against 15 keV) and the orientation (7°/22° here, against
297//! 7°/30°) differ, and so does the measure. The paper discusses the tail
298//! length, and for As at 30 keV our deepest percentile shortens. The paper
299//! uses the same uncorrelated vibration model, so it does not test that
300//! assumption. **The off-axis rise is therefore consistent with one
301//! published simulation and not validated against measurement.** Validating
302//! it needs a measured profile at matched, fully stated conditions, ideally
303//! at two wafer temperatures (`docs/validation.md`, "Channeling (M2)").
304//!
305//! # The 7°/22° criterion (#225)
306//!
307//! #180 asked that dRp at 7°/22° be within 15 % of amorphous. That bound is
308//! withdrawn. The beam lies 2.6° from a {100} plane and 2.7° from a {110}
309//! plane (7.0° from <100>), so it is a near-planar direction, not a random
310//! one. A tail of a few per cent of channeled ions is expected there, static
311//! or vibrating, and the second moment weights it heavily. Without a
312//! matched measurement, the 7°/22° runs are recorded-value regression checks
313//! (`near_planar_7_22_static_lattice` in `tests/crystal_bca.rs` and
314//! `near_planar_7_22_at_300_k` in `tests/crystal_thermal.rs`):
315//!
316//! * The dRp ratio stays within 0.12 (B) or 0.16 (As) of the table value.
317//!   That is three seed-to-seed standard deviations, rounded up.
318//! * The 90th percentile stays within 30 % of the amorphous one.
319//! * Static lattice: Rp stays within 10 %.
320//! * At 300 K: the tail exceeds the amorphous tail by more than five
321//!   binomial standard errors.
322//!
323//! These are not agreement with experiment. They catch unintended changes of
324//! the model.
325//!
326//! At 30°/17° the static lattice keeps both random-direction bounds. At
327//! 300 K the dRp ratio, 1.18 (B) and 1.17 (As), is above the 15 % bound.
328//! This is accepted as a property of the vibrating model, for three reasons:
329//!
330//! * The bulk of the profile still matches: Rp and the 90th percentile are
331//!   within 10 % of amorphous.
332//! * The excess is the thermally fed tail measured above, at 5 σ.
333//! * The one published study read finds that thermal vibration causes this
334//!   kind of feeding-in.
335//!
336//! The 300 K check asserts Rp and the 90th percentile within 10 %, and dRp
337//! below its recorded value plus 0.12 (`random_direction_rp_at_300_k_matches_amorphous`).
338//!
339//! Unless marked `EquipartitionLsOr`, the ranges above use the nonlocal
340//! Lindhard-Scharff loss only
341//! ([`ElectronicLoss::NonLocal`](super::ElectronicLoss::NonLocal), the
342//! default), so a channeled ion loses as much electronic energy per unit
343//! path as in the amorphous target. With the local Oen-Robinson half (next
344//! section) a channeled ion loses less per unit path than one in a random
345//! direction. The off-axis tails under that mode are in the 16000-ion table
346//! above.
347//!
348//! # Local electronic loss (Oen-Robinson)
349//!
350//! Under
351//! [`ElectronicLoss::EquipartitionLsOr`](super::ElectronicLoss::EquipartitionLsOr)
352//! half of the Lindhard-Scharff loss is nonlocal, taken along every flight
353//! segment (above). The other half is local: every partner of a collision
354//! step, so every lattice site that the search accepts within `p_max` of the
355//! path, takes the Oen-Robinson loss
356//! ([`OenRobinson::local_loss`](crate::ion::stopping::oen_robinson::OenRobinson::local_loss))
357//! at its own distance of closest approach `r_min`. That distance comes from
358//! the partner's impact parameter, which with thermal vibration is the
359//! distance to the displaced position. The energy is the energy before the
360//! collision step. The losses `Q_i` enter the energy-conserving scaling above.
361//! O. S. Oen and M. T. Robinson, Nucl. Instrum. Methods 132, 647 (1976).
362//!
363//! What is not modelled:
364//!
365//! * **The `p_max` cutoff on the local half.** Sites farther than `p_max`
366//!   from the path are not partners, so they take no local loss. The local
367//!   half is therefore below half of Lindhard-Scharff wherever `p_max` is not
368//!   large compared with the decay length `a / 0.3` of the Oen-Robinson loss.
369//!   The amorphous model has the same cutoff. In a channel, where the ion
370//!   stays farther than `p_max` from most rows, this cutoff is what lowers
371//!   the loss.
372//! * **Vibration beyond the displaced positions.** The local loss depends on
373//!   the vibration amplitude only through the displaced site positions; there
374//!   is no other thermal term.
375//! * **Verified constants.** The Oen-Robinson constants are not verified
376//!   against the paper
377//!   ([`OR_CONSTANTS_UNVERIFIED`]).
378//!   [`CrystalMetadata::electronic_constants_unverified`] records this in
379//!   the run metadata.
380//!
381//! Measured by `tests/crystal_electronic.rs` (`#[ignore]`d; release build):
382//! `R = E_local / E_nonlocal` of the primary (recoils not followed), counted
383//! until its energy first falls below 80 % of the beam energy. Ar 20 keV into
384//! Si, static lattice unless noted, seed 1, the crystal's `p_max` set to the
385//! amorphous constant-path radius 1.532 Å. Errors are delta-method standard
386//! errors.
387//!
388//! | Case | Histories | R | Notes |
389//! |---|---|---|---|
390//! | amorphous | 4000 | 0.8645 ± 0.0026 | the same at 30°/17° and 7°/22° |
391//! | 30°/17° | 4000 | 0.9304 ± 0.0025 | 1.076 ± 0.004 × amorphous |
392//! | 30°/17°, 300 K | 4000 | 0.9270 ± 0.0025 | 1.072 ± 0.004 × amorphous |
393//! | 7°/22° | 4000 | 0.8744 ± 0.0030 | 1.011 ± 0.005 × amorphous |
394//! | 30°/17°, `p_max` = nn | 4000 | 1.0033 ± 0.0026 | |
395//! | along <110> | 8000 | 0.2448 ± 0.0022 | 0.263 × the 30°/17° crystal value (0.9316, same n) |
396//! | along <100> | 8000 | 0.5600 ± 0.0010 | 0.601 × the 30°/17° crystal value |
397//! | along <110> / <100>, 300 K | 8000 | 0.2548 / 0.5719 | 30°/17° at 300 K: 0.9283 |
398//!
399//! Along both axes the local share is far below the off-axis direction (by
400//! more than 150 standard errors), as channeling requires.
401//!
402//! ## One direction is not a random medium (#250)
403//!
404//! Along the fixed direction 30°/17° the crystal ratio is 7.6 % above the
405//! amorphous one at the same `p_max` (5.5 % at 63°/8°). #226 asked for 5 %.
406//! This is not a counting error. It is what the rule of angular averages of
407//! J. Lindhard, Mat. Fys. Medd. Dan. Vid. Selsk. 34, no. 14 (1965), section 5,
408//! eqs. (5.1)-(5.3), predicts for one direction. The rule states that a
409//! quantity linear in the probability of a particle being at a point of the
410//! crystal "has the same angular average as in a random system", a system
411//! "with the same density, but without directional effects" (the paper names
412//! electronic and nuclear stopping at a fixed energy as examples, and calls
413//! this "compensation" of the directional effect). Only the average over
414//! directions equals the random medium. The channeling directions lie far
415//! below it (the <110> and <100> rows above), so the directions outside the
416//! channels lie above it.
417//!
418//! The engine obeys the rule. `direction_averaged_ratio_matches_amorphous`
419//! averages over 1181 beam directions that cover, evenly in solid angle, the
420//! sixth of the sphere nearest to `[100]` (16 histories each, 18896 in all, a
421//! different seed per direction). It counts over a fixed path of 100 Å,
422//! because the paper limits the rule to small energy loss: the energy window
423//! above lets a channeled ion travel farther, so it weights the channels more.
424//!
425//! | Ar 20 keV into Si, 100 Å path window | R | × amorphous |
426//! |---|---|---|
427//! | amorphous, all directions | 0.8416 ± 0.0011 | |
428//! | crystal, all directions | 0.8333 ± 0.0016 | 0.990 ± 0.002 |
429//! | crystal, all directions, 300 K | 0.8365 ± 0.0016 | 0.994 ± 0.002 |
430//! | amorphous, 30°/17° (4000 histories) | 0.8383 ± 0.0024 | |
431//! | crystal, 30°/17° (4000 histories) | 0.9120 ± 0.0021 | 1.088 ± 0.004 |
432//!
433//! The direction average meets the 5 % of #226 (the test asserts
434//! `max(3 σ, 5 %)`). Its remaining 1 % is not asserted to be zero: the rule
435//! holds for energy-conserving motion, and the ions here slow down. Single
436//! directions of that run (16 histories, so noisy) give R from 0.37, near a
437//! <110> axis, to 1.09. Their median is 0.87, above the amorphous value.
438//!
439//! `fixed_direction_partners_lean_to_small_impact_parameters` shows where the
440//! excess along 30°/17° comes from ([`BcaTally::partner`] reports every
441//! partner's impact parameter `p`):
442//!
443//! * The crystal makes as many collisions per unit path as the amorphous
444//!   model (1.005 times).
445//! * A partner at a given `p` takes the same local loss in both, within 3 %
446//!   in each tenth of the disc area. The two engines share the loss function.
447//! * The crystal's partners are not uniform over the disc of radius `p_max`.
448//!   The inner three tenths of its area hold 1.118 ± 0.004 of their uniform
449//!   share and the outer three tenths 0.903 ± 0.004. The amorphous disc is
450//!   uniform, and so is the crystal's in the direction average (every tenth
451//!   within 1.7 % of its share).
452//!
453//! So along this direction the ions pass closer to the atoms than a uniform
454//! flux would. That is the excess of R, because the Oen-Robinson loss falls
455//! steeply with `p`.
456//!
457//! Each mechanism of the collision step that could bias the partners was
458//! switched off in turn. The switches were temporary and are not in the tree.
459//! Same run as the table, 30°/17°, static:
460//!
461//! * **No deflection** (the direction never changes; 100 Å path window):
462//!   the disc is uniform and R is 0.8476, 1.011 × amorphous. So the lattice
463//!   search and the count per path are right, and the excess needs the
464//!   deflections.
465//! * **No exclusion of the sites just hit**: R is 0.9297 instead of 0.9304.
466//! * **One partner per step only**: R is unchanged (0.9304). Only 0.06 % of
467//!   the steps have two partners at this `p_max`.
468//! * **The same sites at randomised positions** (each site moved by its own
469//!   fixed vector, uniform over a cube of edge `a`, which removes the short-
470//!   range order and keeps the algorithm): R is the same with and without
471//!   deflection (0.8645 and 0.8633, 100 Å path window), and every tenth of
472//!   the disc is within 2.5 % of its share. So the half-tube search that starts
473//!   at the collision point and the ordering by path distance do not bias the
474//!   partners. The half-tube start was not switched off by itself. This
475//!   medium gave 2 % more partners per path than the amorphous model, with
476//!   and without deflection; that was not followed up.
477//! * **`p_max`**: the excess of the lattice over the randomised sites hardly
478//!   depends on it: 1.056, 1.053 and 1.046 at `p_max` = 1.53, 2.35 and
479//!   3.2 Å (energy window). It is not an effect of the cutoff.
480//!
481//! No repeated collision with one site was found in the 30°/17° run (0 of
482//! 137251 partners). Thermal vibration up to 3000 K and `q_max` hardly change
483//! the excess (#250).
484//!
485//! The energy window is not neutral either. In the run without deflection it
486//! gives R = 0.9127 and uneven tenths, where the path window gives 0.8476 and
487//! a uniform disc. A window that ends a history on a large loss selects
488//! histories when successive collisions are correlated, as they are along a
489//! fixed line through a lattice. With deflection the two windows give 1.076
490//! and 1.088 × amorphous.
491//!
492//! The fixed-direction quotients are therefore recorded values
493//! (`fixed_direction_local_to_nonlocal_ratio_is_above_amorphous`: 1.076
494//! static and 1.072 at 300 K, each within 0.02), not agreement with a
495//! reference. A run along one off-axis direction takes a few per cent more
496//! local electronic loss than the amorphous model at the same `p_max`.
497//!
498//! # Thermal vibration (step 21b)
499//!
500//! With [`CrystalTarget::thermal`] set, each lattice site the particle meets
501//! is displaced from its static position by a random vector `u` whose three
502//! Cartesian components are independent Gaussians of standard deviation
503//! `u1`, the one-dimensional RMS amplitude of the Debye model
504//! ([`crate::ion::crystal::debye`]) for the site's species at the target
505//! temperature [`Thermal::temperature_k`]. Displacements of different sites
506//! are uncorrelated; a correlated mode is not implemented. The issue that
507//! asked for this model (#181) names D. S. Gemmell, Rev. Mod. Phys. 46, 129
508//! (1974), doi:10.1103/RevModPhys.46.129, as its reference; that paper could
509//! not be opened, so nothing here is taken from it and none of its equations
510//! is cited. The amplitude and its source are those of the `debye` module.
511//!
512//! * **Encounter.** A site's displacement is drawn when the site first
513//!   appears in a search of the current *flight line* (a straight path of one
514//!   particle in one crystal) and is kept while the line goes on: the next
515//!   search segments of a channel see the same displaced atom. Any collision
516//!   ends the line (and so does a change of direction at a surface or
517//!   leaving the crystal), so every site met afterwards, by the projectile or
518//!   by a recoil, gets a fresh draw. A site is therefore displaced once per
519//!   encounter. Correlations in time (an atom met again later by the same
520//!   cascade) are not kept.
521//! * **What the displacement changes.** The impact parameter, the path
522//!   distance (so the order of the partners and the simultaneity test), the
523//!   direction `n_i` from the line to the atom, and so the deflection, the
524//!   recoil direction and (under `EquipartitionLsOr`) the local electronic
525//!   loss. A site's **region** (and so whether it is a partner at
526//!   all) and the **starting point of its recoil** are its static lattice
527//!   position, so that recoils, vacancies and tallies stay on lattice sites
528//!   and a recoil's own site is recognised as such; the difference is
529//!   `u1`, 0.065 Å for Si at 300 K (`THETA_D_SI`), against 2.35 Å between
530//!   neighbours.
531//! * **Search margin.** The static lattice is searched with the radius
532//!   `p_max + m` over the path range `[-m, L + q_max + m]`, with
533//!   `m =` [`THERMAL_MARGIN_U1`]` * u1` (largest `u1` of the crystal), and the
534//!   static selection rules (`0 <= s <= L + q_max`, `p <= p_max`) are then
535//!   applied to the displaced positions. A site whose displacement
536//!   perpendicular to the path exceeds `m` can be missed: the probability is
537//!   `exp(-m^2 / (2 u1^2)) = exp(-18)`, about `1.5e-8`, per site.
538//! * **Random numbers.** The displacements come from the history's
539//!   counter-based stream ([`crate::rng::stream`]), from a copy positioned at
540//!   word 2^66, a segment that neither the transport draws nor the lattice
541//!   translation (word 2^67) reach. Each displacement consumes exactly six
542//!   words ([`sample_gaussian_3d`]), drawn in the documented order of the
543//!   search results ([`crate::ion::crystal::search`], "Ordering"), and the
544//!   primary and its recoils share the stream in the order they are followed.
545//!   So a history's result depends only on `(seed, index)`, never on the
546//!   thread count, and turning vibration on changes no other draw.
547//! * **Static limit.** With every amplitude zero (`T = 0` and
548//!   [`Thermal::include_zero_point`] `= false`, a test switch) the thermal
549//!   code path gives the static model bit for bit
550//!   (`tests/crystal_thermal.rs`). With the zero-point term (the physical
551//!   case) `T = 0` still vibrates: `u1` = 0.045 Å for Si (0.088 Å at 600 K).
552//! * **Not changed.** The lattice constant is the cited one at its own
553//!   temperature (no thermal expansion). The electronic-loss model is the
554//!   one of the static lattice: the nonlocal loss along a segment, and under
555//!   `EquipartitionLsOr` the local loss at each partner's closest approach.
556//!   The local loss sees the vibration only through the displaced position
557//!   (the impact parameter, so `r_min`); it has no other thermal term.
558//!
559//! DISPLATH (Tier A, above; same commit) also perturbs lattice atoms by
560//! independent Gaussian displacements of a Debye amplitude, but draws them
561//! once per cell and cascade (`LatticeSiteCoordinatesBase!` and
562//! `TemperatureToSigma` in `src/geometry.jl`); the per-encounter scheme here
563//! is this crate's, as #181 asks, and no DISPLATH code was ported for it.
564//!
565//! # What is not here
566//!
567//! The lattice is **perfect** (static, or vibrating as above) and
568//! stays perfect: a displaced atom leaves no vacancy, so a later particle can
569//! still collide with its site (dynamic damage, dechanneling and
570//! amorphization are later steps). The impact-parameter-dependent electronic
571//! loss is the local Oen-Robinson half, taken only at sites within `p_max`
572//! (see "Local electronic loss" for what that leaves out). Boundaries of a
573//! periodic voxel axis do not continue the lattice. The equation-level treatment of Robinson and
574//! Torrens, Phys. Rev. B 9, 5008 (1974) (MARLOWE, Tier C) was not available
575//! to read when this was written; the choices above are DISPLATH's, not
576//! quotations of that paper.
577//!
578//! [`Geometry`]: crate::geometry::Geometry
579//! [`BcaTally::partner`]: super::BcaTally::partner
580//! [`LatticeSearch`]: crate::ion::crystal::LatticeSearch
581
582use crate::geometry::Flight;
583use crate::ion::crystal::debye::{sample_gaussian_3d, ThermalVibration};
584use crate::ion::crystal::search::{compare, path_metrics, unit_direction};
585use crate::ion::crystal::{Lattice, LatticeSearch, Orientation};
586use crate::ion::scattering::closest_approach;
587use crate::ion::stopping::oen_robinson::OR_CONSTANTS_UNVERIFIED;
588use crate::ion::stopping::Ion;
589use crate::rng::ParticleRng;
590use crate::units::J_PER_EV;
591use crate::vec3::dot;
592
593use super::{
594    kinematics, Bca, BcaError, BcaTally, ElectronicChannel, EnergyBudget, MeanFreePath, Particle,
595    Scratch,
596};
597pub(super) use crate::ion::crystal::Candidate;
598
599/// A lattice site: its search cell and index in the cell ([`Lattice::orthogonal_cell`]).
600pub(super) type SiteId = ([i64; 3], u8);
601
602/// Search margin of the thermal model, in units of the largest
603/// one-dimensional RMS displacement `u1` of the crystal's species (module
604/// docs, "Thermal vibration").
605pub const THERMAL_MARGIN_U1: f64 = 6.0;
606
607/// How [`Thermal`] displacements are sampled, as recorded in
608/// [`ThermalMetadata::sampling`].
609pub const THERMAL_SAMPLING: &str = "per-encounter, uncorrelated: each lattice site met along a \
610     straight flight line gets one isotropic Gaussian displacement (standard deviation u1 per \
611     Cartesian axis, Debye model) drawn from the history's thermal stream; redrawn after every \
612     collision";
613
614/// Thermal vibration of the lattice atoms of a [`CrystalTarget`] (step 21b;
615/// module docs, "Thermal vibration").
616#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
617pub struct Thermal {
618    /// Target temperature, K (finite, `>= 0`). This sets the vibration
619    /// amplitude only: the lattice constant stays the cited value at its own
620    /// temperature ([`Lattice::temperature_k`]); no thermal expansion is
621    /// applied.
622    pub temperature_k: f64,
623    /// Debye temperature, K, used for every species of the crystal with that
624    /// species' own mass ([`crate::ion::crystal::debye`] module docs; the
625    /// `THETA_D_*` constants there are cited defaults).
626    pub debye_temperature_k: f64,
627    /// Include the zero-point term of the Debye amplitude (`true` for any
628    /// physical run). `false` keeps only the thermal part, so that `T = 0`
629    /// gives zero amplitude: a **test switch** (the static-lattice identity
630    /// test uses it), not a physical model.
631    pub include_zero_point: bool,
632}
633
634impl Thermal {
635    /// Vibration at `temperature_k` with Debye temperature
636    /// `debye_temperature_k`, zero-point term included.
637    pub fn new(temperature_k: f64, debye_temperature_k: f64) -> Self {
638        Self {
639            temperature_k,
640            debye_temperature_k,
641            include_zero_point: true,
642        }
643    }
644}
645
646/// Run metadata of one crystal of a [`Bca`] ([`Bca::crystal_metadata`]): what
647/// a result needs to be reproduced from its own header.
648#[derive(Debug, Clone, PartialEq, serde::Serialize)]
649pub struct CrystalMetadata {
650    /// Regions filled by this crystal.
651    pub regions: Vec<usize>,
652    /// Lattice constant `a`, m (the cube edge of a cubic lattice, the basal
653    /// constant of a hexagonal one).
654    pub lattice_constant_m: f64,
655    /// Temperature of the cited lattice constant, K (not the target
656    /// temperature; see [`Thermal::temperature_k`]). `None` when the source
657    /// states none ([`Lattice::temperature_k`]).
658    pub lattice_constant_temperature_k: Option<f64>,
659    /// Miller indices of the surface normal and the in-plane reference.
660    pub normal_hkl: [i32; 3],
661    /// Indices of the in-plane reference direction.
662    pub reference_uvw: [i32; 3],
663    /// Tilt, twist and wafer rotation, rad.
664    pub tilt_rad: f64,
665    /// Twist, rad.
666    pub twist_rad: f64,
667    /// Wafer rotation, rad.
668    pub wafer_rotation_rad: f64,
669    /// Search parameters in use, m.
670    pub p_max_m: f64,
671    /// Simultaneous-collision window, m.
672    pub q_max_m: f64,
673    /// Search segment length, m.
674    pub search_length_m: f64,
675    /// Thermal vibration; `None` for a static lattice.
676    pub thermal: Option<ThermalMetadata>,
677    /// `true` when the run takes the local Oen-Robinson loss
678    /// ([`ElectronicLoss::EquipartitionLsOr`](super::ElectronicLoss::EquipartitionLsOr))
679    /// and the constants of that model are not verified against the paper
680    /// ([`OR_CONSTANTS_UNVERIFIED`]). Channeled ranges computed with the local
681    /// loss depend on those constants. `false` for a nonlocal-only run; a
682    /// `false` value is left out of the serialised form.
683    #[serde(skip_serializing_if = "std::ops::Not::not")]
684    pub electronic_constants_unverified: bool,
685}
686
687/// The thermal part of [`CrystalMetadata`].
688#[derive(Debug, Clone, PartialEq, serde::Serialize)]
689pub struct ThermalMetadata {
690    /// The input.
691    pub input: Thermal,
692    /// One-dimensional RMS displacement `u1` per species, as
693    /// `(Z, mass in u, u1 in m)`, in order of first appearance in the
694    /// lattice basis.
695    pub rms_1d_m: Vec<(u8, f64, f64)>,
696    /// Extra search radius and path margin, m ([`THERMAL_MARGIN_U1`] times
697    /// the largest `u1`).
698    pub search_margin_m: f64,
699    /// The sampling rule ([`THERMAL_SAMPLING`]).
700    pub sampling: &'static str,
701}
702
703/// Precomputed thermal data of one crystal.
704#[derive(Debug, Clone)]
705struct ThermalData {
706    meta: ThermalMetadata,
707    /// `u1` per conventional-cell site (index = [`Candidate::basis`]), m.
708    sigma: Vec<f64>,
709    margin: f64,
710}
711
712/// A site closer than this fraction of the lattice constant to the particle
713/// is the particle's own starting site, not a partner.
714const SELF_REL: f64 = 1e-6;
715
716/// Default `q_max`, as a fraction of the nearest-neighbour distance (module
717/// docs, "Choice of the search parameters").
718pub const DEFAULT_Q_MAX_FRACTION: f64 = 0.05;
719
720/// Largest relative density mismatch between lattice and material.
721const DENSITY_TOLERANCE: f64 = 0.05;
722
723/// A crystal for [`Bca::with_crystal`]: the lattice, the orientation of the
724/// target in the lab frame and the three search parameters.
725#[derive(Debug, Clone)]
726pub struct CrystalTarget {
727    /// The lattice (cubic or hexagonal).
728    pub lattice: Lattice,
729    /// Wafer cut and angles. Only its lab-to-crystal rotation is used.
730    pub orientation: Orientation,
731    /// Largest impact parameter of a partner, m. Default `None`: the
732    /// nearest-neighbour distance of the lattice. Steering by the atoms of a
733    /// channel wall needs partners out to about that distance; smaller radii
734    /// (DISPLATH's examples use `a/3` and the amorphous constant-path radius
735    /// `(π N^(2/3))^(-1/2)`, `a/2/π^0.5` for silicon) leave the channeled
736    /// fraction too small. See the module docs, "Choice of the search
737    /// parameters".
738    pub p_max_m: Option<f64>,
739    /// Largest path distance between simultaneous partners, m. Default
740    /// `None`: [`DEFAULT_Q_MAX_FRACTION`] of the nearest-neighbour distance.
741    pub q_max_m: Option<f64>,
742    /// Length of one search segment, m. Default `None`: the lattice
743    /// constant. Affects speed only (a segment with no partner is followed by
744    /// another), never the result.
745    pub search_length_m: Option<f64>,
746    /// Thermal vibration of the lattice atoms. Default `None`: a static,
747    /// perfect lattice (the 20b model, unchanged bit for bit).
748    pub thermal: Option<Thermal>,
749}
750
751impl CrystalTarget {
752    /// A static crystal with the default search parameters.
753    pub fn new(lattice: Lattice, orientation: Orientation) -> Self {
754        Self {
755            lattice,
756            orientation,
757            p_max_m: None,
758            q_max_m: None,
759            search_length_m: None,
760            thermal: None,
761        }
762    }
763
764    /// Whether `material` can fill a crystal region of this lattice: it must
765    /// contain every element of the lattice and its atom density must agree
766    /// with the lattice's to 5 %. The same rule [`Bca::with_crystal`] applies
767    /// to each region, available before an engine exists (input checking).
768    pub fn check_material(&self, material: &crate::material::Material) -> Result<(), String> {
769        let lat = &self.lattice;
770        for (z, _) in lat.conventional_cell_sites() {
771            if !material.components().iter().any(|c| c.z() == z) {
772                return Err(format!("the material has no element Z={z} of the lattice"));
773            }
774        }
775        let n_lat = lat.atom_number_density();
776        let n = material.atom_number_density();
777        if (n / n_lat - 1.0).abs() > DENSITY_TOLERANCE {
778            return Err(format!(
779                "material atom density {n:.4e} m^-3 differs from the lattice's \
780                 {n_lat:.4e} m^-3 by more than 5 %"
781            ));
782        }
783        Ok(())
784    }
785
786    /// The same crystal with thermal vibration `thermal`.
787    pub fn with_thermal(mut self, thermal: Thermal) -> Self {
788        self.thermal = Some(thermal);
789        self
790    }
791}
792
793/// Validated, precomputed crystal data.
794#[derive(Debug, Clone)]
795pub(super) struct CrystalData {
796    search: LatticeSearch,
797    orientation: Orientation,
798    a: f64,
799    /// Edges of the rectangular cell that tiles the crystal, m
800    /// ([`Lattice::orthogonal_cell`]); `[a; 3]` for a cubic lattice.
801    edges: [f64; 3],
802    a_temperature_k: Option<f64>,
803    p_max: f64,
804    q_max: f64,
805    search_len: f64,
806    thermal: Option<ThermalData>,
807}
808
809impl CrystalData {
810    /// Edges of the rectangular cell that tiles the crystal, m: the domain
811    /// of the per-history random translation of the lattice.
812    pub(super) fn cell_edges(&self) -> [f64; 3] {
813        self.edges
814    }
815
816    /// Whether this crystal's sites vibrate (a thermal stream is needed).
817    pub(super) fn is_thermal(&self) -> bool {
818        self.thermal.is_some()
819    }
820
821    /// Crystal-frame position of a lab point for the history translation
822    /// `shift`.
823    fn to_crystal(&self, lab: [f64; 3], shift: [f64; 3]) -> [f64; 3] {
824        let c = self.orientation.to_crystal(lab);
825        [c[0] + shift[0], c[1] + shift[1], c[2] + shift[2]]
826    }
827
828    /// Lab position of a crystal-frame point.
829    fn to_lab(&self, c: [f64; 3], shift: [f64; 3]) -> [f64; 3] {
830        self.orientation
831            .to_lab([c[0] - shift[0], c[1] - shift[1], c[2] - shift[2]])
832    }
833}
834
835/// One partner of the current collision step.
836#[derive(Debug, Clone, Copy)]
837pub(super) struct Partner {
838    id: SiteId,
839    /// Path distance of the closest approach, m.
840    s: f64,
841    /// Impact parameter, m.
842    b: f64,
843    /// Unit vector (lab) from the line to the site.
844    n: [f64; 3],
845    /// Site position (lab), m.
846    site: [f64; 3],
847    /// Region of the site and element index in its material.
848    region: usize,
849    j: usize,
850    /// Transfer, recoil direction and local loss of the pair collision.
851    t: f64,
852    rdir: [f64; 3],
853    q: f64,
854}
855
856/// Nearest-neighbour distance of `lattice`, m, over the sites of its
857/// rectangular cell ([`Lattice::orthogonal_cell`]) and the 26 cells around
858/// it. Every cell edge is at least `a`, longer than any nearest-neighbour
859/// distance, so the neighbouring cells suffice.
860fn nearest_neighbour(lattice: &Lattice) -> f64 {
861    let a = lattice.lattice_constant();
862    let (e, sites) = lattice.orthogonal_cell();
863    let mut best = f64::INFINITY;
864    for (_, f) in &sites {
865        for (_, g) in &sites {
866            for i in -1..=1 {
867                for j in -1..=1 {
868                    for k in -1..=1 {
869                        let dx = e[0] * (g[0] + f64::from(i) - f[0]);
870                        let dy = e[1] * (g[1] + f64::from(j) - f[1]);
871                        let dz = e[2] * (g[2] + f64::from(k) - f[2]);
872                        let d = (dx * dx + dy * dy + dz * dz).sqrt();
873                        if d > 1e-6 * a {
874                            best = best.min(d);
875                        }
876                    }
877                }
878            }
879        }
880    }
881    best
882}
883
884impl<'a> Bca<'a> {
885    /// Use the crystal flight model in `regions` (see the module docs). The
886    /// regions are indices into the geometry's regions: layer indices of a
887    /// stack, flat voxel indices of a voxel grid.
888    ///
889    /// # Errors
890    /// [`BcaError::InvalidConfig`] if the configuration is not the constant
891    /// free path without weak collisions, a region does not exist or already
892    /// belongs to a crystal, a search parameter is not finite and positive, a
893    /// region's material lacks a lattice element, or its atom density differs
894    /// from the lattice's by more than 5 %.
895    pub fn with_crystal(
896        mut self,
897        target: CrystalTarget,
898        regions: &[usize],
899    ) -> Result<Self, BcaError> {
900        let bad = |m: String| BcaError::InvalidConfig(m);
901        if !matches!(self.config.mean_free_path, MeanFreePath::Constant)
902            || self.config.weak_collisions > 0
903        {
904            return Err(bad(
905                "crystal regions need the constant free path and no weak collisions".into(),
906            ));
907        }
908        if regions.is_empty() {
909            return Err(bad("a crystal needs at least one region".into()));
910        }
911        let lat = &target.lattice;
912        let a = lat.lattice_constant();
913        let n_lat = lat.atom_number_density();
914        let zs: Vec<u8> = {
915            let mut v: Vec<u8> = lat.conventional_cell_sites().iter().map(|s| s.0).collect();
916            v.sort_unstable();
917            v.dedup();
918            v
919        };
920        if self.region_crystal.is_empty() {
921            self.region_crystal = vec![None; self.geometry.n_regions()];
922        }
923        let ci = self.crystals.len();
924        for &r in regions {
925            if r >= self.geometry.n_regions() {
926                return Err(bad(format!("crystal region {r} does not exist")));
927            }
928            if self.region_crystal[r].is_some() {
929                return Err(bad(format!("region {r} already belongs to a crystal")));
930            }
931            let lay = self.lay(r);
932            for &z in &zs {
933                if !lay.elems.iter().any(|e| e.z == z) {
934                    return Err(bad(format!(
935                        "region {r}: the material has no element Z={z} of the lattice"
936                    )));
937                }
938            }
939            if (lay.n / n_lat - 1.0).abs() > DENSITY_TOLERANCE {
940                return Err(bad(format!(
941                    "region {r}: material atom density {:.4e} m^-3 differs from the \
942                     lattice's {n_lat:.4e} m^-3 by more than 5 %",
943                    lay.n
944                )));
945            }
946        }
947        let nn = nearest_neighbour(lat);
948        let p_max = target.p_max_m.unwrap_or(nn);
949        let q_max = target.q_max_m.unwrap_or(DEFAULT_Q_MAX_FRACTION * nn);
950        let search_len = target.search_length_m.unwrap_or(a);
951        for (name, v) in [
952            ("p_max_m", p_max),
953            ("q_max_m", q_max),
954            ("search_length_m", search_len),
955        ] {
956            if !(v.is_finite() && v > 0.0) {
957                return Err(bad(format!("{name} = {v} must be finite and positive")));
958            }
959        }
960        let thermal = match target.thermal {
961            None => None,
962            Some(th) => {
963                // One Debye model per species, with the mass the engine uses
964                // for that species (the material's element in the first
965                // region; species data are shared by all regions).
966                let lay = self.lay(regions[0]);
967                let mut rms: Vec<(u8, f64, f64)> = Vec::new();
968                for (z, _) in lat.conventional_cell_sites() {
969                    if rms.iter().any(|r| r.0 == z) {
970                        continue;
971                    }
972                    let elem = lay
973                        .elems
974                        .iter()
975                        .find(|e| e.z == z)
976                        .expect("lattice elements were checked above");
977                    let mass = self.species[elem.species].ion.mass_amu();
978                    let v = ThermalVibration::new(th.debye_temperature_k, mass, th.temperature_k)
979                        .map_err(|e| bad(format!("thermal vibration: {e}")))?;
980                    let u1_sq = if th.include_zero_point {
981                        v.mean_square_1d()
982                    } else {
983                        v.thermal_only_mean_square_1d()
984                    };
985                    rms.push((z, mass, u1_sq.sqrt()));
986                }
987                // Indexed by `Candidate::basis`, i.e. by the sites of the
988                // search cell (`Lattice::orthogonal_cell`), which for a
989                // hexagonal lattice holds each basis site twice.
990                let sigma: Vec<f64> = lat
991                    .orthogonal_cell()
992                    .1
993                    .iter()
994                    .map(|(z, _)| rms.iter().find(|r| r.0 == *z).map_or(0.0, |r| r.2))
995                    .collect();
996                let u1_max = sigma.iter().copied().fold(0.0, f64::max);
997                let margin = THERMAL_MARGIN_U1 * u1_max;
998                Some(ThermalData {
999                    meta: ThermalMetadata {
1000                        input: th,
1001                        rms_1d_m: rms,
1002                        search_margin_m: margin,
1003                        sampling: THERMAL_SAMPLING,
1004                    },
1005                    sigma,
1006                    margin,
1007                })
1008            }
1009        };
1010        for &r in regions {
1011            self.region_crystal[r] = Some(ci);
1012        }
1013        let search = LatticeSearch::new(lat);
1014        let edges = search.cell_edges();
1015        self.crystals.push(CrystalData {
1016            search,
1017            orientation: target.orientation,
1018            a,
1019            edges,
1020            a_temperature_k: lat.temperature_k(),
1021            p_max,
1022            q_max,
1023            search_len,
1024            thermal,
1025        });
1026        Ok(self)
1027    }
1028
1029    /// Run metadata of every crystal ([`Bca::with_crystal`] order): lattice,
1030    /// orientation, search parameters and the thermal model with its target
1031    /// temperature. Empty for an amorphous engine.
1032    pub fn crystal_metadata(&self) -> Vec<CrystalMetadata> {
1033        let local_or = self.config.electronic == super::ElectronicLoss::EquipartitionLsOr;
1034        self.crystals
1035            .iter()
1036            .enumerate()
1037            .map(|(ci, cr)| CrystalMetadata {
1038                regions: (0..self.region_crystal.len())
1039                    .filter(|&r| self.region_crystal[r] == Some(ci))
1040                    .collect(),
1041                lattice_constant_m: cr.a,
1042                lattice_constant_temperature_k: cr.a_temperature_k,
1043                normal_hkl: cr.orientation.normal_hkl(),
1044                reference_uvw: cr.orientation.reference_uvw(),
1045                tilt_rad: cr.orientation.tilt_rad(),
1046                twist_rad: cr.orientation.twist_rad(),
1047                wafer_rotation_rad: cr.orientation.wafer_rotation_rad(),
1048                p_max_m: cr.p_max,
1049                q_max_m: cr.q_max,
1050                search_length_m: cr.search_len,
1051                thermal: cr.thermal.as_ref().map(|t| t.meta.clone()),
1052                electronic_constants_unverified: local_or && OR_CONSTANTS_UNVERIFIED,
1053            })
1054            .collect()
1055    }
1056
1057    /// The crystal of `region`, if it is a crystal region.
1058    #[inline]
1059    pub(super) fn crystal_at(&self, region: usize) -> Option<usize> {
1060        self.region_crystal.get(region).copied().flatten()
1061    }
1062
1063    /// Region and element index of a lattice site that may be a partner:
1064    /// inside the target, in a region of crystal `ci`.
1065    fn site_region(&self, ci: usize, site_lab: [f64; 3], z: u8) -> Option<(usize, usize)> {
1066        let r = self.geometry.locate(site_lab)?;
1067        if self.crystal_at(r) != Some(ci) {
1068            return None;
1069        }
1070        let j = self.lay(r).elems.iter().position(|e| e.z == z)?;
1071        Some((r, j))
1072    }
1073
1074    /// One step of a particle in crystal `ci`: a flight to the next
1075    /// collision (or to a surface or interface, or one search segment) and
1076    /// the collision there. Returns `true` if the particle is finished.
1077    #[allow(clippy::too_many_arguments)]
1078    pub(super) fn crystal_step<T: BcaTally>(
1079        &self,
1080        ci: usize,
1081        shift: [f64; 3],
1082        p: &mut Particle,
1083        pending: &mut Vec<Particle>,
1084        budget: &mut EnergyBudget,
1085        tally: &mut T,
1086        scratch: &mut Scratch,
1087        trng: &mut Option<ParticleRng>,
1088    ) -> Result<bool, crate::ion::stopping::StoppingError> {
1089        let cr = &self.crystals[ci];
1090        let cutoff = self.species[p.species].cutoff_ev;
1091        let Scratch {
1092            cands,
1093            last,
1094            targets,
1095            homes,
1096            encounter,
1097            displaced,
1098            displaced_next,
1099            displaced_line,
1100            ..
1101        } = scratch;
1102        let o = cr.to_crystal(p.pos, shift);
1103        let d = cr.orientation.to_crystal(p.dir);
1104        let self_r2 = (SELF_REL * cr.a) * (SELF_REL * cr.a);
1105        match &cr.thermal {
1106            None => cr
1107                .search
1108                .search_into(o, d, cr.p_max, cr.search_len + cr.q_max, cands)
1109                .expect("particle state is finite"),
1110            Some(th) => {
1111                // Thermal vibration (module docs): search the static lattice
1112                // with a margin, displace every site found (once per
1113                // encounter: a site keeps its displacement while the flight
1114                // line is unchanged), then apply the static selection rules
1115                // to the displaced positions.
1116                let m = th.margin;
1117                let dn = unit_direction(d);
1118                let ob = [o[0] - m * dn[0], o[1] - m * dn[1], o[2] - m * dn[2]];
1119                cr.search
1120                    .search_into(
1121                        ob,
1122                        d,
1123                        cr.p_max + m,
1124                        cr.search_len + cr.q_max + 2.0 * m,
1125                        cands,
1126                    )
1127                    .expect("particle state is finite");
1128                let line = (ci, [d[0].to_bits(), d[1].to_bits(), d[2].to_bits()]);
1129                if *displaced_line != Some(line) {
1130                    displaced.clear();
1131                    *displaced_line = Some(line);
1132                }
1133                let rng = trng
1134                    .as_mut()
1135                    .expect("a thermal crystal has a thermal stream");
1136                displaced_next.clear();
1137                encounter.clear();
1138                let (pm2, len) = (cr.p_max * cr.p_max, cr.search_len + cr.q_max);
1139                for c in cands.iter() {
1140                    // The particle's own starting site (a recoil starts on its
1141                    // static site) is never a partner.
1142                    let (s0, p0sq) = path_metrics(o, dn, c.position);
1143                    let p0 = p0sq.sqrt();
1144                    if s0 * s0 + p0 * p0 < self_r2 {
1145                        continue;
1146                    }
1147                    let id = (c.cell, c.basis);
1148                    let u = match displaced.get(&id) {
1149                        Some(&u) => u,
1150                        None => sample_gaussian_3d(th.sigma[usize::from(c.basis)], rng),
1151                    };
1152                    displaced_next.insert(id, u);
1153                    let r = [
1154                        c.position[0] + u[0],
1155                        c.position[1] + u[1],
1156                        c.position[2] + u[2],
1157                    ];
1158                    let (s, p2) = path_metrics(o, dn, r);
1159                    if s >= 0.0 && s <= len && p2 <= pm2 {
1160                        encounter.push((
1161                            Candidate {
1162                                position: r,
1163                                s,
1164                                p: p2.sqrt(),
1165                                ..*c
1166                            },
1167                            c.position,
1168                        ));
1169                    }
1170                }
1171                // Sites no longer in the window are behind the particle on
1172                // this line and can never be found again.
1173                std::mem::swap(displaced, displaced_next);
1174                encounter.sort_by(|x, y| compare(&x.0, &y.0));
1175                cands.clear();
1176                homes.clear();
1177                for &(c, h) in encounter.iter() {
1178                    cands.push(c);
1179                    homes.push(h);
1180                }
1181            }
1182        }
1183        let thermal = cr.thermal.is_some();
1184        // Static (lattice-site) position of candidate `i`, crystal frame: the
1185        // site's region and the recoil's starting point.
1186        let home = |i: usize| if thermal { homes[i] } else { cands[i].position };
1187
1188        // The nearest valid partner within one search segment.
1189        let usable = |i: usize| -> Option<(usize, usize)> {
1190            let c = &cands[i];
1191            if c.s * c.s + c.p * c.p < self_r2 || last.contains(&(c.cell, c.basis)) {
1192                return None;
1193            }
1194            self.site_region(ci, cr.to_lab(home(i), shift), c.z)
1195        };
1196        let hit = (0..cands.len()).find(|&i| cands[i].s <= cr.search_len && usable(i).is_some());
1197        let limit = hit.map_or(cr.search_len, |i| cands[i].s);
1198
1199        match self.geometry.flight(p.layer, p.pos, p.dir, limit) {
1200            Flight::Event(ex) => {
1201                let from = p.pos;
1202                p.pos = ex.at;
1203                self.electronic_nonlocal(p, from, ex.distance, budget, tally)?;
1204                if p.energy_ev < cutoff {
1205                    return Ok(false);
1206                }
1207                return Ok(self.apply_event(p, ex.outcome, budget, tally));
1208            }
1209            Flight::Clear { region } => {
1210                let from = p.pos;
1211                Self::advance(p, limit);
1212                p.layer = region;
1213                self.electronic_nonlocal(p, from, limit, budget, tally)?;
1214            }
1215        }
1216        let Some(hit) = hit else {
1217            return Ok(false);
1218        };
1219        if p.energy_ev < cutoff || self.crystal_at(p.layer) != Some(ci) {
1220            return Ok(false);
1221        }
1222
1223        // Partners: the nearest, then every later site that is simultaneous
1224        // with all partners accepted so far.
1225        targets.clear();
1226        let s0 = cands[hit].s;
1227        let sp2 = cr.p_max * cr.p_max;
1228        for (i, c) in cands.iter().enumerate().skip(hit) {
1229            if c.s - s0 > cr.q_max {
1230                break;
1231            }
1232            let Some((region, j)) = usable(i) else {
1233                continue;
1234            };
1235            let simultaneous = targets.iter().all(|t: &Partner| {
1236                let dl = c.s - t.s;
1237                dl <= cr.q_max && t.b * t.b + dl * dl <= sp2 && c.p * c.p + dl * dl <= sp2
1238            });
1239            if !simultaneous {
1240                continue;
1241            }
1242            // Unit vector from the line to the site, in the lab frame.
1243            let q = [
1244                c.position[0] - o[0] - c.s * d[0],
1245                c.position[1] - o[1] - c.s * d[1],
1246                c.position[2] - o[2] - c.s * d[2],
1247            ];
1248            let qn = dot(q, q).sqrt();
1249            let n_c = if qn > 1e-9 * cr.a {
1250                [q[0] / qn, q[1] / qn, q[2] / qn]
1251            } else {
1252                // On the line: any perpendicular does (the same rule as the
1253                // azimuth of an amorphous collision).
1254                kinematics::rotate_sc(d, (1.0, 0.0), (1.0, 0.0))
1255            };
1256            targets.push(Partner {
1257                id: (c.cell, c.basis),
1258                s: c.s,
1259                b: c.p,
1260                n: kinematics::normalize(cr.orientation.to_lab(n_c)),
1261                site: cr.to_lab(home(i), shift),
1262                region,
1263                j,
1264                t: 0.0,
1265                rdir: [0.0; 3],
1266                q: 0.0,
1267            });
1268        }
1269        last.clear();
1270        last.extend(targets.iter().map(|t| t.id));
1271        // The collision ends this flight line: every site met after it is a
1272        // new encounter with a fresh displacement.
1273        displaced.clear();
1274        *displaced_line = None;
1275        self.crystal_collide(p, targets, pending, budget, tally)?;
1276        Ok(false)
1277    }
1278
1279    /// The simultaneous collision of `p` with `targets` (module docs).
1280    fn crystal_collide<T: BcaTally>(
1281        &self,
1282        p: &mut Particle,
1283        targets: &mut [Partner],
1284        pending: &mut Vec<Particle>,
1285        budget: &mut EnergyBudget,
1286        tally: &mut T,
1287    ) -> Result<(), crate::ion::stopping::StoppingError> {
1288        let ns = self.species.len();
1289        let e0 = p.energy_ev;
1290        let d0 = p.dir;
1291        let m_p = p.mass_amu;
1292        let beta_min = self.table.spec().beta_min;
1293        let p0 = (2.0 * m_p * e0).sqrt();
1294        let mut mom = [p0 * d0[0], p0 * d0[1], p0 * d0[2]];
1295        let (mut sum_t, mut sum_q) = (0.0, 0.0);
1296        for t in targets.iter() {
1297            tally.partner(p, t.b, targets.len());
1298        }
1299        for t in targets.iter_mut() {
1300            let elem = &self.lay(t.region).elems[t.j];
1301            let pair = self.pairs[p.species * ns + elem.species];
1302            let eps = e0 * pair.eps_per_ev;
1303            let beta = (t.b / pair.a).max(beta_min);
1304            let tan_half = self.half_angle_tan(eps, beta);
1305            let c = 1.0 / (1.0 + tan_half * tan_half).sqrt();
1306            let s = tan_half * c;
1307            t.t = (pair.gamma * e0 * s * s).min(e0);
1308            // Recoil leaves at (pi - Theta)/2 from the incoming direction,
1309            // towards the site: cos = sin(Theta/2), sin = cos(Theta/2).
1310            t.rdir = [
1311                s * d0[0] + c * t.n[0],
1312                s * d0[1] + c * t.n[1],
1313                s * d0[2] + c * t.n[2],
1314            ];
1315            if self.config.electronic == super::ElectronicLoss::EquipartitionLsOr {
1316                let r_min = closest_approach(self.screening, eps, beta) * pair.a;
1317                let ion: &Ion = &self.species[p.species].ion;
1318                let q = self.mix.0.local_loss(ion, elem.z, e0, r_min)? / J_PER_EV;
1319                t.q = q.min(e0 - sum_q).max(0.0);
1320                sum_q += t.q;
1321            }
1322            let pt = (2.0 * self.species[elem.species].ion.mass_amu() * t.t).sqrt();
1323            for (m, r) in mom.iter_mut().zip(t.rdir) {
1324                *m -= pt * r;
1325            }
1326            sum_t += t.t;
1327        }
1328        // Energy conservation with the local losses: scale the transfers and
1329        // the projectile's remaining energy by a common factor.
1330        let pm2 = dot(mom, mom);
1331        let e_p = pm2 / (2.0 * m_p);
1332        let need = (e0 - sum_q).max(0.0);
1333        let lambda = if e_p + sum_t > 0.0 {
1334            need / (e_p + sum_t)
1335        } else {
1336            1.0
1337        };
1338        if pm2 > 0.0 {
1339            p.dir = kinematics::normalize(mom);
1340        }
1341        p.energy_ev = lambda * e_p;
1342        for t in targets.iter() {
1343            if t.q > 0.0 {
1344                budget.electronic_local += t.q;
1345                tally.electronic(p, p.pos, ElectronicChannel::Local, t.q);
1346            }
1347        }
1348        for t in targets.iter() {
1349            let rdir = kinematics::normalize(t.rdir);
1350            self.emit_recoil(
1351                p.generation,
1352                t.site,
1353                t.region,
1354                t.j,
1355                lambda * t.t,
1356                || rdir,
1357                pending,
1358                budget,
1359                tally,
1360            );
1361        }
1362        Ok(())
1363    }
1364}