Loading...
Searching...
No Matches
init.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * PlanewaveChargeDensitySymmetrizer — averages the reciprocal-space charge density
5 * over the crystal's space group.
6 *
7 * It groups G-vectors into shells related by the symmetry operations, then for each
8 * shell replaces every member's density component by the (phase-corrected) average
9 * over the orbit. Non-symmorphic operations contribute the fractional-translation
10 * phase. The symmetrizer owns only the precomputed symmetry operations and shells;
11 * the plane-wave basis and the density are passed in per call.
12 *
13 * The operations are every one spglib reports. For a supercell that includes the
14 * pure translations of the smaller cell it repeats: a 2x2x2 diamond cell has 384
15 * operations, 48 rotations times 8 translations. spglib's list is closed under
16 * composition, so the average is a projector onto densities invariant under the
17 * whole group -- the translations remove every Fourier component the repeated
18 * cell cannot carry, and the rotations act exactly as they do in the primitive
19 * cell. What matters is that this is the same group the k-point mesh was reduced
20 * with (SymmetryAnalyzer::reduce_kpoints_internal reads the same list): a reduced
21 * wedge only stands in for the full mesh once the density is averaged over the
22 * operations that produced it. Quantum ESPRESSO drops every translation-carrying
23 * operation of a supercell because its own operation search is not guaranteed to
24 * produce a group; carried over here, that left a 2x2x2 silicon cell averaged
25 * over the 12-operation subgroup fixing the origin, which happens to leave a
26 * shifted mesh invariant, so the supercell converged to the unsymmetrized-mesh
27 * density and sat 1.4e-4 Ry above eight primitive cells (camsai/q3#6).
28 *
29 * A G-vector that every rotation leaves in place is a shell of its own, and
30 * for it the average is a multiplication: by the mean of the operations'
31 * fractional-translation phases, exp(2 pi i G.t). Those phases form a
32 * character of the group, so the mean is exactly 1 or exactly 0. Where it is
33 * 0 a screw axis or a glide forbids the component outright -- the systematic
34 * absences of crystallography, (0, 0, l) with l odd under the 6_3 screw and
35 * c-glides of wurtzite's P6_3mc -- and those components are set to zero rather
36 * than averaged. Only a polar point group leaves a G-vector other than 0 in
37 * place (inversion, or any rotation turning the axis over, pairs G with -G),
38 * and only a translation along that axis makes the phases cancel. A wedge
39 * k-point leaves such components nonzero whenever its time-reversal partner
40 * lies outside its own star, as it does off wurtzite's k_z = 0 and 1/2 planes,
41 * and skipping one-member shells -- as this class did until 2026-10-09 -- let
42 * them through: the density lost the screw axis, and the levels it makes
43 * degenerate at K, A, L and H came out split by meV (camsai/q3#24).
44 */
45
46#include <array>
47#include <cmath>
48#include <complex>
49#include <cstdint>
50#include <unordered_map>
51#include <vector>
52
53#include "io/logger.hpp"
60
61namespace q3 {
62
64 public:
65 /// Precompute the symmetry operations and G-vector shells for the given cell/basis.
66 void build_shells(const Crystal& crystal_structure, const PlanewaveBasis& planewave_basis) {
67 if (planewave_basis.is_gamma_only()) {
68 symmetry_initialized_ = false;
69 return;
70 }
71
72 symmetry_operations_ = SymmetryAnalyzer::get_symmetry_operations(crystal_structure);
73 log::info(" Density symmetrization: {} symmetry operations",
74 symmetry_operations_.size());
75
76 int number_of_symmetry_operations = static_cast<int>(symmetry_operations_.size());
77
79 symmetry_initialized_ = false;
80 return;
81 }
82
83 int number_of_g_vectors = planewave_basis.number_of_g_vectors();
84 const auto& g_vectors = planewave_basis.g_vectors();
85
86 // Build a hash map from Miller indices to G-vector index for fast lookup.
87
88 // Key = (miller_index_1, miller_index_2, miller_index_3) packed into a single int64.
89 auto miller_to_key =
90 [](int miller_index_1, int miller_index_2, int miller_index_3) -> int64_t {
91 constexpr int64_t offset = 1000;
92 return (int64_t)(miller_index_1 + offset) * (2 * offset + 1) * (2 * offset + 1) +
93 (int64_t)(miller_index_2 + offset) * (2 * offset + 1) +
94 (int64_t)(miller_index_3 + offset);
95 };
96
97 std::unordered_map<int64_t, int> miller_index_map;
98 miller_index_map.reserve(number_of_g_vectors);
99 for (int g_vector_index = 0; g_vector_index < number_of_g_vectors; g_vector_index++) {
100 const auto& miller = g_vectors[g_vector_index].miller;
101 miller_index_map[miller_to_key(miller.x, miller.y, miller.z)] = g_vector_index;
102 }
103
104 std::vector<bool> assigned(number_of_g_vectors, false);
105 symmetry_shells_.clear();
106 symmetry_shells_.reserve(number_of_g_vectors);
107
108 for (int g_vector_index = 0; g_vector_index < number_of_g_vectors; g_vector_index++) {
109 if (assigned[g_vector_index]) {
110 continue;
111 }
112
113 SymmetryShell shell;
114 const auto& miller = g_vectors[g_vector_index].miller;
115 std::array<int, 3> miller_indices = {miller.x, miller.y, miller.z};
116
117 for (int operation_index = 0; operation_index < number_of_symmetry_operations;
118 operation_index++) {
119 std::array<int, 3> rotated = rotate_miller_indices(operation_index, miller_indices);
120 int64_t key = miller_to_key(rotated[0], rotated[1], rotated[2]);
121 auto it = miller_index_map.find(key);
122 if (it == miller_index_map.end()) {
123 continue;
124 }
125 int rotated_index = it->second;
126 if (assigned[rotated_index]) {
127 continue;
128 }
129 bool already_in_shell = false;
130 for (int existing_index : shell.g_vector_indices) {
131 if (existing_index == rotated_index) {
132 already_in_shell = true;
133 break;
134 }
135 }
136 if (!already_in_shell) {
137 shell.g_vector_indices.push_back(rotated_index);
138 assigned[rotated_index] = true;
139 }
140 }
141
142 if (!shell.g_vector_indices.empty()) {
143 build_rotated_member_indices(g_vectors, shell);
144 symmetry_shells_.push_back(std::move(shell));
145 }
146 }
147
148 systematically_absent_g_vector_indices_.clear();
149 for (const auto& shell : symmetry_shells_) {
150 if (shell.g_vector_indices.size() != 1) {
151 continue;
152 }
153 const auto& miller = g_vectors[shell.g_vector_indices[0]].miller;
154 if (is_forbidden_by_a_translation({miller.x, miller.y, miller.z})) {
155 systematically_absent_g_vector_indices_.push_back(shell.g_vector_indices[0]);
156 }
157 }
158
159 symmetry_initialized_ = true;
160 }
161
162 /// Symmetrize the spin-resolved real-space charge density in place.
163 /**
164 * Symmetrize charge density given in G-space and produce both symmetrized
165 * G-space and R-space outputs with a single G→R FFT.
166 *
167 * This avoids the R→G FFT that the real-space interface would require,
168 * saving ~100ms per call for large grids. The caller provides the G-space
169 * density (typically available from construct_density_from_states or
170 * add_augmentation), and receives the symmetrized G-space density in-place
171 * plus the symmetrized R-space density in the output parameter.
172 */
173 void symmetrize_from_g_space(const PlanewaveBasis& planewave_basis,
174 std::vector<std::vector<Complex>>& charge_density_g_components,
175 std::vector<std::vector<Real>>& charge_density_real,
176 int number_of_spin_components) const {
177 if (!symmetry_initialized_) {
178 // No symmetry to apply — just do G→R transform
179 for (int spin_index = 0; spin_index < number_of_spin_components; spin_index++) {
180 planewave_basis.rho_g_to_r(charge_density_g_components[spin_index],
181 charge_density_real[spin_index]);
182 }
183 return;
184 }
185
186 int number_of_symmetry_operations = static_cast<int>(symmetry_operations_.size());
187
188 std::vector<bool> is_non_symmorphic(number_of_symmetry_operations, false);
189 for (int operation_index = 0; operation_index < number_of_symmetry_operations;
190 operation_index++) {
191 const auto& fractional_translation =
192 symmetry_operations_[operation_index].fractional_translation;
193 is_non_symmorphic[operation_index] =
194 (std::abs(fractional_translation.x) >
196 std::abs(fractional_translation.y) >
198 std::abs(fractional_translation.z) >
200 }
201
202 for (const auto& shell : symmetry_shells_) {
203 if (static_cast<int>(shell.g_vector_indices.size()) <= 1) {
204 continue;
205 }
206 symmetrize_shell(planewave_basis,
207 number_of_spin_components,
208 shell,
209 is_non_symmorphic,
210 charge_density_g_components);
211 }
212 zero_systematically_absent_components(number_of_spin_components,
213 charge_density_g_components);
214
215 // Single G→R transform for each spin component
216 for (int spin_index = 0; spin_index < number_of_spin_components; spin_index++) {
217 planewave_basis.rho_g_to_r(charge_density_g_components[spin_index],
218 charge_density_real[spin_index]);
219 }
220 }
221
222 void symmetrize(const PlanewaveBasis& planewave_basis,
223 std::vector<std::vector<Real>>& charge_density_real,
224 int number_of_spin_components) const {
225 if (!symmetry_initialized_) {
226 return;
227 }
228
229 int number_of_symmetry_operations = static_cast<int>(symmetry_operations_.size());
230
231 std::vector<std::vector<Complex>> charge_density_g_components(number_of_spin_components);
232 for (int spin_index = 0; spin_index < number_of_spin_components; spin_index++) {
233 planewave_basis.rho_r_to_g(charge_density_real[spin_index],
234 charge_density_g_components[spin_index]);
235 }
236
237 std::vector<bool> is_non_symmorphic(number_of_symmetry_operations, false);
238 for (int operation_index = 0; operation_index < number_of_symmetry_operations;
239 operation_index++) {
240 const auto& fractional_translation =
241 symmetry_operations_[operation_index].fractional_translation;
242 is_non_symmorphic[operation_index] =
243 (std::abs(fractional_translation.x) >
245 std::abs(fractional_translation.y) >
247 std::abs(fractional_translation.z) >
249 }
250
251 for (const auto& shell : symmetry_shells_) {
252 if (static_cast<int>(shell.g_vector_indices.size()) <= 1) {
253 continue;
254 }
255 symmetrize_shell(planewave_basis,
256 number_of_spin_components,
257 shell,
258 is_non_symmorphic,
259 charge_density_g_components);
260 }
261 zero_systematically_absent_components(number_of_spin_components,
262 charge_density_g_components);
263
264 for (int spin_index = 0; spin_index < number_of_spin_components; spin_index++) {
265 planewave_basis.rho_g_to_r(charge_density_g_components[spin_index],
266 charge_density_real[spin_index]);
267 }
268 }
269
270 [[nodiscard]] bool is_initialized() const {
271 return symmetry_initialized_;
272 }
273 [[nodiscard]] int number_of_shells() const {
274 return static_cast<int>(symmetry_shells_.size());
275 }
276 [[nodiscard]] int number_of_symmetry_operations() const {
277 return static_cast<int>(symmetry_operations_.size());
278 }
279 /// G-vectors whose component a screw axis or glide forbids; zero unless the group is polar.
281 return static_cast<int>(systematically_absent_g_vector_indices_.size());
282 }
283
284 private:
285 struct SymmetryShell {
286 std::vector<int> g_vector_indices;
287
288 /**
289 * Which member of this shell each symmetry operation carries each
290 * member to: `rotated_member_indices[member][operation]`, or -1 when
291 * the rotated G-vector is not in the basis.
292 *
293 * Built once with the shell, because it is a function of the Miller
294 * indices and the point group and neither moves during an SCF. It
295 * used to be rediscovered on every iteration by a linear search over
296 * the shell for every (member, operation) pair -- O(N_ops * size^2)
297 * per shell, per iteration, to recompute an answer that could not
298 * have changed.
299 */
300 std::vector<std::vector<int>> rotated_member_indices;
301 };
302 std::vector<SymmetryOperation> symmetry_operations_;
303 std::vector<SymmetryShell> symmetry_shells_;
304 /// One-member shells whose translation phases cancel; zeroed after every average.
305 std::vector<int> systematically_absent_g_vector_indices_;
306 bool symmetry_initialized_{false};
307
308 /**
309 * Whether the group average of the component at `miller` is zero.
310 *
311 * The operations that leave G in place multiply its component by
312 * exp(2 pi i G.t), and those phases are a character of that subgroup:
313 * composing {R|t} with {R'|t'} adds R t' to t, and R^T G = G turns the
314 * extra phase into exp(2 pi i G.t'). The mean of a character is 1 when it
315 * is trivial and 0 otherwise, so the mean is compared against the midpoint
316 * rather than any phase against a tolerance -- spglib reports translations
317 * unrounded, and a long supercell's pure translations make phases as small
318 * as 1/N of a turn.
319 */
320 [[nodiscard]] bool is_forbidden_by_a_translation(const std::array<int, 3>& miller) const {
321 Complex phase_sum(0.0, 0.0);
322 int fixing_operations = 0;
323 for (int operation_index = 0;
324 operation_index < static_cast<int>(symmetry_operations_.size());
325 operation_index++) {
326 if (rotate_miller_indices(operation_index, miller) != miller) {
327 continue;
328 }
329 const auto& translation = symmetry_operations_[operation_index].fractional_translation;
330 Real argument =
332 (miller[0] * translation.x + miller[1] * translation.y + miller[2] * translation.z);
333 phase_sum += Complex(std::cos(argument), std::sin(argument));
334 fixing_operations++;
335 }
336 return fixing_operations > 0 && std::abs(phase_sum) < 0.5 * fixing_operations;
337 }
338
339 void zero_systematically_absent_components(
340 int number_of_spin_components,
341 std::vector<std::vector<Complex>>& charge_density_g_components) const {
342 for (int spin_index = 0; spin_index < number_of_spin_components; spin_index++) {
343 for (int g_vector_index : systematically_absent_g_vector_indices_) {
344 charge_density_g_components[spin_index][g_vector_index] = Complex(0.0, 0.0);
345 }
346 }
347 }
348
349 std::array<int, 3> rotate_miller_indices(int operation_index,
350 const std::array<int, 3>& miller) const {
351 const auto& reciprocal_rotation = symmetry_operations_[operation_index].reciprocal_rotation;
352 return {{reciprocal_rotation[0][0] * miller[0] + reciprocal_rotation[0][1] * miller[1] +
353 reciprocal_rotation[0][2] * miller[2],
354 reciprocal_rotation[1][0] * miller[0] + reciprocal_rotation[1][1] * miller[1] +
355 reciprocal_rotation[1][2] * miller[2],
356 reciprocal_rotation[2][0] * miller[0] + reciprocal_rotation[2][1] * miller[1] +
357 reciprocal_rotation[2][2] * miller[2]}};
358 }
359
360 /**
361 * Fill one shell's rotation map: for each member and each operation, the
362 * index *within this shell* of the member the operation rotates it to.
363 *
364 * A shell is closed under the group by construction, so a rotated
365 * G-vector that is in the basis at all is in this shell -- which is what
366 * lets the map be an index into the shell rather than into the basis, and
367 * why the search it replaces was over the shell.
368 *
369 * -1 marks an operation whose rotated vector is not in the basis, the
370 * same value the search produced when it found nothing, so the
371 * symmetrization reads it unchanged.
372 */
373 void build_rotated_member_indices(const std::vector<GVector>& g_vectors,
374 SymmetryShell& shell) const {
375 const int shell_size = static_cast<int>(shell.g_vector_indices.size());
376 const int number_of_symmetry_operations = static_cast<int>(symmetry_operations_.size());
377
378 std::vector<std::array<int, 3>> shell_miller_indices(shell_size);
379 for (int member_index = 0; member_index < shell_size; member_index++) {
380 const auto& miller = g_vectors[shell.g_vector_indices[member_index]].miller;
381 shell_miller_indices[member_index] = {miller.x, miller.y, miller.z};
382 }
383
384 shell.rotated_member_indices.assign(shell_size,
385 std::vector<int>(number_of_symmetry_operations, -1));
386 for (int member_index = 0; member_index < shell_size; member_index++) {
387 for (int operation_index = 0; operation_index < number_of_symmetry_operations;
388 operation_index++) {
389 const std::array<int, 3> rotated =
390 rotate_miller_indices(operation_index, shell_miller_indices[member_index]);
391 for (int candidate_index = 0; candidate_index < shell_size; candidate_index++) {
392 if (rotated == shell_miller_indices[candidate_index]) {
393 shell.rotated_member_indices[member_index][operation_index] =
394 candidate_index;
395 break;
396 }
397 }
398 }
399 }
400 }
401
402 void symmetrize_shell(const PlanewaveBasis& planewave_basis,
403 int number_of_spin_components,
404 const SymmetryShell& shell,
405 const std::vector<bool>& is_non_symmorphic,
406 std::vector<std::vector<Complex>>& charge_density_g_components) const {
407 const auto& g_vectors = planewave_basis.g_vectors();
408 int number_of_symmetry_operations = static_cast<int>(symmetry_operations_.size());
409 int number_of_components = number_of_spin_components;
410 int shell_size = static_cast<int>(shell.g_vector_indices.size());
411
412 std::vector<bool> done(shell_size, false);
413
414 for (int shell_member_index = 0; shell_member_index < shell_size; shell_member_index++) {
415 if (done[shell_member_index]) {
416 continue;
417 }
418
419 std::vector<Complex> averaged_density(number_of_components, Complex(0.0, 0.0));
420 const std::vector<int>& rotated_shell_indices =
421 shell.rotated_member_indices[shell_member_index];
422
423 for (int operation_index = 0; operation_index < number_of_symmetry_operations;
424 operation_index++) {
425 if (rotated_shell_indices[operation_index] < 0) {
426 continue;
427 }
428
429 int source_global_index =
430 shell.g_vector_indices[rotated_shell_indices[operation_index]];
431
432 if (is_non_symmorphic[operation_index]) {
433 /**
434 * Correct formula: rho_sym(G) = (1/N) sum_op e^{+2pi i G_source.t}
435 * rho(G_source) where G_source = R_op^{-T} G (the reciprocal-space rotation of
436 * G).
437 *
438 * Derivation: the symmetry constraint rho(G) = e^{+2pi i G_source.t}
439 * rho(G_source) gives contribution rho(G_source) * e^{+2pi i G_source.t} =
440 * rho(G) for each op when the density is self-consistent. Phase uses G_source
441 * (not G_target).
442 */
443 const auto& source_miller = g_vectors[source_global_index].miller;
444 const auto& fractional_translation =
445 symmetry_operations_[operation_index].fractional_translation;
446 Real argument =
447 constants::two_pi * (source_miller.x * fractional_translation.x +
448 source_miller.y * fractional_translation.y +
449 source_miller.z * fractional_translation.z);
450 Complex phase_factor(std::cos(argument), std::sin(argument));
451 for (int spin_index = 0; spin_index < number_of_components; spin_index++) {
452 averaged_density[spin_index] +=
453 charge_density_g_components[spin_index][source_global_index] *
454 phase_factor;
455 }
456 } else {
457 for (int spin_index = 0; spin_index < number_of_components; spin_index++) {
458 averaged_density[spin_index] +=
459 charge_density_g_components[spin_index][source_global_index];
460 }
461 }
462 }
463
464 for (int spin_index = 0; spin_index < number_of_components; spin_index++) {
465 averaged_density[spin_index] /= number_of_symmetry_operations;
466 }
467
468 for (int operation_index = 0; operation_index < number_of_symmetry_operations;
469 operation_index++) {
470 if (rotated_shell_indices[operation_index] < 0) {
471 continue;
472 }
473
474 int target_global_index =
475 shell.g_vector_indices[rotated_shell_indices[operation_index]];
476
477 if (is_non_symmorphic[operation_index]) {
478 /**
479 * Write-back: rho_sym(G_target) = rho_sym(G_ref) * e^{-2pi i G_target.t_map}
480 * where op_map takes G_ref -> G_target = R_map^{-T} G_ref.
481 */
482 const auto& target_miller = g_vectors[target_global_index].miller;
483 const auto& fractional_translation =
484 symmetry_operations_[operation_index].fractional_translation;
485 Real argument =
486 constants::two_pi * (target_miller.x * fractional_translation.x +
487 target_miller.y * fractional_translation.y +
488 target_miller.z * fractional_translation.z);
489 Complex phase_factor(std::cos(argument), -std::sin(argument));
490 for (int spin_index = 0; spin_index < number_of_components; spin_index++) {
491 charge_density_g_components[spin_index][target_global_index] =
492 averaged_density[spin_index] * phase_factor;
493 }
494 } else {
495 for (int spin_index = 0; spin_index < number_of_components; spin_index++) {
496 charge_density_g_components[spin_index][target_global_index] =
497 averaged_density[spin_index];
498 }
499 }
500 done[rotated_shell_indices[operation_index]] = true;
501 }
502 }
503 }
504};
505
506} // namespace q3
int number_of_g_vectors() const
Definition init.hpp:145
void rho_g_to_r(const std::vector< Complex > &charge_density_g_space, std::vector< Real > &charge_density_real_space) const
Definition init.hpp:328
const std::vector< GVector > & g_vectors() const
Definition init.hpp:148
void rho_r_to_g(const std::vector< Real > &charge_density_real_space, std::vector< Complex > &charge_density_g_space) const
Definition init.hpp:334
bool is_gamma_only() const
Definition init.hpp:157
void symmetrize(const PlanewaveBasis &planewave_basis, std::vector< std::vector< Real > > &charge_density_real, int number_of_spin_components) const
Definition init.hpp:222
void symmetrize_from_g_space(const PlanewaveBasis &planewave_basis, std::vector< std::vector< Complex > > &charge_density_g_components, std::vector< std::vector< Real > > &charge_density_real, int number_of_spin_components) const
Symmetrize the spin-resolved real-space charge density in place.
Definition init.hpp:173
void build_shells(const Crystal &crystal_structure, const PlanewaveBasis &planewave_basis)
Precompute the symmetry operations and G-vector shells for the given cell/basis.
Definition init.hpp:66
int number_of_systematically_absent_g_vectors() const
G-vectors whose component a screw axis or glide forbids; zero unless the group is polar.
Definition init.hpp:280
static std::vector< SymmetryOperation > get_symmetry_operations(const Crystal &crystal, Real symmetry_precision=math::tolerances::default_symmetry_precision)
Definition symmetry.hpp:484
constexpr Real two_pi
Definition constants.hpp:25
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
constexpr double zero_vector_magnitude_threshold
double Real
std::complex< double > Complex