Quantum 3xplorer (Q3)
An AI-native quantum simulation engine
Loading...
Searching...
No Matches
bessel.hpp
Go to the documentation of this file.
1#pragma once
2
3/**
4 * Spherical Bessel functions j_l(x).
5 *
6 * Two branches, split at x = l + 1, because neither is accurate over the whole
7 * range: the ascending series converges quickly for small x but suffers
8 * cancellation once x grows past l, and the upward recurrence — along with the
9 * closed forms it starts from — is stable for large x but subtracts nearly
10 * equal quantities below x ≈ l, where it loses every significant digit and can
11 * return the wrong sign outright.
12 */
13
14#include <cmath>
15
18
19namespace q3 {
20
21/**
22 * Spherical Bessel function j_l(x).
23 * Ascending series for x < l+1, closed forms for l = 0..2 above that, and
24 * upward recurrence for l > 2.
25 *
26 * @param degree Order l of the Bessel function
27 * @param argument The argument x
28 * @return j_l(x)
29 */
30inline Real spherical_bessel(int degree, Real argument) {
31 if (std::abs(argument) < math::tolerances::bessel_argument_zero_threshold) {
32 return (degree == 0) ? 1.0 : 0.0;
33 }
34 /**
35 * Ascending series, summed to convergence.
36 *
37 * j_l(x) = x^l/(2l+1)!! · Σ_k (−x²/2)^k / (k! (2l+3)(2l+5)···(2l+2k+1))
38 *
39 * Used whenever x < l + 1, which is not a matter of taste: the upward
40 * recurrence below subtracts two nearly equal quantities once x drops under
41 * l, and by l = 5, x = 0.06 it returns the wrong sign. The series has no
42 * such cancellation there — its terms fall off like (x²/2)^k/k! — so this
43 * branch is the only correct one in that region, and the recurrence keeps
44 * the region where it is the accurate one.
45 *
46 * Summing to convergence rather than to a fixed number of terms is what
47 * lets the branch extend that far; a four-term truncation is only good to
48 * x ≈ 0.05.
49 */
50 if (std::abs(argument) < static_cast<Real>(degree) + 1.0) {
51 Real argument_to_degree_power = (degree == 0) ? 1.0 : std::pow(argument, degree);
52 double double_factorial_value = 1.0;
53 for (int index = 2 * degree + 1; index >= 1; index -= 2) {
54 double_factorial_value *= index;
55 }
56 Real half_argument_squared = 0.5 * argument * argument;
57 Real term = 1.0;
58 Real sum = 1.0;
59 for (int index = 1; index <= 60; index++) {
60 term *= -half_argument_squared / (index * (2.0 * degree + 2.0 * index + 1.0));
61 sum += term;
62 if (std::abs(term) < 1e-18 * std::abs(sum))
63 break;
64 }
65 return (argument_to_degree_power / double_factorial_value) * sum;
66 }
67 switch (degree) {
68 case 0:
69 return std::sin(argument) / argument;
70 case 1:
71 return (std::sin(argument) / argument - std::cos(argument)) / argument;
72 case 2:
73 return ((3.0 / (argument * argument) - 1.0) * std::sin(argument) -
74 3.0 * std::cos(argument) / argument) /
75 argument;
76 default: {
77 Real bessel_minus_one = std::sin(argument) / argument;
78 Real bessel_current = (std::sin(argument) / argument - std::cos(argument)) / argument;
79 for (int index = 2; index <= degree; index++) {
80 Real bessel_plus_one =
81 ((2 * index - 1) * bessel_current / argument) - bessel_minus_one;
82 bessel_minus_one = bessel_current;
83 bessel_current = bessel_plus_one;
84 }
85 return bessel_current;
86 }
87 }
88}
89
90/**
91 * Batch-compute spherical Bessel function j_l(q * r[i]) for all radial grid points.
92 * Whole-array because that is what the caller needs: the radial transform
93 * multiplies j_l(q*r) into an integrand and hands the array to Simpson's
94 * rule, which integrates arrays, not scalars. With l fixed across the mesh
95 * the branch on l is loop-invariant, so it is hoisted out of the point loop
96 * -- which is the whole difference between this routine and a loop calling
97 * the scalar one, and the reason it exists.
98 *
99 * @param degree Angular momentum l
100 * @param q_value The q-point value
101 * @param radial_grid Array of r values
102 * @param mesh_size Number of radial grid points to compute
103 * @param result [out] Pre-allocated array of size >= mesh_size to store j_l(q*r[i])
104 */
106 int degree, Real q_value, const Real* radial_grid, int mesh_size, Real* result) {
107 if (std::abs(q_value) < math::tolerances::bessel_argument_zero_threshold) {
108 Real value = (degree == 0) ? 1.0 : 0.0;
109 for (int index = 0; index < mesh_size; index++) {
110 result[index] = value;
111 }
112 return;
113 }
114
115 /**
116 * Compute q*r for all points, then evaluate j_l with the closed form for
117 * l = 0..2 and upward recurrence from j_0, j_1 above that — each only in
118 * the range where it is the accurate branch, deferring to the scalar
119 * routine's series below.
120 */
121
122 switch (degree) {
123 case 0:
124 for (int index = 0; index < mesh_size; index++) {
125 Real argument = q_value * radial_grid[index];
126 if (std::abs(argument) < 1.0) {
127 result[index] = spherical_bessel(degree, argument);
128 } else {
129 result[index] = std::sin(argument) / argument;
130 }
131 }
132 break;
133
134 case 1:
135 for (int index = 0; index < mesh_size; index++) {
136 Real argument = q_value * radial_grid[index];
137 if (std::abs(argument) < 1.0) {
138 result[index] = spherical_bessel(degree, argument);
139 } else {
140 result[index] = (std::sin(argument) / argument - std::cos(argument)) / argument;
141 }
142 }
143 break;
144
145 case 2:
146 for (int index = 0; index < mesh_size; index++) {
147 Real argument = q_value * radial_grid[index];
148 /**
149 * (3/x² − 1)sin(x)/x − 3cos(x)/x² differences two quantities of
150 * order 3/x², so below x ≈ 1 it loses the digits the answer is
151 * made of; the scalar routine's series is exact there.
152 */
153 if (std::abs(argument) < 1.0) {
154 result[index] = spherical_bessel(degree, argument);
155 } else {
156 result[index] = ((3.0 / (argument * argument) - 1.0) * std::sin(argument) -
157 3.0 * std::cos(argument) / argument) /
158 argument;
159 }
160 }
161 break;
162
163 default:
164 // General case: compute j_0 and j_1 for all points, then use recurrence
165 for (int index = 0; index < mesh_size; index++) {
166 Real argument = q_value * radial_grid[index];
167 /**
168 * Below x = l + 1 the recurrence cancels catastrophically and
169 * the scalar routine's converged series is the accurate branch
170 * — the same crossover, for the same reason. Deferring to it
171 * keeps one definition of where that boundary sits rather than
172 * two that can drift apart.
173 */
174 if (std::abs(argument) < static_cast<Real>(degree) + 1.0) {
175 result[index] = spherical_bessel(degree, argument);
176 } else {
177 Real bessel_minus_one = std::sin(argument) / argument;
178 Real bessel_current =
179 (std::sin(argument) / argument - std::cos(argument)) / argument;
180 for (int recurrence_index = 2; recurrence_index <= degree; recurrence_index++) {
181 Real bessel_plus_one =
182 ((2 * recurrence_index - 1) * bessel_current / argument) -
183 bessel_minus_one;
184 bessel_minus_one = bessel_current;
185 bessel_current = bessel_plus_one;
186 }
187 result[index] = bessel_current;
188 }
189 }
190 break;
191 }
192}
193
194} // namespace q3
constexpr double bessel_argument_zero_threshold
double Real
void spherical_bessel_batch(int degree, Real q_value, const Real *radial_grid, int mesh_size, Real *result)
Definition bessel.hpp:105
Real spherical_bessel(int degree, Real argument)
Definition bessel.hpp:30