1#![allow(clippy::indexing_slicing)] use std::collections::{HashMap, HashSet};
9
10use faer::{MatMut, MatRef};
11
12use super::{
13 CartesianTranslation, Mode,
14 cartesian::{combine, harmonic},
15 coupling::{Coupling, Kinds, term_bound},
16};
17use crate::{
18 Complex, Error, Result,
19 basis::ModeLabel,
20 numerics::{self, finite, parallel::try_fold_ordered},
21 special::{
22 Radial, RadialJet, SolidTable, Wigner3jRow, direction, spherical_hankels, spherical_radial,
23 spherical_radial_sequence, tangent,
24 },
25};
26use rayon::prelude::*;
27
28fn degree_orders(modes: &[Mode]) -> Vec<(i32, i32)> {
30 let mut seen = HashSet::new();
31 modes
32 .iter()
33 .map(|mode| (mode.l, mode.m))
34 .filter(|&pair| seen.insert(pair))
35 .collect()
36}
37
38fn degree(p: i32) -> usize {
40 usize::try_from(p).unwrap_or_default()
41}
42
43fn harmonics(lmax: i32) -> impl Iterator<Item = (i32, i32)> {
46 (0..=lmax).flat_map(|p| (-p..=p).map(move |m| (p, m)))
47}
48
49#[derive(Clone, Copy, Debug, Default)]
51struct Term {
52 index: usize,
54 weight: Complex,
55}
56
57#[derive(Clone, Debug)]
59pub(crate) struct TranslationPlan {
60 terms: Vec<Term>,
62 starts: Vec<usize>,
63 lmax: i32,
65 solids: SolidTable,
66}
67
68impl TranslationPlan {
69 pub(crate) fn between(destination: &[Mode], source: &[Mode], helicity: bool) -> Result<Self> {
72 for mode in destination.iter().chain(source) {
73 mode.validate()?;
74 }
75 let lmax = destination.iter().map(|m| m.l).max().unwrap_or(0)
76 + source.iter().map(|m| m.l).max().unwrap_or(0);
77 let width = destination.len();
81 let mut starts = Vec::new();
82 numerics::reserve(&mut starts, width * source.len() + 1)?;
83 starts.push(0);
84 for &from in source {
85 for &to in destination {
86 starts.push(starts[starts.len() - 1] + term_bound(to, from, helicity));
87 }
88 }
89 let mut terms = numerics::filled(starts[starts.len() - 1], Term::default())?;
90 let mut counts = numerics::filled(width * source.len(), 0_usize)?;
91 let (sources, destinations) = (degree_orders(source), degree_orders(destination));
94 let index = |pairs: &[(i32, i32)]| -> HashMap<_, _> {
95 pairs
96 .iter()
97 .enumerate()
98 .map(|(i, &pair)| (pair, i))
99 .collect()
100 };
101 let (source_index, destination_index) = (index(&sources), index(&destinations));
102 let zeros: HashMap<_, _> = crate::threads::install(|| {
103 sources
104 .iter()
105 .flat_map(|&(l, _)| destinations.iter().map(move |&(lambda, _)| (l, lambda)))
106 .collect::<HashSet<_>>()
107 .into_par_iter()
108 .map(|(l, lambda)| ((l, lambda), Wigner3jRow::new(l, lambda, 0, 0)))
109 .collect()
110 });
111 let mut rows: Vec<Vec<_>> = sources.iter().map(|_| Vec::new()).collect();
114 let mut slots = terms.as_mut_slice();
115 for ((i, &from), counts) in source
116 .iter()
117 .enumerate()
118 .zip(counts.chunks_mut(width.max(1)))
119 {
120 let bounds = &starts[i * width..=(i + 1) * width];
121 let (row, rest) = std::mem::take(&mut slots).split_at_mut(bounds[width] - bounds[0]);
122 slots = rest;
123 rows[source_index[&(from.l, from.m)]].push((from, bounds, row, counts));
124 }
125 crate::threads::install(|| {
126 rows.into_par_iter()
127 .zip(&sources)
128 .try_for_each(|(rows, &(l, m))| {
129 let couplings: Vec<_> = destinations
130 .iter()
131 .map(|&(lambda, mu)| {
132 Coupling::new((l, m), (lambda, mu), &zeros[&(l, lambda)], Kinds::BOTH)
133 })
134 .collect();
135 for (from, bounds, mut row, counts) in rows {
136 for ((to, range), count) in
137 destination.iter().zip(bounds.windows(2)).zip(counts)
138 {
139 let (entry, rest) =
140 std::mem::take(&mut row).split_at_mut(range[1] - range[0]);
141 row = rest;
142 let coupling = &couplings[destination_index[&(to.l, to.m)]];
143 for (p, weight) in coupling.terms(to.pol, from.pol, helicity) {
144 let index =
145 usize::try_from(p * p + p + from.m - to.m).map_err(|_| {
146 Error::InvalidInput("invalid harmonic index".into())
147 })?;
148 entry[*count] = Term { index, weight };
149 *count += 1;
150 }
151 }
152 }
153 Ok(())
154 })
155 })?;
156 let mut end = 0;
157 for (e, &count) in counts.iter().enumerate() {
158 terms.copy_within(starts[e]..starts[e] + count, end);
159 starts[e] = end;
160 end += count;
161 }
162 starts[counts.len()] = end;
163 terms.truncate(end);
164 Ok(Self {
165 terms,
166 starts,
167 lmax,
168 solids: SolidTable::new(lmax),
169 })
170 }
171
172 pub(crate) fn weights_for_normalized_harmonics(&mut self) {
178 let normalization: Vec<_> = harmonics(self.lmax)
179 .map(|(l, m)| crate::special::harmonic_normalization(l, m))
180 .collect();
181 for term in &mut self.terms {
182 term.weight /= normalization[term.index];
183 }
184 }
185
186 fn entries(&self) -> impl Iterator<Item = &[Term]> {
188 self.starts
189 .windows(2)
190 .map(|range| &self.terms[range[0]..range[1]])
191 }
192
193 pub(crate) fn evaluate_table(&self, table: &[Complex]) -> Vec<Complex> {
195 self.entries()
196 .map(|terms| {
197 terms
198 .iter()
199 .map(|term| term.weight * table[term.index])
200 .sum()
201 })
202 .collect()
203 }
204
205 pub(crate) fn pullback_table(&self, cotangent: &[Complex], table: &mut [Complex]) {
208 for (terms, &g) in self.entries().zip(cotangent) {
209 for term in terms {
210 table[term.index] += term.weight.conj() * g;
211 }
212 }
213 }
214
215 fn radials(&self, z: Complex, radial: Radial) -> Result<Vec<RadialJet>> {
217 let lmax =
218 u32::try_from(self.lmax).map_err(|_| Error::InvalidInput("invalid lmax".into()))?;
219 spherical_radial_sequence(lmax, z, radial)
220 }
221
222 fn values(&self, k: Complex, position: [f64; 3], radial: Radial) -> Result<Vec<Complex>> {
225 let mut table = vec![Complex::default(); self.solids.len()];
226 let Some((r, unit)) = direction(position) else {
227 if radial == Radial::Regular {
228 table[0] = spherical_radial(0, k * 0.0, radial)?.value;
229 }
230 return Ok(table);
231 };
232 if radial == Radial::Singular {
233 let lmax =
236 u32::try_from(self.lmax).map_err(|_| Error::InvalidInput("invalid lmax".into()))?;
237 let radials = spherical_hankels(0, lmax, k * r)?;
238 if radials.iter().any(|&value| !finite(value)) {
239 return Err(Error::SpecialFunction("radial function overflow".into()));
240 }
241 self.solids.visit::<false>(unit, |index, p, value, _| {
242 table[index] = radials[degree(p)] * value;
243 });
244 } else {
245 let radials = self.radials(k * r, radial)?;
246 self.solids.visit::<false>(unit, |index, p, value, _| {
247 table[index] = radials[degree(p)].value * value;
248 });
249 }
250 Ok(table)
251 }
252
253 fn table(
255 &self,
256 k: Complex,
257 position: [f64; 3],
258 radial: Radial,
259 ) -> Result<Vec<CartesianTranslation>> {
260 let geometry = direction(position);
261 let mut table = vec![CartesianTranslation::default(); self.solids.len()];
262 if geometry.is_none() && radial == Radial::Singular {
263 return Ok(table);
264 }
265 let radials = self.radials(k * geometry.map_or(0.0, |(r, _)| r), radial)?;
266 let Some((r, unit)) = geometry else {
267 for (entry, (p, m)) in table.iter_mut().zip(harmonics(self.lmax)) {
268 *entry = harmonic(p, m, k, None, radials[degree(p)]);
269 }
270 return Ok(table);
271 };
272 self.solids
273 .visit::<true>(unit, |index, p, value, gradient| {
274 let tangent = tangent(p, value, gradient, unit);
275 table[index] = combine(k, (r, unit), radials[degree(p)], value, tangent);
276 });
277 Ok(table)
278 }
279
280 pub(crate) fn evaluate(
283 &self,
284 k: Complex,
285 position: [f64; 3],
286 radial: Radial,
287 ) -> Result<Vec<Complex>> {
288 Ok(self.evaluate_table(&self.values(k, position, radial)?))
289 }
290
291 pub(crate) fn apply(
296 &self,
297 k: Complex,
298 position: [f64; 3],
299 radial: Radial,
300 input: MatRef<'_, Complex>,
301 output: MatMut<'_, Complex>,
302 adjoint: bool,
303 ) -> Result<()> {
304 let table = self.values(k, position, radial)?;
305 self.apply_table(&table, input, output, adjoint);
306 Ok(())
307 }
308
309 pub(crate) fn apply_pushforward(
312 &self,
313 k: Complex,
314 position: [f64; 3],
315 radial: Radial,
316 position_tangent: [f64; 3],
317 k_tangent: Complex,
318 input: MatRef<'_, Complex>,
319 output: MatMut<'_, Complex>,
320 ) -> Result<()> {
321 let table = self.tangent_table(k, position, radial, position_tangent, k_tangent)?;
322 self.apply_table(&table, input, output, false);
323 Ok(())
324 }
325
326 fn apply_table(
328 &self,
329 table: &[Complex],
330 input: MatRef<'_, Complex>,
331 mut output: MatMut<'_, Complex>,
332 adjoint: bool,
333 ) {
334 let (destinations, sources) = if adjoint {
335 (input.nrows(), output.nrows())
336 } else {
337 (output.nrows(), input.nrows())
338 };
339 for source in 0..sources {
340 let starts = &self.starts[source * destinations..][..=destinations];
341 for (destination, range) in starts.windows(2).enumerate() {
342 let terms = &self.terms[range[0]..range[1]];
343 if terms.is_empty() {
344 continue;
345 }
346 let value: Complex = terms
347 .iter()
348 .map(|term| term.weight * table[term.index])
349 .sum();
350 let (to, from, value) = if adjoint {
351 (source, destination, value.conj())
352 } else {
353 (destination, source, value)
354 };
355 for column in 0..input.ncols() {
356 output[(to, column)] += value * input[(from, column)];
357 }
358 }
359 }
360 }
361
362 fn harmonic_groups(&self, workers: usize) -> Vec<(i32, std::ops::RangeInclusive<i32>)> {
366 let size = if workers > degree(self.lmax + 1) {
367 self.solids.len().div_ceil(workers)
368 } else {
369 self.solids.len()
370 };
371 let last = i32::try_from(size - 1).unwrap_or_default();
372 (0..=self.lmax)
373 .flat_map(|l| {
374 (-l..=l)
375 .step_by(size)
376 .map(move |first| (l, first..=(first + last).min(l)))
377 })
378 .collect()
379 }
380
381 pub(crate) fn evaluate_periodic(
384 &self,
385 k: Complex,
386 position: [f64; 3],
387 lattice: &crate::lattice::BlochLattice,
388 eta: Complex,
389 workers: usize,
390 ) -> Result<Vec<Complex>> {
391 let degrees = crate::threads::install(|| {
392 self.harmonic_groups(workers)
393 .into_par_iter()
394 .map(|(l, orders)| {
395 crate::lattice::spherical_degree(
396 l,
397 orders,
398 k,
399 lattice,
400 position.map(|x| -x),
401 eta,
402 )
403 })
404 .collect::<Result<Vec<_>>>()
405 })?;
406 let table: Vec<_> = degrees
407 .into_iter()
408 .flatten()
409 .zip(harmonics(self.lmax))
410 .map(|(value, (l, m))| value / crate::special::harmonic_normalization(l, m))
411 .collect();
412 Ok(self.evaluate_table(&table))
413 }
414
415 pub(crate) fn pushforward_periodic(
418 &self,
419 ks: [Complex; 2],
420 position: [f64; 3],
421 lattice: &crate::lattice::BlochLattice,
422 eta: Complex,
423 tangents: &[crate::lattice::SumTangent; 2],
424 workers: usize,
425 ) -> Result<[Vec<Complex>; 2]> {
426 let groups = crate::threads::install(|| {
427 self.harmonic_groups(workers)
428 .into_par_iter()
429 .map(|(l, orders)| {
430 let orders: Vec<_> = orders.collect();
431 let mut values = [Vec::new(), Vec::new()];
432 for pol in 0..2 {
433 if pol == 1 && ks[0] == ks[1] {
434 continue;
435 }
436 let derivatives = crate::lattice::spherical_degree_derivatives(
437 l,
438 &orders,
439 ks[pol],
440 lattice,
441 position.map(|x| -x),
442 eta,
443 )?;
444 for (&m, derivative) in orders.iter().zip(derivatives) {
445 let normalization = crate::special::harmonic_normalization(l, m);
446 for target in pol..if ks[0] == ks[1] { 2 } else { pol + 1 } {
447 values[target].push(
448 derivative.pushforward(&tangents[target]) / normalization,
449 );
450 }
451 }
452 }
453 Ok(values)
454 })
455 .collect::<Result<Vec<_>>>()
456 })?;
457 let mut tables = [Vec::new(), Vec::new()];
458 for group in groups {
459 for (table, values) in tables.iter_mut().zip(group) {
460 table.extend(values);
461 }
462 }
463 Ok(tables.map(|table| self.evaluate_table(&table)))
464 }
465
466 pub(crate) fn pullback_periodic(
471 &self,
472 ks: [Complex; 2],
473 position: [f64; 3],
474 lattice: &crate::lattice::BlochLattice,
475 eta: Complex,
476 cotangent: &[Vec<Complex>; 2],
477 workers: usize,
478 ) -> Result<[crate::lattice::SumGradient; 2]> {
479 let [first, second] = cotangent.each_ref().map(|cotangent| {
480 let mut table = vec![Complex::default(); self.solids.len()];
481 self.pullback_table(cotangent, &mut table);
482 table
483 });
484 let g: Vec<_> = first.into_iter().zip(second).map(<[_; 2]>::from).collect();
485 let groups = self
486 .harmonic_groups(workers)
487 .into_iter()
488 .map(|(l, orders)| {
489 let (first, last) = (*orders.start(), *orders.end());
490 let start = degree(l * l + l + first);
491 (l, first, &g[start..degree(l * l + l + last + 1)])
492 })
493 .collect();
494 let gradients = try_fold_ordered(
495 groups,
496 true,
497 Vec::new,
498 |mut gradients, _, (l, first, g): (i32, i32, &[[Complex; 2]])| {
499 let mut result = vec![[crate::lattice::SumGradient::default(); 2]; g.len()];
500 for pol in 0..2 {
501 if pol == 1 && ks[0] == ks[1] {
502 continue;
503 }
504 let orders: Vec<_> = (first..)
505 .zip(g)
506 .filter_map(|(m, g)| {
507 (g[pol] != Complex::default()
508 || (ks[0] == ks[1] && g[1] != Complex::default()))
509 .then_some(m)
510 })
511 .collect();
512 let derivatives = crate::lattice::spherical_degree_derivatives(
513 l,
514 &orders,
515 ks[pol],
516 lattice,
517 position.map(|r| -r),
518 eta,
519 )?;
520 for (m, d) in orders.into_iter().zip(derivatives) {
521 let index = degree(m - first);
522 let normalization = crate::special::harmonic_normalization(l, m);
523 for target in pol..if ks[0] == ks[1] { 2 } else { pol + 1 } {
524 if g[index][target] == Complex::default() {
525 continue;
526 }
527 result[index][target] = d.pullback(g[index][target] / normalization);
528 result[index][target].shift = result[index][target].shift.map(|g| -g);
529 }
530 }
531 }
532 gradients.extend(result);
533 Ok(gradients)
534 },
535 |mut gradients, partial| {
536 gradients.extend(partial);
537 gradients
538 },
539 )?;
540 Ok(gradients.into_iter().fold(
541 [crate::lattice::SumGradient::default(); 2],
542 |mut total, gradient| {
543 for (total, gradient) in total.iter_mut().zip(gradient) {
544 total.add(gradient);
545 }
546 total
547 },
548 ))
549 }
550
551 pub(crate) fn pushforward(
553 &self,
554 k: Complex,
555 position: [f64; 3],
556 radial: Radial,
557 position_tangent: [f64; 3],
558 k_tangent: Complex,
559 ) -> Result<Vec<Complex>> {
560 Ok(self.evaluate_table(&self.tangent_table(
561 k,
562 position,
563 radial,
564 position_tangent,
565 k_tangent,
566 )?))
567 }
568
569 fn tangent_table(
571 &self,
572 k: Complex,
573 position: [f64; 3],
574 radial: Radial,
575 position_tangent: [f64; 3],
576 k_tangent: Complex,
577 ) -> Result<Vec<Complex>> {
578 let table: Vec<_> = self
579 .table(k, position, radial)?
580 .into_iter()
581 .map(|entry| {
582 entry.k * k_tangent
583 + entry
584 .position
585 .into_iter()
586 .zip(position_tangent)
587 .map(|(derivative, tangent)| derivative * tangent)
588 .sum::<Complex>()
589 })
590 .collect();
591 Ok(table)
592 }
593
594 pub(crate) fn pullback(
597 &self,
598 k: Complex,
599 position: [f64; 3],
600 radial: Radial,
601 cotangent: &[Complex],
602 ) -> Result<([f64; 3], Complex)> {
603 let table = self.table(k, position, radial)?;
604 let mut contracted = vec![Complex::default(); table.len()];
605 self.pullback_table(cotangent, &mut contracted);
606 let mut gradient = [0.0; 3];
607 let mut gk = Complex::default();
608 for (g, item) in contracted.iter().zip(table) {
609 gk += g * item.k.conj();
610 for (value, derivative) in gradient.iter_mut().zip(item.position) {
611 *value += (g.conj() * derivative).re;
612 }
613 }
614 Ok((gradient, gk))
615 }
616}
617
618#[cfg(test)]
619mod tests {
620 use std::f64::consts::PI;
625
626 use proptest::{prelude::*, test_runner::TestCaseError};
627
628 use super::TranslationPlan;
629 use crate::{
630 Complex,
631 linalg::{view, view_mut},
632 special::Radial,
633 sw::{self, Mode},
634 test_support::{DEFAULT_CASES, bits, patterned, prop_assert_close, radial, table},
635 };
636
637 fn displacement() -> impl Strategy<Value = [f64; 3]> {
639 prop_oneof![
640 (0.3_f64..3.0).prop_map(|r| [0.0, 0.0, r]),
641 (0.3_f64..3.0).prop_map(|r| [0.0, 0.0, -r]),
642 (0.3_f64..3.0, 0.0..PI, -PI..PI).prop_map(|(r, theta, phi)| {
643 [
644 r * theta.sin() * phi.cos(),
645 r * theta.sin() * phi.sin(),
646 r * theta.cos(),
647 ]
648 }),
649 ]
650 }
651
652 proptest! {
653 #![proptest_config(ProptestConfig::with_cases(DEFAULT_CASES))]
654
655 #[test]
656 fn plan_matches_direct_translation_and_its_symmetries(
657 lmax in 1_u32..=3,
658 helicity in any::<bool>(),
659 radial in radial(),
660 k in (0.5_f64..2.0, 0.0_f64..0.2).prop_map(|(re, im)| Complex::new(re, im)),
661 displacement in displacement(),
662 angle in -PI..PI,
663 seed in 0.0_f64..10.0,
664 ) {
665 let plan = Case::new(lmax, helicity, radial, k);
666 plan.check(displacement, angle, seed)?;
667 }
668
669 #[test]
670 fn regular_plan_at_the_origin_is_the_identity(
671 lmax in 1_u32..=3,
672 helicity in any::<bool>(),
673 k in (0.5_f64..2.0, 0.0_f64..0.2).prop_map(|(re, im)| Complex::new(re, im)),
674 angle in -PI..PI,
675 seed in 0.0_f64..10.0,
676 ) {
677 let plan = Case::new(lmax, helicity, Radial::Regular, k);
678 let block = plan.evaluate([0.0; 3]);
679 for (index, value) in block.iter().enumerate() {
680 let identity = if index % (plan.modes.len() + 1) == 0 { 1.0 } else { 0.0 };
681 prop_assert_close!(*value, Complex::new(identity, 0.0), 1e-13);
682 }
683 plan.check([0.0; 3], angle, seed)?;
684 }
685 }
686
687 struct Case {
690 plan: TranslationPlan,
691 modes: Vec<Mode>,
692 helicity: bool,
693 radial: Radial,
694 k: Complex,
695 }
696
697 impl Case {
698 fn new(lmax: u32, helicity: bool, radial: Radial, k: Complex) -> Self {
699 let modes = sw::modes(lmax).unwrap();
700 let plan = TranslationPlan::between(&modes, &modes, helicity).unwrap();
701 Self {
702 plan,
703 modes,
704 helicity,
705 radial,
706 k,
707 }
708 }
709
710 fn evaluate(&self, displacement: [f64; 3]) -> Vec<Complex> {
711 self.plan
712 .evaluate(self.k, displacement, self.radial)
713 .unwrap()
714 }
715
716 fn entry(&self, block: &[Complex], to: Mode, from: Mode) -> Complex {
718 let index = |mode| self.modes.iter().position(|&m| m == mode).unwrap();
719 block[index(from) * self.modes.len() + index(to)]
720 }
721
722 fn check(&self, d: [f64; 3], angle: f64, seed: f64) -> Result<(), TestCaseError> {
734 let (k, n) = (self.k, self.modes.len());
735 let block = self.evaluate(d);
736 let input = patterned(n, 3, seed + 0.2);
739 for adjoint in [false, true] {
740 let mut actual = patterned(n, 3, seed + 0.7);
741 let mut expected = actual.clone();
742 for column in 0..input.ncols() {
743 for source in 0..n {
744 for destination in 0..n {
745 let coefficient = block[source * n + destination];
746 if adjoint {
747 expected[(source, column)] +=
748 coefficient.conj() * input[(destination, column)];
749 } else {
750 expected[(destination, column)] +=
751 coefficient * input[(source, column)];
752 }
753 }
754 }
755 }
756 self.plan
757 .apply(
758 k,
759 d,
760 self.radial,
761 view(&input),
762 view_mut(&mut actual),
763 adjoint,
764 )
765 .unwrap();
766 prop_assert_eq!(actual, expected);
767 }
768 let inverted = self.evaluate(d.map(|x| -x));
769 let (sin, cos) = angle.sin_cos();
770 let rotated = self.evaluate([d[0] * cos - d[1] * sin, d[0] * sin + d[1] * cos, d[2]]);
771 let scale = block.iter().map(|v| v.norm()).fold(1e-300, f64::max);
772 let cotangent = patterned(n, n, seed);
773 let (mut position, mut wavenumber) = ([0.0; 3], Complex::default());
774 let mut derivative_scale = 0.0;
775 for (&from, g) in self.modes.iter().zip(cotangent.column_iter()) {
776 for (&to, &g) in self.modes.iter().zip(g.iter()) {
777 let value = self.entry(&block, to, from);
778 let direct =
779 sw::cartesian_translation(to, from, k, d, self.helicity, self.radial)
780 .unwrap();
781 prop_assert_close!(
782 value,
783 direct.value,
784 1e-13 * scale,
785 "{:?} <- {:?}",
786 to,
787 from
788 );
789 for (sum, derivative) in position.iter_mut().zip(direct.position) {
790 *sum += (g.conj() * derivative).re;
791 }
792 wavenumber += g * direct.k.conj();
793 derivative_scale += g.norm()
794 * (direct.position.iter().map(|v| v.norm()).sum::<f64>() + direct.k.norm());
795 let sign = if (to.l + from.l) % 2 == 0 { 1.0 } else { -1.0 };
796 let flip = |mode: Mode| Mode {
797 pol: 1 - mode.pol,
798 ..mode
799 };
800 let parity = if self.helicity {
801 sign * self.entry(&block, flip(to), flip(from))
802 } else if to.pol == from.pol {
803 sign * value
804 } else {
805 -sign * value
806 };
807 prop_assert_eq!(self.entry(&inverted, to, from), parity);
808 let reflect = |mode: Mode| Mode { m: -mode.m, ..mode };
809 let sign = if (to.m + from.m) % 2 == 0 { 1.0 } else { -1.0 };
810 let reciprocal = sign * self.entry(&inverted, reflect(from), reflect(to));
811 prop_assert_close!(value, reciprocal, 1e-12 * scale);
812 let phase = Complex::from_polar(1.0, f64::from(from.m - to.m) * angle);
813 prop_assert_close!(
814 self.entry(&rotated, to, from),
815 phase * value,
816 1e-12 * scale
817 );
818 }
819 }
820 let (gradient, gk) = self
821 .plan
822 .pullback(k, d, self.radial, cotangent.as_slice())
823 .unwrap();
824 let tolerance = 1e-13 * derivative_scale;
825 prop_assert_close!(gradient, position, tolerance);
826 prop_assert_close!(gk, wavenumber, tolerance);
827 let spatial: f64 = gradient.iter().zip(d).map(|(g, x)| g * x).sum();
828 let euler_scale =
829 derivative_scale * (1.0 + k.norm() + d.iter().map(|x| x.abs()).sum::<f64>());
830 prop_assert_close!(spatial, (k * gk.conj()).re, 1e-13 * euler_scale);
831 Ok(())
832 }
833 }
834
835 #[test]
836 fn regular_translation_is_continuous_at_the_origin() {
837 let modes = sw::modes(10).unwrap();
843 let plan = TranslationPlan::between(&modes, &modes, true).unwrap();
844 let k = Complex::new(1.3, 0.1);
845 let origin = plan.evaluate(k, [0.0; 3], Radial::Regular).unwrap();
846 for (index, value) in origin.iter().enumerate() {
847 let identity = if index % (modes.len() + 1) == 0 {
848 1.0
849 } else {
850 0.0
851 };
852 assert!((value - identity).norm() < 1e-13, "entry {index} = {value}");
853 }
854 let cotangent = patterned(modes.len(), modes.len(), 0.3);
855 let pullback = |displacement| {
856 plan.pullback(k, displacement, Radial::Regular, cotangent.as_slice())
857 .unwrap()
858 };
859 let (position_limit, k_limit) = pullback([0.0; 3]);
860 let pullback_scale = cotangent.iter().map(|g| g.norm()).sum::<f64>() * k.norm();
861 let few = sw::modes(2).unwrap();
862 let direct = |displacement| {
863 few.iter()
864 .flat_map(|&from| few.iter().map(move |&to| (to, from)))
865 .map(|(to, from)| {
866 sw::cartesian_translation(to, from, k, displacement, true, Radial::Regular)
867 .unwrap()
868 })
869 .collect::<Vec<_>>()
870 };
871 let direct_limit = direct([0.0; 3]);
872 let displacements = [
873 [0.1 + 0.2 - 0.3, 0.0, 0.0],
874 [1e-100, -2e-100, 1e-100],
875 [0.0, 0.0, 1e-300],
876 [1e-310, 0.0, 0.0],
877 [0.0, -5e-324, 5e-324],
878 ];
879 for displacement in displacements {
880 let block = plan.evaluate(k, displacement, Radial::Regular).unwrap();
881 for (index, (value, limit)) in block.iter().zip(&origin).enumerate() {
882 assert!(
883 (value - limit).norm() < 1e-15,
884 "{displacement:?}: entry {index} = {value}"
885 );
886 }
887 let (position, wavenumber) = pullback(displacement);
888 for (derivative, limit) in position.into_iter().zip(position_limit) {
889 assert!(
890 (derivative - limit).abs() < 1e-14 * pullback_scale,
891 "{displacement:?}: position {derivative} != {limit}"
892 );
893 }
894 assert!(
895 (wavenumber - k_limit).norm() < 1e-14 * pullback_scale,
896 "{displacement:?}: wavenumber {wavenumber} != {k_limit}"
897 );
898 for (index, (term, limit)) in direct(displacement).iter().zip(&direct_limit).enumerate()
899 {
900 let distance = (term.value - limit.value).norm()
901 + (term.k - limit.k).norm()
902 + (0..3)
903 .map(|axis| (term.position[axis] - limit.position[axis]).norm())
904 .sum::<f64>();
905 assert!(
906 distance < 1e-15,
907 "{displacement:?}: pair {index}: {term:?} != {limit:?}"
908 );
909 }
910 }
911 }
912
913 #[test]
916 fn singular_values_do_not_evaluate_unused_radial_derivatives() {
917 let case = Case::new(1, true, Radial::Singular, Complex::from(1.0));
918 let r = 1e-64;
919 let block = case.evaluate([0.0, 0.0, r]);
920 assert!(block.iter().all(|&value| crate::numerics::finite(value)));
921 let scale = block.iter().map(|value| value.norm()).fold(0.0, f64::max);
922 for &from in &case.modes {
923 for &to in &case.modes {
924 let polar = sw::PolarTranslation::new(to, from, true, Radial::Singular).unwrap();
925 let expected = polar
926 .value([Complex::from(r), Complex::default(), Complex::default()])
927 .unwrap();
928 let value = case.entry(&block, to, from);
929 assert!((value - expected).norm() < 1e-13 * scale);
930 }
931 }
932 assert!(
933 case.plan
934 .evaluate(case.k, [0.0, 0.0, 1e-120], Radial::Singular)
935 .is_err()
936 );
937 }
938
939 #[test]
940 fn harmonics_match_lean_model() {
941 let cases = table::<i32, i32>(include_str!(concat!(
943 env!("CARGO_MANIFEST_DIR"),
944 "/../../formal/golden/harmonics.txt"
945 )));
946 let bounds: Vec<_> = cases.iter().map(|(key, _)| key.clone()).collect();
947 assert_eq!(bounds, (0..=8).map(|lmax| vec![lmax]).collect::<Vec<_>>());
948 for (key, pairs) in cases {
949 let lmax = key[0];
950 assert_eq!(pairs.len() % 2, 0, "lmax {lmax}: unpaired field");
951 let expected: Vec<(i32, i32)> = pairs
952 .as_chunks::<2>()
953 .0
954 .iter()
955 .map(|&pair| pair.into())
956 .collect();
957 let actual: Vec<_> = super::harmonics(lmax).collect();
958 assert_eq!(actual, expected, "lmax {lmax}");
959 for (index, (p, m)) in actual.into_iter().enumerate() {
960 assert_eq!(usize::try_from(p * p + p + m).unwrap(), index);
961 }
962 }
963 }
964
965 #[test]
968 fn periodic_pullback_does_not_depend_on_the_thread_count() {
969 for centres in [1, 3] {
970 let basis = |offset| {
971 let positions: Vec<_> = (0..centres)
972 .map(|i| [0.2 * f64::from(i) + offset, -0.1 * f64::from(i), 0.0])
973 .collect();
974 sw::Basis {
975 modes: (0..positions.len())
976 .flat_map(|i| sw::modes(4).unwrap().into_iter().map(move |mode| (i, mode)))
977 .collect(),
978 positions,
979 }
980 };
981 let (destination, source) = (basis(0.0), basis(0.13));
982 let lattice =
983 crate::lattice::BlochLattice::new(&[vec![1.0, 0.0], vec![0.0, 1.0]], &[0.3, 0.1])
984 .unwrap();
985 let ks = [Complex::new(1.1, 0.01), Complex::new(1.3, 0.02)];
986 let g = patterned(destination.modes.len(), source.modes.len(), 0.6);
987 let mut reference = None;
988 for workers in [1, 2, 4, 16, 32] {
989 let pool = rayon::ThreadPoolBuilder::new()
990 .num_threads(workers)
991 .build()
992 .unwrap();
993 let actual = pool.install(|| {
994 let (value, residual) = sw::lattice_expansion(
995 destination.clone(),
996 source.clone(),
997 ks,
998 true,
999 lattice.clone(),
1000 Complex::default(),
1001 )
1002 .unwrap();
1003 let g = residual.pullback(&g).unwrap();
1004 let expansion = &g.expansion;
1005 bits(&[
1006 &value,
1007 &expansion.destination,
1008 &expansion.source,
1009 &expansion.ks,
1010 &g.kpar,
1011 &g.vectors,
1012 ])
1013 });
1014 if let Some(reference) = &reference {
1015 assert_eq!(&actual, reference, "{centres} centres, {workers} threads");
1016 } else {
1017 reference = Some(actual);
1018 }
1019 }
1020 }
1021 }
1022}