1use std::{
8 cell::{Cell, OnceCell, RefCell},
9 f64::consts::PI,
10};
11
12use super::{
13 SumPart,
14 accuracy::{
15 Checked, EARLY_FAILURE_LOSS, Rounding, TERM_ULPS, below_automatic, cancellation,
16 derivative_scale, too_small,
17 },
18 cell::{BlochLattice, Reduction},
19 evaluate,
20 inputs::{Evaluation, Inputs},
21 real::{RealKambe, real_term, spherical_from_radial, spherical_radial},
22 reciprocal::{Diffraction, reciprocal_term, self_term},
23 sheets::Split,
24 shells::{
25 NOT_CONVERGED, SHELL_TOLERANCE, Shells, is_shell_limit, norm, not_converged, peak_shells,
26 real_shells, reciprocal_shells,
27 },
28 spectral::{
29 REAL_ULPS, SPECTRAL_FIRST_W, SPECTRAL_SWITCH_LOSS, prefer_spectral_sw1d, spectral_sw1d,
30 },
31 wave::Family,
32};
33use crate::{Complex, Error, Result, numerics::Jet, special::SmallSplitKambe};
34
35pub(super) fn ewald<const N: usize>(
40 wave: Family,
41 lattice: &BlochLattice,
42 mut inputs: Inputs<N>,
43 eta: Complex,
44 evaluation: Evaluation,
45 shared: Option<&SphericalCache<N>>,
46) -> Result<Jet<N>> {
47 let CellReduction {
48 phase,
49 direct_rows: direct,
50 reciprocal_rows: rows,
51 kpar,
52 kpar_reduced: reduced,
53 given_shift,
54 } = reduce_to_cell(lattice, &mut inputs);
55 let inputs = &inputs;
56 let dim = inputs.dim;
57 let symmetry = Symmetry::of(wave, inputs, &direct, &kpar);
58 if symmetry.vanishes && (N == 0 || matches!(evaluation, Evaluation::EtaDerivative)) {
59 return Ok(Jet::default());
60 }
61 let small = below_automatic(eta);
63 let checked = symmetry.checked(inputs, evaluation);
64 let real_limit = real_shells(lattice, inputs.k.value, eta);
65 let square = (inputs.k.value * eta).powi(2);
69 let real_floor = peak_shells(
70 wave,
71 dim,
72 N > 0,
73 square.re,
74 norm(inputs.r.map(|r| r.value.re)),
75 (&lattice.reciprocal, lattice.direct_reduction.as_ref()),
76 );
77 let reciprocal_floor = peak_shells(
78 wave,
79 dim,
80 N > 0,
81 square.inv().re,
82 norm(kpar.map(|q| q.value.re)),
83 (&lattice.direct, lattice.reciprocal_reduction.as_ref()),
84 );
85 let settle = small && matches!(evaluation, Evaluation::Part(SumPart::Full));
88 let early = settle && probes::early_failure_enabled();
89 let automatic_split = AutomaticSplit {
90 wave,
91 k: inputs.k.value,
92 lattice,
93 shift: given_shift,
94 evaluation,
95 };
96 let monitor = SmallSplitMonitor::new(
97 (small && !matches!(evaluation, Evaluation::EtaDerivative))
98 .then(|| SmallSplitKambe::new(eta)),
99 early,
100 automatic_split,
101 &checked,
102 );
103 let real_summands = RealSummands {
104 wave,
105 inputs,
106 direct: &direct,
107 eta,
108 evaluation,
109 monitor: &monitor,
110 shared: if small { None } else { shared },
111 index: Cell::new(0),
112 };
113 let real_summand = |n, bounds: &mut Rounding<N>| real_summands.at(n, bounds);
114 let real_part = || {
115 Shells::sum(
116 dim,
117 [real_limit, real_floor],
118 &mut { real_summand },
119 settle,
120 small,
121 )
122 };
123 if !evaluation.reciprocal() {
124 let real = real_part()?;
125 let sum = phase * real.sum;
126 if small {
127 cancellation(&[&real], &Jet::default(), 0.0, &phase, &sum, &checked)?;
128 }
129 return Ok(symmetry.zeroed(sum));
130 }
131 let sw1d = match wave {
134 Family::Spherical { l, m } if dim == 1 && probes::spectral_series_enabled() => Some((l, m)),
135 _ => None,
136 };
137 let series_cell = OnceCell::new();
138 let series = || {
139 *series_cell.get_or_init(|| {
140 let (mut series, mut bound) = spectral_sw1d(
141 sw1d?,
142 inputs.k,
143 inputs.r,
144 kpar[0],
145 rows[0][0],
146 inputs.measure,
147 )?;
148 if !evaluation.real() {
149 let real = real_part().ok()?.sum;
150 series -= real;
151 bound += Rounding::relative(&real, REAL_ULPS);
152 }
153 Some((phase * series, bound.times(&phase)))
154 })
155 };
156 let rho = (inputs.r[0].value.re).hypot(inputs.r[1].value.re);
159 if sw1d.is_some()
160 && inputs.k.value.norm() * rho * eta.norm() >= SPECTRAL_FIRST_W
161 && let Some((series, bound)) = series()
162 && f64::EPSILON * bound.value <= SPECTRAL_SWITCH_LOSS * series.value.norm().max(1.0)
163 {
164 return Ok(series);
165 }
166 let real = if evaluation.real() {
167 real_part().map(Some)
168 } else {
169 Ok(None)
170 };
171 let [maximum, extension] =
172 reciprocal_shells(lattice, inputs.k.value, eta, kpar.map(|q| q.value.re));
173 let maximum = if sw1d.is_some() { extension } else { maximum };
178 let mut rounding = Rounding::default();
180 let split = Split::new(inputs.k.value, eta);
181 let reciprocal_summands = ReciprocalSummands {
182 wave,
183 inputs,
184 rows: &rows,
185 kpar: &kpar,
186 kpar_reduced: reduced,
187 eta,
188 split: &split,
189 shared: if !small && dim == 2 && inputs.r[2].value == Complex::default() {
190 shared
191 } else {
192 None
193 },
194 index: Cell::new(0),
195 };
196 let reciprocal = Shells::sum(
197 dim,
198 [maximum, reciprocal_floor],
199 &mut |n, _| reciprocal_summands.at(n, &mut rounding),
200 false,
201 small,
202 );
203 let self_term = if inputs.r.iter().all(|r| r.value == Complex::default()) {
204 self_term(wave, inputs.k, inputs.r, eta)
205 } else {
206 Jet::default()
207 };
208 let total = |real: &Option<Shells<N>>, reciprocal: &Shells<N>| {
209 phase * (real.map_or_else(Jet::default, |real| real.sum) + reciprocal.sum + self_term)
210 };
211 let complete_small_split =
215 |mut real: Option<Shells<N>>, mut reciprocal: Shells<N>, rounding: &mut Rounding<N>| {
216 let scale = Some(total(&real, &reciprocal).norm().max(1.0));
217 let mut tail = 0.0;
218 if !reciprocal.extend(
219 dim,
220 extension,
221 &mut |n, _| reciprocal_summands.at(n, rounding),
222 scale,
223 )? {
224 tail += reciprocal.tail();
225 }
226 if let Some(real) = &mut real
227 && !real.extend(dim, real_limit, &mut { real_summand }, scale)?
228 {
229 tail += real.tail();
230 }
231 let sum = total(&real, &reciprocal);
232 let parts: Vec<&Shells<N>> = real.iter().chain([&reciprocal]).collect();
233 let rounding = cancellation(&parts, &self_term, tail, &phase, &sum, &checked)?;
234 Ok((sum, rounding, tail))
235 };
236 let settled = matches!(&real, Ok(Some(real)) if real.settled);
237 let policy = match (real, reciprocal) {
240 (Err(error), _) | (Ok(_), Err(error)) => Err(error),
241 (Ok(real), Ok(reciprocal)) if small => {
242 complete_small_split(real, reciprocal, &mut rounding)
243 }
244 (Ok(real), Ok(reciprocal)) => Ok((total(&real, &reciprocal), Rounding::default(), 0.0)),
245 };
246 let (result, predicted, truncation) = match policy {
247 Ok((sum, predicted, truncation)) => (Ok(sum), predicted, truncation),
248 Err(error) => (Err(error), Rounding::default(), 0.0),
249 };
250 let result = if sw1d.is_none() {
251 result.map(|sum| symmetry.zeroed(sum))
252 } else {
253 let mut rounding = rounding.times(&phase);
254 rounding += predicted;
255 rounding.value += truncation / f64::EPSILON;
256 prefer_spectral_sw1d(result, rounding, series, inputs.k.value)
257 .map(|sum| symmetry.zeroed(sum))
258 };
259 if !settled {
260 return result;
261 }
262 verify_settled(result?, predicted.value, automatic_split)
263}
264
265#[derive(Default)]
272pub(super) struct SphericalCache<const N: usize> {
273 samples: RefCell<Vec<SphericalRealSample<N>>>,
274 plane_samples: RefCell<Vec<SphericalReciprocalSample<N>>>,
275}
276
277const SHARED_REAL_POINTS: usize = 1024;
281
282#[cfg(test)]
283mod cache_tests {
284 use super::{SHARED_REAL_POINTS, SphericalCache};
285 use crate::{
286 Complex,
287 lattice::{BlochLattice, Family, SumPart, evaluate_shared, inputs::Evaluation},
288 };
289
290 #[test]
293 fn spherical_sample_caches_verify_the_visited_lattice_points() {
294 fn check<const N: usize>() {
295 for dim in 1..=3 {
296 let rows: Vec<Vec<f64>> = (0..dim)
297 .map(|i| (0..dim).map(|j| if i == j { 2.0 } else { 0.2 }).collect())
298 .collect();
299 let lattice = BlochLattice::new(&rows, &vec![0.13; dim]).unwrap();
300 let cache = SphericalCache::<N>::default();
301 for m in [0, 2, -1, 3, -3] {
302 let run = |shared| {
303 let jet = evaluate_shared::<N>(
304 Family::Spherical { l: 3, m },
305 Complex::new(1.2, 0.08),
306 &lattice,
307 [0.3, -0.2, 0.0],
308 Complex::new(1.0, 0.0),
309 Evaluation::Part(SumPart::Full),
310 shared,
311 )
312 .unwrap();
313 crate::test_support::bits(&[&jet.value, &jet.derivative])
314 };
315 let reference = run(None);
316 assert_eq!(run(Some(&cache)), reference);
317 let counts = (
318 cache.samples.borrow().len(),
319 cache.plane_samples.borrow().len(),
320 );
321 assert!(counts.0 > 1);
322 if dim == 2 {
323 assert!(counts.1 > 1);
324 }
325 cache.samples.borrow_mut().reverse();
326 cache.plane_samples.borrow_mut().reverse();
327 assert_eq!(run(Some(&cache)), reference);
328 assert_eq!(
329 (
330 cache.samples.borrow().len(),
331 cache.plane_samples.borrow().len()
332 ),
333 counts,
334 );
335 }
336 }
337 }
338 check::<0>();
339 check::<16>();
340 }
341
342 #[test]
343 fn spherical_sample_cache_keeps_only_a_bounded_prefix() {
344 let lattice = BlochLattice::new(
345 &[
346 vec![2.0, 0.2, 0.2],
347 vec![0.2, 2.0, 0.2],
348 vec![0.2, 0.2, 2.0],
349 ],
350 &[0.13; 3],
351 )
352 .unwrap();
353 let cache = SphericalCache::default();
354 for m in [-2, 0, 2] {
355 evaluate_shared::<16>(
356 Family::Spherical { l: 2, m },
357 Complex::new(1.2, 0.08),
358 &lattice,
359 [0.3, -0.2, 0.4],
360 Complex::new(0.4, 0.0),
361 Evaluation::Part(SumPart::Full),
362 Some(&cache),
363 )
364 .unwrap();
365 assert_eq!(cache.samples.borrow().len(), SHARED_REAL_POINTS);
366 }
367 }
368}
369
370#[derive(Clone, Copy)]
371struct SphericalRealSample<const N: usize> {
372 point: [i64; 3],
373 shift: [Jet<N>; 3],
374 radial: Jet<N>,
375 phase: Jet<N>,
376}
377
378struct SphericalReciprocalSample<const N: usize> {
379 point: [i64; 3],
380 diffraction: Diffraction<N>,
381 phase: Jet<N>,
382 integrals: Vec<Option<Complex>>,
383}
384
385struct RealSummands<'a, const N: usize> {
387 wave: Family,
388 inputs: &'a Inputs<N>,
389 direct: &'a [[Jet<N>; 3]; 3],
391 eta: Complex,
392 evaluation: Evaluation,
393 monitor: &'a SmallSplitMonitor<'a, N>,
394 shared: Option<&'a SphericalCache<N>>,
395 index: Cell<usize>,
396}
397
398impl<const N: usize> RealSummands<'_, N> {
399 #[allow(clippy::inline_always)]
403 #[inline(always)]
404 fn at(&self, n: [i64; 3], bounds: &mut Rounding<N>) -> Result<Jet<N>> {
405 let (wave, inputs, direct, eta, evaluation, monitor) = (
406 self.wave,
407 self.inputs,
408 self.direct,
409 self.eta,
410 self.evaluation,
411 self.monitor,
412 );
413 if let (Some(shared), Family::Spherical { l, m }) = (self.shared, wave) {
414 return Ok(self.spherical_at(shared, l, m, n));
415 }
416 let point = inputs.point(direct, n);
417 let shift = inputs.image(&point);
418 if shift.iter().all(|r| r.value == Complex::default()) {
419 return Ok(Jet::default());
420 }
421 probes::count_real_term();
422 let mut small = monitor
425 .kambe
426 .as_ref()
427 .map(|kambe| (kambe, monitor.largest.get(), Rounding::default()));
428 let kambe = match (&mut small, evaluation) {
429 (Some((kambe, current, bound)), _) => RealKambe::SmallSplit {
430 kambe,
431 largest: current,
432 bound,
433 },
434 (None, Evaluation::EtaDerivative) => RealKambe::EtaDerivative,
435 (None, _) => RealKambe::Plain,
436 };
437 let term = real_term(wave, inputs.k, shift, eta, kambe);
438 let phase = (Complex::i() * inputs.phase(&point)).exp();
439 let Some((_, current, bound)) = small else {
440 return Ok(term * phase);
441 };
442 monitor
443 .largest
444 .set(current.max(Rounding::relative(&term, 1.0)));
445 let (term, bound) = (term * phase, bound.times(&phase));
446 *bounds += bound;
447 monitor.add_to_running(&term, bound)?;
448 Ok(term)
449 }
450
451 fn spherical_at(&self, shared: &SphericalCache<N>, l: i32, m: i32, n: [i64; 3]) -> Jet<N> {
454 let index = self.index.replace(self.index.get() + 1);
455 let mut samples = shared.samples.borrow_mut();
456 let sample = if let Some(&sample) = samples.get(index).filter(|sample| sample.point == n) {
457 sample
458 } else {
459 let point = self.inputs.point(self.direct, n);
460 let shift = self.inputs.image(&point);
461 let radial = if shift.iter().all(|r| r.value == Complex::default()) {
462 Jet::default()
463 } else {
464 spherical_radial(l, self.inputs.k, shift, self.eta)
465 };
466 let sample = SphericalRealSample {
467 point: n,
468 shift,
469 radial,
470 phase: (Complex::i() * self.inputs.phase(&point)).exp(),
471 };
472 if index == samples.len() && index < SHARED_REAL_POINTS {
473 samples.push(sample);
474 }
475 sample
476 };
477 if sample.shift.iter().all(|r| r.value == Complex::default()) {
478 return Jet::default();
479 }
480 probes::count_real_term();
481 spherical_from_radial(l, m, self.inputs.k, sample.shift, sample.radial) * sample.phase
482 }
483}
484
485struct ReciprocalSummands<'a, const N: usize> {
487 wave: Family,
488 inputs: &'a Inputs<N>,
489 rows: &'a [[Jet<N>; 3]; 3],
491 kpar: &'a [Jet<N>; 3],
493 kpar_reduced: bool,
495 eta: Complex,
496 split: &'a Split,
497 shared: Option<&'a SphericalCache<N>>,
498 index: Cell<usize>,
499}
500
501impl<const N: usize> ReciprocalSummands<'_, N> {
502 #[allow(clippy::inline_always)]
506 #[inline(always)]
507 fn at(&self, n: [i64; 3], rounding: &mut Rounding<N>) -> Result<Jet<N>> {
508 if let (Some(shared), Family::Spherical { l, .. }) = (self.shared, self.wave) {
509 let index = self.index.replace(self.index.get() + 1);
510 let maximum = 1024 / (usize::try_from(l).unwrap_or_default() + 2);
514 if index < maximum {
515 let mut samples = shared.plane_samples.borrow_mut();
516 if index == samples.len() {
517 samples.push(self.prepare(n)?);
518 }
519 if let Some(sample) = samples.get_mut(index).filter(|sample| sample.point == n) {
520 return self.term(
521 sample.diffraction,
522 sample.phase,
523 rounding,
524 Some(&mut sample.integrals),
525 );
526 }
527 }
528 }
529 let sample = self.prepare(n)?;
530 self.term(sample.diffraction, sample.phase, rounding, None)
531 }
532
533 #[allow(clippy::inline_always)]
535 #[inline(always)]
536 fn prepare(&self, n: [i64; 3]) -> Result<SphericalReciprocalSample<N>> {
537 let (inputs, rows, kpar) = (self.inputs, self.rows, self.kpar);
538 let dim = inputs.dim;
539 let vector = inputs.point(rows, n);
540 let mut q = [Jet::default(); 3];
541 for j in 0..dim {
542 q[inputs.axes[j]] = vector[j] + kpar[j];
543 }
544 let phase = (-Complex::i()
545 * q.into_iter()
546 .zip(inputs.r)
547 .map(|(q, r)| q * r)
548 .sum::<Jet<N>>())
549 .exp();
550 Ok(SphericalReciprocalSample {
551 point: n,
552 diffraction: Diffraction::new(
553 q,
554 !self.kpar_reduced && n == [0; 3],
555 inputs.k,
556 self.eta,
557 )?,
558 phase,
559 integrals: Vec::new(),
560 })
561 }
562
563 #[allow(clippy::inline_always)]
564 #[inline(always)]
565 fn term(
566 &self,
567 diffraction: Diffraction<N>,
568 phase: Jet<N>,
569 rounding: &mut Rounding<N>,
570 plane_integrals: Option<&mut Vec<Option<Complex>>>,
571 ) -> Result<Jet<N>> {
572 let mut bound = Rounding::default();
573 let term = reciprocal_term(
574 self.wave,
575 self.inputs.dim,
576 self.inputs.k,
577 diffraction,
578 self.inputs.r,
579 self.eta,
580 self.split,
581 self.inputs.measure,
582 &mut bound,
583 plane_integrals,
584 )?;
585 *rounding += bound.times(&phase);
586 Ok(term * phase)
587 }
588}
589
590struct CellReduction<const N: usize> {
593 phase: Jet<N>,
596 direct_rows: [[Jet<N>; 3]; 3],
598 reciprocal_rows: [[Jet<N>; 3]; 3],
600 kpar: [Jet<N>; 3],
602 kpar_reduced: bool,
604 given_shift: [f64; 3],
606}
607
608fn reduce_to_cell<const N: usize>(
611 lattice: &BlochLattice,
612 inputs: &mut Inputs<N>,
613) -> CellReduction<N> {
614 let dim = inputs.dim;
615 let coordinates = |dual: &[[f64; 3]; 3], vector: [f64; 3]| -> [f64; 3] {
617 std::array::from_fn(|i| {
618 if i < dim {
619 (0..dim).map(|j| dual[i][j] * vector[j]).sum::<f64>() / (2.0 * PI)
620 } else {
621 0.0
622 }
623 })
624 };
625 let direct_reduction = lattice.direct_reduction.as_ref();
628 let shift = std::array::from_fn(|j| inputs.r[inputs.axes[j]].value.re);
629 let cells = Reduction::given(
630 direct_reduction,
631 Reduction::round(
632 direct_reduction,
633 coordinates(&lattice.reciprocal, shift),
634 dim,
635 ),
636 dim,
637 );
638 let given_shift = inputs.r.map(|r| r.value.re);
639 let cell = inputs.combination(&inputs.direct, cells);
640 for (&axis, &cell) in inputs.axes.iter().zip(&cell).take(dim) {
641 inputs.r[axis] -= cell;
642 }
643 let phase = (-Complex::i() * inputs.phase(&cell)).exp();
644 let direct_rows = inputs.reduce(&inputs.direct, direct_reduction);
645 let reciprocal_reduction = lattice.reciprocal_reduction.as_ref();
646 let reciprocal_rows = inputs.reduce(&inputs.reciprocal, reciprocal_reduction);
647 let cells = Reduction::round(
648 reciprocal_reduction,
649 coordinates(&lattice.direct, lattice.kpar),
650 dim,
651 );
652 let mut kpar = inputs.kpar;
653 let kpar_reduced = cells.iter().any(|&c| c != 0.0);
654 if kpar_reduced {
655 let point = inputs.combination(&reciprocal_rows, cells);
656 for (kpar, point) in kpar.iter_mut().zip(point).take(dim) {
657 *kpar -= point;
658 }
659 }
660 CellReduction {
661 phase,
662 direct_rows,
663 reciprocal_rows,
664 kpar,
665 kpar_reduced,
666 given_shift,
667 }
668}
669
670struct Symmetry<const N: usize> {
674 vanishes: bool,
676 k_alone: [bool; N],
679}
680
681impl<const N: usize> Symmetry<N> {
682 fn of(wave: Family, inputs: &Inputs<N>, direct: &[[Jet<N>; 3]; 3], kpar: &[Jet<N>; 3]) -> Self {
685 Self {
686 vanishes: vanishes(wave, inputs, direct, kpar),
687 k_alone: std::array::from_fn(|slot| {
688 inputs
689 .r
690 .iter()
691 .chain(&inputs.kpar)
692 .chain(inputs.direct.iter().flatten())
693 .all(|input| input.derivative[slot] == Complex::default())
694 }),
695 }
696 }
697
698 fn zeroed(&self, mut sum: Jet<N>) -> Jet<N> {
700 if self.vanishes {
701 sum.value = Complex::default();
702 for (derivative, &k_alone) in sum.derivative.iter_mut().zip(&self.k_alone) {
703 if k_alone {
704 *derivative = Complex::default();
705 }
706 }
707 }
708 sum
709 }
710
711 fn checked(&self, inputs: &Inputs<N>, evaluation: Evaluation) -> Checked<N> {
714 Checked {
715 value: !self.vanishes,
716 units: std::array::from_fn(|slot| {
717 if matches!(evaluation, Evaluation::EtaDerivative)
718 || (self.vanishes && self.k_alone[slot])
719 {
720 None
721 } else {
722 Some(derivative_scale(slot, inputs.dim, inputs.k.value))
723 }
724 }),
725 }
726 }
727}
728
729#[derive(Clone, Copy)]
733struct AutomaticSplit<'a> {
734 wave: Family,
735 k: Complex,
736 lattice: &'a BlochLattice,
737 shift: [f64; 3],
739 evaluation: Evaluation,
740}
741
742impl AutomaticSplit<'_> {
743 fn sum<const N: usize>(&self) -> Result<Jet<N>> {
744 evaluate::<N>(
745 self.wave,
746 self.k,
747 self.lattice,
748 self.shift,
749 Complex::default(),
750 self.evaluation,
751 )
752 }
753}
754
755struct SmallSplitMonitor<'a, const N: usize> {
759 kambe: Option<SmallSplitKambe>,
762 largest: Cell<Rounding<N>>,
765 early: bool,
768 running: Cell<Rounding<N>>,
770 automatic_sum: OnceCell<Option<Jet<N>>>,
773 automatic_split: AutomaticSplit<'a>,
774 checked: &'a Checked<N>,
775}
776
777impl<'a, const N: usize> SmallSplitMonitor<'a, N> {
778 fn new(
779 kambe: Option<SmallSplitKambe>,
780 early: bool,
781 automatic_split: AutomaticSplit<'a>,
782 checked: &'a Checked<N>,
783 ) -> Self {
784 Self {
785 kambe,
786 largest: Cell::new(Rounding::default()),
787 early,
788 running: Cell::new(Rounding::default()),
789 automatic_sum: OnceCell::new(),
790 automatic_split,
791 checked,
792 }
793 }
794
795 fn add_to_running(&self, term: &Jet<N>, bound: Rounding<N>) -> Result<()> {
798 if self.early {
799 let mut total = self.running.get();
800 total += Rounding::relative(term, TERM_ULPS);
801 total += bound;
802 self.running.set(total);
803 self.hopeless(&total)?;
804 }
805 Ok(())
806 }
807
808 fn hopeless(&self, bound: &Rounding<N>) -> Result<()> {
811 let checked = self.checked;
812 let beyond = |bound: f64, scale: f64| f64::EPSILON * bound > EARLY_FAILURE_LOSS * scale;
813 let slots = || bound.derivative.iter().zip(checked.units);
816 let candidate = (checked.value && beyond(bound.value, 1.0))
817 || slots().any(|(&bound, unit)| unit.is_some_and(|unit| beyond(bound, unit)));
818 if !candidate {
819 return Ok(());
820 }
821 let Some(automatic) = self
822 .automatic_sum
823 .get_or_init(|| self.automatic_split.sum::<N>().ok())
824 else {
825 return Ok(());
826 };
827 let scale = automatic.value.norm().max(1.0);
828 if checked.value && beyond(bound.value, scale) {
829 return Err(too_small(f64::EPSILON * bound.value / scale, false));
830 }
831 for ((&bound, unit), derivative) in slots().zip(&automatic.derivative) {
832 if let Some(unit) = unit {
833 let scale = scale.mul_add(unit, derivative.norm());
834 if beyond(bound, scale) {
835 return Err(too_small(f64::EPSILON * bound / scale, true));
836 }
837 }
838 }
839 Ok(())
840 }
841}
842
843fn verify_settled<const N: usize>(
848 sum: Jet<N>,
849 predicted: f64,
850 automatic_split: AutomaticSplit<'_>,
851) -> Result<Jet<N>> {
852 let automatic = automatic_split.sum::<N>().map_err(|error| {
854 if is_shell_limit(&error) {
855 Error::NotConverged(format!(
856 "{NOT_CONVERGED}, neither at this split nor at the automatic one"
857 ))
858 } else {
859 error
860 }
861 })?;
862 let scale = automatic.value.norm().max(1.0);
863 let relative = f64::EPSILON.mul_add(predicted / scale, 4.0 * SHELL_TOLERANCE);
864 let agrees = |a: Complex, b: Complex| (a - b).norm() <= relative * (b.norm() + scale);
865 if agrees(sum.value, automatic.value)
866 && sum
867 .derivative
868 .iter()
869 .zip(&automatic.derivative)
870 .all(|(&a, &b)| agrees(a, b))
871 {
872 Ok(sum)
873 } else {
874 Err(not_converged())
875 }
876}
877
878pub(super) fn vanishes<const N: usize>(
892 wave: Family,
893 inputs: &Inputs<N>,
894 direct: &[[Jet<N>; 3]; 3],
895 kpar: &[Jet<N>; 3],
896) -> bool {
897 let odd = match wave {
898 Family::Spherical { l, .. } => l % 2 != 0,
899 Family::Cylindrical { m } => m % 2 != 0,
900 };
901 let dim = inputs.dim;
902 let lattice_axis = |axis: &usize| inputs.axes[..dim].contains(axis);
903 if !odd
904 || kpar[..dim].iter().any(|q| q.value != Complex::default())
905 || (0..3).any(|axis| !lattice_axis(&axis) && inputs.r[axis].value != Complex::default())
906 {
907 return false;
908 }
909 let twice: [f64; 3] = std::array::from_fn(|j| {
910 if j < dim {
911 2.0 * inputs.r[inputs.axes[j]].value.re
912 } else {
913 0.0
914 }
915 });
916 let count = 3_usize.pow(u32::try_from(dim).unwrap_or(3));
918 (0..count).any(|index| {
919 let mut rest = index;
920 let coefficients: [f64; 3] = std::array::from_fn(|i| {
921 if i >= dim {
922 return 0.0;
923 }
924 let digit = rest % 3;
925 rest /= 3;
926 match digit {
927 0 => 0.0,
928 1 => 1.0,
929 _ => -1.0,
930 }
931 });
932 let point = inputs.combination(direct, coefficients);
933 (0..dim).all(|j| point[j].value.re == twice[j])
934 })
935}
936
937#[cfg(test)]
938pub(crate) mod probes {
939 use std::{cell::Cell, thread::LocalKey};
946
947 use super::super::{
948 Derivatives,
949 cell::BlochLattice,
950 inputs::{Inputs, unpack},
951 reduced::{Reduced, reduced_kambe},
952 sheets::{Split, lower_side, split_sheet},
953 spectral::spectral_sw1d,
954 wave::Family,
955 };
956 use crate::{Complex, numerics::Jet};
957
958 thread_local! {
959 static EWALD_ONLY: Cell<bool> = const { Cell::new(false) };
960 static LATE_FAILURE: Cell<bool> = const { Cell::new(false) };
961 static REAL_TERMS: Cell<u64> = const { Cell::new(0) };
962 }
963
964 struct Restore {
966 switch: &'static LocalKey<Cell<bool>>,
967 previous: bool,
968 }
969
970 impl Drop for Restore {
971 fn drop(&mut self) {
972 self.switch.set(self.previous);
973 }
974 }
975
976 fn with_switch<T>(switch: &'static LocalKey<Cell<bool>>, run: impl FnOnce() -> T) -> T {
978 let _restore = Restore {
979 switch,
980 previous: switch.replace(true),
981 };
982 run()
983 }
984
985 pub(crate) fn ewald_only<T>(run: impl FnOnce() -> T) -> T {
988 with_switch(&EWALD_ONLY, run)
989 }
990
991 pub(crate) fn late_failure<T>(run: impl FnOnce() -> T) -> T {
997 with_switch(&LATE_FAILURE, run)
998 }
999
1000 pub(crate) struct RealTerms {
1003 start: u64,
1004 }
1005
1006 impl RealTerms {
1007 pub(crate) fn count(&self) -> u64 {
1009 REAL_TERMS.get() - self.start
1010 }
1011 }
1012
1013 pub(crate) fn real_terms() -> RealTerms {
1015 RealTerms {
1016 start: REAL_TERMS.get(),
1017 }
1018 }
1019
1020 #[allow(clippy::inline_always)]
1022 #[inline(always)]
1023 pub(super) fn spectral_series_enabled() -> bool {
1024 !EWALD_ONLY.get()
1025 }
1026
1027 #[allow(clippy::inline_always)]
1029 #[inline(always)]
1030 pub(super) fn early_failure_enabled() -> bool {
1031 !LATE_FAILURE.get()
1032 }
1033
1034 #[allow(clippy::inline_always)]
1036 #[inline(always)]
1037 pub(super) fn count_real_term() {
1038 REAL_TERMS.set(REAL_TERMS.get() + 1);
1039 }
1040
1041 pub(crate) fn reduced_paths(
1046 twice_n: i32,
1047 k: Complex,
1048 q2: f64,
1049 eta: Complex,
1050 distance: f64,
1051 ) -> [(Complex, f64); 2] {
1052 let v = lower_side((q2 / (k * k) - 1.0) / (2.0 * eta * eta));
1053 let t = (k * distance * eta).powi(2);
1054 let sheet = Some(split_sheet(&Split::new(k, eta), q2.into()));
1055 let mut reduced = Reduced::<0>::new(Jet::constant(v), Jet::constant(t), sheet);
1056 let kambe = if twice_n % 2 == 0 {
1057 reduced_kambe(twice_n, v, t.sqrt(), sheet)
1058 } else {
1059 (reduced.by_even_recurrence(twice_n), 0.0)
1060 };
1061 [reduced.series(twice_n), kambe]
1062 }
1063
1064 pub(crate) fn spectral_sw1d_derivatives(
1071 (l, m): (i32, i32),
1072 k: Complex,
1073 lattice: &BlochLattice,
1074 r: [f64; 3],
1075 ) -> Option<(Derivatives, Derivatives)> {
1076 let inputs = Inputs::<6>::new(Family::Spherical { l, m }, k, lattice, r, true, true);
1077 let (series, bound) = spectral_sw1d(
1078 (l, m),
1079 inputs.k,
1080 inputs.r,
1081 inputs.kpar[0],
1082 inputs.reciprocal[0][0],
1083 inputs.measure,
1084 )?;
1085 let bound = Jet {
1086 value: bound.value.into(),
1087 derivative: bound.derivative.map(Complex::from),
1088 };
1089 Some((unpack(series, 1), unpack(bound, 1)))
1090 }
1091}
1092
1093#[cfg(not(test))]
1095mod probes {
1096 #[allow(clippy::inline_always)]
1098 #[inline(always)]
1099 pub(super) const fn spectral_series_enabled() -> bool {
1100 true
1101 }
1102
1103 #[allow(clippy::inline_always)]
1105 #[inline(always)]
1106 pub(super) const fn early_failure_enabled() -> bool {
1107 true
1108 }
1109
1110 #[allow(clippy::inline_always)]
1112 #[inline(always)]
1113 pub(super) const fn count_real_term() {}
1114}