Loading...
Searching...
No Matches
wigner_seitz_supercell.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * WignerSeitzSupercell — the real-space dual of a k-point mesh: the lattice
5 * vectors R inside the Wigner-Seitz cell of the supercell the mesh spans,
6 * with the degeneracy of each vector on that cell's boundary.
7 *
8 * This is geometry, not electronic structure: lattice vectors and three mesh
9 * dimensions in, a set of lattice vectors with weights out. It sits beside
10 * KPointNeighborShells, which is the same mesh's reciprocal-space
11 * neighbourhood, and for the same reason: both are properties the mesh
12 * induces on the crystal, and the computation that consumes them (the
13 * Wannier k -> R transform, in layer 4) is not what defines them.
14 *
15 * H(R) = (1/N_k) sum_k e^(-i k.R) H(k) is only well conditioned if the R set
16 * is chosen as the Wigner-Seitz cell of the SUPERCELL spanned by the k-point
17 * mesh -- i.e. every R kept is at least as close to the origin as to any
18 * other superlattice point. Taking a naive box of R instead puts weight on
19 * vectors that are periodic images of shorter ones, and the interpolated
20 * bands acquire spurious structure between the coarse k-points where nothing
21 * constrains them.
22 *
23 * Points exactly equidistant from several superlattice points sit on the
24 * Wigner-Seitz boundary and belong to all of them at once. They are kept, and
25 * carry a degeneracy weight equal to how many they are tied with, so the
26 * transform stays an exact inverse of the k-space one: dividing by the
27 * degeneracy shares such a point's contribution among its images rather than
28 * counting it several times. Getting this wrong is a classic source of
29 * interpolation error that is invisible at the coarse k-points (where the
30 * transform is exact by construction) and only shows up in between -- which
31 * is exactly where interpolation is supposed to be doing work.
32 *
33 * Both search boxes are DERIVED from the lattice rather than fixed. An
34 * earlier version searched candidates in a +-2-supercell box and tested them
35 * against a +-1-supercell box of images, which is right for isotropic meshes
36 * on the cells the tests used and wrong for anisotropic ones on oblique
37 * cells: an FCC primitive cell with a 4x4x2 mesh came out with total weight
38 * 32.33 instead of 32, and an 8x8x1 mesh kept the weight exactly right while
39 * missing six boundary vectors -- so the weight check alone cannot catch
40 * every version of this. The bounds below are rigorous:
41 *
42 * - Every point x of the Wigner-Seitz cell satisfies |x| <= D/2, where D is
43 * the longest body diagonal of the superlattice parallelepiped. (Write x
44 * in superlattice coordinates and round each to the nearest integer: the
45 * remainder is (1/2) sum_i e_i S_i with |e_i| <= 1, whose norm is maximised
46 * at a vertex of the cube, i.e. on a body diagonal. That nearest vertex is
47 * a superlattice point, and x is at least as close to the origin as to it.)
48 * - A lattice vector with |R| <= D/2 has superlattice coefficient
49 * |c_i| = |R . S*_i| <= (D/2) |S*_i|, with S*_i the dual vectors
50 * (S_i . S*_j = delta_ij). That bounds the candidate box per axis.
51 * - The same rounding argument puts every point within D/2 of SOME
52 * superlattice point, so the nearest image T* of any candidate satisfies
53 * |T*| <= |R| + D/2; images that tie for nearest are no farther. That
54 * bounds the image box from the largest candidate norm in the box.
55 *
56 * The identity sum_R 1/degeneracy = N_k is then asserted at the end of
57 * compute(): it holds for every correct Wigner-Seitz construction, so a
58 * violation is a bug in this class, not a property of the input, and it is
59 * refused rather than logged.
60 */
61
62#include <algorithm>
63#include <array>
64#include <cmath>
65#include <stdexcept>
66#include <string>
67#include <vector>
68
70
71namespace q3 {
72
73/// One retained lattice vector with the number of images it ties with.
74struct [[nodiscard]] WignerSeitzLatticeVector {
76 /// Cartesian R in the same units as the lattice vectors handed in.
78 int degeneracy = 1;
79};
80
82 public:
83 /**
84 * Build the R set for a `grid_dimension_x` x `..._y` x `..._z` k-mesh.
85 *
86 * `lattice_vectors` rows are the primitive real-space vectors a1, a2, a3;
87 * the supercell is grid_dimension_i * a_i.
88 *
89 * `distance_tolerance` decides when two distances count as equal, i.e.
90 * when a point is on the boundary. It is relative to the shortest
91 * superlattice vector so it transfers across cell sizes.
92 */
94 int grid_dimension_x,
95 int grid_dimension_y,
96 int grid_dimension_z,
97 Real distance_tolerance = 1e-6) {
98 lattice_vectors_.clear();
99 if (grid_dimension_x <= 0 || grid_dimension_y <= 0 || grid_dimension_z <= 0) {
100 throw std::invalid_argument(
101 "WignerSeitzSupercell::compute: every grid dimension must be positive");
102 }
103 const std::array<int, 3> grid_dimensions = {
104 grid_dimension_x, grid_dimension_y, grid_dimension_z};
105 const int number_of_kpoints = grid_dimension_x * grid_dimension_y * grid_dimension_z;
106
107 Matrix3x3 superlattice;
108 for (int axis = 0; axis < 3; axis++) {
109 superlattice[axis] = lattice_vectors[axis] * static_cast<Real>(grid_dimensions[axis]);
110 }
111 if (std::abs(superlattice.determinant()) <= 0.0) {
112 throw std::invalid_argument(
113 "WignerSeitzSupercell::compute: the lattice vectors are linearly dependent");
114 }
115
116 /**
117 * The longest body diagonal of the superlattice parallelepiped. The
118 * sign of the first vector is fixed by inversion symmetry, so four
119 * diagonals cover all eight.
120 */
121 Real longest_diagonal = 0.0;
122 for (Real sign_y : {-1.0, 1.0}) {
123 for (Real sign_z : {-1.0, 1.0}) {
124 Vector3D diagonal =
125 superlattice[0] + superlattice[1] * sign_y + superlattice[2] * sign_z;
126 longest_diagonal = std::max(longest_diagonal, diagonal.norm());
127 }
128 }
129 const Real ball_radius = 0.5 * longest_diagonal;
130
131 // Dual vectors S*_j are the columns of the inverse of the row matrix.
132 const Matrix3x3 inverse_superlattice = superlattice.inverse();
133 std::array<Real, 3> dual_norm = {0.0, 0.0, 0.0};
134 for (int axis = 0; axis < 3; axis++) {
135 Vector3D dual(inverse_superlattice[0][axis],
136 inverse_superlattice[1][axis],
137 inverse_superlattice[2][axis]);
138 dual_norm[axis] = dual.norm();
139 }
140
141 Real shortest_superlattice_length = superlattice[0].norm();
142 for (int axis = 1; axis < 3; axis++) {
143 shortest_superlattice_length =
144 std::min(shortest_superlattice_length, superlattice[axis].norm());
145 }
146 const Real absolute_tolerance = distance_tolerance * shortest_superlattice_length;
147
148 /**
149 * Candidate box: lattice vectors whose norm can be at most D/2. The
150 * bound is on the superlattice coefficient, so it scales by the grid
151 * dimension to become a bound on the lattice index.
152 */
153 std::array<int, 3> candidate_bound = {0, 0, 0};
154 for (int axis = 0; axis < 3; axis++) {
155 candidate_bound[axis] =
156 std::max(1,
157 static_cast<int>(std::floor(
158 grid_dimensions[axis] * ball_radius * dual_norm[axis] + 1e-9)));
159 }
160
161 // The norm is convex, so the farthest candidate in the box is a corner.
162 Real farthest_candidate = 0.0;
163 for (Real sign_y : {-1.0, 1.0}) {
164 for (Real sign_z : {-1.0, 1.0}) {
165 Vector3D corner = lattice_vectors[0] * static_cast<Real>(candidate_bound[0]) +
166 lattice_vectors[1] * (sign_y * candidate_bound[1]) +
167 lattice_vectors[2] * (sign_z * candidate_bound[2]);
168 farthest_candidate = std::max(farthest_candidate, corner.norm());
169 }
170 }
171
172 /**
173 * Image box: every superlattice point that can be as close to some
174 * candidate as the origin is. The larger of the two bounds covers
175 * both the nearest-image argument and the tie argument.
176 */
177 const Real reach =
178 std::max(farthest_candidate + ball_radius, longest_diagonal) + absolute_tolerance;
179 std::array<int, 3> image_bound = {0, 0, 0};
180 for (int axis = 0; axis < 3; axis++) {
181 image_bound[axis] =
182 std::max(1, static_cast<int>(std::ceil(reach * dual_norm[axis] - 1e-9)));
183 }
184
185 std::vector<Vector3D> superlattice_translations;
186 for (int image_x = -image_bound[0]; image_x <= image_bound[0]; image_x++) {
187 for (int image_y = -image_bound[1]; image_y <= image_bound[1]; image_y++) {
188 for (int image_z = -image_bound[2]; image_z <= image_bound[2]; image_z++) {
189 superlattice_translations.push_back(
190 superlattice[0] * static_cast<Real>(image_x) +
191 superlattice[1] * static_cast<Real>(image_y) +
192 superlattice[2] * static_cast<Real>(image_z));
193 }
194 }
195 }
196
197 for (int index_x = -candidate_bound[0]; index_x <= candidate_bound[0]; index_x++) {
198 for (int index_y = -candidate_bound[1]; index_y <= candidate_bound[1]; index_y++) {
199 for (int index_z = -candidate_bound[2]; index_z <= candidate_bound[2]; index_z++) {
200 Vector3D candidate = lattice_vectors[0] * static_cast<Real>(index_x) +
201 lattice_vectors[1] * static_cast<Real>(index_y) +
202 lattice_vectors[2] * static_cast<Real>(index_z);
203
204 Real own_distance_squared = candidate.norm2();
205 Real minimum_distance_squared = own_distance_squared;
206 for (const Vector3D& translation : superlattice_translations) {
207 Vector3D shifted = candidate - translation;
208 minimum_distance_squared =
209 std::min(minimum_distance_squared, shifted.norm2());
210 }
211
212 Real own_distance = std::sqrt(own_distance_squared);
213 Real minimum_distance = std::sqrt(minimum_distance_squared);
214 if (own_distance > minimum_distance + absolute_tolerance) {
215 continue; // a periodic image sits strictly closer
216 }
217
218 int degeneracy = 0;
219 for (const Vector3D& translation : superlattice_translations) {
220 Vector3D shifted = candidate - translation;
221 if (std::abs(shifted.norm() - minimum_distance) <= absolute_tolerance) {
222 degeneracy++;
223 }
224 }
225 lattice_vectors_.push_back(
226 {IntegerVector3D(index_x, index_y, index_z), candidate, degeneracy});
227 }
228 }
229 }
230
231 if (lattice_vectors_.empty()) {
232 throw std::runtime_error(
233 "WignerSeitzSupercell::compute: no lattice vectors retained -- the search range "
234 "cannot cover the Wigner-Seitz cell of this lattice");
235 }
236
237 const Real weight = total_weight();
238 if (std::abs(weight - static_cast<Real>(number_of_kpoints)) >
239 1e-8 * static_cast<Real>(number_of_kpoints)) {
240 throw std::runtime_error(
241 "WignerSeitzSupercell::compute: the retained set sums to a weight of " +
242 std::to_string(weight) + " but the " + std::to_string(grid_dimension_x) + "x" +
243 std::to_string(grid_dimension_y) + "x" + std::to_string(grid_dimension_z) +
244 " mesh has " + std::to_string(number_of_kpoints) +
245 " k-points. The two must agree for the k -> R -> k transform to be an "
246 "identity; this is a defect in the Wigner-Seitz construction, not in the input.");
247 }
248 }
249
250 [[nodiscard]] const std::vector<WignerSeitzLatticeVector>& lattice_vectors() const {
251 return lattice_vectors_;
252 }
253
254 [[nodiscard]] int number_of_lattice_vectors() const {
255 return static_cast<int>(lattice_vectors_.size());
256 }
257
258 /**
259 * Sum of 1/degeneracy over the retained set. For a correct Wigner-Seitz
260 * construction this equals the number of k-points exactly: each of the
261 * N_k cells of the supercell is covered once, with boundary points
262 * sharing their coverage. compute() refuses to return a set for which it
263 * does not hold; callers may still use it to check that a supercell was
264 * built for the mesh they are about to transform over.
265 */
266 [[nodiscard]] Real total_weight() const {
267 Real total = 0.0;
268 for (const WignerSeitzLatticeVector& entry : lattice_vectors_) {
269 total += 1.0 / static_cast<Real>(entry.degeneracy);
270 }
271 return total;
272 }
273
274 private:
275 std::vector<WignerSeitzLatticeVector> lattice_vectors_;
276};
277
278} // namespace q3
const std::vector< WignerSeitzLatticeVector > & lattice_vectors() const
void compute(const Matrix3x3 &lattice_vectors, int grid_dimension_x, int grid_dimension_y, int grid_dimension_z, Real distance_tolerance=1e-6)
double Real
Integer 3-vector for Miller indices / FFT grid indices.
3×3 matrix stored row-major: row i = mat[i]
Real determinant() const
Determinant.
Matrix3x3 inverse() const
Inverse (assumes non-singular)
Real norm2() const
Definition vector_3d.hpp:65
Real norm() const
Definition vector_3d.hpp:68
One retained lattice vector with the number of images it ties with.
Vector3D cartesian
Cartesian R in the same units as the lattice vectors handed in.