1use nalgebra::DMatrix;
8
9pub fn build_density_matrix(coefficients: &DMatrix<f64>, n_occupied: usize) -> DMatrix<f64> {
13 let n = coefficients.nrows();
14 let mut p = DMatrix::zeros(n, n);
15
16 for i in 0..n {
17 for j in 0..=i {
18 let mut p_ij = 0.0;
19 for k in 0..n_occupied {
20 p_ij += coefficients[(i, k)] * coefficients[(j, k)];
21 }
22 p_ij *= 2.0;
23 p[(i, j)] = p_ij;
24 p[(j, i)] = p_ij;
25 }
26 }
27
28 p
29}
30
31pub fn build_density_matrix_fon(
38 coefficients: &DMatrix<f64>,
39 orbital_energies: &[f64],
40 n_electrons: usize,
41 temperature_au: f64,
42) -> DMatrix<f64> {
43 let n = coefficients.nrows();
44 let n_orb = orbital_energies.len();
45 let _n_occ = n_electrons / 2;
46
47 let mu = find_fermi_level(orbital_energies, n_electrons, temperature_au);
49
50 let occupations: Vec<f64> = orbital_energies
52 .iter()
53 .map(|&e| 2.0 * fermi_dirac(e, mu, temperature_au))
54 .collect();
55
56 let mut p = DMatrix::zeros(n, n);
57 for i in 0..n {
58 for j in 0..=i {
59 let mut p_ij = 0.0;
60 for k in 0..n_orb.min(n) {
61 p_ij += occupations[k] * coefficients[(i, k)] * coefficients[(j, k)];
62 }
63 p[(i, j)] = p_ij;
64 p[(j, i)] = p_ij;
65 }
66 }
67 p
68}
69
70fn fermi_dirac(energy: f64, mu: f64, temperature: f64) -> f64 {
71 if temperature < 1e-15 {
72 return if energy < mu {
73 1.0
74 } else if energy > mu {
75 0.0
76 } else {
77 0.5
78 };
79 }
80 let x = (energy - mu) / temperature;
81 if x > 50.0 {
82 0.0
83 } else if x < -50.0 {
84 1.0
85 } else {
86 1.0 / (1.0 + x.exp())
87 }
88}
89
90fn find_fermi_level(orbital_energies: &[f64], n_electrons: usize, temperature: f64) -> f64 {
91 let target = n_electrons as f64;
92 let mut mu_lo = orbital_energies
93 .iter()
94 .cloned()
95 .fold(f64::INFINITY, f64::min)
96 - 1.0;
97 let mut mu_hi = orbital_energies
98 .iter()
99 .cloned()
100 .fold(f64::NEG_INFINITY, f64::max)
101 + 1.0;
102
103 for _ in 0..100 {
104 let mu = 0.5 * (mu_lo + mu_hi);
105 let n: f64 = orbital_energies
106 .iter()
107 .map(|&e| 2.0 * fermi_dirac(e, mu, temperature))
108 .sum();
109 if n < target {
110 mu_lo = mu;
111 } else {
112 mu_hi = mu;
113 }
114 if (mu_hi - mu_lo).abs() < 1e-12 {
115 break;
116 }
117 }
118 0.5 * (mu_lo + mu_hi)
119}
120
121pub fn density_rms_change(p_new: &DMatrix<f64>, p_old: &DMatrix<f64>) -> f64 {
123 let n = p_new.nrows();
124 let diff = p_new - p_old;
125 let mut sum_sq = 0.0;
126 for i in 0..n {
127 for j in 0..n {
128 sum_sq += diff[(i, j)] * diff[(i, j)];
129 }
130 }
131 (sum_sq / (n * n) as f64).sqrt()
132}
133
134pub fn electron_count(p: &DMatrix<f64>, s: &DMatrix<f64>) -> f64 {
138 let ps = p * s;
139 let mut trace = 0.0;
140 for i in 0..ps.nrows() {
141 trace += ps[(i, i)];
142 }
143 trace
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn test_density_matrix_symmetric() {
152 let c = DMatrix::from_row_slice(3, 3, &[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0]);
153 let p = build_density_matrix(&c, 1);
154
155 for i in 0..3 {
156 for j in 0..3 {
157 assert!((p[(i, j)] - p[(j, i)]).abs() < 1e-14);
158 }
159 }
160 }
161
162 #[test]
163 fn test_density_trace() {
164 let c = DMatrix::identity(3, 3);
165 let p = build_density_matrix(&c, 2);
166 let s = DMatrix::identity(3, 3);
167 let n_e = electron_count(&p, &s);
168 assert!((n_e - 4.0).abs() < 1e-10);
169 }
170
171 #[test]
172 fn test_density_change() {
173 let p1 = DMatrix::identity(2, 2);
174 let mut p2 = DMatrix::identity(2, 2);
175 p2[(0, 0)] = 1.1;
176 let rms = density_rms_change(&p1, &p2);
177 assert!(rms > 0.0);
178 }
179
180 #[test]
181 fn test_fon_density_conserves_electrons() {
182 let c = DMatrix::identity(4, 4);
183 let energies = [-1.0, -0.5, 0.5, 1.0];
184 let n_electrons = 4; let temp_au = 0.001; let p = build_density_matrix_fon(&c, &energies, n_electrons, temp_au);
187 let s = DMatrix::identity(4, 4);
188 let n_e = electron_count(&p, &s);
189 assert!(
190 (n_e - 4.0).abs() < 0.1,
191 "FON should conserve ~4 electrons, got {n_e}"
192 );
193 }
194
195 #[test]
196 fn test_fon_at_zero_temp_matches_integer() {
197 let c = DMatrix::identity(3, 3);
198 let energies = [-1.0, -0.5, 0.5];
199 let p_fon = build_density_matrix_fon(&c, &energies, 2, 1e-20);
200 let p_int = build_density_matrix(&c, 1);
201 for i in 0..3 {
203 for j in 0..3 {
204 assert!(
205 (p_fon[(i, j)] - p_int[(i, j)]).abs() < 1e-6,
206 "FON(T→0) should match integer density at ({i},{j})"
207 );
208 }
209 }
210 }
211
212 #[test]
213 fn test_fon_high_temp_smears_occupation() {
214 let c = DMatrix::identity(3, 3);
215 let energies = [-0.1, 0.0, 0.1]; let p = build_density_matrix_fon(&c, &energies, 2, 1.0);
217 for i in 0..3 {
219 assert!(p[(i, i)] > 0.01, "at high T, all orbitals get partial occ");
220 assert!(p[(i, i)] < 1.99, "at high T, no orbital is fully occupied");
221 }
222 }
223}