treams_core/lattice/accuracy.rs
1//! Rounding-error bounds, and the failure of sums whose Ewald parts cancel.
2//!
3//! treams-rs extension: treams returns the sum of its parts without a bound.
4
5use std::f64::consts::PI;
6
7use super::{
8 inputs::{K, KPAR},
9 shells::Shells,
10};
11use crate::{Complex, Error, Result, numerics::Jet};
12
13/// First-order bounds on the absolute rounding errors of a jet's value and of each of
14/// its derivatives, in units of epsilon.
15#[derive(Clone, Copy, Debug)]
16pub(super) struct Rounding<const N: usize> {
17 pub(super) value: f64,
18 pub(super) derivative: [f64; N],
19}
20
21impl<const N: usize> Default for Rounding<N> {
22 fn default() -> Self {
23 Self::of(0.0)
24 }
25}
26
27impl<const N: usize> Rounding<N> {
28 /// A bound on a value only, whose derivatives are exact.
29 pub(super) const fn of(value: f64) -> Self {
30 Self {
31 value,
32 derivative: [0.0; N],
33 }
34 }
35
36 /// The bounds of `exact * inexact`, where `self` bounds `inexact`. Bounds take the
37 /// cheaper `|Re| + |Im|` for moduli, at most `sqrt 2` larger.
38 pub(super) fn times(self, exact: &Jet<N>) -> Self {
39 if self.value == 0.0 && self.derivative.iter().all(|&d| d == 0.0) {
40 return self;
41 }
42 let scale = exact.value.l1_norm();
43 Self {
44 value: scale * self.value,
45 derivative: std::array::from_fn(|i| {
46 scale * self.derivative[i] + exact.derivative[i].l1_norm() * self.value
47 }),
48 }
49 }
50
51 /// The moduli of a jet's value and derivatives times `factor`: a bound for a jet
52 /// computed with a relative error of `factor` ulps in each component.
53 pub(super) fn relative(jet: &Jet<N>, factor: f64) -> Self {
54 Self {
55 value: factor * jet.value.l1_norm(),
56 derivative: jet.derivative.map(|d| factor * d.l1_norm()),
57 }
58 }
59
60 /// The bounds of `phase * inexact` for a `phase` of modulus 1, where `self` bounds
61 /// `inexact`: its value keeps its bound, which the derivatives of the phase carry
62 /// into theirs.
63 fn turned(self, phase: &Jet<N>) -> Self {
64 Self {
65 value: self.value,
66 derivative: std::array::from_fn(|i| {
67 phase.derivative[i]
68 .norm()
69 .mul_add(self.value, self.derivative[i])
70 }),
71 }
72 }
73
74 /// The larger bound of each component.
75 pub(super) fn max(self, other: Self) -> Self {
76 Self {
77 value: self.value.max(other.value),
78 derivative: std::array::from_fn(|i| self.derivative[i].max(other.derivative[i])),
79 }
80 }
81}
82
83impl<const N: usize> std::ops::AddAssign for Rounding<N> {
84 fn add_assign(&mut self, other: Self) {
85 self.value += other.value;
86 for (bound, other) in self.derivative.iter_mut().zip(other.derivative) {
87 *bound += other;
88 }
89 }
90}
91
92/// Largest rounding error bound of a 1D spherical sum relative to `max(|S|, 1)`, and
93/// largest loss of any sum to the [`cancellation`] of its Ewald parts relative to the
94/// scale of a component, before it fails.
95///
96/// Calibrated: the bounds exceed the errors by a median factor of 17 to 87, and fall
97/// short of them by at most a factor of 4.5.
98pub(super) const MAX_LOSS: f64 = 1e-3;
99
100/// Relative rounding error of every Ewald term and of the self term, in ulps, that
101/// [`cancellation`] counts.
102pub(super) const TERM_ULPS: f64 = 8.0;
103
104/// The largest `Re(1 / (2 eta^2))` of the automatic split ([`resolve_split`]): `16 / pi`
105/// from `|k L| = 8` on, with a relative margin of 1e-12 for its rounding.
106///
107/// It sets the growth `e^Re(1 / (2 eta^2))` of the Ewald parts. Only splits beyond it,
108/// the small splits:
109///
110/// - check the [`cancellation`] of their parts;
111/// - take the real-space integrals of [`SmallSplitKambe`];
112/// - continue both parts to the complete sum ([`Shells`]);
113/// - consider single Kambe integrals ([`Reduced::prefers_single`]).
114///
115/// The other splits keep their values and cost.
116///
117/// [`resolve_split`]: super::cell::resolve_split
118/// [`SmallSplitKambe`]: crate::special::SmallSplitKambe
119/// [`Reduced::prefers_single`]: super::reduced::Reduced::prefers_single
120const AUTOMATIC_GROWTH: f64 = 16.0 / PI * (1.0 + 1e-12);
121
122/// Whether the split `eta` lies below every automatic one ([`AUTOMATIC_GROWTH`]).
123pub(super) fn below_automatic(eta: Complex) -> bool {
124 (0.5 / (eta * eta)).re > AUTOMATIC_GROWTH
125}
126
127/// The components of a sum that [`cancellation`] checks: the value unless the sum
128/// vanishes by symmetry ([`vanishes`]), which returns it as an exact zero, and each
129/// derivative that such a sum does not return as zero either, with the scale `s` of
130/// its slot ([`derivative_scale`]).
131///
132/// [`vanishes`]: super::ewald::vanishes
133pub(super) struct Checked<const N: usize> {
134 pub(super) value: bool,
135 pub(super) units: [Option<f64>; N],
136}
137
138/// The scale `s` of the derivative in jet slot `slot` of a sum with `dim` lattice
139/// dimensions and the wavenumber `k`, on which it counts `|dS| + max(|S|, 1) s`: `1 / |k|`
140/// for the wavenumber and the Bloch vector, `|k|` for the shift and the lattice vectors.
141pub(super) fn derivative_scale(slot: usize, dim: usize, k: Complex) -> f64 {
142 if slot == K || (KPAR..KPAR + dim).contains(&slot) {
143 1.0 / k.norm()
144 } else {
145 k.norm()
146 }
147}
148
149/// The predicted absolute rounding errors, in units of epsilon, of the value and
150/// derivatives of `sum` (`phase` times the Ewald `parts` and the self term `other`), or a
151/// failure where they cancel beyond use: where the error of a `checked` component plus
152/// the truncation `tail` of parts that did not converge to the complete sum
153/// ([`Shells::tail`]) exceeds [`MAX_LOSS`] of its scale, `max(|S|, 1)` for the value
154/// and `|dS| + max(|S|, 1) s` for a derivative (`S = 0` for a sum that vanishes by
155/// symmetry). Each component predicts the bounds of the real-space Kambe integrals
156/// ([`SmallSplitKambe`]) plus [`TERM_ULPS`] times the moduli of the terms and of the self
157/// term, and an ulp of each compensated part ([`Shells`]).
158///
159/// Calibrated against the automatic split: the prediction exceeds the error of every sum
160/// more than 1e-11 off by a median factor of 45 at real and 87 at rotated splits (at
161/// least 4), and no sum that passes is more than 1.2e-4 off (not counting 1D spherical
162/// sums off the axis, see [`prefer_spectral_sw1d`]).
163///
164/// [`SmallSplitKambe`]: crate::special::SmallSplitKambe
165/// [`prefer_spectral_sw1d`]: super::spectral::prefer_spectral_sw1d
166pub(super) fn cancellation<const N: usize>(
167 parts: &[&Shells<N>],
168 other: &Jet<N>,
169 tail: f64,
170 phase: &Jet<N>,
171 sum: &Jet<N>,
172 checked: &Checked<N>,
173) -> Result<Rounding<N>> {
174 let mut rounding = Rounding::relative(other, TERM_ULPS);
175 for part in parts {
176 rounding += part.bound;
177 rounding += Rounding::relative(&part.sum, 1.0);
178 }
179 // The phase of the shift into the cell has modulus 1 (a real Bloch vector).
180 let rounding = rounding.turned(phase);
181 // A sum whose value is not checked is returned as an exact zero, whatever rounding
182 // its parts leave in it: its scale is 1.
183 let scale = if checked.value {
184 sum.value.norm().max(1.0)
185 } else {
186 1.0
187 };
188 // A bound that overflowed to NaN counts as lost.
189 let loss = |bound: f64, scale: f64| f64::EPSILON.mul_add(bound, tail) / scale;
190 let lost = |loss: f64| loss.is_nan() || loss > MAX_LOSS;
191 let value = loss(rounding.value, scale);
192 if checked.value && lost(value) {
193 return Err(too_small(value, false));
194 }
195 for ((&bound, derivative), unit) in rounding
196 .derivative
197 .iter()
198 .zip(&sum.derivative)
199 .zip(checked.units)
200 {
201 if let Some(unit) = unit {
202 let derivative = loss(bound, scale.mul_add(unit, derivative.norm()));
203 if lost(derivative) {
204 return Err(too_small(derivative, true));
205 }
206 }
207 }
208 Ok(rounding)
209}
210
211/// The failure of a sum whose cancelling Ewald parts lose `loss` of `max(|S|, 1)`, or
212/// of a `derivative` on its scale `|dS| + max(|S|, 1) s`.
213pub(super) fn too_small(loss: f64, derivative: bool) -> Error {
214 let scale = if derivative {
215 "of |dS| + max(|S|, 1) s in a derivative"
216 } else {
217 "of max(|S|, 1)"
218 };
219 Error::NotConverged(format!(
220 "Ewald split too small: its cancelling parts lose about {loss:.0e} {scale}; \
221 use a larger split (eta = 0 selects one)"
222 ))
223}
224
225/// The loss beyond which a complete sum below every automatic split fails early, while
226/// its real-space terms still add up.
227///
228/// The loss is relative to the scale of a component of the sum at the automatic split,
229/// as [`cancellation`] counts it. The failure comes before the far shells, where the
230/// Kambe series of the terms grow with `1 / eta^2`. The predicted loss only grows with
231/// the terms, so the complete sum would fail too unless it came back more than a scale
232/// off the automatic one. The automatic sum (the jet, for a jet) is evaluated once,
233/// where a loss exceeds this of 1 for the value or of `s` for a derivative; a sum whose
234/// automatic split fails continues.
235///
236/// Calibrated: without the early failure, a 3D sum at the split 0.03 takes 29 s to fail
237/// at the shell limit.
238pub(super) const EARLY_FAILURE_LOSS: f64 = 2.0 * MAX_LOSS;