use integral_core::os::{self, Prim, MAX_L};
use latx::Cell;
use rustfft::num_complex::Complex64;
use std::f64::consts::PI;
use crate::grad::pair_grad_1e;
use crate::integrals::{contract_pair, place_block, to_func_1e};
use crate::shell::{Basis, Shell};
fn shifted_shell(s: &Shell, shift: [f64; 3]) -> Shell {
let c = s.center();
Shell::with_kind(
s.l(),
[c[0] + shift[0], c[1] + shift[1], c[2] + shift[2]],
s.exponents().to_vec(),
s.coefficients().to_vec(),
s.kind(),
)
.expect("translation preserves shell validity")
}
fn cross_matrix<F>(basis: &Basis, shift: [f64; 3], prim_op: F) -> Vec<f64>
where
F: FnMut(Prim, Prim, f64, &mut [f64]) + Copy,
{
let n = basis.nao();
let offs = basis.offsets();
let mut mat = vec![0.0; n * n];
for (si, sa) in basis.shells().iter().enumerate() {
for (sj, sb) in basis.shells().iter().enumerate() {
let sb_shifted = shifted_shell(sb, shift);
let block = to_func_1e(contract_pair(sa, &sb_shifted, prim_op), sa, &sb_shifted);
place_block(&mut mat, n, offs[si], offs[sj], &block, sb.n_func());
}
}
mat
}
fn bloch_matrix<F>(
basis: &Basis,
cell: &Cell,
k_frac: [f64; 3],
rmax: f64,
prim_op: F,
) -> Vec<Complex64>
where
F: FnMut(Prim, Prim, f64, &mut [f64]) + Copy,
{
let n = basis.nao();
let mut m = vec![Complex64::new(0.0, 0.0); n * n];
for (triple, r) in cell.lattice_images(rmax) {
let block = cross_matrix(basis, r, prim_op);
let theta = 2.0
* PI
* (k_frac[0] * f64::from(triple[0])
+ k_frac[1] * f64::from(triple[1])
+ k_frac[2] * f64::from(triple[2]));
let phase = Complex64::new(theta.cos(), theta.sin());
for (mij, &bij) in m.iter_mut().zip(&block) {
*mij += phase * bij;
}
}
m
}
#[must_use]
pub fn bloch_overlap(basis: &Basis, cell: &Cell, k_frac: [f64; 3], rmax: f64) -> Vec<Complex64> {
bloch_matrix(basis, cell, k_frac, rmax, os::overlap_into)
}
#[must_use]
pub fn bloch_kinetic(basis: &Basis, cell: &Cell, k_frac: [f64; 3], rmax: f64) -> Vec<Complex64> {
bloch_matrix(basis, cell, k_frac, rmax, os::kinetic_into)
}
fn bloch_grad_1e_contract<F>(
basis: &Basis,
cell: &Cell,
k_fracs: &[[f64; 3]],
weights: &[f64],
m_k: &[Vec<Complex64>],
rmax: f64,
op: F,
) -> Vec<[f64; 3]>
where
F: Fn(Prim, Prim, f64, &mut [f64]) + Copy,
{
let nk = k_fracs.len();
assert_eq!(nk, weights.len(), "k-points and weights must align");
assert_eq!(nk, m_k.len(), "k-points and weight matrices must align");
let n = basis.nao();
assert!(
m_k.iter().all(|m| m.len() == n * n),
"each weight matrix must be nao² = {}",
n * n
);
let shells = basis.shells();
assert!(
shells.iter().all(|s| s.l() < MAX_L),
"Bloch 1e gradient needs l < MAX_L (the derivative raises a shell to l+1)"
);
let offs = basis.offsets();
let satom = basis.shell_atom();
let natom = basis.atoms().len();
let mut g = vec![[0.0_f64; 3]; natom];
let mut mr = vec![Complex64::new(0.0, 0.0); n * n]; for (triple, r) in cell.lattice_images(rmax) {
for v in &mut mr {
*v = Complex64::new(0.0, 0.0);
}
for ik in 0..nk {
let theta = 2.0
* PI
* (k_fracs[ik][0] * f64::from(triple[0])
+ k_fracs[ik][1] * f64::from(triple[1])
+ k_fracs[ik][2] * f64::from(triple[2]));
let phase = Complex64::new(theta.cos(), theta.sin()) * weights[ik];
for (mrv, &mkv) in mr.iter_mut().zip(&m_k[ik]) {
*mrv += phase * mkv;
}
}
for (si, sa) in shells.iter().enumerate() {
let nba = sa.n_func();
let (atom_a, ri) = (satom[si], offs[si]);
for (sj, sb) in shells.iter().enumerate() {
let sb_shifted = shifted_shell(sb, r);
let (da, db) = pair_grad_1e(sa, &sb_shifted, op);
let nbf = sb.n_func();
let (atom_b, ci) = (satom[sj], offs[sj]);
for axis in 0..3 {
let fa = to_func_1e(da[axis].clone(), sa, &sb_shifted);
let fb = to_func_1e(db[axis].clone(), sa, &sb_shifted);
let (mut ca, mut cb) = (0.0, 0.0);
for i in 0..nba {
for j in 0..nbf {
let mre = mr[(ci + j) * n + (ri + i)].re;
ca += fa[i * nbf + j] * mre;
cb += fb[i * nbf + j] * mre;
}
}
g[atom_a][axis] += ca;
g[atom_b][axis] += cb;
}
}
}
}
g
}
#[must_use]
pub fn bloch_kinetic_grad_contract(
basis: &Basis,
cell: &Cell,
k_fracs: &[[f64; 3]],
weights: &[f64],
p_k: &[Vec<Complex64>],
rmax: f64,
) -> Vec<[f64; 3]> {
bloch_grad_1e_contract(basis, cell, k_fracs, weights, p_k, rmax, os::kinetic_into)
}
#[must_use]
pub fn bloch_overlap_grad_contract(
basis: &Basis,
cell: &Cell,
k_fracs: &[[f64; 3]],
weights: &[f64],
w_k: &[Vec<Complex64>],
rmax: f64,
) -> Vec<[f64; 3]> {
bloch_grad_1e_contract(basis, cell, k_fracs, weights, w_k, rmax, os::overlap_into)
}
fn bloch_grad_1e_stress_contract<F>(
basis: &Basis,
cell: &Cell,
k_fracs: &[[f64; 3]],
weights: &[f64],
m_k: &[Vec<Complex64>],
rmax: f64,
op: F,
) -> [[f64; 3]; 3]
where
F: Fn(Prim, Prim, f64, &mut [f64]) + Copy,
{
let nk = k_fracs.len();
assert_eq!(nk, weights.len(), "k-points and weights must align");
assert_eq!(nk, m_k.len(), "k-points and weight matrices must align");
let n = basis.nao();
assert!(
m_k.iter().all(|m| m.len() == n * n),
"each weight matrix must be nao² = {}",
n * n
);
let shells = basis.shells();
assert!(
shells.iter().all(|s| s.l() < MAX_L),
"Bloch 1e stress needs l < MAX_L (the derivative raises a shell to l+1)"
);
let offs = basis.offsets();
let mut tau = [[0.0_f64; 3]; 3];
let mut mr = vec![Complex64::new(0.0, 0.0); n * n];
for (triple, r) in cell.lattice_images(rmax) {
for v in &mut mr {
*v = Complex64::new(0.0, 0.0);
}
for ik in 0..nk {
let theta = 2.0
* PI
* (k_fracs[ik][0] * f64::from(triple[0])
+ k_fracs[ik][1] * f64::from(triple[1])
+ k_fracs[ik][2] * f64::from(triple[2]));
let phase = Complex64::new(theta.cos(), theta.sin()) * weights[ik];
for (mrv, &mkv) in mr.iter_mut().zip(&m_k[ik]) {
*mrv += phase * mkv;
}
}
for (si, sa) in shells.iter().enumerate() {
let nba = sa.n_func();
let (ri, ac) = (offs[si], sa.center());
for (sj, sb) in shells.iter().enumerate() {
let sb_shifted = shifted_shell(sb, r);
let nbf = sb.n_func();
let ci = offs[sj];
let bc = sb.center();
let sep = [
bc[0] + r[0] - ac[0],
bc[1] + r[1] - ac[1],
bc[2] + r[2] - ac[2],
];
let (_da, db) = pair_grad_1e(sa, &sb_shifted, op);
for (alpha, ta) in tau.iter_mut().enumerate() {
let fb = to_func_1e(db[alpha].clone(), sa, &sb_shifted);
let mut ca = 0.0;
for i in 0..nba {
for j in 0..nbf {
ca += fb[i * nbf + j] * mr[(ci + j) * n + (ri + i)].re;
}
}
for (beta, tab) in ta.iter_mut().enumerate() {
*tab += ca * sep[beta];
}
}
}
}
}
tau
}
#[must_use]
pub fn bloch_kinetic_stress_contract(
basis: &Basis,
cell: &Cell,
k_fracs: &[[f64; 3]],
weights: &[f64],
p_k: &[Vec<Complex64>],
rmax: f64,
) -> [[f64; 3]; 3] {
bloch_grad_1e_stress_contract(basis, cell, k_fracs, weights, p_k, rmax, os::kinetic_into)
}
#[must_use]
pub fn bloch_overlap_stress_contract(
basis: &Basis,
cell: &Cell,
k_fracs: &[[f64; 3]],
weights: &[f64],
w_k: &[Vec<Complex64>],
rmax: f64,
) -> [[f64; 3]; 3] {
bloch_grad_1e_stress_contract(basis, cell, k_fracs, weights, w_k, rmax, os::overlap_into)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Basis, Shell};
fn two_s_basis() -> Basis {
Basis::new(vec![
Shell::new(0, [1.0, 1.0, 1.0], vec![0.8], vec![1.0]).unwrap(),
Shell::new(0, [2.0, 1.0, 1.0], vec![1.2], vec![1.0]).unwrap(),
])
}
#[test]
fn gamma_reduces_to_molecular_overlap() {
let basis = two_s_basis();
let cell = Cell::cubic(8.0).unwrap();
let s_mol = basis.overlap();
let s_k = bloch_overlap(&basis, &cell, [0.0, 0.0, 0.0], 0.1); for (k, &m) in s_k.iter().zip(&s_mol) {
assert!((k.re - m).abs() < 1e-13 && k.im.abs() < 1e-13, "{k} vs {m}");
}
}
#[test]
fn gamma_reduces_to_molecular_kinetic() {
let basis = two_s_basis();
let cell = Cell::cubic(8.0).unwrap();
let t_mol = basis.kinetic();
let t_k = bloch_kinetic(&basis, &cell, [0.0, 0.0, 0.0], 0.1);
for (k, &m) in t_k.iter().zip(&t_mol) {
assert!((k.re - m).abs() < 1e-13 && k.im.abs() < 1e-13);
}
}
#[test]
fn bloch_overlap_is_hermitian() {
let basis = two_s_basis();
let cell = Cell::cubic(3.0).unwrap();
let n = basis.nao();
let s = bloch_overlap(&basis, &cell, [0.3, -0.1, 0.2], 12.0);
for i in 0..n {
for j in 0..n {
let a = s[i * n + j];
let b = s[j * n + i];
assert!(
(a - b.conj()).norm() < 1e-12,
"S not Hermitian at ({i},{j})"
);
}
}
}
fn s_p_basis(c0: [f64; 3], c1: [f64; 3]) -> Basis {
Basis::new(vec![
Shell::new(0, c0, vec![0.7], vec![1.0]).unwrap(),
Shell::new_spherical(1, c1, vec![0.5], vec![1.0]).unwrap(),
])
}
fn weighted_trace<F>(
basis: &Basis,
cell: &Cell,
kfracs: &[[f64; 3]],
weights: &[f64],
m: &[f64],
rmax: f64,
op: F,
) -> f64
where
F: Fn(&Basis, &Cell, [f64; 3], f64) -> Vec<Complex64>,
{
let n = basis.nao();
let mut e = 0.0;
for (k, &w) in kfracs.iter().zip(weights) {
let ok = op(basis, cell, *k, rmax);
let mut tr = 0.0;
for mu in 0..n {
for nu in 0..n {
tr += (ok[mu * n + nu] * m[nu * n + mu]).re;
}
}
e += w * tr;
}
e
}
#[test]
fn bloch_pulay_gradients_match_finite_difference() {
let cell = Cell::cubic(5.0).unwrap();
let c0 = [1.1, 1.0, 0.9];
let c1 = [2.6, 2.4, 2.7];
let basis = s_p_basis(c0, c1);
let n = basis.nao();
let rmax = 12.0;
let kfracs = [[0.0, 0.0, 0.0], [0.3, -0.1, 0.2]];
let weights = [0.55, 0.45];
let mut m = vec![0.0; n * n];
for a in 0..n {
for b in 0..n {
m[a * n + b] =
0.2 * ((a + 1) as f64) + 0.1 * ((b + 1) as f64) + 0.05 * (a * b) as f64;
}
}
for a in 0..n {
for b in (a + 1)..n {
let s = 0.5 * (m[a * n + b] + m[b * n + a]);
m[a * n + b] = s;
m[b * n + a] = s;
}
}
let m_k: Vec<Vec<Complex64>> = (0..kfracs.len())
.map(|_| m.iter().map(|&x| Complex64::new(x, 0.0)).collect())
.collect();
type BlochOp = fn(&Basis, &Cell, [f64; 3], f64) -> Vec<Complex64>;
let cases: [(&str, Vec<[f64; 3]>, BlochOp); 2] = [
(
"overlap",
bloch_overlap_grad_contract(&basis, &cell, &kfracs, &weights, &m_k, rmax),
bloch_overlap as BlochOp,
),
(
"kinetic",
bloch_kinetic_grad_contract(&basis, &cell, &kfracs, &weights, &m_k, rmax),
bloch_kinetic as BlochOp,
),
];
for (op_name, grad, op) in cases {
let h = 1e-5;
let centers = [c0, c1];
for atom in 0..2 {
for axis in 0..3 {
let mut cp = centers;
cp[atom][axis] += h;
let e_plus = weighted_trace(
&s_p_basis(cp[0], cp[1]),
&cell,
&kfracs,
&weights,
&m,
rmax,
op,
);
cp[atom][axis] -= 2.0 * h;
let e_minus = weighted_trace(
&s_p_basis(cp[0], cp[1]),
&cell,
&kfracs,
&weights,
&m,
rmax,
op,
);
let fd = (e_plus - e_minus) / (2.0 * h);
assert!(
(grad[atom][axis] - fd).abs() < 1e-6,
"{op_name} atom {atom} axis {axis}: analytic {} vs FD {fd}",
grad[atom][axis]
);
}
}
for axis in 0..3 {
let s: f64 = grad.iter().map(|g| g[axis]).sum();
assert!(s.abs() < 1e-9, "{op_name} Σ_I g_I[{axis}] = {s} ≠ 0");
}
}
}
fn deform_vec(m: &[[f64; 3]; 3], lambda: f64, v: [f64; 3]) -> [f64; 3] {
let mut o = v;
for (a, oa) in o.iter_mut().enumerate() {
for (b, &vb) in v.iter().enumerate() {
*oa += lambda * m[a][b] * vb;
}
}
o
}
#[test]
fn bloch_pulay_stress_match_finite_difference() {
let cell = Cell::cubic(5.0).unwrap();
let c0 = [1.1, 1.0, 0.9];
let c1 = [2.6, 2.4, 2.7];
let basis = s_p_basis(c0, c1);
let n = basis.nao();
let rmax = 12.0;
let kfracs = [[0.0, 0.0, 0.0], [0.3, -0.1, 0.2]];
let weights = [0.55, 0.45];
let mut m = vec![0.0; n * n];
for a in 0..n {
for b in 0..n {
m[a * n + b] =
0.2 * ((a + 1) as f64) + 0.1 * ((b + 1) as f64) + 0.05 * (a * b) as f64;
}
}
for a in 0..n {
for b in (a + 1)..n {
let s = 0.5 * (m[a * n + b] + m[b * n + a]);
m[a * n + b] = s;
m[b * n + a] = s;
}
}
let m_k: Vec<Vec<Complex64>> = (0..kfracs.len())
.map(|_| m.iter().map(|&x| Complex64::new(x, 0.0)).collect())
.collect();
let dirs = [
[[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0]],
[[0.4, 0.2, -0.1], [0.2, -0.3, 0.15], [-0.1, 0.15, 0.5]],
];
type StressFn =
fn(&Basis, &Cell, &[[f64; 3]], &[f64], &[Vec<Complex64>], f64) -> [[f64; 3]; 3];
type BlochOp = fn(&Basis, &Cell, [f64; 3], f64) -> Vec<Complex64>;
let cases: [(&str, StressFn, BlochOp); 2] = [
("kinetic", bloch_kinetic_stress_contract, bloch_kinetic),
("overlap", bloch_overlap_stress_contract, bloch_overlap),
];
for (name, stress_fn, op) in cases {
let tau = stress_fn(&basis, &cell, &kfracs, &weights, &m_k, rmax);
for mdir in dirs {
let energy = |lambda: f64| -> f64 {
let dcell = Cell::from_vectors(
deform_vec(&mdir, lambda, cell.vectors().0),
deform_vec(&mdir, lambda, cell.vectors().1),
deform_vec(&mdir, lambda, cell.vectors().2),
)
.unwrap();
let db =
s_p_basis(deform_vec(&mdir, lambda, c0), deform_vec(&mdir, lambda, c1));
weighted_trace(&db, &dcell, &kfracs, &weights, &m, rmax, op)
};
let h = 1e-5;
let fd = (energy(h) - energy(-h)) / (2.0 * h);
let analytic: f64 = (0..3)
.flat_map(|a| (0..3).map(move |b| (a, b)))
.map(|(a, b)| tau[a][b] * mdir[a][b])
.sum();
assert!(
(analytic - fd).abs() < 1e-6,
"{name} strain {mdir:?}: analytic {analytic} vs FD {fd}"
);
}
}
}
#[test]
fn bloch_overlap_periodic_in_k() {
let basis = two_s_basis();
let cell = Cell::cubic(4.0).unwrap();
let s1 = bloch_overlap(&basis, &cell, [0.25, 0.0, 0.0], 10.0);
let s2 = bloch_overlap(&basis, &cell, [1.25, 0.0, 0.0], 10.0);
for (a, b) in s1.iter().zip(&s2) {
assert!((a - b).norm() < 1e-12);
}
}
}