1use std::fmt;
41use std::time::Instant;
42
43#[derive(Debug, Clone)]
49pub struct ValidationOutcome {
50 pub passed: bool,
52 pub input: f64,
54 pub expected: f64,
56 pub computed: f64,
58 pub abs_error: f64,
60 pub rel_error: Option<f64>,
63}
64
65impl ValidationOutcome {
66 pub fn new(input: f64, expected: f64, computed: f64, tol_rel: f64, tol_abs: f64) -> Self {
73 let abs_error = (computed - expected).abs();
74 let rel_error = if expected.abs() > f64::EPSILON {
75 Some(abs_error / expected.abs())
76 } else {
77 None
78 };
79 let passed = abs_error <= tol_abs || rel_error.is_some_and(|r| r <= tol_rel);
80 Self {
81 passed,
82 input,
83 expected,
84 computed,
85 abs_error,
86 rel_error,
87 }
88 }
89}
90
91impl fmt::Display for ValidationOutcome {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 let rel = self
94 .rel_error
95 .map_or_else(|| "N/A".to_string(), |r| format!("{r:.3e}"));
96 write!(
97 f,
98 "x={:.6e} | expected={:.6e} | computed={:.6e} | abs_err={:.3e} | rel_err={} | {}",
99 self.input,
100 self.expected,
101 self.computed,
102 self.abs_error,
103 rel,
104 if self.passed { "PASS" } else { "FAIL" }
105 )
106 }
107}
108
109#[derive(Debug, Clone)]
116pub struct NumericalValidationReport {
117 pub function_name: String,
119 pub total_checks: usize,
121 pub passed_checks: usize,
123 pub failures: Vec<ValidationOutcome>,
125 pub timing_ns: u64,
127 pub max_abs_error: f64,
129 pub max_rel_error: f64,
131}
132
133impl NumericalValidationReport {
134 pub fn pass_rate(&self) -> f64 {
136 if self.total_checks == 0 {
137 1.0
138 } else {
139 self.passed_checks as f64 / self.total_checks as f64
140 }
141 }
142
143 pub fn all_passed(&self) -> bool {
145 self.failures.is_empty()
146 }
147
148 pub fn to_markdown(&self) -> String {
151 let mut md = String::new();
152
153 md.push_str(&format!(
154 "## Validation Report: `{}`\n\n",
155 self.function_name
156 ));
157 md.push_str("| Metric | Value |\n");
158 md.push_str("|--------|-------|\n");
159 md.push_str(&format!("| Total checks | {} |\n", self.total_checks));
160 md.push_str(&format!("| Passed checks | {} |\n", self.passed_checks));
161 md.push_str(&format!(
162 "| Pass rate | {:.1}% |\n",
163 self.pass_rate() * 100.0
164 ));
165 md.push_str(&format!(
166 "| Max absolute error | {:.3e} |\n",
167 self.max_abs_error
168 ));
169 md.push_str(&format!(
170 "| Max relative error | {:.3e} |\n",
171 self.max_rel_error
172 ));
173 md.push_str(&format!("| Timing | {} ns |\n", self.timing_ns));
174 md.push_str(&format!(
175 "| Status | {} |\n\n",
176 if self.all_passed() {
177 "✓ PASS"
178 } else {
179 "✗ FAIL"
180 }
181 ));
182
183 if !self.failures.is_empty() {
184 md.push_str("### Failures\n\n");
185 md.push_str("| Input | Expected | Computed | Abs Error | Rel Error |\n");
186 md.push_str("|-------|----------|----------|-----------|-----------|\n");
187 for f in &self.failures {
188 let rel = f
189 .rel_error
190 .map_or_else(|| "N/A".to_string(), |r| format!("{r:.3e}"));
191 md.push_str(&format!(
192 "| {:.6e} | {:.6e} | {:.6e} | {:.3e} | {} |\n",
193 f.input, f.expected, f.computed, f.abs_error, rel
194 ));
195 }
196 }
197
198 md
199 }
200}
201
202impl fmt::Display for NumericalValidationReport {
203 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
204 write!(
205 f,
206 "{}: {}/{} passed ({:.1}%), max_abs={:.3e}, max_rel={:.3e}",
207 self.function_name,
208 self.passed_checks,
209 self.total_checks,
210 self.pass_rate() * 100.0,
211 self.max_abs_error,
212 self.max_rel_error,
213 )
214 }
215}
216
217#[derive(Debug, Clone)]
223pub struct NumericalValidationConfig {
224 pub relative_tolerance: f64,
226 pub absolute_tolerance: f64,
228 pub num_test_points: usize,
230 pub seed: u64,
232}
233
234impl Default for NumericalValidationConfig {
235 fn default() -> Self {
236 Self {
237 relative_tolerance: 1e-6,
238 absolute_tolerance: 1e-10,
239 num_test_points: 100,
240 seed: 42,
241 }
242 }
243}
244
245pub trait NumericalValidator: Send + Sync {
254 fn name(&self) -> &str;
256
257 fn validate(
259 &self,
260 input: f64,
261 computed: f64,
262 reference: f64,
263 config: &NumericalValidationConfig,
264 ) -> ValidationOutcome;
265
266 fn run_validation(
269 &self,
270 computed_fn: &dyn Fn(f64) -> f64,
271 reference_fn: &dyn Fn(f64) -> f64,
272 inputs: &[f64],
273 config: &NumericalValidationConfig,
274 ) -> NumericalValidationReport;
275}
276
277#[derive(Debug, Clone)]
286pub struct ComparisonValidator {
287 name: String,
288}
289
290impl ComparisonValidator {
291 pub fn new(name: impl Into<String>) -> Self {
293 Self { name: name.into() }
294 }
295}
296
297impl NumericalValidator for ComparisonValidator {
298 fn name(&self) -> &str {
299 &self.name
300 }
301
302 fn validate(
303 &self,
304 input: f64,
305 computed: f64,
306 reference: f64,
307 config: &NumericalValidationConfig,
308 ) -> ValidationOutcome {
309 ValidationOutcome::new(
310 input,
311 reference,
312 computed,
313 config.relative_tolerance,
314 config.absolute_tolerance,
315 )
316 }
317
318 fn run_validation(
319 &self,
320 computed_fn: &dyn Fn(f64) -> f64,
321 reference_fn: &dyn Fn(f64) -> f64,
322 inputs: &[f64],
323 config: &NumericalValidationConfig,
324 ) -> NumericalValidationReport {
325 let start = Instant::now();
326 let mut outcomes: Vec<ValidationOutcome> = Vec::with_capacity(inputs.len());
327
328 for &x in inputs {
329 let computed = computed_fn(x);
330 let reference = reference_fn(x);
331 outcomes.push(self.validate(x, computed, reference, config));
332 }
333
334 let elapsed_ns = start.elapsed().as_nanos() as u64;
335 let total = outcomes.len();
336 let passed = outcomes.iter().filter(|o| o.passed).count();
337 let failures: Vec<ValidationOutcome> =
338 outcomes.iter().filter(|o| !o.passed).cloned().collect();
339
340 let max_abs_error = outcomes.iter().map(|o| o.abs_error).fold(0.0_f64, f64::max);
341 let max_rel_error = outcomes
342 .iter()
343 .filter_map(|o| o.rel_error)
344 .fold(0.0_f64, f64::max);
345
346 NumericalValidationReport {
347 function_name: self.name.clone(),
348 total_checks: total,
349 passed_checks: passed,
350 failures,
351 timing_ns: elapsed_ns,
352 max_abs_error,
353 max_rel_error,
354 }
355 }
356}
357
358#[derive(Debug, Clone)]
368pub struct MonotonicityChecker {
369 pub increasing: bool,
371 pub tolerance: f64,
374}
375
376impl MonotonicityChecker {
377 pub fn new_increasing(tolerance: f64) -> Self {
379 Self {
380 increasing: true,
381 tolerance,
382 }
383 }
384
385 pub fn new_decreasing(tolerance: f64) -> Self {
387 Self {
388 increasing: false,
389 tolerance,
390 }
391 }
392
393 pub fn check(&self, f: &dyn Fn(f64) -> f64, inputs: &[f64]) -> bool {
397 if inputs.len() < 2 {
398 return true;
399 }
400 let values: Vec<f64> = inputs.iter().map(|&x| f(x)).collect();
401 for window in values.windows(2) {
402 let (prev, next) = (window[0], window[1]);
403 if self.increasing {
404 if next < prev - self.tolerance {
406 return false;
407 }
408 } else {
409 if next > prev + self.tolerance {
411 return false;
412 }
413 }
414 }
415 true
416 }
417
418 pub fn check_with_first_violation(
421 &self,
422 f: &dyn Fn(f64) -> f64,
423 inputs: &[f64],
424 ) -> (bool, Option<usize>) {
425 if inputs.len() < 2 {
426 return (true, None);
427 }
428 let values: Vec<f64> = inputs.iter().map(|&x| f(x)).collect();
429 for (i, window) in values.windows(2).enumerate() {
430 let (prev, next) = (window[0], window[1]);
431 let violated = if self.increasing {
432 next < prev - self.tolerance
433 } else {
434 next > prev + self.tolerance
435 };
436 if violated {
437 return (false, Some(i));
438 }
439 }
440 (true, None)
441 }
442}
443
444#[derive(Debug, Clone)]
453pub struct BoundaryChecker {
454 pub name: String,
456}
457
458impl BoundaryChecker {
459 pub fn new(name: impl Into<String>) -> Self {
461 Self { name: name.into() }
462 }
463
464 pub fn check_bounds(
469 &self,
470 f: &dyn Fn(f64) -> f64,
471 lower: f64,
472 upper: f64,
473 expected_lower: f64,
474 expected_upper: f64,
475 tolerance: f64,
476 ) -> (bool, bool) {
477 let lower_ok = (f(lower) - expected_lower).abs() <= tolerance;
478 let upper_ok = (f(upper) - expected_upper).abs() <= tolerance;
479 (lower_ok, upper_ok)
480 }
481
482 pub fn check_point(
484 &self,
485 f: &dyn Fn(f64) -> f64,
486 point: f64,
487 expected: f64,
488 tolerance: f64,
489 ) -> bool {
490 (f(point) - expected).abs() <= tolerance
491 }
492
493 pub fn check_multiple(
496 &self,
497 f: &dyn Fn(f64) -> f64,
498 points: &[(f64, f64)],
499 tolerance: f64,
500 ) -> usize {
501 points
502 .iter()
503 .filter(|&&(x, expected)| (f(x) - expected).abs() <= tolerance)
504 .count()
505 }
506}
507
508#[derive(Debug, Clone)]
518pub struct SymmetryChecker {
519 pub even: bool,
522 pub tolerance: f64,
524}
525
526impl SymmetryChecker {
527 pub fn new_even(tolerance: f64) -> Self {
529 Self {
530 even: true,
531 tolerance,
532 }
533 }
534
535 pub fn new_odd(tolerance: f64) -> Self {
537 Self {
538 even: false,
539 tolerance,
540 }
541 }
542
543 pub fn check(&self, f: &dyn Fn(f64) -> f64, inputs: &[f64]) -> bool {
549 for &x in inputs {
550 let fx = f(x);
551 let fnx = f(-x);
552 let expected = if self.even { fx } else { -fx };
553 if (fnx - expected).abs() > self.tolerance {
554 return false;
555 }
556 }
557 true
558 }
559
560 pub fn check_with_first_violation(
562 &self,
563 f: &dyn Fn(f64) -> f64,
564 inputs: &[f64],
565 ) -> (bool, Option<usize>) {
566 for (i, &x) in inputs.iter().enumerate() {
567 let fx = f(x);
568 let fnx = f(-x);
569 let expected = if self.even { fx } else { -fx };
570 if (fnx - expected).abs() > self.tolerance {
571 return (false, Some(i));
572 }
573 }
574 (true, None)
575 }
576}
577
578#[cfg(test)]
583mod tests {
584 use super::*;
585 use std::f64::consts::PI;
586
587 #[test]
592 fn test_outcome_pass_within_abs_tolerance() {
593 let outcome = ValidationOutcome::new(1.0, 2.0, 2.0 + 1e-11, 1e-6, 1e-10);
594 assert!(outcome.passed);
595 assert!(outcome.abs_error < 1e-10);
596 }
597
598 #[test]
599 fn test_outcome_pass_within_rel_tolerance() {
600 let outcome = ValidationOutcome::new(1.0, 1000.0, 1000.0 * (1.0 + 5e-7), 1e-6, 1e-10);
601 assert!(outcome.passed);
602 }
603
604 #[test]
605 fn test_outcome_fail() {
606 let outcome = ValidationOutcome::new(1.0, 1.0, 2.0, 1e-6, 1e-10);
607 assert!(!outcome.passed);
608 assert!((outcome.abs_error - 1.0).abs() < 1e-12);
609 }
610
611 #[test]
612 fn test_outcome_rel_error_undefined_near_zero() {
613 let outcome = ValidationOutcome::new(0.0, 0.0, 1e-15, 1e-6, 1e-10);
614 assert!(outcome.rel_error.is_none());
616 }
617
618 #[test]
619 fn test_outcome_display() {
620 let outcome = ValidationOutcome::new(0.5, 0.5, 0.5, 1e-6, 1e-10);
621 let s = format!("{outcome}");
622 assert!(s.contains("PASS"));
623 }
624
625 #[test]
630 fn test_report_pass_rate_empty() {
631 let report = NumericalValidationReport {
632 function_name: "empty".to_string(),
633 total_checks: 0,
634 passed_checks: 0,
635 failures: vec![],
636 timing_ns: 0,
637 max_abs_error: 0.0,
638 max_rel_error: 0.0,
639 };
640 assert!((report.pass_rate() - 1.0).abs() < 1e-12);
641 }
642
643 #[test]
644 fn test_report_pass_rate_partial() {
645 let report = NumericalValidationReport {
646 function_name: "partial".to_string(),
647 total_checks: 4,
648 passed_checks: 3,
649 failures: vec![ValidationOutcome::new(0.0, 1.0, 2.0, 1e-6, 1e-10)],
650 timing_ns: 1000,
651 max_abs_error: 1.0,
652 max_rel_error: 1.0,
653 };
654 assert!((report.pass_rate() - 0.75).abs() < 1e-12);
655 assert!(!report.all_passed());
656 }
657
658 #[test]
659 fn test_report_markdown_format() {
660 let report = NumericalValidationReport {
661 function_name: "cosine".to_string(),
662 total_checks: 10,
663 passed_checks: 10,
664 failures: vec![],
665 timing_ns: 12345,
666 max_abs_error: 1e-12,
667 max_rel_error: 1e-11,
668 };
669 let md = report.to_markdown();
670 assert!(md.contains("## Validation Report: `cosine`"));
671 assert!(md.contains("PASS"));
672 assert!(md.contains("100.0%"));
673 }
674
675 #[test]
676 fn test_report_markdown_includes_failure_table() {
677 let report = NumericalValidationReport {
678 function_name: "broken_fn".to_string(),
679 total_checks: 2,
680 passed_checks: 1,
681 failures: vec![ValidationOutcome::new(
682 std::f64::consts::PI,
683 0.0,
684 1.0,
685 1e-6,
686 1e-10,
687 )],
688 timing_ns: 500,
689 max_abs_error: 1.0,
690 max_rel_error: 0.0,
691 };
692 let md = report.to_markdown();
693 assert!(md.contains("### Failures"));
694 assert!(md.contains("FAIL"));
695 }
696
697 #[test]
702 fn test_comparison_validator_identity() {
703 let v = ComparisonValidator::new("identity");
704 let config = NumericalValidationConfig::default();
705 let inputs: Vec<f64> = (0..=20).map(|i| i as f64 * 0.1 - 1.0).collect();
706 let report = v.run_validation(&|x| x, &|x| x, &inputs, &config);
707 assert!(report.all_passed(), "Identity should pass: {report}");
708 assert_eq!(report.total_checks, inputs.len());
709 }
710
711 #[test]
712 fn test_comparison_validator_sin_reference() {
713 let v = ComparisonValidator::new("sin_vs_itself");
714 let config = NumericalValidationConfig::default();
715 let inputs: Vec<f64> = (0..50).map(|i| i as f64 * PI / 50.0).collect();
716 let report = v.run_validation(&|x| x.sin(), &|x| x.sin(), &inputs, &config);
717 assert!(report.all_passed());
718 }
719
720 #[test]
721 fn test_comparison_validator_detects_errors() {
722 let v = ComparisonValidator::new("wrong_fn");
723 let config = NumericalValidationConfig {
724 relative_tolerance: 1e-3,
725 absolute_tolerance: 1e-3,
726 ..Default::default()
727 };
728 let inputs = vec![1.0, 2.0, 3.0];
729 let report = v.run_validation(&|x| x + 1.0, &|x| x, &inputs, &config);
731 assert!(!report.all_passed());
732 assert_eq!(report.failures.len(), 3);
733 }
734
735 #[test]
736 fn test_comparison_validator_name() {
737 let v = ComparisonValidator::new("my_validator");
738 assert_eq!(v.name(), "my_validator");
739 }
740
741 #[test]
746 fn test_monotonicity_increasing_ok() {
747 let checker = MonotonicityChecker::new_increasing(1e-12);
748 let inputs: Vec<f64> = (0..=20).map(|i| i as f64 * 0.1).collect();
749 assert!(checker.check(&|x: f64| x.powi(2), &inputs));
750 }
751
752 #[test]
753 fn test_monotonicity_decreasing_ok() {
754 let checker = MonotonicityChecker::new_decreasing(1e-12);
755 let inputs: Vec<f64> = (0..=20).map(|i| i as f64 * 0.1).collect();
756 assert!(checker.check(&|x: f64| -x, &inputs));
757 }
758
759 #[test]
760 fn test_monotonicity_violation_detected() {
761 let checker = MonotonicityChecker::new_increasing(1e-12);
762 let inputs: Vec<f64> = (0..=100).map(|i| i as f64 * 2.0 * PI / 100.0).collect();
764 let (ok, idx) = checker.check_with_first_violation(&|x: f64| x.sin(), &inputs);
765 assert!(!ok);
766 assert!(idx.is_some());
767 }
768
769 #[test]
770 fn test_monotonicity_single_point_always_passes() {
771 let checker = MonotonicityChecker::new_increasing(0.0);
772 assert!(checker.check(&|x| x, &[1.0]));
773 }
774
775 #[test]
776 fn test_monotonicity_cdf_like() {
777 let checker = MonotonicityChecker::new_increasing(1e-12);
779 let logistic = |x: f64| 1.0 / (1.0 + (-x).exp());
780 let inputs: Vec<f64> = (-50..=50).map(|i| i as f64 * 0.1).collect();
781 assert!(checker.check(&logistic, &inputs));
782 }
783
784 #[test]
789 fn test_boundary_check_bounds_pass() {
790 let bc = BoundaryChecker::new("logistic_cdf");
791 let logistic = |x: f64| 1.0 / (1.0 + (-x).exp());
792 let (lo_ok, hi_ok) = bc.check_bounds(&logistic, -100.0, 100.0, 0.0, 1.0, 1e-6);
793 assert!(lo_ok);
794 assert!(hi_ok);
795 }
796
797 #[test]
798 fn test_boundary_check_point() {
799 let bc = BoundaryChecker::new("identity");
800 assert!(bc.check_point(&|x| x, 0.0, 0.0, 1e-12));
801 assert!(!bc.check_point(&|x| x + 1.0, 0.0, 0.0, 0.5));
802 }
803
804 #[test]
805 fn test_boundary_check_multiple() {
806 let bc = BoundaryChecker::new("quadratic");
807 let points = vec![(0.0, 0.0), (1.0, 1.0), (2.0, 4.0), (3.0, 9.0)];
808 let ok = bc.check_multiple(&|x| x * x, &points, 1e-12);
809 assert_eq!(ok, 4);
810 }
811
812 #[test]
813 fn test_boundary_fail() {
814 let bc = BoundaryChecker::new("bad");
815 let (lo_ok, hi_ok) = bc.check_bounds(&|_| 0.5, -1.0, 1.0, 0.0, 1.0, 1e-6);
816 assert!(!lo_ok);
817 assert!(!hi_ok);
818 }
819
820 #[test]
825 fn test_symmetry_even_cosine() {
826 let sc = SymmetryChecker::new_even(1e-12);
827 let inputs: Vec<f64> = (1..=20).map(|i| i as f64 * 0.1).collect();
828 assert!(sc.check(&|x: f64| x.cos(), &inputs));
829 }
830
831 #[test]
832 fn test_symmetry_odd_sine() {
833 let sc = SymmetryChecker::new_odd(1e-12);
834 let inputs: Vec<f64> = (1..=20).map(|i| i as f64 * 0.1).collect();
835 assert!(sc.check(&|x: f64| x.sin(), &inputs));
836 }
837
838 #[test]
839 fn test_symmetry_even_violation() {
840 let sc = SymmetryChecker::new_even(1e-12);
841 let inputs = vec![0.5, 1.0];
843 assert!(!sc.check(&|x: f64| x.sin(), &inputs));
844 }
845
846 #[test]
847 fn test_symmetry_odd_violation() {
848 let sc = SymmetryChecker::new_odd(1e-12);
849 let inputs = vec![0.5, 1.0];
851 assert!(!sc.check(&|x: f64| x.cos(), &inputs));
852 }
853
854 #[test]
855 fn test_symmetry_with_first_violation() {
856 let sc = SymmetryChecker::new_even(1e-12);
857 let (ok, idx) = sc.check_with_first_violation(&|x: f64| x.sin(), &[0.5, 1.0]);
858 assert!(!ok);
859 assert_eq!(idx, Some(0));
860 }
861
862 #[test]
863 fn test_symmetry_empty_input() {
864 let sc = SymmetryChecker::new_even(1e-12);
865 assert!(sc.check(&|x| x, &[]));
866 }
867
868 #[test]
873 fn test_config_default_values() {
874 let cfg = NumericalValidationConfig::default();
875 assert_eq!(cfg.relative_tolerance, 1e-6);
876 assert_eq!(cfg.absolute_tolerance, 1e-10);
877 assert_eq!(cfg.num_test_points, 100);
878 assert_eq!(cfg.seed, 42);
879 }
880}