1#![allow(clippy::indexing_slicing)] use std::f64::consts::PI;
11
12use super::{factorial_ratio_sqrt, log_factorial};
13use crate::{Complex, numerics::parity};
14
15#[derive(Clone, Copy, Debug, Default)]
17pub(crate) struct Solid {
18 pub(crate) value: Complex,
20 pub(crate) gradient: [Complex; 3],
22 pub(crate) hessian: [[Complex; 3]; 3],
24}
25
26pub(crate) fn solid<const SECOND: bool>(l: i32, m: i32, position: [f64; 3]) -> Solid {
29 if m.abs() > l {
30 return Solid::default();
31 }
32 let [x, y, z] = position;
33 let r2 = x * x + y * y + z * z;
34 let order = m.abs();
35 let xy = Complex::new(x, y);
36 let e = [Complex::new(1.0, 0.0), Complex::i(), Complex::default()];
37 let mut p = Complex::new(1.0, 0.0);
38 let mut grad = [Complex::default(); 3];
39 let mut hessian = [[Complex::default(); 3]; 3];
40 for k in 1..=order {
41 let factor = -f64::from(2 * k - 1);
42 if SECOND {
43 hessian = std::array::from_fn(|i| {
44 std::array::from_fn(|j| {
45 factor * (hessian[i][j] * xy + grad[i] * e[j] + grad[j] * e[i])
46 })
47 });
48 }
49 grad = std::array::from_fn(|i| factor * (grad[i] * xy + p * e[i]));
50 p *= factor * xy;
51 }
52 let (mut prev, mut prev_grad, mut prev_hessian) = (
53 Complex::default(),
54 [Complex::default(); 3],
55 [[Complex::default(); 3]; 3],
56 );
57 for degree in order + 1..=l {
58 let a = f64::from(2 * degree - 1) / f64::from(degree - order);
59 let b = f64::from(degree + order - 1) / f64::from(degree - order);
60 let next = a * z * p - b * r2 * prev;
61 let next_grad = std::array::from_fn(|i| {
62 a * (z * grad[i] + if i == 2 { p } else { Complex::default() })
63 - b * (r2 * prev_grad[i] + 2.0 * position[i] * prev)
64 });
65 if SECOND {
66 let next_hessian = std::array::from_fn(|i| {
67 std::array::from_fn(|j| {
68 a * (z * hessian[i][j]
69 + if i == 2 { grad[j] } else { Complex::default() }
70 + if j == 2 { grad[i] } else { Complex::default() })
71 - b * (r2 * prev_hessian[i][j]
72 + 2.0 * position[i] * prev_grad[j]
73 + 2.0 * position[j] * prev_grad[i]
74 + if i == j {
75 2.0 * prev
76 } else {
77 Complex::default()
78 })
79 })
80 });
81 prev_hessian = hessian;
82 hessian = next_hessian;
83 }
84 (prev, prev_grad) = (p, grad);
85 (p, grad) = (next, next_grad);
86 }
87 if m < 0 {
88 let factor = parity(order) * (log_factorial(l - order) - log_factorial(l + order)).exp();
89 p = factor * p.conj();
90 grad = grad.map(|g| factor * g.conj());
91 if SECOND {
92 hessian = hessian.map(|row| row.map(|g| factor * g.conj()));
93 }
94 }
95 Solid {
96 value: p,
97 gradient: grad,
98 hessian,
99 }
100}
101
102pub(crate) fn direction(position: [f64; 3]) -> Option<(f64, [f64; 3])> {
109 let scale = position.iter().fold(0.0_f64, |scale, x| scale.max(x.abs()));
110 if scale < f64::MIN_POSITIVE {
111 return None;
112 }
113 let scaled = position.map(|x| x / scale);
114 let norm = scaled.iter().map(|x| x * x).sum::<f64>().sqrt();
115 Some((scale * norm, scaled.map(|x| x / norm)))
116}
117
118pub(crate) fn on_sphere(l: i32, m: i32, unit: [f64; 3]) -> (Complex, [Complex; 3]) {
122 let solid = solid::<false>(l, m, unit);
123 (solid.value, tangent(l, solid.value, solid.gradient, unit))
124}
125
126pub(crate) fn tangent(
128 l: i32,
129 value: Complex,
130 gradient: [Complex; 3],
131 unit: [f64; 3],
132) -> [Complex; 3] {
133 let radial = f64::from(l) * value;
134 std::array::from_fn(|axis| gradient[axis] - radial * unit[axis])
135}
136
137pub(crate) fn harmonic_normalization(l: i32, m: i32) -> f64 {
140 (f64::from(2 * l + 1) / (4.0 * PI)).sqrt() * factorial_ratio_sqrt(l, m)
141}
142
143#[derive(Clone, Debug)]
147pub(crate) struct SolidTable {
148 order: i32,
149 reflection: Vec<f64>,
150}
151
152fn table_index(p: i32, m: i32) -> usize {
154 usize::try_from(p * p + p + m).unwrap_or_default()
155}
156
157impl SolidTable {
158 pub(crate) fn new(order: i32) -> Self {
160 let reflection = (0..=order)
161 .flat_map(|l| (-l..=l).map(move |m| (l, m)))
162 .map(|(l, m)| {
163 let order = m.abs();
164 parity(order) * (log_factorial(l - order) - log_factorial(l + order)).exp()
165 })
166 .collect();
167 Self { order, reflection }
168 }
169
170 pub(crate) fn len(&self) -> usize {
172 self.reflection.len()
173 }
174
175 pub(crate) fn visit<const GRADIENT: bool>(
179 &self,
180 position: [f64; 3],
181 mut emit: impl FnMut(usize, i32, Complex, [Complex; 3]),
182 ) {
183 self.visit_impl::<GRADIENT, false>(position, |index, degree, solid| {
184 emit(index, degree, solid.value, solid.gradient);
185 });
186 }
187
188 pub(crate) fn visit_solid<const SECOND: bool>(
191 &self,
192 position: [f64; 3],
193 emit: impl FnMut(usize, i32, Solid),
194 ) {
195 self.visit_impl::<true, SECOND>(position, emit);
196 }
197
198 fn visit_impl<const GRADIENT: bool, const SECOND: bool>(
199 &self,
200 position: [f64; 3],
201 mut emit: impl FnMut(usize, i32, Solid),
202 ) {
203 let [x, y, z] = position;
204 let r2 = x * x + y * y + z * z;
205 let xy = Complex::new(x, y);
206 let e = [Complex::new(1.0, 0.0), Complex::i(), Complex::default()];
207 let (mut diagonal, mut diagonal_gradient) =
208 (Complex::new(1.0, 0.0), [Complex::default(); 3]);
209 let mut diagonal_hessian = [[Complex::default(); 3]; 3];
210 for order in 0..=self.order {
211 if order > 0 {
212 let factor = -f64::from(2 * order - 1);
214 if SECOND {
215 diagonal_hessian = std::array::from_fn(|i| {
216 std::array::from_fn(|j| {
217 factor
218 * (diagonal_hessian[i][j] * xy
219 + diagonal_gradient[i] * e[j]
220 + diagonal_gradient[j] * e[i])
221 })
222 });
223 }
224 if GRADIENT {
225 diagonal_gradient = std::array::from_fn(|i| {
226 factor * (diagonal_gradient[i] * xy + diagonal * e[i])
227 });
228 }
229 diagonal *= factor * xy;
230 }
231 let (mut p, mut grad) = (diagonal, diagonal_gradient);
232 let mut hessian = diagonal_hessian;
233 let (mut prev, mut prev_grad) = (Complex::default(), [Complex::default(); 3]);
234 let mut prev_hessian = [[Complex::default(); 3]; 3];
235 for degree in order..=self.order {
236 if degree > order {
237 let a = f64::from(2 * degree - 1) / f64::from(degree - order);
238 let b = f64::from(degree + order - 1) / f64::from(degree - order);
239 let next = a * z * p - b * r2 * prev;
240 let next_grad = if GRADIENT {
241 std::array::from_fn(|i| {
242 a * (z * grad[i] + if i == 2 { p } else { Complex::default() })
243 - b * (r2 * prev_grad[i] + 2.0 * position[i] * prev)
244 })
245 } else {
246 grad
247 };
248 if SECOND {
249 let next_hessian = std::array::from_fn(|i| {
250 std::array::from_fn(|j| {
251 a * (z * hessian[i][j]
252 + if i == 2 { grad[j] } else { Complex::default() }
253 + if j == 2 { grad[i] } else { Complex::default() })
254 - b * (r2 * prev_hessian[i][j]
255 + 2.0 * position[i] * prev_grad[j]
256 + 2.0 * position[j] * prev_grad[i]
257 + if i == j {
258 2.0 * prev
259 } else {
260 Complex::default()
261 })
262 })
263 });
264 prev_hessian = hessian;
265 hessian = next_hessian;
266 }
267 (prev, prev_grad) = (p, grad);
268 (p, grad) = (next, next_grad);
269 }
270 emit(
271 table_index(degree, order),
272 degree,
273 Solid {
274 value: p,
275 gradient: grad,
276 hessian,
277 },
278 );
279 if order > 0 {
280 let index = table_index(degree, -order);
281 let factor = self.reflection[index];
282 let gradient = if GRADIENT {
283 grad.map(|g| factor * g.conj())
284 } else {
285 grad
286 };
287 let hessian = if SECOND {
288 hessian.map(|row| row.map(|g| factor * g.conj()))
289 } else {
290 hessian
291 };
292 emit(
293 index,
294 degree,
295 Solid {
296 value: factor * p.conj(),
297 gradient,
298 hessian,
299 },
300 );
301 }
302 }
303 }
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use std::f64::consts::PI;
313
314 use proptest::{prelude::*, test_runner::TestCaseError};
315
316 use super::{SolidTable, direction, on_sphere, solid};
317 use crate::{
318 Complex,
319 special::{Angular, angular_value},
320 test_support::{ALGEBRA_CASES, degree_order, prop_assert_close},
321 };
322
323 fn position() -> impl Strategy<Value = [f64; 3]> {
325 prop_oneof![
326 (-2.0_f64..2.0, -2.0_f64..2.0, -2.0_f64..2.0).prop_map(<[f64; 3]>::from),
327 (-2.0_f64..2.0).prop_map(|z| [0.0, 0.0, z]),
328 (-2.0_f64..2.0, -2.0_f64..2.0).prop_map(|(x, y)| [x, y, 0.0]),
329 ]
330 }
331
332 proptest! {
333 #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
334
335 #[test]
336 fn solid_harmonics_are_harmonic_and_homogeneous(
337 (l, m) in degree_order(0..31),
338 position in position(),
339 ) {
340 check_solid_harmonic(l, m, position)?;
341 }
342
343 #[test]
344 fn solid_tables_match_single_harmonics(order in 0_i32..=30, position in position()) {
345 check_solid_table(order, position)?;
346 }
347
348 #[test]
349 fn angular_functions_match_legendre_and_ignore_the_length_unit(
350 (l, m) in degree_order(0..31),
351 theta in prop_oneof![Just(0.0), Just(PI), 0.0..PI],
352 phi in -PI..PI,
353 radius in 0.1_f64..10.0,
354 exponent in -150_i32..=150,
355 ) {
356 check_angular(l, m, [theta, phi], radius, exponent)?;
357 }
358
359 #[test]
362 fn angular_functions_match_legendre_near_the_poles(
363 (l, m) in degree_order(0..31),
364 offset in -12.0_f64..-1.0,
365 south in any::<bool>(),
366 phi in -PI..PI,
367 radius in 0.1_f64..10.0,
368 exponent in -150_i32..=150,
369 ) {
370 let offset = 10_f64.powf(offset);
371 let theta = if south { PI - offset } else { offset };
372 check_angular(l, m, [theta, phi], radius, exponent)?;
373 }
374 }
375
376 fn check_solid_table(order: i32, position: [f64; 3]) -> Result<(), TestCaseError> {
379 let table = SolidTable::new(order);
380 for with_gradient in [false, true] {
381 let mut entries = Vec::new();
382 let push = |index, p, value, gradient| entries.push((index, p, value, gradient));
383 if with_gradient {
384 table.visit::<true>(position, push);
385 } else {
386 table.visit::<false>(position, push);
387 }
388 let mut indices: Vec<usize> = entries.iter().map(|entry| entry.0).collect();
389 indices.sort_unstable();
390 prop_assert_eq!(indices, (0..table.len()).collect::<Vec<_>>());
391 for (index, p, value, gradient) in entries {
392 let m = i32::try_from(index).unwrap() - p * p - p;
393 let expected = solid::<false>(p, m, position);
394 prop_assert_eq!(value, expected.value, "{} {}", p, m);
395 if with_gradient {
396 prop_assert_eq!(gradient, expected.gradient, "{} {}", p, m);
397 }
398 }
399 }
400 let mut second = vec![super::Solid::default(); table.len()];
401 table.visit_solid::<true>(position, |index, _, solid| second[index] = solid);
402 for p in 0..=order {
403 for m in -p..=p {
404 let expected = solid::<true>(p, m, position);
405 let actual = second[super::table_index(p, m)];
406 prop_assert_eq!(actual.value, expected.value, "{} {}", p, m);
407 prop_assert_eq!(actual.gradient, expected.gradient, "{} {}", p, m);
408 prop_assert_eq!(actual.hessian, expected.hessian, "{} {}", p, m);
409 }
410 }
411 Ok(())
412 }
413
414 fn tolerance(scale: f64) -> f64 {
417 1e-12 * scale + f64::MIN_POSITIVE
418 }
419
420 fn check_solid_harmonic(l: i32, m: i32, x: [f64; 3]) -> Result<(), TestCaseError> {
425 for m in [l + 1, -l - 1] {
426 let beyond = solid::<true>(l, m, x);
427 prop_assert_eq!(beyond.value, Complex::default(), "m = {}", m);
428 prop_assert_eq!(beyond.gradient, [Complex::default(); 3], "m = {}", m);
429 prop_assert_eq!(beyond.hessian, [[Complex::default(); 3]; 3], "m = {}", m);
430 }
431 let s = solid::<true>(l, m, x);
432 let laplacian: Complex = (0..3).map(|i| s.hessian[i][i]).sum();
433 let scale: f64 = (0..3).map(|i| s.hessian[i][i].norm()).sum();
434 prop_assert_close!(laplacian, Complex::default(), tolerance(scale));
435 let radial: Complex = (0..3).map(|i| x[i] * s.gradient[i]).sum();
436 let scale = f64::from(l) * s.value.norm()
437 + (0..3).map(|i| (x[i] * s.gradient[i]).norm()).sum::<f64>();
438 prop_assert_close!(radial, f64::from(l) * s.value, tolerance(scale));
439 for i in 0..3 {
440 let radial: Complex = (0..3).map(|j| x[j] * s.hessian[i][j]).sum();
441 let scale = f64::from(l) * s.gradient[i].norm()
442 + (0..3).map(|j| (x[j] * s.hessian[i][j]).norm()).sum::<f64>();
443 let expected = f64::from(l - 1) * s.gradient[i];
444 prop_assert_close!(radial, expected, tolerance(scale), "row {}", i);
445 }
446 Ok(())
447 }
448
449 fn angular(l: i32, m: i32, x: [f64; 3]) -> (Complex, [Complex; 3]) {
452 let (r, unit) = direction(x).unwrap();
453 let (value, tangent) = on_sphere(l, m, unit);
454 (value, tangent.map(|t| t / r))
455 }
456
457 fn check_angular(
463 l: i32,
464 m: i32,
465 [theta, phi]: [f64; 2],
466 radius: f64,
467 exponent: i32,
468 ) -> Result<(), TestCaseError> {
469 let (sin, cos) = theta.sin_cos();
470 let x = [
471 radius * sin * phi.cos(),
472 radius * sin * phi.sin(),
473 radius * cos,
474 ];
475 check_legendre(l, m, cos, phi, radius)?;
476 let (value, gradient) = angular(l, m, x);
477 let tangent: f64 = gradient.iter().map(|g| g.norm()).sum::<f64>() * radius;
479 let scale = value.norm() + tangent;
480 let s = 10_f64.powi(exponent);
481 let (scaled, scaled_gradient) = angular(l, m, x.map(|x| x * s));
482 prop_assert_close!(scaled, value, tolerance(scale));
483 let subnormal = 8.0 * f64::MIN_POSITIVE * f64::EPSILON * s * radius;
485 prop_assert_close!(
486 scaled_gradient.map(|g| g * s * radius),
487 gradient.map(|g| g * radius),
488 tolerance((1.0 + f64::from(l)) * scale) + subnormal
489 );
490 Ok(())
491 }
492
493 fn check_legendre(l: i32, m: i32, z: f64, phi: f64, radius: f64) -> Result<(), TestCaseError> {
502 let sine = ((1.0 - z) * (1.0 + z)).sqrt();
503 let represented = [
504 radius * sine * phi.cos(),
505 radius * sine * phi.sin(),
506 radius * z,
507 ];
508 let (value, gradient) = angular(l, m, represented);
509 let legendre = angular_value(
510 f64::from(l),
511 f64::from(m),
512 Complex::new(z, 0.0),
513 Angular::Legendre,
514 )
515 .unwrap();
516 let expected = legendre * Complex::from_polar(1.0, f64::from(m) * phi);
517 let tangent: f64 = gradient.iter().map(|g| g.norm()).sum::<f64>() * radius;
519 prop_assert_close!(value, expected, tolerance(value.norm() + tangent));
520 Ok(())
521 }
522}