num_valid/algorithms/vector_norms.rs
1//! Functions and utilities for computing vector norms.
2//!
3//! This module provides public utilities for vector-norm computation on validated
4//! scalars (`ScalarType: FpScalar`), supporting both real
5//! ([`RealScalar`]) and complex
6//! ([`ComplexScalar`](crate::ComplexScalar)) scalars.
7//!
8//! # Norm type structs
9//!
10//! Three zero-sized structs identify norm kinds and implement the [`VectorNorm`]
11//! and [`ParallelVectorNorm`] traits:
12//!
13//! | Struct | Formula | Accumulator |
14//! |--------|---------|-------------|
15//! | [`L1Norm<Acc>`] | `‖v‖₁ = Σᵢ \|vᵢ\|` | [`SumAccumulator`] |
16//! | [`L2Norm<Acc>`] | `‖v‖₂ = √(Σᵢ \|vᵢ\|²)` | [`SumAccumulator`] |
17//! | [`LinfNorm<Acc>`] | `‖v‖∞ = maxᵢ \|vᵢ\|` | [`MaxAbsValueAccumulator`] |
18//!
19//! The type parameter `Acc` selects the summation strategy:
20//! - `NaiveSum<R>` — fast plain `+=` (default for most functions)
21//! - `NeumaierSum<R>` — compensated summation for higher accuracy
22//!
23//! For the two most common combinations, convenience type aliases are provided:
24//!
25//! | Alias | Expands to |
26//! |-------|------------|
27//! | [`L1NormNaiveSum<T>`] | `L1Norm<NaiveSum<T::RealType>>` |
28//! | [`L1NormNeumaierSum<T>`] | `L1Norm<NeumaierSum<T::RealType>>` |
29//! | [`L2NormNaiveSum<T>`] | `L2Norm<NaiveSum<T::RealType>>` |
30//! | [`L2NormNeumaierSum<T>`] | `L2Norm<NeumaierSum<T::RealType>>` |
31//!
32//! ## Direct usage
33//!
34//! The structs can be used directly via their static methods — useful when you
35//! need a specific norm+strategy combination without going through the free
36//! functions. The type aliases make the type annotations shorter:
37//!
38//! ```
39//! use num_valid::algorithms::vector_norms::{
40//! L1Norm, L2Norm, LinfNorm, VectorNorm, ParallelVectorNorm,
41//! L1NormNaiveSum, L2NormNaiveSum, L2NormNeumaierSum,
42//! };
43//! use num_valid::algorithms::accumulators::{NaiveSum, NeumaierSum};
44//!
45//! let v = [3.0_f64, -4.0, 5.0];
46//!
47//! // Using a type alias — equivalent to L1Norm::<NaiveSum<f64>>::compute_vector_norm(&v)
48//! let l1 = L1NormNaiveSum::<f64>::compute_vector_norm(&v);
49//! assert_eq!(*l1.as_ref(), 12.0);
50//!
51//! // L2 norm with Neumaier compensated summation (higher accuracy)
52//! let l2 = L2NormNeumaierSum::<f64>::compute_vector_norm(&v);
53//! assert!((*l2.as_ref() - 50.0_f64.sqrt()).abs() < 1e-12);
54//!
55//! // L∞ norm (default marker type `Acc = ()`)
56//! let linf = LinfNorm::<()>::compute_vector_norm(&v);
57//! assert_eq!(*linf.as_ref(), 5.0);
58//!
59//! // L∞ norm in parallel
60//! let linf_par = LinfNorm::<()>::compute_vector_norm_par(&v);
61//! assert_eq!(linf_par, linf);
62//!
63//! // L2 squared norm (not a norm, but avoids the sqrt)
64//! let l2_sq = L2NormNaiveSum::<f64>::compute_vector_norm_sq(&v);
65//! assert_eq!(*l2_sq.as_ref(), 50.0);
66//! ```
67//!
68//! # Trait-based API
69//!
70//! [`VectorNorm<ScalarType>`] and [`ParallelVectorNorm<T>`] provide a generic
71//! interface that lets algorithms be parameterised over the norm type:
72//!
73//! ```
74//! use num_valid::algorithms::vector_norms::{VectorNorm, L2Norm};
75//! use num_valid::algorithms::accumulators::NaiveSum;
76//! use num_valid::FpScalar;
77//! use num_valid::scalars::NonNegativeRealScalar;
78//!
79//! fn compute<T: FpScalar, N: VectorNorm<T>>(data: &[T]) -> NonNegativeRealScalar<T::RealType> {
80//! N::compute_vector_norm(data)
81//! }
82//!
83//! let v = [3.0_f64, 4.0];
84//! let norm = compute::<_, L2Norm<NaiveSum<f64>>>(&v);
85//! assert!((*norm.as_ref() - 5.0).abs() < 1e-12);
86//! ```
87//!
88//! # Free functions
89//!
90//! Ergonomic wrappers for the most common use cases. All variants accept both
91//! slice (`&[ScalarType]`) and owned-iterator (`_iter`-suffixed) inputs.
92//! All return [`NonNegativeRealScalar<ScalarType::RealType>`].
93//!
94//! For real scalars `RealType = ScalarType`; for complex scalars `RealType` is
95//! the underlying real component type — the norm is always real-valued because
96//! each element contributes via its modulus `|·|`.
97//!
98//! When the input is borrowed (e.g. `slice.iter()`), chain `.cloned()` before
99//! passing to an `_iter`-suffixed function.
100//!
101//! ## L2 (Euclidean) norms — `‖v‖₂ = √(Σᵢ |vᵢ|²)`
102//!
103//! | Function | Formula | Summation |
104//! |----------|---------|-----------|
105//! | [`vector_norm_l2`] | `‖v‖₂` | naive |
106//! | [`vector_norm_l2_iter`] | `‖v‖₂` | naive |
107//! | [`vector_norm_l2_neumaier`] | `‖v‖₂` | Neumaier compensated |
108//! | [`vector_norm_l2_neumaier_iter`] | `‖v‖₂` | Neumaier compensated |
109//! | [`vector_norm_l2_sq`] | `‖v‖₂²` | naive |
110//! | [`vector_norm_l2_sq_iter`] | `‖v‖₂²` | naive |
111//! | [`vector_norm_l2_sq_neumaier`] | `‖v‖₂²` | Neumaier compensated |
112//! | [`vector_norm_l2_sq_neumaier_iter`] | `‖v‖₂²` | Neumaier compensated |
113//!
114//! All L2 variants use a **scaled sum-of-squares** algorithm (equivalent to
115//! LAPACK `dnrm2`): the running `scale = max |vᵢ|` ensures that no intermediate
116//! value overflows or underflows. The `_neumaier` variants additionally apply
117//! compensated summation to recover low-order bits lost when components span
118//! many orders of magnitude.
119//!
120//! Note: `‖v‖₂²` is not a norm (it violates the triangle inequality) and is
121//! therefore intentionally absent from the [`VectorNorm`] trait. It is exposed
122//! as a computational convenience and as a method on [`L2Norm`].
123//!
124//! ## L1 (Manhattan) norm — `‖v‖₁ = Σᵢ |vᵢ|`
125//!
126//! | Function | Formula | Summation |
127//! |----------|---------|-----------|
128//! | [`vector_norm_l1`] | `‖v‖₁` | naive |
129//! | [`vector_norm_l1_iter`] | `‖v‖₁` | naive |
130//! | [`vector_norm_l1_neumaier`] | `‖v‖₁` | Neumaier compensated |
131//! | [`vector_norm_l1_neumaier_iter`] | `‖v‖₁` | Neumaier compensated |
132//!
133//! ## L∞ (Chebyshev) norm — `‖v‖∞ = maxᵢ |vᵢ|`
134//!
135//! | Function | Execution |
136//! |----------|-----------|
137//! | [`vector_norm_linf`] | sequential, slice |
138//! | [`vector_norm_linf_iter`] | sequential, iterator |
139//! | [`vector_norm_linf_par`] | parallel, slice |
140//! | [`vector_norm_linf_par_iter`] | parallel, iterator |
141//!
142//! ## Advanced API — custom accumulator
143//!
144//! For full control over the summation strategy use the accumulator-generic
145//! functions. These accept any [`SumAccumulator`] as a type parameter:
146//! - [`vector_norm_l1_with_accumulator`] / [`vector_norm_l1_iter_with_accumulator`]
147//! - [`vector_norm_l2_with_accumulator`] / [`vector_norm_l2_iter_with_accumulator`]
148//! - [`vector_norm_l2_sq_with_accumulator`] / [`vector_norm_l2_sq_iter_with_accumulator`]
149
150#![deny(rustdoc::broken_intra_doc_links)]
151
152use crate::{
153 FpScalar, RealScalar,
154 algorithms::accumulators::{
155 Accumulator, MaxAbsValueAccumulator, NaiveSum, NeumaierSum, SumAccumulator,
156 },
157 functions::{Pow, Sqrt},
158 scalars::NonNegativeRealScalar,
159};
160use num_traits::{One, Zero};
161use rayon::prelude::*;
162use std::marker::PhantomData;
163use try_create::TryNew;
164
165//---------------------------------------------------------
166
167struct ScaledSumSqAccumulator<ScalarType, SumSqAcc>
168where
169 ScalarType: FpScalar,
170 SumSqAcc: SumAccumulator<Input = ScalarType::RealType>,
171{
172 /// - `scale = max |vᵢ|` (0 if all components are zero)
173 scale: ScalarType::RealType,
174 /// - `sumsq = Σ (|vᵢ|/scale)²` accumulated via strategy `SumSqAcc`
175 sumsq_accumulator: SumSqAcc,
176 zero: ScalarType::RealType,
177}
178
179impl<ScalarType, SumSqAcc> ScaledSumSqAccumulator<ScalarType, SumSqAcc>
180where
181 ScalarType: FpScalar,
182 SumSqAcc: SumAccumulator<Input = ScalarType::RealType>,
183{
184 fn add_value_and_update_scale(&mut self, value: ScalarType::RealType) {
185 if value > self.zero {
186 let r_sq = if self.scale < value {
187 if self.scale > self.zero {
188 let r = self.scale.clone() / &value;
189 self.sumsq_accumulator.rescale_by(&r.pow(2));
190 }
191 self.scale = value;
192 ScalarType::RealType::one()
193 } else {
194 (value / &self.scale).pow(2)
195 };
196 self.sumsq_accumulator.push(r_sq);
197 }
198 }
199
200 fn result_sqrt(self) -> NonNegativeRealScalar<ScalarType::RealType> {
201 let sqrt_sum_sq = if self.scale == self.zero {
202 self.zero
203 } else {
204 self.sumsq_accumulator.result().sqrt() * self.scale
205 };
206 NonNegativeRealScalar::try_new(sqrt_sum_sq).expect(
207 "ScaledSumSqAccumulator::result_sqrt: computed value is negative or infinite (bug)",
208 )
209 }
210}
211
212impl<ScalarType, SumSqAcc> Accumulator for ScaledSumSqAccumulator<ScalarType, SumSqAcc>
213where
214 ScalarType: FpScalar,
215 SumSqAcc: SumAccumulator<Input = ScalarType::RealType>,
216{
217 type Input = ScalarType;
218 type Output = NonNegativeRealScalar<ScalarType::RealType>;
219
220 fn new() -> Self {
221 Self {
222 scale: ScalarType::RealType::zero(),
223 sumsq_accumulator: SumSqAcc::new(),
224 zero: ScalarType::RealType::zero(),
225 }
226 }
227
228 fn push(&mut self, value: Self::Input) {
229 self.add_value_and_update_scale(value.abs());
230 }
231
232 fn combine(&mut self, mut other: Self) {
233 if other.scale > self.scale {
234 if self.scale > self.zero {
235 let r = self.scale.clone() / &other.scale;
236 self.sumsq_accumulator.rescale_by(&r.pow(2));
237 }
238 self.scale = other.scale;
239 self.sumsq_accumulator.combine(other.sumsq_accumulator);
240 } else if other.scale > self.zero {
241 if other.scale < self.scale {
242 let r = other.scale / &self.scale;
243 other.sumsq_accumulator.rescale_by(&r.pow(2));
244 }
245 self.sumsq_accumulator.combine(other.sumsq_accumulator);
246 }
247 // when both scale==0: zero vector, nothing to do
248 }
249
250 fn result(self) -> Self::Output {
251 let sum_sq = if self.scale == self.zero {
252 self.zero
253 } else {
254 self.sumsq_accumulator.result() * self.scale.pow(2)
255 };
256 NonNegativeRealScalar::try_new(sum_sq)
257 .expect("ScaledSumSqAccumulator::result: computed value is negative or infinite (bug)")
258 }
259}
260//---------------------------------------------------------
261
262//---------------------------------------------------------
263/// Identifies the Manhattan (L1) norm, `‖v‖₁ = Σᵢ |vᵢ|`.
264///
265/// The type parameter `Acc` selects the summation strategy for absolute values.
266/// Typical choices:
267/// - `NaiveSum<R>` — plain `+=`, fastest option.
268/// - `NeumaierSum<R>` — compensated summation, more accurate when components
269/// span many orders of magnitude.
270///
271/// `L1Norm<Acc>` implements [`VectorNorm`] and [`ParallelVectorNorm`] for any
272/// [`FpScalar`] whose `RealType` matches `Acc::Input`.
273pub struct L1Norm<Acc>(PhantomData<Acc>);
274
275/// Type alias for [`L1Norm`] using [`NaiveSum`] as summation strategy.
276///
277/// `L1NormNaiveSum<T>` expands to `L1Norm<NaiveSum<T::RealType>>`. Use this
278/// when performance is the priority and the data is well-scaled.
279pub type L1NormNaiveSum<ScalarType> = L1Norm<NaiveSum<<ScalarType as FpScalar>::RealType>>;
280
281/// Type alias for [`L1Norm`] using [`NeumaierSum`] as summation strategy.
282///
283/// `L1NormNeumaierSum<T>` expands to `L1Norm<NeumaierSum<T::RealType>>`. Use
284/// this when components span many orders of magnitude and accuracy matters.
285pub type L1NormNeumaierSum<ScalarType> = L1Norm<NeumaierSum<<ScalarType as FpScalar>::RealType>>;
286
287//---------------------------------------------------------
288
289//---------------------------------------------------------
290/// Identifies the Euclidean (L2) norm, `‖v‖₂ = √(Σᵢ |vᵢ|²)`.
291///
292/// The type parameter `Acc` selects the summation strategy for the scaled
293/// sum-of-squares loop. Typical choices:
294/// - `NaiveSum<R>` — plain `+=`, fastest option.
295/// - `NeumaierSum<R>` — compensated summation for higher accuracy when
296/// components span many orders of magnitude.
297///
298/// `L2Norm<Acc>` implements [`VectorNorm`] and [`ParallelVectorNorm`] for any
299/// [`FpScalar`] whose `RealType` matches `Acc::Input`.
300///
301/// In addition to the standard norm, `L2Norm` exposes the squared Euclidean
302/// norm `‖v‖₂² = Σᵢ |vᵢ|²` as a separate operation (avoiding the square root)
303/// via [`L2Norm::compute_vector_norm_sq`] and its iterator/parallel variants.
304/// Note that `‖v‖₂²` is not a norm — it violates the triangle inequality — and
305/// is therefore not part of the [`VectorNorm`] trait.
306pub struct L2Norm<Acc>(PhantomData<Acc>);
307
308/// Type alias for [`L2Norm`] using [`NaiveSum`] as summation strategy.
309///
310/// `L2NormNaiveSum<T>` expands to `L2Norm<NaiveSum<T::RealType>>`. Use this
311/// when performance is the priority and the data is well-scaled.
312pub type L2NormNaiveSum<ScalarType> = L2Norm<NaiveSum<<ScalarType as FpScalar>::RealType>>;
313
314/// Type alias for [`L2Norm`] using [`NeumaierSum`] as summation strategy.
315///
316/// `L2NormNeumaierSum<T>` expands to `L2Norm<NeumaierSum<T::RealType>>`. Use
317/// this when components span many orders of magnitude and accuracy matters.
318pub type L2NormNeumaierSum<ScalarType> = L2Norm<NeumaierSum<<ScalarType as FpScalar>::RealType>>;
319
320impl<Acc: SumAccumulator<Input: RealScalar>> L2Norm<Acc> {
321 /// Computes the squared Euclidean (L2) norm `‖v‖₂²` of a slice.
322 ///
323 /// Equivalent to [`compute_vector_norm_sq_iter`](Self::compute_vector_norm_sq_iter)
324 /// called with `v.iter().cloned()`.
325 ///
326 /// This avoids the square root present in [`VectorNorm::compute_vector_norm`]
327 /// and is useful when only relative comparisons of norms are needed.
328 pub fn compute_vector_norm_sq<T>(v: &[T]) -> NonNegativeRealScalar<Acc::Output>
329 where
330 T: FpScalar<RealType = Acc::Output>,
331 {
332 Self::compute_vector_norm_sq_iter(v.iter().cloned())
333 }
334
335 /// Computes the squared Euclidean (L2) norm `‖v‖₂²` from an owned iterator.
336 ///
337 /// Uses the same scaled sum-of-squares algorithm as [`VectorNorm`] but
338 /// returns `scale² × sumsq` directly without taking the square root.
339 pub fn compute_vector_norm_sq_iter<I>(v: I) -> NonNegativeRealScalar<Acc::Output>
340 where
341 I: IntoIterator<Item: FpScalar<RealType = Acc::Output>>,
342 {
343 ScaledSumSqAccumulator::<I::Item, Acc>::new_sequential(v).result()
344 }
345}
346
347impl<Acc> L2Norm<Acc>
348where
349 Acc: SumAccumulator<Input: RealScalar> + Send,
350{
351 /// Computes the squared Euclidean (L2) norm `‖v‖₂²` of a slice in parallel.
352 ///
353 /// Equivalent to [`compute_vector_norm_sq_par_iter`](Self::compute_vector_norm_sq_par_iter)
354 /// called with `v.par_iter().cloned()`.
355 pub fn compute_vector_norm_sq_par<T>(v: &[T]) -> NonNegativeRealScalar<Acc::Output>
356 where
357 T: FpScalar<RealType = Acc::Output>,
358 {
359 Self::compute_vector_norm_sq_par_iter(v.par_iter().cloned())
360 }
361
362 /// Computes the squared Euclidean (L2) norm `‖v‖₂²` from a parallel iterator.
363 ///
364 /// Chunks are processed in parallel and merged via
365 /// [`Accumulator::combine`],
366 /// preserving the numerical properties of the chosen `Acc` strategy across chunks.
367 pub fn compute_vector_norm_sq_par_iter<I>(v: I) -> NonNegativeRealScalar<Acc::Output>
368 where
369 I: IntoParallelIterator<Item: FpScalar<RealType = Acc::Output>>,
370 {
371 ScaledSumSqAccumulator::<I::Item, Acc>::new_parallel(v).result()
372 }
373}
374
375//---------------------------------------------------------
376
377//pub struct L2SqNorm<Strategy>(PhantomData<Strategy>);
378
379//---------------------------------------------------------
380/// Identifies the Chebyshev (L∞) norm, `‖v‖∞ = maxᵢ |vᵢ|`.
381///
382/// Unlike [`L1Norm`] and [`L2Norm`], `LinfNorm` does not use a summation
383/// strategy: it is a single linear scan for the maximum absolute value.
384///
385/// The `Acc` parameter is currently a marker type (default `()`) and is not
386/// used by the vector implementation itself. It exists to keep the API
387/// extensible and to align type-level dispatch with norm families where a
388/// dedicated accumulator may be useful in related contexts (for example,
389/// matrix norms).
390///
391/// `LinfNorm` implements both [`VectorNorm`] and [`ParallelVectorNorm`] for any
392/// [`FpScalar`]. The parallel variant uses
393/// [`MaxAbsValueAccumulator`]
394/// with rayon `fold` + `reduce`, which is numerically exact (no rounding).
395pub struct LinfNorm<Acc = ()>(PhantomData<Acc>);
396//---------------------------------------------------------
397
398//---------------------------------------------------------
399/// Sequential vector norm computation.
400///
401/// Implemented by [`L1Norm`], [`L2Norm`], and [`LinfNorm`]. Generic code that
402/// accepts any norm should bound `N: VectorNorm<T>`.
403///
404/// All implementations accept both real and complex scalars: for complex types
405/// each element contributes via its modulus `|zᵢ|`, so the result is always
406/// a real-valued [`NonNegativeRealScalar`].
407///
408/// # Methods
409///
410/// - [`compute_vector_norm`](VectorNorm::compute_vector_norm) — slice input
411/// (delegates to `compute_vector_norm_iter` by default).
412/// - [`compute_vector_norm_iter`](VectorNorm::compute_vector_norm_iter) — owned
413/// iterator input; **must** be implemented by each norm type.
414pub trait VectorNorm<ScalarType: FpScalar> {
415 /// Computes the norm of a slice of scalars.
416 ///
417 /// The default implementation calls [`compute_vector_norm_iter`](Self::compute_vector_norm_iter)
418 /// with `v.iter().cloned()`.
419 fn compute_vector_norm(v: &[ScalarType]) -> NonNegativeRealScalar<ScalarType::RealType> {
420 Self::compute_vector_norm_iter(v.iter().cloned())
421 }
422
423 /// Computes the norm from an owned-item iterator.
424 ///
425 /// Iterator-based APIs require **owned** items. When the input is borrowed
426 /// (e.g. from a slice), chain `.iter().cloned()` before calling this method.
427 fn compute_vector_norm_iter<I>(v: I) -> NonNegativeRealScalar<ScalarType::RealType>
428 where
429 I: IntoIterator<Item = ScalarType>;
430}
431
432/// Parallel vector norm computation.
433///
434/// Extends [`VectorNorm`] with parallel variants that distribute work across
435/// threads using rayon. Chunks are accumulated independently and merged via
436/// [`Accumulator::combine`].
437///
438/// Requires `T: Send + Sync`.
439///
440/// # Methods
441///
442/// - [`compute_vector_norm_par`](ParallelVectorNorm::compute_vector_norm_par) — parallel slice.
443/// - [`compute_vector_norm_par_iter`](ParallelVectorNorm::compute_vector_norm_par_iter) —
444/// parallel iterator; **must** be implemented by each norm type.
445pub trait ParallelVectorNorm<T: FpScalar + Send + Sync>: VectorNorm<T> {
446 /// Computes the norm of a slice in parallel.
447 ///
448 /// The default implementation calls
449 /// [`compute_vector_norm_par_iter`](Self::compute_vector_norm_par_iter)
450 /// with `v.par_iter().cloned()`.
451 fn compute_vector_norm_par(v: &[T]) -> NonNegativeRealScalar<T::RealType> {
452 Self::compute_vector_norm_par_iter(v.par_iter().cloned())
453 }
454
455 /// Computes the norm from an owned parallel iterator.
456 fn compute_vector_norm_par_iter<I: IntoParallelIterator<Item = T>>(
457 iter: I,
458 ) -> NonNegativeRealScalar<T::RealType>;
459}
460//---------------------------------------------------------
461
462//---------------------------------------------------------
463impl<ScalarType, SumAcc> VectorNorm<ScalarType> for L2Norm<SumAcc>
464where
465 ScalarType: FpScalar,
466 SumAcc: SumAccumulator<Input = ScalarType::RealType>,
467{
468 fn compute_vector_norm_iter<I>(v: I) -> NonNegativeRealScalar<ScalarType::RealType>
469 where
470 I: IntoIterator<Item = ScalarType>,
471 {
472 ScaledSumSqAccumulator::<ScalarType, SumAcc>::new_sequential(v).result_sqrt()
473 }
474}
475
476impl<T, SumAcc> ParallelVectorNorm<T> for L2Norm<SumAcc>
477where
478 T: FpScalar,
479 SumAcc: SumAccumulator<Input = T::RealType> + Send,
480{
481 fn compute_vector_norm_par_iter<I: IntoParallelIterator<Item = T>>(
482 iter: I,
483 ) -> NonNegativeRealScalar<T::RealType> {
484 ScaledSumSqAccumulator::<T, SumAcc>::new_parallel(iter).result_sqrt()
485 }
486}
487//---------------------------------------------------------
488
489//---------------------------------------------------------
490impl<ScalarType, SumAcc> VectorNorm<ScalarType> for L1Norm<SumAcc>
491where
492 ScalarType: FpScalar,
493 SumAcc: SumAccumulator<Input = ScalarType::RealType>,
494{
495 fn compute_vector_norm_iter<I>(v: I) -> NonNegativeRealScalar<ScalarType::RealType>
496 where
497 I: IntoIterator<Item = ScalarType>,
498 {
499 NonNegativeRealScalar::try_new(SumAcc::new_sequential(v.into_iter().map(|x| x.abs())).result())
500 .expect("<L1Norm as VectorNorm>::compute_vector_norm_iter: computed value is negative or infinite (bug)")
501 }
502}
503
504impl<T, SumAcc> ParallelVectorNorm<T> for L1Norm<SumAcc>
505where
506 T: FpScalar,
507 SumAcc: SumAccumulator<Input = T::RealType> + Send,
508{
509 fn compute_vector_norm_par_iter<I: IntoParallelIterator<Item = T>>(
510 iter: I,
511 ) -> NonNegativeRealScalar<T::RealType> {
512 NonNegativeRealScalar::try_new(SumAcc::new_parallel(iter.into_par_iter().map(|x| x.abs())).result())
513 .expect("<L1Norm as ParallelVectorNorm>::compute_vector_norm_par_iter: computed value is negative or infinite (bug)")
514 }
515}
516
517//---------------------------------------------------------
518
519//---------------------------------------------------------
520impl<ScalarType> VectorNorm<ScalarType> for LinfNorm
521where
522 ScalarType: FpScalar,
523{
524 fn compute_vector_norm_iter<I>(v: I) -> NonNegativeRealScalar<ScalarType::RealType>
525 where
526 I: IntoIterator<Item = ScalarType>,
527 {
528 MaxAbsValueAccumulator::new_sequential(v).result()
529 }
530}
531
532impl<ScalarType> ParallelVectorNorm<ScalarType> for LinfNorm
533where
534 ScalarType: FpScalar,
535{
536 fn compute_vector_norm_par_iter<I>(v: I) -> NonNegativeRealScalar<ScalarType::RealType>
537 where
538 I: IntoParallelIterator<Item = ScalarType>,
539 {
540 MaxAbsValueAccumulator::new_parallel(v).result()
541 }
542}
543//---------------------------------------------------------
544
545// ── Public L2 norm functions ─────────────────────────────────────────────────
546
547/// Accumulator-driven implementation of the Euclidean (L2) norm.
548///
549/// This helper computes `vector_norm_l2` using the provided accumulation strategy
550/// `SumSqAcc` for the scaled sum-of-squares term.
551///
552/// The final value is guaranteed to be non-negative by construction and is
553/// wrapped into [`NonNegativeRealScalar`].
554///
555/// # Panics
556///
557/// Panics only if the computed value is negative, which indicates an internal
558/// invariant violation.
559#[inline(always)]
560pub fn vector_norm_l2_with_accumulator<ScalarType, SumSqAcc>(
561 v: &[ScalarType],
562) -> NonNegativeRealScalar<ScalarType::RealType>
563where
564 ScalarType: FpScalar,
565 SumSqAcc: SumAccumulator<Input = ScalarType::RealType>,
566{
567 L2Norm::<SumSqAcc>::compute_vector_norm(v)
568}
569
570/// Iterator-based accumulator-driven implementation of the Euclidean (L2) norm.
571///
572/// This is the iterator counterpart of [`vector_norm_l2_with_accumulator`] and accepts
573/// any `IntoIterator<Item = ScalarType>`.
574///
575/// Use this when your inputs are already produced as an iterator chain.
576#[inline(always)]
577pub fn vector_norm_l2_iter_with_accumulator<ScalarType, SumSqAcc, I>(
578 v: I,
579) -> NonNegativeRealScalar<ScalarType::RealType>
580where
581 ScalarType: FpScalar,
582 SumSqAcc: SumAccumulator<Input = ScalarType::RealType>,
583 I: IntoIterator<Item = ScalarType>,
584{
585 L2Norm::<SumSqAcc>::compute_vector_norm_iter(v)
586}
587
588/// Accumulator-driven implementation of the squared Euclidean (L2) norm.
589///
590/// This function computes `vector_norm_l2_sq` using the provided accumulation
591/// strategy `SumSqAcc` for the scaled sum-of-squares term.
592///
593/// Use this API when you need explicit control over how terms are accumulated,
594/// for example to compare [`NaiveSum`] with [`NeumaierSum`] or a custom
595/// accumulator implementation.
596///
597/// The final value is guaranteed to be non-negative by construction and is
598/// wrapped into [`NonNegativeRealScalar`].
599///
600/// # Examples
601///
602/// ```
603/// use num_valid::algorithms::vector_norms::vector_norm_l2_sq_with_accumulator;
604/// use num_valid::algorithms::accumulators::{NaiveSum, NeumaierSum};
605///
606/// let v = [3.0_f64, 4.0];
607/// let naive = vector_norm_l2_sq_with_accumulator::<_, NaiveSum<_>>(&v);
608/// let compensated = vector_norm_l2_sq_with_accumulator::<_, NeumaierSum<_>>(&v);
609/// assert_eq!(*naive.as_ref(), 25.0);
610/// assert_eq!(naive, compensated);
611/// ```
612///
613/// # Panics
614///
615/// Panics only if the computed value is negative, which indicates an internal
616/// invariant violation.
617#[inline(always)]
618pub fn vector_norm_l2_sq_with_accumulator<ScalarType, SumSqAcc>(
619 v: &[ScalarType],
620) -> NonNegativeRealScalar<ScalarType::RealType>
621where
622 ScalarType: FpScalar,
623 SumSqAcc: SumAccumulator<Input = ScalarType::RealType>,
624{
625 L2Norm::<SumSqAcc>::compute_vector_norm_sq(v)
626}
627
628/// Iterator-based accumulator-driven implementation of the squared Euclidean
629/// (L2) norm.
630///
631/// This is the iterator counterpart of [`vector_norm_l2_sq_with_accumulator`] and
632/// accepts any `IntoIterator<Item = ScalarType>`.
633#[inline(always)]
634pub fn vector_norm_l2_sq_iter_with_accumulator<ScalarType, SumSqAcc, I>(
635 v: I,
636) -> NonNegativeRealScalar<ScalarType::RealType>
637where
638 ScalarType: FpScalar,
639 SumSqAcc: SumAccumulator<Input = ScalarType::RealType>,
640 I: IntoIterator<Item = ScalarType>,
641{
642 L2Norm::<SumSqAcc>::compute_vector_norm_sq_iter(v)
643}
644
645/// Accumulator-driven implementation of the Manhattan (L1) norm.
646///
647/// This function computes `vector_norm_l1` by summing absolute values with the
648/// accumulation strategy `SumAbsAcc`.
649///
650/// Use this API when you need explicit control over how absolute values are
651/// accumulated, for example to compare [`NaiveSum`] with [`NeumaierSum`] or a
652/// custom accumulator implementation.
653///
654/// The final value is guaranteed to be non-negative by construction and is
655/// wrapped into [`NonNegativeRealScalar`].
656///
657/// # Examples
658///
659/// ```
660/// use num_valid::algorithms::vector_norms::vector_norm_l1_with_accumulator;
661/// use num_valid::algorithms::accumulators::{NaiveSum, NeumaierSum};
662///
663/// let v = [3.0_f64, 4.0];
664/// let naive = vector_norm_l1_with_accumulator::<_, NaiveSum<_>>(&v);
665/// let compensated = vector_norm_l1_with_accumulator::<_, NeumaierSum<_>>(&v);
666/// assert_eq!(*naive.as_ref(), 7.0);
667/// assert_eq!(naive, compensated);
668/// ```
669///
670/// # Panics
671///
672/// Panics only if the computed value is negative, which indicates an internal
673/// invariant violation.
674#[inline(always)]
675pub fn vector_norm_l1_with_accumulator<ScalarType, SumAcc>(
676 a: &[ScalarType],
677) -> NonNegativeRealScalar<ScalarType::RealType>
678where
679 ScalarType: FpScalar,
680 SumAcc: SumAccumulator<Input = ScalarType::RealType>,
681{
682 L1Norm::<SumAcc>::compute_vector_norm(a)
683}
684
685/// Iterator-based accumulator-driven implementation of the Manhattan (L1) norm.
686///
687/// This is the iterator counterpart of [`vector_norm_l1_with_accumulator`] and accepts
688/// any `IntoIterator<Item = ScalarType>`.
689#[inline(always)]
690pub fn vector_norm_l1_iter_with_accumulator<ScalarType, SumAcc, I>(
691 a: I,
692) -> NonNegativeRealScalar<ScalarType::RealType>
693where
694 ScalarType: FpScalar,
695 SumAcc: SumAccumulator<Input = ScalarType::RealType>,
696 I: IntoIterator<Item = ScalarType>,
697{
698 L1Norm::<SumAcc>::compute_vector_norm_iter(a)
699}
700
701/// Computes the Euclidean (L2) norm of a slice of scalars.
702///
703/// Returns ‖**v**‖₂ = √(Σᵢ |vᵢ|²), where |·| denotes the modulus (absolute
704/// value for reals, complex modulus for complex scalars).
705///
706/// # Numerical Stability
707///
708/// The result is computed using a **scaled sum-of-squares** algorithm: the
709/// running `scale` equals the largest |vᵢ| seen so far and `sumsq`
710/// accumulates (|vᵢ| / scale)². The final result is `scale × √sumsq`. This
711/// avoids overflow and underflow of intermediate values.
712///
713/// The algorithm is equivalent to the one used in LAPACK's `dnrm2` routine.
714/// Note: scaling alone does not prevent the loss of small addends when they are
715/// absorbed into a larger running total (absorption error); use [`vector_norm_l2_neumaier`]
716/// when that precision is required.
717///
718/// Equivalent to calling the internal accumulator-driven implementation with
719/// [`NaiveSum`].
720///
721/// # Examples
722///
723/// ```
724/// use num_valid::algorithms::vector_norms::vector_norm_l2;
725///
726/// // 3-4-5 right triangle: norm of (3, 4) is 5
727/// let v = [3.0_f64, 4.0];
728/// assert!((*vector_norm_l2(&v).as_ref() - 5.0).abs() < 1e-12);
729///
730/// // Zero vector
731/// let z = [0.0_f64; 3];
732/// assert_eq!(*vector_norm_l2(&z).as_ref(), 0.0);
733///
734/// // Unit axis vector has norm 1
735/// let e1 = [1.0_f64, 0.0, 0.0];
736/// assert!((*vector_norm_l2(&e1).as_ref() - 1.0).abs() < 1e-15);
737/// ```
738///
739/// Complex scalars are also supported; `|zᵢ|` is the complex modulus:
740///
741/// ```
742/// use num_valid::algorithms::vector_norms::vector_norm_l2;
743/// use num_valid::ComplexNative64StrictFinite;
744/// use num::Complex;
745/// use try_create::TryNew;
746///
747/// // ‖[3+0i, 0+4i]‖₂ = √(3² + 4²) = 5
748/// let v = [
749/// ComplexNative64StrictFinite::try_new(Complex::new(3.0_f64, 0.0)).unwrap(),
750/// ComplexNative64StrictFinite::try_new(Complex::new(0.0_f64, 4.0)).unwrap(),
751/// ];
752/// let result = vector_norm_l2(&v).into_inner();
753/// assert!((*result.as_ref() - 5.0_f64).abs() < 1e-12);
754/// ```
755#[inline(always)]
756pub fn vector_norm_l2<ScalarType>(v: &[ScalarType]) -> NonNegativeRealScalar<ScalarType::RealType>
757where
758 ScalarType: FpScalar,
759{
760 L2NormNaiveSum::<ScalarType>::compute_vector_norm(v)
761}
762
763/// Computes the Euclidean (L2) norm from an owned-item iterator.
764///
765/// This is the iterator counterpart of [`vector_norm_l2`].
766///
767/// # Examples
768///
769/// ```
770/// use num_valid::algorithms::vector_norms::vector_norm_l2_iter;
771///
772/// let v = vec![3.0_f64, 4.0];
773/// assert!((*vector_norm_l2_iter(v).as_ref() - 5.0).abs() < 1e-12);
774/// ```
775#[inline(always)]
776pub fn vector_norm_l2_iter<ScalarType, I>(v: I) -> NonNegativeRealScalar<ScalarType::RealType>
777where
778 ScalarType: FpScalar,
779 I: IntoIterator<Item = ScalarType>,
780{
781 L2NormNaiveSum::<ScalarType>::compute_vector_norm_iter(v)
782}
783
784/// Computes the squared Euclidean (L2) norm of a slice of scalars.
785///
786/// Returns ‖**v**‖₂² = Σᵢ |vᵢ|², where |·| denotes the modulus.
787///
788/// # Numerical Stability
789///
790/// The result is computed using a **scaled sum-of-squares** algorithm: the
791/// running `scale` equals the largest |vᵢ| seen so far and `sumsq`
792/// accumulates (|vᵢ| / scale)². The final result is `scale² × sumsq`. This
793/// avoids overflow and underflow of intermediate values.
794///
795/// Note: `scale²` must not overflow the maximum representable value of the
796/// underlying real type. For vectors where accuracy of small squared terms
797/// matters, prefer [`vector_norm_l2_sq_neumaier`].
798///
799/// Equivalent to calling [`vector_norm_l2_sq_with_accumulator`] with [`NaiveSum`].
800///
801/// # Examples
802///
803/// ```
804/// use num_valid::algorithms::vector_norms::vector_norm_l2_sq;
805///
806/// // 3-4-5 right triangle: ‖(3, 4)‖₂² == 9 + 16 == 25
807/// let v = [3.0_f64, 4.0];
808/// assert_eq!(*vector_norm_l2_sq(&v).as_ref(), 25.0);
809///
810/// // Zero vector
811/// let z = [0.0_f64; 3];
812/// assert_eq!(*vector_norm_l2_sq(&z).as_ref(), 0.0);
813///
814/// // Unit axis vector has squared norm 1
815/// let e1 = [1.0_f64, 0.0, 0.0];
816/// assert_eq!(*vector_norm_l2_sq(&e1).as_ref(), 1.0);
817/// ```
818#[inline(always)]
819pub fn vector_norm_l2_sq<ScalarType: FpScalar>(
820 v: &[ScalarType],
821) -> NonNegativeRealScalar<ScalarType::RealType> {
822 L2NormNaiveSum::<ScalarType>::compute_vector_norm_sq(v)
823}
824
825/// Computes the squared Euclidean (L2) norm from an owned-item iterator.
826///
827/// This is the iterator counterpart of [`vector_norm_l2_sq`].
828#[inline(always)]
829pub fn vector_norm_l2_sq_iter<ScalarType, I>(v: I) -> NonNegativeRealScalar<ScalarType::RealType>
830where
831 ScalarType: FpScalar,
832 I: IntoIterator<Item = ScalarType>,
833{
834 L2NormNaiveSum::<ScalarType>::compute_vector_norm_sq_iter(v)
835}
836
837/// Computes the squared Euclidean (L2) norm of a slice of scalars using
838/// **scaling + Neumaier compensated summation**.
839///
840/// Returns ‖**v**‖₂² = Σᵢ |vᵢ|², where |·| denotes the modulus.
841///
842/// # Numerical Accuracy
843///
844/// This is an improved variant of [`vector_norm_l2_sq`] that combines two complementary
845/// techniques:
846///
847/// 1. **Scaling** (same as [`vector_norm_l2_sq`]): tracks `scale = max |vᵢ|` and
848/// accumulates `Σ (|vᵢ|/scale)²`, so that no individual term overflows or
849/// underflows before being squared.
850///
851/// 2. **Neumaier compensated summation**: replaces the plain `sumsq += r²` with
852/// Kahan/Neumaier compensation, capturing low-order bits that are silently
853/// discarded in the naive loop. This is particularly beneficial when a large
854/// dominant component (near `scale`) causes the unit-in-last-place of `sumsq`
855/// to exceed many small squared terms.
856///
857/// **When rescaling occurs** (a new maximum is found mid-iteration), the
858/// accumulated Neumaier sum is multiplied by `r²` and the compensation term is
859/// reset to zero. Rescaling is rare in practice (occurs at most log(N) times for
860/// random data), so the accuracy benefit of Neumaier is preserved for the bulk
861/// of the summation.
862///
863/// # Special Cases
864///
865/// | Input | Result |
866/// |-------|--------|
867/// | all-zero vector | `0.0` |
868/// | single dominant component | full Neumaier accuracy for the remaining terms |
869///
870/// # Examples
871///
872/// ```
873/// use num_valid::algorithms::vector_norms::vector_norm_l2_sq_neumaier;
874///
875/// // 3-4-5 right triangle: ‖(3, 4)‖₂² == 25
876/// let v = [3.0_f64, 4.0];
877/// assert_eq!(*vector_norm_l2_sq_neumaier(&v).as_ref(), 25.0);
878///
879/// // Zero vector
880/// let z = [0.0_f64; 3];
881/// assert_eq!(*vector_norm_l2_sq_neumaier(&z).as_ref(), 0.0);
882///
883/// // Recovers small squared terms lost by naive accumulation:
884/// // v = [1.0, 1e-9 × 10_000]; true ‖v‖₂² = 1.0 + 1e-14
885/// const N: usize = 10_000;
886/// let mut v_large: [f64; N + 1] = [1e-9; N + 1];
887/// v_large[0] = 1.0;
888/// let result = *vector_norm_l2_sq_neumaier(&v_large).as_ref();
889/// let expected = 1.0_f64 + N as f64 * 1e-18;
890/// assert!((result - expected).abs() < 1e-15 * expected);
891/// ```
892#[inline(always)]
893pub fn vector_norm_l2_sq_neumaier<ScalarType: FpScalar>(
894 v: &[ScalarType],
895) -> NonNegativeRealScalar<ScalarType::RealType> {
896 L2NormNeumaierSum::<ScalarType>::compute_vector_norm_sq(v)
897}
898
899/// Computes the squared Euclidean (L2) norm from an owned-item iterator using
900/// scaling plus Neumaier compensated summation.
901///
902/// This is the iterator counterpart of [`vector_norm_l2_sq_neumaier`].
903#[inline(always)]
904pub fn vector_norm_l2_sq_neumaier_iter<ScalarType, I>(
905 v: I,
906) -> NonNegativeRealScalar<ScalarType::RealType>
907where
908 ScalarType: FpScalar,
909 I: IntoIterator<Item = ScalarType>,
910{
911 L2NormNeumaierSum::<ScalarType>::compute_vector_norm_sq_iter(v)
912}
913
914/// Computes the Euclidean (L2) norm of a slice of scalars using
915/// **scaling + Neumaier compensated summation**.
916///
917/// Returns ‖**v**‖₂ = √(Σᵢ |vᵢ|²), where |·| denotes the modulus.
918///
919/// # Numerical Accuracy
920///
921/// This is an improved variant of [`vector_norm_l2`] that combines two complementary
922/// techniques:
923///
924/// 1. **Scaling** (same as [`vector_norm_l2`]): tracks `scale = max |vᵢ|` and
925/// accumulates `Σ (|vᵢ|/scale)²`, so that no individual term overflows or
926/// underflows before being squared.
927///
928/// 2. **Neumaier compensated summation**: replaces the plain `sumsq += r²` with
929/// Kahan/Neumaier compensation, capturing low-order bits that are silently
930/// discarded in the naive loop. The final result is `scale × √(neumaier.result())`.
931///
932/// **When rescaling occurs** (a new maximum is found mid-iteration), the
933/// accumulated Neumaier sum is multiplied by `r²` and the compensation term is
934/// reset to zero. Rescaling is rare in practice, so the Neumaier accuracy benefit
935/// is preserved for the bulk of the summation.
936///
937/// # Special Cases
938///
939/// | Input | Result |
940/// |-------|--------|
941/// | all-zero vector | `0.0` |
942/// | single dominant component | full Neumaier accuracy for the remaining terms |
943///
944/// # Examples
945///
946/// ```
947/// use num_valid::algorithms::vector_norms::vector_norm_l2_neumaier;
948///
949/// // 3-4-5 right triangle: ‖(3, 4)‖₂ == 5
950/// let v = [3.0_f64, 4.0];
951/// assert!((*vector_norm_l2_neumaier(&v).as_ref() - 5.0).abs() < 1e-12);
952///
953/// // Zero vector
954/// let z = [0.0_f64; 3];
955/// assert_eq!(*vector_norm_l2_neumaier(&z).as_ref(), 0.0);
956///
957/// // Recovers small squared terms lost by naive accumulation:
958/// // v = [1.0, 1e-9 × 10_000]; true ‖v‖₂ = sqrt(1.0 + 1e-14) ≈ 1.0 + 5e-15
959/// const N: usize = 10_000;
960/// let mut v_large: [f64; N + 1] = [1e-9; N + 1];
961/// v_large[0] = 1.0;
962/// let result = *vector_norm_l2_neumaier(&v_large).as_ref();
963/// let expected = (1.0_f64 + N as f64 * 1e-18).sqrt();
964/// assert!((result - expected).abs() < 1e-15 * expected);
965/// ```
966#[inline(always)]
967pub fn vector_norm_l2_neumaier<ScalarType: FpScalar>(
968 v: &[ScalarType],
969) -> NonNegativeRealScalar<ScalarType::RealType> {
970 L2NormNeumaierSum::<ScalarType>::compute_vector_norm(v)
971}
972
973/// Computes the Euclidean (L2) norm from an owned-item iterator using scaling
974/// plus Neumaier compensated summation.
975///
976/// This is the iterator counterpart of [`vector_norm_l2_neumaier`].
977#[inline(always)]
978pub fn vector_norm_l2_neumaier_iter<ScalarType, I>(
979 v: I,
980) -> NonNegativeRealScalar<ScalarType::RealType>
981where
982 ScalarType: FpScalar,
983 I: IntoIterator<Item = ScalarType>,
984{
985 L2NormNeumaierSum::<ScalarType>::compute_vector_norm_iter(v)
986}
987
988/// Computes the Manhattan (L1) norm of a slice of scalars.
989///
990/// Returns ‖**v**‖₁ = Σᵢ |vᵢ|, where |·| denotes the modulus.
991///
992/// # Numerical Accuracy
993///
994/// The absolute values are summed with a **naive `+=` loop**.
995/// For typical inputs this is exact up to the rounding of each individual
996/// `abs()` call. When components span many orders of magnitude a large
997/// running total may absorb small addends (absorption error), silently
998/// discarding their low-order bits. Use [`vector_norm_l1_neumaier`] when that
999/// extra precision matters.
1000///
1001/// Equivalent to calling [`vector_norm_l1_with_accumulator`] with [`NaiveSum`].
1002///
1003/// # Examples
1004///
1005/// ```
1006/// use num_valid::algorithms::vector_norms::vector_norm_l1;
1007///
1008/// // L1 norm of (3, 4) is 3 + 4 = 7
1009/// let v = [3.0_f64, 4.0];
1010/// assert_eq!(*vector_norm_l1(&v).as_ref(), 7.0);
1011///
1012/// // Zero vector
1013/// let z = [0.0_f64; 3];
1014/// assert_eq!(*vector_norm_l1(&z).as_ref(), 0.0);
1015///
1016/// // Unit axis vector has L1 norm 1
1017/// let e1 = [1.0_f64, 0.0, 0.0];
1018/// assert_eq!(*vector_norm_l1(&e1).as_ref(), 1.0);
1019/// ```
1020///
1021/// For complex scalars the L1 norm sums the complex moduli:
1022///
1023/// ```
1024/// use num_valid::algorithms::vector_norms::vector_norm_l1;
1025/// use num_valid::ComplexNative64StrictFinite;
1026/// use num::Complex;
1027/// use try_create::TryNew;
1028///
1029/// // ‖[3+0i, 0+4i]‖₁ = |3+0i| + |0+4i| = 3 + 4 = 7
1030/// let v = [
1031/// ComplexNative64StrictFinite::try_new(Complex::new(3.0_f64, 0.0)).unwrap(),
1032/// ComplexNative64StrictFinite::try_new(Complex::new(0.0_f64, 4.0)).unwrap(),
1033/// ];
1034/// let result = vector_norm_l1(&v).into_inner();
1035/// assert!((*result.as_ref() - 7.0_f64).abs() < 1e-12);
1036/// ```
1037#[inline(always)]
1038pub fn vector_norm_l1<ScalarType: FpScalar>(
1039 a: &[ScalarType],
1040) -> NonNegativeRealScalar<ScalarType::RealType> {
1041 L1NormNaiveSum::<ScalarType>::compute_vector_norm(a)
1042}
1043
1044/// Computes the Manhattan (L1) norm from an owned-item iterator.
1045///
1046/// This is the iterator counterpart of [`vector_norm_l1`].
1047#[inline(always)]
1048pub fn vector_norm_l1_iter<ScalarType, I>(a: I) -> NonNegativeRealScalar<ScalarType::RealType>
1049where
1050 ScalarType: FpScalar,
1051 I: IntoIterator<Item = ScalarType>,
1052{
1053 L1NormNaiveSum::<ScalarType>::compute_vector_norm_iter(a)
1054}
1055
1056/// Computes the Manhattan (L1) norm of a slice of scalars using
1057/// **Neumaier compensated summation**.
1058///
1059/// Returns ‖**v**‖₁ = Σᵢ |vᵢ|, where |·| denotes the modulus.
1060///
1061/// # Numerical Accuracy
1062///
1063/// The absolute values are accumulated using the **Neumaier compensated
1064/// summation** algorithm, which corrects for round-off errors that can
1065/// accumulate in a naïve loop. For typical coordinate values the improvement
1066/// is small, but for vectors whose components span many orders of magnitude
1067/// the compensation captures the round-off lost when a small addend is absorbed
1068/// by a much larger running total (absorption error). Catastrophic cancellation
1069/// — the loss of significant bits by subtracting nearly-equal numbers — cannot
1070/// occur in a sum of absolute values, so the benefit here is specifically the
1071/// recovery of those absorbed low-order bits.
1072///
1073/// # Examples
1074///
1075/// ```
1076/// use num_valid::algorithms::vector_norms::vector_norm_l1_neumaier;
1077///
1078/// // L1 norm of (3, 4) is 3 + 4 = 7
1079/// let v = [3.0_f64, 4.0];
1080/// assert_eq!(*vector_norm_l1_neumaier(&v).as_ref(), 7.0);
1081///
1082/// // Zero vector
1083/// let z = [0.0_f64; 3];
1084/// assert_eq!(*vector_norm_l1_neumaier(&z).as_ref(), 0.0);
1085///
1086/// // Unit axis vector has L1 norm 1
1087/// let e1 = [1.0_f64, 0.0, 0.0];
1088/// assert_eq!(*vector_norm_l1_neumaier(&e1).as_ref(), 1.0);
1089/// ```
1090#[inline(always)]
1091pub fn vector_norm_l1_neumaier<ScalarType: FpScalar>(
1092 a: &[ScalarType],
1093) -> NonNegativeRealScalar<ScalarType::RealType> {
1094 L1NormNeumaierSum::<ScalarType>::compute_vector_norm(a)
1095}
1096
1097/// Computes the Manhattan (L1) norm from an owned-item iterator using
1098/// Neumaier compensated summation.
1099///
1100/// This is the iterator counterpart of [`vector_norm_l1_neumaier`].
1101#[inline(always)]
1102pub fn vector_norm_l1_neumaier_iter<ScalarType, I>(
1103 a: I,
1104) -> NonNegativeRealScalar<ScalarType::RealType>
1105where
1106 ScalarType: FpScalar,
1107 I: IntoIterator<Item = ScalarType>,
1108{
1109 L1NormNeumaierSum::<ScalarType>::compute_vector_norm_iter(a)
1110}
1111
1112/// Computes the Chebyshev (L∞) norm of a slice of scalars.
1113///
1114/// Returns ‖**v**‖∞ = maxᵢ |vᵢ|, where |·| denotes the modulus.
1115///
1116/// # Algorithm
1117///
1118/// A single linear scan keeps the running maximum absolute value. The result
1119/// requires no square root and no compensation — it is numerically exact up to
1120/// a single call to `abs()` on each component.
1121///
1122/// # Examples
1123///
1124/// ```
1125/// use num_valid::algorithms::vector_norms::vector_norm_linf;
1126///
1127/// // L∞ norm of (3, 4) = max(3, 4) = 4
1128/// let v = [3.0_f64, 4.0];
1129/// assert_eq!(*vector_norm_linf(&v).as_ref(), 4.0);
1130///
1131/// // Zero vector
1132/// let z = [0.0_f64; 3];
1133/// assert_eq!(*vector_norm_linf(&z).as_ref(), 0.0);
1134///
1135/// // Unit axis vector has L∞ norm 1
1136/// let e1 = [1.0_f64, 0.0, 0.0];
1137/// assert_eq!(*vector_norm_linf(&e1).as_ref(), 1.0);
1138/// ```
1139///
1140/// For complex scalars `|·|` is the complex modulus:
1141///
1142/// ```
1143/// use num_valid::algorithms::vector_norms::vector_norm_linf;
1144/// use num_valid::ComplexNative64StrictFinite;
1145/// use num::Complex;
1146/// use try_create::TryNew;
1147///
1148/// // ‖[3+0i, 0+4i]‖∞ = max(|3+0i|, |0+4i|) = max(3, 4) = 4
1149/// let v = [
1150/// ComplexNative64StrictFinite::try_new(Complex::new(3.0_f64, 0.0)).unwrap(),
1151/// ComplexNative64StrictFinite::try_new(Complex::new(0.0_f64, 4.0)).unwrap(),
1152/// ];
1153/// let result = vector_norm_linf(&v).into_inner();
1154/// assert!((*result.as_ref() - 4.0_f64).abs() < 1e-12);
1155/// ```
1156#[inline(always)]
1157pub fn vector_norm_linf<ScalarType: FpScalar>(
1158 a: &[ScalarType],
1159) -> NonNegativeRealScalar<ScalarType::RealType> {
1160 LinfNorm::<()>::compute_vector_norm(a)
1161}
1162
1163/// Computes the Chebyshev (L∞) norm of a slice of scalars in parallel.
1164///
1165/// Returns ‖**v**‖∞ = maxᵢ |vᵢ|, where |·| denotes the modulus.
1166///
1167/// This is the parallel counterpart of [`vector_norm_linf`].
1168///
1169/// # Examples
1170///
1171/// ```
1172/// use num_valid::algorithms::vector_norms::vector_norm_linf_par;
1173///
1174/// let v = [3.0_f64, 4.0, -2.0];
1175/// assert_eq!(*vector_norm_linf_par(&v).as_ref(), 4.0);
1176/// ```
1177#[inline(always)]
1178pub fn vector_norm_linf_par<ScalarType: FpScalar>(
1179 a: &[ScalarType],
1180) -> NonNegativeRealScalar<ScalarType::RealType> {
1181 LinfNorm::<()>::compute_vector_norm_par(a)
1182}
1183
1184/// Computes the Chebyshev (L∞) norm from an owned-item iterator.
1185///
1186/// This is the iterator counterpart of [`vector_norm_linf`].
1187#[inline(always)]
1188pub fn vector_norm_linf_iter<ScalarType: FpScalar, I>(
1189 a: I,
1190) -> NonNegativeRealScalar<ScalarType::RealType>
1191where
1192 I: IntoIterator<Item = ScalarType>,
1193{
1194 LinfNorm::<()>::compute_vector_norm_iter(a)
1195}
1196
1197/// Computes the infinity norm (maximum absolute value) of a parallel iterator of values.
1198///
1199/// This function computes the infinity norm, which is the maximum absolute value of the elements
1200/// in the given parallel iterator. It uses the `Abs` trait to compute the absolute value of each
1201/// element and the `Max` trait to find the maximum value.
1202///
1203/// This is the parallel iterator counterpart of [`vector_norm_linf_iter`].
1204///
1205/// # Examples
1206///
1207/// ```
1208/// use num_valid::algorithms::vector_norms::vector_norm_linf_par_iter;
1209///
1210/// let v = vec![3.0_f64, 4.0, -2.0];
1211/// assert_eq!(*vector_norm_linf_par_iter(v).as_ref(), 4.0);
1212/// ```
1213#[inline(always)]
1214pub fn vector_norm_linf_par_iter<ScalarType: FpScalar, I>(
1215 a: I,
1216) -> NonNegativeRealScalar<ScalarType::RealType>
1217where
1218 I: IntoParallelIterator<Item = ScalarType>,
1219{
1220 LinfNorm::<()>::compute_vector_norm_par_iter(a)
1221}
1222
1223// ================================================================================================
1224// Tests
1225// ================================================================================================
1226
1227#[cfg(test)]
1228mod tests {
1229 use super::*;
1230 use crate::RealScalar;
1231
1232 mod vector_norm_l2_tests {
1233 use super::*;
1234
1235 const EPS: f64 = 1e-12;
1236
1237 /// 3-4-5: norm of (3, 4) is 5.
1238 #[test]
1239 fn three_four_five() {
1240 let v = [3.0_f64, 4.0];
1241 assert!((*vector_norm_l2(&v).as_ref() - 5.0).abs() < EPS);
1242 }
1243
1244 /// Zero vector has norm 0.
1245 #[test]
1246 fn zero_vector() {
1247 let v = [0.0_f64; 4];
1248 assert_eq!(*vector_norm_l2(&v).as_ref(), 0.0);
1249 }
1250
1251 /// 1-D: norm is |v[0]|.
1252 #[test]
1253 fn one_dimension_positive() {
1254 let v = [5.0_f64];
1255 assert!((*vector_norm_l2(&v).as_ref() - 5.0).abs() < EPS);
1256 }
1257
1258 /// 1-D: norm of negative scalar is its absolute value.
1259 #[test]
1260 fn one_dimension_negative() {
1261 let v = [-7.0_f64];
1262 assert!((*vector_norm_l2(&v).as_ref() - 7.0).abs() < EPS);
1263 }
1264
1265 /// Unit axis vectors always have norm 1.
1266 #[test]
1267 fn unit_axis_vectors_3d() {
1268 for axis in 0..3 {
1269 let mut e = [0.0_f64; 3];
1270 e[axis] = 1.0;
1271 assert!((*vector_norm_l2(&e).as_ref() - 1.0).abs() < EPS);
1272 }
1273 }
1274
1275 /// Known 3-D norm: sqrt(1² + 2² + 2²) = 3.
1276 #[test]
1277 fn known_3d_norm() {
1278 let v = [1.0_f64, 2.0, 2.0];
1279 assert!((*vector_norm_l2(&v).as_ref() - 3.0).abs() < EPS);
1280 }
1281
1282 /// Negating a vector preserves its norm.
1283 #[test]
1284 fn negation_preserves_norm() {
1285 let v = [3.0_f64, -4.0, 5.0];
1286 let neg_v = [-3.0_f64, 4.0, -5.0];
1287 assert_eq!(vector_norm_l2(&v), vector_norm_l2(&neg_v));
1288 }
1289
1290 /// Numerical stability: component near f64::MAX must not overflow.
1291 #[test]
1292 fn numerical_stability_large_values() {
1293 let huge = f64::MAX / 2.0;
1294 let v = [huge, 0.0_f64];
1295 let n = *vector_norm_l2(&v).as_ref();
1296 assert!(n.is_finite(), "norm should be finite, got {n}");
1297 assert!((n / huge - 1.0).abs() < 1e-10);
1298 }
1299
1300 /// Numerical stability: subnormal component must not underflow to zero.
1301 #[test]
1302 fn numerical_stability_tiny_values() {
1303 let tiny = f64::MIN_POSITIVE;
1304 let v = [tiny, 0.0_f64];
1305 let n = *vector_norm_l2(&v).as_ref();
1306 assert!(n.is_finite());
1307 assert!(n > 0.0);
1308 }
1309 }
1310
1311 mod vector_norm_l2_sq_tests {
1312 use super::*;
1313 use crate::RealNative64StrictFinite;
1314
1315 const EPS: f64 = 1e-16;
1316
1317 /// 3-4-5: vector_norm_l2_sq([3, 4]) == 3² + 4² == 25.
1318 #[test]
1319 fn three_four_five() {
1320 let v = [3.0_f64, 4.0];
1321 assert_eq!(*vector_norm_l2_sq(&v).as_ref(), 25.0);
1322 }
1323
1324 /// Zero vector: squared norm is 0.
1325 #[test]
1326 fn zero_vector() {
1327 let v = [0.0_f64; 4];
1328 assert_eq!(*vector_norm_l2_sq(&v).as_ref(), 0.0);
1329 }
1330
1331 /// 1-D positive: vector_norm_l2_sq([x]) == x².
1332 #[test]
1333 fn one_dimension_positive() {
1334 let v = [5.0_f64];
1335 assert_eq!(*vector_norm_l2_sq(&v).as_ref(), 25.0);
1336 }
1337
1338 /// 1-D negative: vector_norm_l2_sq([x]) == x² (sign disappears).
1339 #[test]
1340 fn one_dimension_negative() {
1341 let v = [-7.0_f64];
1342 assert_eq!(*vector_norm_l2_sq(&v).as_ref(), 49.0);
1343 }
1344
1345 /// Unit axis vectors always have squared norm 1.
1346 #[test]
1347 fn unit_axis_vectors_3d() {
1348 for axis in 0..3 {
1349 let mut e = [0.0_f64; 3];
1350 e[axis] = 1.0;
1351 assert_eq!(*vector_norm_l2_sq(&e).as_ref(), 1.0);
1352 }
1353 }
1354
1355 /// Known 3-D: 1² + 2² + 2² = 1 + 4 + 4 = 9.
1356 #[test]
1357 fn known_3d_vector_norm_sq() {
1358 let v = [1.0_f64, 2.0, 2.0];
1359 assert_eq!(*vector_norm_l2_sq(&v).as_ref(), 9.0);
1360 }
1361
1362 /// Negating a vector preserves its squared norm.
1363 #[test]
1364 fn negation_preserves_vector_norm_sq() {
1365 let v = [3.0_f64, -4.0, 5.0];
1366 let neg_v = [-3.0_f64, 4.0, -5.0];
1367 assert_eq!(vector_norm_l2_sq(&v), vector_norm_l2_sq(&neg_v));
1368 }
1369
1370 /// vector_norm_l2_sq == vector_norm_l2² (consistency with vector_norm_l2).
1371 #[test]
1372 fn consistent_with_vector_norm_l2() {
1373 let v = [2.0_f64, 3.0, 6.0];
1374 let sq = *vector_norm_l2_sq(&v).as_ref();
1375 let n = *vector_norm_l2(&v).as_ref();
1376 assert_eq!(sq, n * n);
1377 }
1378
1379 /// Numerical stability: component below sqrt(f64::MAX) ≈ 1.34e154 must not overflow.
1380 /// Note: vector_norm_l2_sq computes scale² internally; values above sqrt(f64::MAX) will overflow.
1381 #[test]
1382 fn numerical_stability_large_values() {
1383 // Safe upper bound: scale must satisfy scale² < f64::MAX
1384 let large = 1.0e150_f64;
1385 let v = [large, 0.0_f64];
1386 let sq = *vector_norm_l2_sq(&v).as_ref();
1387 assert!(sq.is_finite(), "squared norm should be finite, got {sq}");
1388 assert_eq!(sq / (large * large), 1.0);
1389 }
1390
1391 /// Numerical stability: component above sqrt(f64::MIN_POSITIVE) ≈ 1.05e-154 must not underflow.
1392 /// Note: vector_norm_l2_sq computes scale² internally; values below sqrt(f64::MIN_POSITIVE) will underflow to 0.
1393 #[test]
1394 fn numerical_stability_tiny_values() {
1395 // Safe lower bound: scale must satisfy scale² > 0 (no underflow)
1396 let small = 1.0e-100_f64;
1397 let v = [small, 0.0_f64];
1398 let sq = *vector_norm_l2_sq(&v).as_ref();
1399 assert!(sq.is_finite());
1400 assert!(sq > 0.0);
1401 }
1402
1403 /// Overflow is expected when the mathematically correct squared norm is
1404 /// not representable (e.g. f64::MAX squared).
1405 ///
1406 /// We use strict validated scalars so overflow is rejected in both debug
1407 /// and release builds.
1408 #[test]
1409 #[should_panic]
1410 fn overflow_panics_slice_api() {
1411 let v = [RealNative64StrictFinite::from_f64(f64::MAX)];
1412 let _ = vector_norm_l2_sq(&v);
1413 }
1414
1415 /// Iterator API must fail in the same way when final scale² overflows.
1416 #[test]
1417 #[should_panic]
1418 fn overflow_panics_iter_api() {
1419 let v = vec![RealNative64StrictFinite::from_f64(f64::MAX)];
1420 let _ = vector_norm_l2_sq_iter(v);
1421 }
1422
1423 /// Accumulator-driven API must also panic on non-finite final output.
1424 #[test]
1425 #[should_panic]
1426 fn overflow_panics_accumulator_api() {
1427 let v = [RealNative64StrictFinite::from_f64(f64::MAX)];
1428 let _ = vector_norm_l2_sq_with_accumulator::<_, NaiveSum<_>>(&v);
1429 }
1430
1431 /// vector_norm_l2_sq is always ≥ vector_norm_linf² (since ‖v‖∞ ≤ ‖v‖₂).
1432 #[test]
1433 fn geq_vector_norm_linf_squared() {
1434 let v = [3.0_f64, -4.0, 5.0];
1435 let sq = *vector_norm_l2_sq(&v).as_ref();
1436 let linf = *vector_norm_linf(&v).as_ref();
1437 assert!(sq >= linf * linf - EPS);
1438 }
1439
1440 /// Documents the inaccuracy of naive accumulation for small squared terms.
1441 ///
1442 /// v = [1.0, δ, δ, …, δ] with δ = 1e-9, N = 10_000.
1443 /// True ‖v‖₂² = 1.0 + N·δ² = 1.0 + 1e-14.
1444 ///
1445 /// The scaled algorithm keeps `sumsq ≈ 1.0` throughout; each δ² = 1e-18
1446 /// is well below the floating-point precision of 1.0 (ε ≈ 2.2e-16), so
1447 /// `sumsq += δ²` rounds back to `sumsq` unchanged and every small term is
1448 /// silently discarded. The final result equals exactly 1.0, an error of
1449 /// 1e-14 — four orders of magnitude above machine epsilon.
1450 ///
1451 /// With Neumaier compensated summation the δ² corrections would be captured
1452 /// in the running compensation term and the result would satisfy
1453 /// `(result - expected).abs() < ε·expected ≈ 2.2e-16`.
1454 #[test]
1455 fn naive_accumulation_loses_small_squared_terms() {
1456 const N: usize = 10_000;
1457 let mut v = [1e-9_f64; N + 1];
1458 v[0] = 1.0;
1459
1460 let result = *vector_norm_l2_sq(&v).as_ref();
1461 let expected = 1.0_f64 + N as f64 * 1e-18; // = 1.0 + 1e-14
1462
1463 // Current: every δ² = 1e-18 < ε is lost → result collapses to 1.0.
1464 assert_eq!(result, 1.0_f64);
1465
1466 // The relative error equals the entire small-terms contribution (~1e-14),
1467 // far larger than machine epsilon (~2.2e-16).
1468 let relative_error = (result - expected).abs() / expected;
1469 assert!(
1470 relative_error > 1e-15,
1471 "relative error {relative_error:.2e} should be significant \
1472 (would be ~ε ≈ 2.2e-16 with Neumaier-sum)"
1473 );
1474
1475 // With Neumaier the following assertion would hold instead:
1476 // assert!((result - expected).abs() < 1e-15 * expected);
1477 }
1478 }
1479
1480 mod vector_norm_l2_sq_neumaier_tests {
1481 use super::*;
1482
1483 const EPS: f64 = f64::EPSILON;
1484
1485 /// 3-4-5: vector_norm_l2_sq_neumaier([3, 4]) == 3² + 4² == 25.
1486 #[test]
1487 fn three_four_five() {
1488 let v = [3.0_f64, 4.0];
1489 assert_eq!(*vector_norm_l2_sq_neumaier(&v).as_ref(), 25.0);
1490 }
1491
1492 /// Zero vector → 0.
1493 #[test]
1494 fn zero_vector() {
1495 let v = [0.0_f64; 4];
1496 assert_eq!(*vector_norm_l2_sq_neumaier(&v).as_ref(), 0.0);
1497 }
1498
1499 /// 1-D: vector_norm_l2_sq_neumaier([x]) == x².
1500 #[test]
1501 fn one_dimension() {
1502 let v = [7.0_f64];
1503 assert_eq!(*vector_norm_l2_sq_neumaier(&v).as_ref(), 49.0);
1504 }
1505
1506 /// Matches vector_norm_l2_sq on a generic 3-D vector.
1507 #[test]
1508 fn matches_naive_for_normal_values() {
1509 let v = [1.0_f64, 2.0, 3.0];
1510 let neumaier = *vector_norm_l2_sq_neumaier(&v).as_ref();
1511 let naive = *vector_norm_l2_sq(&v).as_ref();
1512 assert!((neumaier - naive).abs() < EPS);
1513 }
1514
1515 /// Neumaier version recovers small squared terms that naive accumulation loses.
1516 ///
1517 /// v = [1.0, δ × N] where δ = 1e-9. True ‖v‖₂² = 1.0 + N·δ² = 1.0 + 1e-14.
1518 /// Naive: every δ² = 1e-18 < ε is silently dropped → result = 1.0.
1519 /// Neumaier: compensation accumulates all N·δ² → result ≈ 1.0 + 1e-14.
1520 #[test]
1521 fn recovers_small_squared_terms() {
1522 const N: usize = 10_000;
1523 let mut v = [1e-9_f64; N + 1];
1524 v[0] = 1.0;
1525
1526 let result = *vector_norm_l2_sq_neumaier(&v).as_ref();
1527 let expected = 1.0_f64 + N as f64 * 1e-18; // = 1.0 + 1e-14
1528
1529 // The Neumaier-compensated result must be within machine-epsilon of expected.
1530 let relative_error = (result - expected).abs() / expected;
1531 assert!(
1532 relative_error < EPS,
1533 "relative error {relative_error:.2e} should be tiny with Neumaier-sum"
1534 );
1535
1536 // Demonstrate the contrast with naive accumulation.
1537 let naive = *vector_norm_l2_sq(&v).as_ref();
1538 assert_eq!(
1539 naive, 1.0_f64,
1540 "naive should collapse to 1.0 (known limitation)"
1541 );
1542 assert!(
1543 result > naive,
1544 "Neumaier result {result} should exceed naive {naive}"
1545 );
1546 }
1547
1548 /// Numerical stability: large-valued input does not overflow.
1549 #[test]
1550 fn numerical_stability_large_values() {
1551 let v = [1e150_f64, 1e150, 1e150];
1552 let result = *vector_norm_l2_sq_neumaier(&v).as_ref();
1553 let expected = 3.0 * 1e300_f64;
1554 let relative_error = (result - expected).abs() / expected;
1555 assert!(relative_error < EPS, "large values: {relative_error:.2e}");
1556 }
1557
1558 /// Numerical stability: tiny-valued input does not underflow to zero.
1559 #[test]
1560 fn numerical_stability_tiny_values() {
1561 let v = [1e-100_f64, 1e-100, 1e-100];
1562 let result = *vector_norm_l2_sq_neumaier(&v).as_ref();
1563 let expected = 3.0e-200_f64;
1564 let relative_error = (result - expected).abs() / expected;
1565 assert!(relative_error < EPS, "tiny values: {relative_error:.2e}");
1566 }
1567
1568 /// Consistent with vector_norm_l2: vector_norm_l2_sq_neumaier(v) ≈ vector_norm_l2(v)².
1569 #[test]
1570 fn consistent_with_vector_norm_l2() {
1571 let v = [1.5_f64, 2.5, 3.5, 4.5];
1572 let sq = *vector_norm_l2_sq_neumaier(&v).as_ref();
1573 let l2 = *vector_norm_l2(&v).as_ref();
1574 let err = (sq - l2 * l2).abs();
1575 println!("sq - l2² = {}", err);
1576 assert_eq!(err, 0.7105427357601002e-14);
1577 }
1578 }
1579
1580 mod vector_norm_l2_neumaier_tests {
1581 use super::*;
1582
1583 const EPS: f64 = f64::EPSILON;
1584
1585 /// 3-4-5 right triangle: ‖(3, 4)‖₂ == 5.
1586 #[test]
1587 fn three_four_five() {
1588 let v = [3.0_f64, 4.0];
1589 assert_eq!(*vector_norm_l2_neumaier(&v).as_ref(), 5.0);
1590 }
1591
1592 /// Zero vector → 0.
1593 #[test]
1594 fn zero_vector() {
1595 let v = [0.0_f64; 4];
1596 assert_eq!(*vector_norm_l2_neumaier(&v).as_ref(), 0.0);
1597 }
1598
1599 /// 1-D: ‖[x]‖₂ == |x|.
1600 #[test]
1601 fn one_dimension() {
1602 let v = [7.0_f64];
1603 assert_eq!(*vector_norm_l2_neumaier(&v).as_ref(), 7.0);
1604 }
1605
1606 /// Matches vector_norm_l2 on ordinary values (no dominant small-term cancellation).
1607 #[test]
1608 fn matches_naive_for_normal_values() {
1609 let v = [1.0_f64, 2.0, 3.0];
1610 let neumaier = *vector_norm_l2_neumaier(&v).as_ref();
1611 let naive = *vector_norm_l2(&v).as_ref();
1612 assert!((neumaier - naive).abs() < EPS);
1613 }
1614
1615 /// Neumaier version recovers small squared terms that naive accumulation loses.
1616 ///
1617 /// v = [1.0, δ × N] where δ = 1e-9. True ‖v‖₂ = √(1.0 + N·δ²) = √(1.0 + 1e-14).
1618 /// Naive: sumsq collapses to 1.0 → norm = 1.0 (all small terms lost).
1619 /// Neumaier: sumsq ≈ 1.0 + 1e-14 → norm ≈ √(1.0 + 1e-14) ≈ 1.0 + 5e-15.
1620 #[test]
1621 fn recovers_small_squared_terms() {
1622 const N: usize = 10_000;
1623 let mut v = [1e-9_f64; N + 1];
1624 v[0] = 1.0;
1625
1626 let result = *vector_norm_l2_neumaier(&v).as_ref();
1627 let expected = (1.0_f64 + N as f64 * 1e-18).sqrt();
1628
1629 let relative_error = (result - expected).abs() / expected;
1630 assert!(
1631 relative_error < 1e-12,
1632 "relative error {relative_error:.2e} should be tiny with Neumaier-sum"
1633 );
1634
1635 // Contrast with naive: vector_norm_l2 loses the small terms, giving exactly 1.0.
1636 let naive = *vector_norm_l2(&v).as_ref();
1637 assert_eq!(
1638 naive, 1.0_f64,
1639 "naive should collapse to 1.0 (known limitation)"
1640 );
1641 assert!(
1642 result > naive,
1643 "Neumaier result {result} should exceed naive {naive}"
1644 );
1645 }
1646
1647 /// Numerical stability: large-valued input does not overflow.
1648 #[test]
1649 fn numerical_stability_large_values() {
1650 let v = [1e150_f64, 1e150, 1e150];
1651 let result = *vector_norm_l2_neumaier(&v).as_ref();
1652 let expected = (3.0_f64).sqrt() * 1e150;
1653 let relative_error = (result - expected).abs() / expected;
1654 assert!(relative_error < EPS, "large values: {relative_error:.2e}");
1655 }
1656
1657 /// Numerical stability: tiny-valued input does not underflow to zero.
1658 #[test]
1659 fn numerical_stability_tiny_values() {
1660 let v = [1e-150_f64, 1e-150, 1e-150];
1661 let result = *vector_norm_l2_neumaier(&v).as_ref();
1662 let expected = (3.0_f64).sqrt() * 1e-150;
1663 let relative_error = (result - expected).abs() / expected;
1664 assert!(relative_error < EPS, "tiny values: {relative_error:.2e}");
1665 }
1666
1667 /// Consistent with vector_norm_l2_sq_neumaier: vector_norm_l2_neumaier(v)² ≈ vector_norm_l2_sq_neumaier(v).
1668 #[test]
1669 fn consistent_with_vector_norm_l2_sq_neumaier() {
1670 let v = [1.5_f64, 2.5, 3.5, 4.5];
1671 let norm = *vector_norm_l2_neumaier(&v).as_ref();
1672 let sq = *vector_norm_l2_sq_neumaier(&v).as_ref();
1673 assert!(
1674 (norm * norm - sq).abs() < 100. * EPS,
1675 "norm²={} sq={}",
1676 norm * norm,
1677 sq
1678 );
1679 }
1680
1681 /// Unit-axis vectors have norm 1.
1682 #[test]
1683 fn unit_axis_vectors() {
1684 let e1 = [1.0_f64, 0.0, 0.0];
1685 let e2 = [0.0_f64, 1.0, 0.0];
1686 let e3 = [0.0_f64, 0.0, 1.0];
1687 for e in [&e1, &e2, &e3] {
1688 let n = *vector_norm_l2_neumaier(e).as_ref();
1689 assert!((n - 1.0).abs() < EPS, "unit axis: {n}");
1690 }
1691 }
1692 }
1693
1694 mod vector_norm_l1_tests {
1695 use super::*;
1696
1697 const EPS: f64 = 1e-12;
1698
1699 /// L1 norm of (3, 4) is 3 + 4 = 7.
1700 #[test]
1701 fn three_plus_four() {
1702 let v = [3.0_f64, 4.0];
1703 assert_eq!(*vector_norm_l1(&v).as_ref(), 7.0);
1704 }
1705
1706 /// Zero vector has L1 norm 0.
1707 #[test]
1708 fn zero_vector() {
1709 let v = [0.0_f64; 4];
1710 assert_eq!(*vector_norm_l1(&v).as_ref(), 0.0);
1711 }
1712
1713 /// 1-D: norm is |v[0]|.
1714 #[test]
1715 fn one_dimension_positive() {
1716 let v = [5.0_f64];
1717 assert!((*vector_norm_l1(&v).as_ref() - 5.0).abs() < EPS);
1718 }
1719
1720 /// 1-D: norm of negative scalar is its absolute value.
1721 #[test]
1722 fn one_dimension_negative() {
1723 let v = [-7.0_f64];
1724 assert!((*vector_norm_l1(&v).as_ref() - 7.0).abs() < EPS);
1725 }
1726
1727 /// Unit axis vectors always have L1 norm 1.
1728 #[test]
1729 fn unit_axis_vectors_3d() {
1730 for axis in 0..3 {
1731 let mut e = [0.0_f64; 3];
1732 e[axis] = 1.0;
1733 assert_eq!(*vector_norm_l1(&e).as_ref(), 1.0);
1734 }
1735 }
1736
1737 /// Known 3-D: |1| + |2| + |2| = 5.
1738 #[test]
1739 fn known_3d_norm() {
1740 let v = [1.0_f64, 2.0, 2.0];
1741 assert!((*vector_norm_l1(&v).as_ref() - 5.0).abs() < EPS);
1742 }
1743
1744 /// Negating a vector preserves its L1 norm.
1745 #[test]
1746 fn negation_preserves_norm() {
1747 let v = [3.0_f64, -4.0, 5.0];
1748 let neg_v = [-3.0_f64, 4.0, -5.0];
1749 assert_eq!(vector_norm_l1(&v), vector_norm_l1(&neg_v));
1750 }
1751
1752 /// L1 norm is the sum of absolute values: mixed signs.
1753 #[test]
1754 fn mixed_signs() {
1755 let v = [-1.0_f64, 2.0, -3.0, 4.0];
1756 assert!((*vector_norm_l1(&v).as_ref() - 10.0).abs() < EPS);
1757 }
1758
1759 /// L1 ≥ L2 for any vector.
1760 #[test]
1761 fn l1_geq_l2() {
1762 let v = [3.0_f64, -4.0, 5.0];
1763 assert!(vector_norm_l1(&v).as_ref() >= vector_norm_l2(&v).as_ref());
1764 }
1765
1766 /// Numerical stability: large components must not overflow.
1767 #[test]
1768 fn numerical_stability_large_values() {
1769 let huge = f64::MAX / 4.0;
1770 // Two equal components: L1 norm = 2 × huge
1771 let v = [huge, huge];
1772 let n = *vector_norm_l1(&v).as_ref();
1773 assert!(n.is_finite(), "norm should be finite, got {n}");
1774 assert!((n / (2.0 * huge) - 1.0).abs() < 1e-10);
1775 }
1776
1777 /// Numerical stability: subnormal component must not underflow to zero.
1778 #[test]
1779 fn numerical_stability_tiny_values() {
1780 let tiny = f64::MIN_POSITIVE;
1781 let v = [tiny, 0.0_f64];
1782 let n = *vector_norm_l1(&v).as_ref();
1783 assert!(n.is_finite());
1784 assert!(n > 0.0);
1785 }
1786 }
1787
1788 mod vector_norm_l1_neumaier_tests {
1789 use super::*;
1790
1791 const EPS: f64 = 1e-12;
1792
1793 /// L1 norm of (3, 4) is 3 + 4 = 7.
1794 #[test]
1795 fn three_plus_four() {
1796 let v = [3.0_f64, 4.0];
1797 assert_eq!(*vector_norm_l1_neumaier(&v).as_ref(), 7.0);
1798 }
1799
1800 /// Zero vector has L1 norm 0.
1801 #[test]
1802 fn zero_vector() {
1803 let v = [0.0_f64; 4];
1804 assert_eq!(*vector_norm_l1_neumaier(&v).as_ref(), 0.0);
1805 }
1806
1807 /// 1-D: norm is |v[0]|.
1808 #[test]
1809 fn one_dimension_positive() {
1810 let v = [5.0_f64];
1811 assert!((*vector_norm_l1_neumaier(&v).as_ref() - 5.0).abs() < EPS);
1812 }
1813
1814 /// 1-D: norm of negative scalar is its absolute value.
1815 #[test]
1816 fn one_dimension_negative() {
1817 let v = [-7.0_f64];
1818 assert!((*vector_norm_l1_neumaier(&v).as_ref() - 7.0).abs() < EPS);
1819 }
1820
1821 /// Unit axis vectors always have L1 norm 1.
1822 #[test]
1823 fn unit_axis_vectors_3d() {
1824 for axis in 0..3 {
1825 let mut e = [0.0_f64; 3];
1826 e[axis] = 1.0;
1827 assert_eq!(*vector_norm_l1_neumaier(&e).as_ref(), 1.0);
1828 }
1829 }
1830
1831 /// Known 3-D: |1| + |2| + |2| = 5.
1832 #[test]
1833 fn known_3d_norm() {
1834 let v = [1.0_f64, 2.0, 2.0];
1835 assert!((*vector_norm_l1_neumaier(&v).as_ref() - 5.0).abs() < EPS);
1836 }
1837
1838 /// Negating a vector preserves its L1 norm.
1839 #[test]
1840 fn negation_preserves_norm() {
1841 let v = [3.0_f64, -4.0, 5.0];
1842 let neg_v = [-3.0_f64, 4.0, -5.0];
1843 assert_eq!(vector_norm_l1_neumaier(&v), vector_norm_l1_neumaier(&neg_v));
1844 }
1845
1846 /// L1 norm is the sum of absolute values: mixed signs.
1847 #[test]
1848 fn mixed_signs() {
1849 let v = [-1.0_f64, 2.0, -3.0, 4.0];
1850 assert!((*vector_norm_l1_neumaier(&v).as_ref() - 10.0).abs() < EPS);
1851 }
1852
1853 /// L1 ≥ L2 for any vector.
1854 #[test]
1855 fn l1_geq_l2() {
1856 let v = [3.0_f64, -4.0, 5.0];
1857 assert!(vector_norm_l1_neumaier(&v).as_ref() >= vector_norm_l2(&v).as_ref());
1858 }
1859
1860 /// Numerical stability: large components must not overflow.
1861 #[test]
1862 fn numerical_stability_large_values() {
1863 let huge = f64::MAX / 4.0;
1864 // Two equal components: L1 norm = 2 × huge
1865 let v = [huge, huge];
1866 let n = *vector_norm_l1_neumaier(&v).as_ref();
1867 assert!(n.is_finite(), "norm should be finite, got {n}");
1868 assert!((n / (2.0 * huge) - 1.0).abs() < 1e-10);
1869 }
1870
1871 /// Numerical stability: subnormal component must not underflow to zero.
1872 #[test]
1873 fn numerical_stability_tiny_values() {
1874 let tiny = f64::MIN_POSITIVE;
1875 let v = [tiny, 0.0_f64];
1876 let n = *vector_norm_l1_neumaier(&v).as_ref();
1877 assert!(n.is_finite());
1878 assert!(n > 0.0);
1879 }
1880 }
1881
1882 mod vector_norm_linf_tests {
1883 use super::*;
1884
1885 const EPS: f64 = 1e-12;
1886
1887 #[test]
1888 fn parallel_slice_matches_sequential() {
1889 let v = [3.0_f64, -4.0, 5.0, -2.5, 1.0];
1890 assert_eq!(vector_norm_linf_par(&v), vector_norm_linf(&v));
1891 }
1892
1893 #[test]
1894 fn parallel_iter_matches_sequential_iter() {
1895 let v = vec![3.0_f64, -4.0, 5.0, -2.5, 1.0];
1896 let par = vector_norm_linf_par_iter(v.clone());
1897 let seq = vector_norm_linf_iter(v);
1898 assert_eq!(par, seq);
1899 }
1900
1901 /// L∞ norm of (3, 4) = max(3, 4) = 4.
1902 #[test]
1903 fn max_of_three_four() {
1904 let v = [3.0_f64, 4.0];
1905 assert_eq!(*vector_norm_linf(&v).as_ref(), 4.0);
1906 }
1907
1908 /// Zero vector has L∞ norm 0.
1909 #[test]
1910 fn zero_vector() {
1911 let v = [0.0_f64; 4];
1912 assert_eq!(*vector_norm_linf(&v).as_ref(), 0.0);
1913 }
1914
1915 /// 1-D: norm is |v[0]|.
1916 #[test]
1917 fn one_dimension_positive() {
1918 let v = [5.0_f64];
1919 assert!((*vector_norm_linf(&v).as_ref() - 5.0).abs() < EPS);
1920 }
1921
1922 /// 1-D: norm of negative scalar is its absolute value.
1923 #[test]
1924 fn one_dimension_negative() {
1925 let v = [-7.0_f64];
1926 assert!((*vector_norm_linf(&v).as_ref() - 7.0).abs() < EPS);
1927 }
1928
1929 /// Unit axis vectors always have L∞ norm 1.
1930 #[test]
1931 fn unit_axis_vectors_3d() {
1932 for axis in 0..3 {
1933 let mut e = [0.0_f64; 3];
1934 e[axis] = 1.0;
1935 assert_eq!(*vector_norm_linf(&e).as_ref(), 1.0);
1936 }
1937 }
1938
1939 /// Known 3-D: max(|1|, |-2|, |3|) = 3.
1940 #[test]
1941 fn known_3d_norm() {
1942 let v = [1.0_f64, -2.0, 3.0];
1943 assert!((*vector_norm_linf(&v).as_ref() - 3.0).abs() < EPS);
1944 }
1945
1946 /// Negating a vector preserves its L∞ norm.
1947 #[test]
1948 fn negation_preserves_norm() {
1949 let v = [3.0_f64, -4.0, 5.0];
1950 let neg_v = [-3.0_f64, 4.0, -5.0];
1951 assert_eq!(vector_norm_linf(&v), vector_norm_linf(&neg_v));
1952 }
1953
1954 /// The dominant component determines the result.
1955 #[test]
1956 fn dominant_component() {
1957 let v = [1.0_f64, 0.001, 100.0, 0.5];
1958 assert!((*vector_norm_linf(&v).as_ref() - 100.0).abs() < EPS);
1959 }
1960
1961 /// L∞ ≤ L2 for any vector.
1962 #[test]
1963 fn linf_leq_l2() {
1964 let v = [3.0_f64, -4.0, 5.0];
1965 assert!(vector_norm_linf(&v).as_ref() <= vector_norm_l2(&v).as_ref());
1966 }
1967
1968 /// L∞ ≤ L1 for any vector.
1969 #[test]
1970 fn linf_leq_l1() {
1971 let v = [3.0_f64, -4.0, 5.0];
1972 assert!(vector_norm_linf(&v).as_ref() <= vector_norm_l1(&v).as_ref());
1973 }
1974
1975 /// Numerical stability: large component must remain finite.
1976 #[test]
1977 fn numerical_stability_large_values() {
1978 let huge = f64::MAX / 2.0;
1979 let v = [1.0_f64, huge];
1980 let n = *vector_norm_linf(&v).as_ref();
1981 assert!(n.is_finite(), "norm should be finite, got {n}");
1982 assert!((n / huge - 1.0).abs() < 1e-10);
1983 }
1984
1985 /// Numerical stability: subnormal component must not underflow to zero.
1986 #[test]
1987 fn numerical_stability_tiny_values() {
1988 let tiny = f64::MIN_POSITIVE;
1989 let v = [tiny, 0.0_f64];
1990 let n = *vector_norm_linf(&v).as_ref();
1991 assert!(n.is_finite());
1992 assert!(n > 0.0);
1993 }
1994 }
1995
1996 mod other_vector_norm_l2_tests {
1997 use super::*;
1998 use crate::RealNative64StrictFinite;
1999 use approx::assert_relative_eq;
2000
2001 /// Helper to create a vector of validated reals from f64 values
2002 fn vec_f64(vals: &[f64]) -> Vec<RealNative64StrictFinite> {
2003 vals.iter()
2004 .map(|&v| RealNative64StrictFinite::from_f64(v))
2005 .collect()
2006 }
2007
2008 mod basic_cases {
2009 use super::*;
2010
2011 #[test]
2012 fn pythagorean_3_4_5() {
2013 let data = vec_f64(&[3.0, 4.0]);
2014 let norm = vector_norm_l2(&data).into_inner();
2015 assert_relative_eq!(*norm.as_ref(), 5.0, epsilon = 1e-15);
2016 }
2017
2018 #[test]
2019 fn pythagorean_reverse_order() {
2020 // Same result regardless of order
2021 let data = vec_f64(&[4.0, 3.0]);
2022 let norm = vector_norm_l2(&data).into_inner();
2023 assert_relative_eq!(*norm.as_ref(), 5.0, epsilon = 1e-15);
2024 }
2025
2026 #[test]
2027 fn unit_vector_x() {
2028 let data = vec_f64(&[1.0, 0.0, 0.0]);
2029 let norm = vector_norm_l2(&data).into_inner();
2030 assert_relative_eq!(*norm.as_ref(), 1.0, epsilon = 1e-15);
2031 }
2032
2033 #[test]
2034 fn unit_vector_y() {
2035 let data = vec_f64(&[0.0, 1.0, 0.0]);
2036 let norm = vector_norm_l2(&data).into_inner();
2037 assert_relative_eq!(*norm.as_ref(), 1.0, epsilon = 1e-15);
2038 }
2039
2040 #[test]
2041 fn three_equal_values() {
2042 // ||[1, 1, 1]|| = sqrt(3)
2043 let data = vec_f64(&[1.0, 1.0, 1.0]);
2044 let norm = vector_norm_l2(&data).into_inner();
2045 assert_relative_eq!(*norm.as_ref(), 3.0_f64.sqrt(), epsilon = 1e-15);
2046 }
2047
2048 #[test]
2049 fn larger_vector() {
2050 // ||[1, 2, 3, 4, 5]|| = sqrt(1 + 4 + 9 + 16 + 25) = sqrt(55)
2051 let data = vec_f64(&[1.0, 2.0, 3.0, 4.0, 5.0]);
2052 let norm = vector_norm_l2(&data).into_inner();
2053 assert_relative_eq!(*norm.as_ref(), 55.0_f64.sqrt(), epsilon = 1e-14);
2054 }
2055 }
2056
2057 mod edge_cases {
2058 use super::*;
2059
2060 #[test]
2061 fn empty_vector() {
2062 let data: Vec<RealNative64StrictFinite> = vec![];
2063 let norm = vector_norm_l2(&data).into_inner();
2064 assert_eq!(*norm.as_ref(), 0.0);
2065 }
2066
2067 #[test]
2068 fn single_positive_element() {
2069 let data = vec_f64(&[7.0]);
2070 let norm = vector_norm_l2(&data).into_inner();
2071 assert_relative_eq!(*norm.as_ref(), 7.0, epsilon = 1e-15);
2072 }
2073
2074 #[test]
2075 fn single_negative_element() {
2076 let data = vec_f64(&[-7.0]);
2077 let norm = vector_norm_l2(&data).into_inner();
2078 assert_relative_eq!(*norm.as_ref(), 7.0, epsilon = 1e-15);
2079 }
2080
2081 #[test]
2082 fn all_zeros() {
2083 let data = vec_f64(&[0.0, 0.0, 0.0, 0.0]);
2084 let norm = vector_norm_l2(&data).into_inner();
2085 assert_eq!(*norm.as_ref(), 0.0);
2086 }
2087
2088 #[test]
2089 fn zeros_interspersed() {
2090 let data = vec_f64(&[0.0, 3.0, 0.0, 4.0, 0.0]);
2091 let norm = vector_norm_l2(&data).into_inner();
2092 assert_relative_eq!(*norm.as_ref(), 5.0, epsilon = 1e-15);
2093 }
2094
2095 #[test]
2096 fn negative_values() {
2097 let data = vec_f64(&[-3.0, -4.0]);
2098 let norm = vector_norm_l2(&data).into_inner();
2099 assert_relative_eq!(*norm.as_ref(), 5.0, epsilon = 1e-15);
2100 }
2101
2102 #[test]
2103 fn mixed_signs() {
2104 let data = vec_f64(&[-3.0, 4.0]);
2105 let norm = vector_norm_l2(&data).into_inner();
2106 assert_relative_eq!(*norm.as_ref(), 5.0, epsilon = 1e-15);
2107 }
2108
2109 #[test]
2110 fn single_zero() {
2111 let data = vec_f64(&[0.0]);
2112 let norm = vector_norm_l2(&data).into_inner();
2113 assert_eq!(*norm.as_ref(), 0.0);
2114 }
2115 }
2116
2117 mod numerical_stability {
2118 use super::*;
2119
2120 #[test]
2121 fn large_values_no_overflow() {
2122 // Values that would overflow with naive x^2 approach
2123 let scale = 1e154;
2124 let data = vec_f64(&[3.0 * scale, 4.0 * scale]);
2125 let norm = vector_norm_l2(&data).into_inner();
2126 let expected = 5.0 * scale;
2127 let rel_err = (*norm.as_ref() - expected).abs() / expected;
2128 assert!(
2129 rel_err < 1e-14,
2130 "Large values: expected {}, got {}, rel_err {}",
2131 expected,
2132 *norm.as_ref(),
2133 rel_err
2134 );
2135 }
2136
2137 #[test]
2138 fn very_large_values() {
2139 // Even closer to overflow
2140 let scale = 1e300;
2141 let data = vec_f64(&[scale, scale]);
2142 let norm = vector_norm_l2(&data).into_inner();
2143 let expected = scale * 2.0_f64.sqrt();
2144 let rel_err = (*norm.as_ref() - expected).abs() / expected;
2145 assert!(
2146 rel_err < 1e-14,
2147 "Very large values: expected {}, got {}, rel_err {}",
2148 expected,
2149 *norm.as_ref(),
2150 rel_err
2151 );
2152 }
2153
2154 #[test]
2155 fn small_values_no_underflow() {
2156 // Values that would underflow with naive x^2 approach
2157 let scale = 1e-154;
2158 let data = vec_f64(&[3.0 * scale, 4.0 * scale]);
2159 let norm = vector_norm_l2(&data).into_inner();
2160 let expected = 5.0 * scale;
2161 let rel_err = (*norm.as_ref() - expected).abs() / expected;
2162 assert!(
2163 rel_err < 1e-14,
2164 "Small values: expected {}, got {}, rel_err {}",
2165 expected,
2166 *norm.as_ref(),
2167 rel_err
2168 );
2169 }
2170
2171 #[test]
2172 fn very_small_values() {
2173 // Even closer to underflow
2174 let scale = 1e-300;
2175 let data = vec_f64(&[scale, scale]);
2176 let norm = vector_norm_l2(&data).into_inner();
2177 let expected = scale * 2.0_f64.sqrt();
2178 let rel_err = (*norm.as_ref() - expected).abs() / expected;
2179 assert!(
2180 rel_err < 1e-14,
2181 "Very small values: expected {}, got {}, rel_err {}",
2182 expected,
2183 *norm.as_ref(),
2184 rel_err
2185 );
2186 }
2187
2188 #[test]
2189 fn mixed_large_and_small() {
2190 // Large value dominates
2191 let data = vec_f64(&[1e150, 1.0, 1e-150]);
2192 let norm = vector_norm_l2(&data).into_inner();
2193 // Norm is approximately 1e150 (small values negligible)
2194 let rel_err = (*norm.as_ref() - 1e150).abs() / 1e150;
2195 assert!(
2196 rel_err < 1e-14,
2197 "Mixed magnitudes: expected ~1e150, got {}, rel_err {}",
2198 *norm.as_ref(),
2199 rel_err
2200 );
2201 }
2202
2203 #[test]
2204 fn all_same_large_values() {
2205 // n values of x: ||[x, x, ..., x]|| = |x| * sqrt(n)
2206 let x = 1e154;
2207 let n = 100;
2208 let data: Vec<RealNative64StrictFinite> = (0..n)
2209 .map(|_| RealNative64StrictFinite::from_f64(x))
2210 .collect();
2211 let norm = vector_norm_l2(&data).into_inner();
2212 let expected = x * (n as f64).sqrt();
2213 let rel_err = (*norm.as_ref() - expected).abs() / expected;
2214 assert!(
2215 rel_err < 1e-13,
2216 "100 large values: expected {}, got {}, rel_err {}",
2217 expected,
2218 *norm.as_ref(),
2219 rel_err
2220 );
2221 }
2222
2223 #[test]
2224 fn all_same_small_values() {
2225 let x = 1e-154;
2226 let n = 100;
2227 let data: Vec<RealNative64StrictFinite> = (0..n)
2228 .map(|_| RealNative64StrictFinite::from_f64(x))
2229 .collect();
2230 let norm = vector_norm_l2(&data).into_inner();
2231 let expected = x * (n as f64).sqrt();
2232 let rel_err = (*norm.as_ref() - expected).abs() / expected;
2233 assert!(
2234 rel_err < 1e-13,
2235 "100 small values: expected {}, got {}, rel_err {}",
2236 expected,
2237 *norm.as_ref(),
2238 rel_err
2239 );
2240 }
2241
2242 #[test]
2243 fn rescaling_triggered_multiple_times() {
2244 // Ascending order triggers rescaling at each step
2245 let data = vec_f64(&[1.0, 2.0, 4.0, 8.0, 16.0]);
2246 let expected = (1.0 + 4.0 + 16.0 + 64.0 + 256.0_f64).sqrt(); // sqrt(341)
2247 let norm = vector_norm_l2(&data).into_inner();
2248 assert_relative_eq!(*norm.as_ref(), expected, epsilon = 1e-14);
2249 }
2250
2251 #[test]
2252 fn descending_order_no_rescaling() {
2253 // Descending order: max is found first, no rescaling needed
2254 let data = vec_f64(&[16.0, 8.0, 4.0, 2.0, 1.0]);
2255 let expected = (1.0 + 4.0 + 16.0 + 64.0 + 256.0_f64).sqrt();
2256 let norm = vector_norm_l2(&data).into_inner();
2257 assert_relative_eq!(*norm.as_ref(), expected, epsilon = 1e-14);
2258 }
2259 }
2260
2261 mod special_values {
2262 use super::*;
2263
2264 #[test]
2265 fn max_finite_value() {
2266 // Single f64::MAX should give f64::MAX
2267 let data = vec_f64(&[f64::MAX]);
2268 let norm = vector_norm_l2(&data).into_inner();
2269 assert_eq!(*norm.as_ref(), f64::MAX);
2270 }
2271
2272 #[test]
2273 fn min_positive_value() {
2274 // Single f64::MIN_POSITIVE should give f64::MIN_POSITIVE
2275 let data = vec_f64(&[f64::MIN_POSITIVE]);
2276 let norm = vector_norm_l2(&data).into_inner();
2277 assert_eq!(*norm.as_ref(), f64::MIN_POSITIVE);
2278 }
2279
2280 #[test]
2281 fn epsilon() {
2282 let data = vec_f64(&[f64::EPSILON]);
2283 let norm = vector_norm_l2(&data).into_inner();
2284 assert_eq!(*norm.as_ref(), f64::EPSILON);
2285 }
2286 }
2287
2288 #[cfg(feature = "rug")]
2289 mod rug_backend {
2290 use super::*;
2291 use crate::functions::{Abs, Sqrt};
2292 use crate::{Constants, RealRugStrictFinite};
2293 use num::Zero;
2294 use try_create::TryNew;
2295
2296 const PRECISION: u32 = 200;
2297 type R = RealRugStrictFinite<PRECISION>;
2298
2299 /// Helper to create a validated rug real from f64
2300 fn rug_f64(v: f64) -> R {
2301 R::try_from_f64(v).unwrap()
2302 }
2303
2304 /// Helper to create a validated rug real from string (for exact values)
2305 fn rug_str(s: &str) -> R {
2306 R::try_new(rug::Float::with_val(
2307 PRECISION,
2308 rug::Float::parse(s).unwrap(),
2309 ))
2310 .unwrap()
2311 }
2312
2313 #[test]
2314 fn basic_3_4_5() {
2315 let data: Vec<R> = vec![rug_f64(3.0), rug_f64(4.0)];
2316 let norm = vector_norm_l2(&data).into_inner();
2317 let five = rug_f64(5.0);
2318
2319 let diff = (norm.clone() - &five).abs();
2320 assert!(
2321 diff < R::epsilon(),
2322 "3-4-5: expected 5, got {}, diff {}",
2323 norm,
2324 diff
2325 );
2326 }
2327
2328 #[test]
2329 fn empty_vector() {
2330 let data: Vec<R> = vec![];
2331 let norm = vector_norm_l2(&data).into_inner();
2332 assert_eq!(norm, R::zero());
2333 }
2334
2335 #[test]
2336 fn single_element() {
2337 let data = vec![rug_f64(7.0)];
2338 let norm = vector_norm_l2(&data).into_inner();
2339 let seven = rug_f64(7.0);
2340 assert_eq!(norm, seven);
2341 }
2342
2343 #[test]
2344 fn single_negative() {
2345 let data = vec![rug_f64(-7.0)];
2346 let norm = vector_norm_l2(&data).into_inner();
2347 let seven = rug_f64(7.0);
2348 assert_eq!(norm, seven);
2349 }
2350
2351 #[test]
2352 fn all_zeros() {
2353 let data: Vec<R> = vec![R::zero(), R::zero(), R::zero()];
2354 let norm = vector_norm_l2(&data).into_inner();
2355 assert_eq!(norm, R::zero());
2356 }
2357
2358 #[test]
2359 fn high_precision_values() {
2360 // Values that cannot be exactly represented in f64
2361 let data: Vec<R> = vec![rug_str("1e-100"), rug_str("1e-100")];
2362 let norm = vector_norm_l2(&data).into_inner();
2363
2364 // Expected: sqrt(2) * 1e-100
2365 let expected = rug_str("1e-100") * rug_f64(2.0).sqrt();
2366 let diff = (norm.clone() - &expected).abs();
2367
2368 assert!(
2369 diff < R::epsilon() * &expected,
2370 "High precision: expected {}, got {}, diff {}",
2371 expected,
2372 norm,
2373 diff
2374 );
2375 }
2376
2377 #[test]
2378 fn very_large_exponents() {
2379 // Rug can handle much larger exponents than f64
2380 // These values would be infinity in f64
2381 let large = rug_str("1e1000");
2382 let data: Vec<R> = vec![large.clone(), large.clone()];
2383 let norm = vector_norm_l2(&data).into_inner();
2384
2385 // Expected: sqrt(2) * 1e1000
2386 let sqrt2 = rug_f64(2.0).sqrt();
2387 let expected = large * sqrt2;
2388
2389 let rel_diff = ((norm.clone() - &expected) / &expected).abs();
2390 assert!(
2391 rel_diff < R::epsilon(),
2392 "Very large exponents: rel_diff = {}",
2393 rel_diff
2394 );
2395 }
2396
2397 #[test]
2398 fn very_small_exponents() {
2399 // Rug can handle much smaller exponents than f64
2400 // These values would be zero in f64
2401 let small = rug_str("1e-1000");
2402 let data: Vec<R> = vec![small.clone(), small.clone()];
2403 let norm = vector_norm_l2(&data).into_inner();
2404
2405 // Expected: sqrt(2) * 1e-1000
2406 let sqrt2 = rug_f64(2.0).sqrt();
2407 let expected = small * sqrt2;
2408
2409 let rel_diff = ((norm.clone() - &expected) / &expected).abs();
2410 assert!(
2411 rel_diff < R::epsilon(),
2412 "Very small exponents: rel_diff = {}",
2413 rel_diff
2414 );
2415 }
2416
2417 #[test]
2418 fn mixed_signs_rug() {
2419 let data: Vec<R> = vec![rug_f64(-3.0), rug_f64(4.0)];
2420 let norm = vector_norm_l2(&data).into_inner();
2421 let five = rug_f64(5.0);
2422
2423 let diff = (norm.clone() - &five).abs();
2424 assert!(diff < R::epsilon());
2425 }
2426
2427 #[test]
2428 fn zeros_interspersed_rug() {
2429 let data: Vec<R> =
2430 vec![R::zero(), rug_f64(3.0), R::zero(), rug_f64(4.0), R::zero()];
2431 let norm = vector_norm_l2(&data).into_inner();
2432 let five = rug_f64(5.0);
2433
2434 let diff = (norm.clone() - &five).abs();
2435 assert!(diff < R::epsilon());
2436 }
2437
2438 #[test]
2439 fn ascending_order_triggers_rescaling() {
2440 let data: Vec<R> = vec![
2441 rug_f64(1.0),
2442 rug_f64(2.0),
2443 rug_f64(4.0),
2444 rug_f64(8.0),
2445 rug_f64(16.0),
2446 ];
2447 // sqrt(1 + 4 + 16 + 64 + 256) = sqrt(341)
2448 let expected = rug_f64(341.0).sqrt();
2449 let norm = vector_norm_l2(&data).into_inner();
2450
2451 let diff = (norm.clone() - &expected).abs();
2452 assert!(
2453 diff < R::epsilon() * &expected,
2454 "Ascending: expected {}, got {}, diff {}",
2455 expected,
2456 norm,
2457 diff
2458 );
2459 }
2460
2461 #[test]
2462 fn higher_precision() {
2463 // Test with even higher precision
2464 const HIGH_PREC: u32 = 500;
2465 type HP = RealRugStrictFinite<HIGH_PREC>;
2466
2467 let hp_f64 = |v: f64| HP::try_from_f64(v).unwrap();
2468
2469 let data: Vec<HP> = vec![hp_f64(3.0), hp_f64(4.0)];
2470 let norm = vector_norm_l2(&data).into_inner();
2471 let five = hp_f64(5.0);
2472
2473 let diff = (norm.clone() - &five).abs();
2474 assert!(diff < HP::epsilon());
2475 }
2476 }
2477 }
2478
2479 // ── Complex scalar tests ─────────────────────────────────────────────────
2480
2481 mod complex_types {
2482 use super::*;
2483 use crate::{ComplexNative64StrictFinite, RealNative64StrictFinite};
2484 use num::Complex;
2485 use try_create::TryNew;
2486
2487 /// Helper: build a `ComplexNative64StrictFinite` from two f64 components.
2488 fn c(re: f64, im: f64) -> ComplexNative64StrictFinite {
2489 ComplexNative64StrictFinite::try_new(Complex::new(re, im)).unwrap()
2490 }
2491
2492 /// Helper: extract the inner f64 from a `NonNegativeRealScalar<RealNative64StrictFinite>`.
2493 fn inner_f64(x: crate::scalars::NonNegativeRealScalar<RealNative64StrictFinite>) -> f64 {
2494 *x.into_inner().as_ref()
2495 }
2496
2497 mod vector_norm_l2_complex {
2498 use super::*;
2499
2500 /// ‖[3+0i, 0+4i]‖₂ = √(3² + 4²) = 5
2501 #[test]
2502 fn components_on_axes() {
2503 let v = [c(3.0, 0.0), c(0.0, 4.0)];
2504 assert!((inner_f64(vector_norm_l2(&v)) - 5.0).abs() < 1e-12);
2505 }
2506
2507 /// ‖[3+4i]‖₂ = |3+4i| = 5
2508 #[test]
2509 fn single_complex() {
2510 let v = [c(3.0, 4.0)];
2511 assert!((inner_f64(vector_norm_l2(&v)) - 5.0).abs() < 1e-12);
2512 }
2513
2514 /// ‖[0+0i, 0+0i]‖₂ = 0
2515 #[test]
2516 fn zero_vector() {
2517 let v = [c(0.0, 0.0), c(0.0, 0.0)];
2518 assert_eq!(inner_f64(vector_norm_l2(&v)), 0.0);
2519 }
2520
2521 /// Negating imaginary part does not change the norm.
2522 #[test]
2523 fn conjugate_preserves_norm() {
2524 let v = [c(1.0, 2.0), c(3.0, -4.0)];
2525 let vc = [c(1.0, -2.0), c(3.0, 4.0)];
2526 assert!(
2527 (inner_f64(vector_norm_l2(&v)) - inner_f64(vector_norm_l2(&vc))).abs() < 1e-14
2528 );
2529 }
2530
2531 /// vector_norm_l2 and vector_norm_l2_neumaier agree on ordinary values.
2532 #[test]
2533 fn matches_neumaier() {
2534 let v = [c(1.0, 2.0), c(3.0, -1.0), c(-1.0, 4.0)];
2535 let diff =
2536 (inner_f64(vector_norm_l2(&v)) - inner_f64(vector_norm_l2_neumaier(&v))).abs();
2537 assert!(diff < 1e-14);
2538 }
2539 }
2540
2541 mod vector_norm_l2_sq_complex {
2542 use super::*;
2543
2544 /// ‖[3+0i, 0+4i]‖₂² = 9 + 16 = 25
2545 #[test]
2546 fn components_on_axes() {
2547 let v = [c(3.0, 0.0), c(0.0, 4.0)];
2548 assert!((inner_f64(vector_norm_l2_sq(&v)) - 25.0).abs() < 1e-12);
2549 }
2550
2551 /// ‖[3+4i]‖₂² = |3+4i|² = 25
2552 #[test]
2553 fn single_complex() {
2554 let v = [c(3.0, 4.0)];
2555 assert!((inner_f64(vector_norm_l2_sq(&v)) - 25.0).abs() < 1e-12);
2556 }
2557
2558 /// vector_norm_l2_sq(v) ≈ vector_norm_l2(v)²
2559 #[test]
2560 fn consistent_with_vector_norm_l2() {
2561 let v = [c(1.0, 2.0), c(3.0, 4.0)];
2562 let l2 = inner_f64(vector_norm_l2(&v));
2563 let sq = inner_f64(vector_norm_l2_sq(&v));
2564 assert!((sq - l2 * l2).abs() < 1e-12);
2565 }
2566
2567 /// vector_norm_l2_sq and vector_norm_l2_sq_neumaier agree on ordinary values.
2568 #[test]
2569 fn matches_neumaier() {
2570 let v = [c(3.0, 0.0), c(0.0, 4.0)];
2571 let diff = (inner_f64(vector_norm_l2_sq(&v))
2572 - inner_f64(vector_norm_l2_sq_neumaier(&v)))
2573 .abs();
2574 assert!(diff < 1e-14);
2575 }
2576 }
2577
2578 mod vector_norm_l2_sq_neumaier_complex {
2579 use super::*;
2580
2581 #[test]
2582 fn zero_vector() {
2583 let v = [c(0.0, 0.0), c(0.0, 0.0)];
2584 assert_eq!(inner_f64(vector_norm_l2_sq_neumaier(&v)), 0.0);
2585 }
2586
2587 #[test]
2588 fn matches_naive() {
2589 let v = [c(2.0, 1.0), c(0.0, 3.0), c(-1.0, -1.0)];
2590 let diff = (inner_f64(vector_norm_l2_sq_neumaier(&v))
2591 - inner_f64(vector_norm_l2_sq(&v)))
2592 .abs();
2593 assert!(diff < 1e-14);
2594 }
2595 }
2596
2597 mod vector_norm_l2_neumaier_complex {
2598 use super::*;
2599
2600 #[test]
2601 fn zero_vector() {
2602 let v = [c(0.0, 0.0)];
2603 assert_eq!(inner_f64(vector_norm_l2_neumaier(&v)), 0.0);
2604 }
2605
2606 #[test]
2607 fn matches_naive() {
2608 let v = [c(2.0, 1.0), c(0.0, 3.0), c(-1.0, -1.0)];
2609 let diff =
2610 (inner_f64(vector_norm_l2_neumaier(&v)) - inner_f64(vector_norm_l2(&v))).abs();
2611 assert!(diff < 1e-14);
2612 }
2613 }
2614
2615 mod vector_norm_l1_complex {
2616 use super::*;
2617
2618 /// ‖[3+0i, 0+4i]‖₁ = |3+0i| + |0+4i| = 3 + 4 = 7
2619 #[test]
2620 fn components_on_axes() {
2621 let v = [c(3.0, 0.0), c(0.0, 4.0)];
2622 assert!((inner_f64(vector_norm_l1(&v)) - 7.0).abs() < 1e-12);
2623 }
2624
2625 /// ‖[3+4i]‖₁ = |3+4i| = 5
2626 #[test]
2627 fn single_complex() {
2628 let v = [c(3.0, 4.0)];
2629 assert!((inner_f64(vector_norm_l1(&v)) - 5.0).abs() < 1e-12);
2630 }
2631
2632 /// ‖v‖₁ ≥ ‖v‖₂ for multi-element vectors (triangle inequality corollary)
2633 #[test]
2634 fn l1_geq_l2() {
2635 let v = [c(3.0, 0.0), c(0.0, 4.0)];
2636 assert!(inner_f64(vector_norm_l1(&v)) >= inner_f64(vector_norm_l2(&v)) - 1e-14);
2637 }
2638
2639 #[test]
2640 fn zero_vector() {
2641 let v = [c(0.0, 0.0), c(0.0, 0.0)];
2642 assert_eq!(inner_f64(vector_norm_l1(&v)), 0.0);
2643 }
2644
2645 /// vector_norm_l1 and vector_norm_l1_neumaier agree on ordinary values.
2646 #[test]
2647 fn matches_neumaier() {
2648 let v = [c(1.0, 2.0), c(3.0, 4.0), c(5.0, 6.0)];
2649 let diff =
2650 (inner_f64(vector_norm_l1(&v)) - inner_f64(vector_norm_l1_neumaier(&v))).abs();
2651 assert!(diff < 1e-14);
2652 }
2653 }
2654
2655 mod vector_norm_l1_neumaier_complex {
2656 use super::*;
2657
2658 #[test]
2659 fn components_on_axes() {
2660 let v = [c(3.0, 0.0), c(0.0, 4.0)];
2661 assert!((inner_f64(vector_norm_l1_neumaier(&v)) - 7.0).abs() < 1e-12);
2662 }
2663
2664 #[test]
2665 fn zero_vector() {
2666 let v = [c(0.0, 0.0)];
2667 assert_eq!(inner_f64(vector_norm_l1_neumaier(&v)), 0.0);
2668 }
2669 }
2670
2671 mod vector_norm_linf_complex {
2672 use super::*;
2673
2674 /// ‖[3+0i, 0+4i]‖∞ = max(3, 4) = 4
2675 #[test]
2676 fn maximum_modulus() {
2677 let v = [c(3.0, 0.0), c(0.0, 4.0)];
2678 assert!((inner_f64(vector_norm_linf(&v)) - 4.0).abs() < 1e-12);
2679 }
2680
2681 /// ‖[3+4i, 0+5i]‖∞ = max(5, 5) = 5 (equal moduli)
2682 #[test]
2683 fn equal_moduli() {
2684 let v = [c(3.0, 4.0), c(0.0, 5.0)];
2685 assert!((inner_f64(vector_norm_linf(&v)) - 5.0).abs() < 1e-12);
2686 }
2687
2688 /// ‖v‖∞ ≤ ‖v‖₁ always holds
2689 #[test]
2690 fn linf_leq_l1() {
2691 let v = [c(1.0, 2.0), c(3.0, 4.0)];
2692 assert!(inner_f64(vector_norm_linf(&v)) <= inner_f64(vector_norm_l1(&v)) + 1e-14);
2693 }
2694
2695 /// ‖v‖∞ ≤ ‖v‖₂ ≤ √n · ‖v‖∞ (standard norm inequality)
2696 #[test]
2697 fn linf_leq_l2() {
2698 let v = [c(1.0, 2.0), c(3.0, 4.0)];
2699 let linf = inner_f64(vector_norm_linf(&v));
2700 let l2 = inner_f64(vector_norm_l2(&v));
2701 let n = v.len() as f64;
2702 assert!(linf <= l2 + 1e-14);
2703 assert!(l2 <= n.sqrt() * linf + 1e-14);
2704 }
2705
2706 #[test]
2707 fn zero_vector() {
2708 let v = [c(0.0, 0.0), c(0.0, 0.0)];
2709 assert_eq!(inner_f64(vector_norm_linf(&v)), 0.0);
2710 }
2711
2712 #[test]
2713 fn parallel_matches_sequential() {
2714 let v = [c(1.0, 2.0), c(3.0, 4.0), c(-2.0, -1.0)];
2715 let seq = inner_f64(vector_norm_linf(&v));
2716 let par = inner_f64(vector_norm_linf_par(&v));
2717 assert!((seq - par).abs() < 1e-14);
2718 }
2719 }
2720 }
2721}