Loading...
Searching...
No Matches
symmetrization.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Symmetrizing the DFT+U on-site occupation matrix.
5 *
6 * The occupation matrix is accumulated as a sum over the *irreducible* wedge:
7 *
8 * n_mm' = Σ_{k∈IBZ} w_k Σ_v f_kv ⟨φ_m|ψ_kv⟩⟨ψ_kv|φ_m'⟩
9 *
10 * Weighted IBZ sums reproduce full-zone sums only for quantities that are
11 * invariant under the point group. `n` is not: it carries the manifold's
12 * angular indices, and each operation rotates them. Summing the wedge and
13 * stopping leaves a matrix that is *not* symmetry-adapted, which is wrong even
14 * though its trace is right — the trace is the one invariant the omission
15 * preserves.
16 *
17 * The restoration is the group average
18 *
19 * n_a ← (1/N_R) Σ_R D(R)ᵀ · n_{R(a)} · D(R)
20 *
21 * with D(R) the rotation matrix of the real spherical harmonics of the
22 * manifold's l, and R(a) = `atom_map[a]` the atom that a maps *to*. Read the
23 * other way round, it is the statement the crystal makes: n_{R(a)} = D(R) n_a
24 * D(R)ᵀ, so an already-symmetric n is a fixed point of this and of nothing
25 * else — the same pairing, and the same argument for it, that
26 * `SymmetryAnalyzer::symmetrize_vector` sets out for forces. Averaging the
27 * on-site occupation matrix over the point group is the standard restoration in
28 * DFT+U implementations, Quantum ESPRESSO's among them. The loop nesting and
29 * the placement of the 1/N remain free choices the formula does not fix, and no
30 * claim is made here about how any other implementation resolves them.
31 *
32 * **How this showed up.** Silicon's 3p manifold sits on a tetrahedral site, so
33 * symmetry requires n = x·1 and therefore Tr[n²] = (Tr n)²/3. QE's numbers
34 * satisfy that identity exactly; Q3's exceeded it by 0.154, and since
35 * E_U = (U/2)(Tr n − Tr n²) that surplus fed straight into the energy — Q3 gave
36 * E_U = 0.1204 Ry against QE's 0.1918 while Tr[n] agreed to 0.5%. Equal traces
37 * and unequal energies is the signature of exactly this: the *structure* of the
38 * matrix is wrong while its filling is right.
39 *
40 * Running the same case over the full zone (`disable_symmetry: true`), where no
41 * symmetrization is needed because the k-sum performs it, gives 0.191758062
42 * against QE's 0.19175802 — eight significant figures, and the confirmation
43 * that nothing else was involved.
44 *
45 * D(R) is built numerically rather than from closed-form Wigner matrices,
46 * using the defining relation and an angular quadrature that is exact for the
47 * degree involved. That keeps this correct for any l the manifold can carry
48 * without a table of hard-coded rotation formulae, and it reuses the same
49 * grid and harmonics the PAW one-center engine already validates.
50 * `build_real_harmonic_rotation_matrix` and `cartesian_rotation_of` live in
51 * `layer_4_computation/paw/one_center/harmonic_rotation.hpp`, shared with the
52 * PAW on-site occupation symmetrizer, which averages a different array over
53 * the same group.
54 */
55
56#include <array>
57#include <cmath>
58#include <vector>
59
64
65namespace q3 {
66
67/**
68 * The other half of the group the wedge was reduced with: time reversal.
69 *
70 * `reduce_kpoints_internal` folds −Rk into Rk as well as Rk into k, so the
71 * irreducible wedge is the quotient by the *magnetic* group {R} × {1, Θ}, not
72 * by {R} alone. Averaging over the spatial operations therefore restores only
73 * part of what the reduction removed.
74 *
75 * For a collinear Hamiltonian (real potential, real symmetric D matrices — Q3
76 * has no spin-orbit or noncollinear path) Θ acts by complex conjugation:
77 * ψ_{−k} = ψ_k*, so with real atomic orbitals ⟨φ_m|ψ_{−k}⟩ = ⟨φ_m|ψ_k⟩* and
78 *
79 * n^{(−k)} = (n^{(k)})* = (n^{(k)})ᵀ (n is Hermitian)
80 *
81 * A full-zone sum contains both members of every ±k pair and its imaginary
82 * part cancels exactly; a wedge sum keeps only one and leaves the whole
83 * antisymmetric imaginary part behind. The spatial average cannot remove it —
84 * D(R) is real, so D·(iB)·Dᵀ stays antisymmetric-imaginary — which is why this
85 * has to be a separate step rather than another operation in the loop above.
86 *
87 * The average over {1, Θ} is (n + nᵀ)/2 = Re n. Quantum ESPRESSO reaches the
88 * same place from the other direction, accumulating only the real part of the
89 * projector outer product into a real occupation array for the collinear case
90 * (its complex branch is the noncollinear one, where Θ is not a symmetry of a
91 * single spin channel).
92 *
93 * Unconditional, and safe to be: a full-zone n is already real to 1e-11, and
94 * every Monkhorst-Pack mesh is closed under k → −k, so the projection is a
95 * no-op exactly when it is not needed.
96 *
97 * **What it was worth.** Zincblende SiC, PAW, U = 3 eV on silicon with carbon
98 * displaced off its site: the wedge left max|Im n| = 6.8e-3 against an
99 * off-diagonal real part of 1.06e-2, moving Tr[n²] by 1.5e-4 and the total
100 * energy by 7.7e-6 Ry away from the full-zone answer. Because the Hubbard
101 * force contracts a *real* part (`accumulate_hubbard_force_gradient`'s
102 * `2·Re[...]`, which is itself the time-reversal average) against a D matrix
103 * built from a complex n, the two sides disagreed and the analytic force sat
104 * 0.3% off Q3's own energy gradient.
105 */
107 std::vector<std::vector<std::vector<Complex>>>& occupation_matrices) {
108 for (auto& per_spin_matrices : occupation_matrices)
109 for (auto& matrix : per_spin_matrices)
110 for (auto& element : matrix)
111 element = Complex(element.real(), 0.0);
112}
113
114/// One manifold's identity: which atom, which l, and where its rows live.
120
121/**
122 * Replace each manifold's occupation matrix by the point-group average.
123 *
124 * `occupation_matrices[manifold][spin]` is row-major (manifold_size ×
125 * manifold_size) and is modified in place. Manifolds are matched to their
126 * symmetry images by (atom, l): an operation maps atom `a` to
127 * `atom_map[a]`, and the manifold on that atom with the same l supplies the
128 * matrix to rotate. A manifold whose image is not itself a Hubbard manifold —
129 * possible only if the user applied U to some but not all symmetry-equivalent
130 * atoms — is left alone, because averaging against a matrix that was never
131 * accumulated would be worse than not averaging.
132 *
133 * **The pairing is the one thing here that a test has to pin.** Reindexing the
134 * group sum by R → R⁻¹ turns this average into the one that pairs `atom_map`
135 * with D(R) instead, with `atom_map` replaced by its inverse — so the two agree
136 * *exactly* whenever every Hubbard atom's permutation is its own inverse, which
137 * is always true of one or two Hubbard atoms and of one per species. Q3 shipped
138 * the other pairing until 2026-09-16 and nothing caught it, because every DFT+U
139 * case it is gated on is in one of those classes. Seeing the difference takes an
140 * orbit of three or more equivalent atoms carried round a cycle, on sites whose
141 * own symmetry still leaves the manifold free; that is what the cubic-perovskite
142 * cases in `tests/unit/.../hubbard/test_hubbard.cpp` are for.
143 */
145 const Crystal& crystal_structure,
146 const std::vector<SymmetryOperation>& symmetry_operations,
147 const std::vector<HubbardManifoldLayout>& layouts,
148 std::vector<std::vector<std::vector<Complex>>>& occupation_matrices) {
149 int number_of_operations = static_cast<int>(symmetry_operations.size());
150 if (number_of_operations <= 1 || layouts.empty())
151 return;
152
153 /**
154 * Rotation matrices are per (operation, l), not per manifold: two
155 * manifolds sharing an l share the matrix, and building them is the
156 * expensive part.
157 */
158 std::vector<int> distinct_angular_momenta;
159 for (const auto& layout : layouts) {
160 if (std::find(distinct_angular_momenta.begin(),
161 distinct_angular_momenta.end(),
162 layout.angular_momentum) == distinct_angular_momenta.end())
163 distinct_angular_momenta.push_back(layout.angular_momentum);
164 }
165 std::vector<std::vector<std::vector<Real>>> rotation_matrices(number_of_operations);
166 for (int operation_index = 0; operation_index < number_of_operations; operation_index++) {
167 Matrix3x3 cartesian =
168 cartesian_rotation_of(crystal_structure, symmetry_operations[operation_index]);
169 rotation_matrices[operation_index].resize(distinct_angular_momenta.size());
170 for (size_t slot = 0; slot < distinct_angular_momenta.size(); slot++)
171 rotation_matrices[operation_index][slot] =
172 build_real_harmonic_rotation_matrix(distinct_angular_momenta[slot], cartesian);
173 }
174
175 auto find_manifold = [&](int atom_index, int angular_momentum) {
176 for (size_t index = 0; index < layouts.size(); index++)
177 if (layouts[index].atom_index == atom_index &&
178 layouts[index].angular_momentum == angular_momentum)
179 return static_cast<int>(index);
180 return -1;
181 };
182
183 auto original = occupation_matrices;
184
185 for (size_t manifold_index = 0; manifold_index < layouts.size(); manifold_index++) {
186 const auto& layout = layouts[manifold_index];
187 int manifold_size = layout.manifold_size;
188 size_t rotation_slot = static_cast<size_t>(std::find(distinct_angular_momenta.begin(),
189 distinct_angular_momenta.end(),
190 layout.angular_momentum) -
191 distinct_angular_momenta.begin());
192
193 for (size_t spin_index = 0; spin_index < occupation_matrices[manifold_index].size();
194 spin_index++) {
195 std::vector<Complex> averaged(static_cast<size_t>(manifold_size) * manifold_size,
196 Complex(0.0, 0.0));
197 int contributing_operations = 0;
198
199 for (int operation_index = 0; operation_index < number_of_operations;
200 operation_index++) {
201 const auto& operation = symmetry_operations[operation_index];
202 if (layout.atom_index >= static_cast<int>(operation.atom_map.size()))
203 continue;
204 int image_atom = operation.atom_map[layout.atom_index];
205 int image_manifold = find_manifold(image_atom, layout.angular_momentum);
206 if (image_manifold < 0)
207 continue;
208
209 const auto& rotation = rotation_matrices[operation_index][rotation_slot];
210 const auto& source = original[image_manifold][spin_index];
211
212 // (Dᵀ · n · D)_{m m'} = Σ_pq D_{p m} n_{pq} D_{q m'}
213 for (int m = 0; m < manifold_size; m++) {
214 for (int m_prime = 0; m_prime < manifold_size; m_prime++) {
215 Complex accumulated(0.0, 0.0);
216 for (int p = 0; p < manifold_size; p++) {
217 Real left = rotation[static_cast<size_t>(p) * manifold_size + m];
218 if (left == 0.0)
219 continue;
220 for (int q = 0; q < manifold_size; q++) {
221 Real right =
222 rotation[static_cast<size_t>(q) * manifold_size + m_prime];
223 if (right == 0.0)
224 continue;
225 accumulated += left * right *
226 source[static_cast<size_t>(p) * manifold_size + q];
227 }
228 }
229 averaged[static_cast<size_t>(m) * manifold_size + m_prime] += accumulated;
230 }
231 }
232 contributing_operations++;
233 }
234
235 if (contributing_operations == 0)
236 continue;
237 for (auto& element : averaged)
238 element /= static_cast<Real>(contributing_operations);
239 occupation_matrices[manifold_index][spin_index] = averaged;
240 }
241 }
242}
243
244} // namespace q3
double Real
void symmetrize_hubbard_occupation_matrices_under_time_reversal(std::vector< std::vector< std::vector< Complex > > > &occupation_matrices)
void symmetrize_hubbard_occupation_matrices(const Crystal &crystal_structure, const std::vector< SymmetryOperation > &symmetry_operations, const std::vector< HubbardManifoldLayout > &layouts, std::vector< std::vector< std::vector< Complex > > > &occupation_matrices)
Matrix3x3 cartesian_rotation_of(const Crystal &crystal_structure, const SymmetryOperation &operation)
std::complex< double > Complex
std::vector< Real > build_real_harmonic_rotation_matrix(int angular_momentum, const Matrix3x3 &cartesian_rotation)
One manifold's identity: which atom, which l, and where its rows live.
3×3 matrix stored row-major: row i = mat[i]