189 lines
6.3 KiB
Rust
189 lines
6.3 KiB
Rust
//! Integration tests for symclaw-materials.
|
||
//!
|
||
//! Cross-module scenarios: lattice → groups → structure → bonding pipeline.
|
||
|
||
use std::collections::HashMap;
|
||
use symclaw_materials::bonding::{
|
||
BondValence, LennardJones, MadelungStructure, pauling_electronegativity, pauling_ionicity,
|
||
};
|
||
use symclaw_materials::groups::{PointGroup, SpaceGroup, SymOp};
|
||
use symclaw_materials::lattice::{CrystalSystem, LatticeParameters, Miller};
|
||
use symclaw_materials::structure::{CrystalStructure, ScatteringFactor, structure_factor};
|
||
|
||
// ── Lattice → structure pipeline ──────────────────────────────────
|
||
|
||
#[test]
|
||
fn nacl_full_pipeline() {
|
||
// Build structure → compute d-spacings → structure factors
|
||
let nacl = CrystalStructure::nacl();
|
||
let a = nacl.lattice.a;
|
||
|
||
// (200) reflection allowed in Fm-3m
|
||
let m200 = Miller::new(2, 0, 0);
|
||
let d200 = m200.d_spacing_cubic(a);
|
||
assert!(d200 > 0.0, "d(200) > 0");
|
||
|
||
// Bragg angle for Cu Kα
|
||
let two_theta = Miller::bragg_angle_deg(d200, 1.5406).unwrap();
|
||
assert!(
|
||
two_theta > 20.0 && two_theta < 40.0,
|
||
"NaCl (200) Bragg angle should be 20-40°, got {two_theta:.1}°"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn silicon_lattice_parameters() {
|
||
let si = CrystalStructure::silicon();
|
||
let vol = si.lattice.volume();
|
||
// Si a=5.4309 Å → V = a³ ≈ 160.2 ų
|
||
assert!(
|
||
(vol - 160.2).abs() < 1.0,
|
||
"Si unit cell V ≈ 160.2 ų, got {vol:.1}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn reciprocal_lattice_cubic_orthogonal() {
|
||
// For cubic: aᵢ · bⱼ = 2π δᵢⱼ
|
||
let p = LatticeParameters::cubic(4.0);
|
||
let rv = p.reciprocal_vectors();
|
||
let lv = p.lattice_vectors();
|
||
let dot = |u: [f64; 3], v: [f64; 3]| u[0] * v[0] + u[1] * v[1] + u[2] * v[2];
|
||
assert!((dot(rv[0], lv[0]) - 2.0 * std::f64::consts::PI).abs() < 1e-8);
|
||
assert!(dot(rv[0], lv[1]).abs() < 1e-8);
|
||
assert!(dot(rv[1], lv[0]).abs() < 1e-8);
|
||
}
|
||
|
||
// ── Point groups → space groups consistency ────────────────────────
|
||
|
||
#[test]
|
||
fn cubic_point_groups_all_centred() {
|
||
// Not all cubic PGs are centrosymmetric (23, 432, -43m are not)
|
||
let cubic: Vec<PointGroup> = PointGroup::all_32()
|
||
.into_iter()
|
||
.filter(|g| g.crystal_system == CrystalSystem::Cubic)
|
||
.collect();
|
||
assert_eq!(cubic.len(), 5, "5 cubic point groups");
|
||
let centrosym = cubic.iter().filter(|g| g.centrosymmetric).count();
|
||
assert_eq!(
|
||
centrosym, 2,
|
||
"2 centrosymmetric cubic groups (m-3 and m-3m)"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn symop_group_closure() {
|
||
// C4 × C4 = C2, C2 × C2 = E, C4^4 = E
|
||
let c4 = SymOp::c4z();
|
||
let c4_2 = c4.compose(&c4); // C4² = C2
|
||
let c4_4 = c4_2.compose(&c4_2); // C4⁴ = E
|
||
assert_eq!(c4_4.rot, SymOp::identity().rot, "C4⁴ = E");
|
||
}
|
||
|
||
#[test]
|
||
fn space_group_numbers_cover_all_230() {
|
||
assert!(SpaceGroup::is_valid_number(1));
|
||
assert!(SpaceGroup::is_valid_number(115));
|
||
assert!(SpaceGroup::is_valid_number(230));
|
||
assert!(!SpaceGroup::is_valid_number(231));
|
||
}
|
||
|
||
// ── Structure factor pipeline ──────────────────────────────────────
|
||
|
||
#[test]
|
||
fn silicon_f0_near_14() {
|
||
// Si scattering factor at s=0 should be ≈ Z = 14
|
||
let sf = ScatteringFactor::silicon();
|
||
let f0 = sf.eval(0.0);
|
||
assert!((f0 - 14.0).abs() < 0.5, "Si f(0) ≈ 14, got {f0:.2}");
|
||
}
|
||
|
||
#[test]
|
||
fn structure_factor_iron_bcc_110() {
|
||
// BCC: (110) is present (h+k+l = 2 = even)
|
||
let fe = CrystalStructure::bcc_iron();
|
||
let m110 = Miller::new(1, 1, 0);
|
||
let d = m110.d_spacing_cubic(fe.lattice.a);
|
||
let mut sf_map = HashMap::new();
|
||
sf_map.insert(
|
||
"Fe".to_owned(),
|
||
ScatteringFactor {
|
||
a: [11.77, 7.068, 3.982, 2.417],
|
||
b: [4.761, 0.307, 15.35, 76.88],
|
||
c: 1.036,
|
||
},
|
||
);
|
||
let (re, im) = structure_factor(&m110, &fe.sites, &sf_map, d, 1.5406);
|
||
let intensity = re * re + im * im;
|
||
assert!(
|
||
intensity > 1.0,
|
||
"Fe BCC (110) should be present, |F|²={intensity:.1}"
|
||
);
|
||
}
|
||
|
||
// ── Bonding pipeline ──────────────────────────────────────────────
|
||
|
||
#[test]
|
||
fn lj_argon_well_depth() {
|
||
let lj = LennardJones::argon();
|
||
// At r_min: V = -ε = -0.0104 eV
|
||
let v = lj.potential(lj.r_min());
|
||
assert!((v - lj.v_min()).abs() < 1e-12);
|
||
assert!(
|
||
(v + 0.0104).abs() < 1e-12,
|
||
"V(r_min) = -0.0104 eV for Ar, got {v}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn madelung_nacl_born_lande_energy() {
|
||
// NaCl Born-Landé: ~-787 kJ/mol
|
||
let u = MadelungStructure::NaCl.lattice_energy_kj_mol(1.0, 1.0, 2.82, 9.0);
|
||
assert!(
|
||
u < -700.0 && u > -900.0,
|
||
"NaCl lattice energy should be -700 to -900 kJ/mol, got {u:.1}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn bvs_silicon_four_coord() {
|
||
// Si⁴⁺ in tetrahedral coordination: 4 bonds each contributing BVS = 1
|
||
// BVS = n_bonds * exp((R0 - R)/b); target = 4
|
||
// For 4 equal bonds: each s = 1, so R = R0 = 1.624 Å
|
||
let bv = BondValence::new();
|
||
let r0 = 1.624_f64; // Si-O R0
|
||
let lengths = vec![r0; 4]; // each bond at R0 gives s=1 per bond
|
||
let sum = bv.bvs("Si", "O", &lengths).unwrap();
|
||
assert!((sum - 4.0).abs() < 0.01, "BVS(Si,4×O@R0) = 4, got {sum:.4}");
|
||
}
|
||
|
||
#[test]
|
||
fn pauling_ionicity_naf_vs_hf() {
|
||
// NaF more ionic than HF
|
||
let en_na = pauling_electronegativity("Na").unwrap();
|
||
let en_f = pauling_electronegativity("F").unwrap();
|
||
let en_h = pauling_electronegativity("H").unwrap();
|
||
let ion_naf = pauling_ionicity(en_na, en_f);
|
||
let ion_hf = pauling_ionicity(en_h, en_f);
|
||
assert!(
|
||
ion_naf > ion_hf,
|
||
"NaF more ionic than HF: {ion_naf:.2} > {ion_hf:.2}"
|
||
);
|
||
}
|
||
|
||
// ── Miller indices cross-check ────────────────────────────────────
|
||
|
||
#[test]
|
||
fn miller_zone_law_systematic() {
|
||
// [100] zone axis: planes (0kl) only — h=0
|
||
for k in -2..=2i32 {
|
||
for l in -2..=2i32 {
|
||
let m = Miller::new(0, k, l);
|
||
assert!(m.in_zone(1, 0, 0), "(0{k}{l}) should be in [100] zone");
|
||
}
|
||
}
|
||
// (100) itself is NOT in its own zone axis [100]
|
||
let m100 = Miller::new(1, 0, 0);
|
||
assert!(!m100.in_zone(1, 0, 0), "(100) is not in [100] zone");
|
||
}
|