Loading...
Searching...
No Matches
coefficients.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Gaunt coefficients for real spherical harmonics.
5 *
6 * Gaunt coefficients are the integrals of three spherical harmonics:
7 * ap(lp, lm1, lm2) = ∫ Y_lp(Ω) Y_lm1(Ω) Y_lm2(Ω) dΩ
8 *
9 * They appear in the angular expansion of augmentation charges (USPP)
10 * and are a pure mathematical quantity with no physics dependencies.
11 *
12 * The table is built by exact collocation. A product of two harmonics of
13 * degree <= L is a polynomial on the sphere of degree <= 2L, so it lies
14 * exactly in the span of the (2L+1)^2 harmonics of degree <= 2L. Sampling
15 * that many directions therefore determines the expansion coefficients with
16 * no quadrature error: evaluate the harmonics at the directions, invert the
17 * resulting square matrix, and apply the inverse to the sampled products.
18 *
19 * Collocation is one of several exact routes -- closed forms via
20 * Clebsch-Gordan coefficients, and quadrature rules that integrate a triple
21 * product exactly, both work. Which route this file should use is an open
22 * review item; the reason it uses this one is that it replaced a
23 * 10,000-point quadrature and is ~75x faster than that quadrature was.
24 */
25
26#include <algorithm>
27#include <cmath>
28#include <cstdlib>
29#include <stdexcept>
30#include <string>
31
32#include <vector>
33
37
38namespace q3 {
39
40/**
41 * Pre-computed Gaunt coefficient table.
42 *
43 * Stores the sparse Gaunt coefficient tensor for a given maximum
44 * degree. The sparsity tables (lpx, lpl) allow efficient iteration
45 * over non-zero entries.
46 */
48 /**
49 * Gaunt coefficients: ap[lp][lm1][lm2] = ∫ Y_lp Y_lm1 Y_lm2 dΩ
50 * Dimensions: [nlm_product][nlm_projector][nlm_projector]
51 * where nlm_product = (2*lmax+1)^2 and nlm_projector = (lmax+1)^2
52 */
53 std::vector<std::vector<std::vector<Real>>> coefficients;
54
55 /**
56 * Number of non-zero lp entries for each (lm1, lm2) pair.
57 * Dimensions: [nlm_projector][nlm_projector]
58 */
59 std::vector<std::vector<int>> number_of_nonzero_product_indices;
60
61 /**
62 * List of non-zero lp indices for each (lm1, lm2) pair.
63 * Dimensions: [nlm_projector][nlm_projector][variable]
64 */
65 std::vector<std::vector<std::vector<int>>> nonzero_product_indices;
66};
67
68/**
69 * Invert a square matrix in-place using Gauss-Jordan elimination.
70 * Returns the inverse in 'inverse'. The input matrix 'matrix' is modified.
71 *
72 * @param matrix [in/out] The matrix to invert (modified in-place)
73 * @param inverse [out] The inverse matrix
74 * @param dimension The matrix dimension
75 */
76inline void invert_matrix(std::vector<std::vector<Real>>& matrix,
77 std::vector<std::vector<Real>>& inverse,
78 int dimension) {
79 // Initialize inverse as identity
80 inverse.resize(dimension, std::vector<Real>(dimension, 0.0));
81 for (int row_index = 0; row_index < dimension; row_index++) {
82 inverse[row_index][row_index] = 1.0;
83 }
84
85 // Gauss-Jordan elimination with partial pivoting
86 for (int column_index = 0; column_index < dimension; column_index++) {
87 // Find pivot
88 int pivot_row = column_index;
89 Real max_value = std::abs(matrix[column_index][column_index]);
90 for (int row_index = column_index + 1; row_index < dimension; row_index++) {
91 if (std::abs(matrix[row_index][column_index]) > max_value) {
92 max_value = std::abs(matrix[row_index][column_index]);
93 pivot_row = row_index;
94 }
95 }
96
97 // Swap rows if needed
98 if (pivot_row != column_index) {
99 std::swap(matrix[column_index], matrix[pivot_row]);
100 std::swap(inverse[column_index], inverse[pivot_row]);
101 }
102
103 Real pivot_value = matrix[column_index][column_index];
104 if (std::abs(pivot_value) < 1e-15) {
105 continue;
106 }
107
108 // Scale pivot row
109 for (int element_index = 0; element_index < dimension; element_index++) {
110 matrix[column_index][element_index] /= pivot_value;
111 inverse[column_index][element_index] /= pivot_value;
112 }
113
114 // Eliminate column
115 for (int row_index = 0; row_index < dimension; row_index++) {
116 if (row_index == column_index)
117 continue;
118 Real factor = matrix[row_index][column_index];
119 for (int element_index = 0; element_index < dimension; element_index++) {
120 matrix[row_index][element_index] -= factor * matrix[column_index][element_index];
121 inverse[row_index][element_index] -= factor * inverse[column_index][element_index];
122 }
123 }
124 }
125}
126
127/**
128 * Generate deterministic unit vectors on the sphere using a Fibonacci-like spiral.
129 * Provides well-distributed points without any randomness.
130 *
131 * @param number_of_points Number of unit vectors to generate
132 * @return Vector of unit direction vectors
133 */
134inline std::vector<Vector3D> generate_deterministic_unit_vectors(int number_of_points) {
135 std::vector<Vector3D> directions(number_of_points);
136 // Use golden-ratio spiral for deterministic, well-distributed points
137 Real golden_ratio = constants::pi * (1.0 + std::sqrt(5.0));
138 for (int point_index = 0; point_index < number_of_points; point_index++) {
139 Real theta = std::acos(1.0 - 2.0 * (point_index + 0.5) / number_of_points);
140 Real phi = golden_ratio * point_index;
141 directions[point_index] = Vector3D(
142 std::sin(theta) * std::cos(phi), std::sin(theta) * std::sin(phi), std::cos(theta));
143 }
144 return directions;
145}
146
147/**
148 * Generate Gaunt coefficients for spherical harmonics up to a given max degree.
149 *
150 * Exact collocation: evaluate the harmonics at as many sphere directions as
151 * there are product harmonics, invert that square matrix, and apply the
152 * inverse to the sampled products. Exact rather than approximate because the
153 * product lies in the sampled span; O(n^3) in the product-space dimension
154 * rather than O(N_quad * nlm^2 * nlm_prod) for a quadrature.
155 *
156 * LIMIT: this needs harmonics up to degree 2*max_degree, and
157 * compute_real_spherical_harmonic implements degrees 0..4 and returns 0.0
158 * above that. For max_degree > 2 the collocation matrix is therefore singular
159 * by construction, the inverter skips those pivots, and entries whose two
160 * projector degrees sum above 4 come out wrong with no error raised. Entries
161 * at or below that sum stay exact. Callers must keep 2*max_degree <= 4 until
162 * the harmonics reach l = 5, 6.
163 *
164 * @param max_degree Maximum degree for projectors (nlm_proj = (max_degree+1)^2)
165 * @return GauntCoefficientTable with coefficients up to product L = 2*max_degree
166 */
169
170 int nlm_projector = (max_degree + 1) * (max_degree + 1);
171 int max_product_degree = 2 * max_degree;
172 int nlm_product = (max_product_degree + 1) * (max_product_degree + 1);
173
174 /**
175 * `max_degree` is the degree of the *projector* harmonics whose pairwise
176 * products this table expands. The collocation below therefore evaluates
177 * harmonics up to `2 * max_degree`, and compute_real_spherical_harmonic
178 * has closed forms only to degree 4.
179 *
180 * Checking here rather than letting the harmonic call refuse is what
181 * names the real bound to the caller: the limit on this table is a
182 * projector degree of 2, not a harmonic degree of 4, and a message about
183 * degree 5 would send a reader looking in the wrong place. A d-channel
184 * dataset sits exactly at the boundary; an f-channel one is over it and
185 * is refused rather than silently given a table whose sampling matrix is
186 * singular in the rows the missing harmonics should have filled.
187 */
188 constexpr int highest_implemented_harmonic_degree = 4;
189 if (max_product_degree > highest_implemented_harmonic_degree) {
190 throw std::invalid_argument(
191 "generate_gaunt_coefficients: a projector degree of " + std::to_string(max_degree) +
192 " needs harmonics up to degree " + std::to_string(max_product_degree) +
193 ", above the highest implemented (" +
194 std::to_string(highest_implemented_harmonic_degree) +
195 "). The collocation matrix would be singular in the missing rows and the "
196 "table silently wrong, so no table is built. Q3 supports projector "
197 "channels up to d.");
198 }
199
200 table.coefficients.resize(nlm_product);
201 for (auto& ap_row : table.coefficients) {
202 ap_row.resize(nlm_projector);
203 for (auto& ap_element : ap_row)
204 ap_element.resize(nlm_projector, 0.0);
205 }
206 table.number_of_nonzero_product_indices.resize(nlm_projector);
207 table.nonzero_product_indices.resize(nlm_projector);
208 for (int lm1 = 0; lm1 < nlm_projector; lm1++) {
209 table.number_of_nonzero_product_indices[lm1].resize(nlm_projector, 0);
210 table.nonzero_product_indices[lm1].resize(nlm_projector);
211 }
212
213 /**
214 * One sample direction per product harmonic, and exactly that many.
215 * Exact collocation of a function known to lie in an n-dimensional span
216 * needs a square n x n sample matrix to invert, and n is nlm_product --
217 * so equality is forced, not chosen. It is also the only value this code
218 * admits: invert_matrix is called with dimension llx against a matrix
219 * allocated nlm_product square.
220 *
221 * The directions are deterministic (a golden-ratio spiral), not drawn
222 * from a generator; test_coefficients.cpp pins that.
223 */
224 int llx = nlm_product;
225 auto directions = generate_deterministic_unit_vectors(llx);
226
227 /**
228 * Compute Y_lm for all random vectors: ylm[point][lm]
229 * We need up to max_product_degree for the product Y_lm values
230 */
231 std::vector<std::vector<Real>> ylm_at_points(llx, std::vector<Real>(nlm_product, 0.0));
232 for (int point_index = 0; point_index < llx; point_index++) {
233 for (int degree = 0; degree <= max_product_degree; degree++) {
234 for (int order : harmonic_order_sequence(degree)) {
235 int lm_index = harmonic_combined_degree_order_index(degree, order);
236 ylm_at_points[point_index][lm_index] =
237 compute_real_spherical_harmonic(degree, order, directions[point_index]);
238 }
239 }
240 }
241
242 /**
243 * Copy the sampled harmonics into the square matrix to be inverted,
244 * indexed [point][lm]. No transposition happens here or is needed: the
245 * inverse of a matrix indexed [point][lm] is naturally indexed
246 * [lm][point], which is how the contraction below reads it.
247 */
248 std::vector<std::vector<Real>> ylm_matrix(nlm_product, std::vector<Real>(nlm_product, 0.0));
249 for (int point_index = 0; point_index < llx; point_index++) {
250 for (int lm_index = 0; lm_index < nlm_product; lm_index++) {
251 ylm_matrix[point_index][lm_index] = ylm_at_points[point_index][lm_index];
252 }
253 }
254
255 /**
256 * Invert the Y_lm matrix: mly = ylm^(-1)
257 * mly[lm_product][point] = inverse of ylm[point][lm_product]
258 */
259 std::vector<std::vector<Real>> mly;
260 invert_matrix(ylm_matrix, mly, llx);
261
262 /**
263 * Apply the inverse to the sampled products. Since the harmonic matrix
264 * maps coefficients to sampled values, its inverse maps sampled values
265 * back to coefficients, and the samples of Y_i * Y_j are just the
266 * pointwise products of the two columns:
267 *
268 * coefficients[LM][i][j] = sum over directions of
269 * inverse[LM][direction] * Y_i(direction) * Y_j(direction)
270 *
271 * i and j range over the projector harmonics, LM over the product ones.
272 */
273 for (int lm1 = 0; lm1 < nlm_projector; lm1++) {
274 for (int lm2 = 0; lm2 < nlm_projector; lm2++) {
275 for (int lp = 0; lp < nlm_product; lp++) {
276 Real sum = 0.0;
277 for (int point_index = 0; point_index < llx; point_index++) {
278 sum += mly[lp][point_index] * ylm_at_points[point_index][lm1] *
279 ylm_at_points[point_index][lm2];
280 }
281 table.coefficients[lp][lm1][lm2] = sum;
282 }
283 }
284 }
285
286 // Sparsify: record non-zero entries
287 for (int lm1 = 0; lm1 < nlm_projector; lm1++) {
288 for (int lm2 = 0; lm2 < nlm_projector; lm2++) {
289 for (int lp = 0; lp < nlm_product; lp++) {
290 if (std::abs(table.coefficients[lp][lm1][lm2]) >
292 table.nonzero_product_indices[lm1][lm2].push_back(lp);
293 table.number_of_nonzero_product_indices[lm1][lm2]++;
294 } else {
295 table.coefficients[lp][lm1][lm2] = 0.0;
296 }
297 }
298 }
299 }
300
301 return table;
302}
303
304} // namespace q3
constexpr Real pi
Definition constants.hpp:24
constexpr double gaunt_coefficient_threshold
int harmonic_combined_degree_order_index(int degree, int order)
Definition harmonics.hpp:61
double Real
Real compute_real_spherical_harmonic(int degree, int order, const Vector3D &direction)
Definition harmonics.hpp:85
std::vector< Vector3D > generate_deterministic_unit_vectors(int number_of_points)
void invert_matrix(std::vector< std::vector< Real > > &matrix, std::vector< std::vector< Real > > &inverse, int dimension)
GauntCoefficientTable generate_gaunt_coefficients(int max_degree)
std::vector< int > harmonic_order_sequence(int degree)
Definition harmonics.hpp:40
std::vector< std::vector< std::vector< Real > > > coefficients
std::vector< std::vector< std::vector< int > > > nonzero_product_indices
std::vector< std::vector< int > > number_of_nonzero_product_indices