use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct UnitCell {
pub a: f64,
pub b: f64,
pub c: f64,
pub alpha: f64,
pub beta: f64,
pub gamma: f64,
}
impl UnitCell {
#[must_use]
pub fn new(a: f64, b: f64, c: f64, alpha: f64, beta: f64, gamma: f64) -> Self {
Self {
a,
b,
c,
alpha,
beta,
gamma,
}
}
#[must_use]
pub fn cubic(a: f64) -> Self {
Self::new(a, a, a, 90.0, 90.0, 90.0)
}
#[must_use]
pub fn hexagonal(a: f64, c: f64) -> Self {
Self::new(a, a, c, 90.0, 90.0, 120.0)
}
#[must_use]
pub fn rhombohedral(a: f64, c: f64) -> Self {
Self::hexagonal(a, c)
}
#[must_use]
pub fn orthorhombic(a: f64, b: f64, c: f64) -> Self {
Self::new(a, b, c, 90.0, 90.0, 90.0)
}
#[must_use]
pub fn halite() -> Self {
Self::cubic(5.64)
}
#[must_use]
pub fn quartz() -> Self {
Self::hexagonal(4.913, 5.405)
}
#[must_use]
pub fn calcite() -> Self {
Self::rhombohedral(4.989, 17.062)
}
#[must_use]
pub fn diamond() -> Self {
Self::cubic(3.567)
}
#[must_use]
pub fn volume(&self) -> f64 {
let ca = self.alpha.to_radians().cos();
let cb = self.beta.to_radians().cos();
let cg = self.gamma.to_radians().cos();
let factor = 1.0 - ca * ca - cb * cb - cg * cg + 2.0 * ca * cb * cg;
self.a * self.b * self.c * factor.sqrt()
}
#[must_use]
pub fn is_cubic(&self) -> bool {
let tol = 1e-6;
(self.a - self.b).abs() < tol
&& (self.b - self.c).abs() < tol
&& (self.alpha - 90.0).abs() < tol
&& (self.beta - 90.0).abs() < tol
&& (self.gamma - 90.0).abs() < tol
}
#[must_use]
pub fn is_hexagonal(&self) -> bool {
let tol = 1e-6;
(self.a - self.b).abs() < tol
&& (self.alpha - 90.0).abs() < tol
&& (self.beta - 90.0).abs() < tol
&& (self.gamma - 120.0).abs() < tol
}
#[must_use]
pub fn is_tetragonal(&self) -> bool {
let tol = 1e-6;
(self.a - self.b).abs() < tol
&& (self.a - self.c).abs() >= tol
&& (self.alpha - 90.0).abs() < tol
&& (self.beta - 90.0).abs() < tol
&& (self.gamma - 90.0).abs() < tol
}
#[must_use]
pub fn is_orthorhombic(&self) -> bool {
let tol = 1e-6;
(self.alpha - 90.0).abs() < tol
&& (self.beta - 90.0).abs() < tol
&& (self.gamma - 90.0).abs() < tol
&& (self.a - self.b).abs() >= tol
&& (self.b - self.c).abs() >= tol
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MillerIndex {
pub h: i32,
pub k: i32,
pub l: i32,
}
impl MillerIndex {
#[must_use]
pub fn new(h: i32, k: i32, l: i32) -> Self {
Self { h, k, l }
}
}
impl core::fmt::Display for MillerIndex {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "({} {} {})", self.h, self.k, self.l)
}
}
#[must_use]
pub fn d_spacing(cell: &UnitCell, hkl: &MillerIndex) -> f64 {
let h2 = f64::from(hkl.h * hkl.h);
let k2 = f64::from(hkl.k * hkl.k);
let l2 = f64::from(hkl.l * hkl.l);
if cell.is_cubic() {
cell.a / (h2 + k2 + l2).sqrt()
} else {
let inv_d2 = h2 / (cell.a * cell.a) + k2 / (cell.b * cell.b) + l2 / (cell.c * cell.c);
1.0 / inv_d2.sqrt()
}
}
#[must_use]
pub fn bragg_angle(d_spacing: f64, wavelength: f64) -> Option<f64> {
let sin_theta = wavelength / (2.0 * d_spacing);
if sin_theta.abs() > 1.0 {
None
} else {
Some(sin_theta.asin().to_degrees())
}
}
#[must_use]
pub fn bragg_wavelength(d_spacing: f64, angle_deg: f64) -> f64 {
2.0 * d_spacing * angle_deg.to_radians().sin()
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-6;
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn cubic_volume_is_a_cubed() {
let cell = UnitCell::cubic(5.0);
assert!(approx(cell.volume(), 125.0, EPS));
}
#[test]
fn halite_volume() {
let cell = UnitCell::halite();
let expected = 5.64_f64.powi(3); assert!(approx(cell.volume(), expected, 1e-3));
}
#[test]
fn orthorhombic_volume() {
let cell = UnitCell::orthorhombic(3.0, 4.0, 5.0);
assert!(approx(cell.volume(), 60.0, EPS));
}
#[test]
fn halite_is_cubic() {
assert!(UnitCell::halite().is_cubic());
assert!(!UnitCell::halite().is_hexagonal());
}
#[test]
fn quartz_is_hexagonal() {
assert!(UnitCell::quartz().is_hexagonal());
assert!(!UnitCell::quartz().is_cubic());
}
#[test]
fn nacl_111_d_spacing() {
let cell = UnitCell::halite();
let hkl = MillerIndex::new(1, 1, 1);
let d = d_spacing(&cell, &hkl);
let expected = 5.64 / 3.0_f64.sqrt();
assert!(approx(d, expected, 1e-4));
}
#[test]
fn nacl_200_d_spacing() {
let cell = UnitCell::halite();
let hkl = MillerIndex::new(2, 0, 0);
let d = d_spacing(&cell, &hkl);
assert!(approx(d, 2.82, 1e-4));
}
#[test]
fn bragg_angle_cu_ka_nacl_111() {
let d = 5.64 / 3.0_f64.sqrt();
let angle = bragg_angle(d, 1.5406).expect("valid angle");
assert!(approx(angle, 13.68, 0.05));
}
#[test]
fn bragg_angle_returns_none_when_impossible() {
assert!(bragg_angle(0.5, 1.5406).is_none());
}
#[test]
fn bragg_roundtrip() {
let d = 3.256;
let wavelength = 1.5406;
let angle = bragg_angle(d, wavelength).unwrap();
let recovered = bragg_wavelength(d, angle);
assert!(approx(recovered, wavelength, 1e-6));
}
#[test]
fn preset_diamond() {
let d = UnitCell::diamond();
assert!(d.is_cubic());
assert!(approx(d.a, 3.567, EPS));
}
#[test]
fn preset_calcite() {
let c = UnitCell::calcite();
assert!(c.is_hexagonal());
assert!(approx(c.a, 4.989, EPS));
assert!(approx(c.c, 17.062, EPS));
}
#[test]
fn serde_roundtrip_unit_cell() {
let cell = UnitCell::quartz();
let json = serde_json::to_string(&cell).unwrap();
let back: UnitCell = serde_json::from_str(&json).unwrap();
assert_eq!(cell, back);
}
#[test]
fn serde_roundtrip_miller() {
let hkl = MillerIndex::new(1, -1, 2);
let json = serde_json::to_string(&hkl).unwrap();
let back: MillerIndex = serde_json::from_str(&json).unwrap();
assert_eq!(hkl, back);
}
#[test]
fn miller_display() {
let hkl = MillerIndex::new(1, 1, 1);
assert_eq!(format!("{hkl}"), "(1 1 1)");
}
}