Triumvirate C++ API 0.5.0.post1.dev301+g026f21751
Three-point clustering measurements in large-scale structure analyses.
Loading...
Searching...
No Matches
maths.cpp
Go to the documentation of this file.
1// Triumvirate: Three-Point Clustering Measurements in LSS
2//
3// Copyright (C) 2023 Mike S Wang & Naonori S Sugiyama [GPL-3.0-or-later]
4//
5// This file is part of the Triumvirate program. See the COPYRIGHT
6// and LICENCE files at the top-level directory of this distribution
7// for details of copyright and licensing.
8//
9// This program is free software: you can redistribute it and/or modify it
10// under the terms of the GNU General Public License as published by
11// the Free Software Foundation, either version 3 of the License, or
12// (at your option) any later version.
13//
14// This program is distributed in the hope that it will be useful,
15// but WITHOUT ANY WARRANTY; without even the implied warranty of
16// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
17// See the GNU General Public License for more details.
18//
19// You should have received a copy of the GNU General Public License
20// along with this program. If not, see <https://www.gnu.org/licenses/>.
21
28
29#include "maths.hpp"
30
31namespace trvs = trv::sys;
32
33namespace trv {
34namespace maths {
35
36// ***********************************************************************
37// Complex numbers
38// ***********************************************************************
39
41const std::complex<double> M_I(0., 1.);
43
44std::complex<double> eval_complex_in_polar(double r, double theta) {
45 return r * (std::cos(theta) + M_I * std::sin(theta));
46}
47
48
49// ***********************************************************************
50// Vectors
51// ***********************************************************************
52
53double get_vec3d_magnitude(std::vector<double> vec) {
54 return std::sqrt(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]);
55}
56
57double get_vec3d_magnitude(double* vec) {
58 return std::sqrt(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]);
59}
60
61
62// ***********************************************************************
63// Gamma function
64// ***********************************************************************
65
66// -----------------------------------------------------------------------
67// Lanzcos approximation
68// -----------------------------------------------------------------------
69
71const double gconst_lanczos = 7.;
72
73std::complex<double> eval_lanczos_approx_series(std::complex<double> z) {
74 // CAVEAT: Discretionary choices (for the Lanczos approximation series).
75 // Declare the number of approximation terms and the corresponding
76 // Lanczos approximation coefficients.
77 const int nterm_lanczos = 9;
78 const double pcoeff_lanczos[] = {
79 0.999999999999809932277,
80 676.520368121885098567,
81 -1259.13921672240287047,
82 771.323428777653078849,
83 -176.615029162140599066,
84 12.5073432786869048145,
85 -0.138571095265720116896,
86 9.98436957801957085956e-6,
87 1.50563273514931155834e-7,
88 }; // NOTE: constant number of significant figures.
89
90 std::complex<double> series = pcoeff_lanczos[0];
91 for (int i = 1; i < nterm_lanczos; i++) {
92 series += pcoeff_lanczos[i] / (z + double(i));
93 }
94
95 return series;
96}
97
98std::complex<double> eval_gamma_lanczos(std::complex<double> z) {
99 // Exploit Euler's reflection formula as the Lanczos approximation
100 // is only valid for Re{z} > 1/2.
101 if (z.real() < 1./2) {
102 return M_PI / (std::sin(M_PI * z) * eval_gamma_lanczos(1. - z));
103 }
104
105 // Substitute variables into the approximation formula.
106 z -= 1.;
107
108 std::complex<double> t = z + gconst_lanczos + 1./2;
109 std::complex<double> series = eval_lanczos_approx_series(z);
110
111 std::complex<double> gamma = std::sqrt(2.*M_PI)
112 * std::pow(t, z + 1./2) * std::exp(-t) * series;
113
114 return gamma;
115}
116
117
118// -----------------------------------------------------------------------
119// Component evaluation
120// -----------------------------------------------------------------------
121
122void get_lngamma_parts(double x, double y, double& lnr, double& theta) {
123 gsl_sf_result lnr_result, theta_result;
124 gsl_sf_lngamma_complex_e(x, y, &lnr_result, &theta_result);
125
126 lnr = lnr_result.val;
127 theta = theta_result.val;
128}
129
130std::complex<double> eval_gamma_lnratio(double mu, std::complex<double> nu) {
131 std::complex<double> x_p = (mu + 1. + nu)/2.;
132 std::complex<double> x_m = (mu + 1. - nu)/2.;
133
134 // Although the Stirling approximation is used, the Lanczos
135 // approximation for log-gamma should remain accurate enough.
136 const double cutoff_asymp = 100.;
137 std::complex<double> lnratio;
138 if (nu.imag() < cutoff_asymp) {
139 double lnr_p, theta_p;
140 double lnr_m, theta_m;
141
142 get_lngamma_parts(x_p.real(), x_p.imag(), lnr_p, theta_p);
143 get_lngamma_parts(x_m.real(), x_m.imag(), lnr_m, theta_m);
144
145 lnratio.real(lnr_p - lnr_m);
146 lnratio.imag(theta_p - theta_m);
147 } else {
148 lnratio =
149 - nu
150 + (x_p - 1./2) * std::log(x_p) - (x_m - 1./2) * std::log(x_m)
151 + 1./12 * (1./x_p - 1./x_m)
152 - 1./360 * (1./std::pow(x_p, 3) - 1./std::pow(x_m, 3))
153 + 1./1260 * (1./std::pow(x_p, 5) - 1./std::pow(x_m, 5));
154 }
155
156 return lnratio;
157}
158
159
160// ***********************************************************************
161// Spherical harmonics
162// ***********************************************************************
163
164// CAVEAT: Discretionary choice such that eps = 1.e-9.
165const double eps_coupling = 1.e-9;
166
167double wigner_3j(int j1, int j2, int j3, int m1, int m2, int m3) {
168 return gsl_sf_coupling_3j(2*j1, 2*j2, 2*j3, 2*m1, 2*m2, 2*m3);
169}
170
171std::complex<double> \
172SphericalHarmonicCalculator::calc_reduced_spherical_harmonic(
173 const int ell, const int m, double pos[3]
174) {
175 // CAVEAT: Discretionary choice such that eps = 1.e-9.
176 const double eps = 1.e-9;
177
178 // Return unity in the trivial case.
179 if (ell == 0 && m == 0) {return 1.;}
180
181 // Calculate modulus.
182 double xyz_mod_sq = 0.;
183 for (int iaxis = 0; iaxis < 3; iaxis++) {
184 xyz_mod_sq += pos[iaxis] * pos[iaxis];
185 } // r² = x² + y² + z²
186
187 double xyz_mod = std::sqrt(xyz_mod_sq); // r = √(x² + y² + z²)
188
189 // Return zero in the trivial case.
190 if (std::fabs(xyz_mod) < eps) {return 0.;}
191
192 // Calculate the angular variable μ = cos(θ).
193 double mu = pos[2] / xyz_mod; // μ = z / r
194
195 // Calculate the angular variable Ï•.
196 double xy_mod = std::sqrt(pos[0] * pos[0] + pos[1] * pos[1]); // r_xy =
197 // √(x² + y²)
198
199 double phi = 0.;
200 if (std::fabs(xy_mod) >= eps) {
201 phi = std::acos(pos[0] / xy_mod); // Ï• = arccos(x / r_xy)
202 if (pos[1] < 0.) {
203 phi = - phi + 2.*M_PI; // ϕ ∈ [π, 2π] if y < 0
204 }
205 }
206
207 // Calculate spherical harmonics with m >= 0 via the normalised
208 // associated Legendre polynomial, i.e. Y_lm = √((2l + 1)/(4π))
209 // √((l - |m|)!/(l + |m|)!) P_l^|m|(μ) * exp(imϕ).
210 std::complex<double> ylm = std::exp(M_I * double(m) * phi)
211 * gsl_sf_legendre_sphPlm(ell, std::abs(m), mu);
212
213 // Impose parity and conjugation.
214 ylm = std::pow(-1, (m - std::abs(m))/2) * std::conj(ylm);
215
216 // Normalise to the reduced form.
217 ylm *= std::sqrt(4.*M_PI / (2.*ell + 1.));
218
219 return ylm;
220}
221
222void SphericalHarmonicCalculator::\
223store_reduced_spherical_harmonic_in_fourier_space(
224 const int ell, const int m,
225 const double boxsize[3], const int ngrid[3],
226 std::vector< std::complex<double> >& ylm_out
227) {
228 // Determine the fundamental wavenumber in each dimension.
229 double dk[3] = {
230 2.*M_PI / boxsize[0], 2.*M_PI / boxsize[1], 2.*M_PI / boxsize[2]
231 };
232
233 // Assign a wavevector to each grid cell.
234#ifdef TRV_USE_OMP
235#pragma omp parallel for collapse(3)
236#endif // TRV_USE_OMP
237 for (int i = 0; i < ngrid[0]; i++) {
238 for (int j = 0; j < ngrid[1]; j++) {
239 for (int k = 0; k < ngrid[2]; k++) {
240 // Lay the 'bricks' vertically, then inwards, then to
241 // the right, i.e. along z-axis, y-axis and then x-axis.
242 // The assigned flattened-grid array index is
243 // (i * ngrid_y * ngrid_z + j * ngrid_z + k)
244 // where ngrid is the grid cell number along each axis.
245 long long idx_grid =
246 (i * static_cast<long long>(ngrid[1]) + j) * ngrid[2] + k;
247
248 // This conforms to the (absurd) FFT array-ordering convention
249 // that negative wavenumbers/frequencies come after zero and
250 // positive wavenumbers/frequencies.
251 double kvec[3];
252 kvec[0] = (i < ngrid[0]/2) ? i * dk[0] : (i - ngrid[0]) * dk[0];
253 kvec[1] = (j < ngrid[1]/2) ? j * dk[1] : (j - ngrid[1]) * dk[1];
254 kvec[2] = (k < ngrid[2]/2) ? k * dk[2] : (k - ngrid[2]) * dk[2];
255
256 ylm_out[idx_grid] = calc_reduced_spherical_harmonic(ell, m, kvec);
257 }
258 }
259 }
260}
261
262void SphericalHarmonicCalculator::\
263store_reduced_spherical_harmonic_in_config_space(
264 const int ell, const int m,
265 const double boxsize[3], const int ngrid[3],
266 std::vector< std::complex<double> >& ylm_out
267) {
268 // Determine the grid cell size in each dimension.
269 double dr[3] = {
270 boxsize[0] / double(ngrid[0]),
271 boxsize[1] / double(ngrid[1]),
272 boxsize[2] / double(ngrid[2])
273 };
274
275 // Assign a position vector to each grid cell.
276#ifdef TRV_USE_OMP
277#pragma omp parallel for collapse(3)
278#endif // TRV_USE_OMP
279 for (int i = 0; i < ngrid[0]; i++) {
280 for (int j = 0; j < ngrid[1]; j++) {
281 for (int k = 0; k < ngrid[2]; k++) {
282 // Lay the 'bricks' vertically, then inwards, then to
283 // the right, i.e. along z-axis, y-axis and then x-axis.
284 // The assigned flattened-grid array index is
285 // (i * ngrid_y * ngrid_z + j * ngrid_z + k)
286 // where ngrid is the grid cell number along each axis.
287 long long idx_grid =
288 (i * static_cast<long long>(ngrid[1]) + j) * ngrid[2] + k;
289
290 // This conforms to the (absurd) FFT array-ordering convention
291 // that negative wavenumbers/frequencies come after zero and
292 // positive wavenumbers/frequencies.
293 double rvec[3];
294 rvec[0] = (i < ngrid[0]/2) ? i * dr[0] : (i - ngrid[0]) * dr[0];
295 rvec[1] = (j < ngrid[1]/2) ? j * dr[1] : (j - ngrid[1]) * dr[1];
296 rvec[2] = (k < ngrid[2]/2) ? k * dr[2] : (k - ngrid[2]) * dr[2];
297
298 ylm_out[idx_grid] = calc_reduced_spherical_harmonic(ell, m, rvec);
299 }
300 }
301 }
302}
303
304
305// ***********************************************************************
306// Spherical Bessel function
307// ***********************************************************************
308
310 const int ell
311) : order(ell) {
312 // Set up sampling range and number.
313 this->split = (this->split >= this->order * this->order) ?
314 this->split : this->order * this->order;
315
316 const double xmin = 0.; // minimum of interpolation range
317 const double xmax = this->split; // maximum of interpolation range
318 const double dx = this->step; // interpolation step size
319
320 int nsample = int((xmax - xmin)/dx) + 1; // interpolation sample number
321
322 // Initialise and evaluate at sample points.
323 double* x = new double[nsample];
324 double* j_ell = new double[nsample];
325
326#ifdef TRV_USE_OMP
327#pragma omp parallel for
328#endif // TRV_USE_OMP
329 for (int i = 0; i < nsample; i++) {
330 x[i] = xmin + dx * i;
331 j_ell[i] = gsl_sf_bessel_jl(this->order, x[i]);
332 }
333
334 // Initialise the interpolator using cubic spline and the accelerator.
335 this->accel = gsl_interp_accel_alloc();
336 this->spline = gsl_spline_alloc(gsl_interp_cspline, nsample);
337
338 gsl_spline_init(this->spline, x, j_ell, nsample);
339
340 delete[] x; delete[] j_ell;
341}
342
344 const SphericalBesselCalculator& other
345) {
346 this->order = other.order;
347 this->split = other.split;
348 this->step = other.step;
349
350 this->accel = gsl_interp_accel_alloc();
351 this->spline = gsl_spline_alloc(gsl_interp_cspline, other.spline->size);
352
353 gsl_spline_init(
354 this->spline, other.spline->x, other.spline->y, other.spline->size
355 );
356}
357
359 if (this->accel != nullptr) {
360 gsl_interp_accel_free(this->accel); this->accel = nullptr;
361 }
362
363 if (this->spline != nullptr) {
364 gsl_spline_free(this->spline); this->spline = nullptr;
365 }
366}
367
369 if (x >= this->split) {
370 return gsl_sf_bessel_jl(this->order, x);
371 } else {
372 // NOTE: This is a computational bottleneck.
373 return gsl_spline_eval(this->spline, x, this->accel);
374 }
375}
376
377} // namespace trv::maths
378} // namespace trv
double eval(double x)
Evaluate the interpolated function.
Definition maths.cpp:368
SphericalBesselCalculator(const int ell)
Construct the interpolated function calculator.
Definition maths.cpp:309
~SphericalBesselCalculator()
Destruct the interpolated function.
Definition maths.cpp:358
static std::complex< double > calc_reduced_spherical_harmonic(const int ell, const int m, double pos[3])
Calculate the reduced spherical harmonic.
Definition maths.cpp:172
Mathematical calculations.
std::complex< double > eval_complex_in_polar(double r, double theta)
Evaluate a complex number in the polar form.
Definition maths.cpp:44
double get_vec3d_magnitude(std::vector< double > vec)
Return the magnitude of a 3-d vector.
Definition maths.cpp:53
std::complex< double > eval_gamma_lanczos(std::complex< double > z)
Evaluate the gamma function on the complex plane using the Lanczos approximation.
Definition maths.cpp:98
void get_lngamma_parts(double x, double y, double &lnr, double &theta)
Get the real and imaginary parts of the log-gamma function .
Definition maths.cpp:122
std::complex< double > eval_gamma_lnratio(double mu, std::complex< double > nu)
Evaluate the logarithm of the ratio of two gamma functions.
Definition maths.cpp:130
std::complex< double > eval_lanczos_approx_series(std::complex< double > z)
Evaluate the Lanczos approximation series for the gamma function.
Definition maths.cpp:73
const std::complex< double > M_I
imaginary unit
const double eps_coupling
zero-tolerance for Wigner 3-j coupling coefficients
Definition maths.cpp:165
const double gconst_lanczos
Lanczos approximation constant.
Definition maths.cpp:71
double wigner_3j(int j1, int j2, int j3, int m1, int m2, int m3)
Calculate Wigner 3-j symbol.
Definition maths.cpp:167