1#![allow(clippy::indexing_slicing)] use crate::{
19 Complex, Error, MAX_DEGREE, Result,
20 numerics::{Jet, broadcast, finite, parallel::Parallel},
21 special::{self, Radial, SERIES_RADIUS, polar_trig, polarized_wave, radial_jet},
22};
23use std::f64::consts::PI;
24
25#[derive(Clone, Copy, Debug)]
33pub enum Family {
34 HarmonicX,
36 HarmonicY,
38 HarmonicZ,
41 Spherical(Radial),
44 Cylindrical(Radial),
47 Plane,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub struct WaveLabel {
68 pub l: i32,
70 pub m: i32,
72 pub pol: u8,
74}
75
76fn angular<const N: usize>(
88 l: i32,
89 m: i32,
90 theta: Jet<N>,
91 phi: Jet<N>,
92 tangential: bool,
93) -> [Jet<N>; 3] {
94 let [cosine, sine] = polar_trig(theta);
95 let order = m.abs();
96 let mut diagonal = (1.0 / (4.0 * PI)).sqrt();
99 for j in 1..=order {
100 diagonal *= -(f64::from(2 * j + 1) / f64::from(2 * j)).sqrt();
101 }
102 if m < 0 && order % 2 != 0 {
103 diagonal = -diagonal;
104 }
105 let mut previous = Jet::default();
106 let mut polynomial = Jet::constant(diagonal);
107 let mut previous_derivative = Jet::default();
108 let mut derivative = Jet::default();
109 for j in order + 1..=l {
110 let denominator = f64::from(j * j - order * order);
111 let a = (f64::from(4 * j * j - 1) / denominator).sqrt();
112 let b = if j == order + 1 {
113 0.0
114 } else {
115 (f64::from((2 * j + 1) * ((j - 1) * (j - 1) - order * order))
116 / (f64::from(2 * j - 3) * denominator))
117 .sqrt()
118 };
119 if order == 0 && tangential {
120 let next = a * (polynomial + cosine * derivative) - b * previous_derivative;
121 previous_derivative = derivative;
122 derivative = next;
123 }
124 let next = a * cosine * polynomial - b * previous;
125 previous = polynomial;
126 polynomial = next;
127 }
128 let phase = (Complex::i() * f64::from(m) * phi).exp();
129 let power = sine.powi((order - 1).max(0));
130 let harmonic = phase
131 * polynomial
132 * if order == 0 {
133 Jet::constant(1.0)
134 } else {
135 power * sine
136 };
137 if l == 0 || !tangential {
138 return [harmonic, Jet::default(), Jet::default()];
139 }
140 let norm = f64::from(l * (l + 1)).sqrt();
141 if order == 0 {
142 return [harmonic, Jet::default(), -phase * sine * derivative / norm];
143 }
144 let adjacent = (f64::from((l * l - order * order) * (2 * l + 1)) / f64::from(2 * l - 1)).sqrt();
145 [
146 harmonic,
147 phase * f64::from(m) * power * polynomial / norm,
148 phase * power * (f64::from(l) * cosine * polynomial - adjacent * previous) / norm,
149 ]
150}
151
152fn harmonics<const N: usize>(
155 label: WaveLabel,
156 theta: Jet<N>,
157 phi: Jet<N>,
158 tangential: bool,
159) -> Option<[Jet<N>; 3]> {
160 (label.m.abs() <= label.l).then(|| angular(label.l, label.m, theta, phi, tangential))
161}
162
163fn tangential<const N: usize>([_, pi, tau]: [Jet<N>; 3]) -> [[Jet<N>; 3]; 2] {
165 [
166 [Jet::default(), -pi, -Complex::i() * tau],
167 [Jet::default(), Complex::i() * tau, -pi],
168 ]
169}
170
171fn spherical<const N: usize>(
173 radial: Radial,
174 label: WaveLabel,
175 [kr, theta, phi]: [Jet<N>; 3],
176 helicity: bool,
177) -> Result<[Jet<N>; 3]> {
178 if label.l == 0 {
179 return Ok([Jet::default(); 3]);
180 }
181 let Some(harmonics) = harmonics(label, theta, phi, true) else {
182 return Ok([Jet::default(); 3]);
183 };
184 let [x, tangent] = tangential(harmonics);
185 if !helicity && label.pol == 0 {
186 let value = radial_jet(label.l, kr, radial, true)?;
187 return Ok(x.map(|v| v * value));
188 }
189 let l = label.l.unsigned_abs();
190 let bessel = special::spherical_radial(l, kr.value, radial)?;
191 let value = kr.chain(bessel.value, bessel.first);
192 let first = kr.chain(bessel.first, bessel.second);
193 let divided = if radial == Radial::Regular && kr.value.norm() < SERIES_RADIUS {
194 let lower = special::spherical_radial(l - 1, kr.value, radial)?;
195 let upper = special::spherical_radial(l + 1, kr.value, radial)?;
196 kr.chain(
197 (lower.value + upper.value) / f64::from(2 * l + 1),
198 (lower.first + upper.first) / f64::from(2 * l + 1),
199 )
200 } else {
201 value / kr
202 };
203 let mut n = tangent.map(|v| v * (divided + first));
204 n[0] = Complex::i() * harmonics[0] * divided * f64::from(label.l * (label.l + 1)).sqrt();
205 Ok(polarized_wave(x.map(|v| v * value), n, label.pol, helicity))
206}
207
208fn cylindrical<const N: usize>(
211 radial: Radial,
212 label: WaveLabel,
213 [kz, krr, phi, z, k, _]: [Jet<N>; 6],
214 helicity: bool,
215) -> Result<[Jet<N>; 3]> {
216 let bessel = special::cylindrical_radial(label.m, krr.value, radial)?;
217 let value = krr.chain(bessel.value, bessel.first);
218 let first = krr.chain(bessel.first, bessel.second);
219 let divided = if label.m == 0 {
220 Jet::default()
221 } else if radial == Radial::Regular && krr.value.norm() < SERIES_RADIUS {
222 let lower = special::cylindrical_radial(label.m - 1, krr.value, radial)?;
223 let upper = special::cylindrical_radial(label.m + 1, krr.value, radial)?;
224 krr.chain(
225 0.5 * (lower.value + upper.value),
226 0.5 * (lower.first + upper.first),
227 )
228 } else {
229 f64::from(label.m) * value / krr
230 };
231 let phase = (Complex::i() * (f64::from(label.m) * phi + kz * z)).exp();
232 let m = [
233 Complex::i() * divided * phase,
234 -first * phase,
235 Jet::default(),
236 ];
237 if !helicity && label.pol == 0 {
238 return Ok(m);
239 }
240 if k.value == Complex::default() {
241 return Err(Error::InvalidInput(
242 "cylindrical N waves require nonzero k".into(),
243 ));
244 }
245 let transverse = (k * k - kz * kz).sqrt();
248 let factor = phase / k;
249 let n = [
250 Complex::i() * kz * first * factor,
251 -kz * divided * factor,
252 transverse * value * factor,
253 ];
254 Ok(polarized_wave(m, n, label.pol, helicity))
255}
256
257fn plane<const N: usize>(
260 label: WaveLabel,
261 args: [Jet<N>; 6],
262 helicity: bool,
263) -> Result<[Jet<N>; 3]> {
264 let vector = [args[0].value, args[1].value, args[2].value];
265 if N == 0 {
266 return Ok(crate::pw::field_value(
267 crate::pw::polarization(vector, label.pol, helicity)?,
268 vector,
269 [args[3].value, args[4].value, args[5].value],
270 )?
271 .map(Jet::constant));
272 }
273 let polarization = crate::pw::polarization_jet::<3>(vector, label.pol, helicity)?
274 .map(|p| p.compose([args[0], args[1], args[2]]));
275 let phase = (Complex::i() * (0..3).map(|a| args[a] * args[a + 3]).sum::<Jet<N>>()).exp();
276 Ok(polarization.map(|p| p * phase))
277}
278
279#[inline]
280fn evaluate<const N: usize>(
281 family: Family,
282 label: WaveLabel,
283 args: [Jet<N>; 6],
284 helicity: bool,
285) -> Result<[Jet<N>; 3]> {
286 if label.pol > 1
287 || label.l < 0
288 || label.l > MAX_DEGREE
289 || label.m.unsigned_abs() > MAX_DEGREE.unsigned_abs()
290 || args.iter().any(|v| !finite(v.value))
291 {
292 return Err(Error::InvalidInput(
293 "require finite wave arguments, 0 <= l <= 128, |m| <= 128 and polarization 0 or 1"
295 .into(),
296 ));
297 }
298 let [theta, phi] = [args[0], args[1]];
299 let zero = [Jet::default(); 3];
300 Ok(match family {
301 Family::HarmonicX => harmonics(label, theta, phi, true).map_or(zero, |h| tangential(h)[0]),
302 Family::HarmonicY => harmonics(label, theta, phi, true).map_or(zero, |h| tangential(h)[1]),
303 Family::HarmonicZ => harmonics(label, theta, phi, false).map_or(zero, |[y, _, _]| {
304 [Complex::i() * y, Jet::default(), Jet::default()]
305 }),
306 Family::Spherical(radial) => {
307 spherical(radial, label, [args[0], args[1], args[2]], helicity)?
308 }
309 Family::Cylindrical(radial) => cylindrical(radial, label, args, helicity)?,
310 Family::Plane => plane(label, args, helicity)?,
311 })
312}
313fn checked<const N: usize>(result: [Jet<N>; 3]) -> Result<[Jet<N>; 3]> {
314 if result.iter().any(|v| !v.finite()) {
315 return Err(Error::NonFinite(
316 "non-finite vector wave or undefined derivative".into(),
317 ));
318 }
319 Ok(result)
320}
321
322#[inline]
329pub fn vector_wave(
330 family: Family,
331 label: WaveLabel,
332 args: [Complex; 6],
333 helicity: bool,
334) -> Result<[Complex; 3]> {
335 Ok(checked(evaluate::<0>(
336 family,
337 label,
338 args.map(Jet::constant),
339 helicity,
340 )?)?
341 .map(|v| v.value))
342}
343
344pub fn vector_wave_pullback(
347 family: Family,
348 label: WaveLabel,
349 args: [Complex; 6],
350 helicity: bool,
351 cotangent: [Complex; 3],
352) -> Result<[Complex; 6]> {
353 if cotangent.iter().any(|&v| !finite(v)) {
354 return Err(Error::InvalidInput(
355 "vector-wave cotangent must be finite".into(),
356 ));
357 }
358 if cotangent == [Complex::default(); 3] {
359 vector_wave(family, label, args, helicity)?;
360 return Ok([Complex::default(); 6]);
361 }
362 match family {
363 Family::HarmonicX | Family::HarmonicY | Family::HarmonicZ => {
364 pullback_impl::<2>(family, label, args, helicity, cotangent)
365 }
366 Family::Spherical(_) => pullback_impl::<3>(family, label, args, helicity, cotangent),
367 Family::Cylindrical(_) => pullback_impl::<5>(family, label, args, helicity, cotangent),
368 Family::Plane => pullback_impl::<6>(family, label, args, helicity, cotangent),
369 }
370}
371
372pub fn vector_wave_pushforward(
375 family: Family,
376 label: WaveLabel,
377 args: [Complex; 6],
378 helicity: bool,
379 tangent: [Complex; 6],
380) -> Result<[Complex; 3]> {
381 if tangent.iter().any(|&v| !finite(v)) {
382 return Err(Error::InvalidInput(
383 "vector-wave tangents must be finite".into(),
384 ));
385 }
386 if tangent == [Complex::default(); 6] {
387 vector_wave(family, label, args, helicity)?;
388 return Ok([Complex::default(); 3]);
389 }
390 Ok(checked(evaluate::<1>(
391 family,
392 label,
393 std::array::from_fn(|i| Jet {
394 value: args[i],
395 derivative: [tangent[i]],
396 }),
397 helicity,
398 )?)?
399 .map(|value| value.derivative[0]))
400}
401fn pullback_impl<const N: usize>(
404 family: Family,
405 label: WaveLabel,
406 args: [Complex; 6],
407 helicity: bool,
408 cotangent: [Complex; 3],
409) -> Result<[Complex; 6]> {
410 let values = checked(evaluate::<N>(
411 family,
412 label,
413 std::array::from_fn(|a| Jet::variable(args[a], a)),
414 helicity,
415 )?)?;
416 Ok(std::array::from_fn(|a| {
417 values
418 .iter()
419 .zip(cotangent)
420 .map(|(v, g)| v.derivative.get(a).copied().unwrap_or_default().conj() * g)
421 .sum()
422 }))
423}
424
425const PARALLEL: Parallel = Parallel::AtLeast(1024);
427
428#[derive(Debug)]
435pub struct VectorWaveResidual {
436 family: Family,
437 labels: Vec<WaveLabel>,
438 arguments: [Vec<Complex>; 6],
439 helicity: bool,
440 size: usize,
441}
442impl VectorWaveResidual {
443 pub fn new(
445 family: Family,
446 labels: Vec<WaveLabel>,
447 arguments: [Vec<Complex>; 6],
448 helicity: bool,
449 ) -> Result<Self> {
450 let [a, b, c, d, e, f] = arguments.each_ref().map(Vec::len);
451 let size = broadcast::size(
452 &[labels.len(), a, b, c, d, e, f],
453 "wave arrays must have equal lengths or scalar inputs",
454 )?;
455 Ok(Self {
456 family,
457 labels,
458 arguments,
459 helicity,
460 size,
461 })
462 }
463
464 pub fn values(&self) -> Result<Vec<[Complex; 3]>> {
466 let plane = match self.fixed_plane() {
467 Some((label, k)) => {
468 vector_wave(self.family, label, self.element(0).1, self.helicity)?;
470 Some((crate::pw::polarization(k, label.pol, self.helicity)?, k))
471 }
472 None => None,
473 };
474 broadcast::map(self.size, PARALLEL, |i| {
475 if let Some((p, k)) = plane {
476 crate::pw::field_value(p, k, self.point(i))
477 } else {
478 let (m, a) = self.element(i);
479 vector_wave(self.family, m, a, self.helicity)
480 }
481 })
482 }
483}
484
485impl VectorWaveResidual {
486 fn element(&self, i: usize) -> (WaveLabel, [Complex; 6]) {
487 (
488 broadcast::element(&self.labels, i),
489 self.arguments.each_ref().map(|a| broadcast::element(a, i)),
490 )
491 }
492 fn fixed_plane(&self) -> Option<(WaveLabel, [Complex; 3])> {
495 let [kx, ky, kz, ..] = self.arguments.each_ref().map(Vec::as_slice);
496 match (self.family, self.labels.as_slice(), kx, ky, kz) {
497 (Family::Plane, &[label], &[kx], &[ky], &[kz]) if self.size > 1 => {
498 Some((label, [kx, ky, kz]))
499 }
500 _ => None,
501 }
502 }
503 fn point(&self, i: usize) -> [Complex; 3] {
505 let [_, _, _, x, y, z] = &self.arguments;
506 [x, y, z].map(|v| broadcast::element(v, i))
507 }
508 pub fn pushforward(&self, tangents: [&[Complex]; 6]) -> Result<Vec<[Complex; 3]>> {
511 let plane = match self.fixed_plane() {
513 Some((label, k)) => {
514 let p = if tangents[..3]
515 .iter()
516 .any(|t| t.iter().any(|&v| v != Complex::default()))
517 {
518 crate::pw::polarization_jet::<3>(k, label.pol, self.helicity)?
519 } else {
520 crate::pw::polarization(k, label.pol, self.helicity)?.map(Jet::constant)
521 };
522 Some((p, k))
523 }
524 None => None,
525 };
526 broadcast::pushforward(
527 tangents,
528 self.size,
529 "wave tangents must be finite and broadcast to output",
530 PARALLEL,
531 |i, tangent| {
532 if let Some((p, k)) = &plane {
533 return fixed_plane_pushforward(p, *k, self.point(i), tangent);
534 }
535 let (label, arguments) = self.element(i);
536 vector_wave_pushforward(self.family, label, arguments, self.helicity, tangent)
537 },
538 )
539 }
540 pub fn pullback(&self, cotangent: &[[Complex; 3]]) -> Result<[Vec<Complex>; 6]> {
543 let plane = match self.fixed_plane() {
546 Some((label, k)) if cotangent.iter().flatten().any(|&v| v != Complex::default()) => {
547 let p = crate::pw::polarization_jet::<3>(k, label.pol, self.helicity)?;
548 Some((p, k))
549 }
550 _ => None,
551 };
552 broadcast::pullback(
553 cotangent,
554 self.size,
555 "wave cotangent must be finite and match output",
556 self.arguments.each_ref().map(Vec::len),
557 PARALLEL,
558 |i, &g| {
559 if let Some((p, k)) = &plane {
560 fixed_plane_pullback(p, *k, self.point(i), g)
561 } else {
562 let (m, a) = self.element(i);
563 vector_wave_pullback(self.family, m, a, self.helicity, g)
564 }
565 },
566 )
567 }
568}
569
570fn fixed_plane_pushforward(
572 p: &[Jet<3>; 3],
573 k: [Complex; 3],
574 r: [Complex; 3],
575 tangent: [Complex; 6],
576) -> Result<[Complex; 3]> {
577 let phase = (Complex::i() * (0..3).map(|j| k[j] * r[j]).sum::<Complex>()).exp();
578 let dphase = Complex::i()
579 * (0..3)
580 .map(|j| tangent[j] * r[j] + k[j] * tangent[j + 3])
581 .sum::<Complex>();
582 let result = p.map(|p| {
583 phase
584 * (p.value * dphase
585 + (0..3)
586 .map(|j| p.derivative[j] * tangent[j])
587 .sum::<Complex>())
588 });
589 if result.iter().any(|&v| !finite(v)) {
590 return Err(Error::NonFinite("non-finite plane-wave pushforward".into()));
591 }
592 Ok(result)
593}
594
595fn fixed_plane_pullback(
598 p: &[Jet<3>; 3],
599 k: [Complex; 3],
600 r: [Complex; 3],
601 g: [Complex; 3],
602) -> Result<[Complex; 6]> {
603 let phase = (Complex::i() * (0..3).map(|j| k[j] * r[j]).sum::<Complex>()).exp();
604 let g = g.map(|v| v * phase.conj());
605 let amplitude = p
606 .iter()
607 .zip(g)
608 .map(|(p, g)| p.value.conj() * g)
609 .sum::<Complex>();
610 let gradient = std::array::from_fn(|j| {
611 if j < 3 {
612 p.iter()
613 .zip(g)
614 .map(|(p, g)| p.derivative[j].conj() * g)
615 .sum::<Complex>()
616 + (Complex::i() * r[j]).conj() * amplitude
617 } else {
618 (Complex::i() * k[j - 3]).conj() * amplitude
619 }
620 });
621 if gradient.iter().any(|&v| !finite(v)) {
622 return Err(Error::NonFinite("non-finite plane-wave pullback".into()));
623 }
624 Ok(gradient)
625}
626
627pub fn vector_wave_array(
632 family: Family,
633 labels: Vec<WaveLabel>,
634 arguments: [Vec<Complex>; 6],
635 helicity: bool,
636) -> Result<(Vec<[Complex; 3]>, VectorWaveResidual)> {
637 let residual = VectorWaveResidual::new(family, labels, arguments, helicity)?;
638 Ok((residual.values()?, residual))
639}
640
641#[inline]
652pub fn sph_harm(l: i32, m: i32, theta: Complex, phi: Complex) -> Result<Complex> {
653 let zero = Complex::default();
654 let label = WaveLabel { l, m, pol: 0 };
655 let wave = vector_wave(
656 Family::HarmonicZ,
657 label,
658 [theta, phi, zero, zero, zero, zero],
659 false,
660 )?;
661 Ok(sph_harm_from_vsh_z(wave))
662}
663
664#[inline]
667#[must_use]
668pub fn sph_harm_from_vsh_z([z_r, _, _]: [Complex; 3]) -> Complex {
669 -Complex::i() * z_r
670}
671
672#[inline]
675#[must_use]
676pub fn sph_harm_from_vsh_z_pullback(cotangent: Complex) -> [Complex; 3] {
677 [
678 Complex::i() * cotangent,
679 Complex::default(),
680 Complex::default(),
681 ]
682}
683
684#[cfg(test)]
685mod tests {
686 use super::{
687 Complex, Family, PI, Radial, SERIES_RADIUS, WaveLabel, vector_wave, vector_wave_pullback,
688 vector_wave_pushforward,
689 };
690 use crate::{
691 cw, fields,
692 special::coordinates::{self, Transform},
693 sw,
694 test_support::{EXPENSIVE_CASES, complex, degree_order, prop_assert_close, radial},
695 };
696 use proptest::{prelude::*, test_runner::TestCaseError};
697
698 const ZERO: Complex = Complex::new(0.0, 0.0);
699
700 proptest! {
704 #![proptest_config(ProptestConfig::with_cases(40))]
705
706 #[test]
707 fn spherical_and_cylindrical_cartesian_reconstruction(
708 (l, m) in degree_order(1..8),
709 r in 0.05_f64..1.5,
710 theta in 0.3_f64..2.8,
711 phi in -3.0_f64..3.0,
712 helicity in any::<bool>(),
713 radial in radial(),
714 ) {
715 check_reconstruction(l, m, [r, theta, phi], helicity, radial)?;
716 }
717
718 #[test]
719 fn vector_wave_all_argument_adjoints(
720 (l, m) in degree_order(1..6),
721 pol in 0_u8..2,
722 magnitude in prop_oneof![0.05_f64..0.45, 0.6_f64..2.0],
723 phase in -0.5_f64..0.5,
724 theta in 0.3_f64..2.8,
725 phi in -2.0_f64..2.0,
726 helicity in any::<bool>(),
727 g in prop::array::uniform3(complex(0.5)),
728 ) {
729 let z = Complex::from_polar(magnitude, phase);
730 check_adjoints(WaveLabel { l, m, pol }, z, [theta, phi], helicity, g)?;
731 }
732
733 #[test]
734 fn regular_small_argument_branches_are_continuous(
735 (l, m) in degree_order(1..9),
736 phase in -PI..PI,
737 theta in 0.3_f64..2.8,
738 phi in -3.0_f64..3.0,
739 g in prop::array::uniform3(complex(0.5)),
740 ) {
741 check_small_argument_continuity(l, m, phase, [theta, phi], g)?;
742 }
743 }
744
745 proptest! {
747 #![proptest_config(ProptestConfig::with_cases(EXPENSIVE_CASES))]
748
749 #[test]
750 fn vector_harmonics_obey_unsolds_theorem(
751 l in 20_i32..129,
752 theta in prop_oneof![Just(0.0), Just(1e-10), Just(PI), 0.0..PI],
753 phi in -3.0_f64..3.0,
754 ) {
755 check_unsolds_theorem(l, theta, phi)?;
756 }
757 }
758
759 fn arguments<const N: usize>(leading: [Complex; N]) -> [Complex; 6] {
761 std::array::from_fn(|i| leading.get(i).copied().unwrap_or_default())
762 }
763
764 fn check_reconstruction(
768 l: i32,
769 m: i32,
770 [r, theta, phi]: [f64; 3],
771 helicity: bool,
772 radial: Radial,
773 ) -> Result<(), TestCaseError> {
774 let k = Complex::new(1.3, 0.05);
775 let kz = 0.2;
776 let krho = (k * k - kz * kz).sqrt();
777 for pol in [0, 1] {
778 let label = WaveLabel { l, m, pol };
779 let spherical = [r, theta, phi];
780 let args = arguments([k * r, theta.into(), phi.into()]);
781 let local = vector_wave(Family::Spherical(radial), label, args, helicity).unwrap();
782 let actual = coordinates::vector(local, spherical, Transform::SphToCar).unwrap();
783 let position = coordinates::point(spherical, Transform::SphToCar).unwrap();
784 let mode = sw::Mode { l, m, pol };
785 let expected = fields::spherical_wave(mode, k, position, helicity, radial)
786 .unwrap()
787 .value;
788 let scale = 1e-11 * (1.0 + expected.map(Complex::norm).iter().sum::<f64>());
789 prop_assert_close!(actual, expected, scale, "spherical {label:?}");
790 let cylindrical = [r, phi, 0.3];
791 let args = arguments([kz.into(), krho * r, phi.into(), 0.3.into(), k]);
792 let local = vector_wave(Family::Cylindrical(radial), label, args, helicity).unwrap();
793 let actual = coordinates::vector(local, cylindrical, Transform::CylToCar).unwrap();
794 let position = coordinates::point(cylindrical, Transform::CylToCar).unwrap();
795 let expected =
796 fields::cylindrical_wave(cw::Mode { kz, m, pol }, k, position, helicity, radial)
797 .unwrap()
798 .value;
799 let scale = 1e-11 * (1.0 + expected.map(Complex::norm).iter().sum::<f64>());
800 prop_assert_close!(actual, expected, scale, "cylindrical {label:?}");
801 }
802 Ok(())
803 }
804
805 fn paired_difference(
808 family: Family,
809 label: WaveLabel,
810 args: [Complex; 6],
811 helicity: bool,
812 g: [Complex; 3],
813 a: usize,
814 direction: Complex,
815 ) -> (Complex, f64) {
816 let h = 1e-6;
817 let step = |t: f64| {
818 let mut shifted = args;
819 shifted[a] += t * h * direction;
820 vector_wave(family, label, shifted, helicity).unwrap()
821 };
822 let (plus, minus) = (step(1.0), step(-1.0));
823 let terms: Vec<Complex> = (0..3)
824 .map(|c| g[c].conj() * (plus[c] - minus[c]) / (2.0 * h))
825 .collect();
826 (terms.iter().sum(), terms.iter().map(|t| t.norm()).sum())
827 }
828
829 fn check_adjoints(
833 label: WaveLabel,
834 z: Complex,
835 [theta, phi]: [f64; 2],
836 helicity: bool,
837 g: [Complex; 3],
838 ) -> Result<(), TestCaseError> {
839 let k = Complex::new(1.3, 0.05);
840 let families = [
841 (Family::HarmonicX, arguments([theta.into(), phi.into()])),
842 (Family::HarmonicY, arguments([theta.into(), phi.into()])),
843 (Family::HarmonicZ, arguments([theta.into(), phi.into()])),
844 (
845 Family::Spherical(Radial::Regular),
846 arguments([z, theta.into(), phi.into()]),
847 ),
848 (
849 Family::Spherical(Radial::Singular),
850 arguments([z, theta.into(), phi.into()]),
851 ),
852 (
853 Family::Cylindrical(Radial::Regular),
854 arguments([0.2.into(), z, phi.into(), 0.3.into(), k]),
855 ),
856 (
857 Family::Cylindrical(Radial::Singular),
858 arguments([0.2.into(), z, phi.into(), 0.3.into(), k]),
859 ),
860 (
861 Family::Plane,
862 [
863 Complex::new(0.3, 0.1),
864 Complex::new(0.4, -0.05),
865 z,
866 0.2.into(),
867 0.3.into(),
868 0.4.into(),
869 ],
870 ),
871 ];
872 let direction = Complex::new(0.2, 0.1);
873 for (family, args) in families {
874 let gradient = vector_wave_pullback(family, label, args, helicity, g).unwrap();
875 let tangent =
876 std::array::from_fn(|a| direction * f64::from(u32::try_from(a + 1).unwrap()));
877 let forward = vector_wave_pushforward(family, label, args, helicity, tangent).unwrap();
878 let forward_pair: Complex = g.into_iter().zip(forward).map(|(g, d)| g.conj() * d).sum();
879 let reverse_pair: Complex = gradient
880 .into_iter()
881 .zip(tangent)
882 .map(|(g, d)| g.conj() * d)
883 .sum();
884 prop_assert_close!(
885 forward_pair,
886 reverse_pair,
887 1e-12 * (1.0 + reverse_pair.norm()),
888 "{family:?} forward/reverse duality"
889 );
890 for (a, gradient) in gradient.into_iter().enumerate() {
891 let (numeric, scale) =
892 paired_difference(family, label, args, helicity, g, a, direction);
893 prop_assert_close!(
894 gradient.conj() * direction,
895 numeric,
896 2e-7 * (1.0 + scale),
897 "{family:?} argument {a}"
898 );
899 }
900 }
901 Ok(())
902 }
903
904 fn check_small_argument_continuity(
908 l: i32,
909 m: i32,
910 phase: f64,
911 [theta, phi]: [f64; 2],
912 g: [Complex; 3],
913 ) -> Result<(), TestCaseError> {
914 let at = |family: Family, label: WaveLabel, helicity: bool, magnitude: f64| {
915 let z = Complex::from_polar(magnitude, phase);
916 assert_eq!(
917 z.norm() < SERIES_RADIUS,
918 magnitude < SERIES_RADIUS,
919 "{z} is on the wrong branch"
920 );
921 let args = match family {
922 Family::Cylindrical(_) => arguments([
923 0.2.into(),
924 z,
925 phi.into(),
926 0.3.into(),
927 Complex::new(1.3, 0.05),
928 ]),
929 _ => arguments([z, theta.into(), phi.into()]),
930 };
931 (
932 vector_wave(family, label, args, helicity).unwrap(),
933 vector_wave_pullback(family, label, args, helicity, g).unwrap(),
934 )
935 };
936 for family in [
937 Family::Spherical(Radial::Regular),
938 Family::Cylindrical(Radial::Regular),
939 ] {
940 for (pol, helicity) in [(0, false), (1, false), (0, true), (1, true)] {
941 let label = WaveLabel { l, m, pol };
942 let (below, below_gradient) =
944 at(family, label, helicity, SERIES_RADIUS * (1.0 - 1e-15));
945 let (above, above_gradient) =
946 at(family, label, helicity, SERIES_RADIUS * (1.0 + 1e-15));
947 let scale = below.iter().map(|v| v.norm()).sum::<f64>();
948 prop_assert_close!(
949 below,
950 above,
951 1e-12 * scale + f64::MIN_POSITIVE,
952 "{family:?} {label:?} {helicity}"
953 );
954 let scale = below_gradient.iter().map(|v| v.norm()).sum::<f64>();
955 prop_assert_close!(
956 below_gradient,
957 above_gradient,
958 1e-12 * scale + f64::MIN_POSITIVE,
959 "{family:?} {label:?} {helicity} pullback"
960 );
961 }
962 }
963 Ok(())
964 }
965
966 fn check_unsolds_theorem(l: i32, theta: f64, phi: f64) -> Result<(), TestCaseError> {
970 let args = arguments([theta.into(), phi.into()]);
971 for family in [Family::HarmonicX, Family::HarmonicY, Family::HarmonicZ] {
972 let total = (-l..=l)
973 .map(|m| {
974 vector_wave(family, WaveLabel { l, m, pol: 0 }, args, false)
975 .unwrap()
976 .iter()
977 .map(Complex::norm_sqr)
978 .sum::<f64>()
979 })
980 .sum::<f64>();
981 let expected = f64::from(2 * l + 1) / (4.0 * PI);
982 prop_assert_close!(total, expected, 2e-11 * expected, "{family:?}");
983 }
984 Ok(())
985 }
986
987 #[test]
988 fn regular_spherical_origin_matches_the_cartesian_field() {
989 for (l, m) in (1..=3).flat_map(|l| (-l..=l).map(move |m| (l, m))) {
990 let label = WaveLabel { l, m, pol: 1 };
991 let mode = sw::Mode { l, m, pol: 1 };
992 for theta in [0.0, 0.7, PI] {
993 let local = vector_wave(
994 Family::Spherical(Radial::Regular),
995 label,
996 arguments([ZERO, theta.into(), 0.3.into()]),
997 false,
998 )
999 .unwrap();
1000 let actual =
1001 coordinates::vector(local, [0.0, theta, 0.3], Transform::SphToCar).unwrap();
1002 let expected =
1003 fields::spherical_wave(mode, 1.3.into(), [0.0; 3], false, Radial::Regular)
1004 .unwrap()
1005 .value;
1006 for (a, b) in actual.into_iter().zip(expected) {
1007 assert!((a - b).norm() < 1e-13, "{label:?} {theta}");
1008 }
1009 }
1010 }
1011 }
1012
1013 #[test]
1016 fn pole_pullbacks_are_one_sided_derivatives() {
1017 let g = [
1018 Complex::new(0.3, 0.2),
1019 Complex::new(-0.1, 0.4),
1020 Complex::new(0.2, -0.3),
1021 ];
1022 let h = 1e-5;
1023 let kr = Complex::new(1.2, 0.1);
1024 for (l, m) in (1..=3).flat_map(|l| (-l..=l).map(move |m| (l, m))) {
1025 for (pol, helicity) in [(0, false), (1, false), (1, true)] {
1026 let label = WaveLabel { l, m, pol };
1027 for (family, offset) in [
1028 (Family::HarmonicX, 0),
1029 (Family::HarmonicY, 0),
1030 (Family::HarmonicZ, 0),
1031 (Family::Spherical(Radial::Regular), 1),
1032 (Family::Spherical(Radial::Singular), 1),
1033 ] {
1034 for (pole, inward) in [(0.0, 1.0), (PI, -1.0)] {
1035 let args = |theta: f64| {
1036 let mut args = arguments([kr, theta.into(), 0.3.into()]);
1037 if offset == 0 {
1038 args.rotate_left(1);
1039 }
1040 args
1041 };
1042 let at =
1043 |t: f64| vector_wave(family, label, args(pole + t), helicity).unwrap();
1044 let pole_value = at(0.0);
1045 let gradient =
1046 vector_wave_pullback(family, label, args(pole), helicity, g).unwrap();
1047 let azimuthal: Complex = pole_value
1048 .iter()
1049 .zip(g)
1050 .map(|(x, g)| (Complex::i() * f64::from(m) * x).conj() * g)
1051 .sum();
1052 let scale = 1.0 + pole_value.iter().map(|x| x.norm()).sum::<f64>();
1053 let context = format!("{family:?} {label:?} {helicity} at {pole}");
1054 assert!(
1055 (gradient[offset + 1] - azimuthal).norm() < 1e-14 * scale,
1056 "{context}: {} != {azimuthal}",
1057 gradient[offset + 1]
1058 );
1059 let (one, two) = (at(inward * h), at(2.0 * inward * h));
1060 let polar: Complex = (0..3)
1061 .map(|c| {
1062 let derivative = inward
1063 * (-3.0 * pole_value[c] + 4.0 * one[c] - two[c])
1064 / (2.0 * h);
1065 derivative.conj() * g[c]
1066 })
1067 .sum();
1068 assert!(
1069 (gradient[offset] - polar).norm() < 1e-7 * (1.0 + polar.norm()),
1070 "{context}: {} != {polar}",
1071 gradient[offset]
1072 );
1073 }
1074 }
1075 }
1076 }
1077 }
1078}