#[test]
fn test_uhf_h2_singlet() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
let result = sci_form::compute_uhf(&elements, &positions, 0, 1).unwrap();
assert!(result.converged, "H₂ singlet UHF should converge");
assert_eq!(result.n_alpha, 1);
assert_eq!(result.n_beta, 1);
assert!(
result.spin_contamination.abs() < 0.1,
"Spin contamination = {:.4}, expected ~0 for singlet",
result.spin_contamination
);
assert!(result.total_energy < 0.0, "Total energy should be negative");
assert!(result.n_basis >= 2, "At least 2 basis functions for H₂");
}
#[test]
fn test_uhf_h2_cation_doublet() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let result = sci_form::compute_uhf(&elements, &positions, 1, 2).unwrap();
assert!(result.converged, "H₂⁺ doublet should converge");
assert_eq!(result.n_alpha, 1, "H₂⁺ has 1 alpha electron");
assert_eq!(result.n_beta, 0, "H₂⁺ has 0 beta electrons");
assert!(
(result.s2_expectation - 0.75).abs() < 0.1,
"<S²> = {:.4}, expected ~0.75 for doublet",
result.s2_expectation
);
}
#[test]
fn test_uhf_h2_triplet() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [1.5, 0.0, 0.0]]; let result = sci_form::compute_uhf(&elements, &positions, 0, 3).unwrap();
assert_eq!(result.n_alpha, 2);
assert_eq!(result.n_beta, 0);
assert!(
(result.s2_expectation - 2.0).abs() < 0.3,
"<S²> = {:.4}, expected ~2.0 for triplet",
result.s2_expectation
);
assert!(result.total_energy.is_finite());
}
#[test]
fn test_rohf_h2_cation() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let result = sci_form::compute_rohf(&elements, &positions, 1, 2).unwrap();
assert!(result.converged, "ROHF should converge for H₂⁺");
assert_eq!(result.n_alpha, 1);
assert_eq!(result.n_beta, 0);
assert!(
result.spin_contamination.abs() < 0.05,
"ROHF spin contamination = {:.4}, expected ~0",
result.spin_contamination
);
}
#[test]
fn test_uhf_configured_h2() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
let config = sci_form::scf::uhf::UhfConfig {
max_iterations: 200,
level_shift: 0.1,
damping: 0.3,
..Default::default()
};
let result = sci_form::compute_uhf_configured(&elements, &positions, 0, 1, &config).unwrap();
assert!(result.converged, "UHF with level shift should converge");
assert!(result.total_energy < 0.0);
}
#[test]
fn test_uhf_singlet_alpha_beta_symmetry() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
let result = sci_form::compute_uhf(&elements, &positions, 0, 1).unwrap();
if result.converged {
for (i, (ea, eb)) in result
.alpha_orbital_energies
.iter()
.zip(result.beta_orbital_energies.iter())
.enumerate()
{
assert!(
(ea - eb).abs() < 0.01,
"Orbital {}: alpha={:.4} vs beta={:.4}",
i,
ea,
eb
);
}
}
}
#[test]
fn test_cif_parse_simple_structure() {
let cif_text = r#"data_test
_cell_length_a 5.640
_cell_length_b 5.640
_cell_length_c 5.640
_cell_angle_alpha 90.0
_cell_angle_beta 90.0
_cell_angle_gamma 90.0
_symmetry_space_group_name_H-M 'Fm-3m'
loop_
_atom_site_label
_atom_site_type_symbol
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
Na1 Na 0.0 0.0 0.0
Cl1 Cl 0.5 0.5 0.5
"#;
let structure = sci_form::parse_cif(cif_text).unwrap();
assert_eq!(structure.atom_sites.len(), 2);
assert!((structure.cell_params.a - 5.640).abs() < 0.001);
assert_eq!(structure.space_group_hm.as_deref(), Some("Fm-3m"));
}
#[test]
fn test_cif_roundtrip() {
let cif_text = r#"data_test
_cell_length_a 10.0
_cell_length_b 10.0
_cell_length_c 10.0
_cell_angle_alpha 90.0
_cell_angle_beta 90.0
_cell_angle_gamma 90.0
loop_
_atom_site_label
_atom_site_type_symbol
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
C1 C 0.25 0.25 0.25
O1 O 0.50 0.50 0.50
"#;
let s1 = sci_form::parse_cif(cif_text).unwrap();
let output = sci_form::write_cif(&s1);
let s2 = sci_form::parse_cif(&output).unwrap();
assert_eq!(s1.atom_sites.len(), s2.atom_sites.len());
for (a, b) in s1.atom_sites.iter().zip(s2.atom_sites.iter()) {
assert!(
(a.frac_x - b.frac_x).abs() < 1e-4,
"Roundtrip frac_x mismatch"
);
assert!(
(a.frac_y - b.frac_y).abs() < 1e-4,
"Roundtrip frac_y mismatch"
);
assert!(
(a.frac_z - b.frac_z).abs() < 1e-4,
"Roundtrip frac_z mismatch"
);
assert_eq!(a.type_symbol, b.type_symbol, "Element mismatch");
}
}
#[test]
fn test_cif_parse_uncertainty_notation() {
let cif_text = r#"data_test
_cell_length_a 5.640(1)
_cell_length_b 5.640(2)
_cell_length_c 5.640(3)
_cell_angle_alpha 90.00(5)
_cell_angle_beta 90.0
_cell_angle_gamma 90.0
loop_
_atom_site_label
_atom_site_type_symbol
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
Na1 Na 0.0 0.0 0.0
"#;
let structure = sci_form::parse_cif(cif_text).unwrap();
assert!(
(structure.cell_params.a - 5.640).abs() < 0.001,
"Failed to parse uncertainty notation: a={}",
structure.cell_params.a
);
}
#[test]
fn test_cif_write_contains_required_fields() {
let cif_text = r#"data_test
_cell_length_a 4.0
_cell_length_b 4.0
_cell_length_c 4.0
_cell_angle_alpha 90.0
_cell_angle_beta 90.0
_cell_angle_gamma 90.0
loop_
_atom_site_label
_atom_site_type_symbol
_atom_site_fract_x
_atom_site_fract_y
_atom_site_fract_z
Fe1 Fe 0.0 0.0 0.0
"#;
let structure = sci_form::parse_cif(cif_text).unwrap();
let output = sci_form::write_cif(&structure);
assert!(output.contains("_cell_length_a"), "Missing _cell_length_a");
assert!(output.contains("_atom_site_label"), "Missing loop header");
assert!(output.contains("Fe"), "Missing Fe atom");
}
#[test]
fn test_mo_integrals_4fold_symmetry() {
use sci_form::hf::mo_transform::MoIntegrals;
assert!(
std::mem::size_of::<MoIntegrals>() > 0,
"MoIntegrals type should exist"
);
}
#[cfg(feature = "experimental-gpu")]
#[test]
fn test_gpu_stda_module_exists() {
use sci_form::gpu::stda_gpu::compute_stda_j_matrix_gpu;
let _ = std::mem::size_of_val(&(compute_stda_j_matrix_gpu as fn(_, _, _, _, _) -> _));
}
#[cfg(feature = "experimental-gpu")]
#[test]
fn test_gpu_hessian_displacement_generation() {
use sci_form::gpu::hessian_gpu::generate_hessian_displacements;
let positions = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
let (displacements, _) = generate_hessian_displacements(&positions, 0.005);
assert_eq!(
displacements.len(),
12,
"Should have 12 displaced geometries"
);
}
#[test]
fn test_pm3_gaussians_present_for_main_group() {
let main_group = [1u8, 6, 7, 8, 9, 15, 16, 17, 35, 53, 13, 14];
for &z in &main_group {
let p = sci_form::pm3::params::get_pm3_params(z).unwrap();
assert!(
!p.gaussians.is_empty(),
"Element Z={} should have Gaussian corrections, got empty",
z
);
assert_eq!(
p.gaussians.len(),
2,
"Element Z={} should have 2 Gaussians, got {}",
z,
p.gaussians.len()
);
}
}
#[test]
fn test_pm3_gaussians_empty_for_transition_metals() {
let tms = [22u8, 24, 25, 26, 27, 28, 29, 30];
for &z in &tms {
let p = sci_form::pm3::params::get_pm3_params(z).unwrap();
assert!(
p.gaussians.is_empty(),
"TM Z={} should have empty gaussians, got {} entries",
z,
p.gaussians.len()
);
}
}
#[test]
fn test_pm3_water_with_gaussians() {
let conf = sci_form::embed("O", 42);
assert!(conf.error.is_none(), "Embed should succeed for water");
let positions: Vec<[f64; 3]> = conf.coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
let result = sci_form::compute_pm3(&conf.elements, &positions).unwrap();
assert!(result.converged, "PM3 water should converge");
assert!(
result.total_energy.is_finite(),
"Total energy should be finite"
);
assert!(
result.total_energy < 0.0,
"Total energy should be negative: {:.2}",
result.total_energy
);
}
#[test]
fn test_pm3_methane_with_gaussians() {
let elements = [6u8, 1, 1, 1, 1];
let positions = [
[0.0, 0.0, 0.0],
[0.63, 0.63, 0.63],
[-0.63, -0.63, 0.63],
[-0.63, 0.63, -0.63],
[0.63, -0.63, -0.63],
];
let result = sci_form::compute_pm3(&elements, &positions).unwrap();
assert!(result.converged, "CH₄ PM3 should converge");
assert!(result.gap > 0.0, "CH₄ should have positive HOMO-LUMO gap");
let total_charge: f64 = result.mulliken_charges.iter().sum();
assert!(
total_charge.abs() < 0.01,
"Charge conservation: {:.4}",
total_charge
);
}
#[test]
fn test_pm3_gaussian_energy_contribution() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
let result = sci_form::compute_pm3(&elements, &positions).unwrap();
assert!(result.converged, "H₂ PM3 should converge");
assert!(
result.total_energy.is_finite(),
"Total energy should be finite"
);
assert!(
result.nuclear_repulsion.is_finite(),
"Nuclear repulsion should be finite"
);
assert!(
result.nuclear_repulsion > 0.0,
"Nuclear repulsion should be positive for H₂"
);
}
#[test]
fn test_xtb_gfn0_water_broyden_convergence() {
let elements = [8u8, 1, 1];
let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
let result = sci_form::compute_xtb(&elements, &positions).unwrap();
assert!(result.converged, "GFN0 water should converge with Broyden");
assert!(result.gap > 0.0, "Water should have a positive gap");
assert!(
result.mulliken_charges[0] < 0.0,
"O charge should be negative: {:.4}",
result.mulliken_charges[0]
);
}
#[test]
fn test_xtb_gfn0_h2_broyden() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
let result = sci_form::compute_xtb(&elements, &positions).unwrap();
assert!(result.converged, "GFN0 H₂ should converge");
assert!(
(result.mulliken_charges[0] - result.mulliken_charges[1]).abs() < 0.01,
"H₂ charges should be symmetric"
);
}
#[test]
fn test_gfn1_water_broyden_convergence() {
use sci_form::xtb::gfn1::solve_gfn1;
let elements = [8u8, 1, 1];
let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
let result = solve_gfn1(&elements, &positions).unwrap();
assert!(result.converged, "GFN1 water should converge with Broyden");
assert!(result.gap > 0.0, "GFN1 water gap should be positive");
assert!(
!result.shell_charges.is_empty(),
"GFN1 should produce shell charges"
);
}
#[test]
fn test_gfn1_ethanol_broyden() {
use sci_form::xtb::gfn1::solve_gfn1;
let conf = sci_form::embed("CCO", 42);
assert!(conf.error.is_none(), "Embed should succeed");
let positions: Vec<[f64; 3]> = conf.coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
let result = solve_gfn1(&conf.elements, &positions).unwrap();
assert!(result.converged, "GFN1 ethanol should converge");
assert!(
result.total_energy < 0.0,
"GFN1 ethanol energy should be negative"
);
}
#[test]
fn test_nmr_5j_naphthalene() {
use sci_form::nmr::coupling::predict_j_couplings;
let conf = sci_form::embed("c1cccc2ccccc12", 42);
assert!(conf.error.is_none(), "Naphthalene embed should succeed");
let positions: Vec<[f64; 3]> = conf.coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
let mol = sci_form::parse("c1cccc2ccccc12").unwrap();
let couplings = predict_j_couplings(&mol, &positions);
let five_j: Vec<_> = couplings.iter().filter(|c| c.n_bonds == 5).collect();
if !five_j.is_empty() {
for c in &five_j {
assert!(c.j_hz.abs() < 2.0, "5J should be small: {:.2} Hz", c.j_hz);
assert!(
c.coupling_type.contains("5J"),
"Coupling type should contain '5J': {}",
c.coupling_type
);
}
}
}
#[test]
fn test_nmr_coupling_bond_counts() {
use sci_form::nmr::coupling::predict_j_couplings;
let conf = sci_form::embed("CC", 42);
assert!(conf.error.is_none());
let positions: Vec<[f64; 3]> = conf.coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
let mol = sci_form::parse("CC").unwrap();
let couplings = predict_j_couplings(&mol, &positions);
let bond_counts: std::collections::HashSet<usize> =
couplings.iter().map(|c| c.n_bonds).collect();
assert!(
bond_counts.contains(&3),
"Ethane should have ³J couplings, found: {:?}",
bond_counts
);
assert!(
!bond_counts.contains(&5),
"Ethane should not have ⁵J couplings"
);
}
#[test]
fn test_nmr_5j_coupling_type_format() {
use sci_form::nmr::coupling::predict_j_couplings;
let conf = sci_form::embed("c1cccc2ccccc12", 42); assert!(conf.error.is_none());
let positions: Vec<[f64; 3]> = conf.coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
let mol = sci_form::parse("c1cccc2ccccc12").unwrap();
let couplings = predict_j_couplings(&mol, &positions);
for c in &couplings {
if c.n_bonds == 5 {
assert!(
c.coupling_type.starts_with("long_range_5J_H-"),
"Expected 'long_range_5J_H-...' format, got: {}",
c.coupling_type
);
}
}
}
#[test]
fn test_smirks_multi_component_parse() {
let smirks = "[C:1](=[O:2])[OH:3].[O:4]>>[C:1](=[O:2])[O:3].[O:4]";
let parsed = sci_form::smirks::parse_smirks(smirks);
assert!(parsed.is_ok(), "Multi-component SMIRKS should parse");
let p = parsed.unwrap();
assert!(!p.atom_map.is_empty());
assert!(!p.smirks.is_empty());
}
#[test]
fn test_smirks_multi_component_apply() {
let smirks = "[C:1](=[O:2])[OH:3].[C:4][OH:5]>>[C:1](=[O:2])[O:5][C:4]";
let result = sci_form::smirks::apply_smirks_multi(smirks, &["CC(=O)O", "CO"]);
assert!(
result.is_ok(),
"Multi-component apply should not error: {:?}",
result.err()
);
}
#[test]
fn test_smirks_single_component_still_works() {
let result =
sci_form::smirks::apply_smirks("[C:1](=O)[OH:2]>>[C:1](=O)[O-:2]", "CC(=O)O").unwrap();
assert!(result.success, "Single-component deprotonation should work");
assert_eq!(result.n_transforms, 1);
}
#[test]
fn test_smirks_dot_separated_input() {
let smirks = "[C:1](=[O:2])[OH:3].[O:4]>>[C:1](=[O:2])[O:3].[O:4]";
let result = sci_form::smirks::apply_smirks(smirks, "CC(=O)O.O");
assert!(result.is_ok(), "Dot-separated input should be handled");
}
#[test]
fn test_population_water_public_api() {
let elements = [8u8, 1, 1];
let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
let pop = sci_form::compute_population(&elements, &positions).unwrap();
assert!(
pop.mulliken_charges[0] < 0.0,
"O Mulliken should be negative"
);
let total: f64 = pop.mulliken_charges.iter().sum();
assert!(total.abs() < 0.01, "Charge conservation: {:.4}", total);
assert!(
(pop.mulliken_charges[1] - pop.mulliken_charges[2]).abs() < 0.01,
"H charges should be symmetric"
);
}
#[test]
fn test_population_ethanol_public_api() {
let conf = sci_form::embed("CCO", 42);
assert!(conf.error.is_none());
let positions: Vec<[f64; 3]> = conf.coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
let pop = sci_form::compute_population(&conf.elements, &positions).unwrap();
let o_idx = conf.elements.iter().position(|&z| z == 8).unwrap();
assert!(
pop.mulliken_charges[o_idx] < 0.0,
"O should be negative in ethanol: {:.4}",
pop.mulliken_charges[o_idx]
);
}
#[test]
fn test_eeq_charge_neutrality_after_damping_change() {
use sci_form::charges_eeq::{compute_eeq_charges, EeqConfig};
let elements = [6u8, 6, 8, 1, 1, 1, 1, 1];
let positions = [
[0.0, 0.0, 0.0],
[1.54, 0.0, 0.0],
[2.57, 1.03, 0.0],
[-0.63, 0.89, 0.0],
[-0.63, -0.89, 0.0],
[1.54, -0.63, 0.89],
[1.54, -0.63, -0.89],
[3.40, 0.50, 0.0],
];
let config = EeqConfig {
total_charge: 0.0,
regularization: 1e-8,
};
let result = compute_eeq_charges(&elements, &positions, &config);
let sum: f64 = result.charges.iter().sum();
assert!(sum.abs() < 0.01, "EEQ charges should sum to ~0: {:.6}", sum);
}
#[test]
fn test_eeq_oxygen_negative_after_damping_change() {
use sci_form::charges_eeq::{compute_eeq_charges, EeqConfig};
let _elements = [6u8, 6, 8, 1, 1, 1, 1, 1, 1];
let conf = sci_form::embed("CCO", 42);
assert!(conf.error.is_none());
let positions: Vec<[f64; 3]> = conf.coords.chunks(3).map(|c| [c[0], c[1], c[2]]).collect();
let config = EeqConfig {
total_charge: 0.0,
regularization: 1e-8,
};
let result = compute_eeq_charges(&conf.elements, &positions, &config);
let o_idx = conf.elements.iter().position(|&z| z == 8).unwrap();
assert!(
result.charges[o_idx] < 0.0,
"O charge should be negative: {:.4}",
result.charges[o_idx]
);
}
#[test]
fn test_eeq_charged_molecule() {
use sci_form::charges_eeq::{compute_eeq_charges, EeqConfig};
let elements = [8u8, 1, 1];
let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
let config = EeqConfig {
total_charge: -1.0,
regularization: 1e-8,
};
let result = compute_eeq_charges(&elements, &positions, &config);
let sum: f64 = result.charges.iter().sum();
assert!(
(sum - (-1.0)).abs() < 0.05,
"Charges should sum to -1: {:.4}",
sum
);
}
#[test]
fn test_eeq_symmetric_molecule() {
use sci_form::charges_eeq::{compute_eeq_charges, EeqConfig};
let elements = [8u8, 1, 1];
let positions = [[0.0, 0.0, 0.0], [0.757, 0.586, 0.0], [-0.757, 0.586, 0.0]];
let config = EeqConfig {
total_charge: 0.0,
regularization: 1e-8,
};
let result = compute_eeq_charges(&elements, &positions, &config);
let h_diff = (result.charges[1] - result.charges[2]).abs();
assert!(
h_diff < 0.01,
"Water H charges should be symmetric: H1={:.4} H2={:.4}",
result.charges[1],
result.charges[2]
);
}