Skip to main content

num_valid/algorithms/
accumulators.rs

1#![deny(rustdoc::broken_intra_doc_links)]
2
3//! Pluggable accumulation strategies for numerical algorithms.
4//!
5//! This module defines the [`Accumulator`] trait and a set of concrete
6//! implementations used internally by
7//! [`crate::algorithms::vector_norms`] and available for use in custom
8//! algorithms.
9//!
10//! # Trait hierarchy
11//!
12//! ```text
13//! Accumulator
14//! ├── SumAccumulator    (adds rescale_by; used for L1/L2 norms)
15//! │   ├── NaiveSum
16//! │   └── NeumaierSum
17//! ├── MaxAccumulator    (running maximum of real values)
18//! ├── MinAccumulator    (running minimum of real values)
19//! ├── MaxAbsValueAccumulator  (running maximum of |x|; used for L∞ norm)
20//! └── ScaledSumSqAccumulator  (scaled sum-of-squares with dynamic scaling; used for L2 norm)
21//! ```
22//!
23//! # Choosing a summation strategy
24//!
25//! | Strategy | Speed | Accuracy | Use when |
26//! |----------|-------|----------|----------|
27//! | [`NaiveSum`] | fastest | O(N·ε) error | data is well-scaled |
28//! | [`NeumaierSum`] | ~2× slower | O(ε) error | components span many magnitudes |
29//!
30//! # Parallel reduction
31//!
32//! All accumulators support parallel chunked reductions via the default
33//! [`Accumulator::new_parallel`] method, which uses rayon `fold` + `reduce`
34//! with [`Accumulator::combine`] as the merge operation. `Accumulator::new()`
35//! must be the identity element for `combine` — this holds for all types in
36//! this module.
37
38use crate::{
39    FpScalar, RealScalar,
40    core::traits::validation::sealed,
41    functions::{Abs, Pow},
42    scalars::NonNegativeRealScalar,
43};
44use getset::Getters;
45use num::{Complex, One, Zero};
46use rayon::prelude::*;
47use std::ops::{Add, AddAssign, Mul, MulAssign};
48
49//---------------------------------------------------------
50/// Base trait for incremental accumulation over a sequence of values.
51///
52/// An accumulator collects items one at a time via [`push`](Self::push) and
53/// produces a final result via [`result`](Self::result). Partial accumulators
54/// can be merged with [`combine`](Self::combine), enabling chunked and parallel
55/// reductions.
56///
57/// # Correctness requirement for `combine`
58///
59/// [`new()`](Self::new) must be the identity element for `combine`: for any
60/// accumulator `x`, both `combine(new(), x)` and `combine(x, new())` must
61/// produce an accumulator equivalent to `x`. This invariant is required by
62/// [`new_parallel`](Self::new_parallel).
63pub trait Accumulator: Sized {
64    /// The type of values pushed into the accumulator.
65    type Input;
66    /// The type of the final result produced by [`result`](Self::result).
67    type Output;
68
69    /// Creates a new, empty accumulator (the identity for [`combine`](Self::combine)).
70    fn new() -> Self;
71
72    /// Adds a single value to the accumulator.
73    fn push(&mut self, value: Self::Input);
74
75    /// Merges `other` into `self`.
76    ///
77    /// After this call `self` represents the accumulation of all values pushed
78    /// into either `self` or `other`. Used by [`new_parallel`](Self::new_parallel)
79    /// to merge per-chunk partial results.
80    fn combine(&mut self, other: Self);
81
82    /// Consumes the accumulator and returns the final result.
83    fn result(self) -> Self::Output;
84
85    /// Creates a new accumulator by sequentially adding all values from `values`.
86    ///
87    /// This is the simplest way to build an accumulator from owned input data.
88    /// For chunked or parallel reductions, prefer [`Accumulator::combine`]
89    /// on partial accumulators.
90    ///
91    /// # Example
92    ///
93    /// ```
94    /// use num_valid::algorithms::accumulators::*;
95    ///
96    /// let values = vec![1.0, 1.0e100, 1.0, -1.0e100];
97    ///
98    /// let neumaier = NeumaierSum::new_sequential(values);
99    /// let sum = neumaier.result();
100    /// println!("Sum: {}", sum);
101    /// assert_eq!(sum, 2.0);
102    /// ```
103    fn new_sequential<I>(values: I) -> Self
104    where
105        I: IntoIterator<Item = Self::Input>,
106    {
107        let mut acc = Self::new();
108        values.into_iter().for_each(|v| acc.push(v));
109        acc
110    }
111
112    /// Builds an accumulator by processing a parallel iterator.
113    ///
114    /// The iterator is partitioned into chunks by rayon; each chunk is folded
115    /// into a partial accumulator with [`push`](Self::push), and the partial
116    /// results are merged with [`combine`](Self::combine).
117    ///
118    /// Requires `Self: Send`.
119    ///
120    /// Any additional `Send` constraints on iterator items are enforced by the
121    /// concrete [`IntoParallelIterator`] implementation.
122    ///
123    /// # Example
124    ///
125    /// ```
126    /// use num_valid::algorithms::accumulators::*;
127    /// use rayon::prelude::*;
128    ///
129    /// let values = vec![1.0_f64, 2.0, 3.0, 4.0];
130    /// let sum = NaiveSum::new_parallel(values.into_par_iter()).result();
131    /// assert_eq!(sum, 10.0);
132    /// ```
133    fn new_parallel<I>(values: I) -> Self
134    where
135        Self: Send,
136        I: IntoParallelIterator<Item = Self::Input>,
137    {
138        values
139            .into_par_iter()
140            .fold(
141                || Self::new(),
142                |mut acc, item| {
143                    acc.push(item);
144                    acc
145                },
146            )
147            .reduce(
148                || Self::new(),
149                |mut a, b| {
150                    a.combine(b);
151                    a
152                },
153            )
154    }
155}
156//---------------------------------------------------------
157
158//---------------------------------------------------------
159/// A generic accumulator for summing a sequence of values with configurable
160/// precision/performance trade-offs.
161///
162/// The trait abstracts over how terms are collected, allowing different
163/// accumulation strategies to be swapped in without duplicating the calling
164/// code.  It is used internally by the [`crate::algorithms::vector_norms`] vector-norm functions (scaled
165/// sum-of-squares loop), but is general enough for any sequential summation
166/// task.  Two strategies ship with the crate:
167///
168/// - **Naive** ([`NaiveSum`]): plain `+=` —
169///   fast but accumulates O(N·ε) rounding error.
170/// - **Neumaier** ([`NeumaierSum`]): Kahan/Neumaier compensated
171///   summation — recovers O(ε) rounding error at a small overhead cost.
172///
173/// Implement this trait to plug a custom accumulation strategy wherever it is
174/// accepted.
175pub trait SumAccumulator:
176    Accumulator<Input: Zero + Clone, Output = <Self as Accumulator>::Input>
177{
178    /// Multiply the entire accumulated sum by `r`.
179    fn rescale_by(&mut self, r: &Self::Input);
180}
181//---------------------------------------------------------
182
183//---------------------------------------------------------
184/// Naive (plain `+=`) accumulator.
185///
186/// This strategy keeps a single running sum and updates it with standard
187/// floating-point addition. It has minimal overhead and is usually the fastest
188/// option, but it may lose small addends when values have very different
189/// magnitudes.
190///
191/// In norm routines this is used by the default APIs such as
192/// [`crate::algorithms::vector_norms::vector_norm_l1`],
193/// [`crate::algorithms::vector_norms::vector_norm_l2`], and
194/// [`crate::algorithms::vector_norms::vector_norm_l2_sq`].
195///
196/// # Example
197///
198/// ```
199/// use num_valid::algorithms::accumulators::*;
200///
201/// let mut naive = NaiveSum::<f64>::new();
202/// naive.push(1.0);
203/// naive.push(2.0);
204/// naive.push(3.0);
205/// assert_eq!(naive.result(), 6.0);
206/// ```
207pub struct NaiveSum<T>(T);
208
209impl<T> Accumulator for NaiveSum<T>
210where
211    T: Zero + Clone + for<'a> Add<&'a T, Output = T> + for<'a> Mul<&'a T, Output = T> + AddAssign,
212{
213    type Input = T;
214    type Output = T;
215
216    #[inline(always)]
217    fn new() -> Self {
218        NaiveSum(T::zero())
219    }
220
221    #[inline(always)]
222    fn push(&mut self, r: T) {
223        self.0 += r;
224    }
225
226    #[inline(always)]
227    fn result(self) -> T {
228        self.0
229    }
230
231    #[inline(always)]
232    fn combine(&mut self, other: Self) {
233        self.0 += other.0;
234    }
235}
236
237impl<T> SumAccumulator for NaiveSum<T>
238where
239    T: Zero
240        + Clone
241        + for<'a> Add<&'a T, Output = T>
242        + for<'a> Mul<&'a T, Output = T>
243        + AddAssign
244        + for<'a> MulAssign<&'a T>,
245{
246    #[inline(always)]
247    fn rescale_by(&mut self, r: &T) {
248        self.0 *= r;
249    }
250}
251//---------------------------------------------------------
252
253//---------------------------------------------------------
254#[inline(always)]
255fn neumaier_sum_and_compensation_native_real<T>(value: T, sum: &mut T, compensation: &mut T)
256where
257    T: sealed::NativeReal + AddAssign,
258{
259    let sum_before_compensation = *sum;
260    *sum += value; // here we may lose low-order digits of value if sum is much larger than value or vice versa
261
262    // NOTE: the parenthesis are necessary in order for this algorithm to work correctly.
263    *compensation += if sum_before_compensation.abs() >= value.abs() {
264        // If sum is bigger, low-order digits of value are lost.
265        (sum_before_compensation - *sum) + value
266    } else {
267        // Else low-order digits of sum are lost.
268        (value - *sum) + sum_before_compensation
269    };
270}
271
272#[inline(always)]
273pub(crate) fn neumaier_sum_and_compensation_real<RealType>(
274    value: RealType,
275    sum: &mut RealType,
276    compensation: &mut RealType,
277) where
278    RealType: RealScalar,
279{
280    let sum_before_compensation = sum.clone();
281    *sum += &value; // here we may lose low-order digits of value if sum is much larger than value or vice versa
282
283    // NOTE: the parenthesis are necessary in order for this algorithm to work correctly.
284    *compensation += if sum_before_compensation.clone().abs() >= value.clone().abs() {
285        // If sum is bigger, low-order digits of value are lost.
286        (sum_before_compensation - &*sum) + value
287    } else {
288        // Else low-order digits of sum are lost.
289        (value - &*sum) + sum_before_compensation
290    };
291}
292
293/// A trait for types that can participate in Neumaier compensated summation.
294///
295/// This trait provides the core operation for the Neumaier summation algorithm,
296/// which is a numerically stable method for summing floating-point numbers with
297/// reduced rounding errors compared to naive summation.
298///
299/// # Algorithm
300///
301/// The Neumaier algorithm is an improvement over Kahan summation that handles
302/// the case when the new value is larger than the running sum. It maintains
303/// a compensation term to capture the low-order bits that are lost during addition.
304pub trait NeumaierAddable: Sized {
305    /// Performs one step of the Neumaier compensated summation algorithm.
306    ///
307    /// This method adds `value` to the running `sum` while tracking lost precision
308    /// in the `compensation` term. The final result should be computed as
309    /// `sum + compensation` after all values have been added.
310    ///
311    /// # Arguments
312    ///
313    /// * `value` - The value to add to the sum.
314    /// * `sum` - A mutable reference to the running sum.
315    /// * `compensation` - A mutable reference to the compensation term that tracks lost precision.
316    fn neumaier_compensated_sum(value: Self, sum: &mut Self, compensation: &mut Self);
317}
318
319impl<T> NeumaierAddable for T
320where
321    T: sealed::NativeReal + AddAssign,
322{
323    #[inline(always)]
324    fn neumaier_compensated_sum(value: Self, sum: &mut Self, compensation: &mut Self) {
325        neumaier_sum_and_compensation_native_real(value, sum, compensation)
326    }
327}
328
329impl<T> NeumaierAddable for Complex<T>
330where
331    T: sealed::NativeReal + AddAssign,
332{
333    #[inline(always)]
334    fn neumaier_compensated_sum(value: Self, sum: &mut Self, compensation: &mut Self) {
335        neumaier_sum_and_compensation_native_real(value.re, &mut sum.re, &mut compensation.re);
336        neumaier_sum_and_compensation_native_real(value.im, &mut sum.im, &mut compensation.im);
337    }
338}
339
340#[cfg(feature = "rug")]
341mod rug_impls {
342    use super::*;
343
344    #[inline(always)]
345    fn neumaier_sum_and_compensation_rug_float(
346        value: rug::Float,
347        sum: &mut rug::Float,
348        compensation: &mut rug::Float,
349    ) {
350        let sum_before_compensation = sum.clone();
351        *sum += &value;
352
353        // NOTE: the parenthesis are necessary in order for this algorithm to work correctly.
354        *compensation += if sum_before_compensation.clone().abs() >= value.clone().abs() {
355            // If sum is bigger, low-order digits of value are lost.
356            (sum_before_compensation - &*sum) + value
357        } else {
358            // Else low-order digits of sum are lost.
359            (value - &*sum) + sum_before_compensation
360        };
361    }
362
363    impl NeumaierAddable for rug::Float {
364        #[inline(always)]
365        fn neumaier_compensated_sum(value: Self, sum: &mut Self, compensation: &mut Self) {
366            neumaier_sum_and_compensation_rug_float(value, sum, compensation)
367        }
368    }
369
370    impl NeumaierAddable for rug::Complex {
371        #[inline(always)]
372        fn neumaier_compensated_sum(value: Self, sum: &mut Self, compensation: &mut Self) {
373            let (value_real, value_imag) = value.into_real_imag();
374
375            neumaier_sum_and_compensation_rug_float(
376                value_real,
377                sum.mut_real(),
378                compensation.mut_real(),
379            );
380
381            neumaier_sum_and_compensation_rug_float(
382                value_imag,
383                sum.mut_imag(),
384                compensation.mut_imag(),
385            );
386        }
387    }
388}
389
390//-----------------------------------------------------------------------------------
391
392//------------------------------------------------------------------------------------
393/// Neumaier compensated accumulator for sums of floating-point (real or complex) numbers.
394///
395/// Standard sequential summation loses precision when values of very different
396/// magnitudes are added: a small addend can be completely swallowed by a much
397/// larger running total.  `NeumaierSum` corrects for this by maintaining a
398/// *compensation* term that captures the low-order bits discarded at each step,
399/// then folds them back into the result at the end.
400///
401/// `NeumaierSum<T>` implements [`SumAccumulator`],
402/// so it can be used directly as the accumulation strategy in the
403/// [`norms`](crate::algorithms::vector_norms) vector-norm functions (e.g.,
404/// [`vector_norm_l2_neumaier`](crate::algorithms::vector_norms::vector_norm_l2_neumaier),
405/// [`vector_norm_l1_neumaier`](crate::algorithms::vector_norms::vector_norm_l1_neumaier)).
406///
407/// # Example
408///
409/// ```
410/// use num_valid::algorithms::accumulators::*;
411///
412/// let mut neumaier = NeumaierSum::<f64>::new();
413/// neumaier.push(1.0);
414/// neumaier.push(1e100);
415/// neumaier.push(1.0);
416/// neumaier.push(-1e100);
417///
418/// let sum = neumaier.result();
419/// println!("Sum: {}", sum);
420/// assert_eq!(sum, 2.0);
421/// ```
422///
423/// # References
424///
425/// * [Neumaier Summation](https://en.wikipedia.org/wiki/Kahan_summation_algorithm#Further_enhancements)
426#[derive(Debug, Clone, Getters)]
427pub struct NeumaierSum<ScalarType> {
428    /// The sum before the compensation term is added.
429    #[getset(get = "pub")]
430    sum_before_compensation: ScalarType,
431
432    /// The compensation term.
433    /// This is the correction term that is added to the `sum_before_compensation` to correct for the loss of precision.
434    #[getset(get = "pub")]
435    compensation: ScalarType,
436}
437
438impl<ScalarType> Accumulator for NeumaierSum<ScalarType>
439where
440    ScalarType: Clone
441        + Zero
442        + for<'a> Add<&'a ScalarType, Output = ScalarType>
443        + for<'a> AddAssign<&'a ScalarType>
444        + for<'a> Mul<&'a ScalarType, Output = ScalarType>
445        + for<'a> MulAssign<&'a ScalarType>
446        + NeumaierAddable,
447{
448    type Input = ScalarType;
449    type Output = ScalarType;
450
451    #[inline(always)]
452    fn new() -> Self {
453        Self {
454            sum_before_compensation: ScalarType::zero(),
455            compensation: ScalarType::zero(),
456        }
457    }
458
459    #[inline(always)]
460    fn push(&mut self, value: Self::Input) {
461        <ScalarType as NeumaierAddable>::neumaier_compensated_sum(
462            value,
463            &mut self.sum_before_compensation,
464            &mut self.compensation,
465        );
466    }
467
468    #[inline(always)]
469    fn result(self) -> Self::Input {
470        self.sum_before_compensation + self.compensation
471    }
472
473    #[inline(always)]
474    fn combine(&mut self, other: Self) {
475        self.push(other.sum_before_compensation);
476        self.push(other.compensation);
477    }
478}
479
480impl<ScalarType> SumAccumulator for NeumaierSum<ScalarType>
481where
482    ScalarType: Clone
483        + Zero
484        + for<'a> Add<&'a ScalarType, Output = ScalarType>
485        + for<'a> AddAssign<&'a ScalarType>
486        + for<'a> Mul<&'a ScalarType, Output = ScalarType>
487        + for<'a> MulAssign<&'a ScalarType>
488        + NeumaierAddable,
489{
490    #[inline(always)]
491    fn rescale_by(&mut self, r: &Self::Input) {
492        // Compute the current true sum before rescaling.
493        self.sum_before_compensation += &self.compensation;
494
495        // Rescale the sum and reset the compensation term to zero.
496        self.sum_before_compensation *= r;
497        self.compensation = ScalarType::zero();
498    }
499}
500//------------------------------------------------------------------------------------
501
502//------------------------------------------------------------------------------------
503/// Accumulator that tracks the running maximum of real values.
504///
505/// Initialises to [`Constants::min_finite`](crate::Constants::min_finite) so that any finite input
506/// immediately becomes the maximum. For non-empty inputs this is always the
507/// true maximum; for empty inputs [`result`](Accumulator::result) returns
508/// `min_finite`.
509///
510/// # Note on empty input
511///
512/// For norm computations prefer [`MaxAbsValueAccumulator`], which initialises
513/// to zero and is therefore correct for empty vectors (norm = 0).
514pub struct MaxAccumulator<RealType: RealScalar> {
515    max_value: RealType,
516}
517
518impl<RealType: RealScalar> Accumulator for MaxAccumulator<RealType> {
519    type Input = RealType;
520    type Output = RealType;
521
522    #[inline(always)]
523    fn new() -> Self {
524        Self {
525            max_value: RealType::min_finite(),
526        }
527    }
528
529    #[inline(always)]
530    fn push(&mut self, value: Self::Input) {
531        if value > self.max_value {
532            self.max_value = value;
533        }
534    }
535
536    #[inline(always)]
537    fn result(self) -> Self::Output {
538        self.max_value
539    }
540
541    #[inline(always)]
542    fn combine(&mut self, other: Self) {
543        if other.max_value > self.max_value {
544            self.max_value = other.max_value;
545        }
546    }
547}
548
549/// Accumulator that tracks the running minimum of real values.
550///
551/// Initialises to [`Constants::max_finite`](crate::Constants::max_finite) so that any finite input
552/// immediately becomes the minimum. For empty inputs
553/// [`result`](Accumulator::result) returns `max_finite`.
554pub struct MinAccumulator<RealType: RealScalar> {
555    min_value: RealType,
556}
557
558impl<RealType: RealScalar> Accumulator for MinAccumulator<RealType> {
559    type Input = RealType;
560    type Output = RealType;
561
562    #[inline(always)]
563    fn new() -> Self {
564        Self {
565            min_value: RealType::max_finite(),
566        }
567    }
568
569    #[inline(always)]
570    fn push(&mut self, value: Self::Input) {
571        if value < self.min_value {
572            self.min_value = value;
573        }
574    }
575
576    #[inline(always)]
577    fn result(self) -> Self::Output {
578        self.min_value
579    }
580
581    #[inline(always)]
582    fn combine(&mut self, other: Self) {
583        if other.min_value < self.min_value {
584            self.min_value = other.min_value;
585        }
586    }
587}
588//------------------------------------------------------------------------------------
589
590//------------------------------------------------------------------------------------
591/// Accumulator that computes the maximum absolute value over a sequence of
592/// scalars: `maxᵢ |xᵢ|`.
593///
594/// Unlike [`MaxAccumulator`], this type:
595/// - accepts any [`FpScalar`] (real **or** complex) as input, applying `abs()`
596///   internally, so the modulus is used for complex scalars.
597/// - initialises to **zero** rather than `min_finite`, making it correct for
598///   empty inputs (result = 0, matching the L∞ norm of an empty vector).
599/// - returns a validated [`NonNegativeRealScalar`] from
600///   [`result`](Accumulator::result), encoding the non-negativity invariant
601///   at the type level.
602///
603/// This is the accumulator used internally by [`LinfNorm`](crate::algorithms::vector_norms::LinfNorm).
604pub struct MaxAbsValueAccumulator<ScalarType: FpScalar> {
605    max_abs: NonNegativeRealScalar<ScalarType::RealType>, // internal: RealType for comparison of absolute values
606}
607
608impl<ScalarType: FpScalar> Accumulator for MaxAbsValueAccumulator<ScalarType> {
609    type Input = ScalarType;
610    type Output = NonNegativeRealScalar<ScalarType::RealType>;
611
612    fn new() -> Self {
613        Self {
614            max_abs: NonNegativeRealScalar::zero(),
615        }
616    }
617
618    fn push(&mut self, value: ScalarType) {
619        let abs_v = value.abs();
620        if abs_v > self.max_abs {
621            self.max_abs = abs_v;
622        }
623    }
624
625    fn combine(&mut self, other: Self) {
626        if other.max_abs > self.max_abs {
627            self.max_abs = other.max_abs;
628        }
629    }
630
631    fn result(self) -> NonNegativeRealScalar<ScalarType::RealType> {
632        self.max_abs
633    }
634}
635//------------------------------------------------------------------------------------
636
637//---------------------------------------------------------
638/// Accumulator for computing scaled sum-of-squares: `scale² × Σ(xᵢ/scale)²`.
639///
640/// This accumulator is used internally for computing vector norms (especially L2),
641/// but is general enough for any algorithm that needs to sum squared non-negative values
642/// while maintaining numerical stability through dynamic scaling.
643///
644/// The key invariant is that `scale` tracks the running maximum absolute value,
645/// preventing overflow and underflow. When a larger value is encountered, previous
646/// accumulated terms are rescaled by `(old_scale / new_scale)²`.
647///
648/// # Type parameter `SumSqAcc`
649///
650/// The accumulation strategy for the scaled sum-of-squares term:
651/// - [`NaiveSum`]: plain `+=`, fastest
652/// - [`NeumaierSum`]: compensated summation for better accuracy
653///
654/// # Input contract
655///
656/// The [`Input`](Accumulator::Input) type is [`NonNegativeRealScalar<ScalarType::RealType>`],
657/// which explicitly encodes that all pushed values must be non-negative.
658/// Callers must validate and wrap values with [`NonNegativeRealScalar`] before pushing.
659///
660/// # Example
661///
662/// ```ignore
663/// use num_valid::algorithms::accumulators::*;
664/// use num_valid::scalars::NonNegativeRealScalar;
665///
666/// let mut acc = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
667/// acc.push(NonNegativeRealScalar::new(3.0));  // validated non-negative
668/// acc.push(NonNegativeRealScalar::new(4.0));
669/// let result = acc.result();  // 3² + 4² = 25
670/// ```
671pub struct ScaledSumSqAccumulator<SumSqAcc>
672where
673    SumSqAcc: SumAccumulator<Input: RealScalar>,
674{
675    /// - `scale = max |vᵢ|`  (0 if all components are zero)
676    scale: NonNegativeRealScalar<SumSqAcc::Input>,
677    /// - `sumsq = Σ (|vᵢ|/scale)²`  accumulated via strategy `SumSqAcc`
678    sumsq_accumulator: SumSqAcc,
679    zero: NonNegativeRealScalar<SumSqAcc::Input>,
680}
681
682impl<SumSqAcc> ScaledSumSqAccumulator<SumSqAcc>
683where
684    SumSqAcc: SumAccumulator<Input: RealScalar>,
685{
686    fn add_value_and_update_scale(&mut self, value: NonNegativeRealScalar<SumSqAcc::Input>) {
687        if value > self.zero {
688            let r_sq = if self.scale < value {
689                if self.scale > self.zero {
690                    // Update scale and rescale the sum-of-squares accumulator
691                    //let r = self.scale.as_raw_scalar_ref().clone() / value.as_raw_scalar_ref();
692                    //self.scale = value;
693                    //
694                    // old_scale = self.scale;
695                    // r = value / old_scale
696                    // self.scale = value;
697                    let r = std::mem::replace(&mut self.scale, value).into_inner()
698                        / self.scale.as_ref();
699
700                    self.sumsq_accumulator.rescale_by(&r.pow(2));
701                } else {
702                    self.scale = value;
703                }
704                SumSqAcc::Input::one()
705            } else {
706                (value.into_inner() / self.scale.as_ref()).pow(2)
707            };
708            self.sumsq_accumulator.push(r_sq);
709        }
710    }
711
712    pub fn result_sqrt(self) -> NonNegativeRealScalar<SumSqAcc::Input> {
713        if self.scale == self.zero {
714            self.zero
715        } else {
716            NonNegativeRealScalar::new(self.sumsq_accumulator.result()).sqrt() * self.scale
717        }
718    }
719
720    /// Build a new accumulator by sequentially pushing non-negative values.
721    ///
722    /// This is the basic constructor for building from validated non-negative inputs.
723    /// For building from arbitrary scalars (which need abs()), use
724    /// [`new_sequential_from_scalars`](Self::new_sequential_from_scalars).
725    pub fn new_sequential<I>(values: I) -> Self
726    where
727        I: IntoIterator<Item = NonNegativeRealScalar<SumSqAcc::Input>>,
728    {
729        let mut acc = Self::new();
730        values.into_iter().for_each(|v| acc.push(v));
731        acc
732    }
733
734    /// Build a new accumulator by sequentially processing scalars, automatically taking abs().
735    ///
736    /// This is a convenience method for L2 norm computation, where scalar values
737    /// (real or complex) are automatically converted to their absolute value
738    /// (modulus for complex scalars) before accumulation.
739    ///
740    /// # Example
741    ///
742    /// ```ignore
743    /// use num_valid::algorithms::accumulators::*;
744    ///
745    /// let values = [3.0_f64, -4.0, 5.0];
746    /// let mut acc = ScaledSumSqAccumulator::<NaiveSum<f64>>::new_sequential_from_scalars(values);
747    /// // Accumulates |3| = 3, |-4| = 4, |5| = 5
748    /// // Result: √(9 + 16 + 25) = √50
749    /// ```
750    pub fn new_sequential_from_scalars<I>(values: I) -> Self
751    where
752        I: IntoIterator<Item: FpScalar<RealType = SumSqAcc::Input>>,
753    {
754        let mut acc = Self::new();
755        values.into_iter().for_each(|v| {
756            acc.push(v.abs());
757        });
758        acc
759    }
760
761    /// Build a new accumulator in parallel, automatically taking abs() of each scalar.
762    ///
763    /// Distributes work across threads using rayon. Each thread accumulates its chunk
764    /// independently, then results are merged via [`Accumulator::combine`].
765    pub fn new_parallel_from_scalars<I>(values: I) -> Self
766    where
767        Self: Send,
768        I: IntoParallelIterator<Item: FpScalar<RealType = SumSqAcc::Input>>,
769    {
770        values
771            .into_par_iter()
772            .fold(Self::new, |mut acc, item| {
773                acc.push(item.abs());
774                acc
775            })
776            .reduce(Self::new, |mut a, b| {
777                a.combine(b);
778                a
779            })
780    }
781}
782
783impl<SumSqAcc> Accumulator for ScaledSumSqAccumulator<SumSqAcc>
784where
785    SumSqAcc: SumAccumulator<Input: RealScalar>,
786{
787    /// Input must be already validated as non-negative
788    type Input = NonNegativeRealScalar<SumSqAcc::Input>;
789    type Output = NonNegativeRealScalar<SumSqAcc::Input>;
790
791    fn new() -> Self {
792        Self {
793            scale: NonNegativeRealScalar::<SumSqAcc::Input>::zero(),
794            sumsq_accumulator: SumSqAcc::new(),
795            zero: NonNegativeRealScalar::<SumSqAcc::Input>::zero(),
796        }
797    }
798
799    /// Adds a non-negative value to the accumulator.
800    /// The input must already be validated as non-negative via [`NonNegativeRealScalar`].
801    fn push(&mut self, value: Self::Input) {
802        self.add_value_and_update_scale(value);
803    }
804
805    /// Merges another accumulator, rescaling as needed when scales differ.
806    /// Handles all permutations of scale relationships (>, <, ==) and empty accumulators.
807    fn combine(&mut self, mut other: Self) {
808        if other.scale > self.scale {
809            if self.scale > self.zero {
810                // old_scale = self.scale
811                // self.scale = other.scale
812                // r = old_scale / self.scale
813                let r = std::mem::replace(&mut self.scale, other.scale).into_inner()
814                    / self.scale.as_ref();
815                //let r = self.scale.clone().into_inner() / other.scale.as_ref();
816                //self.scale = other.scale;
817                self.sumsq_accumulator.rescale_by(&r.pow(2));
818            } else {
819                self.scale = other.scale;
820            }
821            self.sumsq_accumulator.combine(other.sumsq_accumulator);
822        } else if other.scale > self.zero {
823            if other.scale < self.scale {
824                let r = other.scale.into_inner() / self.scale.as_ref();
825                other.sumsq_accumulator.rescale_by(&r.pow(2));
826            }
827            self.sumsq_accumulator.combine(other.sumsq_accumulator);
828        }
829        // when both scale==0: zero vector, nothing to do
830    }
831
832    /// Returns the squared result: `scale² × sumsq`.
833    fn result(self) -> Self::Output {
834        if self.scale == self.zero {
835            self.zero
836        } else {
837            NonNegativeRealScalar::new(
838                self.sumsq_accumulator.result() * self.scale.into_inner().pow(2),
839            )
840        }
841    }
842}
843//---------------------------------------------------------
844
845//------------------------------------------------------------------------------------
846#[cfg(test)]
847mod tests {
848    use super::*;
849
850    mod neumaier_sum {
851        use super::*;
852
853        mod native {
854            use super::*;
855
856            mod real {
857                use super::*;
858
859                #[test]
860                fn new() {
861                    let neumaier = NeumaierSum::<f64>::new();
862                    assert_eq!(neumaier.sum_before_compensation, 0.0);
863                    assert_eq!(neumaier.compensation, 0.0);
864                }
865
866                #[test]
867                fn add() {
868                    let mut neumaier = NeumaierSum::<f64>::new();
869                    neumaier.push(1.0);
870                    neumaier.push(1e-16);
871                    neumaier.push(-1.0);
872                    assert_eq!(neumaier.sum_before_compensation, 0.0);
873                    assert_eq!(neumaier.compensation, 1e-16);
874                }
875
876                #[test]
877                fn sum() {
878                    let mut neumaier = NeumaierSum::<f64>::new();
879                    neumaier.push(1.0);
880                    neumaier.push(1e-16);
881                    neumaier.push(-1.0);
882                    assert_eq!(neumaier.sum_before_compensation, 0.0);
883                    assert_eq!(neumaier.compensation, 1e-16);
884                    let sum = neumaier.result();
885                    assert_eq!(sum, 1e-16);
886                    println!("compensated sum = {}", sum);
887                }
888
889                #[test]
890                fn sum_big_values() {
891                    let values = vec![1.0, 1e100, 1.0, -1e100];
892                    let sum = values.iter().sum::<f64>();
893                    assert_eq!(sum, 0.0);
894
895                    let neumaier = NeumaierSum::<f64>::new_sequential(values);
896                    let sum = neumaier.result();
897                    assert_eq!(sum, 2.0);
898                    println!("compensated sum = {}", sum);
899                }
900
901                #[test]
902                fn sum_small_values() {
903                    let values = [1.0, 1e-100, -1.0];
904                    let sum = values.iter().sum::<f64>();
905                    assert_eq!(sum, 0.0);
906
907                    let neumaier = NeumaierSum::<f64>::new_sequential(values);
908                    let sum = neumaier.result();
909                    assert_eq!(sum, 1e-100);
910                    println!("compensated sum = {}", sum);
911                }
912
913                #[test]
914                fn combine_partial_sums() {
915                    let mut left = NaiveSum::<f64>::new();
916                    left.push(1.0);
917                    left.push(2.0);
918
919                    let mut right = NaiveSum::<f64>::new();
920                    right.push(3.0);
921                    right.push(4.0);
922
923                    left.combine(right);
924                    assert_eq!(left.result(), 10.0);
925                }
926
927                #[test]
928                fn combine_partial_neumaier_sums() {
929                    let mut left = NeumaierSum::<f64>::new();
930                    left.push(1.0);
931                    left.push(1e-16);
932
933                    let mut right = NeumaierSum::<f64>::new();
934                    right.push(2.0);
935                    right.push(1e-16);
936
937                    left.combine(right);
938                    let sum = left.result();
939                    assert!((sum - (3.0 + 2e-16)).abs() < 1e-15);
940                }
941
942                #[test]
943                fn combine_matches_new_sequential_for_chunks() {
944                    let values = [1.0_f64, 1e100, 1.0, -1e100, 1e-16, 2.0];
945
946                    let mut chunk_1 = NaiveSum::<f64>::new();
947                    chunk_1.push(values[0]);
948                    chunk_1.push(values[1]);
949
950                    let mut chunk_2 = NaiveSum::<f64>::new();
951                    chunk_2.push(values[2]);
952                    chunk_2.push(values[3]);
953
954                    let mut chunk_3 = NaiveSum::<f64>::new();
955                    chunk_3.push(values[4]);
956                    chunk_3.push(values[5]);
957
958                    chunk_1.combine(chunk_2);
959                    chunk_1.combine(chunk_3);
960
961                    let sequential = NaiveSum::<f64>::new_sequential(values);
962                    assert_eq!(chunk_1.result(), sequential.result());
963                }
964
965                #[test]
966                fn combine_is_order_dependent_for_naive_sum() {
967                    let mut left_grouped = NaiveSum::<f64>::new();
968                    left_grouped.push(1e16);
969
970                    let mut middle = NaiveSum::<f64>::new();
971                    middle.push(1.0);
972
973                    let mut right_last = NaiveSum::<f64>::new();
974                    right_last.push(-1e16);
975
976                    left_grouped.combine(middle);
977                    left_grouped.combine(right_last);
978                    let left_result = left_grouped.result();
979
980                    let mut right_grouped = NaiveSum::<f64>::new();
981                    right_grouped.push(1e16);
982
983                    let mut right_last = NaiveSum::<f64>::new();
984                    right_last.push(-1e16);
985
986                    let mut middle = NaiveSum::<f64>::new();
987                    middle.push(1.0);
988
989                    right_grouped.combine(right_last);
990                    right_grouped.combine(middle);
991                    let right_result = right_grouped.result();
992
993                    assert_eq!(left_result, 0.0);
994                    assert_eq!(right_result, 1.0);
995                    assert_ne!(left_result, right_result);
996                }
997
998                #[test]
999                fn neumaier_combine_matches_new_sequential_for_chunks() {
1000                    let values = [1.0_f64, 1e100, 1.0, -1e100, 1e-16, 2.0];
1001
1002                    let mut chunk_1 = NeumaierSum::<f64>::new();
1003                    chunk_1.push(values[0]);
1004                    chunk_1.push(values[1]);
1005
1006                    let mut chunk_2 = NeumaierSum::<f64>::new();
1007                    chunk_2.push(values[2]);
1008                    chunk_2.push(values[3]);
1009
1010                    let mut chunk_3 = NeumaierSum::<f64>::new();
1011                    chunk_3.push(values[4]);
1012                    chunk_3.push(values[5]);
1013
1014                    chunk_1.combine(chunk_2);
1015                    chunk_1.combine(chunk_3);
1016
1017                    let sequential = NeumaierSum::<f64>::new_sequential(values);
1018                    assert_eq!(chunk_1.result(), sequential.result());
1019                }
1020
1021                #[test]
1022                fn neumaier_combine_is_stable_across_orders() {
1023                    let mut left_order = NeumaierSum::<f64>::new();
1024                    left_order.push(1e16);
1025
1026                    let mut middle = NeumaierSum::<f64>::new();
1027                    middle.push(1.0);
1028
1029                    let mut right_last = NeumaierSum::<f64>::new();
1030                    right_last.push(-1e16);
1031
1032                    left_order.combine(middle);
1033                    left_order.combine(right_last);
1034                    let left_result = left_order.result();
1035
1036                    let mut right_order = NeumaierSum::<f64>::new();
1037                    right_order.push(1e16);
1038
1039                    let mut right_last = NeumaierSum::<f64>::new();
1040                    right_last.push(-1e16);
1041
1042                    let mut middle = NeumaierSum::<f64>::new();
1043                    middle.push(1.0);
1044
1045                    right_order.combine(right_last);
1046                    right_order.combine(middle);
1047                    let right_result = right_order.result();
1048
1049                    assert_eq!(left_result, 1.0);
1050                    assert_eq!(right_result, 1.0);
1051                    assert_eq!(left_result, right_result);
1052                }
1053            }
1054
1055            mod complex {
1056                use super::*;
1057                use num::Complex;
1058
1059                #[test]
1060                fn new() {
1061                    let neumaier = NeumaierSum::<Complex<f64>>::new();
1062
1063                    let zero = Complex::new(0.0, 0.0);
1064                    assert_eq!(&neumaier.sum_before_compensation, &zero);
1065                    assert_eq!(&neumaier.compensation, &zero);
1066                }
1067
1068                #[test]
1069                fn add() {
1070                    let mut neumaier = NeumaierSum::<Complex<f64>>::new();
1071
1072                    let zero = Complex::new(0.0, 0.0);
1073                    let v = Complex::new(1e-16, 2e-16);
1074
1075                    neumaier.push(Complex::new(1.0, 2.0));
1076                    neumaier.push(v);
1077                    neumaier.push(Complex::new(-1.0, -2.0));
1078
1079                    assert_eq!(neumaier.sum_before_compensation, zero);
1080                    assert_eq!(neumaier.compensation, v);
1081                }
1082
1083                #[test]
1084                fn sum() {
1085                    let mut neumaier = NeumaierSum::<Complex<f64>>::new();
1086
1087                    let zero = Complex::new(0.0, 0.0);
1088                    let v = Complex::new(1e-16, 2e-16);
1089
1090                    neumaier.push(Complex::new(1.0, 2.0));
1091                    neumaier.push(v);
1092                    neumaier.push(Complex::new(-1.0, -2.0));
1093                    assert_eq!(neumaier.sum_before_compensation, zero);
1094                    assert_eq!(neumaier.compensation, v);
1095                    let sum = neumaier.result();
1096                    assert_eq!(sum, v);
1097                    println!("compensated sum = {}", sum);
1098                }
1099
1100                #[test]
1101                fn sum_big_values() {
1102                    let values = vec![
1103                        Complex::new(1.0, 2.0),
1104                        Complex::new(1e100, 2e100),
1105                        Complex::new(1.0, 2.0),
1106                        Complex::new(-1e100, -2e100),
1107                    ];
1108                    let sum = values.iter().sum::<Complex<f64>>();
1109                    assert_eq!(sum, Complex::new(0.0, 0.0));
1110
1111                    let neumaier = NeumaierSum::<Complex<f64>>::new_sequential(values);
1112                    let sum = neumaier.result();
1113                    assert_eq!(sum, Complex::new(2.0, 4.0));
1114                    println!("compensated sum = {}", sum);
1115                }
1116
1117                #[test]
1118                fn sum_small_values() {
1119                    let v = Complex::new(1e-100, 2e-100);
1120
1121                    let values = [Complex::new(1.0, 2.0), v, Complex::new(-1.0, -2.0)];
1122                    let sum = values.iter().sum::<Complex<f64>>();
1123                    assert_eq!(sum, Complex::new(0.0, 0.0));
1124
1125                    let neumaier = NeumaierSum::<Complex<f64>>::new_sequential(values);
1126                    let sum = neumaier.result();
1127                    assert_eq!(sum, v);
1128                    println!("compensated sum = {}", sum);
1129                }
1130
1131                #[test]
1132                fn combine_partial_sums() {
1133                    let mut left = NaiveSum::<Complex<f64>>::new();
1134                    left.push(Complex::new(1.0, 2.0));
1135
1136                    let mut right = NaiveSum::<Complex<f64>>::new();
1137                    right.push(Complex::new(3.0, 4.0));
1138
1139                    left.combine(right);
1140                    assert_eq!(left.result(), Complex::new(4.0, 6.0));
1141                }
1142
1143                #[test]
1144                fn combine_partial_neumaier_sums() {
1145                    let mut left = NeumaierSum::<Complex<f64>>::new();
1146                    left.push(Complex::new(1.0, 2.0));
1147                    left.push(Complex::new(1e-16, 2e-16));
1148
1149                    let mut right = NeumaierSum::<Complex<f64>>::new();
1150                    right.push(Complex::new(3.0, 4.0));
1151                    right.push(Complex::new(1e-16, 2e-16));
1152
1153                    left.combine(right);
1154                    assert_eq!(
1155                        left.result(),
1156                        Complex::new(4.0, 6.0) + Complex::new(2e-16, 4e-16)
1157                    );
1158                }
1159
1160                #[test]
1161                fn combine_matches_new_sequential_for_chunks() {
1162                    let values = [
1163                        Complex::new(1.0, 2.0),
1164                        Complex::new(1e100, 2e100),
1165                        Complex::new(1.0, 2.0),
1166                        Complex::new(-1e100, -2e100),
1167                        Complex::new(1e-16, 2e-16),
1168                        Complex::new(2.0, 3.0),
1169                    ];
1170
1171                    let mut chunk_1 = NaiveSum::<Complex<f64>>::new();
1172                    chunk_1.push(values[0]);
1173                    chunk_1.push(values[1]);
1174
1175                    let mut chunk_2 = NaiveSum::<Complex<f64>>::new();
1176                    chunk_2.push(values[2]);
1177                    chunk_2.push(values[3]);
1178
1179                    let mut chunk_3 = NaiveSum::<Complex<f64>>::new();
1180                    chunk_3.push(values[4]);
1181                    chunk_3.push(values[5]);
1182
1183                    chunk_1.combine(chunk_2);
1184                    chunk_1.combine(chunk_3);
1185
1186                    let sequential = NaiveSum::<Complex<f64>>::new_sequential(values);
1187                    assert_eq!(chunk_1.result(), sequential.result());
1188                }
1189
1190                #[test]
1191                fn neumaier_combine_matches_new_sequential_for_chunks() {
1192                    let values = [
1193                        Complex::new(1.0, 2.0),
1194                        Complex::new(1e100, 2e100),
1195                        Complex::new(1.0, 2.0),
1196                        Complex::new(-1e100, -2e100),
1197                        Complex::new(1e-16, 2e-16),
1198                        Complex::new(2.0, 3.0),
1199                    ];
1200
1201                    let mut chunk_1 = NeumaierSum::<Complex<f64>>::new();
1202                    chunk_1.push(values[0]);
1203                    chunk_1.push(values[1]);
1204
1205                    let mut chunk_2 = NeumaierSum::<Complex<f64>>::new();
1206                    chunk_2.push(values[2]);
1207                    chunk_2.push(values[3]);
1208
1209                    let mut chunk_3 = NeumaierSum::<Complex<f64>>::new();
1210                    chunk_3.push(values[4]);
1211                    chunk_3.push(values[5]);
1212
1213                    chunk_1.combine(chunk_2);
1214                    chunk_1.combine(chunk_3);
1215
1216                    let sequential = NeumaierSum::<Complex<f64>>::new_sequential(values);
1217                    assert_eq!(chunk_1.result(), sequential.result());
1218                }
1219            }
1220        }
1221
1222        #[cfg(feature = "rug")]
1223        mod rug100 {
1224            use super::*;
1225            use crate::{ComplexRugStrictFinite, RealRugStrictFinite};
1226            use try_create::TryNew;
1227
1228            const PRECISION: u32 = 100;
1229
1230            mod real {
1231                use super::*;
1232                use crate::RealScalar;
1233
1234                #[test]
1235                fn new() {
1236                    let neumaier = NeumaierSum::<RealRugStrictFinite<PRECISION>>::new();
1237                    assert!(neumaier.sum_before_compensation.is_zero());
1238                    assert!(neumaier.compensation.is_zero());
1239                }
1240
1241                #[test]
1242                fn add() {
1243                    let mut neumaier = NeumaierSum::<RealRugStrictFinite<PRECISION>>::new();
1244
1245                    let v = RealRugStrictFinite::<PRECISION>::try_new(rug::Float::with_val(
1246                        PRECISION,
1247                        rug::Float::parse("1e-100").unwrap(),
1248                    ))
1249                    .expect("valid test value");
1250
1251                    neumaier.push(RealRugStrictFinite::<PRECISION>::try_from_f64(1.0).unwrap());
1252                    neumaier.push(v.clone());
1253                    neumaier.push(RealRugStrictFinite::<PRECISION>::try_from_f64(-1.0).unwrap());
1254
1255                    assert_eq!(
1256                        neumaier.sum_before_compensation,
1257                        RealRugStrictFinite::<PRECISION>::try_from_f64(0.0).unwrap()
1258                    );
1259                    assert_eq!(&neumaier.compensation, &v);
1260                }
1261
1262                #[test]
1263                fn sum() {
1264                    let mut neumaier = NeumaierSum::<RealRugStrictFinite<PRECISION>>::new();
1265
1266                    let v = RealRugStrictFinite::<PRECISION>::try_new(rug::Float::with_val(
1267                        PRECISION,
1268                        rug::Float::parse("1e-100").unwrap(),
1269                    ))
1270                    .expect("valid test value");
1271
1272                    neumaier.push(RealRugStrictFinite::<PRECISION>::try_from_f64(1.0).unwrap());
1273                    neumaier.push(v.clone());
1274                    neumaier.push(RealRugStrictFinite::<PRECISION>::try_from_f64(-1.0).unwrap());
1275
1276                    assert!(neumaier.sum_before_compensation.is_zero());
1277                    assert_eq!(&neumaier.compensation, &v);
1278                    let sum = neumaier.result();
1279                    assert_eq!(sum, v);
1280                    println!("compensated sum = {}", sum);
1281                }
1282
1283                #[test]
1284                fn sum_big_values() {
1285                    let values = ["1.0", "1e100", "1.0", "-1e100"]
1286                        .iter()
1287                        .map(|v| {
1288                            RealRugStrictFinite::<PRECISION>::try_new(rug::Float::with_val(
1289                                PRECISION,
1290                                rug::Float::parse(v).unwrap(),
1291                            ))
1292                            .expect("valid test value")
1293                        })
1294                        .collect::<Vec<_>>();
1295
1296                    let sum = values
1297                        .iter()
1298                        .fold(RealRugStrictFinite::<PRECISION>::zero(), |acc, x| acc + x);
1299                    assert!(sum.is_zero());
1300
1301                    let neumaier =
1302                        NeumaierSum::<RealRugStrictFinite<PRECISION>>::new_sequential(values);
1303                    let sum = neumaier.result();
1304                    assert_eq!(
1305                        sum,
1306                        RealRugStrictFinite::<PRECISION>::try_from_f64(2.0).unwrap()
1307                    );
1308                    println!("compensated sum = {}", sum);
1309                }
1310
1311                #[test]
1312                fn sum_small_values() {
1313                    let values = ["1.0", "1e-100", "-1.0"]
1314                        .iter()
1315                        .map(|v| {
1316                            RealRugStrictFinite::<PRECISION>::try_new(rug::Float::with_val(
1317                                PRECISION,
1318                                rug::Float::parse(v).unwrap(),
1319                            ))
1320                            .expect("valid test value")
1321                        })
1322                        .collect::<Vec<_>>();
1323
1324                    let sum = values
1325                        .iter()
1326                        .fold(RealRugStrictFinite::<PRECISION>::zero(), |acc, x| acc + x);
1327                    assert_eq!(sum, RealRugStrictFinite::<PRECISION>::zero());
1328
1329                    let neumaier =
1330                        NeumaierSum::<RealRugStrictFinite<PRECISION>>::new_sequential(values);
1331                    let sum = neumaier.result();
1332                    assert_eq!(
1333                        sum,
1334                        RealRugStrictFinite::<PRECISION>::try_new(rug::Float::with_val(
1335                            PRECISION,
1336                            rug::Float::parse("1e-100").unwrap(),
1337                        ))
1338                        .expect("valid test value")
1339                    );
1340                    println!("compensated sum = {}", sum);
1341                }
1342
1343                #[test]
1344                fn combine_matches_new_sequential_for_chunks() {
1345                    let values = ["1.0", "1e100", "1.0", "-1e100", "1e-100", "2.0"]
1346                        .iter()
1347                        .map(|v| {
1348                            RealRugStrictFinite::<PRECISION>::try_new(rug::Float::with_val(
1349                                PRECISION,
1350                                rug::Float::parse(v).unwrap(),
1351                            ))
1352                            .expect("valid test value")
1353                        })
1354                        .collect::<Vec<_>>();
1355
1356                    let mut chunk_1 = NeumaierSum::<RealRugStrictFinite<PRECISION>>::new();
1357                    chunk_1.push(values[0].clone());
1358                    chunk_1.push(values[1].clone());
1359
1360                    let mut chunk_2 = NeumaierSum::<RealRugStrictFinite<PRECISION>>::new();
1361                    chunk_2.push(values[2].clone());
1362                    chunk_2.push(values[3].clone());
1363
1364                    let mut chunk_3 = NeumaierSum::<RealRugStrictFinite<PRECISION>>::new();
1365                    chunk_3.push(values[4].clone());
1366                    chunk_3.push(values[5].clone());
1367
1368                    chunk_1.combine(chunk_2);
1369                    chunk_1.combine(chunk_3);
1370
1371                    let sequential =
1372                        NeumaierSum::<RealRugStrictFinite<PRECISION>>::new_sequential(values);
1373                    assert_eq!(chunk_1.result(), sequential.result());
1374                }
1375
1376                #[test]
1377                fn neumaier_combine_is_stable_across_orders() {
1378                    let mut left_order = NeumaierSum::<RealRugStrictFinite<PRECISION>>::new();
1379                    left_order.push(RealRugStrictFinite::<PRECISION>::try_from_f64(1e16).unwrap());
1380
1381                    let mut middle = NeumaierSum::<RealRugStrictFinite<PRECISION>>::new();
1382                    middle.push(RealRugStrictFinite::<PRECISION>::try_from_f64(1.0).unwrap());
1383
1384                    let mut right_last = NeumaierSum::<RealRugStrictFinite<PRECISION>>::new();
1385                    right_last.push(RealRugStrictFinite::<PRECISION>::try_from_f64(-1e16).unwrap());
1386
1387                    left_order.combine(middle);
1388                    left_order.combine(right_last);
1389                    let left_result = left_order.result();
1390
1391                    let mut right_order = NeumaierSum::<RealRugStrictFinite<PRECISION>>::new();
1392                    right_order.push(RealRugStrictFinite::<PRECISION>::try_from_f64(1e16).unwrap());
1393
1394                    let mut right_last = NeumaierSum::<RealRugStrictFinite<PRECISION>>::new();
1395                    right_last.push(RealRugStrictFinite::<PRECISION>::try_from_f64(-1e16).unwrap());
1396
1397                    let mut middle = NeumaierSum::<RealRugStrictFinite<PRECISION>>::new();
1398                    middle.push(RealRugStrictFinite::<PRECISION>::try_from_f64(1.0).unwrap());
1399
1400                    right_order.combine(right_last);
1401                    right_order.combine(middle);
1402                    let right_result = right_order.result();
1403
1404                    assert_eq!(
1405                        left_result,
1406                        RealRugStrictFinite::<PRECISION>::try_from_f64(1.0).unwrap()
1407                    );
1408                    assert_eq!(
1409                        right_result,
1410                        RealRugStrictFinite::<PRECISION>::try_from_f64(1.0).unwrap()
1411                    );
1412                    assert_eq!(left_result, right_result);
1413                }
1414            }
1415
1416            mod complex {
1417                use super::*;
1418
1419                #[test]
1420                fn new() {
1421                    let neumaier = NeumaierSum::<ComplexRugStrictFinite<PRECISION>>::new();
1422                    assert_eq!(
1423                        neumaier.sum_before_compensation,
1424                        ComplexRugStrictFinite::<PRECISION>::zero()
1425                    );
1426                    assert_eq!(
1427                        neumaier.compensation,
1428                        ComplexRugStrictFinite::<PRECISION>::zero()
1429                    );
1430                }
1431
1432                #[test]
1433                fn add() {
1434                    let mut neumaier = NeumaierSum::<ComplexRugStrictFinite<PRECISION>>::new();
1435
1436                    let v = ComplexRugStrictFinite::<PRECISION>::try_new(rug::Complex::with_val(
1437                        PRECISION,
1438                        rug::Complex::parse("(1e-100,2e-100)").unwrap(),
1439                    ))
1440                    .expect("valid test value");
1441
1442                    neumaier.push(
1443                        ComplexRugStrictFinite::<PRECISION>::try_from(Complex::new(1.0, 2.0))
1444                            .unwrap(),
1445                    );
1446                    neumaier.push(v.clone());
1447                    neumaier.push(
1448                        ComplexRugStrictFinite::<PRECISION>::try_from(Complex::new(-1.0, -2.0))
1449                            .unwrap(),
1450                    );
1451
1452                    assert_eq!(
1453                        neumaier.sum_before_compensation,
1454                        ComplexRugStrictFinite::<PRECISION>::zero()
1455                    );
1456                    assert_eq!(&neumaier.compensation, &v);
1457                }
1458
1459                #[test]
1460                fn sum() {
1461                    let mut neumaier = NeumaierSum::<ComplexRugStrictFinite<PRECISION>>::new();
1462
1463                    let v = ComplexRugStrictFinite::<PRECISION>::try_new(rug::Complex::with_val(
1464                        PRECISION,
1465                        rug::Complex::parse("(1e-100,2e-100)").unwrap(),
1466                    ))
1467                    .expect("valid test value");
1468
1469                    neumaier.push(
1470                        ComplexRugStrictFinite::<PRECISION>::try_from(Complex::new(1.0, 2.0))
1471                            .unwrap(),
1472                    );
1473                    neumaier.push(v.clone());
1474                    neumaier.push(
1475                        ComplexRugStrictFinite::<PRECISION>::try_from(Complex::new(-1.0, -2.0))
1476                            .unwrap(),
1477                    );
1478
1479                    assert_eq!(
1480                        neumaier.sum_before_compensation,
1481                        ComplexRugStrictFinite::<PRECISION>::zero()
1482                    );
1483                    assert_eq!(&neumaier.compensation, &v);
1484                    let sum = neumaier.result();
1485                    assert_eq!(sum, v);
1486                    println!("compensated sum = {}", sum);
1487                }
1488
1489                #[test]
1490                fn sum_big_values() {
1491                    let values = ["(1.0,2.0)", "(1e100,2e100)", "(1.0,2.0)", "(-1e100,-2e100)"]
1492                        .iter()
1493                        .map(|v| {
1494                            ComplexRugStrictFinite::<PRECISION>::try_new(rug::Complex::with_val(
1495                                PRECISION,
1496                                rug::Complex::parse(v).unwrap(),
1497                            ))
1498                            .expect("valid test value")
1499                        })
1500                        .collect::<Vec<_>>();
1501
1502                    let zero = ComplexRugStrictFinite::<PRECISION>::zero();
1503                    let sum = values.iter().fold(zero.clone(), |acc, x| acc + x);
1504                    assert_eq!(sum, zero);
1505
1506                    let neumaier =
1507                        NeumaierSum::<ComplexRugStrictFinite<PRECISION>>::new_sequential(values);
1508                    let sum = neumaier.result();
1509                    assert_eq!(
1510                        sum,
1511                        ComplexRugStrictFinite::<PRECISION>::try_from(Complex::new(2.0, 4.0))
1512                            .unwrap()
1513                    );
1514                    println!("compensated sum = {}", sum);
1515                }
1516
1517                #[test]
1518                fn sum_small_values() {
1519                    let values = ["(1.0,2.0)", "(1e-100,2e-100)", "(-1.0,-2.0)"]
1520                        .iter()
1521                        .map(|v| {
1522                            ComplexRugStrictFinite::<PRECISION>::try_new(rug::Complex::with_val(
1523                                PRECISION,
1524                                rug::Complex::parse(v).unwrap(),
1525                            ))
1526                            .expect("valid test value")
1527                        })
1528                        .collect::<Vec<_>>();
1529
1530                    let zero = ComplexRugStrictFinite::<PRECISION>::zero();
1531                    let sum = values.iter().fold(zero.clone(), |acc, x| acc + x);
1532                    assert_eq!(sum, zero);
1533
1534                    let neumaier =
1535                        NeumaierSum::<ComplexRugStrictFinite<PRECISION>>::new_sequential(values);
1536                    let sum = neumaier.result();
1537                    assert_eq!(
1538                        sum,
1539                        ComplexRugStrictFinite::<PRECISION>::try_new(rug::Complex::with_val(
1540                            PRECISION,
1541                            rug::Complex::parse("(1e-100,2e-100)").unwrap(),
1542                        ))
1543                        .expect("valid test value")
1544                    );
1545                    println!("compensated sum = {}", sum);
1546                }
1547
1548                #[test]
1549                fn combine_matches_new_sequential_for_chunks() {
1550                    let values = [
1551                        "(1.0,2.0)",
1552                        "(1e100,2e100)",
1553                        "(1.0,2.0)",
1554                        "(-1e100,-2e100)",
1555                        "(1e-100,2e-100)",
1556                        "(2.0,3.0)",
1557                    ]
1558                    .iter()
1559                    .map(|v| {
1560                        ComplexRugStrictFinite::<PRECISION>::try_new(rug::Complex::with_val(
1561                            PRECISION,
1562                            rug::Complex::parse(v).unwrap(),
1563                        ))
1564                        .expect("valid test value")
1565                    })
1566                    .collect::<Vec<_>>();
1567
1568                    let mut chunk_1 = NeumaierSum::<ComplexRugStrictFinite<PRECISION>>::new();
1569                    chunk_1.push(values[0].clone());
1570                    chunk_1.push(values[1].clone());
1571
1572                    let mut chunk_2 = NeumaierSum::<ComplexRugStrictFinite<PRECISION>>::new();
1573                    chunk_2.push(values[2].clone());
1574                    chunk_2.push(values[3].clone());
1575
1576                    let mut chunk_3 = NeumaierSum::<ComplexRugStrictFinite<PRECISION>>::new();
1577                    chunk_3.push(values[4].clone());
1578                    chunk_3.push(values[5].clone());
1579
1580                    chunk_1.combine(chunk_2);
1581                    chunk_1.combine(chunk_3);
1582
1583                    let sequential =
1584                        NeumaierSum::<ComplexRugStrictFinite<PRECISION>>::new_sequential(values);
1585                    assert_eq!(chunk_1.result(), sequential.result());
1586                }
1587            }
1588        }
1589    }
1590
1591    mod scaled_sum_sq_accumulator_tests {
1592        use super::*;
1593        use crate::ScalarCore;
1594
1595        const EPS: f64 = f64::EPSILON;
1596
1597        /// Test single value: √(1²) = 1
1598        #[test]
1599        fn single_value() {
1600            let mut acc = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1601            acc.push(NonNegativeRealScalar::new(3.0));
1602            let result = acc.result_sqrt();
1603            assert!((*result.as_raw_scalar_ref() - 3.0).abs() < EPS);
1604        }
1605
1606        /// Test 3-4-5 right triangle: √(3² + 4²) = 5
1607        #[test]
1608        fn three_four_five_sqrt() {
1609            let mut acc = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1610            acc.push(NonNegativeRealScalar::new(3.0));
1611            acc.push(NonNegativeRealScalar::new(4.0));
1612            let result = acc.result_sqrt();
1613            assert!((*result.as_raw_scalar_ref() - 5.0).abs() < EPS);
1614        }
1615
1616        /// Test 3-4-5 squared: 3² + 4² = 25
1617        #[test]
1618        fn three_four_five_squared() {
1619            let mut acc = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1620            acc.push(NonNegativeRealScalar::new(3.0));
1621            acc.push(NonNegativeRealScalar::new(4.0));
1622            let result = acc.result();
1623            assert!((*result.as_raw_scalar_ref() - 25.0).abs() < EPS);
1624        }
1625
1626        /// Test zero vector
1627        #[test]
1628        fn zero_vector_sqrt() {
1629            let acc = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1630            let result = acc.result_sqrt();
1631            assert_eq!(*result.as_raw_scalar_ref(), 0.0);
1632        }
1633
1634        /// Test zero vector squared
1635        #[test]
1636        fn zero_vector_squared() {
1637            let acc = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1638            let result = acc.result();
1639            assert_eq!(*result.as_raw_scalar_ref(), 0.0);
1640        }
1641
1642        /// Test negative values (abs should handle): √((-3)² + 4²) = 5
1643        #[test]
1644        fn negative_values() {
1645            let mut acc = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1646            acc.push(NonNegativeRealScalar::new(3.0));
1647            acc.push(NonNegativeRealScalar::new(4.0));
1648            let result = acc.result_sqrt();
1649            assert!((*result.as_raw_scalar_ref() - 5.0).abs() < EPS);
1650        }
1651
1652        /// Test combine when other.scale > self.scale (rescale self)
1653        #[test]
1654        fn combine_other_scale_larger() {
1655            // acc1: [1, 1] → scale=1, sumsq=2
1656            let mut acc1 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1657            acc1.push(NonNegativeRealScalar::new(1.0));
1658            acc1.push(NonNegativeRealScalar::new(1.0));
1659
1660            // acc2: [100, 100] → scale=100, sumsq=2
1661            let mut acc2 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1662            acc2.push(NonNegativeRealScalar::new(100.0));
1663            acc2.push(NonNegativeRealScalar::new(100.0));
1664
1665            // Combined: scale=100, sumsq = (1/100)² * 2 + 2 = 0.0002 + 2 = 2.0002
1666            // result_sqrt: 100 * √(2.0002) ≈ 141.42
1667            acc1.combine(acc2);
1668            let result = acc1.result_sqrt();
1669            let expected = (1.0_f64 + 1.0 + 100.0 * 100.0 + 100.0 * 100.0).sqrt();
1670            assert!((*result.as_raw_scalar_ref() - expected).abs() < EPS * expected);
1671        }
1672
1673        /// Test combine when self.scale > other.scale (rescale other)
1674        #[test]
1675        fn combine_self_scale_larger() {
1676            // acc1: [100, 100] → scale=100, sumsq=2
1677            let mut acc1 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1678            acc1.push(NonNegativeRealScalar::new(100.0));
1679            acc1.push(NonNegativeRealScalar::new(100.0));
1680
1681            // acc2: [1, 1] → scale=1, sumsq=2
1682            let mut acc2 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1683            acc2.push(NonNegativeRealScalar::new(1.0));
1684            acc2.push(NonNegativeRealScalar::new(1.0));
1685
1686            // Combined: scale=100, sumsq = 2 + (1/100)² * 2 = 2 + 0.0002
1687            acc1.combine(acc2);
1688            let result = acc1.result_sqrt();
1689            let expected = (100.0_f64 * 100.0 + 100.0 * 100.0 + 1.0 + 1.0).sqrt();
1690            assert!((*result.as_raw_scalar_ref() - expected).abs() < EPS * expected);
1691        }
1692
1693        /// Test combine when both are zero (edge case)
1694        #[test]
1695        fn combine_both_zero() {
1696            let mut acc1 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1697            let acc2 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1698            acc1.combine(acc2);
1699            let result = acc1.result_sqrt();
1700            assert_eq!(*result.as_raw_scalar_ref(), 0.0);
1701        }
1702
1703        /// Test combine: one zero, one non-zero
1704        #[test]
1705        fn combine_one_zero() {
1706            // acc1: zero
1707            let mut acc1 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1708
1709            // acc2: [3, 4]
1710            let mut acc2 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1711            acc2.push(NonNegativeRealScalar::new(3.0));
1712            acc2.push(NonNegativeRealScalar::new(4.0));
1713
1714            acc1.combine(acc2);
1715            let result = acc1.result_sqrt();
1716            assert!((*result.as_raw_scalar_ref() - 5.0).abs() < EPS);
1717        }
1718
1719        /// Test combine with Neumaier (higher accuracy)
1720        #[test]
1721        fn combine_neumaier() {
1722            // acc1: small values
1723            let mut acc1 = ScaledSumSqAccumulator::<NeumaierSum<f64>>::new();
1724            for _ in 0..100 {
1725                acc1.push(NonNegativeRealScalar::new(0.01));
1726            }
1727
1728            // acc2: single large value
1729            let mut acc2 = ScaledSumSqAccumulator::<NeumaierSum<f64>>::new();
1730            acc2.push(NonNegativeRealScalar::new(10.0));
1731
1732            acc1.combine(acc2);
1733            let result = acc1.result_sqrt();
1734
1735            // Expected: √(100*0.01² + 10²) = √(0.01 + 100) = √100.01
1736            let expected = (0.01_f64 + 100.0).sqrt();
1737            assert!((*result.as_raw_scalar_ref() - expected).abs() < EPS * expected);
1738        }
1739
1740        /// Test result() vs result_sqrt(): result_sqrt()² should equal result()
1741        /// Because result() = scale² × sumsq and result_sqrt() = scale × √sumsq
1742        /// So (result_sqrt())² = scale² × sumsq = result()
1743        #[test]
1744        fn result_vs_result_sqrt() {
1745            let mut acc1 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1746            acc1.push(NonNegativeRealScalar::new(3.0));
1747            acc1.push(NonNegativeRealScalar::new(4.0));
1748
1749            let mut acc2 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1750            acc2.push(NonNegativeRealScalar::new(3.0));
1751            acc2.push(NonNegativeRealScalar::new(4.0));
1752
1753            let result = acc1.result();
1754            let sqrt_result = acc2.result_sqrt();
1755
1756            // result_sqrt()² should equal result()
1757            let sqrt_squared = (*sqrt_result.as_raw_scalar_ref()).powi(2);
1758
1759            assert!((*result.as_raw_scalar_ref() - sqrt_squared).abs() < EPS);
1760        }
1761
1762        /// Test with drastically different scales (numerically sensitive)
1763        #[test]
1764        fn extreme_scale_difference() {
1765            let mut acc1 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1766            acc1.push(NonNegativeRealScalar::new(1e-8)); // very small
1767
1768            let mut acc2 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1769            acc2.push(NonNegativeRealScalar::new(1e8)); // very large
1770
1771            acc1.combine(acc2);
1772            let result = acc1.result_sqrt();
1773
1774            // Expected: √((1e-8)² + (1e8)²) ≈ 1e8
1775            let expected = ((1e-8_f64).powi(2) + (1e8_f64).powi(2)).sqrt();
1776            let rel_err = (*result.as_raw_scalar_ref() - expected).abs() / expected;
1777            assert!(rel_err < 1e-10);
1778        }
1779
1780        /// Test sequential vs combined: process all data at once vs in chunks
1781        #[test]
1782        fn sequential_vs_combined() {
1783            let data = [1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0];
1784
1785            // Sequential: process all at once
1786            let mut acc_seq = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1787            for &val in &data {
1788                acc_seq.push(NonNegativeRealScalar::new(val));
1789            }
1790            let result_seq = acc_seq.result_sqrt();
1791
1792            // Chunked: process in two chunks and combine
1793            let mut acc_chunk1 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1794            for &val in &data[0..3] {
1795                acc_chunk1.push(NonNegativeRealScalar::new(val));
1796            }
1797
1798            let mut acc_chunk2 = ScaledSumSqAccumulator::<NaiveSum<f64>>::new();
1799            for &val in &data[3..] {
1800                acc_chunk2.push(NonNegativeRealScalar::new(val));
1801            }
1802
1803            acc_chunk1.combine(acc_chunk2);
1804            let result_combined = acc_chunk1.result_sqrt();
1805
1806            // Results should match
1807            assert!(
1808                (*result_seq.as_raw_scalar_ref() - *result_combined.as_raw_scalar_ref()).abs()
1809                    < EPS
1810            );
1811        }
1812    }
1813}