1use crate::error::{CoreError, CoreResult, ErrorContext};
7use std::collections::HashMap;
8use std::fmt;
9use std::sync::RwLock;
10use std::time::Instant;
11
12#[derive(Debug, Clone)]
14pub struct PrecisionContext {
15 pub precision: f64,
17 pub error_bounds: f64,
19 pub significant_digits: u32,
21 pub precision_loss_sources: Vec<PrecisionLossSource>,
23 pub condition_number: Option<f64>,
25 pub is_stable: bool,
27 pub last_updated: Instant,
29}
30
31#[derive(Debug, Clone)]
33pub struct PrecisionLossSource {
34 pub operation: String,
36 pub precision_lost: f64,
38 pub description: String,
40 pub severity: PrecisionLossSeverity,
42 pub location: Option<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
48pub enum PrecisionLossSeverity {
49 Minimal,
51 Moderate,
53 Significant,
55 Severe,
57 Catastrophic,
59}
60
61impl fmt::Display for PrecisionLossSeverity {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 Self::Minimal => write!(f, "Minimal"),
65 Self::Moderate => write!(f, "Moderate"),
66 Self::Significant => write!(f, "Significant"),
67 Self::Severe => write!(f, "Severe"),
68 Self::Catastrophic => write!(f, "Catastrophic"),
69 }
70 }
71}
72
73impl Default for PrecisionContext {
74 fn default() -> Self {
75 Self {
76 precision: 15.0, error_bounds: 0.0,
78 significant_digits: 15,
79 precision_loss_sources: Vec::new(),
80 condition_number: None,
81 is_stable: true,
82 last_updated: Instant::now(),
83 }
84 }
85}
86
87impl PrecisionContext {
88 pub fn new() -> Self {
89 Self {
90 precision: 1e-15, error_bounds: 0.0,
92 significant_digits: 15,
93 precision_loss_sources: Vec::new(),
94 condition_number: None,
95 is_stable: true,
96 last_updated: Instant::now(),
97 }
98 }
99
100 pub fn with_precision(precision: f64) -> Self {
102 Self {
103 precision,
104 significant_digits: precision as u32,
105 ..Default::default()
106 }
107 }
108
109 pub fn single_precision() -> Self {
111 Self::with_precision(7.0) }
113
114 pub fn double_precision() -> Self {
116 Self::with_precision(15.0) }
118
119 pub fn extended_precision() -> Self {
121 Self::with_precision(18.0) }
123
124 pub fn update_precision(&mut self, newprecision: f64, operation: &str) {
126 if newprecision < self.precision {
127 let loss = self.precision - newprecision;
128 let severity = Self::classify_precision_loss(loss);
129
130 self.precision_loss_sources.push(PrecisionLossSource {
131 operation: operation.to_string(),
132 precision_lost: loss,
133 description: format!(
134 "Precision reduced from {:.2} to {:.2} digits",
135 self.precision, newprecision
136 ),
137 severity,
138 location: None,
139 });
140 }
141
142 self.precision = newprecision;
143 self.significant_digits = newprecision as u32;
144 self.is_stable = self.precision > 3.0; self.last_updated = Instant::now();
146 }
147
148 pub fn record_precision_loss(
150 &mut self,
151 operation: &str,
152 loss: f64,
153 description: Option<String>,
154 location: Option<String>,
155 ) {
156 let severity = Self::classify_precision_loss(loss);
157
158 self.precision_loss_sources.push(PrecisionLossSource {
159 operation: operation.to_string(),
160 precision_lost: loss,
161 description: description
162 .unwrap_or_else(|| format!("Precision loss of {loss:.2} digits in {operation}")),
163 severity,
164 location,
165 });
166
167 self.precision = (self.precision - loss).max(0.0);
168 self.significant_digits = self.precision as u32;
169 self.is_stable = self.precision > 3.0;
170 self.last_updated = Instant::now();
171 }
172
173 pub fn set_condition_number(&mut self, cond: f64) {
175 self.condition_number = Some(cond);
176
177 if cond > 1e12 {
179 self.record_precision_loss(
180 "ill_conditioning",
181 (cond.log10() - 12.0).max(0.0),
182 Some(format!("Ill-conditioned problem (κ = {cond:.2e})")),
183 None,
184 );
185 self.is_stable = false;
187 } else if cond > 1e8 {
188 self.record_precision_loss(
190 "moderate_conditioning",
191 (cond.log10() - 8.0).max(0.0),
192 Some(format!("Moderately ill-conditioned (κ = {cond:.2e})")),
193 None,
194 );
195 }
196 }
197
198 pub fn total_precision_loss(&self) -> f64 {
200 self.precision_loss_sources
201 .iter()
202 .map(|source| source.precision_lost)
203 .sum()
204 }
205
206 pub fn worst_severity(&self) -> Option<PrecisionLossSeverity> {
208 self.precision_loss_sources
209 .iter()
210 .map(|source| &source.severity)
211 .max()
212 .cloned()
213 }
214
215 pub fn has_acceptable_precision(&self, minprecision: f64) -> bool {
217 self.precision >= minprecision && self.is_stable
218 }
219
220 pub fn check_acceptable(&self, threshold: f64) -> Option<PrecisionWarning> {
222 if self.precision < threshold {
223 Some(PrecisionWarning {
224 current_precision: self.precision,
225 required_precision: threshold,
226 severity: if self.precision < threshold / 2.0 {
227 PrecisionLossSeverity::Severe
228 } else {
229 PrecisionLossSeverity::Moderate
230 },
231 message: format!(
232 "Precision ({:.2} digits) below acceptable threshold ({:.2} digits)",
233 self.precision, threshold
234 ),
235 suggestions: self.generate_suggestions(),
236 })
237 } else {
238 None
239 }
240 }
241
242 fn classify_precision_loss(loss: f64) -> PrecisionLossSeverity {
244 if loss < 1.0 {
245 PrecisionLossSeverity::Minimal
246 } else if loss < 3.0 {
247 PrecisionLossSeverity::Moderate
248 } else if loss < 6.0 {
249 PrecisionLossSeverity::Significant
250 } else if loss < 10.0 {
251 PrecisionLossSeverity::Severe
252 } else {
253 PrecisionLossSeverity::Catastrophic
254 }
255 }
256
257 fn generate_suggestions(&self) -> Vec<String> {
259 let mut suggestions = Vec::new();
260
261 if self.precision < 3.0 {
262 suggestions.push("Consider using higher precision arithmetic".to_string());
263 suggestions.push("Review algorithm for numerical stability".to_string());
264 }
265
266 if let Some(cond) = self.condition_number {
267 if cond > 1e10 {
268 suggestions.push("Problem is ill-conditioned; consider regularization".to_string());
269 suggestions.push("Try alternative algorithms or preconditioning".to_string());
270 }
271 }
272
273 for source in &self.precision_loss_sources {
274 match source.severity {
275 PrecisionLossSeverity::Significant
276 | PrecisionLossSeverity::Severe
277 | PrecisionLossSeverity::Catastrophic => {
278 suggestions.push(format!(
279 "Review {} operation for numerical stability",
280 source.operation
281 ));
282 }
283 _ => {}
284 }
285 }
286
287 if suggestions.is_empty() {
288 suggestions.push("Monitor precision throughout computation".to_string());
289 }
290
291 suggestions
292 }
293
294 pub fn check_precision_warning(&self, minprecision: f64) -> Option<PrecisionWarning> {
296 if self.precision < minprecision {
297 Some(PrecisionWarning {
298 current_precision: self.precision,
299 required_precision: minprecision,
300 severity: PrecisionLossSeverity::Severe,
301 message: format!(
302 "Precision ({:.2} digits) below required minimum ({:.2} digits)",
303 self.precision, minprecision
304 ),
305 suggestions: vec![
306 "Use higher precision data types".to_string(),
307 "Consider rescaling the problem".to_string(),
308 ],
309 })
310 } else {
311 None
312 }
313 }
314}
315
316#[derive(Debug, Clone)]
318pub struct PrecisionWarning {
319 pub current_precision: f64,
321 pub required_precision: f64,
323 pub severity: PrecisionLossSeverity,
325 pub message: String,
327 pub suggestions: Vec<String>,
329}
330
331impl fmt::Display for PrecisionWarning {
332 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333 writeln!(f, "Precision Warning ({}): {}", self.severity, self.message)?;
334 if !self.suggestions.is_empty() {
335 writeln!(f, "Suggestions:")?;
336 for suggestion in &self.suggestions {
337 writeln!(f, " • {suggestion}")?;
338 }
339 }
340 Ok(())
341 }
342}
343
344pub trait PrecisionTracked {
346 fn precision_context(&self) -> &PrecisionContext;
348
349 fn precision_context_mut(&mut self) -> &mut PrecisionContext;
351
352 fn update_precision(&mut self, resultprecision: f64, operation: &str) {
354 self.precision_context_mut()
355 .update_precision(resultprecision, operation);
356 }
357
358 fn record_loss(&mut self, operation: &str, loss: f64, description: Option<String>) {
360 self.precision_context_mut()
361 .record_precision_loss(operation, loss, description, None);
362 }
363
364 fn check_precision(&self, minprecision: f64) -> CoreResult<()> {
366 if let Some(warning) = self
367 .precision_context()
368 .check_precision_warning(minprecision)
369 {
370 Err(CoreError::ValidationError(ErrorContext::new(
371 warning.message,
372 )))
373 } else {
374 Ok(())
375 }
376 }
377}
378
379#[derive(Debug, Clone)]
381pub struct TrackedFloat<T> {
382 pub value: T,
384 pub context: PrecisionContext,
386}
387
388impl<T> TrackedFloat<T>
389where
390 T: Copy + PartialOrd + fmt::Display,
391{
392 pub fn new(value: T) -> Self {
394 Self {
395 value,
396 context: PrecisionContext::default(),
397 }
398 }
399
400 pub fn with_precision(value: T, precision: f64) -> Self {
402 Self {
403 value,
404 context: PrecisionContext::with_precision(precision),
405 }
406 }
407
408 pub fn value(&self) -> T {
410 self.value
411 }
412
413 pub fn is_finite(&self) -> bool
415 where
416 T: num_traits::Float,
417 {
418 self.value.is_finite()
419 }
420
421 pub fn is_precision_critical(&self) -> bool {
423 self.context.precision < 2.0 || !self.context.is_stable
424 }
425}
426
427impl<T> PrecisionTracked for TrackedFloat<T> {
428 fn precision_context(&self) -> &PrecisionContext {
429 &self.context
430 }
431
432 fn precision_context_mut(&mut self) -> &mut PrecisionContext {
433 &mut self.context
434 }
435}
436
437impl TrackedFloat<f64> {
439 pub fn add(&self, other: &Self) -> Self {
441 let result_value = self.value + other.value;
442 let mut result = Self::new(result_value);
443
444 let min_precision = self.context.precision.min(other.context.precision);
446 let relativeerror = estimate_additionerror(self.value, other.value);
447 let precision_loss = -relativeerror.log10().max(0.0);
448
449 result.context.precision = (min_precision - precision_loss).max(0.0);
450 result.record_loss("addition", precision_loss, None);
451
452 result
453 }
454
455 pub fn sub(&self, other: &Self) -> Self {
457 let result_value = self.value - other.value;
458 let mut result = Self::new(result_value);
459
460 let relative_magnitude =
462 (self.value - other.value).abs() / self.value.abs().max(other.value.abs());
463 if relative_magnitude < 1e-10 {
464 result.record_loss(
465 "subtraction",
466 10.0,
467 Some("Catastrophic cancellation detected".to_string()),
468 );
469 } else {
470 let min_precision = self.context.precision.min(other.context.precision);
471 let precision_loss = -relative_magnitude.log10().max(0.0);
472 result.context.precision = (min_precision - precision_loss).max(0.0);
473 result.record_loss("subtraction", precision_loss, None);
474 }
475
476 result
477 }
478
479 pub fn mul(&self, other: &Self) -> Self {
481 let result_value = self.value * other.value;
482 let mut result = Self::new(result_value);
483
484 let min_precision = self.context.precision.min(other.context.precision);
485 let relativeerror = estimate_multiplicationerror(self.value, other.value);
486 let precision_loss = -relativeerror.log10().max(0.0);
487
488 result.context.precision = (min_precision - precision_loss).max(0.0);
489 result.record_loss("multiplication", precision_loss, None);
490
491 result
492 }
493
494 pub fn div(&self, other: &Self) -> CoreResult<Self> {
496 if other.value.abs() < f64::EPSILON {
497 return Err(CoreError::DomainError(ErrorContext::new(
498 "Division by zero or near-zero value",
499 )));
500 }
501
502 let result_value = self.value / other.value;
503 let mut result = Self::new(result_value);
504
505 let min_precision = self.context.precision.min(other.context.precision);
506 let relativeerror = estimate_divisionerror(self.value, other.value);
507 let precision_loss = -relativeerror.log10().max(0.0);
508
509 if other.value.abs() < 1e-10 {
511 let extra_loss = -other.value.abs().log10() - 10.0;
512 result.record_loss(
513 "division",
514 precision_loss + extra_loss,
515 Some("Division by small number".to_string()),
516 );
517 } else {
518 result.context.precision = (min_precision - precision_loss).max(0.0);
519 result.record_loss("division", precision_loss, None);
520 }
521
522 Ok(result)
523 }
524
525 pub fn sqrt(&self) -> CoreResult<Self> {
527 if self.value < 0.0 {
528 return Err(CoreError::DomainError(ErrorContext::new(
529 "Square root of negative number",
530 )));
531 }
532
533 let result_value = self.value.sqrt();
534 let mut result = Self::new(result_value);
535
536 let precision_loss = 0.5; result.context.precision = (self.context.precision - precision_loss).max(0.0);
539 result.record_loss("sqrt", precision_loss, None);
540
541 Ok(result)
542 }
543
544 pub fn ln(&self) -> CoreResult<Self> {
546 if self.value <= 0.0 {
547 return Err(CoreError::DomainError(ErrorContext::new(
548 "Logarithm of non-positive number",
549 )));
550 }
551
552 let result_value = self.value.ln();
553 let mut result = Self::new(result_value);
554
555 if (self.value - 1.0).abs() < 1e-10 {
557 let extra_loss = -(self.value - 1.0).abs().log10() - 10.0;
558 result.record_loss(
559 "logarithm",
560 extra_loss,
561 Some("Logarithm near 1".to_string()),
562 );
563 } else {
564 let precision_loss = 1.0; result.context.precision = (self.context.precision - precision_loss).max(0.0);
566 result.record_loss("logarithm", precision_loss, None);
567 }
568
569 Ok(result)
570 }
571}
572
573#[allow(dead_code)]
575fn estimate_additionerror(a: f64, b: f64) -> f64 {
576 let result = a + b;
577 if result == 0.0 {
578 f64::EPSILON
579 } else {
580 (f64::EPSILON * (a.abs() + b.abs())) / result.abs()
581 }
582}
583
584#[allow(dead_code)]
585fn estimate_multiplicationerror(_a: f64, b: f64) -> f64 {
586 2.0 * f64::EPSILON }
588
589#[allow(dead_code)]
590fn estimate_divisionerror(a: f64, b: f64) -> f64 {
591 let relerror_a = f64::EPSILON;
592 let relerror_b = f64::EPSILON;
593 (relerror_a + relerror_b) * (a / b).abs()
594}
595
596#[derive(Debug)]
598pub struct PrecisionRegistry {
599 computations: RwLock<HashMap<String, PrecisionContext>>,
601 warnings: RwLock<Vec<PrecisionWarning>>,
603}
604
605impl PrecisionRegistry {
606 pub fn new() -> Self {
608 Self {
609 computations: RwLock::new(HashMap::new()),
610 warnings: RwLock::new(Vec::new()),
611 }
612 }
613
614 pub fn register_computation(&self, name: &str, context: PrecisionContext) -> CoreResult<()> {
616 let mut computations = self.computations.write().map_err(|_| {
617 CoreError::ComputationError(ErrorContext::new("Failed to acquire write lock"))
618 })?;
619 computations.insert(name.to_string(), context);
620 Ok(())
621 }
622
623 pub fn update_computation_precision(
625 &self,
626 name: &str,
627 precision: f64,
628 operation: &str,
629 ) -> CoreResult<()> {
630 let mut computations = self.computations.write().map_err(|_| {
631 CoreError::ComputationError(ErrorContext::new("Failed to acquire write lock"))
632 })?;
633
634 if let Some(context) = computations.get_mut(name) {
635 context.update_precision(precision, operation);
636 } else {
637 return Err(CoreError::ValidationError(ErrorContext::new(format!(
638 "Computation '{name}' not found in registry"
639 ))));
640 }
641 Ok(())
642 }
643
644 pub fn get_computation_context(&self, name: &str) -> CoreResult<Option<PrecisionContext>> {
646 let computations = self.computations.read().map_err(|_| {
647 CoreError::ComputationError(ErrorContext::new("Failed to acquire read lock"))
648 })?;
649 Ok(computations.get(name).cloned())
650 }
651
652 pub fn add_warning(&self, warning: PrecisionWarning) -> CoreResult<()> {
654 let mut warnings = self.warnings.write().map_err(|_| {
655 CoreError::ComputationError(ErrorContext::new("Failed to acquire write lock"))
656 })?;
657 warnings.push(warning);
658 Ok(())
659 }
660
661 pub fn get_warnings(&self) -> CoreResult<Vec<PrecisionWarning>> {
663 let warnings = self.warnings.read().map_err(|_| {
664 CoreError::ComputationError(ErrorContext::new("Failed to acquire read lock"))
665 })?;
666 Ok(warnings.clone())
667 }
668
669 pub fn clear_warnings(&self) -> CoreResult<()> {
671 let mut warnings = self.warnings.write().map_err(|_| {
672 CoreError::ComputationError(ErrorContext::new("Failed to acquire write lock"))
673 })?;
674 warnings.clear();
675 Ok(())
676 }
677
678 pub fn generate_report(&self) -> CoreResult<String> {
680 let computations = self.computations.read().map_err(|_| {
681 CoreError::ComputationError(ErrorContext::new("Failed to acquire read lock"))
682 })?;
683 let warnings = self.warnings.read().map_err(|_| {
684 CoreError::ComputationError(ErrorContext::new("Failed to acquire read lock"))
685 })?;
686
687 let mut report = String::new();
688 report.push_str("=== Precision Tracking Report ===\n\n");
689
690 if computations.is_empty() {
691 report.push_str("No computations tracked.\n");
692 } else {
693 report.push_str("Tracked Computations:\n");
694 for (name, context) in computations.iter() {
695 report.push_str(&format!(
696 " {}: {:.2} digits, {} sources of loss, stable: {}\n",
697 name,
698 context.precision,
699 context.precision_loss_sources.len(),
700 context.is_stable
701 ));
702 }
703 }
704
705 if !warnings.is_empty() {
706 report.push_str(&format!("\nWarnings ({}):\n", warnings.len()));
707 for warning in warnings.iter() {
708 report.push_str(&format!(" {}\n", warning.message));
709 }
710 }
711
712 Ok(report)
713 }
714}
715
716impl Default for PrecisionRegistry {
717 fn default() -> Self {
718 Self::new()
719 }
720}
721
722static GLOBAL_PRECISION_REGISTRY: std::sync::LazyLock<PrecisionRegistry> =
724 std::sync::LazyLock::new(PrecisionRegistry::new);
725
726#[allow(dead_code)]
728pub fn global_precision_registry() -> &'static PrecisionRegistry {
729 &GLOBAL_PRECISION_REGISTRY
730}
731
732#[macro_export]
734macro_rules! track_precision {
735 ($name:expr, $precision:expr) => {
736 $crate::numeric::precision_tracking::global_precision_registry()
737 .register_computation(
738 $name,
739 $crate::numeric::precision_tracking::PrecisionContext::new($precision),
740 )
741 .unwrap_or_else(|e| eprintln!("Failed to track precision: {:?}", e));
742 };
743}
744
745#[macro_export]
746macro_rules! update_precision {
747 ($name:expr, $precision:expr, $operation:expr) => {
748 $crate::numeric::precision_tracking::global_precision_registry()
749 .update_computation_precision($name, $precision, $operation)
750 .unwrap_or_else(|e| eprintln!("Failed to update precision: {:?}", e));
751 };
752}
753
754#[cfg(test)]
755mod tests {
756 use super::*;
757
758 #[test]
759 fn test_precision_context() {
760 let mut context = PrecisionContext::double_precision();
761 assert_eq!(context.precision, 15.0);
762 assert!(context.is_stable);
763
764 context.update_precision(10.0, "test_operation");
765 assert_eq!(context.precision, 10.0);
766 assert_eq!(context.precision_loss_sources.len(), 1);
767 }
768
769 #[test]
770 fn test_tracked_float_arithmetic() {
771 let a = TrackedFloat::with_precision(1.0, 15.0);
772 let b = TrackedFloat::with_precision(1e-15, 15.0);
773
774 let result = a.add(&b);
775 assert!(
777 result.context.precision <= 15.0,
778 "Precision should not increase, got: {}",
779 result.context.precision
780 );
781 let expected = 1.0 + 1e-15;
783 assert!(
784 (result.value - expected).abs() < 1e-14,
785 "Expected {}, got {}",
786 expected,
787 result.value
788 );
789 }
790
791 #[test]
792 fn test_catastrophic_cancellation() {
793 let a = TrackedFloat::with_precision(1.000_000_000_000_1, 15.0);
794 let b = TrackedFloat::with_precision(1.0, 15.0);
795
796 let result = a.sub(&b);
797 assert!(result.context.precision < 10.0);
799 assert!(!result.context.precision_loss_sources.is_empty());
800 }
801
802 #[test]
803 fn test_division_by_small_number() {
804 let a = TrackedFloat::with_precision(1.0, 15.0);
805 let b = TrackedFloat::with_precision(1e-12, 15.0);
806
807 let result = a.div(&b).expect("Division should succeed for test values");
808 assert!(result.context.precision < 15.0);
810 }
811
812 #[test]
813 fn test_precision_warning() {
814 let context = PrecisionContext::with_precision(2.0);
815 let warning = context.check_precision_warning(5.0);
816 assert!(warning.is_some());
817
818 let warning = warning.expect("Warning should be present when precision is lost");
819 assert_eq!(warning.current_precision, 2.0);
820 assert_eq!(warning.required_precision, 5.0);
821 }
822
823 #[test]
824 fn test_precision_registry() {
825 let registry = PrecisionRegistry::new();
826 let context = PrecisionContext::with_precision(10.0);
827
828 registry
829 .register_computation("test", context)
830 .expect("Registering computation should succeed");
831
832 let retrieved = registry
833 .get_computation_context("test")
834 .expect("Retrieving registered computation should succeed");
835 assert!(retrieved.is_some());
836 assert_eq!(
837 retrieved.expect("Retrieved context should exist").precision,
838 10.0
839 );
840 }
841
842 #[test]
843 fn test_sqrt_precision() {
844 let a = TrackedFloat::with_precision(4.0, 15.0);
845 let result = a
846 .sqrt()
847 .expect("Square root of positive number should succeed");
848 assert_eq!(result.value, 2.0);
849 assert!(result.context.precision < 15.0); }
851
852 #[test]
853 fn test_ln_near_one() {
854 let a = TrackedFloat::with_precision(1.0 + 1e-12, 15.0);
855 let result = a
856 .ln()
857 .expect("Natural log of positive number should succeed");
858 assert!(
860 result.context.precision < 15.0,
861 "Expected precision loss for ln near 1, got precision: {}",
862 result.context.precision
863 );
864 assert!(
866 !result.context.precision_loss_sources.is_empty(),
867 "Should have recorded precision loss sources"
868 );
869 }
870
871 #[test]
872 fn test_condition_number() {
873 let mut context = PrecisionContext::double_precision();
874 assert!(context.is_stable);
876 assert!(context.precision_loss_sources.is_empty());
877
878 context.set_condition_number(1e15);
879
880 assert!(
883 !context.is_stable,
884 "Context should be unstable with condition number 1e15"
885 );
886 assert!(
887 !context.precision_loss_sources.is_empty(),
888 "Should have precision loss sources after setting high condition number"
889 );
890 assert!(context.condition_number.is_some());
891 assert_eq!(
892 context
893 .condition_number
894 .expect("Condition number should be set"),
895 1e15
896 );
897 }
898
899 #[test]
900 fn test_precision_loss_severity() {
901 assert_eq!(
902 PrecisionContext::classify_precision_loss(0.5),
903 PrecisionLossSeverity::Minimal
904 );
905 assert_eq!(
906 PrecisionContext::classify_precision_loss(2.0),
907 PrecisionLossSeverity::Moderate
908 );
909 assert_eq!(
910 PrecisionContext::classify_precision_loss(5.0),
911 PrecisionLossSeverity::Significant
912 );
913 assert_eq!(
914 PrecisionContext::classify_precision_loss(8.0),
915 PrecisionLossSeverity::Severe
916 );
917 assert_eq!(
918 PrecisionContext::classify_precision_loss(12.0),
919 PrecisionLossSeverity::Catastrophic
920 );
921 }
922}