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