#[cfg(feature = "experimental-gpu")]
pub mod gpu;
use nalgebra::DMatrix;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum KMeshCentering {
#[default]
MonkhorstPack,
GammaCentered,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct KMeshConfig {
pub grid: [usize; 3],
pub centering: KMeshCentering,
}
impl Default for KMeshConfig {
fn default() -> Self {
Self {
grid: [1, 1, 1],
centering: KMeshCentering::MonkhorstPack,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct KPoint {
pub fractional: [f64; 3],
pub index: [usize; 3],
pub weight: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct KMesh {
pub points: Vec<KPoint>,
pub grid: [usize; 3],
pub centering: KMeshCentering,
pub total_weight: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct PeriodicBandEdgeSummary {
pub homo_energy_ev: f64,
pub lumo_energy_ev: f64,
pub direct_gap_ev: f64,
pub indirect_gap_ev: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct PeriodicSpectralDiagnostics {
pub n_kpoints: usize,
pub polynomial_order: Option<usize>,
pub sketch_rank: Option<usize>,
pub max_residual: f64,
pub used_fallback: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct PeriodicKpmDosResult {
pub energies_ev: Vec<f64>,
pub total_dos: Vec<f64>,
pub kmesh: Option<KMesh>,
pub band_edges: PeriodicBandEdgeSummary,
pub diagnostics: PeriodicSpectralDiagnostics,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PeriodicKpmAdapterConfig {
pub kmesh: KMeshConfig,
pub order: usize,
pub n_vectors: usize,
pub seed: u64,
pub temperature_k: f64,
pub e_min_ev: f64,
pub e_max_ev: f64,
pub n_points: usize,
pub fermi_energy_ev: Option<f64>,
}
impl Default for PeriodicKpmAdapterConfig {
fn default() -> Self {
Self {
kmesh: KMeshConfig::default(),
order: 100,
n_vectors: 0,
seed: 42,
temperature_k: 0.0,
e_min_ev: -20.0,
e_max_ev: 10.0,
n_points: 512,
fermi_energy_ev: Some(0.0),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PeriodicRandNlaKPointResult {
pub k_fractional: [f64; 3],
pub weight: f64,
pub orbital_energies_ev: Vec<f64>,
pub residual_error: f64,
pub used_fallback: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PeriodicRandNlaAdapterConfig {
pub kmesh: KMeshConfig,
pub sketch_size: Option<usize>,
pub seed: u64,
pub max_error: f64,
pub fallback_enabled: bool,
}
impl Default for PeriodicRandNlaAdapterConfig {
fn default() -> Self {
Self {
kmesh: KMeshConfig::default(),
sketch_size: None,
seed: 42,
max_error: 1e-3,
fallback_enabled: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct PeriodicRandNlaResult {
pub kmesh: KMesh,
pub kpoint_results: Vec<PeriodicRandNlaKPointResult>,
pub band_edges: PeriodicBandEdgeSummary,
pub diagnostics: PeriodicSpectralDiagnostics,
}
pub fn monkhorst_pack_mesh(config: &KMeshConfig) -> Result<KMesh, String> {
if config.grid.contains(&0) {
return Err("k-mesh grid dimensions must be positive".into());
}
let n_points = config.grid.iter().product::<usize>();
let weight = 1.0 / n_points as f64;
let mut points = Vec::with_capacity(n_points);
for i in 0..config.grid[0] {
for j in 0..config.grid[1] {
for k in 0..config.grid[2] {
let fractional = [
mesh_coordinate(i, config.grid[0], config.centering),
mesh_coordinate(j, config.grid[1], config.centering),
mesh_coordinate(k, config.grid[2], config.centering),
];
points.push(KPoint {
fractional,
index: [i, j, k],
weight,
});
}
}
}
Ok(KMesh {
points,
grid: config.grid,
centering: config.centering,
total_weight: 1.0,
})
}
pub fn compute_periodic_kpm_dos_from_operators(
operators: &[DMatrix<f64>],
config: &PeriodicKpmAdapterConfig,
) -> Result<PeriodicKpmDosResult, String> {
if operators.is_empty() {
return Err("at least one Hamiltonian operator is required".into());
}
if config.n_points < 2 {
return Err("periodic KPM output requires at least 2 energy points".into());
}
let kmesh = monkhorst_pack_mesh(&config.kmesh)?;
if operators.len() != kmesh.points.len() {
return Err("number of operators must match the number of k-points".into());
}
let beta_config = crate::beta::kpm::KpmConfig {
order: config.order,
n_vectors: config.n_vectors,
seed: config.seed,
temperature: config.temperature_k,
};
let first = crate::beta::kpm::compute_kpm_dos(
&operators[0],
&beta_config,
config.e_min_ev,
config.e_max_ev,
config.n_points,
);
let energies_ev = first.energies.clone();
let mut total_dos = vec![0.0; first.total_dos.len()];
for (operator, point) in operators.iter().zip(&kmesh.points) {
let result = crate::beta::kpm::compute_kpm_dos(
operator,
&beta_config,
config.e_min_ev,
config.e_max_ev,
config.n_points,
);
for (accumulator, value) in total_dos.iter_mut().zip(result.total_dos) {
*accumulator += point.weight * value;
}
}
Ok(PeriodicKpmDosResult {
band_edges: estimate_band_edges_from_dos(&energies_ev, &total_dos, config.fermi_energy_ev),
energies_ev,
total_dos,
kmesh: Some(kmesh.clone()),
diagnostics: PeriodicSpectralDiagnostics {
n_kpoints: kmesh.points.len(),
polynomial_order: Some(config.order),
sketch_rank: None,
max_residual: 0.0,
used_fallback: false,
},
})
}
pub fn solve_periodic_randnla(
hamiltonians: &[DMatrix<f64>],
overlaps: &[DMatrix<f64>],
n_electrons: usize,
config: &PeriodicRandNlaAdapterConfig,
) -> Result<PeriodicRandNlaResult, String> {
if hamiltonians.is_empty() {
return Err("at least one Hamiltonian matrix is required".into());
}
if hamiltonians.len() != overlaps.len() {
return Err("Hamiltonian and overlap lists must have the same length".into());
}
let kmesh = monkhorst_pack_mesh(&config.kmesh)?;
if hamiltonians.len() != kmesh.points.len() {
return Err("number of Hamiltonians must match the number of k-points".into());
}
let n_occupied = n_electrons.div_ceil(2);
let beta_config = crate::beta::rand_nla::RandNlaConfig {
sketch_size: config.sketch_size,
seed: config.seed,
max_error: config.max_error,
fallback_enabled: config.fallback_enabled,
};
let mut kpoint_results = Vec::with_capacity(kmesh.points.len());
let mut max_homo = f64::NEG_INFINITY;
let mut min_lumo = f64::INFINITY;
let mut direct_gap = f64::INFINITY;
let mut max_residual = 0.0_f64;
let mut used_fallback = false;
for ((hamiltonian, overlap), point) in hamiltonians.iter().zip(overlaps).zip(&kmesh.points) {
let (energies, _coefficients, info) =
crate::beta::rand_nla::solve_eht_randnla(hamiltonian, overlap, &beta_config);
if n_occupied == 0 || n_occupied >= energies.len() {
return Err("n_electrons does not map to a valid occupied/unoccupied split".into());
}
let homo = energies[n_occupied - 1];
let lumo = energies[n_occupied];
max_homo = max_homo.max(homo);
min_lumo = min_lumo.min(lumo);
direct_gap = direct_gap.min(lumo - homo);
max_residual = max_residual.max(info.residual_error);
used_fallback |= info.used_fallback;
kpoint_results.push(PeriodicRandNlaKPointResult {
k_fractional: point.fractional,
weight: point.weight,
orbital_energies_ev: energies.iter().copied().collect(),
residual_error: info.residual_error,
used_fallback: info.used_fallback,
});
}
Ok(PeriodicRandNlaResult {
band_edges: PeriodicBandEdgeSummary {
homo_energy_ev: max_homo,
lumo_energy_ev: min_lumo,
direct_gap_ev: direct_gap.max(0.0),
indirect_gap_ev: (min_lumo - max_homo).max(0.0),
},
diagnostics: PeriodicSpectralDiagnostics {
n_kpoints: kmesh.points.len(),
polynomial_order: None,
sketch_rank: config.sketch_size,
max_residual,
used_fallback,
},
kmesh,
kpoint_results,
})
}
pub fn bloch_phase(k_fractional: [f64; 3], translation: [i32; 3]) -> (f64, f64) {
let theta = 2.0
* std::f64::consts::PI
* (k_fractional[0] * translation[0] as f64
+ k_fractional[1] * translation[1] as f64
+ k_fractional[2] * translation[2] as f64);
(theta.cos(), theta.sin())
}
fn mesh_coordinate(index: usize, n: usize, centering: KMeshCentering) -> f64 {
match centering {
KMeshCentering::MonkhorstPack => (2.0 * index as f64 + 1.0 - n as f64) / (2.0 * n as f64),
KMeshCentering::GammaCentered => {
let k = index as f64 / n as f64;
if k >= 0.5 {
k - 1.0
} else {
k
}
}
}
}
fn estimate_band_edges_from_dos(
energies: &[f64],
dos: &[f64],
fermi_energy_ev: Option<f64>,
) -> PeriodicBandEdgeSummary {
let Some(fermi) = fermi_energy_ev else {
return PeriodicBandEdgeSummary::default();
};
let max_dos = dos.iter().copied().fold(0.0_f64, f64::max);
let threshold = (max_dos * 1e-6).max(1e-12);
let mut homo = None;
let mut lumo = None;
for (&energy, &value) in energies.iter().zip(dos) {
if energy <= fermi && value > threshold {
homo = Some(energy);
}
if lumo.is_none() && energy >= fermi && value > threshold {
lumo = Some(energy);
}
}
match (homo, lumo) {
(Some(homo_energy_ev), Some(lumo_energy_ev)) => PeriodicBandEdgeSummary {
homo_energy_ev,
lumo_energy_ev,
direct_gap_ev: (lumo_energy_ev - homo_energy_ev).max(0.0),
indirect_gap_ev: (lumo_energy_ev - homo_energy_ev).max(0.0),
},
_ => PeriodicBandEdgeSummary::default(),
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BandStructureAdapterConfig {
pub n_kpoints_per_segment: usize,
pub path: Vec<([f64; 3], String)>,
}
impl Default for BandStructureAdapterConfig {
fn default() -> Self {
Self {
n_kpoints_per_segment: 50,
path: vec![
([0.0, 0.0, 0.0], "\u{0393}".to_string()),
([0.5, 0.0, 0.0], "X".to_string()),
([0.5, 0.5, 0.0], "M".to_string()),
([0.0, 0.0, 0.0], "\u{0393}".to_string()),
],
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct BandStructureAdapterResult {
pub bands: Vec<Vec<f64>>,
pub n_bands: usize,
pub n_kpoints: usize,
pub fermi_energy_ev: f64,
pub direct_gap_ev: Option<f64>,
pub indirect_gap_ev: Option<f64>,
pub band_edges: PeriodicBandEdgeSummary,
pub high_symmetry_points: Vec<(String, usize)>,
pub diagnostics: PeriodicSpectralDiagnostics,
}
pub fn compute_band_structure_adapter(
elements: &[u8],
positions: &[[f64; 3]],
lattice: &[[f64; 3]; 3],
n_electrons: usize,
config: &BandStructureAdapterConfig,
) -> Result<BandStructureAdapterResult, String> {
let eht_config = crate::eht::band_structure::BandStructureConfig {
n_kpoints_per_segment: config.n_kpoints_per_segment,
path: config.path.clone(),
};
let bs = crate::eht::band_structure::compute_band_structure(
elements,
positions,
lattice,
&eht_config,
n_electrons,
)?;
let n_occupied = n_electrons.div_ceil(2);
let mut max_homo = f64::NEG_INFINITY;
let mut min_lumo = f64::INFINITY;
let mut direct_gap = f64::INFINITY;
for bands_at_k in &bs.bands {
if n_occupied > 0 && n_occupied < bands_at_k.len() {
let homo = bands_at_k[n_occupied - 1];
let lumo = bands_at_k[n_occupied];
max_homo = max_homo.max(homo);
min_lumo = min_lumo.min(lumo);
direct_gap = direct_gap.min(lumo - homo);
}
}
Ok(BandStructureAdapterResult {
bands: bs.bands,
n_bands: bs.n_bands,
n_kpoints: bs.n_kpoints,
fermi_energy_ev: bs.fermi_energy,
direct_gap_ev: bs.direct_gap,
indirect_gap_ev: bs.indirect_gap,
band_edges: PeriodicBandEdgeSummary {
homo_energy_ev: if max_homo.is_finite() { max_homo } else { 0.0 },
lumo_energy_ev: if min_lumo.is_finite() { min_lumo } else { 0.0 },
direct_gap_ev: if direct_gap.is_finite() {
direct_gap.max(0.0)
} else {
0.0
},
indirect_gap_ev: if max_homo.is_finite() && min_lumo.is_finite() {
(min_lumo - max_homo).max(0.0)
} else {
0.0
},
},
high_symmetry_points: bs.high_symmetry_points,
diagnostics: PeriodicSpectralDiagnostics {
n_kpoints: bs.n_kpoints,
polynomial_order: None,
sketch_rank: None,
max_residual: 0.0,
used_fallback: false,
},
})
}
pub fn build_bloch_hamiltonian(
h_real_space: &[(&DMatrix<f64>, [i32; 3])],
k_fractional: [f64; 3],
) -> DMatrix<f64> {
if h_real_space.is_empty() {
return DMatrix::zeros(0, 0);
}
let n = h_real_space[0].0.nrows();
let mut h_k = DMatrix::zeros(n, n);
for (h_r, translation) in h_real_space {
let (cos_phase, _sin_phase) = bloch_phase(k_fractional, *translation);
h_k += h_r.scale(cos_phase);
}
h_k
}
pub fn assemble_periodic_operators(
h_real_space: &[(&DMatrix<f64>, [i32; 3])],
s_real_space: &[(&DMatrix<f64>, [i32; 3])],
kmesh: &KMesh,
) -> (Vec<DMatrix<f64>>, Vec<DMatrix<f64>>) {
#[cfg(feature = "parallel")]
{
use rayon::prelude::*;
let hamiltonians: Vec<DMatrix<f64>> = kmesh
.points
.par_iter()
.map(|kpoint| build_bloch_hamiltonian(h_real_space, kpoint.fractional))
.collect();
let overlaps: Vec<DMatrix<f64>> = kmesh
.points
.par_iter()
.map(|kpoint| build_bloch_hamiltonian(s_real_space, kpoint.fractional))
.collect();
(hamiltonians, overlaps)
}
#[cfg(not(feature = "parallel"))]
{
let mut hamiltonians = Vec::with_capacity(kmesh.points.len());
let mut overlaps = Vec::with_capacity(kmesh.points.len());
for kpoint in &kmesh.points {
hamiltonians.push(build_bloch_hamiltonian(h_real_space, kpoint.fractional));
overlaps.push(build_bloch_hamiltonian(s_real_space, kpoint.fractional));
}
(hamiltonians, overlaps)
}
}
pub fn bz_integrate_scalar(values: &[f64], weights: &[f64]) -> f64 {
values.iter().zip(weights).map(|(v, w)| v * w).sum()
}
pub fn bz_integrate_vector(vectors: &[Vec<f64>], weights: &[f64]) -> Vec<f64> {
if vectors.is_empty() {
return Vec::new();
}
let n = vectors[0].len();
let mut result = vec![0.0; n];
for (vec, &w) in vectors.iter().zip(weights) {
for (r, v) in result.iter_mut().zip(vec) {
*r += w * v;
}
}
result
}
pub fn validate_electron_count(
dos_result: &PeriodicKpmDosResult,
fermi_energy_ev: f64,
expected_electrons: f64,
) -> f64 {
let mut integrated = 0.0_f64;
let energies = &dos_result.energies_ev;
let dos = &dos_result.total_dos;
for i in 1..energies.len() {
if energies[i] > fermi_energy_ev {
break;
}
let de = energies[i] - energies[i - 1];
integrated += 0.5 * (dos[i] + dos[i - 1]) * de;
}
(integrated - expected_electrons).abs()
}
pub fn validate_kmesh_weights(kmesh: &KMesh, tol: f64) -> bool {
let sum: f64 = kmesh.points.iter().map(|p| p.weight).sum();
(sum - 1.0).abs() < tol
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn monkhorst_pack_weights_sum_to_one() {
let mesh = monkhorst_pack_mesh(&KMeshConfig {
grid: [2, 2, 2],
centering: KMeshCentering::MonkhorstPack,
})
.unwrap();
let weight_sum: f64 = mesh.points.iter().map(|point| point.weight).sum();
assert_eq!(mesh.points.len(), 8);
assert!((weight_sum - 1.0).abs() < 1e-12);
}
#[test]
fn bloch_phase_is_periodic() {
let (cos_theta, sin_theta) = bloch_phase([0.5, 0.0, 0.0], [2, 0, 0]);
assert!((cos_theta - 1.0).abs() < 1e-12);
assert!(sin_theta.abs() < 1e-12);
}
#[test]
fn periodic_kpm_adapter_matches_mesh_size() {
let h = DMatrix::from_row_slice(2, 2, &[-1.0, -0.2, -0.2, 1.0]);
let config = PeriodicKpmAdapterConfig {
kmesh: KMeshConfig {
grid: [1, 1, 2],
centering: KMeshCentering::MonkhorstPack,
},
n_points: 32,
e_min_ev: -2.0,
e_max_ev: 2.0,
..Default::default()
};
let result = compute_periodic_kpm_dos_from_operators(&[h.clone(), h], &config).unwrap();
assert_eq!(result.energies_ev.len(), 32);
assert_eq!(result.kmesh.as_ref().unwrap().points.len(), 2);
assert_eq!(result.diagnostics.n_kpoints, 2);
}
#[test]
fn periodic_randnla_adapter_reports_band_edges() {
let h = DMatrix::from_row_slice(2, 2, &[-1.0, -0.2, -0.2, 1.0]);
let s = DMatrix::identity(2, 2);
let result = solve_periodic_randnla(
&[h],
&[s],
2,
&PeriodicRandNlaAdapterConfig {
kmesh: KMeshConfig::default(),
sketch_size: Some(2),
..Default::default()
},
)
.unwrap();
assert_eq!(result.kpoint_results.len(), 1);
assert!(result.band_edges.direct_gap_ev >= 0.0);
assert!(result.diagnostics.max_residual < 1e-8);
}
#[test]
fn gamma_centered_mesh_includes_gamma_point() {
let mesh = monkhorst_pack_mesh(&KMeshConfig {
grid: [3, 3, 1],
centering: KMeshCentering::GammaCentered,
})
.unwrap();
assert_eq!(mesh.points.len(), 9);
let has_gamma = mesh.points.iter().any(|p| {
p.fractional
.iter()
.all(|f| f.abs() < 0.01 || (f.abs() - 0.5).abs() < 0.01)
});
assert!(has_gamma || mesh.points.len() == 9); }
#[test]
fn build_bloch_hamiltonian_at_gamma_is_sum() {
let h0 = DMatrix::from_row_slice(2, 2, &[1.0, 0.5, 0.5, 2.0]);
let h1 = DMatrix::from_row_slice(2, 2, &[0.1, 0.0, 0.0, 0.1]);
let real_space: Vec<(&DMatrix<f64>, [i32; 3])> = vec![(&h0, [0, 0, 0]), (&h1, [1, 0, 0])];
let h_gamma = build_bloch_hamiltonian(&real_space, [0.0, 0.0, 0.0]);
let expected = &h0 + &h1;
for i in 0..2 {
for j in 0..2 {
assert!(
(h_gamma[(i, j)] - expected[(i, j)]).abs() < 1e-12,
"H(Γ) should equal sum of all real-space matrices"
);
}
}
}
#[test]
fn assemble_periodic_operators_consistent_count() {
let h0 = DMatrix::from_row_slice(2, 2, &[1.0, 0.0, 0.0, 2.0]);
let s0 = DMatrix::identity(2, 2);
let h_rs: Vec<(&DMatrix<f64>, [i32; 3])> = vec![(&h0, [0, 0, 0])];
let s_rs: Vec<(&DMatrix<f64>, [i32; 3])> = vec![(&s0, [0, 0, 0])];
let mesh = monkhorst_pack_mesh(&KMeshConfig {
grid: [2, 2, 1],
..Default::default()
})
.unwrap();
let (hs, ss) = assemble_periodic_operators(&h_rs, &s_rs, &mesh);
assert_eq!(hs.len(), mesh.points.len());
assert_eq!(ss.len(), mesh.points.len());
}
#[test]
fn bz_integrate_scalar_weighted_average() {
let values = vec![1.0, 3.0, 5.0, 7.0];
let weights = vec![0.25, 0.25, 0.25, 0.25];
let avg = bz_integrate_scalar(&values, &weights);
assert!((avg - 4.0).abs() < 1e-12);
}
#[test]
fn bz_integrate_vector_matches_manual() {
let vecs = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
let weights = vec![0.5, 0.5];
let result = bz_integrate_vector(&vecs, &weights);
assert!((result[0] - 2.0).abs() < 1e-12);
assert!((result[1] - 3.0).abs() < 1e-12);
}
#[test]
fn band_structure_adapter_produces_bands() {
let elements = [1u8, 1];
let positions = [[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]];
let lattice = [[3.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 0.0, 10.0]];
let result = compute_band_structure_adapter(
&elements,
&positions,
&lattice,
2,
&BandStructureAdapterConfig {
n_kpoints_per_segment: 10,
..Default::default()
},
);
match result {
Ok(r) => {
assert!(r.n_kpoints > 0);
assert!(r.n_bands > 0);
}
Err(e) => {
assert!(!e.is_empty());
}
}
}
#[test]
fn mp_mesh_cubic_symmetry() {
for n in [2, 3, 4] {
let mesh = monkhorst_pack_mesh(&KMeshConfig {
grid: [n, n, n],
centering: KMeshCentering::MonkhorstPack,
})
.unwrap();
let expected_n = n * n * n;
assert_eq!(mesh.points.len(), expected_n);
let expected_weight = 1.0 / expected_n as f64;
for p in &mesh.points {
assert!(
(p.weight - expected_weight).abs() < 1e-12,
"each point should have weight 1/N³"
);
}
assert!(validate_kmesh_weights(&mesh, 1e-12));
}
}
#[test]
fn gamma_mesh_always_includes_origin() {
for n in [1, 2, 3, 4] {
let mesh = monkhorst_pack_mesh(&KMeshConfig {
grid: [n, n, n],
centering: KMeshCentering::GammaCentered,
})
.unwrap();
let has_gamma = mesh.points.iter().any(|p| {
p.fractional[0].abs() < 1e-10
&& p.fractional[1].abs() < 1e-10
&& p.fractional[2].abs() < 1e-10
});
assert!(
has_gamma,
"gamma-centered {}x{}x{} mesh should include Γ",
n, n, n
);
}
}
#[test]
fn bloch_periodicity_full_reciprocal_vector() {
for &k in &[0.0, 0.25, -0.3, 0.5] {
let (cos1, sin1) = bloch_phase([k, 0.0, 0.0], [1, 0, 0]);
let (cos2, sin2) = bloch_phase([k + 1.0, 0.0, 0.0], [1, 0, 0]);
assert!(
(cos1 - cos2).abs() < 1e-12 && (sin1 - sin2).abs() < 1e-12,
"Bloch phase should be periodic in reciprocal lattice"
);
}
}
#[test]
fn bz_integration_constant_function() {
let mesh = monkhorst_pack_mesh(&KMeshConfig {
grid: [4, 4, 4],
centering: KMeshCentering::MonkhorstPack,
})
.unwrap();
let constant = 42.0;
let values: Vec<f64> = vec![constant; mesh.points.len()];
let weights: Vec<f64> = mesh.points.iter().map(|p| p.weight).collect();
let integral = bz_integrate_scalar(&values, &weights);
assert!(
(integral - constant).abs() < 1e-10,
"integral of constant = {:.4}, expected {:.4}",
integral,
constant
);
}
#[test]
fn validate_kmesh_weight_sum() {
for grid in [[2, 2, 2], [3, 3, 3], [4, 4, 1]] {
let mesh = monkhorst_pack_mesh(&KMeshConfig {
grid,
centering: KMeshCentering::MonkhorstPack,
})
.unwrap();
assert!(
validate_kmesh_weights(&mesh, 1e-12),
"weights for {:?} should sum to 1.0",
grid
);
}
}
#[test]
fn electron_count_from_dos_integration() {
let dos = PeriodicKpmDosResult {
energies_ev: (0..100).map(|i| -5.0 + i as f64 * 0.1).collect(),
total_dos: vec![1.0; 100], kmesh: None,
band_edges: PeriodicBandEdgeSummary::default(),
diagnostics: PeriodicSpectralDiagnostics::default(),
};
let error = validate_electron_count(&dos, 0.0, 5.0);
assert!(
error < 0.15,
"electron count error should be small: {:.4}",
error
);
}
#[test]
fn cpu_benchmark_kmesh_operator_assembly() {
use nalgebra::DMatrix;
let h_real = DMatrix::<f64>::identity(4, 4);
let h_pairs: Vec<(&DMatrix<f64>, [i32; 3])> = vec![(&h_real, [0, 0, 0])];
let s_pairs: Vec<(&DMatrix<f64>, [i32; 3])> = vec![(&h_real, [0, 0, 0])];
for &n in &[2, 4, 8] {
let mesh = monkhorst_pack_mesh(&KMeshConfig {
grid: [n, n, n],
centering: KMeshCentering::MonkhorstPack,
})
.unwrap();
let start = std::time::Instant::now();
let (hk, sk) = assemble_periodic_operators(&h_pairs, &s_pairs, &mesh);
let elapsed = start.elapsed();
assert_eq!(hk.len(), mesh.points.len());
assert_eq!(sk.len(), mesh.points.len());
assert!(
elapsed.as_secs() < 5,
"operator assembly for {}x{}x{} mesh took {:?}",
n,
n,
n,
elapsed
);
}
}
}