Loading...
Searching...
No Matches
lattice_fourier_transform.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * lattice_fourier_transform — the discrete Fourier pair between a k-point set
5 * and a set of lattice vectors.
6 *
7 * f(R) = (1/N_k) sum_k e^{-2 pi i k.R} f(k) to_real_space()
8 * f(k) = sum_R w_R e^{+2 pi i k.R} f(R) to_kpoint()
9 *
10 * k-points and translations are both in crystal coordinates, so the phase is
11 * the plain dot product with no lattice matrix in sight -- which is why this
12 * belongs beside the k-point grid and the Wigner-Seitz supercell rather than
13 * inside any one consumer.
14 *
15 * Values are flat blocks of arbitrary, equal size, transformed element by
16 * element: a matrix (Wannier interpolation transforms U^dagger E U) and a
17 * single band energy (a Fourier star-function fit; a block of one) go through
18 * the same code.
19 *
20 * This is NOT an FFT and deliberately does not use the FFT machinery in
21 * layer 3. The forward transform lands on a Wigner-Seitz set with fractional
22 * boundary weights rather than on a rectangular box, and the inverse is
23 * evaluated at arbitrary k off any grid, which an FFT cannot do at all. At the
24 * sizes involved -- hundreds of k-points, blocks of a few orbitals -- a direct
25 * sum costs microseconds and an FFT plan would cost more than the work.
26 *
27 * The weights belong to the inverse alone. The forward transform divides by
28 * the k-point count; the degeneracy weights of a Wigner-Seitz boundary vector
29 * enter when the sum over R is taken, and their total must come back to the
30 * k-point count -- the identity every consumer of a transformed model checks.
31 */
32
33#include <cmath>
34#include <cstddef>
35#include <stdexcept>
36#include <string>
37#include <vector>
38
41
43
44namespace internal {
45
46/// e^{sign * 2 pi i k.R} for crystal-coordinate k and integer R.
47[[nodiscard]] inline Complex
48phase(const Vector3D& kpoint_crystal, const IntegerVector3D& translation, Real sign) {
49 const Real angle = sign * constants::two_pi *
50 (kpoint_crystal.x * static_cast<Real>(translation.x) +
51 kpoint_crystal.y * static_cast<Real>(translation.y) +
52 kpoint_crystal.z * static_cast<Real>(translation.z));
53 return {std::cos(angle), std::sin(angle)};
54}
55
56} // namespace internal
57
58/**
59 * f(R) = (1/N_k) sum_k e^{-2 pi i k.R} f(k), one block per lattice vector.
60 *
61 * `values_at_kpoints` holds one equally sized block per k-point, in the order
62 * of `kpoints_crystal`. The k-set must be the full, unreduced grid the model
63 * was built on: a symmetry-reduced set would weight the sum wrongly, and this
64 * function has no way to detect that, so its callers own that guarantee.
65 */
66[[nodiscard]] inline std::vector<std::vector<Complex>>
67to_real_space(const std::vector<Vector3D>& kpoints_crystal,
68 const std::vector<std::vector<Complex>>& values_at_kpoints,
69 const std::vector<IntegerVector3D>& lattice_translations) {
70 const std::size_t number_of_kpoints = kpoints_crystal.size();
71 if (number_of_kpoints == 0) {
72 throw std::invalid_argument("lattice_fourier_transform::to_real_space: no k-points");
73 }
74 if (values_at_kpoints.size() != number_of_kpoints) {
75 throw std::invalid_argument(
76 "lattice_fourier_transform::to_real_space: " + std::to_string(number_of_kpoints) +
77 " k-points but " + std::to_string(values_at_kpoints.size()) + " value blocks");
78 }
79 if (lattice_translations.empty()) {
80 throw std::invalid_argument("lattice_fourier_transform::to_real_space: no lattice vectors");
81 }
82 const std::size_t block_size = values_at_kpoints[0].size();
83 if (block_size == 0) {
84 throw std::invalid_argument("lattice_fourier_transform::to_real_space: empty value block");
85 }
86 for (std::size_t kpoint_index = 1; kpoint_index < number_of_kpoints; kpoint_index++) {
87 if (values_at_kpoints[kpoint_index].size() != block_size) {
88 throw std::invalid_argument(
89 "lattice_fourier_transform::to_real_space: the block at k-point " +
90 std::to_string(kpoint_index) + " holds " +
91 std::to_string(values_at_kpoints[kpoint_index].size()) + " values, not " +
92 std::to_string(block_size) + " as the first one does");
93 }
94 }
95
96 const Real inverse_kpoint_count = 1.0 / static_cast<Real>(number_of_kpoints);
97 std::vector<std::vector<Complex>> values_at_lattice_vectors(
98 lattice_translations.size(), std::vector<Complex>(block_size, Complex(0.0, 0.0)));
99
100 /**
101 * k outermost so each k-point's phase is computed once per lattice vector
102 * rather than once per element, and the block is streamed contiguously.
103 */
104 for (std::size_t kpoint_index = 0; kpoint_index < number_of_kpoints; kpoint_index++) {
105 const std::vector<Complex>& block_at_kpoint = values_at_kpoints[kpoint_index];
106 for (std::size_t vector_index = 0; vector_index < lattice_translations.size();
107 vector_index++) {
108 const Complex weighted_phase = internal::phase(kpoints_crystal[kpoint_index],
109 lattice_translations[vector_index],
110 -1.0) *
111 inverse_kpoint_count;
112 std::vector<Complex>& block_at_lattice_vector = values_at_lattice_vectors[vector_index];
113 for (std::size_t element = 0; element < block_size; element++) {
114 block_at_lattice_vector[element] += weighted_phase * block_at_kpoint[element];
115 }
116 }
117 }
118 return values_at_lattice_vectors;
119}
120
121/**
122 * f(k) = sum_R w_R e^{+2 pi i k.R} f(R), the inverse, at one arbitrary k.
123 *
124 * `kpoint_crystal` need not lie on any mesh -- evaluating between the mesh
125 * points is what an interpolated model is for. `weights` are the Bloch-sum
126 * weights, 1/degeneracy for a Wigner-Seitz boundary vector and 1 otherwise;
127 * they are passed alongside the translations rather than as a struct so that a
128 * caller storing them in its own type needs no per-call conversion on what is,
129 * for a dense mesh sampling, the hot path.
130 */
131[[nodiscard]] inline std::vector<Complex>
132to_kpoint(const Vector3D& kpoint_crystal,
133 const std::vector<std::vector<Complex>>& values_at_lattice_vectors,
134 const std::vector<IntegerVector3D>& lattice_translations,
135 const std::vector<Real>& weights) {
136 const std::size_t number_of_vectors = lattice_translations.size();
137 if (number_of_vectors == 0) {
138 throw std::invalid_argument("lattice_fourier_transform::to_kpoint: no lattice vectors");
139 }
140 if (values_at_lattice_vectors.size() != number_of_vectors ||
141 weights.size() != number_of_vectors) {
142 throw std::invalid_argument(
143 "lattice_fourier_transform::to_kpoint: " + std::to_string(number_of_vectors) +
144 " lattice vectors but " + std::to_string(values_at_lattice_vectors.size()) +
145 " value blocks and " + std::to_string(weights.size()) + " weights");
146 }
147 const std::size_t block_size = values_at_lattice_vectors[0].size();
148 if (block_size == 0) {
149 throw std::invalid_argument("lattice_fourier_transform::to_kpoint: empty value block");
150 }
151
152 std::vector<Complex> value_at_kpoint(block_size, Complex(0.0, 0.0));
153 for (std::size_t vector_index = 0; vector_index < number_of_vectors; vector_index++) {
154 const std::vector<Complex>& block = values_at_lattice_vectors[vector_index];
155 if (block.size() != block_size) {
156 throw std::invalid_argument(
157 "lattice_fourier_transform::to_kpoint: the block at lattice vector " +
158 std::to_string(vector_index) + " holds " + std::to_string(block.size()) +
159 " values, not " + std::to_string(block_size) + " as the first one does");
160 }
161 const Complex weighted_phase =
162 internal::phase(kpoint_crystal, lattice_translations[vector_index], 1.0) *
163 weights[vector_index];
164 for (std::size_t element = 0; element < block_size; element++) {
165 value_at_kpoint[element] += weighted_phase * block[element];
166 }
167 }
168 return value_at_kpoint;
169}
170
171} // namespace q3::lattice_fourier_transform
constexpr Real two_pi
Definition constants.hpp:25
Complex phase(const Vector3D &kpoint_crystal, const IntegerVector3D &translation, Real sign)
e^{sign * 2 pi i k.R} for crystal-coordinate k and integer R.
std::vector< std::vector< Complex > > to_real_space(const std::vector< Vector3D > &kpoints_crystal, const std::vector< std::vector< Complex > > &values_at_kpoints, const std::vector< IntegerVector3D > &lattice_translations)
std::vector< Complex > to_kpoint(const Vector3D &kpoint_crystal, const std::vector< std::vector< Complex > > &values_at_lattice_vectors, const std::vector< IntegerVector3D > &lattice_translations, const std::vector< Real > &weights)
double Real
std::complex< double > Complex
Integer 3-vector for Miller indices / FFT grid indices.