Skip to main content

num_valid/algorithms/
l2_norm.rs

1#![deny(rustdoc::broken_intra_doc_links)]
2
3//! # L2 Norm (Euclidean Norm) Computation
4//!
5//! This module provides a numerically stable implementation of the L2 norm
6//! (Euclidean norm) for slices of [`RealScalar`] values.
7//!
8//! ## Algorithm: BLAS/LAPACK-Style Incremental Scaling
9//!
10//! The implementation uses the scaled sum of squares approach, which is the
11//! same algorithm used in modern BLAS/LAPACK implementations (e.g., `DNRM2`).
12//! This single-pass algorithm avoids overflow and underflow by maintaining
13//! the invariant:
14//!
15//! $$\|x\|_2 = \text{scale} \cdot \sqrt{\text{sumsq}}$$
16//!
17//! where:
18//! - `scale` is the maximum absolute value seen so far
19//! - `sumsq` is the sum of squared normalized values: $\sum_i (|x_i| / \text{scale})^2$
20//!
21//! ### Key Properties
22//!
23//! | Property | Description |
24//! |----------|-------------|
25//! | **Single-pass** | O(n) time complexity with one iteration over the data |
26//! | **No overflow** | All squared values are ≤ 1 (normalized by max) |
27//! | **No underflow** | Scale factor preserves the magnitude of small values |
28//! | **Order-independent** | Same result regardless of element ordering |
29//! | **Backend-agnostic** | Works with both `f64` and `rug::Float` backends |
30//!
31//! ### How It Works
32//!
33//! 1. Track `scale` (max |xᵢ| seen) and `sumsq` (sum of normalized squares)
34//! 2. For each element xᵢ:
35//!    - If |xᵢ| > scale: rescale sumsq to new scale, then accumulate
36//!    - Otherwise: just accumulate (|xᵢ|/scale)²
37//! 3. Return scale × √sumsq
38//!
39//! ## Why Not Naive Implementation?
40//!
41//! The naive approach `sqrt(sum(x²))` fails for extreme values:
42//!
43//! ```text
44//! Naive overflow:   ||[1e200, 1e200]|| → (1e200)² = Inf → sqrt(Inf) = Inf  ❌
45//! Naive underflow:  ||[1e-200, 1e-200]|| → (1e-200)² = 0 → sqrt(0) = 0    ❌
46//!
47//! Scaled approach:  ||[1e200, 1e200]|| → 1e200 × sqrt(2) ≈ 1.41e200        ✅
48//! Scaled approach:  ||[1e-200, 1e-200]|| → 1e-200 × sqrt(2) ≈ 1.41e-200    ✅
49//! ```
50//!
51//! ## Usage Examples
52//!
53//! ### Basic Usage
54//!
55//! ```rust
56//! use num_valid::{RealNative64StrictFinite, RealScalar, algorithms::l2_norm::l2_norm};
57//!
58//! let data: Vec<RealNative64StrictFinite> = vec![
59//!     RealNative64StrictFinite::from_f64(3.0),
60//!     RealNative64StrictFinite::from_f64(4.0),
61//! ];
62//!
63//! let norm = l2_norm(&data);
64//! assert_eq!(*norm.as_ref(), 5.0); // 3² + 4² = 25, √25 = 5
65//! ```
66//!
67//! ### Handling Extreme Values
68//!
69//! ```rust
70//! use num_valid::{RealNative64StrictFinite, RealScalar, algorithms::l2_norm::l2_norm};
71//!
72//! // Values near overflow threshold - naive approach would fail
73//! let large: Vec<RealNative64StrictFinite> = vec![
74//!     RealNative64StrictFinite::from_f64(1e154),
75//!     RealNative64StrictFinite::from_f64(1e154),
76//! ];
77//! let norm = l2_norm(&large);
78//! // Result: √2 × 1e154 ≈ 1.41e154 (no overflow!)
79//! assert!((norm.as_ref() / 1e154 - std::f64::consts::SQRT_2).abs() < 1e-10);
80//!
81//! // Values near underflow threshold
82//! let small: Vec<RealNative64StrictFinite> = vec![
83//!     RealNative64StrictFinite::from_f64(1e-154),
84//!     RealNative64StrictFinite::from_f64(1e-154),
85//! ];
86//! let norm = l2_norm(&small);
87//! // Result: √2 × 1e-154 ≈ 1.41e-154 (no underflow!)
88//! assert!((norm.as_ref() / 1e-154 - std::f64::consts::SQRT_2).abs() < 1e-10);
89//! ```
90//!
91//! ### With Arbitrary-Precision Backend
92//!
93//! ```rust
94//! # #[cfg(feature = "rug")]
95//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
96//! use num_valid::{RealRugStrictFinite, algorithms::l2_norm::l2_norm};
97//! use try_create::TryNew;
98//!
99//! type R = RealRugStrictFinite<200>; // 200-bit precision
100//!
101//! // Values beyond f64 range (would be infinity in f64)
102//! let huge = R::try_new(rug::Float::with_val(200, rug::Float::parse("1e1000")?))?;
103//! let data: Vec<R> = vec![huge.clone(), huge.clone()];
104//!
105//! let norm = l2_norm(&data);
106//! // Result: √2 × 1e1000 - computed exactly with arbitrary precision
107//! # Ok(())
108//! # }
109//! # #[cfg(not(feature = "rug"))] fn main() {}
110//! ```
111//!
112//! ## Edge Cases
113//!
114//! | Input | Result |
115//! |-------|--------|
116//! | Empty slice `[]` | `0` |
117//! | Single element `[x]` | `|x|` |
118//! | All zeros `[0, 0, 0]` | `0` |
119//! | Contains zeros `[0, 3, 0, 4]` | Same as `[3, 4]` → `5` |
120//! | Negative values `[-3, -4]` | Same as `[3, 4]` → `5` |
121//!
122//! ## Performance Characteristics
123//!
124//! - **Time complexity**: O(n) - single pass over data
125//! - **Space complexity**: O(1) - only two accumulator variables
126//! - **Operations per element**: 1 comparison, 1-2 divisions, 1 multiply-add
127//!
128//! The algorithm performs more divisions than the naive approach, but this
129//! tradeoff is necessary for numerical stability. For performance-critical
130//! code where values are known to be in a safe range, consider the naive
131//! approach with appropriate bounds checking.
132//!
133//! ## References
134//!
135//! - Blue, J. L. (1978). "A Portable Fortran Program to Find the Euclidean
136//!   Norm of a Vector". ACM Transactions on Mathematical Software, 4(1), 15-23.
137//! - Anderson, E. (2017). "Algorithm 978: Safe Scaling in the Level 1 BLAS".
138//!   ACM Transactions on Mathematical Software, 44(1), 1-28.
139//! - LAPACK Working Note 148: "On Computing LAPACK's XNRM2"
140//!
141//! [`RealScalar`]: crate::RealScalar
142
143use crate::RealScalar;
144
145/// Computes the L2 norm (Euclidean norm) of a slice of real scalars.
146///
147/// Uses BLAS/LAPACK-style incremental scaling to prevent overflow and underflow.
148/// The algorithm maintains the invariant `||x||₂ = scale × √sumsq` where all
149/// accumulated squared values are normalized to the range [0, 1].
150///
151/// # Arguments
152///
153/// * `x` - A slice of [`RealScalar`] values
154///
155/// # Returns
156///
157/// The L2 norm: $\|x\|_2 = \sqrt{\sum_i x_i^2}$
158///
159/// # Algorithm Complexity
160///
161/// - **Time**: O(n) single-pass
162/// - **Space**: O(1)
163///
164/// # Examples
165///
166/// ```rust
167/// use num_valid::{RealNative64StrictFinite, RealScalar, algorithms::l2_norm::l2_norm};
168///
169/// // Pythagorean triple: 3² + 4² = 5²
170/// let v = vec![
171///     RealNative64StrictFinite::from_f64(3.0),
172///     RealNative64StrictFinite::from_f64(4.0),
173/// ];
174/// assert_eq!(*l2_norm(&v).as_ref(), 5.0);
175///
176/// // Empty slice returns zero
177/// let empty: Vec<RealNative64StrictFinite> = vec![];
178/// assert_eq!(*l2_norm(&empty).as_ref(), 0.0);
179///
180/// // Works with extreme values (no overflow)
181/// let large = vec![
182///     RealNative64StrictFinite::from_f64(1e154),
183///     RealNative64StrictFinite::from_f64(1e154),
184/// ];
185/// assert!(l2_norm(&large).as_ref().is_finite());
186/// ```
187pub fn l2_norm<T: RealScalar>(x: &[T]) -> T {
188    let mut scale = T::zero();
189    let mut sumsq = T::zero();
190
191    let zero = T::zero();
192
193    for xi in x.iter().cloned() {
194        let abs_xi = xi.abs();
195
196        if abs_xi == zero {
197            continue;
198        }
199
200        if scale < abs_xi {
201            // Rescale previous sum to new scale
202            if scale > zero {
203                let r = scale.clone() / &abs_xi;
204                sumsq *= r.pow(2);
205            }
206            scale = abs_xi.clone();
207        }
208
209        // Always accumulate (key insight!)
210        let r = abs_xi.clone() / &scale;
211        sumsq += r.pow(2);
212    }
213
214    if scale == zero {
215        zero
216    } else {
217        scale * sumsq.sqrt()
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use crate::RealNative64StrictFinite;
225    use approx::assert_relative_eq;
226
227    /// Helper to create a vector of validated reals from f64 values
228    fn vec_f64(vals: &[f64]) -> Vec<RealNative64StrictFinite> {
229        vals.iter()
230            .map(|&v| RealNative64StrictFinite::from_f64(v))
231            .collect()
232    }
233
234    mod basic_cases {
235        use super::*;
236
237        #[test]
238        fn pythagorean_3_4_5() {
239            let data = vec_f64(&[3.0, 4.0]);
240            let norm = l2_norm(&data);
241            assert_relative_eq!(*norm.as_ref(), 5.0, epsilon = 1e-15);
242        }
243
244        #[test]
245        fn pythagorean_reverse_order() {
246            // Same result regardless of order
247            let data = vec_f64(&[4.0, 3.0]);
248            let norm = l2_norm(&data);
249            assert_relative_eq!(*norm.as_ref(), 5.0, epsilon = 1e-15);
250        }
251
252        #[test]
253        fn unit_vector_x() {
254            let data = vec_f64(&[1.0, 0.0, 0.0]);
255            let norm = l2_norm(&data);
256            assert_relative_eq!(*norm.as_ref(), 1.0, epsilon = 1e-15);
257        }
258
259        #[test]
260        fn unit_vector_y() {
261            let data = vec_f64(&[0.0, 1.0, 0.0]);
262            let norm = l2_norm(&data);
263            assert_relative_eq!(*norm.as_ref(), 1.0, epsilon = 1e-15);
264        }
265
266        #[test]
267        fn three_equal_values() {
268            // ||[1, 1, 1]|| = sqrt(3)
269            let data = vec_f64(&[1.0, 1.0, 1.0]);
270            let norm = l2_norm(&data);
271            assert_relative_eq!(*norm.as_ref(), 3.0_f64.sqrt(), epsilon = 1e-15);
272        }
273
274        #[test]
275        fn larger_vector() {
276            // ||[1, 2, 3, 4, 5]|| = sqrt(1 + 4 + 9 + 16 + 25) = sqrt(55)
277            let data = vec_f64(&[1.0, 2.0, 3.0, 4.0, 5.0]);
278            let norm = l2_norm(&data);
279            assert_relative_eq!(*norm.as_ref(), 55.0_f64.sqrt(), epsilon = 1e-14);
280        }
281    }
282
283    mod edge_cases {
284        use super::*;
285
286        #[test]
287        fn empty_vector() {
288            let data: Vec<RealNative64StrictFinite> = vec![];
289            let norm = l2_norm(&data);
290            assert_eq!(*norm.as_ref(), 0.0);
291        }
292
293        #[test]
294        fn single_positive_element() {
295            let data = vec_f64(&[7.0]);
296            let norm = l2_norm(&data);
297            assert_relative_eq!(*norm.as_ref(), 7.0, epsilon = 1e-15);
298        }
299
300        #[test]
301        fn single_negative_element() {
302            let data = vec_f64(&[-7.0]);
303            let norm = l2_norm(&data);
304            assert_relative_eq!(*norm.as_ref(), 7.0, epsilon = 1e-15);
305        }
306
307        #[test]
308        fn all_zeros() {
309            let data = vec_f64(&[0.0, 0.0, 0.0, 0.0]);
310            let norm = l2_norm(&data);
311            assert_eq!(*norm.as_ref(), 0.0);
312        }
313
314        #[test]
315        fn zeros_interspersed() {
316            let data = vec_f64(&[0.0, 3.0, 0.0, 4.0, 0.0]);
317            let norm = l2_norm(&data);
318            assert_relative_eq!(*norm.as_ref(), 5.0, epsilon = 1e-15);
319        }
320
321        #[test]
322        fn negative_values() {
323            let data = vec_f64(&[-3.0, -4.0]);
324            let norm = l2_norm(&data);
325            assert_relative_eq!(*norm.as_ref(), 5.0, epsilon = 1e-15);
326        }
327
328        #[test]
329        fn mixed_signs() {
330            let data = vec_f64(&[-3.0, 4.0]);
331            let norm = l2_norm(&data);
332            assert_relative_eq!(*norm.as_ref(), 5.0, epsilon = 1e-15);
333        }
334
335        #[test]
336        fn single_zero() {
337            let data = vec_f64(&[0.0]);
338            let norm = l2_norm(&data);
339            assert_eq!(*norm.as_ref(), 0.0);
340        }
341    }
342
343    mod numerical_stability {
344        use super::*;
345
346        #[test]
347        fn large_values_no_overflow() {
348            // Values that would overflow with naive x^2 approach
349            let scale = 1e154;
350            let data = vec_f64(&[3.0 * scale, 4.0 * scale]);
351            let norm = l2_norm(&data);
352            let expected = 5.0 * scale;
353            let rel_err = (*norm.as_ref() - expected).abs() / expected;
354            assert!(
355                rel_err < 1e-14,
356                "Large values: expected {}, got {}, rel_err {}",
357                expected,
358                *norm.as_ref(),
359                rel_err
360            );
361        }
362
363        #[test]
364        fn very_large_values() {
365            // Even closer to overflow
366            let scale = 1e300;
367            let data = vec_f64(&[scale, scale]);
368            let norm = l2_norm(&data);
369            let expected = scale * 2.0_f64.sqrt();
370            let rel_err = (*norm.as_ref() - expected).abs() / expected;
371            assert!(
372                rel_err < 1e-14,
373                "Very large values: expected {}, got {}, rel_err {}",
374                expected,
375                *norm.as_ref(),
376                rel_err
377            );
378        }
379
380        #[test]
381        fn small_values_no_underflow() {
382            // Values that would underflow with naive x^2 approach
383            let scale = 1e-154;
384            let data = vec_f64(&[3.0 * scale, 4.0 * scale]);
385            let norm = l2_norm(&data);
386            let expected = 5.0 * scale;
387            let rel_err = (*norm.as_ref() - expected).abs() / expected;
388            assert!(
389                rel_err < 1e-14,
390                "Small values: expected {}, got {}, rel_err {}",
391                expected,
392                *norm.as_ref(),
393                rel_err
394            );
395        }
396
397        #[test]
398        fn very_small_values() {
399            // Even closer to underflow
400            let scale = 1e-300;
401            let data = vec_f64(&[scale, scale]);
402            let norm = l2_norm(&data);
403            let expected = scale * 2.0_f64.sqrt();
404            let rel_err = (*norm.as_ref() - expected).abs() / expected;
405            assert!(
406                rel_err < 1e-14,
407                "Very small values: expected {}, got {}, rel_err {}",
408                expected,
409                *norm.as_ref(),
410                rel_err
411            );
412        }
413
414        #[test]
415        fn mixed_large_and_small() {
416            // Large value dominates
417            let data = vec_f64(&[1e150, 1.0, 1e-150]);
418            let norm = l2_norm(&data);
419            // Norm is approximately 1e150 (small values negligible)
420            let rel_err = (*norm.as_ref() - 1e150).abs() / 1e150;
421            assert!(
422                rel_err < 1e-14,
423                "Mixed magnitudes: expected ~1e150, got {}, rel_err {}",
424                *norm.as_ref(),
425                rel_err
426            );
427        }
428
429        #[test]
430        fn all_same_large_values() {
431            // n values of x: ||[x, x, ..., x]|| = |x| * sqrt(n)
432            let x = 1e154;
433            let n = 100;
434            let data: Vec<RealNative64StrictFinite> = (0..n)
435                .map(|_| RealNative64StrictFinite::from_f64(x))
436                .collect();
437            let norm = l2_norm(&data);
438            let expected = x * (n as f64).sqrt();
439            let rel_err = (*norm.as_ref() - expected).abs() / expected;
440            assert!(
441                rel_err < 1e-13,
442                "100 large values: expected {}, got {}, rel_err {}",
443                expected,
444                *norm.as_ref(),
445                rel_err
446            );
447        }
448
449        #[test]
450        fn all_same_small_values() {
451            let x = 1e-154;
452            let n = 100;
453            let data: Vec<RealNative64StrictFinite> = (0..n)
454                .map(|_| RealNative64StrictFinite::from_f64(x))
455                .collect();
456            let norm = l2_norm(&data);
457            let expected = x * (n as f64).sqrt();
458            let rel_err = (*norm.as_ref() - expected).abs() / expected;
459            assert!(
460                rel_err < 1e-13,
461                "100 small values: expected {}, got {}, rel_err {}",
462                expected,
463                *norm.as_ref(),
464                rel_err
465            );
466        }
467
468        #[test]
469        fn rescaling_triggered_multiple_times() {
470            // Ascending order triggers rescaling at each step
471            let data = vec_f64(&[1.0, 2.0, 4.0, 8.0, 16.0]);
472            let expected = (1.0 + 4.0 + 16.0 + 64.0 + 256.0_f64).sqrt(); // sqrt(341)
473            let norm = l2_norm(&data);
474            assert_relative_eq!(*norm.as_ref(), expected, epsilon = 1e-14);
475        }
476
477        #[test]
478        fn descending_order_no_rescaling() {
479            // Descending order: max is found first, no rescaling needed
480            let data = vec_f64(&[16.0, 8.0, 4.0, 2.0, 1.0]);
481            let expected = (1.0 + 4.0 + 16.0 + 64.0 + 256.0_f64).sqrt();
482            let norm = l2_norm(&data);
483            assert_relative_eq!(*norm.as_ref(), expected, epsilon = 1e-14);
484        }
485    }
486
487    mod special_values {
488        use super::*;
489
490        #[test]
491        fn max_finite_value() {
492            // Single f64::MAX should give f64::MAX
493            let data = vec_f64(&[f64::MAX]);
494            let norm = l2_norm(&data);
495            assert_eq!(*norm.as_ref(), f64::MAX);
496        }
497
498        #[test]
499        fn min_positive_value() {
500            // Single f64::MIN_POSITIVE should give f64::MIN_POSITIVE
501            let data = vec_f64(&[f64::MIN_POSITIVE]);
502            let norm = l2_norm(&data);
503            assert_eq!(*norm.as_ref(), f64::MIN_POSITIVE);
504        }
505
506        #[test]
507        fn epsilon() {
508            let data = vec_f64(&[f64::EPSILON]);
509            let norm = l2_norm(&data);
510            assert_eq!(*norm.as_ref(), f64::EPSILON);
511        }
512    }
513
514    #[cfg(feature = "rug")]
515    mod rug_backend {
516        use super::*;
517        use crate::functions::{Abs, Sqrt};
518        use crate::{Constants, RealRugStrictFinite};
519        use num::Zero;
520        use try_create::TryNew;
521
522        const PRECISION: u32 = 200;
523        type R = RealRugStrictFinite<PRECISION>;
524
525        /// Helper to create a validated rug real from f64
526        fn rug_f64(v: f64) -> R {
527            R::try_from_f64(v).unwrap()
528        }
529
530        /// Helper to create a validated rug real from string (for exact values)
531        fn rug_str(s: &str) -> R {
532            R::try_new(rug::Float::with_val(
533                PRECISION,
534                rug::Float::parse(s).unwrap(),
535            ))
536            .unwrap()
537        }
538
539        #[test]
540        fn basic_3_4_5() {
541            let data: Vec<R> = vec![rug_f64(3.0), rug_f64(4.0)];
542            let norm = l2_norm(&data);
543            let five = rug_f64(5.0);
544
545            let diff = (norm.clone() - &five).abs();
546            assert!(
547                diff < R::epsilon(),
548                "3-4-5: expected 5, got {}, diff {}",
549                norm,
550                diff
551            );
552        }
553
554        #[test]
555        fn empty_vector() {
556            let data: Vec<R> = vec![];
557            let norm = l2_norm(&data);
558            assert_eq!(norm, R::zero());
559        }
560
561        #[test]
562        fn single_element() {
563            let data = vec![rug_f64(7.0)];
564            let norm = l2_norm(&data);
565            let seven = rug_f64(7.0);
566            assert_eq!(norm, seven);
567        }
568
569        #[test]
570        fn single_negative() {
571            let data = vec![rug_f64(-7.0)];
572            let norm = l2_norm(&data);
573            let seven = rug_f64(7.0);
574            assert_eq!(norm, seven);
575        }
576
577        #[test]
578        fn all_zeros() {
579            let data: Vec<R> = vec![R::zero(), R::zero(), R::zero()];
580            let norm = l2_norm(&data);
581            assert_eq!(norm, R::zero());
582        }
583
584        #[test]
585        fn high_precision_values() {
586            // Values that cannot be exactly represented in f64
587            let data: Vec<R> = vec![rug_str("1e-100"), rug_str("1e-100")];
588            let norm = l2_norm(&data);
589
590            // Expected: sqrt(2) * 1e-100
591            let expected = rug_str("1e-100") * rug_f64(2.0).sqrt();
592            let diff = (norm.clone() - &expected).abs();
593
594            assert!(
595                diff < R::epsilon() * &expected,
596                "High precision: expected {}, got {}, diff {}",
597                expected,
598                norm,
599                diff
600            );
601        }
602
603        #[test]
604        fn very_large_exponents() {
605            // Rug can handle much larger exponents than f64
606            // These values would be infinity in f64
607            let large = rug_str("1e1000");
608            let data: Vec<R> = vec![large.clone(), large.clone()];
609            let norm = l2_norm(&data);
610
611            // Expected: sqrt(2) * 1e1000
612            let sqrt2 = rug_f64(2.0).sqrt();
613            let expected = large * sqrt2;
614
615            let rel_diff = ((norm.clone() - &expected) / &expected).abs();
616            assert!(
617                rel_diff < R::epsilon(),
618                "Very large exponents: rel_diff = {}",
619                rel_diff
620            );
621        }
622
623        #[test]
624        fn very_small_exponents() {
625            // Rug can handle much smaller exponents than f64
626            // These values would be zero in f64
627            let small = rug_str("1e-1000");
628            let data: Vec<R> = vec![small.clone(), small.clone()];
629            let norm = l2_norm(&data);
630
631            // Expected: sqrt(2) * 1e-1000
632            let sqrt2 = rug_f64(2.0).sqrt();
633            let expected = small * sqrt2;
634
635            let rel_diff = ((norm.clone() - &expected) / &expected).abs();
636            assert!(
637                rel_diff < R::epsilon(),
638                "Very small exponents: rel_diff = {}",
639                rel_diff
640            );
641        }
642
643        #[test]
644        fn mixed_signs_rug() {
645            let data: Vec<R> = vec![rug_f64(-3.0), rug_f64(4.0)];
646            let norm = l2_norm(&data);
647            let five = rug_f64(5.0);
648
649            let diff = (norm.clone() - &five).abs();
650            assert!(diff < R::epsilon());
651        }
652
653        #[test]
654        fn zeros_interspersed_rug() {
655            let data: Vec<R> = vec![R::zero(), rug_f64(3.0), R::zero(), rug_f64(4.0), R::zero()];
656            let norm = l2_norm(&data);
657            let five = rug_f64(5.0);
658
659            let diff = (norm.clone() - &five).abs();
660            assert!(diff < R::epsilon());
661        }
662
663        #[test]
664        fn ascending_order_triggers_rescaling() {
665            let data: Vec<R> = vec![
666                rug_f64(1.0),
667                rug_f64(2.0),
668                rug_f64(4.0),
669                rug_f64(8.0),
670                rug_f64(16.0),
671            ];
672            // sqrt(1 + 4 + 16 + 64 + 256) = sqrt(341)
673            let expected = rug_f64(341.0).sqrt();
674            let norm = l2_norm(&data);
675
676            let diff = (norm.clone() - &expected).abs();
677            assert!(
678                diff < R::epsilon() * &expected,
679                "Ascending: expected {}, got {}, diff {}",
680                expected,
681                norm,
682                diff
683            );
684        }
685
686        #[test]
687        fn higher_precision() {
688            // Test with even higher precision
689            const HIGH_PREC: u32 = 500;
690            type HP = RealRugStrictFinite<HIGH_PREC>;
691
692            let hp_f64 = |v: f64| HP::try_from_f64(v).unwrap();
693
694            let data: Vec<HP> = vec![hp_f64(3.0), hp_f64(4.0)];
695            let norm = l2_norm(&data);
696            let five = hp_f64(5.0);
697
698            let diff = (norm.clone() - &five).abs();
699            assert!(diff < HP::epsilon());
700        }
701    }
702}