Loading...
Searching...
No Matches
lowdin_orthogonalization.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Löwdin (symmetric) orthogonalization of a G-space orbital set.
5 *
6 * Given a set of (generally non-orthogonal) atomic pseudo-orbitals |φ_i⟩,
7 * builds the overlap matrix S_ij = ⟨φ_i|φ_j⟩ and returns the symmetrically
8 * orthogonalized basis |φ'_i⟩ = Σ_j |φ_j⟩ (S^{-1/2})_ji, where S^{-1/2} is
9 * formed from the eigendecomposition S = W·diag(e)·W^H. This is the unique
10 * orthogonalization that stays "closest" (in a least-squares sense) to the
11 * original basis. It is the convention Quantum ESPRESSO's projwfc.x names
12 * "ortho-atomic": the atomic projector orbitals are unconditionally
13 * Löwdin-orthogonalized before Kohn-Sham states are projected onto them, with
14 * no "plain atomic, unorthogonalized" alternative offered by that tool (the
15 * distinction exists only for the DFT+U Hubbard_projectors input variable).
16 * Operates on the orbital set for a single k-point, as built by
17 * atomic_orbital_builder.hpp.
18 */
19
20#include <cmath>
21#include <complex>
22#include <stdexcept>
23#include <string>
24#include <vector>
25
29
30namespace q3 {
31
32/**
33 * S^{-1/2} for one orbital set, together with the eigendecomposition it was
34 * built from.
35 *
36 * The eigenpairs are kept rather than discarded because the *derivative* of
37 * S^{-1/2} cannot be recovered from S^{-1/2} alone: dS and S^{-1/2} do not
38 * commute, so d(S^{-1/2}) is the solution of a Lyapunov equation that is
39 * diagonal only in S's own eigenbasis (see
40 * lowdin_inverse_square_root_derivative). DFT+U forces with ortho-atomic
41 * projectors need exactly that, and re-diagonalizing S per displaced atom and
42 * Cartesian direction to get it back would be both wasteful and a second
43 * definition of the same transform.
44 *
45 * Both matrices are column-major -- element (row, column) at
46 * `row + column * number_of_orbitals`. `eigenvectors` is ZHEEV's own output
47 * (column k is eigenvector k); `inverse_square_root` is Hermitian, so its
48 * layout only fixes which index is written first.
49 */
52 std::vector<Real> eigenvalues;
53 std::vector<Complex> eigenvectors;
54 std::vector<Complex> inverse_square_root;
55};
56
57/**
58 * Diagonalize the overlap matrix `overlap_column_major` (Hermitian, column-major
59 * number_of_orbitals x number_of_orbitals) and form S^{-1/2} from it.
60 *
61 * Takes the overlap by value: ZHEEV overwrites its input with the eigenvectors,
62 * which is exactly what the returned transform wants, and the caller keeps its
63 * own copy. Throws if the overlap is (near-)singular, i.e. the orbitals are not
64 * linearly independent -- not expected for atomic orbitals in practice, but a
65 * real possibility that must fail loudly rather than silently produce NaN/Inf.
66 */
67[[nodiscard]] inline LowdinTransform
68compute_lowdin_transform(std::vector<Complex> overlap_column_major, int number_of_orbitals) {
69 LowdinTransform transform;
70 transform.number_of_orbitals = number_of_orbitals;
71 if (number_of_orbitals == 0)
72 return transform;
73
74 // Diagonalize S = W * diag(eigenvalues) * W^H via ZHEEV.
75 transform.eigenvalues.assign(static_cast<size_t>(number_of_orbitals), 0.0);
76 int work_size = std::max(1, 2 * number_of_orbitals - 1);
77 std::vector<Complex> work(work_size);
78 std::vector<Real> rwork(static_cast<size_t>(std::max(1, 3 * number_of_orbitals - 2)));
79 int info = 0;
80 char jobz = 'V';
81 char uplo = 'U';
82 lapack::zheev_(&jobz,
83 &uplo,
84 &number_of_orbitals,
85 reinterpret_cast<std::complex<double>*>(overlap_column_major.data()),
86 &number_of_orbitals,
87 transform.eigenvalues.data(),
88 reinterpret_cast<std::complex<double>*>(work.data()),
89 &work_size,
90 rwork.data(),
91 &info);
92 if (info != 0) {
93 throw std::runtime_error("compute_lowdin_transform: ZHEEV failed, info=" +
94 std::to_string(info));
95 }
96
97 for (int k = 0; k < number_of_orbitals; k++) {
98 if (transform.eigenvalues[static_cast<size_t>(k)] <
100 throw std::runtime_error(
101 "compute_lowdin_transform: near-singular overlap matrix (eigenvalue " +
102 std::to_string(transform.eigenvalues[static_cast<size_t>(k)]) +
103 ") -- atomic orbitals are not linearly independent");
104 }
105 }
106 transform.eigenvectors = std::move(overlap_column_major);
107
108 // S^{-1/2}(a,b) = sum_k W(a,k) * (1/sqrt(e_k)) * conj(W(b,k)).
109 transform.inverse_square_root.assign(
110 static_cast<size_t>(number_of_orbitals) * number_of_orbitals, Complex(0.0, 0.0));
111 for (int k = 0; k < number_of_orbitals; k++) {
112 Real inverse_square_root_eigenvalue =
113 1.0 / std::sqrt(transform.eigenvalues[static_cast<size_t>(k)]);
114 for (int a = 0; a < number_of_orbitals; a++) {
115 Complex weighted_component =
116 transform.eigenvectors[static_cast<size_t>(a + k * number_of_orbitals)] *
117 inverse_square_root_eigenvalue;
118 for (int b = 0; b < number_of_orbitals; b++) {
119 transform.inverse_square_root[static_cast<size_t>(a + b * number_of_orbitals)] +=
120 weighted_component *
121 std::conj(
122 transform.eigenvectors[static_cast<size_t>(b + k * number_of_orbitals)]);
123 }
124 }
125 }
126 return transform;
127}
128
129/**
130 * Form the rank-local orbital overlap matrix in column-major layout.
131 *
132 * A caller whose plane-wave dimension is distributed must sum this matrix
133 * across the distribution group before constructing a Lowdin transform.
134 */
135[[nodiscard]] inline std::vector<Complex>
136compute_orbital_overlap_matrix(const std::vector<std::vector<Complex>>& orbitals) {
137 int number_of_orbitals = static_cast<int>(orbitals.size());
138 if (number_of_orbitals == 0)
139 return {};
140 int number_of_plane_waves = static_cast<int>(orbitals[0].size());
141
142 // Overlap S_ij = <phi_i|phi_j>, packed column-major for LAPACK.
143 std::vector<Complex> overlap_column_major(static_cast<size_t>(number_of_orbitals) *
144 number_of_orbitals);
145 for (int column = 0; column < number_of_orbitals; column++) {
146 for (int row = 0; row < number_of_orbitals; row++) {
147 Complex value(0.0, 0.0);
148 for (int g_index = 0; g_index < number_of_plane_waves; g_index++) {
149 value += std::conj(orbitals[row][g_index]) * orbitals[column][g_index];
150 }
151 overlap_column_major[static_cast<size_t>(row + column * number_of_orbitals)] = value;
152 }
153 }
154 return overlap_column_major;
155}
156
157/**
158 * Symmetrically orthogonalize `orbitals` using a complete overlap matrix.
159 *
160 * The explicit overlap form lets a distributed caller sum rank-local
161 * contractions before every rank constructs the same Lowdin transform.
162 */
163inline void lowdin_orthogonalize_orbitals(std::vector<std::vector<Complex>>& orbitals,
164 std::vector<Complex> overlap_column_major) {
165 int number_of_orbitals = static_cast<int>(orbitals.size());
166 if (number_of_orbitals == 0)
167 return;
168 int number_of_plane_waves = static_cast<int>(orbitals[0].size());
169 size_t expected_overlap_size = static_cast<size_t>(number_of_orbitals) * number_of_orbitals;
170 if (overlap_column_major.size() != expected_overlap_size) {
171 throw std::runtime_error("lowdin_orthogonalize_orbitals: overlap matrix has " +
172 std::to_string(overlap_column_major.size()) +
173 " elements, expected " + std::to_string(expected_overlap_size));
174 }
175
176 auto transform = compute_lowdin_transform(std::move(overlap_column_major), number_of_orbitals);
177
178 // new_orbitals[i] = sum_j orbitals[j] * (S^{-1/2})(j,i).
179 std::vector<std::vector<Complex>> orthogonalized_orbitals(
180 number_of_orbitals, std::vector<Complex>(number_of_plane_waves, Complex(0.0, 0.0)));
181 for (int i = 0; i < number_of_orbitals; i++) {
182 for (int j = 0; j < number_of_orbitals; j++) {
183 Complex weight =
184 transform.inverse_square_root[static_cast<size_t>(j + i * number_of_orbitals)];
185 for (int g_index = 0; g_index < number_of_plane_waves; g_index++) {
186 orthogonalized_orbitals[i][g_index] += weight * orbitals[j][g_index];
187 }
188 }
189 }
190
191 orbitals = std::move(orthogonalized_orbitals);
192}
193
194/**
195 * Symmetrically orthogonalize a complete, locally stored orbital set.
196 */
197inline void lowdin_orthogonalize_orbitals(std::vector<std::vector<Complex>>& orbitals) {
199}
200
201/**
202 * d(S^{-1/2}) given dS, in the same column-major layout.
203 *
204 * S^{1/2} S^{1/2} = S differentiates into a Sylvester equation, which is
205 * diagonal in S's eigenbasis; inverting it there and mapping back gives the
206 * closed form
207 *
208 * d(S^{-1/2}) = -W * [ (W^H dS W)_ij / (e_i sqrt(e_j) + e_j sqrt(e_i)) ] * W^H
209 *
210 * -- Eq. (32) of Phys. Rev. B 102, 235159 (2020), the same closed form
211 * Quantum ESPRESSO evaluates for ortho-atomic DFT+U forces. The denominator
212 * is `sqrt(e_i) sqrt(e_j) (sqrt(e_i) + sqrt(e_j))` regrouped: the outer factors
213 * come from d(S^{-1/2}) = -S^{-1/2} d(S^{1/2}) S^{-1/2} and the bracket from
214 * d(S^{1/2}) itself.
215 *
216 * dS Hermitian in, d(S^{-1/2}) Hermitian out: the eigenbasis transform and the
217 * real symmetric denominator both preserve it, which is what lets a caller read
218 * the result with either index order.
219 */
220[[nodiscard]] inline std::vector<Complex>
222 const std::vector<Complex>& overlap_derivative) {
223 int number_of_orbitals = transform.number_of_orbitals;
224 size_t element_count = static_cast<size_t>(number_of_orbitals) * number_of_orbitals;
225 if (overlap_derivative.size() != element_count) {
226 throw std::runtime_error("lowdin_inverse_square_root_derivative: overlap derivative has " +
227 std::to_string(overlap_derivative.size()) +
228 " elements, expected " + std::to_string(element_count));
229 }
230 std::vector<Complex> result(element_count, Complex(0.0, 0.0));
231 if (number_of_orbitals == 0)
232 return result;
233
234 const auto& eigenvectors = transform.eigenvectors;
235 const auto& eigenvalues = transform.eigenvalues;
236 auto index = [number_of_orbitals](int row, int column) {
237 return static_cast<size_t>(row + column * number_of_orbitals);
238 };
239
240 // dS * W, then W^H * (dS * W): the derivative in S's own eigenbasis.
241 std::vector<Complex> right_product(element_count, Complex(0.0, 0.0));
242 for (int column = 0; column < number_of_orbitals; column++)
243 for (int inner = 0; inner < number_of_orbitals; inner++) {
244 Complex eigenvector_component = eigenvectors[index(inner, column)];
245 if (eigenvector_component == Complex(0.0, 0.0))
246 continue;
247 for (int row = 0; row < number_of_orbitals; row++)
248 right_product[index(row, column)] +=
249 overlap_derivative[index(row, inner)] * eigenvector_component;
250 }
251 std::vector<Complex> eigenbasis_derivative(element_count, Complex(0.0, 0.0));
252 for (int column = 0; column < number_of_orbitals; column++)
253 for (int row = 0; row < number_of_orbitals; row++) {
254 Complex accumulated(0.0, 0.0);
255 for (int inner = 0; inner < number_of_orbitals; inner++)
256 accumulated += std::conj(eigenvectors[index(inner, row)]) *
257 right_product[index(inner, column)];
258 eigenbasis_derivative[index(row, column)] = accumulated;
259 }
260
261 // Divide by the Sylvester denominator, then map back: -W * (...) * W^H.
262 std::vector<Real> square_root_eigenvalues(static_cast<size_t>(number_of_orbitals));
263 for (int k = 0; k < number_of_orbitals; k++)
264 square_root_eigenvalues[static_cast<size_t>(k)] =
265 std::sqrt(eigenvalues[static_cast<size_t>(k)]);
266 for (int column = 0; column < number_of_orbitals; column++)
267 for (int row = 0; row < number_of_orbitals; row++)
268 eigenbasis_derivative[index(row, column)] /=
269 eigenvalues[static_cast<size_t>(row)] *
270 square_root_eigenvalues[static_cast<size_t>(column)] +
271 eigenvalues[static_cast<size_t>(column)] *
272 square_root_eigenvalues[static_cast<size_t>(row)];
273
274 std::vector<Complex> left_product(element_count, Complex(0.0, 0.0));
275 for (int column = 0; column < number_of_orbitals; column++)
276 for (int inner = 0; inner < number_of_orbitals; inner++) {
277 Complex eigenvector_component = std::conj(eigenvectors[index(column, inner)]);
278 if (eigenvector_component == Complex(0.0, 0.0))
279 continue;
280 for (int row = 0; row < number_of_orbitals; row++)
281 left_product[index(row, column)] +=
282 eigenbasis_derivative[index(row, inner)] * eigenvector_component;
283 }
284 for (int column = 0; column < number_of_orbitals; column++)
285 for (int row = 0; row < number_of_orbitals; row++) {
286 Complex accumulated(0.0, 0.0);
287 for (int inner = 0; inner < number_of_orbitals; inner++)
288 accumulated += eigenvectors[index(row, inner)] * left_product[index(inner, column)];
289 result[index(row, column)] = -accumulated;
290 }
291 return result;
292}
293
294} // namespace q3
void zheev_(const char *jobz, const char *uplo, const int *n, std::complex< double > *a, const int *lda, double *w, std::complex< double > *work, const int *lwork, double *rwork, int *info)
constexpr double lowdin_minimum_overlap_eigenvalue
std::vector< Complex > compute_orbital_overlap_matrix(const std::vector< std::vector< Complex > > &orbitals)
double Real
void lowdin_orthogonalize_orbitals(std::vector< std::vector< Complex > > &orbitals, std::vector< Complex > overlap_column_major)
std::vector< Complex > lowdin_inverse_square_root_derivative(const LowdinTransform &transform, const std::vector< Complex > &overlap_derivative)
std::complex< double > Complex
LowdinTransform compute_lowdin_transform(std::vector< Complex > overlap_column_major, int number_of_orbitals)
std::vector< Complex > inverse_square_root
std::vector< Real > eigenvalues
std::vector< Complex > eigenvectors