1#![allow(clippy::indexing_slicing)] use std::{
13 array::from_fn,
14 f64::consts::{FRAC_1_SQRT_2, PI},
15};
16
17use crate::{
18 Complex, Error, Result,
19 basis::{ModeLabel, MultipoleBasis, validate_wavenumbers},
20 cw,
21 numerics::{
22 self, Jet, finite,
23 parallel::{try_fill_chunks, try_fold_ordered, try_map},
24 },
25 saved::{self, Reader, SavedState, Writer},
26 special::{
27 Radial, SERIES_RADIUS, Solid, SolidTable, bessel, helicity_sign, polarized_wave, solid,
28 spherical_hankels,
29 },
30 sw::Mode,
31};
32
33#[derive(Clone, Copy, Debug, Default)]
38pub struct VectorWave {
39 pub value: [Complex; 3],
41 pub position: [[Complex; 3]; 3],
43 pub k: [Complex; 3],
45}
46
47pub fn cylindrical_wave(
53 mode: cw::Mode,
54 k: Complex,
55 position: [f64; 3],
56 helicity: bool,
57 radial: Radial,
58) -> Result<VectorWave> {
59 let [m, n] = cylindrical_components::<SPATIAL_AND_K>(mode, k, position, radial)?;
60 Ok(pack_cylindrical(polarized_wave(m, n, mode.pol, helicity)))
61}
62
63fn pack_cylindrical<const N: usize>(fields: [Jet<N>; 3]) -> VectorWave {
66 VectorWave {
67 value: fields.map(|v| v.value),
68 position: fields.map(|v| from_fn(|i| v.derivative.get(i).copied().unwrap_or_default())),
69 k: fields.map(|v| v.derivative.get(3).copied().unwrap_or_default()),
70 }
71}
72
73fn cylindrical_components<const N: usize>(
76 mode: cw::Mode,
77 k: Complex,
78 position: [f64; 3],
79 radial: Radial,
80) -> Result<[[Jet<N>; 3]; 2]> {
81 mode.validate()?;
82 if !finite(k) || k == Complex::default() || position.iter().any(|x| !x.is_finite()) {
83 return Err(Error::InvalidInput(
84 "require a finite nonzero wavenumber and finite field position".into(),
85 ));
86 }
87 let r: [Jet<N>; 3] = from_fn(|i| Jet::variable(position[i], i));
88 let k = Jet::<N>::variable(k, 3);
89 let kz = Jet::<N>::variable(mode.kz, 4);
90 let transverse = cw::transverse(k, kz);
91 if transverse.value == Complex::default() {
92 return Err(Error::InvalidInput(
93 "cylindrical cutoff requires a limiting formulation".into(),
94 ));
95 }
96 let rho = position[0].hypot(position[1]);
97 let [lower, center, upper] =
98 if radial == Radial::Regular && (transverse.value * rho).norm() < SERIES_RADIUS {
99 [mode.m - 1, mode.m, mode.m + 1].map(|m| cw::regular_harmonic(m, transverse, r, kz))
100 } else {
101 if rho == 0.0 {
102 return Err(Error::NonFinite(
103 "outgoing cylindrical wave is singular on the axis".into(),
104 ));
105 }
106 let radius = (r[0] * r[0] + r[1] * r[1]).sqrt();
107 let argument = transverse * radius;
108 let radial = crate::special::cylindrical_radial(mode.m, argument.value, radial)?;
109 let value = argument.chain(radial.value, radial.first);
110 let derivative = argument.chain(radial.first, radial.second);
111 let azimuth = (r[0] + Complex::i() * r[1]) / radius;
112 let phase = (Complex::i() * kz * r[2]).exp() * azimuth.powi(mode.m);
113 let adjacent = f64::from(mode.m) * value / argument;
114 [
115 (adjacent + derivative) * phase / azimuth,
116 value * phase,
117 (adjacent - derivative) * phase * azimuth,
118 ]
119 };
120 let m = [
121 0.5 * Complex::i() * (lower + upper),
122 0.5 * (upper - lower),
123 Jet::default(),
124 ];
125 let n = [
126 0.5 * Complex::i() * kz / k * (lower - upper),
127 -0.5 * kz / k * (lower + upper),
128 transverse / k * center,
129 ];
130 Ok([m, n])
131}
132
133fn cross(a: [Complex; 3], b: [Complex; 3]) -> [Complex; 3] {
134 [
135 a[1] * b[2] - a[2] * b[1],
136 a[2] * b[0] - a[0] * b[2],
137 a[0] * b[1] - a[1] * b[0],
138 ]
139}
140
141fn scaled_radial(l: u32, k: Complex, r: f64, radial: Radial) -> Result<Complex> {
145 let x = k * r;
146 if radial == Radial::Regular && x.norm() < SERIES_RADIUS {
147 let mut denominator = 1.0;
148 for i in 0..=l {
149 denominator *= f64::from(2 * i + 1);
150 }
151 let mut coefficient = k.powu(l) / denominator;
152 let mut sum = Complex::default();
153 for q in 0..12 {
157 sum += coefficient * x.powu(2 * q);
158 coefficient /= -2.0 * f64::from(q + 1) * f64::from(2 * l + 2 * q + 3);
159 }
160 return Ok(sum);
161 }
162 if x.norm_sqr() == 0.0 {
163 return Err(Error::InvalidInput(
164 "outgoing spherical waves are singular at zero".into(),
165 ));
166 }
167 Ok(bessel(f64::from(l), x, radial.into(), true, 0)? / r.powf(f64::from(l)))
168}
169
170fn spherical_arguments(k: Complex, position: [f64; 3], radial: Radial) -> (Complex, [f64; 3], f64) {
174 let mut scale = position.iter().map(|x| x.abs()).fold(0.0, f64::max);
175 if radial == Radial::Regular || scale == 0.0 {
179 scale = scale.max(1.0 / k.re.abs().max(k.im.abs()));
180 }
181 (k * scale, position.map(|x| x / scale), scale)
182}
183
184pub fn spherical_wave(
190 mode: Mode,
191 k: Complex,
192 position: [f64; 3],
193 helicity: bool,
194 radial: Radial,
195) -> Result<VectorWave> {
196 spherical_wave_impl::<true>(mode, k, position, helicity, radial)
197}
198
199fn spherical_wave_impl<const DERIVATIVES: bool>(
201 mode: Mode,
202 k: Complex,
203 position: [f64; 3],
204 helicity: bool,
205 radial: Radial,
206) -> Result<VectorWave> {
207 mode.validate()?;
208 if !finite(k) || k.norm_sqr() == 0.0 || position.iter().any(|v| !v.is_finite()) {
209 return Err(Error::InvalidInput(
210 "require a finite nonzero wavenumber and finite position".into(),
211 ));
212 }
213 let (k, position, scale) = spherical_arguments(k, position, radial);
214 let r2 = position.iter().map(|v| v * v).sum::<f64>();
215 let l = mode.l.unsigned_abs();
216 let radials = [
217 scaled_radial(l, k, r2.sqrt(), radial)?,
218 scaled_radial(l + 1, k, r2.sqrt(), radial)?,
219 if DERIVATIVES {
220 scaled_radial(l + 2, k, r2.sqrt(), radial)?
221 } else {
222 Complex::default()
223 },
224 ];
225 let solid = solid::<DERIVATIVES>(mode.l, mode.m, position);
226 let parts = spherical_parts::<DERIVATIVES>(mode.l, k, position, r2, &solid, radials, scale);
227 Ok(combine::<DERIVATIVES>(
228 &parts,
229 normalization(mode.l, mode.m),
230 weights(mode.pol, helicity),
231 ))
232}
233
234fn spherical_parts<const DERIVATIVES: bool>(
244 l: i32,
245 k: Complex,
246 position: [f64; 3],
247 r2: f64,
248 solid: &Solid,
249 [c, e, f]: [Complex; 3],
250 scale: f64,
251) -> [VectorWave; 2] {
252 let degree = f64::from(l);
253 let d = (degree + 1.0) / k * c - r2 * e;
254 let vector = position.map(|v| Complex::new(v, 0.0));
255 let rotation = cross(vector, solid.gradient);
256 let mut n = VectorWave {
257 value: from_fn(|i| d * solid.gradient[i] + degree * e * solid.value * position[i]),
258 ..VectorWave::default()
259 };
260 let mut m = VectorWave {
261 value: from_fn(|i| c * rotation[i]),
262 ..VectorWave::default()
263 };
264 if DERIVATIVES {
265 let ck = degree / k * c - r2 * e;
266 let ek = (degree + 1.0) / k * e - r2 * f;
267 let dk = (degree + 1.0) / k * ck - (degree + 1.0) / k.powu(2) * c - r2 * ek;
268 n.k =
269 from_fn(|i| scale * (dk * solid.gradient[i] + degree * ek * solid.value * position[i]));
270 m.k = from_fn(|i| scale * ck * rotation[i]);
271 for axis in 0..3 {
272 let ca = -k * e * position[axis];
273 let ea = -k * f * position[axis];
274 let da = (degree + 1.0) / k * ca - 2.0 * position[axis] * e - r2 * ea;
275 let mut unit = [Complex::default(); 3];
276 unit[axis] = Complex::new(1.0, 0.0);
277 let first = cross(unit, solid.gradient);
278 let second = cross(vector, from_fn(|i| solid.hessian[i][axis]));
279 for i in 0..3 {
280 n.position[i][axis] = (da * solid.gradient[i]
281 + d * solid.hessian[i][axis]
282 + degree
283 * (ea * solid.value * position[i]
284 + e * solid.gradient[axis] * position[i]
285 + if i == axis {
286 e * solid.value
287 } else {
288 Complex::default()
289 }))
290 / scale;
291 m.position[i][axis] = (ca * rotation[i] + c * (first[i] + second[i])) / scale;
292 }
293 }
294 }
295 [n, m]
296}
297
298fn normalization(l: i32, m: i32) -> Complex {
301 let degree = f64::from(l);
302 Complex::i()
303 * ((2.0 * degree + 1.0) / (4.0 * PI * degree * (degree + 1.0))).sqrt()
304 * crate::special::factorial_ratio_sqrt(l, m)
305}
306
307fn weights(pol: u8, helicity: bool) -> [f64; 2] {
311 if helicity {
312 [FRAC_1_SQRT_2, helicity_sign(pol) * FRAC_1_SQRT_2]
313 } else if pol == 1 {
314 [1.0, 0.0]
315 } else {
316 [0.0, 1.0]
317 }
318}
319
320fn combine<const DERIVATIVES: bool>(
324 [n, m]: &[VectorWave; 2],
325 normalization: Complex,
326 [weight_n, weight_m]: [f64; 2],
327) -> VectorWave {
328 let combine = |n: Complex, m: Complex| normalization * (weight_n * n - weight_m * m);
329 let mut wave = VectorWave {
330 value: from_fn(|i| combine(n.value[i], m.value[i])),
331 ..VectorWave::default()
332 };
333 if DERIVATIVES {
334 wave.k = from_fn(|i| combine(n.k[i], m.k[i]));
335 wave.position = from_fn(|i| from_fn(|a| combine(n.position[i][a], m.position[i][a])));
336 }
337 wave
338}
339
340#[derive(Debug)]
343pub struct FieldResidual {
344 waves: WaveSet,
345 points: Vec<[f64; 3]>,
346 coefficients: Vec<Complex>,
347}
348
349pub(crate) const VALUES: usize = 0;
352pub(crate) const SPATIAL_AND_K: usize = 4;
355pub(crate) const WITH_AXIAL: usize = 5;
358
359#[derive(Debug)]
362pub(crate) struct WaveSet {
363 basis: MultipoleBasis,
364 ks: [Complex; 2],
365 helicity: bool,
366 radial: Radial,
367 normalizations: Vec<Complex>,
369 radial_table_len: usize,
372 harmonic_tables: Vec<Option<SolidTable>>,
375 radial_ranges: Vec<Option<(u32, u32)>>,
378}
379
380pub(crate) struct SampleCache<const N: usize> {
387 radials: Vec<Option<Complex>>,
388 harmonics: [Option<(usize, Vec<Complex>)>; 2],
392 spherical: Option<(PartsKey, [VectorWave; 2])>,
393 cylindrical: Option<(ComponentsKey, [[Jet<N>; 3]; 2])>,
394}
395
396type PartsKey = (usize, usize, i32, i32);
398type ComponentsKey = (usize, usize, i32, u64);
400
401fn sampled_waves(
404 basis: MultipoleBasis,
405 points: Vec<[f64; 3]>,
406 ks: [Complex; 2],
407 helicity: bool,
408 radial: Radial,
409) -> Result<(WaveSet, Vec<[f64; 3]>)> {
410 basis.validate()?;
411 validate_points(&points)?;
412 Ok((
413 WaveSet::of_valid_basis(basis, ks, helicity, radial)?,
414 points,
415 ))
416}
417
418fn validate_points(points: &[[f64; 3]]) -> Result<()> {
419 if points.iter().flatten().any(|v| !v.is_finite()) {
420 return Err(Error::InvalidInput("field points must be finite".into()));
421 }
422 Ok(())
423}
424
425fn validate_coefficients(basis: &MultipoleBasis, coefficients: &[Complex]) -> Result<()> {
426 if coefficients.len() != basis.len() || coefficients.iter().any(|&v| !finite(v)) {
427 return Err(Error::InvalidInput(
428 "require one finite field coefficient per mode".into(),
429 ));
430 }
431 Ok(())
432}
433
434impl WaveSet {
435 fn write_state(&self, writer: &mut Writer) {
436 saved::write_basis(writer, &self.basis);
437 for k in self.ks {
438 writer.complex(k);
439 }
440 writer.byte(u8::from(self.helicity));
441 saved::write_radial(writer, self.radial);
442 }
443
444 fn read_state(reader: &mut Reader<'_>) -> Result<Self> {
447 let basis = saved::read_basis(reader)?;
448 let ks = [reader.complex()?, reader.complex()?];
449 let helicity = match reader.byte()? {
450 0 => false,
451 1 => true,
452 _ => return Err(saved::invalid()),
453 };
454 let radial = saved::read_radial(reader)?;
455 Self::new(basis, ks, helicity, radial)
456 }
457
458 pub(crate) fn new(
459 basis: MultipoleBasis,
460 ks: [Complex; 2],
461 helicity: bool,
462 radial: Radial,
463 ) -> Result<Self> {
464 basis.validate()?;
465 Self::of_valid_basis(basis, ks, helicity, radial)
466 }
467
468 fn of_valid_basis(
470 basis: MultipoleBasis,
471 ks: [Complex; 2],
472 helicity: bool,
473 radial: Radial,
474 ) -> Result<Self> {
475 validate_wavenumbers(ks, helicity, false)?;
476 let (harmonic_tables, radial_ranges) = match &basis {
477 MultipoleBasis::Spherical(basis) => {
478 let mut labels = vec![Vec::new(); 2 * basis.positions.len()];
479 let mut runs = vec![0; basis.positions.len()];
480 let mut previous = None;
481 for &(pidx, mode) in &basis.modes {
482 if previous != Some(pidx) {
483 runs[pidx] += 1;
484 previous = Some(pidx);
485 }
486 let wavenumber = if ks[0] == ks[1] {
487 0
488 } else {
489 usize::from(mode.pol)
490 };
491 labels[2 * pidx + wavenumber].push((mode.l, mode.m));
492 }
493 labels
494 .into_iter()
495 .enumerate()
496 .map(|(key, mut labels)| {
497 labels.sort_unstable();
498 labels.dedup();
499 let order = labels.last().map_or(0, |&(l, _)| l);
500 let work: i32 = labels.iter().map(|&(l, _)| l).sum();
504 let dense = work >= 2 * (order + 1).pow(2);
505 let first = labels.first().map_or(0, |&(l, _)| l);
506 let radial_sequence = !labels.is_empty()
509 && labels.windows(2).all(|pair| pair[1].0 <= pair[0].0 + 2);
510 (
511 (runs[key / 2] == 1 && dense).then(|| SolidTable::new(order)),
512 radial_sequence.then_some((first.unsigned_abs(), order.unsigned_abs())),
513 )
514 })
515 .unzip()
516 }
517 MultipoleBasis::Cylindrical(_) => (Vec::new(), Vec::new()),
518 };
519 let (normalizations, radial_table_len) = match &basis {
520 MultipoleBasis::Spherical(b) => (
521 b.modes
522 .iter()
523 .map(|&(_, mode)| normalization(mode.l, mode.m))
524 .collect(),
525 b.modes
526 .iter()
527 .map(|&(_, mode)| mode.l.unsigned_abs() as usize + 3)
528 .max()
529 .unwrap_or_default(),
530 ),
531 MultipoleBasis::Cylindrical(_) => (Vec::new(), 0),
532 };
533 Ok(Self {
534 basis,
535 ks,
536 helicity,
537 radial,
538 normalizations,
539 radial_table_len,
540 harmonic_tables,
541 radial_ranges,
542 })
543 }
544
545 pub(crate) fn cache<const N: usize>(&self) -> SampleCache<N> {
546 SampleCache {
547 radials: vec![None; 2 * self.basis.positions().len() * self.radial_table_len],
548 harmonics: [None, None],
549 spherical: None,
550 cylindrical: None,
551 }
552 }
553
554 fn wavenumber(&self, pol: u8) -> usize {
556 if self.ks[0] == self.ks[1] {
557 0
558 } else {
559 usize::from(pol)
560 }
561 }
562
563 pub(crate) fn wave<const N: usize>(
566 &self,
567 i: usize,
568 point: [f64; 3],
569 cache: &mut SampleCache<N>,
570 ) -> Result<(VectorWave, [Complex; 3])> {
571 match &self.basis {
572 MultipoleBasis::Spherical(basis) => {
573 let (pidx, mode) = basis.modes[i];
574 let wavenumber = self.wavenumber(mode.pol);
575 let key = (pidx, wavenumber, mode.l, mode.m);
576 let parts = match cache.spherical {
577 Some((cached, parts)) if cached == key => parts,
578 _ => {
579 let parts = self.cached_parts(pidx, wavenumber, mode, point, cache)?;
580 cache.spherical = Some((key, parts));
581 parts
582 }
583 };
584 let normalization = self.normalizations[i];
585 let weights = weights(mode.pol, self.helicity);
586 let wave = if N > VALUES {
587 combine::<true>(&parts, normalization, weights)
588 } else {
589 combine::<false>(&parts, normalization, weights)
590 };
591 Ok((wave, [Complex::default(); 3]))
592 }
593 MultipoleBasis::Cylindrical(basis) => {
594 let (pidx, mode) = basis.modes[i];
595 let wavenumber = self.wavenumber(mode.pol);
596 let key = (pidx, wavenumber, mode.m, mode.kz.to_bits());
597 let components = match cache.cylindrical {
598 Some((cached, components)) if cached == key => components,
599 _ => {
600 let position = from_fn(|a| point[a] - basis.positions[pidx][a]);
601 let k = self.ks[wavenumber];
602 let components = cylindrical_components(mode, k, position, self.radial)?;
603 cache.cylindrical = Some((key, components));
604 components
605 }
606 };
607 let [m, n] = components;
608 let fields = polarized_wave(m, n, mode.pol, self.helicity);
609 let axial = fields.map(|v| v.derivative.get(4).copied().unwrap_or_default());
610 Ok((pack_cylindrical(fields), axial))
611 }
612 }
613 }
614
615 fn cached_parts<const N: usize>(
619 &self,
620 pidx: usize,
621 wavenumber: usize,
622 mode: Mode,
623 point: [f64; 3],
624 cache: &mut SampleCache<N>,
625 ) -> Result<[VectorWave; 2]> {
626 let position: [f64; 3] = from_fn(|a| point[a] - self.basis.positions()[pidx][a]);
627 let k = self.ks[wavenumber];
628 if k.norm_sqr() == 0.0 || position.iter().any(|v| !v.is_finite()) {
629 return Err(Error::InvalidInput(
630 "require a finite nonzero wavenumber and finite position".into(),
631 ));
632 }
633 let (k, position, scale) = spherical_arguments(k, position, self.radial);
634 let r2 = position.iter().map(|v| v * v).sum::<f64>();
635 let key = 2 * pidx + wavenumber;
636 let table = key * self.radial_table_len;
637 if self.radial == Radial::Singular
638 && let Some((first, last)) = self.radial_ranges[key]
639 && cache.radials[table + first as usize].is_none()
640 {
641 let radius = r2.sqrt();
642 let argument = k * radius;
643 if finite(argument) && argument.norm_sqr() != 0.0 {
646 let last = last + if N > VALUES { 2 } else { 1 };
647 for (degree, value) in (first..=last).zip(spherical_hankels(first, last, argument)?)
648 {
649 if !finite(value) {
650 return Err(Error::SpecialFunction("non-finite Bessel result".into()));
651 }
652 cache.radials[table + degree as usize] =
653 Some(value / radius.powf(f64::from(degree)));
654 }
655 }
656 }
657 let mut radial = |order: u32| -> Result<Complex> {
658 let entry = &mut cache.radials[table + order as usize];
659 if let Some(value) = *entry {
660 return Ok(value);
661 }
662 let value = scaled_radial(order, k, r2.sqrt(), self.radial)?;
663 *entry = Some(value);
664 Ok(value)
665 };
666 let l = mode.l.unsigned_abs();
667 let radials = [
668 radial(l)?,
669 radial(l + 1)?,
670 if N > VALUES {
671 radial(l + 2)?
672 } else {
673 Complex::default()
674 },
675 ];
676 let solid = if let Some(table) = &self.harmonic_tables[key] {
677 let stride = if N > VALUES { 13 } else { 4 };
678 let slot = &mut cache.harmonics[wavenumber];
679 if slot.as_ref().is_some_and(|&(cached, _)| cached != pidx) {
680 *slot = None;
681 }
682 let (_, entries) = slot.get_or_insert_with(|| {
683 let mut entries = vec![Complex::default(); stride * table.len()];
684 let mut emit = |index, _, solid: Solid| {
685 let start = stride * index;
686 entries[start] = solid.value;
687 entries[start + 1..start + 4].copy_from_slice(&solid.gradient);
688 if N > VALUES {
689 entries[start + 4..start + 13]
690 .copy_from_slice(solid.hessian.as_flattened());
691 }
692 };
693 if N > VALUES {
694 table.visit_solid::<true>(position, &mut emit);
695 } else {
696 table.visit_solid::<false>(position, &mut emit);
697 }
698 (pidx, entries)
699 });
700 let start =
701 stride * usize::try_from(mode.l * mode.l + mode.l + mode.m).unwrap_or_default();
702 Solid {
703 value: entries[start],
704 gradient: from_fn(|axis| entries[start + 1 + axis]),
705 hessian: if N > VALUES {
706 from_fn(|i| from_fn(|j| entries[start + 4 + 3 * i + j]))
707 } else {
708 [[Complex::default(); 3]; 3]
709 },
710 }
711 } else if N > VALUES {
712 solid::<true>(mode.l, mode.m, position)
713 } else {
714 solid::<false>(mode.l, mode.m, position)
715 };
716 Ok(if N > VALUES {
717 spherical_parts::<true>(mode.l, k, position, r2, &solid, radials, scale)
718 } else {
719 spherical_parts::<false>(mode.l, k, position, r2, &solid, radials, scale)
720 })
721 }
722
723 fn sampled_operator<const N: usize>(
726 &self,
727 points: &[[f64; 3]],
728 value: impl Fn(usize, usize, VectorWave, [Complex; 3]) -> [Complex; 3] + Sync,
729 ) -> Result<nalgebra::DMatrix<Complex>> {
730 let samples = points.len();
731 let mut output = numerics::zeros(3 * samples, self.basis.len())?;
732 if samples == 0 {
733 return Ok(output);
734 }
735 let block = samples
738 .div_ceil(crate::threads::current_num_threads().saturating_mul(8))
739 .clamp(1, 64);
740 let mut blocks: Vec<Vec<&mut [Complex]>> = (0..samples.div_ceil(block))
741 .map(|_| {
742 let mut columns = Vec::new();
743 numerics::reserve(&mut columns, self.basis.len()).map(|()| columns)
744 })
745 .collect::<Result<_>>()?;
746 for column in output.as_mut_slice().chunks_exact_mut(3 * samples) {
747 for (rows, columns) in column.chunks_mut(3 * block).zip(&mut blocks) {
748 columns.push(rows);
749 }
750 }
751 try_fill_chunks(
752 &mut blocks,
753 1,
754 samples > block,
755 |index, chunk| -> Result<()> {
756 let columns = &mut chunk[0];
757 for (sample, &point) in points.iter().skip(index * block).take(block).enumerate() {
758 let mut cache = self.cache::<N>();
759 for (mode, column) in columns.iter_mut().enumerate() {
760 let (wave, axial) = self.wave(mode, point, &mut cache)?;
761 let result = value(index * block + sample, mode, wave, axial);
762 column[3 * sample..3 * sample + 3].copy_from_slice(&result);
763 }
764 }
765 Ok(())
766 },
767 )?;
768 Ok(output)
769 }
770
771 fn pullback<const N: usize>(
774 &self,
775 points: &[[f64; 3]],
776 coefficients: Option<&[Complex]>,
777 cotangent: impl Fn(usize, usize) -> [Complex; 3] + Sync,
778 ) -> Result<(FieldGradient, Vec<f64>)> {
779 let with_axial = N == WITH_AXIAL;
780 if with_axial && matches!(self.basis, MultipoleBasis::Spherical(_)) {
781 return Err(Error::InvalidInput(
782 "axial field derivatives require a cylindrical basis".into(),
783 ));
784 }
785 let zero = || Accumulator {
786 gradient: FieldGradient {
787 coefficients: vec![Complex::default(); coefficients.map_or(0, <[Complex]>::len)],
788 points: Vec::new(),
789 positions: vec![[0.0; 3]; self.basis.positions().len()],
790 ks: [Complex::default(); 2],
791 },
792 axial: vec![0.0; if with_axial { self.basis.len() } else { 0 }],
793 };
794 let mut point_gradients = vec![[0.0; 3]; points.len()];
795 let samples: Vec<_> = point_gradients.iter_mut().zip(points).collect();
796 let Accumulator {
797 mut gradient,
798 axial,
799 } = try_fold_ordered(
800 samples,
801 true,
802 zero,
803 |mut sum, sample, (point_gradient, point)| {
804 let mut cache = self.cache::<N>();
805 for i in 0..self.basis.len() {
806 let (pidx, pol) = self.basis.position_pol(i);
807 let (wave, axial_derivative) = self.wave(i, *point, &mut cache)?;
808 let amplitude = coefficients.map_or(Complex::new(1.0, 0.0), |c| c[i]);
809 let g = cotangent(sample, i);
810 for (component, &cot) in g.iter().enumerate() {
811 if coefficients.is_some() {
812 sum.gradient.coefficients[i] += wave.value[component].conj() * cot;
813 }
814 sum.gradient.ks[pol] += (amplitude * wave.k[component]).conj() * cot;
815 if with_axial {
816 sum.axial[i] +=
817 (cot.conj() * amplitude * axial_derivative[component]).re;
818 }
819 for (axis, point_derivative) in point_gradient.iter_mut().enumerate() {
820 let derivative =
821 (cot.conj() * amplitude * wave.position[component][axis]).re;
822 *point_derivative += derivative;
823 sum.gradient.positions[pidx][axis] -= derivative;
824 }
825 }
826 }
827 Ok(sum)
828 },
829 Accumulator::merge,
830 )?;
831 gradient.points = point_gradients;
832 Ok((gradient, axial))
833 }
834}
835
836#[derive(Clone, Copy)]
839struct GeometryTangent<'a> {
840 points: &'a [[f64; 3]],
841 positions: &'a [[f64; 3]],
842 ks: [Complex; 2],
843 axial: Option<&'a [f64]>,
844}
845
846impl GeometryTangent<'_> {
847 fn validate(&self, waves: &WaveSet, samples: usize) -> Result<()> {
848 if self.points.len() != samples
849 || self.positions.len() != waves.basis.positions().len()
850 || self
851 .points
852 .iter()
853 .chain(self.positions)
854 .flatten()
855 .any(|v| !v.is_finite())
856 || self.ks.iter().any(|&v| !finite(v))
857 {
858 return Err(Error::InvalidInput("invalid field geometry tangent".into()));
859 }
860 if let Some(axial) = self.axial {
861 if matches!(waves.basis, MultipoleBasis::Spherical(_)) {
862 return Err(Error::InvalidInput(
863 "axial field derivatives require a cylindrical basis".into(),
864 ));
865 }
866 if axial.len() != waves.basis.len() || axial.iter().any(|v| !v.is_finite()) {
867 return Err(Error::InvalidInput("invalid field axial tangent".into()));
868 }
869 }
870 Ok(())
871 }
872
873 fn contract(
874 &self,
875 waves: &WaveSet,
876 sample: usize,
877 mode: usize,
878 wave: VectorWave,
879 axial: [Complex; 3],
880 ) -> [Complex; 3] {
881 let (position, polarization) = waves.basis.position_pol(mode);
882 from_fn(|component| {
883 wave.k[component] * self.ks[polarization]
884 + (0..3)
885 .map(|axis| {
886 wave.position[component][axis]
887 * (self.points[sample][axis] - self.positions[position][axis])
888 })
889 .sum::<Complex>()
890 + self
891 .axial
892 .map_or_else(Complex::default, |kz| axial[component] * kz[mode])
893 })
894 }
895}
896
897struct Accumulator {
900 gradient: FieldGradient,
901 axial: Vec<f64>,
902}
903
904impl Accumulator {
905 fn merge(mut self, other: Self) -> Self {
906 let (gradient, sum) = (other.gradient, &mut self.gradient);
907 for (a, b) in sum.coefficients.iter_mut().zip(gradient.coefficients) {
908 *a += b;
909 }
910 for (a, b) in sum
911 .positions
912 .iter_mut()
913 .flatten()
914 .zip(gradient.positions.iter().flatten())
915 {
916 *a += b;
917 }
918 for (a, b) in sum.ks.iter_mut().zip(gradient.ks) {
919 *a += b;
920 }
921 for (a, b) in self.axial.iter_mut().zip(other.axial) {
922 *a += b;
923 }
924 self
925 }
926}
927
928#[derive(Debug)]
931pub struct FieldGradient {
932 pub coefficients: Vec<Complex>,
934 pub points: Vec<[f64; 3]>,
936 pub positions: Vec<[f64; 3]>,
938 pub ks: [Complex; 2],
941}
942
943pub fn field(
948 basis: impl Into<MultipoleBasis>,
949 coefficients: Vec<Complex>,
950 points: Vec<[f64; 3]>,
951 ks: [Complex; 2],
952 helicity: bool,
953 radial: Radial,
954) -> Result<(Vec<[Complex; 3]>, FieldResidual)> {
955 let basis = basis.into();
956 validate_coefficients(&basis, &coefficients)?;
957 let (waves, points) = sampled_waves(basis, points, ks, helicity, radial)?;
958 let value = try_map(points.len(), points.len() > 1, |index| -> Result<_> {
959 let mut cache = waves.cache::<VALUES>();
960 let mut value = [Complex::default(); 3];
961 for (i, &litude) in coefficients.iter().enumerate() {
962 let (wave, _) = waves.wave(i, points[index], &mut cache)?;
963 for (v, f) in value.iter_mut().zip(wave.value) {
964 *v += amplitude * f;
965 }
966 }
967 Ok(value)
968 })?;
969 Ok((
970 value,
971 FieldResidual {
972 waves,
973 points,
974 coefficients,
975 },
976 ))
977}
978
979impl FieldResidual {
980 pub fn state_size(
983 modes: usize,
984 positions: usize,
985 points: usize,
986 cylindrical: bool,
987 ) -> Result<usize> {
988 OperatorResidual::state_size(modes, positions, points, cylindrical)?
989 .checked_add(modes.checked_mul(16).ok_or_else(saved::invalid)?)
990 .ok_or_else(saved::invalid)
991 }
992
993 #[must_use]
995 pub fn shape(&self) -> (usize, usize) {
996 (self.points.len(), 3)
997 }
998
999 #[must_use]
1001 pub fn input_sizes(&self) -> (usize, usize) {
1002 (self.coefficients.len(), self.waves.basis.positions().len())
1003 }
1004
1005 pub fn pushforward(
1008 &self,
1009 coefficients: &[Complex],
1010 points: &[[f64; 3]],
1011 positions: &[[f64; 3]],
1012 ks: [Complex; 2],
1013 ) -> Result<Vec<[Complex; 3]>> {
1014 self.pushforward_impl::<SPATIAL_AND_K>(
1015 coefficients,
1016 GeometryTangent {
1017 points,
1018 positions,
1019 ks,
1020 axial: None,
1021 },
1022 )
1023 }
1024
1025 pub fn pushforward_axial(
1027 &self,
1028 coefficients: &[Complex],
1029 points: &[[f64; 3]],
1030 positions: &[[f64; 3]],
1031 ks: [Complex; 2],
1032 kz: &[f64],
1033 ) -> Result<Vec<[Complex; 3]>> {
1034 self.pushforward_impl::<WITH_AXIAL>(
1035 coefficients,
1036 GeometryTangent {
1037 points,
1038 positions,
1039 ks,
1040 axial: Some(kz),
1041 },
1042 )
1043 }
1044
1045 fn pushforward_impl<const N: usize>(
1046 &self,
1047 coefficients: &[Complex],
1048 tangent: GeometryTangent<'_>,
1049 ) -> Result<Vec<[Complex; 3]>> {
1050 if coefficients.len() != self.coefficients.len() || coefficients.iter().any(|&v| !finite(v))
1051 {
1052 return Err(Error::InvalidInput(
1053 "invalid field coefficient tangent".into(),
1054 ));
1055 }
1056 tangent.validate(&self.waves, self.points.len())?;
1057 try_map(self.points.len(), self.points.len() > 1, |sample| {
1058 let mut cache = self.waves.cache::<N>();
1059 let mut value = [Complex::default(); 3];
1060 for (mode, &coefficient) in coefficients.iter().enumerate() {
1061 let (wave, axial) = self.waves.wave(mode, self.points[sample], &mut cache)?;
1062 let derivative = tangent.contract(&self.waves, sample, mode, wave, axial);
1063 for component in 0..3 {
1064 value[component] += coefficient * wave.value[component]
1065 + self.coefficients[mode] * derivative[component];
1066 }
1067 }
1068 Ok(value)
1069 })
1070 }
1071
1072 pub fn pullback(&self, cotangent: &[[Complex; 3]]) -> Result<FieldGradient> {
1082 self.pullback_impl::<SPATIAL_AND_K>(cotangent)
1083 .map(|(gradient, _)| gradient)
1084 }
1085
1086 pub fn pullback_axial(&self, cotangent: &[[Complex; 3]]) -> Result<(FieldGradient, Vec<f64>)> {
1089 self.pullback_impl::<WITH_AXIAL>(cotangent)
1090 }
1091
1092 fn pullback_impl<const N: usize>(
1093 &self,
1094 cotangent: &[[Complex; 3]],
1095 ) -> Result<(FieldGradient, Vec<f64>)> {
1096 if cotangent.len() != self.points.len() || cotangent.iter().flatten().any(|&g| !finite(g)) {
1097 return Err(Error::InvalidInput("invalid field cotangent".into()));
1098 }
1099 self.waves
1100 .pullback::<N>(&self.points, Some(&self.coefficients), |sample, _| {
1101 cotangent[sample]
1102 })
1103 }
1104}
1105
1106impl SavedState for FieldResidual {
1107 fn save_state(&self) -> Result<Vec<u8>> {
1108 let (modes, positions) = self.input_sizes();
1109 let cylindrical = matches!(self.waves.basis, MultipoleBasis::Cylindrical(_));
1110 let mut writer = Writer::new(Self::state_size(
1111 modes,
1112 positions,
1113 self.points.len(),
1114 cylindrical,
1115 )?);
1116 write_sampled_state(&mut writer, &self.waves, &self.points);
1117 for &coefficient in &self.coefficients {
1118 writer.complex(coefficient);
1119 }
1120 Ok(writer.finish())
1121 }
1122
1123 fn from_state(bytes: &[u8]) -> Result<Self> {
1124 let (waves, points, mut reader) = read_sampled_state(bytes, true)?;
1125 let coefficients = (0..waves.basis.len())
1126 .map(|_| reader.complex())
1127 .collect::<Result<Vec<_>>>()?;
1128 reader.finish()?;
1129 validate_coefficients(&waves.basis, &coefficients)?;
1130 Ok(Self {
1131 waves,
1132 points,
1133 coefficients,
1134 })
1135 }
1136}
1137
1138fn write_sampled_state(writer: &mut Writer, waves: &WaveSet, points: &[[f64; 3]]) {
1139 writer.usize(points.len());
1140 waves.write_state(writer);
1141 for &coordinate in points.as_flattened() {
1142 writer.f64(coordinate);
1143 }
1144}
1145
1146fn read_sampled_state(
1149 bytes: &[u8],
1150 coefficients: bool,
1151) -> Result<(WaveSet, Vec<[f64; 3]>, Reader<'_>)> {
1152 let mut header = Reader::new(bytes);
1153 let samples = header.usize()?;
1154 let (modes, positions, cylindrical) = saved::read_basis_dimensions(&mut header)?;
1155 let size = if coefficients {
1156 FieldResidual::state_size(modes, positions, samples, cylindrical)?
1157 } else {
1158 OperatorResidual::state_size(modes, positions, samples, cylindrical)?
1159 };
1160 if bytes.len() != size {
1161 return Err(saved::invalid());
1162 }
1163 let mut reader = Reader::new(bytes);
1164 reader.usize()?;
1165 let waves = WaveSet::read_state(&mut reader)?;
1166 let points = (0..samples)
1167 .map(|_| Ok([reader.f64()?, reader.f64()?, reader.f64()?]))
1168 .collect::<Result<Vec<_>>>()?;
1169 validate_points(&points)?;
1170 Ok((waves, points, reader))
1171}
1172
1173#[derive(Debug)]
1176pub struct OperatorResidual {
1177 waves: WaveSet,
1178 points: Vec<[f64; 3]>,
1179}
1180
1181pub fn operator(
1187 basis: impl Into<MultipoleBasis>,
1188 points: Vec<[f64; 3]>,
1189 ks: [Complex; 2],
1190 helicity: bool,
1191 radial: Radial,
1192) -> Result<(nalgebra::DMatrix<Complex>, OperatorResidual)> {
1193 let (waves, points) = sampled_waves(basis.into(), points, ks, helicity, radial)?;
1194 let value = waves.sampled_operator::<VALUES>(&points, |_, _, wave, _| wave.value)?;
1195 Ok((value, OperatorResidual { waves, points }))
1196}
1197
1198impl OperatorResidual {
1199 pub fn state_size(
1202 modes: usize,
1203 positions: usize,
1204 points: usize,
1205 cylindrical: bool,
1206 ) -> Result<usize> {
1207 let basis = if cylindrical {
1208 saved::cw_basis_size(modes, positions)?
1209 } else {
1210 saved::sw_basis_size(modes, positions)?
1211 };
1212 basis
1214 .checked_add(43)
1215 .and_then(|size| {
1216 points
1217 .checked_mul(24)
1218 .and_then(|points| size.checked_add(points))
1219 })
1220 .ok_or_else(saved::invalid)
1221 }
1222
1223 #[must_use]
1225 pub fn shape(&self) -> (usize, usize) {
1226 (3 * self.points.len(), self.waves.basis.len())
1227 }
1228
1229 #[must_use]
1231 pub fn position_count(&self) -> usize {
1232 self.waves.basis.positions().len()
1233 }
1234
1235 pub fn pushforward(
1238 &self,
1239 points: &[[f64; 3]],
1240 positions: &[[f64; 3]],
1241 ks: [Complex; 2],
1242 ) -> Result<nalgebra::DMatrix<Complex>> {
1243 self.pushforward_impl::<SPATIAL_AND_K>(GeometryTangent {
1244 points,
1245 positions,
1246 ks,
1247 axial: None,
1248 })
1249 }
1250
1251 pub fn pushforward_axial(
1253 &self,
1254 points: &[[f64; 3]],
1255 positions: &[[f64; 3]],
1256 ks: [Complex; 2],
1257 kz: &[f64],
1258 ) -> Result<nalgebra::DMatrix<Complex>> {
1259 self.pushforward_impl::<WITH_AXIAL>(GeometryTangent {
1260 points,
1261 positions,
1262 ks,
1263 axial: Some(kz),
1264 })
1265 }
1266
1267 fn pushforward_impl<const N: usize>(
1268 &self,
1269 tangent: GeometryTangent<'_>,
1270 ) -> Result<nalgebra::DMatrix<Complex>> {
1271 tangent.validate(&self.waves, self.points.len())?;
1272 self.waves
1273 .sampled_operator::<N>(&self.points, |sample, mode, wave, axial| {
1274 tangent.contract(&self.waves, sample, mode, wave, axial)
1275 })
1276 }
1277
1278 pub fn pullback(&self, cotangent: &nalgebra::DMatrix<Complex>) -> Result<FieldGradient> {
1285 self.pullback_impl::<SPATIAL_AND_K>(cotangent)
1286 .map(|(gradient, _)| gradient)
1287 }
1288
1289 pub fn pullback_axial(
1292 &self,
1293 cotangent: &nalgebra::DMatrix<Complex>,
1294 ) -> Result<(FieldGradient, Vec<f64>)> {
1295 self.pullback_impl::<WITH_AXIAL>(cotangent)
1296 }
1297
1298 fn pullback_impl<const N: usize>(
1299 &self,
1300 cotangent: &nalgebra::DMatrix<Complex>,
1301 ) -> Result<(FieldGradient, Vec<f64>)> {
1302 if cotangent.shape() != self.shape() || cotangent.iter().any(|&g| !finite(g)) {
1303 return Err(Error::InvalidInput(
1304 "invalid field operator cotangent".into(),
1305 ));
1306 }
1307 self.waves
1308 .pullback::<N>(&self.points, None, |sample, mode| {
1309 from_fn(|i| cotangent[(3 * sample + i, mode)])
1310 })
1311 }
1312}
1313
1314impl SavedState for OperatorResidual {
1315 fn save_state(&self) -> Result<Vec<u8>> {
1316 let cylindrical = matches!(self.waves.basis, MultipoleBasis::Cylindrical(_));
1317 let mut writer = Writer::new(Self::state_size(
1318 self.waves.basis.len(),
1319 self.position_count(),
1320 self.points.len(),
1321 cylindrical,
1322 )?);
1323 write_sampled_state(&mut writer, &self.waves, &self.points);
1324 Ok(writer.finish())
1325 }
1326
1327 fn from_state(bytes: &[u8]) -> Result<Self> {
1328 let (waves, points, reader) = read_sampled_state(bytes, false)?;
1329 reader.finish()?;
1330 Ok(Self { waves, points })
1331 }
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336 use std::array::from_fn;
1337
1338 use proptest::{prelude::*, test_runner::TestCaseError};
1339
1340 use super::{field, operator, spherical_wave, spherical_wave_impl};
1341 use crate::{
1342 Complex, Error,
1343 special::Radial,
1344 sw::Mode,
1345 test_support::{
1346 assert_same_bits_on_pools, bits, degree_order, patterned, radial, spherical_basis,
1347 },
1348 };
1349
1350 proptest! {
1351 #![proptest_config(ProptestConfig::with_cases(64))]
1352
1353 #[test]
1354 fn forward_waves_carry_no_derivatives(
1355 (l, m) in degree_order(1..6),
1356 pol in 0_u8..2,
1357 position in prop::array::uniform3(-1.5_f64..1.5),
1358 helicity in any::<bool>(),
1359 radial in radial(),
1360 ) {
1361 check_forward_wave(Mode { l, m, pol }, position, helicity, radial)?;
1362 }
1363 }
1364
1365 #[test]
1368 fn regular_origin_series_matches_bessel_values() {
1369 for l in 0..=64 {
1370 for k in [
1371 Complex::new(1.2, 0.0),
1372 Complex::new(1.1, 0.3),
1373 Complex::new(0.8, -0.5),
1374 ] {
1375 for radius in [0.01_f64, 0.2, 0.38] {
1376 let actual = super::scaled_radial(l, k, radius, Radial::Regular).unwrap();
1377 let argument = k * radius;
1378 let expected = (super::PI / (2.0 * argument)).sqrt()
1379 * complex_bessel::besselj(f64::from(l) + 0.5, argument).unwrap()
1380 / radius.powf(f64::from(l));
1381 assert!(
1382 (actual - expected).norm() <= 1e-13 * expected.norm(),
1383 "l={l}, k={k}, r={radius}, actual={actual}, expected={expected}"
1384 );
1385 }
1386 }
1387 }
1388 }
1389
1390 #[test]
1394 fn radial_sequences_include_only_contiguous_needed_degrees() {
1395 for (degrees, range) in [
1396 (vec![1], Some((1, 1))),
1397 (vec![2], Some((2, 2))),
1398 (vec![1, 3], Some((1, 3))),
1399 (vec![1, 4], None),
1400 (vec![3, 128], None),
1401 ] {
1402 let mut basis = crate::sw::Basis {
1403 modes: degrees
1404 .into_iter()
1405 .map(|l| (0, Mode { l, m: 0, pol: 1 }))
1406 .collect(),
1407 positions: vec![[0.0; 3], [1.0, 0.0, 0.0]],
1408 };
1409 for _ in 0..2 {
1410 let waves = super::WaveSet::new(
1411 basis.clone().into(),
1412 [Complex::new(1.0, 0.0); 2],
1413 true,
1414 Radial::Singular,
1415 )
1416 .unwrap();
1417 assert_eq!(waves.radial_ranges, vec![range, None, None, None]);
1418 assert!(waves.harmonic_tables.iter().all(Option::is_none));
1419 basis.modes.reverse();
1420 }
1421 }
1422 }
1423
1424 #[test]
1427 fn outgoing_sequences_stop_at_each_centres_degree() {
1428 let mut basis = spherical_basis(3, [0.0; 3]);
1429 basis.positions.push([1.0, 0.0, 0.0]);
1430 basis.modes.extend(
1431 crate::sw::modes(8)
1432 .unwrap()
1433 .into_iter()
1434 .map(|mode| (1, mode)),
1435 );
1436 let waves = super::WaveSet::new(
1437 basis.clone().into(),
1438 [Complex::new(1.0, 0.0); 2],
1439 true,
1440 Radial::Singular,
1441 )
1442 .unwrap();
1443 let point = [1e-40, 0.0, 0.0];
1444 let mut values = waves.cache::<{ super::VALUES }>();
1445 let mut derivatives = waves.cache::<{ super::SPATIAL_AND_K }>();
1446 for i in 0..basis.modes.len() {
1447 let (value, _) = waves.wave(i, point, &mut values).unwrap();
1448 let (wave, _) = waves.wave(i, point, &mut derivatives).unwrap();
1449 assert!(value.value.into_iter().all(crate::numerics::finite));
1450 assert!(
1451 wave.position
1452 .into_iter()
1453 .flatten()
1454 .all(crate::numerics::finite)
1455 );
1456 assert!(wave.k.into_iter().all(crate::numerics::finite));
1457 }
1458 }
1459
1460 #[test]
1466 fn spherical_harmonic_tables_match_individual_waves() {
1467 for radial in [Radial::Regular, Radial::Singular] {
1468 for chirality in [0.0, 0.2] {
1469 for exponent in [-100, 0, 100] {
1470 let unit = 10_f64.powi(exponent);
1471 let ks = [Complex::new(1.2, 0.1), Complex::new(1.2 + chirality, 0.1)]
1472 .map(|k| k / unit);
1473 let mut basis = spherical_basis(8, [0.0; 3]);
1474 basis.positions.push([0.2 * unit, -0.1 * unit, 0.0]);
1475 let modes = basis.modes.clone();
1476 basis
1477 .modes
1478 .extend(modes.into_iter().map(|(_, mode)| (1, mode)));
1479 basis.modes.reverse();
1480 for ordering in 0..3 {
1481 let mut basis = basis.clone();
1482 if ordering == 1 {
1483 basis.modes.retain(|&(_, mode)| mode.l == 8 && mode.m == 3);
1484 } else if ordering == 2 {
1485 basis.modes.sort_unstable_by_key(|&(pidx, mode)| {
1486 (mode.l, mode.m, mode.pol, pidx)
1487 });
1488 }
1489 let mut waves =
1490 super::WaveSet::new(basis.clone().into(), ks, true, radial).unwrap();
1491 waves.radial_ranges.fill(None);
1492 assert_eq!(
1493 waves.harmonic_tables.iter().any(Option::is_some),
1494 ordering == 0
1495 );
1496 for point in [[0.0; 3], [0.0, 0.0, 0.7], [0.3, -0.8, 1.1]] {
1497 if radial == Radial::Singular && point.iter().all(|&v| v == 0.0) {
1498 continue;
1499 }
1500 let point = point.map(|x| x * unit);
1501 let mut cache = waves.cache::<{ super::SPATIAL_AND_K }>();
1502 let mut value_cache = waves.cache::<{ super::VALUES }>();
1503 for (i, &(pidx, mode)) in basis.modes.iter().enumerate() {
1504 let offset = from_fn(|a| point[a] - basis.positions[pidx][a]);
1505 let expected = spherical_wave(
1506 mode,
1507 ks[usize::from(mode.pol)],
1508 offset,
1509 true,
1510 radial,
1511 )
1512 .unwrap();
1513 let (actual, _) = waves.wave(i, point, &mut cache).unwrap();
1514 let (value, _) = waves.wave(i, point, &mut value_cache).unwrap();
1515 assert_eq!(actual.value, expected.value);
1516 assert_eq!(value.value, expected.value);
1517 assert_eq!(actual.position, expected.position);
1518 assert_eq!(actual.k, expected.k);
1519 }
1520 }
1521 }
1522 }
1523 }
1524 }
1525 }
1526
1527 #[test]
1532 fn spherical_fields_reject_what_a_single_wave_rejects() {
1533 let message = "require a finite nonzero wavenumber and finite position";
1534 let rejected = |result: crate::Result<()>| matches!(result, Err(Error::InvalidInput(ref text)) if text == message);
1535 let tiny = [Complex::new(1e-170, 1e-170); 2];
1536 let unit = [Complex::new(1.0, 0.0); 2];
1537 let cases = [
1538 (tiny, [0.0; 3], [0.3, 0.2, 0.1]),
1539 (unit, [-1e308, 0.0, 0.0], [1e308, 0.0, 0.0]),
1540 ];
1541 for (ks, basis_position, point) in cases {
1542 for radial in [Radial::Regular, Radial::Singular] {
1543 for helicity in [true, false] {
1544 let basis = spherical_basis(1, basis_position);
1545 let position = from_fn(|a| point[a] - basis_position[a]);
1546 let mode = basis.modes[0].1;
1547 let wave = spherical_wave(mode, ks[0], position, helicity, radial);
1548 assert!(rejected(wave.map(drop)));
1549 let coefficients = vec![Complex::new(1.0, 0.0); basis.modes.len()];
1550 let values = field(
1551 basis.clone(),
1552 coefficients,
1553 vec![point],
1554 ks,
1555 helicity,
1556 radial,
1557 );
1558 assert!(rejected(values.map(drop)), "{radial:?}");
1559 let matrix = operator(basis, vec![point], ks, helicity, radial);
1560 assert!(rejected(matrix.map(drop)), "{radial:?}");
1561 }
1562 }
1563 }
1564 }
1565
1566 #[test]
1571 fn pullbacks_do_not_depend_on_the_thread_count() {
1572 let points: Vec<[f64; 3]> = (0..120_u32)
1573 .map(|i| {
1574 let t = f64::from(i);
1575 [
1576 1.5 * (0.37 * t).sin(),
1577 1.2 * (0.53 * t).cos(),
1578 0.9 * (0.71 * t).sin() + 0.1,
1579 ]
1580 })
1581 .collect();
1582 let ks = [Complex::new(1.1, 0.02), Complex::new(1.3, 0.01)];
1583 let modes = crate::sw::modes(3).unwrap();
1584 let spheres = crate::sw::Basis {
1585 modes: (0..2)
1586 .flat_map(|p| modes.iter().map(move |&mode| (p, mode)))
1587 .collect(),
1588 positions: vec![[0.1, 0.2, -0.3], [-0.2, 0.0, 0.4]],
1589 };
1590 let coefficients = patterned(spheres.modes.len(), 1, 0.2).as_slice().to_vec();
1591 let g = patterned(3, points.len(), 0.9);
1592 let g = g.as_slice().as_chunks::<3>().0;
1593 assert_same_bits_on_pools(|| {
1594 let (_, residual) = field(
1595 spheres.clone(),
1596 coefficients.clone(),
1597 points.clone(),
1598 ks,
1599 true,
1600 Radial::Regular,
1601 )
1602 .unwrap();
1603 let g = residual.pullback(g).unwrap();
1604 bits(&[&g.coefficients, &g.points, &g.positions, &g.ks])
1605 });
1606 let cylinders = crate::cw::Basis {
1607 modes: (0..2)
1608 .flat_map(|p| {
1609 [0.2, -0.35].into_iter().flat_map(move |kz| {
1610 (-3..=3).flat_map(move |m| {
1611 [1, 0].map(|pol| (p, crate::cw::Mode { kz, m, pol }))
1612 })
1613 })
1614 })
1615 .collect(),
1616 positions: vec![[0.1, 0.2, 0.0], [-0.2, 0.0, 0.0]],
1617 };
1618 let g = patterned(3 * points.len(), cylinders.modes.len(), 0.4);
1619 assert_same_bits_on_pools(|| {
1620 let (_, residual) =
1621 operator(cylinders.clone(), points.clone(), ks, true, Radial::Regular).unwrap();
1622 let (g, axial) = residual.pullback_axial(&g).unwrap();
1623 bits(&[&g.points, &g.positions, &g.ks, &axial])
1624 });
1625 }
1626
1627 fn check_forward_wave(
1630 mode: Mode,
1631 position: [f64; 3],
1632 helicity: bool,
1633 radial: Radial,
1634 ) -> Result<(), TestCaseError> {
1635 let distance = position[0].hypot(position[1]).hypot(position[2]);
1636 prop_assume!(radial == Radial::Regular || distance > 0.1);
1637 let k = Complex::new(1.3, 0.1);
1638 let full = spherical_wave_impl::<true>(mode, k, position, helicity, radial).unwrap();
1639 let forward = spherical_wave_impl::<false>(mode, k, position, helicity, radial).unwrap();
1640 prop_assert_eq!(forward.value, full.value);
1641 prop_assert_eq!(forward.position, [[Complex::default(); 3]; 3]);
1642 prop_assert_eq!(forward.k, [Complex::default(); 3]);
1643 Ok(())
1644 }
1645}