lindhard/ion/crystal/orientation.rs
1//! Wafer cut and beam angles: the lab-to-crystal rotation from
2//! (tilt, twist, wafer rotation).
3//!
4//! # Conventions
5//!
6//! The figure and a worked example are in `docs/crystal-orientation.md`.
7//!
8//! * **Lab frame** (as in [`crate::geometry`]): `+x` is depth, pointing into
9//! the target along the surface normal; `y`, `z` are lateral.
10//! * **Wafer cut.** The surface is the plane `(hkl)` (Miller indices on the
11//! conventional cell: the cube, or the hexagonal `a1, a2, c`; hexagonal
12//! cuts may also be given as `(hkil)` and `[uvtw]`, see
13//! [`Orientation::new_miller_bravais`]). Its normal `n = g_hkl / |g_hkl|`
14//! ([`super::Lattice::plane_normal`]) is the crystal direction of lab
15//! `+x`, i.e. `(hkl)` names the **inward** normal: at zero tilt the beam
16//! travels along `n`. (To name the outward normal `(hkl)`, pass
17//! `(-h, -k, -l)`; it matters only for polar faces.) The **reference
18//! direction** `[uvw]` is a lattice direction in the surface plane (the
19//! zone law `h u + k v + l w = 0` must hold); its unit vector `r` is the
20//! crystal direction of lab `+y` at zero wafer rotation. The third axis is
21//! `t = n x r`, the crystal direction of lab `+z`, so `(n, r, t)` is
22//! right-handed like `(x, y, z)`.
23//! * **Tilt** `θ` is the polar angle of the beam from lab `+x` (the inward
24//! normal), in `[0, π/2)`.
25//! * **Twist** `φ` is the azimuth of the beam about lab `+x`, measured in the
26//! surface plane from lab `+y` towards lab `+z`. The beam direction in the
27//! lab is `(cos θ, sin θ cos φ, sin θ sin φ)`, the same as
28//! [`crate::ion::bca::Beam::direction`] with `polar_rad = θ` and
29//! `azimuth_rad = φ`.
30//! * **Wafer rotation** `ω` turns the wafer (and the crystal) about lab `+x`
31//! by `ω`, right-handed: the reference direction `r` moves from lab `+y`
32//! towards lab `+z`.
33//!
34//! So the crystal-to-lab rotation is `M = R_x(ω) B`, where `B` has rows
35//! `n`, `r`, `t` (it takes crystal components to wafer components) and
36//! `R_x(ω)` is the right-handed rotation by `ω` about `x`. The lab-to-crystal
37//! rotation is its transpose, and the beam in the crystal frame is
38//!
39//! ```text
40//! d = cos θ n + sin θ cos(φ - ω) r + sin θ sin(φ - ω) t.
41//! ```
42//!
43//! For the beam direction only the difference `φ - ω` matters; the two are
44//! kept apart because the lab frame (where the beam and the target geometry
45//! live) and the crystal frame are distinct.
46//!
47//! **Reference direction.** The meaning of "twist" differs between tools,
48//! because the azimuth needs a zero. Here the zero is always the reference
49//! direction the caller names, so there is no hidden default. As a cited
50//! example of the polar/azimuth decomposition of an incidence direction on a
51//! cubic crystal: K. Nordlund, F. Djurabekova and G. Hobler, "Large fraction
52//! of crystal directions leads to ion channeling", Phys. Rev. B 94, 214109
53//! (2016), doi:10.1103/PhysRevB.94.214109, Sec. II B, p. 214109-3, set up a
54//! cell "with a \[001\] surface normal and tilting (θ) and twisting (ϕ) the
55//! incoming ion direction", and reach `[011]` at `θ = 45°, ϕ = 0°` and
56//! `[111]` at `θ = 54.73°, ϕ = 45°`: their twist is zero towards an in-plane
57//! `<100>` axis. With `n = [001]` and `r = [010]` our convention gives the
58//! same two channels (the second as `[-111]`, a member of `<111>`; the paper
59//! does not fix the sense of `ϕ`, which we fix above). A wafer flat or notch
60//! direction can be passed as the reference instead.
61
62use super::lattice::{direction_from_uvtw, plane_from_hkil};
63use super::{add, CrystalError, Lattice};
64use crate::vec3::{cross, dot, scale};
65
66/// A 3x3 rotation matrix, row-major: `v' = M v` with
67/// `v'_i = sum_j M[i][j] v_j`.
68pub type Mat3 = [[f64; 3]; 3];
69
70fn mat_vec(m: &Mat3, v: [f64; 3]) -> [f64; 3] {
71 [dot(m[0], v), dot(m[1], v), dot(m[2], v)]
72}
73
74fn transpose(m: &Mat3) -> Mat3 {
75 [
76 [m[0][0], m[1][0], m[2][0]],
77 [m[0][1], m[1][1], m[2][1]],
78 [m[0][2], m[1][2], m[2][2]],
79 ]
80}
81
82fn check_angle(
83 name: &'static str,
84 value: f64,
85 ok: bool,
86 why: &'static str,
87) -> Result<f64, CrystalError> {
88 if value.is_finite() && ok {
89 Ok(value)
90 } else {
91 Err(CrystalError::InvalidAngle { name, value, why })
92 }
93}
94
95/// Unit beam direction in the lab frame for tilt `θ` and twist `φ`:
96/// `(cos θ, sin θ cos φ, sin θ sin φ)`.
97pub fn lab_direction(tilt_rad: f64, twist_rad: f64) -> [f64; 3] {
98 let (s, c) = tilt_rad.sin_cos();
99 let (sa, ca) = twist_rad.sin_cos();
100 [c, s * ca, s * sa]
101}
102
103/// A wafer cut and the beam angles. See the module docs for the
104/// conventions.
105#[derive(Debug, Clone, Copy, PartialEq)]
106pub struct Orientation {
107 normal_hkl: [i32; 3],
108 reference_uvw: [i32; 3],
109 /// Rows `n`, `r`, `t`: crystal components to wafer components.
110 wafer: Mat3,
111 tilt_rad: f64,
112 twist_rad: f64,
113 wafer_rotation_rad: f64,
114}
115
116impl Orientation {
117 /// Orientation of a wafer of `lattice` cut on `(hkl)` (inward normal)
118 /// with in-plane reference direction `[uvw]`, and a beam at `tilt_rad`
119 /// (in `[0, π/2)`), `twist_rad` and `wafer_rotation_rad` (finite).
120 pub fn new(
121 lattice: &Lattice,
122 normal_hkl: [i32; 3],
123 reference_uvw: [i32; 3],
124 tilt_rad: f64,
125 twist_rad: f64,
126 wafer_rotation_rad: f64,
127 ) -> Result<Self, CrystalError> {
128 let n = lattice.plane_normal(normal_hkl)?;
129 let r = lattice.direction(reference_uvw)?;
130 let zone: i64 = (0..3)
131 .map(|i| i64::from(normal_hkl[i]) * i64::from(reference_uvw[i]))
132 .sum();
133 if zone != 0 {
134 return Err(CrystalError::ReferenceNotInPlane {
135 normal: normal_hkl,
136 reference: reference_uvw,
137 dot: zone,
138 });
139 }
140 check_angle(
141 "tilt",
142 tilt_rad,
143 (0.0..std::f64::consts::FRAC_PI_2).contains(&tilt_rad),
144 "must be in [0, pi/2)",
145 )?;
146 check_angle("twist", twist_rad, true, "must be finite")?;
147 check_angle("wafer rotation", wafer_rotation_rad, true, "must be finite")?;
148 let t = cross(n, r);
149 Ok(Self {
150 normal_hkl,
151 reference_uvw,
152 wafer: [n, r, t],
153 tilt_rad,
154 twist_rad,
155 wafer_rotation_rad,
156 })
157 }
158
159 /// As [`Self::new`] for a hexagonal `lattice`, with the surface plane
160 /// `(hkil)` and the reference direction `[uvtw]` in four-index
161 /// (Miller-Bravais) notation. They are converted to three-index form by
162 /// [`super::lattice::plane_from_hkil`] and
163 /// [`super::lattice::direction_from_uvtw`] (convention in the
164 /// [`super::lattice`] module docs), and those are what
165 /// [`Self::normal_hkl`] and [`Self::reference_uvw`] return. The zone law
166 /// in three-index form is equivalent to `h u + k v + i t + l w = 0`.
167 ///
168 /// # Errors
169 /// [`CrystalError::NotHexagonal`] for a cubic lattice,
170 /// [`CrystalError::InvalidMillerBravais`] for indices that break their
171 /// constraint, and the errors of [`Self::new`].
172 pub fn new_miller_bravais(
173 lattice: &Lattice,
174 normal_hkil: [i32; 4],
175 reference_uvtw: [i32; 4],
176 tilt_rad: f64,
177 twist_rad: f64,
178 wafer_rotation_rad: f64,
179 ) -> Result<Self, CrystalError> {
180 if !lattice.is_hexagonal() {
181 return Err(CrystalError::NotHexagonal);
182 }
183 Self::new(
184 lattice,
185 plane_from_hkil(normal_hkil)?,
186 direction_from_uvtw(reference_uvtw)?,
187 tilt_rad,
188 twist_rad,
189 wafer_rotation_rad,
190 )
191 }
192
193 /// Miller indices of the surface plane (inward normal).
194 pub fn normal_hkl(&self) -> [i32; 3] {
195 self.normal_hkl
196 }
197
198 /// Indices of the in-plane reference direction.
199 pub fn reference_uvw(&self) -> [i32; 3] {
200 self.reference_uvw
201 }
202
203 /// Tilt, radians.
204 pub fn tilt_rad(&self) -> f64 {
205 self.tilt_rad
206 }
207
208 /// Twist, radians.
209 pub fn twist_rad(&self) -> f64 {
210 self.twist_rad
211 }
212
213 /// Wafer rotation, radians.
214 pub fn wafer_rotation_rad(&self) -> f64 {
215 self.wafer_rotation_rad
216 }
217
218 /// Crystal-to-lab rotation `M = R_x(ω) B`.
219 pub fn crystal_to_lab(&self) -> Mat3 {
220 let (s, c) = self.wafer_rotation_rad.sin_cos();
221 let b = &self.wafer;
222 // R_x(ω) = [[1, 0, 0], [0, c, -s], [0, s, c]] times B.
223 [
224 b[0],
225 add(scale(b[1], c), scale(b[2], -s)),
226 add(scale(b[1], s), scale(b[2], c)),
227 ]
228 }
229
230 /// Lab-to-crystal rotation, the transpose of [`Self::crystal_to_lab`].
231 pub fn lab_to_crystal(&self) -> Mat3 {
232 transpose(&self.crystal_to_lab())
233 }
234
235 /// Rotate a lab-frame vector into the crystal frame.
236 pub fn to_crystal(&self, v_lab: [f64; 3]) -> [f64; 3] {
237 mat_vec(&self.lab_to_crystal(), v_lab)
238 }
239
240 /// Rotate a crystal-frame vector into the lab frame.
241 pub fn to_lab(&self, v_crystal: [f64; 3]) -> [f64; 3] {
242 mat_vec(&self.crystal_to_lab(), v_crystal)
243 }
244
245 /// Unit beam direction in the lab frame, [`lab_direction`] of the tilt
246 /// and twist.
247 pub fn beam_lab(&self) -> [f64; 3] {
248 lab_direction(self.tilt_rad, self.twist_rad)
249 }
250
251 /// Unit beam direction in the crystal frame.
252 pub fn beam_crystal(&self) -> [f64; 3] {
253 self.to_crystal(self.beam_lab())
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 fn close(a: [f64; 3], b: [f64; 3], tol: f64) -> bool {
262 (0..3).all(|i| (a[i] - b[i]).abs() <= tol)
263 }
264
265 #[test]
266 fn rotation_is_orthonormal_and_right_handed() {
267 let si = Lattice::silicon();
268 let o = Orientation::new(&si, [1, 1, 1], [1, -1, 0], 0.3, 1.1, -0.4).unwrap();
269 let m = o.crystal_to_lab();
270 for i in 0..3 {
271 for j in 0..3 {
272 let want = if i == j { 1.0 } else { 0.0 };
273 assert!((dot(m[i], m[j]) - want).abs() < 1e-15);
274 }
275 }
276 let det = dot(m[0], cross(m[1], m[2]));
277 assert!((det - 1.0).abs() < 1e-15);
278 let v = [0.2, -0.7, 0.4];
279 assert!(close(o.to_lab(o.to_crystal(v)), v, 1e-15));
280 }
281
282 #[test]
283 fn surface_normal_maps_to_lab_x() {
284 let si = Lattice::silicon();
285 let o = Orientation::new(&si, [1, 1, 0], [0, 0, 1], 0.2, 0.5, 0.9).unwrap();
286 let n = si.plane_normal([1, 1, 0]).unwrap();
287 assert!(close(o.to_lab(n), [1.0, 0.0, 0.0], 1e-15));
288 }
289
290 #[test]
291 fn invalid_inputs_are_rejected() {
292 let si = Lattice::silicon();
293 assert_eq!(
294 Orientation::new(&si, [1, 0, 0], [1, 1, 0], 0.0, 0.0, 0.0),
295 Err(CrystalError::ReferenceNotInPlane {
296 normal: [1, 0, 0],
297 reference: [1, 1, 0],
298 dot: 1
299 })
300 );
301 for tilt in [-0.1, std::f64::consts::FRAC_PI_2, f64::NAN] {
302 assert!(matches!(
303 Orientation::new(&si, [1, 0, 0], [0, 1, 0], tilt, 0.0, 0.0),
304 Err(CrystalError::InvalidAngle { name: "tilt", .. })
305 ));
306 }
307 assert!(matches!(
308 Orientation::new(&si, [1, 0, 0], [0, 1, 0], 0.1, f64::INFINITY, 0.0),
309 Err(CrystalError::InvalidAngle { name: "twist", .. })
310 ));
311 assert!(matches!(
312 Orientation::new(&si, [1, 0, 0], [0, 1, 0], 0.1, 0.0, f64::NAN),
313 Err(CrystalError::InvalidAngle {
314 name: "wafer rotation",
315 ..
316 })
317 ));
318 assert_eq!(
319 Orientation::new(&si, [0, 0, 0], [0, 1, 0], 0.0, 0.0, 0.0),
320 Err(CrystalError::ZeroIndex([0; 3]))
321 );
322 }
323}