Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
occupation_symmetrization.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Symmetrizing the PAW on-site occupations ρ_ij.
5 *
6 * ρ_ij is accumulated as a weighted sum over the *irreducible* wedge,
7 *
8 * ρ^a_ij = Σ_{k∈IBZ} w_k Σ_v f_kv Re[⟨ψ_kv|β^a_i⟩⟨β^a_j|ψ_kv⟩],
9 *
10 * and a weighted wedge sum reproduces the full-zone sum only for quantities the
11 * point group leaves alone. ρ_ij is not one: each projector channel carries an
12 * angular index (β_i is a radial function times Y_{l_i m_i}), and every
13 * operation mixes those indices and permutes equivalent atoms. Summing the
14 * wedge and stopping leaves an array that is not symmetry-adapted — its trace
15 * is right, its structure is not.
16 *
17 * The restoration is the group average, with the same pairing
18 * `SymmetryAnalyzer::symmetrize_vector` uses for forces and for the same
19 * reason — it is the one combination that leaves an already-symmetric array
20 * unchanged:
21 *
22 * ρ^a ← (1/N_R) Σ_R D(R)ᵀ · ρ^{R(a)} · D(R)
23 *
24 * with R(a) = `atom_map[a]` the atom `a` is carried *to*, and D(R) the
25 * real-harmonic rotation of the channel's l. Read the other way round, it is
26 * the statement the crystal makes: ρ^{R(a)} = D(R) ρ^a D(R)ᵀ. The radial index
27 * of a channel is untouched — a rotation does not mix β's of different radial
28 * shape — so the average acts only inside each (β_i, β_j) block, on the
29 * (2l_i+1)×(2l_j+1) angular indices.
30 *
31 * **Why ultrasoft never needed this.** The same array builds the augmentation
32 * charge, and that reaches the grid, where the density symmetrizer repairs it.
33 * PAW's one-centre Hartree and XC terms read ρ_ij directly and never pass
34 * through a grid, so nothing downstream repairs them. Measured at 30 Ry,
35 * 2×2×2 Γ-centred (3 irreducible k-points against the full 8): diamond PAW
36 * split its Γ valence maximum 13.7274 / 13.7427 / 13.7465 eV where the full
37 * mesh gives 13.7393 three times, and sat 0.19 mRy above it; silicon PAW 0.25
38 * mRy; the same silicon cell with ultrasoft or norm-conserving datasets agreed
39 * with its full mesh to 1e-7 Ry.
40 *
41 * **Time reversal needs no separate step here**, unlike the DFT+U occupation
42 * matrix. ρ_ij is accumulated as `Re[conj(⟨β_i|ψ⟩)⟨β_j|ψ⟩]`, which is
43 * already the average over the ±k pair the wedge folded together — the real
44 * part *is* the Θ projection. Quantum ESPRESSO's collinear becsum is real by
45 * the same construction.
46 *
47 * **Spin.** Each channel is averaged over the crystal's spatial operations
48 * separately, exactly as `PlanewaveChargeDensitySymmetrizer` treats each spin
49 * component of the density; the assumption that the chemical cell's operations
50 * are symmetries of each spin channel is the one Q3 already makes on the grid,
51 * not a new one taken here.
52 */
53
54#include <algorithm>
55#include <vector>
56
62
63namespace q3::paw_one_center {
64
65/**
66 * What each ρ_ij channel of one species means angularly.
67 *
68 * The expanded channels run over (radial β, m) in `harmonic_order_sequence`
69 * order, so a channel's angular partners are the ones sharing its β — a
70 * contiguous run of 2l+1 entries. `manifold_base` is where that run starts and
71 * `manifold_position` is the offset into it, which is also the row and column
72 * index of D^l.
73 */
76 std::vector<int> angular_momentum;
77 std::vector<int> manifold_base;
78 std::vector<int> manifold_position;
79};
80
81/**
82 * Derive the layout from the per-channel radial index and l that the projector
83 * mapping table already publishes, rather than re-deriving the expansion rule.
84 */
85[[nodiscard]] inline ChannelAngularLayout
86build_channel_angular_layout(const std::vector<int>& beta_index_per_channel,
87 const std::vector<int>& angular_momentum_per_channel) {
89 layout.number_of_channels = static_cast<int>(beta_index_per_channel.size());
90 layout.angular_momentum = angular_momentum_per_channel;
91 layout.manifold_base.resize(beta_index_per_channel.size());
92 layout.manifold_position.resize(beta_index_per_channel.size());
93
94 for (size_t channel = 0; channel < beta_index_per_channel.size(); channel++) {
95 size_t base = channel;
96 while (base > 0 && beta_index_per_channel[base - 1] == beta_index_per_channel[channel])
97 base--;
98 layout.manifold_base[channel] = static_cast<int>(base);
99 layout.manifold_position[channel] = static_cast<int>(channel - base);
100 }
101 return layout;
102}
103
104/**
105 * The point-group average of ρ_ij, with D^l(R) built once and reused.
106 *
107 * Built once per run (the operations and the datasets do not change during an
108 * SCF) and applied to every ρ_ij array each iteration. `is_active()` is
109 * false when there is nothing to do — one operation, or no channels — so the
110 * caller does not need a second condition of its own.
111 */
113 public:
114 /**
115 * @param crystal_structure supplies the lattice for R_cart and the atom→species map
116 * @param symmetry_operations the crystal's operations, `atom_map` filled in
117 * @param layout_by_species one entry per species type, indexed as `species_type_index`
118 */
119 void build(const Crystal& crystal_structure,
120 const std::vector<SymmetryOperation>& symmetry_operations,
121 std::vector<ChannelAngularLayout> layout_by_species) {
122 layout_by_species_ = std::move(layout_by_species);
123 symmetry_operations_ = symmetry_operations;
124 rotations_by_operation_.clear();
125 rotation_slot_by_species_.clear();
126 angular_momenta_.clear();
127
128 if (symmetry_operations_.size() <= 1)
129 return;
130
131 for (const auto& layout : layout_by_species_)
132 for (int momentum : layout.angular_momentum)
133 if (std::find(angular_momenta_.begin(), angular_momenta_.end(), momentum) ==
134 angular_momenta_.end())
135 angular_momenta_.push_back(momentum);
136 if (angular_momenta_.empty())
137 return;
138
139 rotation_slot_by_species_.assign(layout_by_species_.size(), {});
140 for (size_t species = 0; species < layout_by_species_.size(); species++)
141 for (int momentum : layout_by_species_[species].angular_momentum)
142 rotation_slot_by_species_[species].push_back(static_cast<int>(
143 std::find(angular_momenta_.begin(), angular_momenta_.end(), momentum) -
144 angular_momenta_.begin()));
145
146 /**
147 * One D^l per (operation, l): two channels sharing an l share the
148 * matrix, and the angular quadrature that builds it is the expensive
149 * part.
150 */
151 rotations_by_operation_.resize(symmetry_operations_.size());
152 for (size_t operation = 0; operation < symmetry_operations_.size(); operation++) {
153 Matrix3x3 cartesian =
154 cartesian_rotation_of(crystal_structure, symmetry_operations_[operation]);
155 rotations_by_operation_[operation].resize(angular_momenta_.size());
156 for (size_t slot = 0; slot < angular_momenta_.size(); slot++)
157 rotations_by_operation_[operation][slot] =
158 build_real_harmonic_rotation_matrix(angular_momenta_[slot], cartesian);
159 }
160 }
161
162 [[nodiscard]] bool is_active() const {
163 return !rotations_by_operation_.empty();
164 }
165
166 [[nodiscard]] int number_of_symmetry_operations() const {
167 return static_cast<int>(symmetry_operations_.size());
168 }
169
170 /**
171 * Replace ρ_ij by its group average, in place.
172 *
173 * `occupations[atom][i * nh_atom + j]`, with `nh_atom` the atom's *own*
174 * channel count — the indexing stride the ultrasoft accumulation writes
175 * with, which is not the allocated row length in a mixed-species cell.
176 * An atom whose image under some operation could not be identified (an
177 * `atom_map` entry left unresolved) contributes nothing for that
178 * operation, and the average is taken over the operations that did
179 * contribute: averaging against an array that was never accumulated would
180 * be worse than averaging over fewer operations.
181 */
182 void symmetrize(const Crystal& crystal_structure, Matrix<Real>& occupations) const {
183 if (!is_active())
184 return;
185 int number_of_atoms = crystal_structure.number_of_atoms();
186 Matrix<Real> original = occupations;
187
188 for (int atom_index = 0; atom_index < number_of_atoms; atom_index++) {
189 int species_index = crystal_structure.species_type_index(atom_index);
190 if (species_index < 0 || species_index >= static_cast<int>(layout_by_species_.size()))
191 continue;
192 const auto& layout = layout_by_species_[species_index];
193 const auto& rotation_slots = rotation_slot_by_species_[species_index];
194 int channels = layout.number_of_channels;
195 if (channels == 0)
196 continue;
197
198 std::vector<Real> averaged(static_cast<size_t>(channels) * channels, 0.0);
199 int contributing_operations = 0;
200
201 for (size_t operation = 0; operation < symmetry_operations_.size(); operation++) {
202 const auto& atom_map = symmetry_operations_[operation].atom_map;
203 if (atom_index >= static_cast<int>(atom_map.size()))
204 continue;
205 int image_atom = atom_map[atom_index];
206 if (image_atom < 0 || image_atom >= number_of_atoms)
207 continue;
208 if (crystal_structure.species_type_index(image_atom) != species_index)
209 continue;
210
211 const Real* source = original[image_atom];
212 const auto& rotations = rotations_by_operation_[operation];
213
214 for (int first = 0; first < channels; first++) {
215 const auto& rotation_first =
216 rotations[static_cast<size_t>(rotation_slots[first])];
217 int size_first = 2 * layout.angular_momentum[first] + 1;
218 int base_first = layout.manifold_base[first];
219 int position_first = layout.manifold_position[first];
220
221 for (int second = 0; second < channels; second++) {
222 const auto& rotation_second =
223 rotations[static_cast<size_t>(rotation_slots[second])];
224 int size_second = 2 * layout.angular_momentum[second] + 1;
225 int base_second = layout.manifold_base[second];
226 int position_second = layout.manifold_position[second];
227
228 /**
229 * (D ᵀ ρ D)_{m m'} = Σ_pq D_{p m} ρ_{pq} D_{q m'}, with
230 * p and q running over the partners of `first` and
231 * `second` — the channels sharing their radial index.
232 */
233 Real accumulated = 0.0;
234 for (int p = 0; p < size_first; p++) {
235 Real left = rotation_first[static_cast<size_t>(p) * size_first +
236 position_first];
237 if (left == 0.0)
238 continue;
239 const Real* row = source +
240 static_cast<size_t>(base_first + p) * channels +
241 base_second;
242 for (int q = 0; q < size_second; q++) {
243 Real right = rotation_second[static_cast<size_t>(q) * size_second +
244 position_second];
245 if (right == 0.0)
246 continue;
247 accumulated += left * right * row[q];
248 }
249 }
250 averaged[static_cast<size_t>(first) * channels + second] += accumulated;
251 }
252 }
253 contributing_operations++;
254 }
255
256 if (contributing_operations == 0)
257 continue;
258 Real normalization = 1.0 / static_cast<Real>(contributing_operations);
259 Real* destination = occupations[atom_index];
260 for (int index = 0; index < channels * channels; index++)
261 destination[index] = averaged[static_cast<size_t>(index)] * normalization;
262 }
263 }
264
265 private:
266 std::vector<SymmetryOperation> symmetry_operations_;
267 std::vector<ChannelAngularLayout> layout_by_species_;
268 /// The distinct l values across every species, in the order D^l was built for them.
269 std::vector<int> angular_momenta_;
270 /// rotation_slot_by_species_[species][channel] — which of those l that channel carries.
271 std::vector<std::vector<int>> rotation_slot_by_species_;
272 /// rotations_by_operation_[operation][slot] — D^l for `angular_momenta_[slot]`.
273 std::vector<std::vector<std::vector<Real>>> rotations_by_operation_;
274};
275
276} // namespace q3::paw_one_center
int species_type_index(int atom_index) const
Definition crystal.hpp:273
int number_of_atoms() const
Definition crystal.hpp:209
void symmetrize(const Crystal &crystal_structure, Matrix< Real > &occupations) const
void build(const Crystal &crystal_structure, const std::vector< SymmetryOperation > &symmetry_operations, std::vector< ChannelAngularLayout > layout_by_species)
ChannelAngularLayout build_channel_angular_layout(const std::vector< int > &beta_index_per_channel, const std::vector< int > &angular_momentum_per_channel)
double Real
Matrix3x3 cartesian_rotation_of(const Crystal &crystal_structure, const SymmetryOperation &operation)
std::vector< Real > build_real_harmonic_rotation_matrix(int angular_momentum, const Matrix3x3 &cartesian_rotation)
3×3 matrix stored row-major: row i = mat[i]