//! 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::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"); }