Files
symclaw/crates/symclaw-qchem/tests/integration.rs
T

166 lines
5.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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(&params, "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);
}