1#![allow(clippy::indexing_slicing)] use nalgebra::DMatrix;
8
9use super::{field::phase, polarization::Direction};
10use crate::{
11 Complex, Error, Result,
12 basis::{ModeLabel, MultipoleBasis},
13 numerics::{
14 self, Jet, finite,
15 parallel::{try_fill_chunks, try_fold_ordered},
16 },
17 special::{check_pol, polarized_angular},
18 sw::Mode,
19};
20
21fn spherical_prefactor(mode: Mode) -> Complex {
24 let (l, m) = (mode.l, mode.m);
25 let normalization = 2.0
26 * (std::f64::consts::PI * f64::from(2 * l + 1) / f64::from(l * (l + 1))).sqrt()
27 * crate::special::factorial_ratio_sqrt(l, m);
28 normalization * Complex::i().powi(l)
29}
30
31fn spherical_coefficient<const N: usize>(
35 mode: Mode,
36 prefactor: Complex,
37 vector: [Complex; 3],
38 direction: &Direction<N>,
39 pol: u8,
40 helicity: bool,
41) -> Jet<N> {
42 if helicity && mode.pol != pol {
43 return Jet::default();
44 }
45 let m = mode.m;
46 let axis = direction.transverse.value == Complex::default();
47 let azimuth = if axis {
48 Jet::constant(1.0)
49 } else {
50 (direction.xy[0] - Complex::i() * direction.xy[1]).powi(m)
51 };
52 let cosine = if axis {
53 Jet::constant(if (vector[2] / direction.k.value).re >= 0.0 {
54 1.0
55 } else {
56 -1.0
57 })
58 } else {
59 Jet::variable(vector[2], 2) / direction.k
60 };
61 let sine = direction.transverse / direction.k;
64 let angular = polarized_angular(mode.l, m, [cosine, sine], [mode.pol, pol], helicity);
65 prefactor * azimuth * angular
66}
67
68pub fn to_sw(mode: Mode, vector: [Complex; 3], pol: u8, helicity: bool) -> Result<Complex> {
72 mode.validate()?;
73 check_pol(pol)?;
74 let direction = Direction::<0>::new(vector)?;
75 let prefactor = spherical_prefactor(mode);
76 Ok(spherical_coefficient(mode, prefactor, vector, &direction, pol, helicity).value)
77}
78
79#[cfg(test)]
82pub(crate) fn reference_spherical_expansion(
83 basis: &crate::sw::Basis,
84 vector: [Complex; 3],
85 pol: u8,
86 helicity: bool,
87) -> Result<Vec<Complex>> {
88 basis.validate()?;
89 check_pol(pol)?;
90 let direction = Direction::<0>::new(vector)?;
91 let labels = AngularLabels::spherical(&basis.modes);
92 let angular = labels.evaluate_spherical(&basis.modes, vector, &direction, pol, helicity);
93 let phases: Vec<_> = basis.positions.iter().map(|&r| phase(vector, r)).collect();
94 Ok(basis
95 .modes
96 .iter()
97 .zip(&labels.slot)
98 .map(|(&(pidx, _), &slot)| phases[pidx] * angular[slot].value)
99 .collect())
100}
101
102pub(crate) fn cylindrical_mode_matches(mode: crate::cw::Mode, kz: Complex, pol: u8) -> bool {
109 mode.pol == pol
110 && kz.im == 0.0
111 && (mode.kz - kz.re).abs() <= 16.0 * f64::EPSILON * mode.kz.abs().max(kz.re.abs())
112}
113
114fn cylindrical_angular<const N: usize>(m: i32, direction: &Direction<N>) -> Jet<N> {
117 if direction.transverse.value == Complex::default() {
118 Jet::constant(Complex::i().powi(m))
119 } else {
120 (Complex::i() * direction.xy[0] + direction.xy[1]).powi(m)
121 }
122}
123
124fn cylindrical_coefficient<const N: usize>(
127 mode: crate::cw::Mode,
128 vector: [Complex; 3],
129 direction: &Direction<N>,
130 pol: u8,
131) -> Jet<N> {
132 if cylindrical_mode_matches(mode, vector[2], pol) {
133 cylindrical_angular(mode.m, direction)
134 } else {
135 Jet::default()
136 }
137}
138
139#[allow(clippy::float_cmp)] pub fn to_cw(mode: crate::cw::Mode, vector: [Complex; 3], pol: u8) -> Result<Complex> {
148 mode.validate()?;
149 if pol > 1 || vector.iter().any(|&v| !finite(v)) || vector[2].im != 0.0 {
150 return Err(Error::InvalidInput(
151 "finite wavevector, real axial component and polarization 0/1 required".into(),
152 ));
153 }
154 if mode.pol != pol || mode.kz != vector[2].re {
155 return Ok(Complex::default());
156 }
157 Ok(cylindrical_angular(mode.m, &Direction::<0>::new(vector)?).value)
158}
159
160#[cfg(test)]
163pub(crate) fn reference_cylindrical_expansion(
164 basis: &crate::cw::Basis,
165 vector: [Complex; 3],
166 pol: u8,
167) -> Result<Vec<Complex>> {
168 basis.validate()?;
169 let direction = Direction::<0>::new(vector)?;
170 if pol > 1 || vector[2].im != 0.0 {
171 return Err(Error::InvalidInput(
172 "cylindrical expansion requires real axial wavenumber and polarization 0 or 1".into(),
173 ));
174 }
175 Ok(basis
176 .modes
177 .iter()
178 .map(|&(pidx, mode)| {
179 phase(vector, basis.positions[pidx])
180 * cylindrical_coefficient(mode, vector, &direction, pol).value
181 })
182 .collect())
183}
184
185struct AngularLabels {
188 slot: Vec<usize>,
190 first: Vec<(usize, Complex)>,
192}
193
194impl AngularLabels {
195 fn new<K: std::hash::Hash + Eq>(
196 len: usize,
197 key: impl Fn(usize) -> K,
198 prefactor: impl Fn(usize) -> Complex,
199 ) -> Self {
200 let mut index = std::collections::HashMap::new();
201 let mut first = Vec::new();
202 let slot = (0..len)
203 .map(|i| {
204 *index.entry(key(i)).or_insert_with(|| {
205 first.push((i, prefactor(i)));
206 first.len() - 1
207 })
208 })
209 .collect();
210 Self { slot, first }
211 }
212
213 fn spherical(modes: &[(usize, Mode)]) -> Self {
214 Self::new(
215 modes.len(),
216 |i| modes[i].1,
217 |i| spherical_prefactor(modes[i].1),
218 )
219 }
220
221 fn cylindrical(modes: &[(usize, crate::cw::Mode)]) -> Self {
222 Self::new(
223 modes.len(),
224 |i| (modes[i].1.kz.to_bits(), modes[i].1.m, modes[i].1.pol),
225 |_| Complex::default(),
226 )
227 }
228
229 fn of(basis: &MultipoleBasis) -> Self {
230 match basis {
231 MultipoleBasis::Spherical(b) => Self::spherical(&b.modes),
232 MultipoleBasis::Cylindrical(b) => Self::cylindrical(&b.modes),
233 }
234 }
235
236 fn evaluate_spherical<const N: usize>(
238 &self,
239 modes: &[(usize, Mode)],
240 vector: [Complex; 3],
241 direction: &Direction<N>,
242 pol: u8,
243 helicity: bool,
244 ) -> Vec<Jet<N>> {
245 self.first
246 .iter()
247 .map(|&(i, prefactor)| {
248 spherical_coefficient(modes[i].1, prefactor, vector, direction, pol, helicity)
249 })
250 .collect()
251 }
252
253 fn evaluate<const N: usize>(
255 &self,
256 basis: &MultipoleBasis,
257 vector: [Complex; 3],
258 direction: &Direction<N>,
259 pol: u8,
260 helicity: bool,
261 ) -> Vec<Jet<N>> {
262 match basis {
263 MultipoleBasis::Spherical(b) => {
264 self.evaluate_spherical(&b.modes, vector, direction, pol, helicity)
265 }
266 MultipoleBasis::Cylindrical(b) => self
267 .first
268 .iter()
269 .map(|&(i, _)| cylindrical_coefficient(b.modes[i].1, vector, direction, pol))
270 .collect(),
271 }
272 }
273}
274
275#[derive(Debug)]
283pub struct ExpansionResidual {
284 pub(super) basis: MultipoleBasis,
285 pub(super) vectors: Vec<[Complex; 3]>,
286 pub(super) polarizations: Vec<u8>,
287 pub(super) helicity: bool,
288 pub(super) fixed_vectors: bool,
289}
290#[derive(Debug)]
292pub struct ExpansionGradient {
293 pub positions: Vec<[f64; 3]>,
295 pub vectors: Vec<[Complex; 3]>,
298}
299pub fn expansion(
309 basis: impl Into<MultipoleBasis>,
310 vectors: Vec<[Complex; 3]>,
311 polarizations: Vec<u8>,
312 helicity: bool,
313 fixed_vectors: bool,
314) -> Result<(DMatrix<Complex>, ExpansionResidual)> {
315 let basis = basis.into();
316 basis.validate()?;
317 if vectors.is_empty()
318 || vectors.len() != polarizations.len()
319 || polarizations.iter().any(|&p| p > 1)
320 || (matches!(basis, MultipoleBasis::Cylindrical(_))
321 && vectors.iter().any(|k| k[2].im != 0.0))
322 {
323 return Err(Error::InvalidInput(
324 "require nonempty plane vectors and matching polarizations 0/1; cylindrical axial labels must be real".into(),
325 ));
326 }
327 let labels = AngularLabels::of(&basis);
328 let mut value = numerics::zeros(basis.len(), vectors.len())?;
329 try_fill_chunks(
330 value.as_mut_slice(),
331 basis.len(),
332 vectors.len() > 1,
333 |j, column| -> Result<()> {
334 let direction = Direction::<0>::new(vectors[j])?;
335 let angular =
336 labels.evaluate(&basis, vectors[j], &direction, polarizations[j], helicity);
337 let phases: Vec<_> = basis
338 .positions()
339 .iter()
340 .map(|&p| phase(vectors[j], p))
341 .collect();
342 for (i, (out, &slot)) in column.iter_mut().zip(&labels.slot).enumerate() {
343 *out = angular[slot].value * phases[basis.position_pol(i).0];
344 }
345 Ok(())
346 },
347 )?;
348 if value.iter().any(|&v| !finite(v)) {
349 return Err(Error::NonFinite("plane expansion overflow".into()));
350 }
351 Ok((
352 value,
353 ExpansionResidual {
354 basis,
355 vectors,
356 polarizations,
357 helicity,
358 fixed_vectors,
359 },
360 ))
361}
362impl ExpansionResidual {
363 #[must_use]
365 pub fn shape(&self) -> (usize, usize) {
366 (self.basis.len(), self.vectors.len())
367 }
368
369 pub fn validate_tangents(
371 &self,
372 positions: &[[f64; 3]],
373 vectors: &[[Complex; 3]],
374 ) -> Result<()> {
375 if positions.len() != self.basis.positions().len()
376 || vectors.len() != self.vectors.len()
377 || positions.iter().flatten().any(|r| !r.is_finite())
378 || vectors.iter().flatten().any(|&k| !finite(k))
379 {
380 return Err(Error::InvalidInput(
381 "invalid plane-expansion tangents".into(),
382 ));
383 }
384 Ok(())
385 }
386
387 pub fn pushforward(
390 &self,
391 positions: &[[f64; 3]],
392 vectors: &[[Complex; 3]],
393 ) -> Result<DMatrix<Complex>> {
394 self.validate_tangents(positions, vectors)?;
395 let labels = AngularLabels::of(&self.basis);
396 let axes = if matches!(self.basis, MultipoleBasis::Spherical(_)) {
397 3
398 } else {
399 2
400 };
401 let mut tangent = numerics::zeros(self.basis.len(), self.vectors.len())?;
402 try_fill_chunks(
403 tangent.as_mut_slice(),
404 self.basis.len(),
405 self.vectors.len() > 1,
406 |j, column| -> Result<()> {
407 if self.fixed_vectors || vectors[j][..axes].iter().all(|&v| v == Complex::default())
408 {
409 self.push_column(
410 &labels,
411 j,
412 &Direction::<0>::new(self.vectors[j])?,
413 positions,
414 vectors,
415 column,
416 );
417 } else {
418 self.push_column(
419 &labels,
420 j,
421 &Direction::<3>::new(self.vectors[j])?,
422 positions,
423 vectors,
424 column,
425 );
426 }
427 Ok(())
428 },
429 )?;
430 if tangent.iter().any(|&v| !finite(v)) {
431 return Err(Error::NonFinite("plane expansion tangent overflow".into()));
432 }
433 Ok(tangent)
434 }
435
436 fn push_column<const N: usize>(
438 &self,
439 labels: &AngularLabels,
440 j: usize,
441 direction: &Direction<N>,
442 positions: &[[f64; 3]],
443 vectors: &[[Complex; 3]],
444 column: &mut [Complex],
445 ) {
446 let vector = self.vectors[j];
447 let angular = labels.evaluate(
448 &self.basis,
449 vector,
450 direction,
451 self.polarizations[j],
452 self.helicity,
453 );
454 let axes = if matches!(self.basis, MultipoleBasis::Spherical(_)) {
455 3
456 } else {
457 2
458 };
459 let phases: Vec<_> = self
460 .basis
461 .positions()
462 .iter()
463 .enumerate()
464 .map(|(p, &position)| {
465 let angle: Complex = (0..3).map(|a| vector[a] * positions[p][a]).sum::<Complex>()
466 + (0..N.min(axes))
467 .map(|a| vectors[j][a] * position[a])
468 .sum::<Complex>();
469 (phase(vector, position), Complex::i() * angle)
470 })
471 .collect();
472 for (i, (out, &slot)) in column.iter_mut().zip(&labels.slot).enumerate() {
473 let angular = angular[slot];
474 let (phase, angle) = phases[self.basis.position_pol(i).0];
475 let derivative: Complex = (0..N.min(axes))
476 .map(|a| angular.derivative[a] * vectors[j][a])
477 .sum();
478 *out = phase * (derivative + angular.value * angle);
479 }
480 }
481 pub fn pullback(&self, cotangent: &DMatrix<Complex>) -> Result<ExpansionGradient> {
483 if cotangent.shape() != self.shape() || cotangent.iter().any(|&v| !finite(v)) {
484 return Err(Error::InvalidInput(
485 "invalid plane-expansion cotangent".into(),
486 ));
487 }
488 let fixed_vectors = self.fixed_vectors;
489 let labels = AngularLabels::of(&self.basis);
490 let mut vectors = vec![[Complex::default(); 3]; self.vectors.len()];
493 let modes: Vec<_> = self.vectors.iter().zip(&mut vectors).collect();
494 let positions = try_fold_ordered(
495 modes,
496 true,
497 || vec![[0.0; 3]; self.basis.positions().len()],
498 |mut positions, j, (&vector, vector_gradient)| -> Result<_> {
499 if fixed_vectors {
502 let direction = Direction::<0>::new(vector)?;
503 self.contract(
504 &labels,
505 j,
506 &direction,
507 cotangent,
508 &mut positions,
509 vector_gradient,
510 );
511 } else {
512 let direction = Direction::<3>::new(vector)?;
513 self.contract(
514 &labels,
515 j,
516 &direction,
517 cotangent,
518 &mut positions,
519 vector_gradient,
520 );
521 }
522 Ok(positions)
523 },
524 |mut total, partial| {
525 for (a, b) in total.iter_mut().flatten().zip(partial.iter().flatten()) {
526 *a += b;
527 }
528 total
529 },
530 )?;
531 Ok(ExpansionGradient { positions, vectors })
532 }
533
534 fn contract<const N: usize>(
537 &self,
538 labels: &AngularLabels,
539 j: usize,
540 direction: &Direction<N>,
541 cotangent: &DMatrix<Complex>,
542 positions: &mut [[f64; 3]],
543 vector_gradient: &mut [Complex; 3],
544 ) {
545 let vector = self.vectors[j];
546 let angular = labels.evaluate(
547 &self.basis,
548 vector,
549 direction,
550 self.polarizations[j],
551 self.helicity,
552 );
553 let phases: Vec<_> = self
554 .basis
555 .positions()
556 .iter()
557 .map(|&p| phase(vector, p))
558 .collect();
559 let axes = if matches!(self.basis, MultipoleBasis::Spherical(_)) {
561 3
562 } else {
563 2
564 };
565 for (i, &slot) in labels.slot.iter().enumerate() {
566 let p = self.basis.position_pol(i).0;
567 let position = self.basis.positions()[p];
568 let angular = &angular[slot];
569 let phase = phases[p];
570 let value = phase * angular.value;
571 for axis in 0..3 {
572 positions[p][axis] +=
573 (cotangent[(i, j)].conj() * value * Complex::i() * vector[axis]).re;
574 if N > 0 && axis < axes {
575 vector_gradient[axis] += cotangent[(i, j)]
576 * (phase
577 * (angular.derivative[axis]
578 + Complex::i() * position[axis] * angular.value))
579 .conj();
580 }
581 }
582 }
583 }
584}
585
586#[cfg(test)]
587mod tests {
588 use super::expansion;
589 use crate::{
590 Complex, Error, sw,
591 test_support::{assert_same_bits_on_pools, bits, patterned, spherical_basis},
592 };
593
594 #[test]
596 fn overflowing_plane_expansions_report_nonfinite() {
597 let k = vec![[0.6.into(), 0.0.into(), Complex::new(0.8, 2.0)]];
598 let basis = |z| spherical_basis(1, [0.0, 0.0, z]);
599 let at = |z| expansion(basis(z), k.clone(), vec![0], true, false);
600 assert!(matches!(at(-400.0), Err(Error::NonFinite(_))));
601 let (_, residual) = at(-350.0).unwrap();
602 let tangent = residual.pushforward(&[[0.0, 0.0, 1e10]], &[[0.0.into(); 3]]);
603 assert!(matches!(tangent, Err(Error::NonFinite(_))));
604 }
605
606 #[test]
609 fn pullback_does_not_depend_on_the_thread_count() {
610 let modes = sw::modes(3).unwrap();
611 let basis = sw::Basis {
612 modes: (0..2)
613 .flat_map(|p| modes.iter().map(move |&mode| (p, mode)))
614 .collect(),
615 positions: vec![[0.1, -0.2, 0.3], [-0.3, 0.2, -0.1]],
616 };
617 let count = 100_u32;
618 let vectors: Vec<[Complex; 3]> = (0..count)
619 .map(|j| {
620 let t = f64::from(j);
621 let (theta, phi) = (1.55 + 1.3 * (0.61 * t).sin(), 2.3 * t);
622 [
623 theta.sin() * phi.cos(),
624 theta.sin() * phi.sin(),
625 theta.cos(),
626 ]
627 .map(|x| Complex::new(1.2 * x, 0.0))
628 })
629 .collect();
630 let polarizations: Vec<u8> = (0..count).map(|j| u8::from(j % 2 == 0)).collect();
631 let g = patterned(basis.modes.len(), vectors.len(), 0.5);
632 assert_same_bits_on_pools(|| {
633 let (_, residual) = expansion(
634 basis.clone(),
635 vectors.clone(),
636 polarizations.clone(),
637 true,
638 false,
639 )
640 .unwrap();
641 let g = residual.pullback(&g).unwrap();
642 bits(&[&g.positions, &g.vectors])
643 });
644 }
645}