Loading...
Searching...
No Matches
kpoint_neighbor_shells.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * KPointNeighborShells: b-vector shells and finite-difference weights for a
5 * uniform (Monkhorst-Pack) k-point grid.
6 *
7 * Wannier interpolation and localization need a set of vectors b connecting
8 * each k-point to its neighbors on the grid, with weights w_b satisfying
9 * the completeness condition (Eq. B1 of Marzari & Vanderbilt, PRB 56, 12847
10 * (1997)):
11 *
12 * sum_b w_b * b_alpha * b_beta = delta_alpha_beta alpha, beta in {x,y,z}
13 *
14 * so that a finite difference of a k-dependent quantity along the b-vectors
15 * reconstructs its true gradient. This class finds the smallest set of
16 * "shells" (groups of b-vectors of equal length, always closed under
17 * b -> -b) that satisfies the condition, growing the set in order of
18 * increasing |b| and rejecting any shell whose vectors are linearly
19 * dependent on the ones already kept -- so the final set uses the shortest
20 * (most local) b-vectors that make the condition solvable.
21 *
22 * b-vectors are stored as an integer "grid step" (the offset, in
23 * Monkhorst-Pack grid-index units, from a k-point to its neighbor) together
24 * with the Cartesian translation that step corresponds to. The grid step
25 * lets resolve_neighbor() find a specific k-point's neighbor by exact
26 * integer arithmetic -- no floating-point ambiguity near the zone boundary.
27 */
28
29#include <algorithm>
30#include <array>
31#include <cmath>
32#include <stdexcept>
33#include <string>
34#include <utility>
35#include <vector>
36
40
41namespace q3 {
42
43/**
44 * One direction within a neighbor shell: an integer grid step and the
45 * Cartesian translation it corresponds to. All directions in the same
46 * shell (same |b|) share one weight.
47 */
53
54/**
55 * The k-point and reciprocal-lattice-vector shift a neighbor direction
56 * resolves to, starting from a specific physical k-point on the grid.
57 */
58struct [[nodiscard]] ResolvedKPointNeighbor {
59 int neighbor_kpoint_index = 0;
60 /**
61 * Integer reciprocal-lattice-vector (Miller-index) shift: the
62 * physical k-point at `neighbor_kpoint_index` plus this shift, in
63 * reciprocal lattice vector units, equals the starting k-point plus
64 * the unwrapped Cartesian translation.
65 */
67};
68
70 public:
72
73 /**
74 * Find the b-vector shells and weights for a Monkhorst-Pack grid with
75 * the given dimensions, in the reciprocal lattice described by
76 * `reciprocal_lattice_vectors` (rows = b1, b2, b3, in any consistent
77 * Cartesian unit system -- the completeness condition is scale
78 * covariant, so the computed weights come out in the inverse square of
79 * whatever units the caller passes in).
80 *
81 * `search_range` bounds the cube of candidate integer grid steps
82 * searched for shells (each component ranges over
83 * [-search_range, search_range]); `maximum_shell_count` bounds how many
84 * shells may be accepted before giving up.
85 *
86 * Throws std::runtime_error if no set of shells within those bounds
87 * satisfies the completeness condition to `completeness_tolerance`, or
88 * if a solved weight comes out negative beyond numerical noise -- both
89 * signal a lattice/grid combination this search range cannot handle.
90 */
91 void compute(const Matrix3x3& reciprocal_lattice_vectors,
92 int grid_dimension_x,
93 int grid_dimension_y,
94 int grid_dimension_z,
95 int search_range = 3,
96 int maximum_shell_count = 12,
97 Real shell_relative_tolerance = 1e-6,
98 Real singular_value_tolerance = 1e-10,
99 Real completeness_tolerance = 1e-10) {
100 neighbor_directions_.clear();
101 number_of_shells_ = 0;
102
103 const std::array<int, 3> grid_dimensions = {
104 grid_dimension_x, grid_dimension_y, grid_dimension_z};
105
106 std::vector<Shell> candidate_shells = enumerate_candidate_shells(
107 reciprocal_lattice_vectors, grid_dimensions, search_range, shell_relative_tolerance);
108
109 /**
110 * 6 independent components of a symmetric 3x3 tensor, in the fixed
111 * order xx, yy, zz, xy, xz, yz.
112 */
113 std::vector<std::array<Real, 6>> accepted_shell_tensors;
114 std::vector<const Shell*> accepted_shells;
115 const std::array<Real, 6> target = {1.0, 1.0, 1.0, 0.0, 0.0, 0.0};
116
117 for (const Shell& candidate : candidate_shells) {
118 if (static_cast<int>(accepted_shells.size()) >= maximum_shell_count) {
119 break;
120 }
121
122 std::array<Real, 6> candidate_tensor = shell_tensor(candidate);
123 accepted_shell_tensors.push_back(candidate_tensor);
124 accepted_shells.push_back(&candidate);
125
126 LeastSquaresSolution solution =
127 solve_least_squares(accepted_shell_tensors, target, singular_value_tolerance);
128
129 if (!solution.smallest_singular_value_cleared_tolerance) {
130 /**
131 * This shell added no new independent direction to the
132 * completeness system -- it is redundant with the shells
133 * already accepted. Reject it and keep searching.
134 */
135 accepted_shell_tensors.pop_back();
136 accepted_shells.pop_back();
137 continue;
138 }
139
140 if (solution.residual_norm < completeness_tolerance) {
141 finalize(accepted_shells, solution.weights);
142 return;
143 }
144 }
145
146 throw std::runtime_error("KPointNeighborShells::compute: no set of shells up to " +
147 std::to_string(maximum_shell_count) + " shells (search range " +
148 std::to_string(search_range) +
149 ") satisfies the completeness condition to the requested "
150 "tolerance -- widen search_range or maximum_shell_count.");
151 }
152
153 [[nodiscard]] const std::vector<KPointNeighborDirection>& neighbor_directions() const {
154 return neighbor_directions_;
155 }
156
157 [[nodiscard]] int number_of_shells() const {
158 return number_of_shells_;
159 }
160
161 /**
162 * Which physical k-point k + b lands on, from the grid shape alone.
163 *
164 * Separate from resolve_neighbor() because a consumer that already trusts
165 * the k-point list -- the stencil, walking a record's own neighbour table
166 * -- needs the index and not the periodic-image shift, and must get it
167 * from exactly this arithmetic rather than a second copy of it. The
168 * enumeration order is the one KPointsGrid produces: first index slowest,
169 * third fastest.
170 */
171 [[nodiscard]] static int neighbor_kpoint_index(int starting_kpoint_index,
172 const IntegerVector3D& grid_step,
173 const std::array<int, 3>& grid_dimensions) {
174 const int dimension_x = grid_dimensions[0];
175 const int dimension_y = grid_dimensions[1];
176 const int dimension_z = grid_dimensions[2];
177
178 const int index_z = starting_kpoint_index % dimension_z;
179 const int index_y = (starting_kpoint_index / dimension_z) % dimension_y;
180 const int index_x = starting_kpoint_index / (dimension_y * dimension_z);
181
182 return wrap_index(index_x + grid_step.x, dimension_x) * dimension_y * dimension_z +
183 wrap_index(index_y + grid_step.y, dimension_y) * dimension_z +
184 wrap_index(index_z + grid_step.z, dimension_z);
185 }
186
187 /**
188 * Resolve the physical k-point index and periodic-image (reciprocal
189 * lattice vector) shift that `grid_step` leads to, starting from
190 * `starting_kpoint_index` on a full, unreduced Monkhorst-Pack grid of
191 * `grid_dimensions`. `physical_kpoints` must be that grid's coordinate
192 * list, enumerated in the same (x-major, y, z-minor) nested order as
193 * KPointsGrid::generate() -- the neighbor's flat index is recovered by
194 * exact integer arithmetic on that ordering, not by searching
195 * coordinates.
196 */
197 [[nodiscard]] static ResolvedKPointNeighbor
198 resolve_neighbor(int starting_kpoint_index,
199 const IntegerVector3D& grid_step,
200 const std::array<int, 3>& grid_dimensions,
201 const std::vector<KPoint>& physical_kpoints) {
202 ResolvedKPointNeighbor resolved;
203 resolved.neighbor_kpoint_index =
204 neighbor_kpoint_index(starting_kpoint_index, grid_step, grid_dimensions);
205
206 const Vector3D& starting_coordinates = physical_kpoints[starting_kpoint_index].coordinates;
207 const Vector3D& neighbor_coordinates =
208 physical_kpoints[resolved.neighbor_kpoint_index].coordinates;
209
210 for (int component = 0; component < 3; component++) {
211 const Real target =
212 starting_coordinates[component] + static_cast<Real>(grid_step[component]) /
213 static_cast<Real>(grid_dimensions[component]);
214 const Real difference = target - neighbor_coordinates[component];
215 const int shift_component = static_cast<int>(std::lround(difference));
216
217 if (std::abs(difference - static_cast<Real>(shift_component)) > 1e-6) {
218 throw std::runtime_error(
219 "KPointNeighborShells::resolve_neighbor: the k-point list does not "
220 "enumerate a full, unreduced Monkhorst-Pack grid with the given "
221 "dimensions in the expected order (first index slowest, third fastest, "
222 "as KPointsGrid and q3.workflows.interpolation.full_grid_kpoints produce) "
223 "-- the neighbor's coordinate does not differ from the target by an "
224 "integer number of reciprocal lattice vectors.");
225 }
226 resolved.periodic_image_shift[component] = shift_component;
227 }
228
229 return resolved;
230 }
231
232 private:
233 struct Shell {
234 std::vector<IntegerVector3D> grid_steps;
235 std::vector<Vector3D> translations;
236 Real magnitude_squared = 0.0;
237 };
238
239 struct LeastSquaresSolution {
240 std::vector<Real> weights;
241 Real residual_norm = 0.0;
242 bool smallest_singular_value_cleared_tolerance = false;
243 };
244
245 static int wrap_index(int raw_index, int dimension) {
246 int wrapped = raw_index % dimension;
247 if (wrapped < 0) {
248 wrapped += dimension;
249 }
250 return wrapped;
251 }
252
253 /**
254 * Sum, over every vector in the shell, of its outer product with
255 * itself, packed into the 6 independent symmetric components (xx, yy,
256 * zz, xy, xz, yz).
257 */
258 static std::array<Real, 6> shell_tensor(const Shell& shell) {
259 std::array<Real, 6> tensor = {0, 0, 0, 0, 0, 0};
260 for (const Vector3D& translation : shell.translations) {
261 tensor[0] += translation.x * translation.x;
262 tensor[1] += translation.y * translation.y;
263 tensor[2] += translation.z * translation.z;
264 tensor[3] += translation.x * translation.y;
265 tensor[4] += translation.x * translation.z;
266 tensor[5] += translation.y * translation.z;
267 }
268 return tensor;
269 }
270
271 /**
272 * Enumerate integer grid steps in the search cube, convert each to a
273 * Cartesian translation, and group them into shells of equal length
274 * (within `shell_relative_tolerance`), sorted by increasing length.
275 * Every shell is closed under grid_step -> -grid_step by construction,
276 * since the search cube is symmetric about the origin.
277 */
278 static std::vector<Shell>
279 enumerate_candidate_shells(const Matrix3x3& reciprocal_lattice_vectors,
280 const std::array<int, 3>& grid_dimensions,
281 int search_range,
282 Real shell_relative_tolerance) {
283 struct Candidate {
284 IntegerVector3D grid_step;
285 Vector3D translation;
286 Real magnitude_squared;
287 };
288
289 std::vector<Candidate> candidates;
290 for (int step_x = -search_range; step_x <= search_range; step_x++) {
291 for (int step_y = -search_range; step_y <= search_range; step_y++) {
292 for (int step_z = -search_range; step_z <= search_range; step_z++) {
293 if (step_x == 0 && step_y == 0 && step_z == 0) {
294 continue;
295 }
296 Vector3D translation = (static_cast<Real>(step_x) / grid_dimensions[0]) *
297 reciprocal_lattice_vectors[0] +
298 (static_cast<Real>(step_y) / grid_dimensions[1]) *
299 reciprocal_lattice_vectors[1] +
300 (static_cast<Real>(step_z) / grid_dimensions[2]) *
301 reciprocal_lattice_vectors[2];
302 candidates.push_back(
303 {{step_x, step_y, step_z}, translation, translation.norm2()});
304 }
305 }
306 }
307
308 /**
309 * Stable, so directions of equal length keep the enumeration order
310 * above. With std::sort the within-shell order -- hence
311 * `direction_index`, the `.nnkp`/`.mmn` block order and the record's
312 * bytes -- would depend on the standard library's introsort, i.e. on
313 * libc++ versus libstdc++.
314 */
315 std::stable_sort(candidates.begin(),
316 candidates.end(),
317 [](const Candidate& left, const Candidate& right) {
318 return left.magnitude_squared < right.magnitude_squared;
319 });
320
321 std::vector<Shell> shells;
322 for (const Candidate& candidate : candidates) {
323 if (!shells.empty() &&
324 std::abs(candidate.magnitude_squared - shells.back().magnitude_squared) <=
325 shell_relative_tolerance * shells.back().magnitude_squared) {
326 shells.back().grid_steps.push_back(candidate.grid_step);
327 shells.back().translations.push_back(candidate.translation);
328 continue;
329 }
330 Shell shell;
331 shell.grid_steps.push_back(candidate.grid_step);
332 shell.translations.push_back(candidate.translation);
333 shell.magnitude_squared = candidate.magnitude_squared;
334 shells.push_back(std::move(shell));
335 }
336 return shells;
337 }
338
339 /**
340 * Solve the (accumulated shell count) x 6 least-squares system
341 * A^T w = target for the per-shell weights w via the SVD-based
342 * pseudo-inverse, where each row of A is one shell's tensor. Also
343 * reports whether the smallest singular value of A (transposed to
344 * shells x 6) clears `singular_value_tolerance`, which is how a
345 * newly added shell is judged to contribute a genuinely new
346 * direction rather than one linearly dependent on the shells already
347 * present.
348 */
349 static LeastSquaresSolution
350 solve_least_squares(const std::vector<std::array<Real, 6>>& shell_tensors,
351 const std::array<Real, 6>& target,
352 Real singular_value_tolerance) {
353 const int number_of_shells = static_cast<int>(shell_tensors.size());
354 const int number_of_equations = 6;
355
356 /**
357 * Column-major A, number_of_equations rows x number_of_shells
358 * columns: column s holds shell_tensors[s].
359 */
360 std::vector<Real> matrix_a(static_cast<std::size_t>(number_of_equations) *
362 for (int shell_index = 0; shell_index < number_of_shells; shell_index++) {
363 for (int component = 0; component < number_of_equations; component++) {
364 matrix_a[shell_index * number_of_equations + component] =
365 shell_tensors[shell_index][component];
366 }
367 }
368
369 const int minimum_dimension = std::min(number_of_equations, number_of_shells);
370 std::vector<Real> singular_values(minimum_dimension);
371 std::vector<Real> left_singular_vectors(static_cast<std::size_t>(number_of_equations) *
372 number_of_equations);
373 std::vector<Real> right_singular_vectors_transposed(
374 static_cast<std::size_t>(number_of_shells) * number_of_shells);
375
376 std::vector<Real> matrix_a_copy = matrix_a;
377 int workspace_size = -1;
378 Real optimal_workspace_size = 0.0;
379 int info = 0;
380
381 lapack::dgesvd_("A",
382 "A",
383 &number_of_equations,
385 matrix_a_copy.data(),
386 &number_of_equations,
387 singular_values.data(),
388 left_singular_vectors.data(),
389 &number_of_equations,
390 right_singular_vectors_transposed.data(),
392 &optimal_workspace_size,
393 &workspace_size,
394 &info);
395 if (info != 0) {
396 throw std::runtime_error(
397 "KPointNeighborShells: DGESVD workspace query failed with info = " +
398 std::to_string(info));
399 }
400
401 workspace_size = static_cast<int>(optimal_workspace_size);
402 std::vector<Real> workspace(workspace_size);
403 matrix_a_copy = matrix_a;
404
405 lapack::dgesvd_("A",
406 "A",
407 &number_of_equations,
409 matrix_a_copy.data(),
410 &number_of_equations,
411 singular_values.data(),
412 left_singular_vectors.data(),
413 &number_of_equations,
414 right_singular_vectors_transposed.data(),
416 workspace.data(),
417 &workspace_size,
418 &info);
419 if (info != 0) {
420 throw std::runtime_error("KPointNeighborShells: DGESVD failed with info = " +
421 std::to_string(info));
422 }
423
424 const Real largest_singular_value = singular_values.empty() ? 0.0 : singular_values[0];
425 const Real smallest_singular_value =
426 singular_values.empty() ? 0.0 : singular_values[minimum_dimension - 1];
427
428 LeastSquaresSolution solution;
429 solution.smallest_singular_value_cleared_tolerance =
430 (number_of_shells <= number_of_equations) &&
431 (smallest_singular_value >
432 singular_value_tolerance * std::max(1.0, largest_singular_value));
433
434 /**
435 * Pseudo-inverse solve: w = V * Sigma^+ * U^T * target, truncating
436 * singular values at the tolerance so a still-degenerate system
437 * (more shells than independent directions) does not blow up.
438 * left_singular_vectors is U, column-major with leading dimension
439 * number_of_equations: U(equation_row, singular_index) is at
440 * flat[singular_index * number_of_equations + equation_row].
441 */
442 std::vector<Real> u_transpose_target(number_of_equations, 0.0);
443 for (int singular_index = 0; singular_index < number_of_equations; singular_index++) {
444 Real sum = 0.0;
445 for (int equation_row = 0; equation_row < number_of_equations; equation_row++) {
446 sum += left_singular_vectors[singular_index * number_of_equations + equation_row] *
447 target[equation_row];
448 }
449 u_transpose_target[singular_index] = sum;
450 }
451
452 /**
453 * right_singular_vectors_transposed is V**T, column-major with
454 * leading dimension number_of_shells: VT(singular_index,
455 * shell_index) = V(shell_index, singular_index) -- the
456 * shell_index-th component of the singular_index-th right
457 * singular vector -- is at flat[shell_index * number_of_shells +
458 * singular_index].
459 */
460 std::vector<Real> weights(number_of_shells, 0.0);
461 for (int shell_index = 0; shell_index < number_of_shells; shell_index++) {
462 Real sum = 0.0;
463 for (int singular_index = 0; singular_index < minimum_dimension; singular_index++) {
464 if (singular_values[singular_index] <=
465 singular_value_tolerance * std::max(1.0, largest_singular_value)) {
466 continue;
467 }
468 sum += right_singular_vectors_transposed[shell_index * number_of_shells +
469 singular_index] *
470 u_transpose_target[singular_index] / singular_values[singular_index];
471 }
472 weights[shell_index] = sum;
473 }
474
475 Real residual_squared = 0.0;
476 for (int component = 0; component < number_of_equations; component++) {
477 Real predicted = 0.0;
478 for (int shell_index = 0; shell_index < number_of_shells; shell_index++) {
479 predicted += shell_tensors[shell_index][component] * weights[shell_index];
480 }
481 Real component_residual = predicted - target[component];
482 residual_squared += component_residual * component_residual;
483 }
484
485 solution.weights = std::move(weights);
486 solution.residual_norm = std::sqrt(residual_squared);
487 return solution;
488 }
489
490 void finalize(const std::vector<const Shell*>& accepted_shells,
491 const std::vector<Real>& weights) {
492 for (int shell_index = 0; shell_index < static_cast<int>(accepted_shells.size());
493 shell_index++) {
494 const Shell& shell = *accepted_shells[shell_index];
495 const Real weight = weights[shell_index];
496 if (weight < -1e-8) {
497 throw std::runtime_error(
498 "KPointNeighborShells::compute: the least-squares solve produced a "
499 "negative shell weight (" +
500 std::to_string(weight) +
501 ") -- the completeness condition has no physically valid solution "
502 "with the shells found; widen search_range.");
503 }
504 for (std::size_t direction_index = 0; direction_index < shell.grid_steps.size();
505 direction_index++) {
506 neighbor_directions_.push_back({shell.grid_steps[direction_index],
507 shell.translations[direction_index],
508 weight});
509 }
510 }
511 number_of_shells_ = static_cast<int>(accepted_shells.size());
512 }
513
514 std::vector<KPointNeighborDirection> neighbor_directions_;
515 int number_of_shells_ = 0;
516};
517
518} // namespace q3
const std::vector< KPointNeighborDirection > & neighbor_directions() const
void compute(const Matrix3x3 &reciprocal_lattice_vectors, int grid_dimension_x, int grid_dimension_y, int grid_dimension_z, int search_range=3, int maximum_shell_count=12, Real shell_relative_tolerance=1e-6, Real singular_value_tolerance=1e-10, Real completeness_tolerance=1e-10)
static ResolvedKPointNeighbor resolve_neighbor(int starting_kpoint_index, const IntegerVector3D &grid_step, const std::array< int, 3 > &grid_dimensions, const std::vector< KPoint > &physical_kpoints)
static int neighbor_kpoint_index(int starting_kpoint_index, const IntegerVector3D &grid_step, const std::array< int, 3 > &grid_dimensions)
void dgesvd_(const char *jobu, const char *jobvt, const int *m, const int *n, double *a, const int *lda, double *s, double *u, const int *ldu, double *vt, const int *ldvt, double *work, const int *lwork, int *info)
void info(spdlog::format_string_t< Args... > fmt, Args &&... args)
Definition logger.hpp:183
double Real
Integer 3-vector for Miller indices / FFT grid indices.
3×3 matrix stored row-major: row i = mat[i]