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}