use crate::error::{OxiPhotonError, Result};
use std::f64::consts::PI;
const R_ELECTRON: f64 = 2.817_940_3e-15;
const C0: f64 = 2.997_924_58e8;
const H_PLANCK: f64 = 6.626_070_15e-34;
const E_CHARGE: f64 = 1.602_176_634e-19;
#[inline]
fn lambda_to_ev(lambda_m: f64) -> f64 {
H_PLANCK * C0 / (lambda_m * E_CHARGE)
}
#[derive(Debug, Clone)]
pub enum CrystalMaterial {
Silicon,
Germanium,
Diamond,
Quartz,
LithiumNiobate,
Custom {
d_spacing: f64,
structure_factor: f64,
},
}
impl CrystalMaterial {
fn lattice_a(&self) -> Option<f64> {
match self {
Self::Silicon => Some(5.431_020_511e-10),
Self::Germanium => Some(5.658_e-10),
Self::Diamond => Some(3.5668e-10),
_ => None,
}
}
pub fn d_spacing(&self, h: i32, k: i32, l: i32) -> f64 {
match self {
Self::Silicon | Self::Germanium | Self::Diamond => {
let a = self.lattice_a().unwrap_or(5.431e-10);
let denom = ((h * h + k * k + l * l) as f64).sqrt();
if denom < 1e-10 {
return f64::INFINITY;
}
a / denom
}
Self::Quartz => {
let a = 4.913e-10_f64;
let c = 5.405e-10_f64;
let num =
(4.0 / 3.0) * (h * h + h * k + k * k) as f64 + (a / c).powi(2) * (l * l) as f64;
if num <= 0.0 {
return f64::INFINITY;
}
a / num.sqrt()
}
Self::LithiumNiobate => {
let a = 5.148e-10_f64;
let c = 13.863e-10_f64;
let num =
(4.0 / 3.0) * (h * h + h * k + k * k) as f64 + (a / c).powi(2) * (l * l) as f64;
if num <= 0.0 {
return f64::INFINITY;
}
a / num.sqrt()
}
Self::Custom { d_spacing, .. } => *d_spacing,
}
}
pub fn structure_factor(&self, h: i32, k: i32, l: i32) -> f64 {
let sum_hkl = h + k + l;
let all_same_parity = (h % 2 == k % 2) && (k % 2 == l % 2);
match self {
Self::Silicon => {
diamond_cubic_structure_factor(h, k, l, 14.0)
}
Self::Germanium => {
diamond_cubic_structure_factor(h, k, l, 32.0)
}
Self::Diamond => {
diamond_cubic_structure_factor(h, k, l, 6.0)
}
Self::Quartz => {
if all_same_parity {
30.0
} else {
0.0
}
}
Self::LithiumNiobate => {
if all_same_parity || sum_hkl % 2 == 0 {
52.0
} else {
0.0
}
}
Self::Custom {
structure_factor, ..
} => *structure_factor,
}
}
}
fn diamond_cubic_structure_factor(h: i32, k: i32, l: i32, z: f64) -> f64 {
let all_odd = h % 2 != 0 && k % 2 != 0 && l % 2 != 0;
let all_even = h % 2 == 0 && k % 2 == 0 && l % 2 == 0;
if !all_odd && !all_even {
return 0.0;
}
let sum = h + k + l;
if all_odd {
4.0 * z * (2.0_f64).sqrt()
} else {
match sum.unsigned_abs() % 4 {
0 => 8.0 * z, 2 => 0.0, _ => 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct BraggCrystal {
pub material: CrystalMaterial,
pub miller_h: i32,
pub miller_k: i32,
pub miller_l: i32,
pub d_spacing: f64,
}
impl BraggCrystal {
pub fn new(material: CrystalMaterial, h: i32, k: i32, l: i32) -> Result<Self> {
if h == 0 && k == 0 && l == 0 {
return Err(OxiPhotonError::NumericalError(
"Miller indices (0,0,0) are not a valid reflection".into(),
));
}
let d = material.d_spacing(h, k, l);
if !d.is_finite() || d <= 0.0 {
return Err(OxiPhotonError::NumericalError(format!(
"non-positive d-spacing {d:.3e} m for ({h},{k},{l})"
)));
}
Ok(Self {
d_spacing: d,
material,
miller_h: h,
miller_k: k,
miller_l: l,
})
}
pub fn bragg_angle(&self, wavelength: f64) -> Option<f64> {
let arg = wavelength / (2.0 * self.d_spacing);
if arg.abs() > 1.0 {
return None;
}
Some(arg.asin())
}
pub fn wavelength_at_angle(&self, theta_rad: f64) -> f64 {
2.0 * self.d_spacing * theta_rad.sin()
}
fn unit_cell_volume(&self) -> f64 {
match &self.material {
CrystalMaterial::Silicon => {
let a = 5.431_020_511e-10_f64;
a.powi(3)
}
CrystalMaterial::Germanium => {
let a = 5.658e-10_f64;
a.powi(3)
}
CrystalMaterial::Diamond => {
let a = 3.5668e-10_f64;
a.powi(3)
}
CrystalMaterial::Quartz => {
let a = 4.913e-10_f64;
let c = 5.405e-10_f64;
(3.0_f64).sqrt() / 2.0 * a * a * c
}
CrystalMaterial::LithiumNiobate => {
let a = 5.148e-10_f64;
let c = 13.863e-10_f64;
(3.0_f64).sqrt() / 2.0 * a * a * c
}
CrystalMaterial::Custom { d_spacing, .. } => {
d_spacing.powi(3)
}
}
}
pub fn darwin_width_rad(&self, wavelength: f64) -> f64 {
let theta = match self.bragg_angle(wavelength) {
Some(t) => t,
None => return 0.0,
};
let sin2t = (2.0 * theta).sin();
if sin2t.abs() < 1e-15 {
return 0.0;
}
let f_hkl = self
.material
.structure_factor(self.miller_h, self.miller_k, self.miller_l);
let v = self.unit_cell_volume();
2.0 * R_ELECTRON * wavelength * wavelength * f_hkl / (PI * v * sin2t)
}
pub fn integrated_reflectivity(&self, wavelength: f64) -> f64 {
self.darwin_width_rad(wavelength) * PI / 2.0
}
pub fn energy_dispersion(&self, wavelength: f64) -> f64 {
let theta = match self.bragg_angle(wavelength) {
Some(t) => t,
None => return 0.0,
};
let energy_ev = lambda_to_ev(wavelength);
-energy_ev / theta.tan()
}
pub fn resolving_power(&self, wavelength: f64) -> f64 {
let theta = match self.bragg_angle(wavelength) {
Some(t) => t,
None => return 0.0,
};
let w = self.darwin_width_rad(wavelength);
if w <= 0.0 {
return 0.0;
}
1.0 / (theta.tan() * w)
}
pub fn is_bragg_geometry(&self, wavelength: f64, crystal_thickness: f64) -> bool {
let f_hkl = self
.material
.structure_factor(self.miller_h, self.miller_k, self.miller_l);
let v = self.unit_cell_volume();
if f_hkl <= 0.0 || wavelength <= 0.0 {
return true; }
let extinction_length = PI * v / (R_ELECTRON * wavelength * f_hkl);
crystal_thickness > 10.0 * extinction_length
}
}
#[derive(Debug, Clone)]
pub struct JohannSpectrometer {
pub crystal: BraggCrystal,
pub bending_radius: f64,
pub source_distance: f64,
}
impl JohannSpectrometer {
pub fn new(crystal: BraggCrystal, radius: f64) -> Result<Self> {
if radius <= 0.0 || !radius.is_finite() {
return Err(OxiPhotonError::NumericalError(
"bending_radius must be positive and finite".into(),
));
}
let source_distance = radius / 2.0;
Ok(Self {
crystal,
bending_radius: radius,
source_distance,
})
}
pub fn resolving_power(&self, wavelength: f64) -> f64 {
self.crystal.resolving_power(wavelength)
}
pub fn energy_range(&self) -> (f64, f64) {
let d = self.crystal.d_spacing;
let theta_min = PI / 18.0;
let lambda_at_90 = 2.0 * d;
let lambda_at_min = 2.0 * d * theta_min.sin();
let e_min = lambda_to_ev(lambda_at_90);
let e_max = lambda_to_ev(lambda_at_min);
(e_min, e_max)
}
pub fn image_distance(&self, wavelength: f64) -> Option<f64> {
let theta = self.crystal.bragg_angle(wavelength)?;
let r_sin = self.bending_radius * theta.sin();
let p = self.source_distance;
let denom = 2.0 * p - r_sin;
if denom.abs() < 1e-15 {
return None;
}
Some(p * r_sin / denom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
const CU_KALPHA: f64 = 0.154_056e-9;
fn si_111() -> BraggCrystal {
BraggCrystal::new(CrystalMaterial::Silicon, 1, 1, 1).expect("valid Si(111)")
}
#[test]
fn si_d_spacing_111() {
let crystal = si_111();
let expected = 5.431_020_511e-10 / (3.0_f64).sqrt();
assert_abs_diff_eq!(crystal.d_spacing, expected, epsilon = 1e-14);
}
#[test]
fn bragg_angle_cu_kalpha() {
let crystal = si_111();
let theta = crystal
.bragg_angle(CU_KALPHA)
.expect("Cu Kα diffracts on Si(111)");
let expected_rad = (CU_KALPHA / (2.0 * crystal.d_spacing)).asin();
assert_abs_diff_eq!(theta, expected_rad, epsilon = 1e-10);
}
#[test]
fn bragg_angle_returns_none_for_too_long_wavelength() {
let crystal = si_111();
let long_lambda = 2.0 * crystal.d_spacing + 1e-11;
assert!(crystal.bragg_angle(long_lambda).is_none());
}
#[test]
fn wavelength_at_angle_roundtrip() {
let crystal = si_111();
let theta = crystal.bragg_angle(CU_KALPHA).expect("valid angle");
let lambda_back = crystal.wavelength_at_angle(theta);
assert_abs_diff_eq!(lambda_back, CU_KALPHA, epsilon = 1e-18);
}
#[test]
fn darwin_width_positive() {
let crystal = si_111();
let w = crystal.darwin_width_rad(CU_KALPHA);
assert!(w > 0.0, "Darwin width should be positive, got {w:.3e}");
assert!(w < 1e-3, "Darwin width unexpectedly large: {w:.3e} rad");
}
#[test]
fn resolving_power_positive() {
let crystal = si_111();
let rp = crystal.resolving_power(CU_KALPHA);
assert!(rp > 0.0 && rp.is_finite());
}
#[test]
fn energy_dispersion_negative() {
let crystal = si_111();
let disp = crystal.energy_dispersion(CU_KALPHA);
assert!(
disp < 0.0,
"energy dispersion should be negative, got {disp:.3e}"
);
}
#[test]
fn bad_miller_indices() {
assert!(BraggCrystal::new(CrystalMaterial::Silicon, 0, 0, 0).is_err());
}
#[test]
fn johann_energy_range_ordered() {
let crystal = si_111();
let spec = JohannSpectrometer::new(crystal, 0.5).expect("valid spectrometer");
let (e_min, e_max) = spec.energy_range();
assert!(
e_min < e_max,
"E_min should be less than E_max: {e_min:.1} vs {e_max:.1}"
);
}
#[test]
fn johann_image_distance_positive() {
let crystal = si_111();
let spec = JohannSpectrometer::new(crystal, 0.5).expect("valid spectrometer");
let img = spec
.image_distance(CU_KALPHA)
.expect("image distance exists");
assert!(img > 0.0 && img.is_finite());
}
}