use super::chebyshev::{
estimate_spectral_bounds, jackson_kernel, rescale_matrix, ChebyshevExpansion,
};
use nalgebra::DMatrix;
#[derive(Debug, Clone)]
pub struct KpmConfig {
pub order: usize,
pub n_vectors: usize,
pub seed: u64,
pub temperature: f64,
}
impl Default for KpmConfig {
fn default() -> Self {
Self {
order: 100,
n_vectors: 0, seed: 42,
temperature: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct KpmDosResult {
pub energies: Vec<f64>,
pub total_dos: Vec<f64>,
pub e_min: f64,
pub e_max: f64,
pub order: usize,
}
#[derive(Debug, Clone)]
pub struct KpmMullikenResult {
pub charges: Vec<f64>,
pub n_electrons_kpm: f64,
pub n_electrons_target: usize,
pub fermi_level: f64,
}
pub fn fermi_dirac_coefficients(
order: usize,
a: f64,
b: f64,
mu: f64,
temperature: f64,
) -> Vec<f64> {
let n_quad = order * 4; let mut coeffs = vec![0.0; order];
for q in 0..n_quad {
let theta = std::f64::consts::PI * (q as f64 + 0.5) / n_quad as f64;
let x = theta.cos();
let energy = a * x + b;
let f_fd = if temperature < 1e-10 {
if energy < mu {
1.0
} else if energy > mu {
0.0
} else {
0.5
}
} else {
let kt = temperature * 8.617333262e-5; 1.0 / (1.0 + ((energy - mu) / kt).exp())
};
let mut t_prev = 1.0;
let mut t_curr = x;
coeffs[0] += f_fd * t_prev;
if order > 1 {
coeffs[1] += f_fd * t_curr;
}
for k in 2..order {
let t_next = 2.0 * x * t_curr - t_prev;
coeffs[k] += f_fd * t_next;
t_prev = t_curr;
t_curr = t_next;
}
}
let factor = 2.0 / n_quad as f64;
for c in coeffs.iter_mut() {
*c *= factor;
}
coeffs[0] *= 0.5;
coeffs
}
pub fn compute_kpm_dos(
h: &DMatrix<f64>,
config: &KpmConfig,
e_min_out: f64,
e_max_out: f64,
n_points: usize,
) -> KpmDosResult {
let expansion = if config.n_vectors == 0 {
ChebyshevExpansion::from_matrix_exact(h, config.order)
} else {
ChebyshevExpansion::from_matrix(h, config.order, config.n_vectors, config.seed)
};
let gk = jackson_kernel(config.order);
let step = (e_max_out - e_min_out) / (n_points - 1).max(1) as f64;
let energies: Vec<f64> = (0..n_points).map(|i| e_min_out + i as f64 * step).collect();
let total_dos: Vec<f64> = energies
.iter()
.map(|&e| expansion.dos_at_energy(e, &gk))
.collect();
KpmDosResult {
energies,
total_dos,
e_min: expansion.b - expansion.a,
e_max: expansion.b + expansion.a,
order: config.order,
}
}
pub fn compute_kpm_mulliken(
h: &DMatrix<f64>,
s: &DMatrix<f64>,
n_electrons: usize,
nuclear_charges: &[f64],
config: &KpmConfig,
) -> KpmMullikenResult {
let n = h.nrows();
let s_eigen = nalgebra::SymmetricEigen::new(s.clone());
let mut s_inv_sqrt = DMatrix::zeros(n, n);
for i in 0..n {
let v = s_eigen.eigenvalues[i];
if v > 1e-10 {
s_inv_sqrt[(i, i)] = 1.0 / v.sqrt();
}
}
let s_inv_sqrt = &s_eigen.eigenvectors * &s_inv_sqrt * s_eigen.eigenvectors.transpose();
let h_orth = &s_inv_sqrt * h * &s_inv_sqrt;
let (e_min, e_max) = estimate_spectral_bounds(&h_orth);
let a = (e_max - e_min) / 2.0;
let b = (e_max + e_min) / 2.0;
let h_tilde = rescale_matrix(&h_orth, e_min, e_max);
let target = n_electrons as f64;
let mut mu_lo = e_min;
let mut mu_hi = e_max;
for _ in 0..60 {
let mu = (mu_lo + mu_hi) / 2.0;
let ne = electron_count_kpm(&h_tilde, a, b, mu, config);
if ne < target {
mu_lo = mu;
} else {
mu_hi = mu;
}
}
let mu = (mu_lo + mu_hi) / 2.0;
let coeffs = fermi_dirac_coefficients(config.order, a, b, mu, config.temperature);
let gk = jackson_kernel(config.order);
let mut p_orth = DMatrix::zeros(n, n);
let mut t_prev = DMatrix::identity(n, n);
let mut t_curr = h_tilde.clone();
let c0 = coeffs[0] * gk[0];
p_orth += &t_prev * c0;
if config.order > 1 {
let c1 = 2.0 * coeffs[1] * gk[1];
p_orth += &t_curr * c1;
}
for k in 2..config.order {
let t_next = &h_tilde * &t_curr * 2.0 - &t_prev;
let ck = 2.0 * coeffs[k] * gk[k.min(gk.len() - 1)];
p_orth += &t_next * ck;
t_prev = t_curr;
t_curr = t_next;
}
let p_ao = &s_inv_sqrt * &p_orth * &s_inv_sqrt;
let ps = &p_ao * s;
let n_elec_kpm: f64 = (0..n).map(|i| ps[(i, i)]).sum();
let mut charges = Vec::with_capacity(nuclear_charges.len());
if nuclear_charges.len() == n {
for i in 0..n {
charges.push(nuclear_charges[i] - ps[(i, i)]);
}
} else {
for z in nuclear_charges {
charges.push(*z);
}
}
KpmMullikenResult {
charges,
n_electrons_kpm: n_elec_kpm,
n_electrons_target: n_electrons,
fermi_level: mu,
}
}
fn electron_count_kpm(h_tilde: &DMatrix<f64>, a: f64, b: f64, mu: f64, config: &KpmConfig) -> f64 {
let n = h_tilde.nrows();
let coeffs = fermi_dirac_coefficients(config.order, a, b, mu, config.temperature);
let gk = jackson_kernel(config.order);
let mut t_prev = DMatrix::identity(n, n);
let mut t_curr = h_tilde.clone();
let mut ne = coeffs[0] * gk[0] * t_prev.trace();
if config.order > 1 {
ne += 2.0 * coeffs[1] * gk[1] * t_curr.trace();
}
for k in 2..config.order {
let t_next = h_tilde * &t_curr * 2.0 - &t_prev;
ne += 2.0 * coeffs[k] * gk[k.min(gk.len() - 1)] * t_next.trace();
t_prev = t_curr;
t_curr = t_next;
}
ne
}
#[cfg(test)]
mod tests {
use super::*;
fn huckel_chain(n: usize, alpha: f64, beta: f64) -> DMatrix<f64> {
let mut h = DMatrix::zeros(n, n);
for i in 0..n {
h[(i, i)] = alpha;
if i + 1 < n {
h[(i, i + 1)] = beta;
h[(i + 1, i)] = beta;
}
}
h
}
#[test]
fn test_fermi_dirac_coefficients_step_function() {
let coeffs = fermi_dirac_coefficients(50, 5.0, -5.0, -5.0, 0.0);
assert!(coeffs[0].abs() < 2.0, "c_0 = {}", coeffs[0]);
}
#[test]
fn test_kpm_dos_shape() {
let h = huckel_chain(20, 0.0, -1.0);
let config = KpmConfig {
order: 80,
n_vectors: 0,
..Default::default()
};
let dos = compute_kpm_dos(&h, &config, -3.0, 3.0, 100);
for &d in &dos.total_dos {
assert!(d >= -1e-10, "Negative DOS: {}", d);
}
let mid_idx = dos.total_dos.len() / 2;
assert!(
dos.total_dos[mid_idx] > 0.0,
"DOS at band center = {}",
dos.total_dos[mid_idx]
);
}
#[test]
fn test_kpm_mulliken_charge_conservation() {
let n = 10;
let h = huckel_chain(n, -10.0, -2.0);
let s = DMatrix::identity(n, n);
let n_electrons = n; let nuclear_charges: Vec<f64> = vec![1.0; n];
let config = KpmConfig {
order: 60,
n_vectors: 0,
..Default::default()
};
let result = compute_kpm_mulliken(&h, &s, n_electrons, &nuclear_charges, &config);
let total: f64 = result.charges.iter().sum();
assert!(
total.abs() < 1.0,
"Total Mulliken charge = {:.4}, expected ~0",
total
);
}
#[test]
fn test_electron_count_at_fermi_level() {
let n = 10;
let h = huckel_chain(n, 0.0, -1.0);
let (e_min, e_max) = estimate_spectral_bounds(&h);
let h_tilde = rescale_matrix(&h, e_min, e_max);
let a = (e_max - e_min) / 2.0;
let b_val = (e_max + e_min) / 2.0;
let config = KpmConfig {
order: 80,
n_vectors: 0,
..Default::default()
};
let ne = electron_count_kpm(&h_tilde, a, b_val, 0.0, &config);
assert!(
(ne - n as f64 / 2.0).abs() < 2.0,
"Expected ~{} electrons at mu=0, got {:.2}",
n / 2,
ne
);
}
}