Skip to main content

tenflowers_core/
gradient_validation_framework.rs

1/// Gradient Validation Framework with Property-Based Testing
2///
3/// This module provides a comprehensive framework for validating gradient implementations
4/// using property-based testing strategies. It integrates with the gradient coverage audit
5/// to ensure all operations are tested systematically.
6///
7/// ## Features
8///
9/// - **Property-Based Testing**: Automatically generates test cases with various properties
10/// - **Systematic Coverage**: Tests all operations across dtypes and shapes
11/// - **Numerical Validation**: Compares analytical vs numerical gradients
12/// - **Error Analysis**: Detailed reporting of gradient errors
13/// - **CI Integration**: Generates reports suitable for continuous integration
14///
15/// ## Usage
16///
17/// ```rust,ignore
18/// use tenflowers_core::gradient_validation_framework::{GradientValidator, get_validator};
19///
20/// // Get the global validator
21/// let validator = get_validator();
22///
23/// // Run comprehensive validation
24/// let report = validator.validate_all_operations();
25///
26/// // Check specific operation
27/// let result = validator.validate_operation("matmul");
28/// assert!(result.all_tests_passed());
29/// ```
30use crate::gradient_coverage_audit::{get_auditor, GradientStatus};
31use crate::gradient_executor::{get_gradient_executor, GradientExecutor};
32use crate::numerical_gradient::{GradientCheckConfig, NumericalGradientChecker};
33use crate::ops::shape_inference_registry::{get_registry, OperationMetadata};
34use crate::{DType, Result, Shape, Tensor, TensorError};
35use std::collections::HashMap;
36use std::sync::{Arc, Mutex, OnceLock};
37
38/// Property that a gradient must satisfy
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum GradientProperty {
41    /// Gradient should match numerical gradient
42    NumericalConsistency,
43    /// Gradient should be zero for constant functions
44    ZeroForConstants,
45    /// Gradient should be linear (additivity)
46    Linearity,
47    /// Gradient should satisfy chain rule
48    ChainRule,
49    /// Gradient should be finite (no NaN/Inf)
50    Finiteness,
51    /// Gradient shape should match input shape
52    ShapeConsistency,
53}
54
55impl GradientProperty {
56    /// Get a description of this property
57    pub fn description(&self) -> &'static str {
58        match self {
59            Self::NumericalConsistency => "Analytical gradient matches numerical approximation",
60            Self::ZeroForConstants => "Gradient is zero for constant functions",
61            Self::Linearity => "Gradient satisfies linearity (additivity)",
62            Self::ChainRule => "Gradient satisfies chain rule composition",
63            Self::Finiteness => "Gradient contains only finite values",
64            Self::ShapeConsistency => "Gradient shape matches input shape",
65        }
66    }
67}
68
69/// Test case for gradient validation
70#[derive(Debug, Clone)]
71pub struct GradientTestCase {
72    /// Operation being tested
73    pub operation: String,
74    /// Data type being tested
75    pub dtype: DType,
76    /// Input shapes for the test
77    pub input_shapes: Vec<Shape>,
78    /// Properties to check
79    pub properties: Vec<GradientProperty>,
80    /// Configuration for numerical checking
81    pub config: GradientCheckConfig,
82}
83
84impl GradientTestCase {
85    pub fn new(operation: &str, dtype: DType, input_shapes: Vec<Shape>) -> Self {
86        Self {
87            operation: operation.to_string(),
88            dtype,
89            input_shapes,
90            properties: vec![
91                GradientProperty::NumericalConsistency,
92                GradientProperty::Finiteness,
93                GradientProperty::ShapeConsistency,
94            ],
95            config: GradientCheckConfig::default(),
96        }
97    }
98
99    /// Add a property to check
100    pub fn with_property(mut self, property: GradientProperty) -> Self {
101        if !self.properties.contains(&property) {
102            self.properties.push(property);
103        }
104        self
105    }
106
107    /// Set custom configuration
108    pub fn with_config(mut self, config: GradientCheckConfig) -> Self {
109        self.config = config;
110        self
111    }
112}
113
114/// Result of validating a single test case
115#[derive(Debug, Clone)]
116pub struct ValidationResult {
117    /// Test case that was run
118    pub test_case: GradientTestCase,
119    /// Whether all properties passed
120    pub passed: bool,
121    /// Results for each property
122    pub property_results: HashMap<GradientProperty, PropertyCheckResult>,
123    /// Execution time in milliseconds
124    pub execution_time_ms: u64,
125    /// Error message if validation failed
126    pub error: Option<String>,
127}
128
129impl ValidationResult {
130    pub fn new(test_case: GradientTestCase) -> Self {
131        Self {
132            test_case,
133            passed: true,
134            property_results: HashMap::new(),
135            execution_time_ms: 0,
136            error: None,
137        }
138    }
139
140    /// Check if all properties passed
141    pub fn all_properties_passed(&self) -> bool {
142        self.property_results.values().all(|r| r.passed)
143    }
144
145    /// Get failed properties
146    pub fn failed_properties(&self) -> Vec<GradientProperty> {
147        self.property_results
148            .iter()
149            .filter(|(_, result)| !result.passed)
150            .map(|(prop, _)| *prop)
151            .collect()
152    }
153}
154
155/// Result of checking a single property
156#[derive(Debug, Clone)]
157pub struct PropertyCheckResult {
158    /// Property that was checked
159    pub property: GradientProperty,
160    /// Whether the check passed
161    pub passed: bool,
162    /// Maximum error observed (if applicable)
163    pub max_error: Option<f64>,
164    /// Details about the check
165    pub details: String,
166}
167
168/// Validation report for an operation
169#[derive(Debug, Clone)]
170pub struct OperationValidationReport {
171    /// Operation name
172    pub operation: String,
173    /// All test cases run
174    pub test_cases: Vec<ValidationResult>,
175    /// Total tests run
176    pub total_tests: usize,
177    /// Tests passed
178    pub tests_passed: usize,
179    /// Tests failed
180    pub tests_failed: usize,
181    /// Coverage percentage
182    pub coverage_percentage: f64,
183}
184
185impl OperationValidationReport {
186    pub fn new(operation: &str) -> Self {
187        Self {
188            operation: operation.to_string(),
189            test_cases: Vec::new(),
190            total_tests: 0,
191            tests_passed: 0,
192            tests_failed: 0,
193            coverage_percentage: 0.0,
194        }
195    }
196
197    /// Add a test result
198    pub fn add_result(&mut self, result: ValidationResult) {
199        self.total_tests += 1;
200        if result.passed {
201            self.tests_passed += 1;
202        } else {
203            self.tests_failed += 1;
204        }
205        self.test_cases.push(result);
206        self.update_coverage();
207    }
208
209    /// Update coverage percentage
210    fn update_coverage(&mut self) {
211        if self.total_tests > 0 {
212            self.coverage_percentage = (self.tests_passed as f64 / self.total_tests as f64) * 100.0;
213        }
214    }
215
216    /// Check if all tests passed
217    pub fn all_tests_passed(&self) -> bool {
218        self.tests_failed == 0 && self.total_tests > 0
219    }
220
221    /// Print summary
222    pub fn print_summary(&self) {
223        println!("\n╔══════════════════════════════════════════════════════════════╗");
224        println!(
225            "║  Gradient Validation: {}                             ",
226            self.operation
227        );
228        println!("╚══════════════════════════════════════════════════════════════╝\n");
229
230        println!("Total Tests:   {}", self.total_tests);
231        println!(
232            "Passed:        {} ({:.1}%)",
233            self.tests_passed, self.coverage_percentage
234        );
235        println!("Failed:        {}", self.tests_failed);
236
237        if self.tests_failed > 0 {
238            println!("\n✗ Failed Tests:");
239            for result in &self.test_cases {
240                if !result.passed {
241                    println!(
242                        "  • {:?} on {:?}",
243                        result.test_case.dtype, result.test_case.input_shapes
244                    );
245                    if let Some(ref err) = result.error {
246                        println!("    Error: {}", err);
247                    }
248                    for prop in result.failed_properties() {
249                        println!("    Failed property: {:?}", prop);
250                    }
251                }
252            }
253        }
254
255        println!("\n");
256    }
257}
258
259/// Comprehensive validation report for all operations
260#[derive(Debug, Clone)]
261pub struct ComprehensiveValidationReport {
262    /// Reports for each operation
263    pub operations: HashMap<String, OperationValidationReport>,
264    /// Total operations tested
265    pub total_operations: usize,
266    /// Operations with all tests passing
267    pub operations_passed: usize,
268    /// Operations with failures
269    pub operations_failed: usize,
270    /// Overall validation timestamp
271    pub timestamp: std::time::SystemTime,
272}
273
274impl Default for ComprehensiveValidationReport {
275    fn default() -> Self {
276        Self::new()
277    }
278}
279
280impl ComprehensiveValidationReport {
281    pub fn new() -> Self {
282        Self {
283            operations: HashMap::new(),
284            total_operations: 0,
285            operations_passed: 0,
286            operations_failed: 0,
287            timestamp: std::time::SystemTime::now(),
288        }
289    }
290
291    /// Add an operation report
292    pub fn add_operation_report(&mut self, report: OperationValidationReport) {
293        self.total_operations += 1;
294        if report.all_tests_passed() {
295            self.operations_passed += 1;
296        } else {
297            self.operations_failed += 1;
298        }
299        self.operations.insert(report.operation.clone(), report);
300    }
301
302    /// Calculate overall pass rate
303    pub fn overall_pass_rate(&self) -> f64 {
304        if self.total_operations == 0 {
305            0.0
306        } else {
307            (self.operations_passed as f64 / self.total_operations as f64) * 100.0
308        }
309    }
310
311    /// Print comprehensive summary
312    pub fn print_summary(&self) {
313        println!("\n╔══════════════════════════════════════════════════════════════╗");
314        println!("║        Comprehensive Gradient Validation Report             ║");
315        println!("╚══════════════════════════════════════════════════════════════╝\n");
316
317        println!("Overall Pass Rate: {:.1}%", self.overall_pass_rate());
318        println!("\nOperation Summary:");
319        println!("  ✓ All Passed:  {} operations", self.operations_passed);
320        println!("  ✗ Some Failed: {} operations", self.operations_failed);
321        println!("  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
322        println!("  Total:         {} operations", self.total_operations);
323
324        if self.operations_failed > 0 {
325            println!("\n⚠ Operations with Validation Failures:");
326            for (op_name, report) in &self.operations {
327                if !report.all_tests_passed() {
328                    println!(
329                        "  • {} ({}/{} tests passed)",
330                        op_name, report.tests_passed, report.total_tests
331                    );
332                }
333            }
334        }
335
336        println!("\n");
337    }
338}
339
340/// Gradient validation framework
341pub struct GradientValidator {
342    /// Test configurations for different operation types
343    configs: Arc<Mutex<HashMap<String, GradientCheckConfig>>>,
344    /// Test shapes to use
345    test_shapes: Vec<Shape>,
346    /// Test dtypes to use
347    test_dtypes: Vec<DType>,
348}
349
350impl Default for GradientValidator {
351    fn default() -> Self {
352        Self::new()
353    }
354}
355
356impl GradientValidator {
357    /// Create a new gradient validator
358    pub fn new() -> Self {
359        Self {
360            configs: Arc::new(Mutex::new(HashMap::new())),
361            test_shapes: vec![
362                Shape::from_slice(&[2, 3]),
363                Shape::from_slice(&[4, 5]),
364                Shape::from_slice(&[1, 10]),
365            ],
366            test_dtypes: vec![DType::Float32, DType::Float64],
367        }
368    }
369
370    /// Set custom configuration for an operation
371    pub fn set_operation_config(&self, operation: &str, config: GradientCheckConfig) {
372        if let Ok(mut g) = self.configs.lock() {
373            g.insert(operation.to_string(), config);
374        }
375    }
376
377    /// Get configuration for an operation
378    fn get_config(&self, operation: &str) -> GradientCheckConfig {
379        match self.configs.lock() {
380            Ok(g) => g.get(operation).cloned().unwrap_or_default(),
381            Err(_) => GradientCheckConfig::default(),
382        }
383    }
384
385    /// Generate test cases for an operation
386    pub fn generate_test_cases(&self, operation: &str) -> Vec<GradientTestCase> {
387        let mut test_cases = Vec::new();
388        let config = self.get_config(operation);
389
390        // Generate test cases for each dtype and shape combination
391        for &dtype in &self.test_dtypes {
392            for shape in &self.test_shapes {
393                let test_case = GradientTestCase::new(operation, dtype, vec![shape.clone()])
394                    .with_config(config.clone());
395                test_cases.push(test_case);
396            }
397        }
398
399        test_cases
400    }
401
402    /// Validate a single test case
403    pub fn validate_test_case(&self, test_case: GradientTestCase) -> ValidationResult {
404        let start = std::time::Instant::now();
405        let mut result = ValidationResult::new(test_case.clone());
406
407        // Check each property
408        for &property in &test_case.properties {
409            let prop_result = self.check_property(&test_case, property);
410            if !prop_result.passed {
411                result.passed = false;
412            }
413            result.property_results.insert(property, prop_result);
414        }
415
416        result.execution_time_ms = start.elapsed().as_millis() as u64;
417        result
418    }
419
420    /// Check a specific property
421    fn check_property(
422        &self,
423        test_case: &GradientTestCase,
424        property: GradientProperty,
425    ) -> PropertyCheckResult {
426        match property {
427            GradientProperty::NumericalConsistency => {
428                // This would require actual gradient computation
429                // For now, just check if operation has gradient registered
430                let auditor = get_auditor();
431                let has_grad = auditor.has_gradient(&test_case.operation);
432
433                PropertyCheckResult {
434                    property,
435                    passed: has_grad,
436                    max_error: None,
437                    details: if has_grad {
438                        "Gradient implementation registered".to_string()
439                    } else {
440                        "No gradient implementation found".to_string()
441                    },
442                }
443            }
444            GradientProperty::Finiteness => self.check_finiteness(test_case),
445            GradientProperty::ShapeConsistency => self.check_shape_consistency(test_case),
446            GradientProperty::ZeroForConstants => self.check_zero_for_constants(test_case),
447            GradientProperty::Linearity => self.check_linearity(test_case),
448            GradientProperty::ChainRule => self.check_chain_rule(test_case),
449        }
450    }
451
452    /// Check shape consistency using the shape-inference registry (metadata-level
453    /// only: uses shapes already present on `test_case`, no gradient tensor
454    /// computation needed).
455    fn check_shape_consistency(&self, test_case: &GradientTestCase) -> PropertyCheckResult {
456        let property = GradientProperty::ShapeConsistency;
457        let registry = get_registry();
458        let registered_ops = registry.list_operations();
459
460        if !registered_ops
461            .iter()
462            .any(|name| name == &test_case.operation)
463        {
464            return PropertyCheckResult {
465                property,
466                passed: false,
467                max_error: None,
468                details: format!(
469                    "Cannot verify shape consistency: operation '{}' is not registered in the \
470                     shape inference registry. Registered operations: {}",
471                    test_case.operation,
472                    registered_ops.join(", ")
473                ),
474            };
475        }
476
477        let metadata = OperationMetadata::new();
478        match registry.infer(&test_case.operation, &test_case.input_shapes, &metadata) {
479            Ok(output_shape) => PropertyCheckResult {
480                property,
481                passed: true,
482                max_error: None,
483                details: format!(
484                    "Shape inference succeeded for '{}' with inputs {:?} -> output shape {:?}",
485                    test_case.operation, test_case.input_shapes, output_shape
486                ),
487            },
488            Err(e) => PropertyCheckResult {
489                property,
490                passed: false,
491                max_error: None,
492                details: format!(
493                    "Shape inference failed for '{}' with inputs {:?}: {e}",
494                    test_case.operation, test_case.input_shapes
495                ),
496            },
497        }
498    }
499
500    /// Check finiteness by calling the registered [`GradientExecutor`] with
501    /// synthesized concrete input tensors and inspecting every returned gradient
502    /// element for NaN/Inf.
503    fn check_finiteness(&self, test_case: &GradientTestCase) -> PropertyCheckResult {
504        let property = GradientProperty::Finiteness;
505        let Some(executor) = get_gradient_executor() else {
506            return PropertyCheckResult {
507                property,
508                passed: false,
509                max_error: None,
510                details: "Cannot verify finiteness: no GradientExecutor has been registered. \
511                          Call tenflowers_autograd::init() (or register a custom \
512                          GradientExecutor via tenflowers_core::gradient_executor) before \
513                          running gradient validation."
514                    .to_string(),
515            };
516        };
517
518        let arity = synthetic_op_arity(&test_case.operation);
519        let inputs = match synthesize_inputs(test_case, arity) {
520            Ok(inputs) => inputs,
521            Err(e) => {
522                return PropertyCheckResult {
523                    property,
524                    passed: false,
525                    max_error: None,
526                    details: format!(
527                        "Could not synthesize input tensors for '{}': {e}",
528                        test_case.operation
529                    ),
530                }
531            }
532        };
533
534        match executor.compute_gradient(&test_case.operation, &inputs) {
535            Ok(grads) => {
536                let mut non_finite = 0usize;
537                let mut total = 0usize;
538                for g in &grads {
539                    total += g.data().len();
540                    non_finite += g.data().iter().filter(|v| !v.is_finite()).count();
541                }
542                if non_finite == 0 {
543                    PropertyCheckResult {
544                        property,
545                        passed: true,
546                        max_error: None,
547                        details: format!(
548                            "All {total} gradient element(s) across {} output tensor(s) are \
549                             finite for operation '{}'",
550                            grads.len(),
551                            test_case.operation
552                        ),
553                    }
554                } else {
555                    PropertyCheckResult {
556                        property,
557                        passed: false,
558                        max_error: None,
559                        details: format!(
560                            "{non_finite} of {total} gradient element(s) are non-finite \
561                             (NaN/Inf) for operation '{}'",
562                            test_case.operation
563                        ),
564                    }
565                }
566            }
567            Err(e) => PropertyCheckResult {
568                property,
569                passed: false,
570                max_error: None,
571                details: format!(
572                    "Gradient computation failed for operation '{}': {e}",
573                    test_case.operation
574                ),
575            },
576        }
577    }
578
579    /// Check that the gradient of a provably-constant function (y = x * 0) is
580    /// zero, using the registered executor. Uses the canonical "mul" op rather
581    /// than `test_case.operation` directly (see doc comment inside for why).
582    fn check_zero_for_constants(&self, test_case: &GradientTestCase) -> PropertyCheckResult {
583        let property = GradientProperty::ZeroForConstants;
584        let Some(executor) = get_gradient_executor() else {
585            return PropertyCheckResult {
586                property,
587                passed: false,
588                max_error: None,
589                details: "Cannot verify zero-for-constants: no GradientExecutor has been \
590                          registered. Call tenflowers_autograd::init() first."
591                    .to_string(),
592            };
593        };
594
595        let shape = test_case
596            .input_shapes
597            .first()
598            .cloned()
599            .unwrap_or_else(|| Shape::from_slice(&[2, 3]));
600        let n = shape.elements().max(1);
601        let lhs_data: Vec<f64> = (0..n).map(|i| i as f64 + 1.0).collect();
602        let zero_data: Vec<f64> = vec![0.0; n];
603
604        let lhs = match Tensor::from_vec(lhs_data, shape.dims()) {
605            Ok(t) => t,
606            Err(e) => {
607                return PropertyCheckResult {
608                    property,
609                    passed: false,
610                    max_error: None,
611                    details: format!("Could not construct zero-for-constants fixture: {e}"),
612                }
613            }
614        };
615        let rhs = match Tensor::from_vec(zero_data, shape.dims()) {
616            Ok(t) => t,
617            Err(e) => {
618                return PropertyCheckResult {
619                    property,
620                    passed: false,
621                    max_error: None,
622                    details: format!("Could not construct zero-for-constants fixture: {e}"),
623                }
624            }
625        };
626
627        // y = lhs * rhs with rhs held at exactly zero is provably constant (always
628        // zero) with respect to lhs, so d(y)/d(lhs) must be exactly zero
629        // everywhere. This validates the framework's constant-gradient machinery
630        // via the multiplicative annihilator (0); it does not directly exercise
631        // `test_case.operation`'s own backward path (which may not even be a
632        // multiplicative op).
633        let grads = match executor.compute_gradient("mul", &[lhs, rhs]) {
634            Ok(g) if g.len() == 2 => g,
635            Ok(g) => {
636                return PropertyCheckResult {
637                    property,
638                    passed: false,
639                    max_error: None,
640                    details: format!(
641                        "GradientExecutor returned {} gradient(s) for 'mul', expected 2",
642                        g.len()
643                    ),
644                }
645            }
646            Err(e) => {
647                return PropertyCheckResult {
648                    property,
649                    passed: false,
650                    max_error: None,
651                    details: format!(
652                        "Gradient computation failed for zero-for-constants fixture (op \
653                         'mul'): {e}"
654                    ),
655                }
656            }
657        };
658
659        let max_abs = grads[0]
660            .data()
661            .iter()
662            .fold(0.0_f64, |acc, v| acc.max(v.abs()));
663        let passed = max_abs < 1e-9;
664        PropertyCheckResult {
665            property,
666            passed,
667            max_error: Some(max_abs),
668            details: format!(
669                "ZeroForConstants uses the canonical construction y = x * 0, which is provably \
670                 constant with respect to x; its gradient must be exactly zero. Max |gradient| \
671                 observed: {max_abs:.3e}."
672            ),
673        }
674    }
675
676    /// Check that the gradient of a linear operation ("add") is independent of
677    /// input magnitude -- the defining property of a linear map's derivative
678    /// being constant.
679    fn check_linearity(&self, test_case: &GradientTestCase) -> PropertyCheckResult {
680        let property = GradientProperty::Linearity;
681        let Some(executor) = get_gradient_executor() else {
682            return PropertyCheckResult {
683                property,
684                passed: false,
685                max_error: None,
686                details: "Cannot verify linearity: no GradientExecutor has been registered. \
687                          Call tenflowers_autograd::init() first."
688                    .to_string(),
689            };
690        };
691
692        let shape = test_case
693            .input_shapes
694            .first()
695            .cloned()
696            .unwrap_or_else(|| Shape::from_slice(&[2, 3]));
697        let n = shape.elements().max(1);
698
699        // d(x+y)/dx and d(x+y)/dy are constant (= 1) everywhere, independent of
700        // the values of x and y -- this is the defining property of a linear
701        // operation's gradient. Evaluating "add" at two very different input
702        // magnitudes and requiring identical gradients is therefore a direct,
703        // real test of gradient linearity (additivity/homogeneity), independent
704        // of ChainRule below (which tests composition, not linearity).
705        let build = |scale: f64| -> Result<(Tensor<f64>, Tensor<f64>)> {
706            let x: Vec<f64> = (0..n).map(|i| (i as f64 + 1.0) * scale).collect();
707            let y: Vec<f64> = (0..n).map(|i| (i as f64 + 2.0) * scale).collect();
708            Ok((
709                Tensor::from_vec(x, shape.dims())?,
710                Tensor::from_vec(y, shape.dims())?,
711            ))
712        };
713
714        let (x1, y1) = match build(1.0) {
715            Ok(v) => v,
716            Err(e) => {
717                return PropertyCheckResult {
718                    property,
719                    passed: false,
720                    max_error: None,
721                    details: format!("Could not construct linearity fixture: {e}"),
722                }
723            }
724        };
725        let (x2, y2) = match build(37.5) {
726            Ok(v) => v,
727            Err(e) => {
728                return PropertyCheckResult {
729                    property,
730                    passed: false,
731                    max_error: None,
732                    details: format!("Could not construct linearity fixture: {e}"),
733                }
734            }
735        };
736
737        let g1 = match executor.compute_gradient("add", &[x1, y1]) {
738            Ok(g) if g.len() == 2 => g,
739            Ok(g) => {
740                return PropertyCheckResult {
741                    property,
742                    passed: false,
743                    max_error: None,
744                    details: format!(
745                        "GradientExecutor returned {} gradient(s) for 'add', expected 2",
746                        g.len()
747                    ),
748                }
749            }
750            Err(e) => {
751                return PropertyCheckResult {
752                    property,
753                    passed: false,
754                    max_error: None,
755                    details: format!("Gradient computation failed for linearity fixture: {e}"),
756                }
757            }
758        };
759        let g2 = match executor.compute_gradient("add", &[x2, y2]) {
760            Ok(g) if g.len() == 2 => g,
761            Ok(g) => {
762                return PropertyCheckResult {
763                    property,
764                    passed: false,
765                    max_error: None,
766                    details: format!(
767                        "GradientExecutor returned {} gradient(s) for 'add', expected 2",
768                        g.len()
769                    ),
770                }
771            }
772            Err(e) => {
773                return PropertyCheckResult {
774                    property,
775                    passed: false,
776                    max_error: None,
777                    details: format!("Gradient computation failed for linearity fixture: {e}"),
778                }
779            }
780        };
781
782        let checker = NumericalGradientChecker::<f64>::new(test_case.config.clone());
783        let cmp_x = match checker.compare_gradients(&g1[0], &g2[0]) {
784            Ok(c) => c,
785            Err(e) => {
786                return PropertyCheckResult {
787                    property,
788                    passed: false,
789                    max_error: None,
790                    details: format!("Could not compare linearity gradients (x): {e}"),
791                }
792            }
793        };
794        let cmp_y = match checker.compare_gradients(&g1[1], &g2[1]) {
795            Ok(c) => c,
796            Err(e) => {
797                return PropertyCheckResult {
798                    property,
799                    passed: false,
800                    max_error: None,
801                    details: format!("Could not compare linearity gradients (y): {e}"),
802                }
803            }
804        };
805
806        let passed = cmp_x.passed && cmp_y.passed;
807        PropertyCheckResult {
808            property,
809            passed,
810            max_error: Some(cmp_x.max_absolute_error.max(cmp_y.max_absolute_error)),
811            details: if passed {
812                "Gradient of 'add' is identical at two different input magnitudes, confirming \
813                 the gradient of a linear operation is constant (linearity holds)."
814                    .to_string()
815            } else {
816                format!(
817                    "Gradient of 'add' differs across input magnitudes -- x: {}; y: {}",
818                    cmp_x.summary(),
819                    cmp_y.summary()
820                )
821            },
822        }
823    }
824
825    /// Check the chain rule by comparing a genuinely tape-composed gradient of
826    /// tanh(sigmoid(x)) against a manually chain-multiplied combination of the
827    /// two stages' independently-computed local gradients.
828    fn check_chain_rule(&self, test_case: &GradientTestCase) -> PropertyCheckResult {
829        let property = GradientProperty::ChainRule;
830        let Some(executor) = get_gradient_executor() else {
831            return PropertyCheckResult {
832                property,
833                passed: false,
834                max_error: None,
835                details: "Cannot verify chain rule: no GradientExecutor has been registered. \
836                          Call tenflowers_autograd::init() first."
837                    .to_string(),
838            };
839        };
840
841        let shape = test_case
842            .input_shapes
843            .first()
844            .cloned()
845            .unwrap_or_else(|| Shape::from_slice(&[2, 3]));
846        let n = shape.elements().max(1);
847        let x_data: Vec<f64> = (0..n).map(|i| (i as f64 + 1.0) * 0.25).collect();
848        let x = match Tensor::from_vec(x_data, shape.dims()) {
849            Ok(t) => t,
850            Err(e) => {
851                return PropertyCheckResult {
852                    property,
853                    passed: false,
854                    max_error: None,
855                    details: format!("Could not construct chain-rule fixture: {e}"),
856                }
857            }
858        };
859
860        // Composed (automatic): tanh(sigmoid(x)), differentiated through a
861        // single, continuous computation that chains both stages -- this
862        // exercises the executor's own internal chain-rule accumulation. The
863        // "stage1->stage2" op-name convention is a private contract between this
864        // module and whatever GradientExecutor is registered (see
865        // tenflowers-autograd's implementation).
866        let composed = match executor.compute_gradient("sigmoid->tanh", std::slice::from_ref(&x)) {
867            Ok(g) if g.len() == 1 => g,
868            Ok(g) => {
869                return PropertyCheckResult {
870                    property,
871                    passed: false,
872                    max_error: None,
873                    details: format!(
874                        "GradientExecutor returned {} gradient(s) for 'sigmoid->tanh', \
875                         expected 1",
876                        g.len()
877                    ),
878                }
879            }
880            Err(e) => {
881                return PropertyCheckResult {
882                    property,
883                    passed: false,
884                    max_error: None,
885                    details: format!(
886                        "Gradient computation failed for composed chain-rule fixture: {e}"
887                    ),
888                }
889            }
890        };
891
892        // Manual chain rule: d(tanh)/ds (evaluated at s = sigmoid(x)) times
893        // d(sigmoid)/dx, computed from two independent, single-stage gradient
894        // calls and combined here with plain elementwise multiplication -- a
895        // completely separate code path from the composed call above, so
896        // comparing the two is a real, non-circular check.
897        let sigmoid_x = match x.sigmoid() {
898            Ok(t) => t,
899            Err(e) => {
900                return PropertyCheckResult {
901                    property,
902                    passed: false,
903                    max_error: None,
904                    details: format!("Could not evaluate sigmoid(x) for chain-rule fixture: {e}"),
905                }
906            }
907        };
908        let d_sigmoid = match executor.compute_gradient("sigmoid", std::slice::from_ref(&x)) {
909            Ok(g) if g.len() == 1 => g,
910            Ok(g) => {
911                return PropertyCheckResult {
912                    property,
913                    passed: false,
914                    max_error: None,
915                    details: format!(
916                        "GradientExecutor returned {} gradient(s) for 'sigmoid', expected 1",
917                        g.len()
918                    ),
919                }
920            }
921            Err(e) => {
922                return PropertyCheckResult {
923                    property,
924                    passed: false,
925                    max_error: None,
926                    details: format!("Gradient computation failed for 'sigmoid' stage: {e}"),
927                }
928            }
929        };
930        let d_tanh = match executor.compute_gradient("tanh", std::slice::from_ref(&sigmoid_x)) {
931            Ok(g) if g.len() == 1 => g,
932            Ok(g) => {
933                return PropertyCheckResult {
934                    property,
935                    passed: false,
936                    max_error: None,
937                    details: format!(
938                        "GradientExecutor returned {} gradient(s) for 'tanh', expected 1",
939                        g.len()
940                    ),
941                }
942            }
943            Err(e) => {
944                return PropertyCheckResult {
945                    property,
946                    passed: false,
947                    max_error: None,
948                    details: format!("Gradient computation failed for 'tanh' stage: {e}"),
949                }
950            }
951        };
952
953        let manual_data: Vec<f64> = d_tanh[0]
954            .data()
955            .iter()
956            .zip(d_sigmoid[0].data().iter())
957            .map(|(a, b)| a * b)
958            .collect();
959        let manual = match Tensor::from_vec(manual_data, shape.dims()) {
960            Ok(t) => t,
961            Err(e) => {
962                return PropertyCheckResult {
963                    property,
964                    passed: false,
965                    max_error: None,
966                    details: format!("Could not assemble manual chain-rule gradient: {e}"),
967                }
968            }
969        };
970
971        let checker = NumericalGradientChecker::<f64>::new(test_case.config.clone());
972        match checker.compare_gradients(&manual, &composed[0]) {
973            Ok(cmp) if cmp.passed => PropertyCheckResult {
974                property,
975                passed: true,
976                max_error: Some(cmp.max_absolute_error),
977                details: "Composed gradient of tanh(sigmoid(x)) matches the manually \
978                          chain-multiplied per-stage gradients."
979                    .to_string(),
980            },
981            Ok(cmp) => PropertyCheckResult {
982                property,
983                passed: false,
984                max_error: Some(cmp.max_absolute_error),
985                details: format!("Chain rule mismatch: {}", cmp.summary()),
986            },
987            Err(e) => PropertyCheckResult {
988                property,
989                passed: false,
990                max_error: None,
991                details: format!("Could not compare chain-rule gradients: {e}"),
992            },
993        }
994    }
995
996    /// Validate a specific operation
997    pub fn validate_operation(&self, operation: &str) -> OperationValidationReport {
998        let mut report = OperationValidationReport::new(operation);
999        let test_cases = self.generate_test_cases(operation);
1000
1001        for test_case in test_cases {
1002            let result = self.validate_test_case(test_case);
1003            report.add_result(result);
1004        }
1005
1006        report
1007    }
1008
1009    /// Validate all operations with gradient implementations
1010    pub fn validate_all_operations(&self) -> ComprehensiveValidationReport {
1011        let mut report = ComprehensiveValidationReport::new();
1012        let auditor = get_auditor();
1013        let audit_report = auditor.audit_all();
1014
1015        // Only validate operations with gradient implementations
1016        for (op_name, grad_info) in &audit_report.operations {
1017            if grad_info.status == GradientStatus::Implemented
1018                || grad_info.status == GradientStatus::Partial
1019            {
1020                let op_report = self.validate_operation(op_name);
1021                report.add_operation_report(op_report);
1022            }
1023        }
1024
1025        report
1026    }
1027
1028    /// Set test shapes
1029    pub fn set_test_shapes(&mut self, shapes: Vec<Shape>) {
1030        self.test_shapes = shapes;
1031    }
1032
1033    /// Set test dtypes
1034    pub fn set_test_dtypes(&mut self, dtypes: Vec<DType>) {
1035        self.test_dtypes = dtypes;
1036    }
1037}
1038
1039/// Best-effort arity hint used only to decide how many synthetic input tensors
1040/// to construct before calling into the registered [`GradientExecutor`]. This is
1041/// NOT authoritative -- the executor itself is the source of truth for what it
1042/// supports, and returns a clear `Err` if this guess is wrong.
1043fn synthetic_op_arity(op: &str) -> usize {
1044    match op {
1045        "relu" | "sigmoid" | "tanh" | "neg" | "abs" | "exp" | "log" | "sqrt" | "sin" | "cos" => 1,
1046        _ => 2,
1047    }
1048}
1049
1050/// Synthesize `count` concrete f64 input tensors from `test_case.input_shapes`.
1051///
1052/// `generate_test_cases` currently records a single shape per test case
1053/// regardless of an operation's real arity, so this pads with the first
1054/// available shape (or a small default) when there aren't enough. Values follow
1055/// a fixed, deterministic pattern: the first tensor is strictly positive
1056/// (`1, 2, 3, ...`), and any subsequent tensor starts at exactly zero
1057/// (`0, 1, 2, ...`). This keeps operations that are well-defined at zero
1058/// (add/sub/mul/matmul/relu/sigmoid/tanh/pow-with-positive-base) finite, while
1059/// operations with a genuine mathematical singularity at zero in that position
1060/// (division) naturally -- not artificially -- surface a non-finite gradient.
1061fn synthesize_inputs(test_case: &GradientTestCase, count: usize) -> Result<Vec<Tensor<f64>>> {
1062    let default_shape = Shape::from_slice(&[2, 2]);
1063    let fallback = test_case
1064        .input_shapes
1065        .first()
1066        .cloned()
1067        .unwrap_or_else(|| default_shape.clone());
1068
1069    let mut shapes: Vec<Shape> = test_case.input_shapes.iter().take(count).cloned().collect();
1070    while shapes.len() < count {
1071        shapes.push(fallback.clone());
1072    }
1073
1074    shapes
1075        .into_iter()
1076        .enumerate()
1077        .map(|(idx, shape)| {
1078            let n = shape.elements().max(1);
1079            let data: Vec<f64> = if idx == 0 {
1080                (0..n).map(|i| i as f64 + 1.0).collect()
1081            } else {
1082                (0..n).map(|i| i as f64).collect()
1083            };
1084            Tensor::from_vec(data, shape.dims())
1085        })
1086        .collect()
1087}
1088
1089// ============================================================================
1090// Global Validator Access
1091// ============================================================================
1092
1093static GLOBAL_VALIDATOR: OnceLock<GradientValidator> = OnceLock::new();
1094
1095/// Get the global gradient validator
1096pub fn get_validator() -> &'static GradientValidator {
1097    GLOBAL_VALIDATOR.get_or_init(GradientValidator::new)
1098}
1099
1100/// Initialize the global validator
1101pub fn initialize_validator() {
1102    let _ = get_validator();
1103}
1104
1105// ============================================================================
1106// Tests
1107// ============================================================================
1108
1109#[cfg(test)]
1110mod tests {
1111    use super::*;
1112
1113    #[test]
1114    fn test_validator_creation() {
1115        let validator = GradientValidator::new();
1116        assert!(!validator.test_shapes.is_empty());
1117        assert!(!validator.test_dtypes.is_empty());
1118    }
1119
1120    #[test]
1121    fn test_generate_test_cases() {
1122        let validator = GradientValidator::new();
1123        let test_cases = validator.generate_test_cases("add");
1124
1125        // Should generate cases for each dtype x shape combination
1126        assert!(!test_cases.is_empty());
1127        assert!(test_cases.iter().all(|tc| tc.operation == "add"));
1128    }
1129
1130    #[test]
1131    fn test_property_descriptions() {
1132        assert!(!GradientProperty::NumericalConsistency
1133            .description()
1134            .is_empty());
1135        assert!(!GradientProperty::Finiteness.description().is_empty());
1136    }
1137
1138    #[test]
1139    fn test_validation_result() {
1140        let test_case =
1141            GradientTestCase::new("add", DType::Float32, vec![Shape::from_slice(&[2, 3])]);
1142        let result = ValidationResult::new(test_case);
1143
1144        assert!(result.passed);
1145        assert!(result.property_results.is_empty());
1146    }
1147
1148    #[test]
1149    fn test_operation_validation_report() {
1150        let mut report = OperationValidationReport::new("add");
1151        assert_eq!(report.total_tests, 0);
1152        assert_eq!(report.tests_passed, 0);
1153
1154        let test_case =
1155            GradientTestCase::new("add", DType::Float32, vec![Shape::from_slice(&[2, 3])]);
1156        let mut result = ValidationResult::new(test_case);
1157        result.passed = true;
1158
1159        report.add_result(result);
1160        assert_eq!(report.total_tests, 1);
1161        assert_eq!(report.tests_passed, 1);
1162        assert!(report.all_tests_passed());
1163    }
1164
1165    #[test]
1166    fn test_comprehensive_report() {
1167        let mut report = ComprehensiveValidationReport::new();
1168        assert_eq!(report.total_operations, 0);
1169
1170        let op_report = OperationValidationReport::new("add");
1171        report.add_operation_report(op_report);
1172
1173        assert_eq!(report.total_operations, 1);
1174    }
1175
1176    #[test]
1177    fn test_validate_operation() {
1178        let validator = GradientValidator::new();
1179        let report = validator.validate_operation("add");
1180
1181        assert!(!report.operation.is_empty());
1182        assert!(report.total_tests > 0);
1183    }
1184
1185    #[test]
1186    fn test_global_validator() {
1187        let validator1 = get_validator();
1188        let validator2 = get_validator();
1189
1190        // Should return the same instance
1191        assert!(std::ptr::eq(validator1, validator2));
1192    }
1193
1194    #[test]
1195    fn test_property_check_result() {
1196        let result = PropertyCheckResult {
1197            property: GradientProperty::Finiteness,
1198            passed: true,
1199            max_error: Some(1e-5),
1200            details: "Test".to_string(),
1201        };
1202
1203        assert!(result.passed);
1204        assert_eq!(result.max_error, Some(1e-5));
1205    }
1206
1207    #[test]
1208    fn test_shape_consistency_passes_for_compatible_shapes() {
1209        let validator = GradientValidator::new();
1210        let test_case = GradientTestCase::new(
1211            "add",
1212            DType::Float32,
1213            vec![Shape::from_slice(&[2, 3]), Shape::from_slice(&[2, 3])],
1214        );
1215        let result = validator.validate_test_case(test_case);
1216        let prop = result
1217            .property_results
1218            .get(&GradientProperty::ShapeConsistency)
1219            .expect("ShapeConsistency should have been checked");
1220        assert!(prop.passed, "expected pass: {}", prop.details);
1221    }
1222
1223    #[test]
1224    fn test_shape_consistency_fails_for_mismatched_shapes() {
1225        let validator = GradientValidator::new();
1226        // matmul requires matching inner dimensions; [2,3] @ [4,5] is incompatible.
1227        let test_case = GradientTestCase::new(
1228            "matmul",
1229            DType::Float32,
1230            vec![Shape::from_slice(&[2, 3]), Shape::from_slice(&[4, 5])],
1231        );
1232        let result = validator.validate_test_case(test_case);
1233        let prop = result
1234            .property_results
1235            .get(&GradientProperty::ShapeConsistency)
1236            .expect("ShapeConsistency should have been checked");
1237        assert!(
1238            !prop.passed,
1239            "expected ShapeConsistency to fail for incompatible matmul shapes"
1240        );
1241    }
1242
1243    #[test]
1244    fn test_shape_consistency_unregistered_operation_is_honest() {
1245        let validator = GradientValidator::new();
1246        let test_case = GradientTestCase::new(
1247            "definitely_not_a_registered_op",
1248            DType::Float32,
1249            vec![Shape::from_slice(&[2, 3])],
1250        );
1251        let result = validator.validate_test_case(test_case);
1252        let prop = result
1253            .property_results
1254            .get(&GradientProperty::ShapeConsistency)
1255            .expect("ShapeConsistency should have been checked");
1256        assert!(
1257            !prop.passed,
1258            "an unregistered operation must not be reported as passed"
1259        );
1260    }
1261
1262    #[test]
1263    fn test_finiteness_without_registered_executor_is_honest_not_fabricated() {
1264        // This crate's own unit test binary never calls register_gradient_executor,
1265        // so Finiteness must honestly report that it cannot verify, rather than
1266        // fabricating `passed: true` as it did before this fix.
1267        let validator = GradientValidator::new();
1268        let test_case =
1269            GradientTestCase::new("add", DType::Float32, vec![Shape::from_slice(&[2, 3])]);
1270        let result = validator.validate_test_case(test_case);
1271        let prop = result
1272            .property_results
1273            .get(&GradientProperty::Finiteness)
1274            .expect("Finiteness should have been checked");
1275        assert!(
1276            !prop.passed,
1277            "must not fabricate passed:true without a registered executor"
1278        );
1279        assert!(
1280            prop.details.to_lowercase().contains("executor"),
1281            "details should explain that no gradient executor is registered: {}",
1282            prop.details
1283        );
1284    }
1285}