treams_core/
vectorwaves.rs

1//! Vector waves one at a time: the vector spherical harmonics and the spherical,
2//! cylindrical and plane vector waves in their local component frames, with pullbacks.
3//!
4//! Upstream: `treams.special.vsh_X`, `vsh_Y`, `vsh_Z`, `vsw_*`, `vcw_*`, `vpw_*` and
5//! `sph_harm`.
6//!
7//! [`vector_wave`] evaluates one wave of a [`Family`] at six complex arguments,
8//! [`vector_wave_pullback`] gives the gradients of all six, and [`vector_wave_array`]
9//! broadcasts both over arrays of labels and arguments. Spherical waves are in spherical
10//! components `(r, theta, phi)`, cylindrical waves in `(rho, phi, z)` and plane waves in
11//! Cartesian components, as in treams.
12//!
13//! The inline `tests` module checks the waves against the Cartesian fields of
14//! [`crate::fields`], their pullbacks against finite differences, the small-argument
15//! branches and the normalization of the harmonics.
16#![allow(clippy::indexing_slicing)] // Fixed wave argument and component arrays.
17
18use 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/// The kind of vector wave [`vector_wave`] evaluates.
26///
27/// Polarization 0/1 selects M/N (upstream `*_M`/`*_N`), or negative/positive helicity
28/// (upstream `*_A`) when `helicity` is true. Harmonics ignore the polarization.
29///
30/// Upstream: `treams.special.vsh_*`, `vsw_*`, `vcw_*` and `vpw_*`; each variant names
31/// its functions.
32#[derive(Clone, Copy, Debug)]
33pub enum Family {
34    /// Tangential X harmonic in spherical components (upstream `vsh_X`).
35    HarmonicX,
36    /// Tangential Y harmonic in spherical components (upstream `vsh_Y`).
37    HarmonicY,
38    /// Radial Z harmonic, equal to i times the scalar spherical harmonic (upstream
39    /// `vsh_Z`).
40    HarmonicZ,
41    /// Spherical vector wave at `(kr, theta, phi)`: upstream `vsw_rM`, `vsw_rN` and
42    /// `vsw_rA` when regular, `vsw_M`, `vsw_N` and `vsw_A` when singular.
43    Spherical(Radial),
44    /// Cylindrical vector wave at `(kz, k_rho*rho, phi, z, k)`: upstream `vcw_rM`,
45    /// `vcw_rN` and `vcw_rA` when regular, `vcw_M`, `vcw_N` and `vcw_A` when singular.
46    Cylindrical(Radial),
47    /// Plane vector wave at `(kx, ky, kz, x, y, z)`: upstream `vpw_M`, `vpw_N` and
48    /// `vpw_A`.
49    Plane,
50}
51
52/// The integer labels of one vector wave.
53///
54/// Each [`Family`] reads some of the fields:
55///
56/// | Family | Reads |
57/// |---|---|
58/// | `HarmonicX`, `HarmonicY`, `HarmonicZ` | `l`, `m` |
59/// | `Spherical` | `l`, `m`, `pol` |
60/// | `Cylindrical` | `m`, `pol` |
61/// | `Plane` | `pol` |
62///
63/// Every evaluation accepts `0 <= l <= 128`, `|m| <= 128` and `pol` 0 or 1, whatever the
64/// family. Every harmonic and spherical wave with `|m| > l` is zero. At `l = 0` the
65/// spherical waves and the X and Y harmonics are zero, and the Z harmonic is `i Y_00`.
66#[derive(Clone, Copy, Debug, PartialEq, Eq)]
67pub struct WaveLabel {
68    /// Degree.
69    pub l: i32,
70    /// Order.
71    pub m: i32,
72    /// Polarization index: M/N (0/1), or negative/positive helicity.
73    pub pol: u8,
74}
75
76/// The normalized scalar spherical harmonic `Y_lm(theta, phi)` and, if `tangential`
77/// and `l > 0`, the angular functions `pi = m Y_lm / sin(theta)` and
78/// `tau = dY_lm / dtheta`, both divided by `sqrt(l (l + 1))`: `[Y, pi, tau]`, or
79/// `[Y, 0, 0]` otherwise.
80///
81/// The recurrence runs over normalized associated Legendre functions: it starts from
82/// the normalized diagonal term of degree `|m|` and steps the degree up to `l`. Every
83/// term stays of order one, which avoids the factorial overflow of the unnormalized
84/// functions at high orders. For `m = 0` the formula for `tau` used at other orders
85/// would divide by `sin(theta)`, so a second recurrence gives the derivative with
86/// respect to `cos(theta)` instead.
87fn 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    // Normalize the Legendre recurrence before evaluating it. This removes
97    // factorial overflow for high orders and repeated adjacent-order recurrences.
98    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
152/// The scalar harmonic `Y` and, if `tangential`, the angular functions `pi` and
153/// `tau` of `label`; `None` when `|m| > l`.
154fn 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
163/// The tangential X and Y vector harmonics from `[Y, pi, tau]`.
164fn 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
171/// Spherical M and N waves at `(kr, theta, phi)`, or their helicity combinations.
172fn 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
208/// Cylindrical M and N waves at `(kz, krr, phi, z, k)`, or their helicity combinations;
209/// `krr` is `k_rho rho`, as in upstream `vcw_*`.
210fn 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    // The principal root, as upstream `vcw_N` takes it. For a gain medium it can be the
246    // negative of the root with a nonnegative imaginary part that `cw` uses.
247    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
257/// Plane wave `p exp(i k·r)` at `(kx, ky, kz, x, y, z)`, with the chain rule through
258/// the polarization `p(k)`.
259fn 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            // 128 is MAX_DEGREE.
294            "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/// Evaluate a vector wave in its local component frame.
323///
324/// Harmonics use `(theta,phi)`, spherical waves `(kr,theta,phi)`, cylinders
325/// `(kz,k_rho*rho,phi,z,k)`, and planes `(kx,ky,kz,x,y,z)`. Unused arguments are zero.
326///
327/// Upstream: `treams.special.vsh_X`, `vsh_Y`, `vsh_Z`, `vsw_*`, `vcw_*` and `vpw_*`.
328#[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
344/// The gradients of all six complex arguments of [`vector_wave`] for a `cotangent` of
345/// the wave. A real physical input takes the real part of its gradient.
346pub 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
372/// Directional derivative in the wave's six complex arguments. One jet slot
373/// propagates the supplied direction through the same kernels as the value.
374pub 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}
401/// [`vector_wave_pullback`] with derivatives in the first `N` arguments, the ones the
402/// family reads; the others get zero.
403fn 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
425/// Vector-wave arrays evaluate in parallel from this many elements.
426const PARALLEL: Parallel = Parallel::AtLeast(1024);
427
428/// What [`vector_wave_array`] saves for its pullback: the family, the labels, the
429/// convention and the arguments, each one value for all outputs or one per output.
430///
431/// The pullback computes each output's gradients on its own and adds the gradients of a
432/// scalar argument in output order on the calling thread, so its result does not
433/// depend on the thread count.
434#[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    /// Retain broadcast inputs without evaluating the waves.
444    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    /// Evaluate the values of the retained wave inputs.
465    pub fn values(&self) -> Result<Vec<[Complex; 3]>> {
466        let plane = match self.fixed_plane() {
467            Some((label, k)) => {
468                // Check the shared label and wavevector once before the plane sweep.
469                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    /// The label and wave vector that several plane waves share, and hence their
493    /// one polarization, when they differ only in their points.
494    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    /// The point `(x, y, z)` of plane wave `i`.
504    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    /// Directional derivative of every wave from scalar or elementwise argument
509    /// tangents. It carries one direction, irrespective of the number of arguments.
510    pub fn pushforward(&self, tangents: [&[Complex]; 6]) -> Result<Vec<[Complex; 3]>> {
511        // A common plane-wave polarization is evaluated once for all positions.
512        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    /// The gradients of the six arguments from one complex vector cotangent per output;
541    /// an argument given as one value for all outputs gets the sum of its gradients.
542    pub fn pullback(&self, cotangent: &[[Complex; 3]]) -> Result<[Vec<Complex>; 6]> {
543        // Waves of one direction share the polarization jet; each element then
544        // reads only its point.
545        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
570/// Directional product of a shared polarization and the point-dependent phase.
571fn 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
595/// Pullback to `(k, r)` of a plane wave `p exp(i k·r)`, given the polarization jet
596/// `p` in the wave vector.
597fn 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
627/// [`vector_wave`] over arrays of labels and arguments, each one value for all outputs
628/// or one per output; the residual keeps only the inputs.
629///
630/// Upstream: `treams.special.vsh_*`, `vsw_*`, `vcw_*` and `vpw_*` on broadcast arrays.
631pub 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/// Scalar spherical harmonic `Y_l^m(theta, phi)` at the polar angle `theta` and the
642/// azimuth `phi`.
643///
644/// `Y` is `-i` times the radial component of the [`Family::HarmonicZ`] wave
645/// ([`sph_harm_from_vsh_z`]).
646///
647/// Upstream: `treams.special.sph_harm`, which takes `(m, l, phi, theta)`.
648/// Differences: degrees outside `[0, 128]`, orders with `|m| > 128` and non-finite angles
649/// give an error, where treams returns a value or NaN; other orders with `|m| > l` give
650/// zero, where treams returns NaN.
651#[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/// The scalar spherical harmonic `Y = -i Z_r` of a [`Family::HarmonicZ`] wave `Z`
665/// (`treams.special.vsh_Z`), whose only nonzero component is the radial one, `Z_r = i Y`.
666#[inline]
667#[must_use]
668pub fn sph_harm_from_vsh_z([z_r, _, _]: [Complex; 3]) -> Complex {
669    -Complex::i() * z_r
670}
671
672/// Pullback of [`sph_harm_from_vsh_z`]: the [`Family::HarmonicZ`] wave cotangent
673/// `[i cotangent, 0, 0]` for the scalar-harmonic `cotangent`.
674#[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    // Fewer cases than DEFAULT_CASES: each case checks several families and
701    // polarizations, and the adjoint check takes central differences in all six
702    // arguments of eight families.
703    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    // Each case sums 2l + 1 harmonics of degree up to 128 for three families.
746    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    /// The six wave arguments with `leading` in front and zeros after.
760    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    /// Spherical and cylindrical waves in their local frames, rotated to Cartesian
765    /// components, equal the Cartesian fields of `fields`, on both sides of the
766    /// small-argument series switch `|z| = SERIES_RADIUS` (0.5).
767    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    /// Central differences of `vector_wave` along a complex `direction` in argument `a`,
806    /// paired with the cotangent `g`, and the scale of the paired terms.
807    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    /// Every family's pullback, paired with a complex direction in each of the six
830    /// arguments, is the holomorphic directional derivative of `vector_wave`; the
831    /// radial argument lies on either side of the small-argument threshold.
832    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    /// Regular spherical and cylindrical waves and their pullbacks agree just below
905    /// and just above `|z| = SERIES_RADIUS` (0.5), where their radial functions switch
906    /// from power series to Bessel evaluations.
907    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                // Nine and four ulps from the threshold, respectively.
943                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    /// Unsöld's theorem `Σ_m |X_lm|² = (2l + 1) / 4π` for the normalized vector
967    /// spherical harmonics at high degree, including both poles and a polar angle
968    /// of `1e-10`.
969    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    /// At the poles the azimuthal cotangent is exactly that of `∂_φ X = i m X`, and
1014    /// the polar cotangent is the one-sided derivative from inside `(0, π)`.
1015    #[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}