Skip to main content

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}