//! Integration tests for symclaw-qchem. //! //! Cross-module scenarios: second quantization + Wick → integrals → spin → VQE. use std::collections::HashMap; use std::f64::consts::PI; use symclaw_qchem::integrals::{GaussianBasis, kinetic_integral, overlap_integral}; use symclaw_qchem::second_quant::{FermionOp, vacuum_expectation, wick_expand}; use symclaw_qchem::spin::clebsch_gordan; use symclaw_qchem::vqe::{Ansatz, HFReference, parameter_shift_gradient}; // ── Wick's theorem pipeline ─────────────────────────────────────── #[test] fn vev_annihilation_creation_same_index() { // ⟨0| a_0 a†_0 |0⟩ = 1 let ops = vec![(0usize, false), (0, true)]; assert_eq!(vacuum_expectation(&ops), 1, "⟨0|a_0 a†_0|0⟩ = 1"); } #[test] fn vev_normal_ordered_is_zero() { // ⟨0| a†_0 a_0 |0⟩ = 0 (no particles in vacuum) let ops = vec![(0usize, true), (0, false)]; assert_eq!(vacuum_expectation(&ops), 0, "⟨0|a†a|0⟩ = 0"); } #[test] fn wick_4_operator_expansion() { // a_0 a_1 a†_0 a†_1: Wick expansion has multiple terms let ops = vec![(0, false), (1, false), (0, true), (1, true)]; let terms = wick_expand(&ops); assert!( terms.len() >= 2, "4-op Wick should have ≥ 2 terms, got {}", terms.len() ); } #[test] fn fermion_op_number_conserving() { // a†_0 a_0: equal creates and annihilates → number-conserving let op = FermionOp::create(0).mul(&FermionOp::annihilate(0)); assert!( op.is_number_conserving(), "a†_0 a_0 should be number-conserving" ); } #[test] fn fermion_op_not_number_conserving() { // a†_0 alone: creates but doesn't annihilate let op = FermionOp::create(0); assert!( !op.is_number_conserving(), "a†_0 alone is not number-conserving" ); } // ── Molecular integrals ─────────────────────────────────────────── #[test] fn overlap_self_is_one() { // ⟨g|g⟩ = 1 for normalised Gaussian let g = GaussianBasis::s_type(1.0, [0.0, 0.0, 0.0]); let s = overlap_integral(&g, &g); assert!((s - 1.0).abs() < 1e-6, "⟨g|g⟩ = 1, got {s}"); } #[test] fn overlap_symmetric() { // ⟨a|b⟩ = ⟨b|a⟩ let g1 = GaussianBasis::s_type(1.0, [0.0, 0.0, 0.0]); let g2 = GaussianBasis::s_type(0.5, [1.5, 0.0, 0.0]); let s12 = overlap_integral(&g1, &g2); let s21 = overlap_integral(&g2, &g1); assert!((s12 - s21).abs() < 1e-10, "⟨a|b⟩ = ⟨b|a⟩: {s12} vs {s21}"); } #[test] fn overlap_decays_with_distance() { let g0 = GaussianBasis::s_type(1.0, [0.0, 0.0, 0.0]); let g_near = GaussianBasis::s_type(1.0, [1.0, 0.0, 0.0]); let g_far = GaussianBasis::s_type(1.0, [5.0, 0.0, 0.0]); let s_near = overlap_integral(&g0, &g_near); let s_far = overlap_integral(&g0, &g_far); assert!( s_near > s_far, "overlap decreases with distance: {s_near:.4} > {s_far:.4}" ); } #[test] fn kinetic_nonzero_for_s_type() { let g = GaussianBasis::s_type(1.0, [0.0, 0.0, 0.0]); let t = kinetic_integral(&g, &g); assert!(t.abs() > 0.0, "⟨g|T|g⟩ should be non-zero, got {t}"); } #[test] fn kinetic_larger_for_tighter_gaussian() { // Higher α → more localised → higher kinetic energy let tight = GaussianBasis::s_type(4.0, [0.0, 0.0, 0.0]); let loose = GaussianBasis::s_type(0.5, [0.0, 0.0, 0.0]); let t_tight = kinetic_integral(&tight, &tight).abs(); let t_loose = kinetic_integral(&loose, &loose).abs(); assert!( t_tight > t_loose, "tight Gaussian (α=4) has higher KE: {t_tight:.3} > {t_loose:.3}" ); } // ── Clebsch-Gordan / angular momentum ───────────────────────────── #[test] fn cg_singlet_state() { // ⟨1/2,+1/2; 1/2,-1/2 | 0,0⟩ = 1/√2 let cg = clebsch_gordan(1, 1, 1, -1, 0, 0); assert!( (cg.abs() - 1.0 / 2.0_f64.sqrt()).abs() < 1e-8, "CG singlet = ±1/√2, got {cg}" ); } #[test] fn cg_completeness() { // Σ_J |CG(1/2,1/2;1/2,-1/2|J,0)|² = 1 let cg_s = clebsch_gordan(1, 1, 1, -1, 0, 0); let cg_t = clebsch_gordan(1, 1, 1, -1, 2, 0); let sum = cg_s * cg_s + cg_t * cg_t; assert!((sum - 1.0).abs() < 1e-8, "CG completeness: {sum}"); } // ── VQE / UCCSD ─────────────────────────────────────────────────── #[test] fn uccsd_h2_params() { // H₂: 2 electrons, 4 spin-orbitals → 4 singles + 1 double = 5 params let hf = HFReference::new(2, 4); assert_eq!(hf.n_uccsd_params(), 5); } #[test] fn parameter_shift_sin() { // ∂sin(θ)/∂θ = cos(θ) let theta = PI / 4.0; let mut params = HashMap::new(); params.insert("theta".to_owned(), theta); let grad = parameter_shift_gradient(¶ms, "theta", &|p| p["theta"].sin()); let expected = theta.cos(); assert!( (grad - expected).abs() < 1e-6, "param-shift: {grad} ≈ {expected}" ); } #[test] fn ansatz_builder_layers() { let mut ans = Ansatz::new(4); ans.add_rz_layer(0, "t0", 0.0); ans.add_cnot_layer(0, 1); ans.add_rz_layer(1, "t1", 0.5); assert_eq!(ans.n_params(), 2); assert_eq!(ans.layers.len(), 3); }