166 lines
5.3 KiB
Rust
166 lines
5.3 KiB
Rust
//! 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);
|
||
}
|