1#![allow(clippy::indexing_slicing)] use super::{Basis, CartesianTranslation, cartesian_translation, transverse_wavenumber};
9use crate::{
10 Complex, Error, Result,
11 basis::validate_wavenumbers,
12 numerics::{self, broadcast, finite, parallel::Parallel},
13 special::Radial,
14};
15use nalgebra::DMatrix;
16use rayon::prelude::*;
17use std::collections::HashMap;
18
19#[path = "expansion_lattice_state.rs"]
20mod lattice_state;
21
22fn axial_groups(destination: &Basis, source: &Basis) -> Vec<f64> {
25 let mut groups: Vec<_> = destination
26 .modes
27 .iter()
28 .chain(&source.modes)
29 .map(|(_, m)| m.kz)
30 .collect();
31 groups.sort_by(f64::total_cmp);
32 groups.dedup();
33 groups
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
39struct CouplingKey {
40 destination: usize,
42 source: usize,
44 kz: u64,
46 slot: u8,
48 order: i32,
50}
51
52#[derive(Debug)]
56struct Couplings {
57 requests: Vec<(CouplingKey, (usize, usize))>,
59 entries: Vec<u32>,
63}
64
65#[allow(clippy::float_cmp)] fn for_each_coupled(destination: &Basis, source: &Basis, mut f: impl FnMut(usize, usize)) {
69 for (j, &(_, from)) in source.modes.iter().enumerate() {
70 for (i, &(_, to)) in destination.modes.iter().enumerate() {
71 if to.kz == from.kz && to.pol == from.pol {
72 f(i, j);
73 }
74 }
75 }
76}
77
78impl Couplings {
79 fn new(destination: &Basis, source: &Basis, ks: [Complex; 2]) -> Result<Self> {
81 let shared = ks[0] == ks[1];
82 let mut indices = HashMap::new();
83 let mut requests = Vec::new();
84 let mut entries = Vec::new();
85 let mut overflow = false;
86 for_each_coupled(destination, source, |i, j| {
87 let (p, to) = destination.modes[i];
88 let (q, from) = source.modes[j];
89 let key = CouplingKey {
90 destination: p,
91 source: q,
92 kz: from.kz.to_bits(),
93 slot: if shared { 0 } else { from.pol },
94 order: from.m - to.m,
95 };
96 let index = *indices.entry(key).or_insert_with(|| {
97 requests.push((key, (i, j)));
98 requests.len() - 1
99 });
100 match u32::try_from(index) {
101 Ok(index) => entries.push(index),
102 Err(_) => overflow = true,
103 }
104 });
105 if overflow {
106 return Err(Error::InvalidInput(
107 "too many distinct cylindrical couplings".into(),
108 ));
109 }
110 Ok(Self { requests, entries })
111 }
112
113 fn for_each(
115 &self,
116 destination: &Basis,
117 source: &Basis,
118 mut f: impl FnMut(usize, usize, usize),
119 ) {
120 let mut entries = self.entries.iter();
121 for_each_coupled(destination, source, |i, j| {
122 if let Some(&request) = entries.next() {
123 f(i, j, request as usize);
124 }
125 });
126 }
127
128 fn matrix(
130 &self,
131 destination: &Basis,
132 source: &Basis,
133 values: &[Complex],
134 ) -> Result<DMatrix<Complex>> {
135 let mut matrix = numerics::zeros(destination.modes.len(), source.modes.len())?;
136 self.for_each(destination, source, |i, j, request| {
137 matrix[(i, j)] = values[request];
138 });
139 Ok(matrix)
140 }
141
142 fn gather(
144 &self,
145 destination: &Basis,
146 source: &Basis,
147 cotangent: &DMatrix<Complex>,
148 ) -> Vec<[Complex; 2]> {
149 let mut g = vec![[Complex::default(); 2]; self.requests.len()];
150 self.for_each(destination, source, |i, j, request| {
151 g[request][usize::from(source.modes[j].1.pol)] += cotangent[(i, j)];
152 });
153 g
154 }
155}
156
157#[derive(Debug)]
166pub struct ExpansionResidual {
167 destination: Basis,
168 source: Basis,
169 ks: [Complex; 2],
170 radial: Radial,
171 couplings: Couplings,
172}
173
174const PARALLEL: Parallel = Parallel::AtLeast(64);
177
178pub fn expansion(
183 destination: Basis,
184 source: Basis,
185 ks: [Complex; 2],
186 radial: Radial,
187) -> Result<(DMatrix<Complex>, ExpansionResidual)> {
188 let residual = ExpansionResidual::prepare(destination, source, ks, radial)?;
189 let requests = &residual.couplings.requests;
190 let values = broadcast::map(requests.len(), PARALLEL, |r| {
191 Ok(residual.translation(requests[r].1)?.value)
192 })?;
193 let value = residual
194 .couplings
195 .matrix(&residual.destination, &residual.source, &values)?;
196 Ok((value, residual))
197}
198
199impl ExpansionResidual {
200 fn prepare(
201 destination: Basis,
202 source: Basis,
203 ks: [Complex; 2],
204 radial: Radial,
205 ) -> Result<Self> {
206 destination.validate()?;
207 source.validate()?;
208 validate_wavenumbers(ks, true, false)?;
211 if ks.iter().any(|k| k.norm_sqr() == 0.0) {
214 return Err(Error::InvalidInput(
215 "medium wavenumber squared underflows to zero".into(),
216 ));
217 }
218 let couplings = Couplings::new(&destination, &source, ks)?;
219 Ok(Self {
220 destination,
221 source,
222 ks,
223 radial,
224 couplings,
225 })
226 }
227
228 pub fn state_size(
230 destination_modes: usize,
231 destination_positions: usize,
232 source_modes: usize,
233 source_positions: usize,
234 ) -> Result<usize> {
235 let destination = crate::saved::cw_basis_size(destination_modes, destination_positions)?;
236 let source = crate::saved::cw_basis_size(source_modes, source_positions)?;
237 destination
238 .checked_add(source)
239 .and_then(|n| n.checked_add(33))
240 .ok_or_else(crate::saved::invalid)
241 }
242
243 pub(crate) fn write_state(&self, writer: &mut crate::saved::Writer) {
245 crate::saved::write_cw_basis(writer, &self.destination);
246 crate::saved::write_cw_basis(writer, &self.source);
247 for k in self.ks {
248 writer.complex(k);
249 }
250 crate::saved::write_radial(writer, self.radial);
251 }
252 #[must_use]
254 pub fn shape(&self) -> (usize, usize) {
255 (self.destination.modes.len(), self.source.modes.len())
256 }
257
258 pub fn pushforward(
260 &self,
261 destination: &[[f64; 3]],
262 source: &[[f64; 3]],
263 ks: [Complex; 2],
264 ) -> Result<DMatrix<Complex>> {
265 self.pushforward_impl(destination, source, ks, None)
266 }
267
268 pub fn pushforward_axial(
271 &self,
272 destination: &[[f64; 3]],
273 source: &[[f64; 3]],
274 ks: [Complex; 2],
275 kz: &[f64],
276 ) -> Result<DMatrix<Complex>> {
277 self.pushforward_impl(destination, source, ks, Some(kz))
278 }
279
280 fn pushforward_impl(
281 &self,
282 destination: &[[f64; 3]],
283 source: &[[f64; 3]],
284 ks: [Complex; 2],
285 kz: Option<&[f64]>,
286 ) -> Result<DMatrix<Complex>> {
287 crate::basis::validate_expansion_tangent(
288 destination,
289 source,
290 ks,
291 (
292 self.destination.positions.len(),
293 self.source.positions.len(),
294 ),
295 )?;
296 let groups = kz.map(|_| axial_groups(&self.destination, &self.source));
297 if let (Some(kz), Some(groups)) = (kz, &groups)
298 && (kz.len() != groups.len() || kz.iter().any(|k| !k.is_finite()))
299 {
300 return Err(Error::InvalidInput(
301 "axial tangents must be finite and match shared labels".into(),
302 ));
303 }
304 let requests = &self.couplings.requests;
305 let values = broadcast::map(requests.len(), PARALLEL, |r| {
308 let (key, pair) = requests[r];
309 let jet = self.translation(pair)?;
310 let axial = if let (Some(kz), Some(groups)) = (kz, &groups) {
311 jet.kz * kz[groups.partition_point(|&k| k < f64::from_bits(key.kz))]
312 } else {
313 Complex::default()
314 };
315 Ok([0, 1].map(|pol| {
316 crate::basis::pair_tangent(
317 destination,
318 source,
319 ks,
320 [key.destination, key.source, pol],
321 jet.position,
322 jet.k,
323 ) + axial
324 }))
325 })?;
326 let mut result = numerics::zeros(self.shape().0, self.shape().1)?;
327 self.couplings
328 .for_each(&self.destination, &self.source, |i, j, request| {
329 result[(i, j)] = values[request][usize::from(self.source.modes[j].1.pol)];
330 });
331 Ok(result)
332 }
333
334 pub fn pullback(
336 &self,
337 cotangent: &DMatrix<Complex>,
338 ) -> Result<crate::basis::ExpansionGradient> {
339 self.pullback_impl::<false>(cotangent).map(|(g, _)| g)
340 }
341
342 pub fn pullback_axial(
345 &self,
346 cotangent: &DMatrix<Complex>,
347 ) -> Result<(crate::basis::ExpansionGradient, Vec<f64>)> {
348 self.pullback_impl::<true>(cotangent)
349 }
350
351 fn translation(&self, (i, j): (usize, usize)) -> Result<CartesianTranslation> {
353 let (p, to) = self.destination.modes[i];
354 let (q, from) = self.source.modes[j];
355 let position =
356 std::array::from_fn(|a| self.destination.positions[p][a] - self.source.positions[q][a]);
357 cartesian_translation(
358 to,
359 from,
360 self.ks[usize::from(from.pol)],
361 position,
362 self.radial,
363 )
364 }
365
366 fn pullback_impl<const AXIAL: bool>(
367 &self,
368 cotangent: &DMatrix<Complex>,
369 ) -> Result<(crate::basis::ExpansionGradient, Vec<f64>)> {
370 if cotangent.shape() != self.shape() || cotangent.iter().any(|&g| !finite(g)) {
371 return Err(Error::InvalidInput(
372 "invalid cylindrical expansion cotangent".into(),
373 ));
374 }
375 let requests = &self.couplings.requests;
376 let g = self
377 .couplings
378 .gather(&self.destination, &self.source, cotangent);
379 let jets = broadcast::map(requests.len(), PARALLEL, |r| {
380 if g[r] == [Complex::default(); 2] {
381 Ok(None)
382 } else {
383 self.translation(requests[r].1).map(Some)
384 }
385 })?;
386 let groups = if AXIAL {
387 axial_groups(&self.destination, &self.source)
388 } else {
389 Vec::new()
390 };
391 let mut axial = vec![0.0; groups.len()];
392 let mut result = crate::basis::ExpansionGradient::zeros(
393 self.destination.positions.len(),
394 self.source.positions.len(),
395 );
396 for ((&(key, _), g), jet) in self.couplings.requests.iter().zip(g).zip(jets) {
397 let Some(jet) = jet else { continue };
398 for (out, g) in result.ks.iter_mut().zip(g) {
399 *out += jet.k.conj() * g;
400 }
401 let total = g[0] + g[1];
402 if AXIAL {
403 let kz = f64::from_bits(key.kz);
404 axial[groups.partition_point(|&k| k < kz)] += (total.conj() * jet.kz).re;
405 }
406 for axis in 0..3 {
407 let derivative = (total.conj() * jet.position[axis]).re;
408 result.destination[key.destination][axis] += derivative;
409 result.source[key.source][axis] -= derivative;
410 }
411 }
412 Ok((result, axial))
413 }
414}
415
416impl crate::saved::SavedState for ExpansionResidual {
417 fn save_state(&self) -> Result<Vec<u8>> {
418 let mut writer = crate::saved::Writer::new(Self::state_size(
419 self.destination.modes.len(),
420 self.destination.positions.len(),
421 self.source.modes.len(),
422 self.source.positions.len(),
423 )?);
424 self.write_state(&mut writer);
425 Ok(writer.finish())
426 }
427
428 fn from_state(bytes: &[u8]) -> Result<Self> {
429 let mut shape = crate::saved::Reader::new(bytes);
430 let (dm, dp) = crate::saved::read_cw_basis_dimensions(&mut shape)?;
431 let (sm, sp) = crate::saved::read_cw_basis_dimensions(&mut shape)?;
432 if bytes.len() != Self::state_size(dm, dp, sm, sp)? {
433 return Err(crate::saved::invalid());
434 }
435 let mut reader = crate::saved::Reader::new(bytes);
436 let destination = crate::saved::read_cw_basis(&mut reader)?;
437 let source = crate::saved::read_cw_basis(&mut reader)?;
438 let ks = [reader.complex()?, reader.complex()?];
439 let radial = crate::saved::read_radial(&mut reader)?;
440 reader.finish()?;
441 Self::prepare(destination, source, ks, radial)
442 }
443}
444
445pub fn lattice_expansion(
450 destination: Basis,
451 source: Basis,
452 ks: [Complex; 2],
453 lattice: crate::lattice::BlochLattice,
454 eta: Complex,
455) -> Result<(DMatrix<Complex>, LatticeExpansionResidual)> {
456 destination.validate()?;
457 source.validate()?;
458 validate_wavenumbers(ks, true, false)?;
461 let couplings = Couplings::new(&destination, &source, ks)?;
462 let residual = LatticeExpansionResidual {
463 destination,
464 source,
465 ks,
466 lattice,
467 eta,
468 couplings,
469 };
470 let values = crate::threads::install(|| {
471 residual
472 .couplings
473 .requests
474 .par_iter()
475 .map(|&(key, _)| {
476 let (_, krho, r, phase) = residual.geometry(key);
477 Ok(crate::lattice::sum(
478 crate::lattice::Family::Cylindrical { m: key.order },
479 krho,
480 &residual.lattice,
481 [r[0], r[1], 0.0],
482 residual.eta,
483 )? * phase)
484 })
485 .collect::<Result<Vec<_>>>()
486 })?;
487 let value = residual
488 .couplings
489 .matrix(&residual.destination, &residual.source, &values)?;
490 Ok((value, residual))
491}
492
493#[derive(Debug)]
502pub struct LatticeExpansionResidual {
503 destination: Basis,
504 source: Basis,
505 ks: [Complex; 2],
506 lattice: crate::lattice::BlochLattice,
507 eta: Complex,
508 couplings: Couplings,
509}
510
511impl LatticeExpansionResidual {
512 pub fn pushforward(
514 &self,
515 destination: &[[f64; 3]],
516 source: &[[f64; 3]],
517 ks: [Complex; 2],
518 kpar: &[f64],
519 vectors: &DMatrix<f64>,
520 ) -> Result<DMatrix<Complex>> {
521 self.pushforward_impl(destination, source, ks, kpar, vectors, None)
522 }
523
524 pub fn pushforward_axial(
527 &self,
528 destination: &[[f64; 3]],
529 source: &[[f64; 3]],
530 ks: [Complex; 2],
531 kpar: &[f64],
532 vectors: &DMatrix<f64>,
533 axial: &[f64],
534 ) -> Result<DMatrix<Complex>> {
535 self.pushforward_impl(destination, source, ks, kpar, vectors, Some(axial))
536 }
537
538 fn pushforward_impl(
539 &self,
540 destination: &[[f64; 3]],
541 source: &[[f64; 3]],
542 ks: [Complex; 2],
543 kpar: &[f64],
544 vectors: &DMatrix<f64>,
545 axial: Option<&[f64]>,
546 ) -> Result<DMatrix<Complex>> {
547 crate::basis::validate_expansion_tangent(
548 destination,
549 source,
550 ks,
551 (
552 self.destination.positions.len(),
553 self.source.positions.len(),
554 ),
555 )?;
556 let geometry =
557 crate::lattice::SumTangent::with_lattice(kpar, vectors, self.lattice.dimension())?;
558 let groups = axial
559 .map(|_| axial_groups(&self.destination, &self.source))
560 .unwrap_or_default();
561 if axial.is_some_and(|a| a.len() != groups.len() || a.iter().any(|x| !x.is_finite())) {
562 return Err(Error::InvalidInput(
563 "axial tangent must be finite and match the shared wavenumbers".into(),
564 ));
565 }
566 let tangents = crate::threads::install(|| {
567 self.couplings
568 .requests
569 .par_iter()
570 .map(|&(key, _)| {
571 let kz = f64::from_bits(key.kz);
572 let dkz = axial.map_or(0.0, |a| a[groups.partition_point(|&k| k < kz)]);
573 let (k, krho, r, phase) = self.geometry(key);
574 let shift: [f64; 3] = std::array::from_fn(|a| {
575 source[key.source][a] - destination[key.destination][a]
576 });
577 let derivative = crate::lattice::derivatives(
578 crate::lattice::Family::Cylindrical { m: key.order },
579 krho,
580 &self.lattice,
581 [r[0], r[1], 0.0],
582 self.eta,
583 )?;
584 let phase_tangent = -Complex::i() * (kz * shift[2] + dkz * r[2]) * phase;
585 Ok(ks.map(|dk| {
586 phase
587 * derivative.pushforward(&crate::lattice::SumTangent {
588 k: (k * dk - kz * dkz) / krho,
589 shift: [shift[0], shift[1], 0.0],
590 ..geometry
591 })
592 + derivative.value * phase_tangent
593 }))
594 })
595 .collect::<Result<Vec<_>>>()
596 })?;
597 let mut result = numerics::zeros(self.shape().0, self.shape().1)?;
598 self.couplings
599 .for_each(&self.destination, &self.source, |i, j, request| {
600 result[(i, j)] = tangents[request][usize::from(self.source.modes[j].1.pol)];
601 });
602 Ok(result)
603 }
604
605 #[must_use]
607 pub fn shape(&self) -> (usize, usize) {
608 (self.destination.modes.len(), self.source.modes.len())
609 }
610
611 fn geometry(&self, key: CouplingKey) -> (Complex, Complex, [f64; 3], Complex) {
614 let kz = f64::from_bits(key.kz);
615 let k = self.ks[usize::from(key.slot)];
616 let r: [f64; 3] = std::array::from_fn(|a| {
617 self.source.positions[key.source][a] - self.destination.positions[key.destination][a]
618 });
619 let phase = (-Complex::i() * kz * r[2]).exp();
620 (k, transverse_wavenumber(k, kz), r, phase)
621 }
622
623 pub fn pullback(
625 &self,
626 cotangent: &DMatrix<Complex>,
627 ) -> Result<crate::basis::LatticeExpansionGradient> {
628 self.pullback_impl::<false>(cotangent).map(|(g, _)| g)
629 }
630
631 pub fn pullback_axial(
633 &self,
634 cotangent: &DMatrix<Complex>,
635 ) -> Result<(crate::basis::LatticeExpansionGradient, Vec<f64>)> {
636 self.pullback_impl::<true>(cotangent)
637 }
638
639 fn pullback_impl<const AXIAL: bool>(
640 &self,
641 cotangent: &DMatrix<Complex>,
642 ) -> Result<(crate::basis::LatticeExpansionGradient, Vec<f64>)> {
643 if cotangent.shape() != self.shape() || cotangent.iter().any(|&g| !finite(g)) {
644 return Err(Error::InvalidInput(
645 "invalid periodic expansion cotangent".into(),
646 ));
647 }
648 let g = self
649 .couplings
650 .gather(&self.destination, &self.source, cotangent);
651 let gradients = crate::threads::install(|| {
652 self.couplings
653 .requests
654 .par_iter()
655 .zip(g)
656 .map(|(&(key, _), g)| {
657 let kz = f64::from_bits(key.kz);
658 let (k, krho, r, phase) = self.geometry(key);
659 let jet = crate::lattice::derivatives(
660 crate::lattice::Family::Cylindrical { m: key.order },
661 krho,
662 &self.lattice,
663 [r[0], r[1], 0.0],
664 self.eta,
665 )?;
666 let total: Complex = g.iter().sum();
668 let scalar_g = total * phase.conj();
669 let spectral = g.map(|g| (jet.k * k / krho).conj() * g * phase.conj());
670 let mut gradient = crate::lattice::SumGradient {
671 k: Complex::default(),
672 eta: Complex::default(),
673 shift: jet.shift.map(|d| (scalar_g.conj() * d).re),
674 kpar: jet.kpar.map(|d| (scalar_g.conj() * d).re),
675 vectors: jet.vectors.map(|row| row.map(|d| (scalar_g.conj() * d).re)),
676 };
677 gradient.shift[2] =
678 (total.conj() * (-Complex::i() * kz) * phase * jet.value).re;
679 let axial = if AXIAL {
680 (total.conj()
681 * phase
682 * (-jet.k * kz / krho - Complex::i() * r[2] * jet.value))
683 .re
684 } else {
685 0.0
686 };
687 Ok((spectral, gradient, axial))
688 })
689 .collect::<Result<Vec<_>>>()
690 })?;
691 let mut result = crate::basis::LatticeExpansionGradient::zeros(
692 self.destination.positions.len(),
693 self.source.positions.len(),
694 self.lattice.dimension(),
695 );
696 let groups = if AXIAL {
697 axial_groups(&self.destination, &self.source)
698 } else {
699 Vec::new()
700 };
701 let mut axial = vec![0.0; groups.len()];
702 for ((spectral, g, gkz), &(key, _)) in gradients.into_iter().zip(&self.couplings.requests) {
703 if AXIAL {
704 axial[groups.partition_point(|&k| k < f64::from_bits(key.kz))] += gkz;
705 }
706 result.add_lattice(&g);
707 for (g, k) in result.expansion.ks.iter_mut().zip(spectral) {
708 *g += k;
709 }
710 for (axis, value) in g.shift.into_iter().enumerate() {
711 result.expansion.destination[key.destination][axis] -= value;
712 result.expansion.source[key.source][axis] += value;
713 }
714 }
715 Ok((result, axial))
716 }
717}
718
719#[cfg(test)]
720mod tests {
721 use super::{Basis, Complex, Radial, cartesian_translation, expansion};
725 use crate::{
726 cw::Mode,
727 test_support::{DEFAULT_CASES, patterned, prop_assert_close, radial, re_dot},
728 };
729 use nalgebra::DMatrix;
730 use proptest::{prelude::*, test_runner::TestCaseError};
731
732 proptest! {
733 #![proptest_config(ProptestConfig::with_cases(DEFAULT_CASES))]
734
735 #[test]
736 fn expansion_pullback_matches_pairwise_translations(
737 mmax in 0_i32..3,
738 kzs in (-0.7_f64..0.35, 0.35_f64..0.7),
739 positions in prop::array::uniform3(prop::array::uniform3(-1.0_f64..1.0)),
740 shared in any::<bool>(),
741 radial in radial(),
742 seed in -3.0_f64..3.0,
743 ) {
744 check_expansion_pullback(mmax, kzs.into(), positions, shared, radial, seed)?;
745 }
746 }
747
748 #[test]
749 fn expansion_pullback_near_coaxial_pair() -> Result<(), TestCaseError> {
750 check_expansion_pullback(
751 2,
752 [0.0, 0.35],
753 [
754 [0.885_096_859_301_242_2, 0.389_709_429_113_160_26, 0.0],
755 [
756 0.886_584_657_828_228_6,
757 0.390_127_017_402_346_07,
758 -0.441_485_675_024_714_25,
759 ],
760 [0.0, 0.0, 0.0],
761 ],
762 false,
763 Radial::Singular,
764 0.0,
765 )
766 }
767
768 #[allow(clippy::float_cmp)] fn check_expansion_pullback(
773 mmax: i32,
774 kzs: [f64; 2],
775 [first, second, third]: [[f64; 3]; 3],
776 shared: bool,
777 radial: Radial,
778 seed: f64,
779 ) -> Result<(), TestCaseError> {
780 let modes = |count: usize| -> Vec<(usize, Mode)> {
781 (0..count)
782 .flat_map(|p| {
783 kzs.into_iter().flat_map(move |kz| {
784 (-mmax..=mmax).flat_map(move |m| [1, 0].map(|pol| (p, Mode { kz, m, pol })))
785 })
786 })
787 .collect()
788 };
789 let destination = Basis {
791 modes: modes(2),
792 positions: vec![first, second],
793 };
794 let source = Basis {
795 modes: modes(2),
796 positions: vec![first, third],
797 };
798 let ks = if shared {
799 [Complex::new(1.3, 0.05); 2]
800 } else {
801 [Complex::new(1.3, 0.05), Complex::new(1.5, 0.07)]
802 };
803 let g = patterned(destination.modes.len(), source.modes.len(), seed);
804 let mut expected = DMatrix::zeros(g.nrows(), g.ncols());
805 let mut gradient = (
806 vec![[0.0; 3]; 2],
807 vec![[0.0; 3]; 2],
808 [Complex::default(); 2],
809 );
810 let mut axial = [0.0; 2];
811 let mut magnitude = (vec![[0.0; 3]; 2], vec![[0.0; 3]; 2], [0.0; 2]);
815 let mut axial_magnitude = [0.0; 2];
816 for (j, &(q, from)) in source.modes.iter().enumerate() {
817 for (i, &(p, to)) in destination.modes.iter().enumerate() {
818 let position =
819 std::array::from_fn(|a| destination.positions[p][a] - source.positions[q][a]);
820 let pol = usize::from(from.pol);
821 let jet = cartesian_translation(to, from, ks[pol], position, radial).unwrap();
822 expected[(i, j)] = jet.value;
823 gradient.2[pol] += jet.k.conj() * g[(i, j)];
824 let weight = g[(i, j)].norm();
825 magnitude.2[pol] += weight * jet.k.norm();
826 axial[usize::from(from.kz == kzs[1])] += re_dot([g[(i, j)]], [jet.kz]);
827 axial_magnitude[usize::from(from.kz == kzs[1])] += weight * jet.kz.norm();
828 for axis in 0..3 {
829 let derivative = re_dot([g[(i, j)]], [jet.position[axis]]);
830 gradient.0[p][axis] += derivative;
831 gradient.1[q][axis] -= derivative;
832 let product = weight * jet.position[axis].norm();
833 magnitude.0[p][axis] += product;
834 magnitude.1[q][axis] += product;
835 }
836 }
837 }
838 let n = f64::from(u32::try_from(g.len()).unwrap()) + 8.0;
844 let roundoff = 2.0 * n * f64::EPSILON / (1.0 - n * f64::EPSILON);
845 let (value, residual) = expansion(destination.clone(), source.clone(), ks, radial).unwrap();
846 prop_assert_close!(&value, &expected, 1e-14 * expected.norm());
847 let actual = residual.pullback(&g).unwrap();
848 let (with_axial, actual_axial) = expansion(destination, source, ks, radial)
849 .unwrap()
850 .1
851 .pullback_axial(&g)
852 .unwrap();
853 for actual in [actual, with_axial] {
854 for (actual, expected, magnitude) in [
855 (&actual.destination, &gradient.0, &magnitude.0),
856 (&actual.source, &gradient.1, &magnitude.1),
857 ] {
858 for ((a, e), m) in actual
859 .iter()
860 .flatten()
861 .zip(expected.iter().flatten())
862 .zip(magnitude.iter().flatten())
863 {
864 prop_assert_close!(*a, *e, roundoff * m + f64::MIN_POSITIVE);
865 }
866 }
867 for ((a, e), m) in actual.ks.into_iter().zip(gradient.2).zip(magnitude.2) {
868 prop_assert_close!(a, e, roundoff * m + f64::MIN_POSITIVE);
869 }
870 }
871 for ((a, e), m) in actual_axial.into_iter().zip(axial).zip(axial_magnitude) {
872 prop_assert_close!(a, e, roundoff * m + f64::MIN_POSITIVE);
873 }
874 Ok(())
875 }
876}