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}