1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
40pub enum GradientProperty {
41 NumericalConsistency,
43 ZeroForConstants,
45 Linearity,
47 ChainRule,
49 Finiteness,
51 ShapeConsistency,
53}
54
55impl GradientProperty {
56 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#[derive(Debug, Clone)]
71pub struct GradientTestCase {
72 pub operation: String,
74 pub dtype: DType,
76 pub input_shapes: Vec<Shape>,
78 pub properties: Vec<GradientProperty>,
80 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 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 pub fn with_config(mut self, config: GradientCheckConfig) -> Self {
109 self.config = config;
110 self
111 }
112}
113
114#[derive(Debug, Clone)]
116pub struct ValidationResult {
117 pub test_case: GradientTestCase,
119 pub passed: bool,
121 pub property_results: HashMap<GradientProperty, PropertyCheckResult>,
123 pub execution_time_ms: u64,
125 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 pub fn all_properties_passed(&self) -> bool {
142 self.property_results.values().all(|r| r.passed)
143 }
144
145 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#[derive(Debug, Clone)]
157pub struct PropertyCheckResult {
158 pub property: GradientProperty,
160 pub passed: bool,
162 pub max_error: Option<f64>,
164 pub details: String,
166}
167
168#[derive(Debug, Clone)]
170pub struct OperationValidationReport {
171 pub operation: String,
173 pub test_cases: Vec<ValidationResult>,
175 pub total_tests: usize,
177 pub tests_passed: usize,
179 pub tests_failed: usize,
181 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 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 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 pub fn all_tests_passed(&self) -> bool {
218 self.tests_failed == 0 && self.total_tests > 0
219 }
220
221 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#[derive(Debug, Clone)]
261pub struct ComprehensiveValidationReport {
262 pub operations: HashMap<String, OperationValidationReport>,
264 pub total_operations: usize,
266 pub operations_passed: usize,
268 pub operations_failed: usize,
270 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 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 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 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
340pub struct GradientValidator {
342 configs: Arc<Mutex<HashMap<String, GradientCheckConfig>>>,
344 test_shapes: Vec<Shape>,
346 test_dtypes: Vec<DType>,
348}
349
350impl Default for GradientValidator {
351 fn default() -> Self {
352 Self::new()
353 }
354}
355
356impl GradientValidator {
357 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 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 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 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 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 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 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 fn check_property(
422 &self,
423 test_case: &GradientTestCase,
424 property: GradientProperty,
425 ) -> PropertyCheckResult {
426 match property {
427 GradientProperty::NumericalConsistency => {
428 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn set_test_shapes(&mut self, shapes: Vec<Shape>) {
1030 self.test_shapes = shapes;
1031 }
1032
1033 pub fn set_test_dtypes(&mut self, dtypes: Vec<DType>) {
1035 self.test_dtypes = dtypes;
1036 }
1037}
1038
1039fn 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
1050fn 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
1089static GLOBAL_VALIDATOR: OnceLock<GradientValidator> = OnceLock::new();
1094
1095pub fn get_validator() -> &'static GradientValidator {
1097 GLOBAL_VALIDATOR.get_or_init(GradientValidator::new)
1098}
1099
1100pub fn initialize_validator() {
1102 let _ = get_validator();
1103}
1104
1105#[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 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 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 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 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}