1#![allow(clippy::indexing_slicing)] use crate::{
9 Complex, Error, Result,
10 basis::ModeLabel,
11 numerics::{Jet, broadcast, finite, parallel::Parallel},
12 special::{self, Bessel, Radial, SERIES_RADIUS, polar_trig, radial_jet},
13 sw::{self, Mode},
14};
15
16#[derive(Debug)]
22pub struct PolarTranslation {
23 terms: Vec<(i32, Complex)>,
25 order: i32,
27 degrees: [i32; 2],
29 radial: Radial,
30}
31
32impl PolarTranslation {
33 pub fn new(destination: Mode, source: Mode, helicity: bool, radial: Radial) -> Result<Self> {
35 destination.validate()?;
36 source.validate()?;
37 let order = source.m - destination.m;
38 let terms: Vec<_> = sw::terms(destination, source, helicity)
39 .into_iter()
40 .map(|(degree, term_order, weight)| {
41 debug_assert_eq!(term_order, order);
42 (degree, weight)
43 })
44 .collect();
45 let degrees = terms
46 .iter()
47 .fold([i32::MAX, 0], |[low, high], &(degree, _)| {
48 [low.min(degree), high.max(degree)]
49 });
50 Ok(Self {
51 terms,
52 order,
53 degrees,
54 radial,
55 })
56 }
57
58 fn radial_row<const N: usize>(&self, z: Jet<N>) -> Result<Option<Vec<Jet<N>>>> {
66 let [first, last] = self.degrees;
67 let last = last + i32::from(N > 0);
68 let offset = |p: i32| usize::try_from(p - first).unwrap_or_default();
70 let values = match self.radial {
71 Radial::Singular => {
72 if z.value.norm_sqr() == 0.0 {
73 return Err(Error::InvalidInput(
74 "outgoing spherical waves are singular at zero".into(),
75 ));
76 }
77 let degree = |p: i32| u32::try_from(p).unwrap_or_default();
78 special::spherical_hankels(degree(first), degree(last), z.value)?
79 }
80 Radial::Regular if N > 0 && z.value.norm() >= SERIES_RADIUS => {
81 let mut values = vec![Complex::default(); offset(last) + 1];
82 for &(degree, _) in &self.terms {
83 for p in [degree, degree + 1] {
84 let value = &mut values[offset(p)];
85 if *value == Complex::default() {
86 *value = special::bessel(f64::from(p), z.value, Bessel::J, true, 0)?;
87 }
88 }
89 }
90 values
91 }
92 Radial::Regular => return Ok(None),
93 };
94 if N == 0 {
95 return Ok(Some(values.into_iter().map(Jet::constant).collect()));
96 }
97 Ok(Some(
99 (first..)
100 .zip(values.windows(2))
101 .map(|(p, pair)| z.chain(pair[0], f64::from(p) * pair[0] / z.value - pair[1]))
102 .collect(),
103 ))
104 }
105
106 fn evaluate<const N: usize>(&self, args: [Complex; 3]) -> Result<Jet<N>> {
110 self.evaluate_jets(std::array::from_fn(|i| Jet::<N>::variable(args[i], i)))
111 }
112
113 fn evaluate_jets<const N: usize>(&self, args: [Jet<N>; 3]) -> Result<Jet<N>> {
115 if args.iter().any(|&v| !v.finite()) {
116 return Err(Error::InvalidInput(
117 "translation arguments must be finite".into(),
118 ));
119 }
120 if self.terms.is_empty() {
121 return Ok(Jet::default());
122 }
123 let [kr, theta, phi] = args;
124 let [cosine, sine] = polar_trig(theta);
125 let order = self.order;
126 let phase = sine.powi(order.abs()) * (Complex::i() * f64::from(order) * phi).exp();
127 let [low, high] = self.degrees;
128 let mut legendre =
129 Vec::with_capacity(usize::try_from(high - order.abs() + 1).unwrap_or_default());
130 special::legendre_factors(high, order, cosine, |p| legendre.push(p));
131 let row = self.radial_row(kr)?;
132 let mut result = Jet::default();
133 for &(degree, weight) in &self.terms {
134 let radial = match &row {
135 Some(row) => row[usize::try_from(degree - low).unwrap_or_default()],
136 None => radial_jet(degree, kr, self.radial, true)?,
137 };
138 let factor = legendre[usize::try_from(degree - order.abs()).unwrap_or_default()];
139 result += weight * radial * (phase * factor);
140 }
141 if !result.finite() {
142 return Err(Error::NonFinite(
143 "non-finite translation or derivative".into(),
144 ));
145 }
146 Ok(result)
147 }
148
149 #[inline]
157 #[must_use]
158 pub fn is_self_term(radial: Radial, kr: Complex) -> bool {
159 radial == Radial::Singular && kr.norm() < 1e-16
160 }
161
162 pub fn value(&self, args: [Complex; 3]) -> Result<Complex> {
164 Ok(self.evaluate::<0>(args)?.value)
165 }
166
167 pub fn pullback(&self, args: [Complex; 3], cotangent: Complex) -> Result<[Complex; 3]> {
169 if !finite(cotangent) {
170 return Err(Error::InvalidInput(
171 "translation cotangent must be finite".into(),
172 ));
173 }
174 if cotangent == Complex::default() {
175 self.value(args)?;
176 return Ok([Complex::default(); 3]);
177 }
178 Ok(self
179 .evaluate::<3>(args)?
180 .derivative
181 .map(|d| d.conj() * cotangent))
182 }
183}
184
185const PARALLEL: Parallel = Parallel::AtLeast(1024);
187const PLANS: Parallel = Parallel::AtLeast(64);
190
191#[derive(Debug)]
197pub struct PolarTranslationResidual {
198 plans: Vec<PolarTranslation>,
199 indices: Vec<usize>,
201 arguments: [Vec<Complex>; 3],
202 size: usize,
203}
204
205impl PolarTranslationResidual {
206 fn element(&self, i: usize) -> (&PolarTranslation, [Complex; 3]) {
207 (
208 &self.plans[broadcast::element(&self.indices, i)],
209 self.arguments.each_ref().map(|a| broadcast::element(a, i)),
210 )
211 }
212
213 pub fn pushforward(&self, tangents: [&[Complex]; 3]) -> Result<Vec<Complex>> {
215 broadcast::pushforward(
216 tangents,
217 self.size,
218 "translation tangents must be finite and match output",
219 PARALLEL,
220 |i, tangent| {
221 let (plan, args) = self.element(i);
222 let jets = std::array::from_fn(|a| Jet {
223 value: args[a],
224 derivative: [tangent[a]],
225 });
226 Ok(plan.evaluate_jets(jets)?.derivative[0])
227 },
228 )
229 }
230
231 pub fn pullback(&self, cotangent: &[Complex]) -> Result<[Vec<Complex>; 3]> {
234 broadcast::pullback(
235 cotangent,
236 self.size,
237 "translation cotangent must be finite and match output",
238 self.arguments.each_ref().map(Vec::len),
239 PARALLEL,
240 |i, &g| {
241 let (plan, args) = self.element(i);
242 plan.pullback(args, g)
243 },
244 )
245 }
246}
247
248pub fn polar_translation_array(
252 modes: Vec<[Mode; 2]>,
253 arguments: [Vec<Complex>; 3],
254 helicity: bool,
255 radial: Radial,
256) -> Result<(Vec<Complex>, PolarTranslationResidual)> {
257 let residual = PolarTranslationResidual::new(modes, arguments, helicity, radial)?;
258 let values = broadcast::map(residual.size, PARALLEL, |i| {
259 let (plan, args) = residual.element(i);
260 plan.value(args)
261 })?;
262 Ok((values, residual))
263}
264
265impl PolarTranslationResidual {
266 pub fn new(
268 modes: Vec<[Mode; 2]>,
269 arguments: [Vec<Complex>; 3],
270 helicity: bool,
271 radial: Radial,
272 ) -> Result<Self> {
273 let [a, b, c] = arguments.each_ref().map(Vec::len);
274 let size = broadcast::size(
275 &[modes.len(), a, b, c],
276 "translation arrays must have equal lengths or scalar inputs",
277 )?;
278 let mut lookup = std::collections::HashMap::new();
279 let mut distinct = Vec::new();
280 let indices = modes
281 .into_iter()
282 .map(|pair| {
283 *lookup.entry(pair).or_insert_with(|| {
284 distinct.push(pair);
285 distinct.len() - 1
286 })
287 })
288 .collect();
289 let plans = broadcast::map(distinct.len(), PLANS, |i| {
290 let [to, from] = distinct[i];
291 PolarTranslation::new(to, from, helicity, radial)
292 })?;
293 Ok(Self {
294 plans,
295 indices,
296 arguments,
297 size,
298 })
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::{Complex, Mode, PolarTranslation, Radial, sw};
308 use crate::{
309 special::{self, on_sphere},
310 test_support::{DEFAULT_CASES, degree_order, log_uniform, prop_assert_close, radial},
311 };
312 use proptest::{prelude::*, test_runner::TestCaseError};
313 use std::f64::consts::PI;
314
315 #[test]
318 fn recorded_polar_translation_cases() -> Result<(), TestCaseError> {
319 let kr = Complex::new(1.3, 0.2);
322 check_spherical(
325 Mode { l: 4, m: 3, pol: 1 },
326 Mode { l: 4, m: 3, pol: 1 },
327 kr,
328 [2.788_667_920_988_279_6, 0.0],
329 true,
330 Radial::Singular,
331 )?;
332 check_spherical(
333 Mode { l: 4, m: 2, pol: 1 },
334 Mode { l: 4, m: 2, pol: 1 },
335 kr,
336 [0.1, 0.0],
337 false,
338 Radial::Singular,
339 )?;
340 check_spherical(
341 Mode { l: 5, m: 3, pol: 1 },
342 Mode {
343 l: 3,
344 m: -2,
345 pol: 1,
346 },
347 kr,
348 [2.255_087_340_294_624_5, -0.095_390_013_154_433_45],
349 true,
350 Radial::Singular,
351 )?;
352 check_spherical(
353 Mode {
354 l: 3,
355 m: -2,
356 pol: 1,
357 },
358 Mode { l: 5, m: 3, pol: 1 },
359 kr,
360 [0.813_134_622_536_094_8, 0.0],
361 false,
362 Radial::Singular,
363 )?;
364 check_spherical(
367 Mode { l: 2, m: 2, pol: 0 },
368 Mode { l: 1, m: 0, pol: 0 },
369 Complex::new(1.0, 0.0),
370 [PI, 0.0],
371 false,
372 Radial::Regular,
373 )
374 }
375
376 proptest! {
377 #![proptest_config(ProptestConfig::with_cases(DEFAULT_CASES))]
378 #[test]
379 fn cartesian_translation_and_all_polar_adjoints(
380 (l, m) in degree_order(1..6), (lambda, mu) in degree_order(1..6), pol in 0_u8..2,
381 theta in 0.1_f64..3.0, phi in -3.0_f64..3.0, helicity in any::<bool>(),
382 radial in radial(),
383 ) {
384 let to = Mode { l: lambda, m: mu, pol: 1 };
385 let from = Mode { l, m, pol };
386 check_spherical(to, from, Complex::new(1.3, 0.2), [theta, phi], helicity, radial)?;
387 }
388 }
389
390 proptest! {
391 #![proptest_config(ProptestConfig::with_cases(2 * DEFAULT_CASES))]
394 #[test]
395 fn spherical_translation_across_argument_decades_and_poles(
396 (l, m) in degree_order(1..9), (lambda, mu) in degree_order(1..9),
397 pol in 0_u8..2, to_pol in 0_u8..2, magnitude in log_uniform(-2.0..1.5),
398 angle in -0.3_f64..0.3, theta in prop_oneof![Just(0.0), Just(PI), 0.0..PI],
399 phi in -3.0_f64..3.0, helicity in any::<bool>(), radial in radial(),
400 ) {
401 let to = Mode { l: lambda, m: mu, pol: to_pol };
402 let from = Mode { l, m, pol };
403 let kr = Complex::from_polar(magnitude, angle);
404 check_spherical(to, from, kr, [theta, phi], helicity, radial)?;
405 }
406 }
407
408 fn check_spherical(
419 to: Mode,
420 from: Mode,
421 kr: Complex,
422 [theta, phi]: [f64; 2],
423 helicity: bool,
424 radial: Radial,
425 ) -> Result<(), TestCaseError> {
426 let args = [kr, theta.into(), phi.into()];
427 let plan = PolarTranslation::new(to, from, helicity, radial).unwrap();
428 let (sine, cosine) = if theta % PI == 0.0 {
430 (0.0, theta.cos())
431 } else {
432 theta.sin_cos()
433 };
434 let position = [sine * phi.cos(), sine * phi.sin(), cosine];
435 let reference =
436 sw::cartesian_translation(to, from, kr, position, helicity, radial).unwrap();
437 let tangents = [
438 [cosine * phi.cos(), cosine * phi.sin(), -sine],
439 [-sine * phi.sin(), sine * phi.cos(), 0.0],
440 ];
441 let [d_theta, d_phi] = tangents.map(|t| {
442 (0..3)
443 .map(|axis| reference.position[axis] * t[axis])
444 .sum::<Complex>()
445 });
446 let scale = term_scale(&plan, kr, position);
447 let tolerance = 1e-13 * scale + f64::MIN_POSITIVE;
448 let value = plan.value(args).unwrap();
449 prop_assert_close!(value, reference.value, tolerance);
450 let g = Complex::new(0.3, -0.2);
451 let derivative = plan.pullback(args, g).unwrap().map(|d| d.conj() / g.conj());
452 prop_assert_close!(derivative[0], reference.k, tolerance, "kr");
453 prop_assert_close!(derivative[1], d_theta, tolerance, "theta");
454 prop_assert_close!(derivative[2], d_phi, tolerance, "phi");
455 let order = f64::from(from.m - to.m);
456 prop_assert_close!(
457 derivative[2],
458 Complex::i() * order * value,
459 1e-14 * (1.0 + order.abs()) * scale + f64::MIN_POSITIVE,
460 "azimuthal"
461 );
462 Ok(())
463 }
464
465 fn term_scale(plan: &PolarTranslation, kr: Complex, position: [f64; 3]) -> f64 {
471 plan.terms
472 .iter()
473 .map(|&(degree, weight)| {
474 let z = special::spherical_radial(degree.unsigned_abs(), kr, plan.radial).unwrap();
475 let (harmonic, gradient) = on_sphere(degree, plan.order, position);
476 let harmonic = harmonic.norm() + gradient.iter().map(|g| g.norm()).sum::<f64>();
477 weight.norm() * (z.value.norm() + (1.0 + kr.norm()) * z.first.norm()) * harmonic
478 })
479 .sum()
480 }
481}