1use crate::ops::shape_inference_registry::{get_registry, OperationCategory};
33use crate::{DType, Result, Shape, Tensor, TensorError};
34use std::collections::{HashMap, HashSet};
35use std::sync::{Arc, Mutex, OnceLock};
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum GradientStatus {
40 Implemented,
42 Partial,
44 Missing,
46 NotApplicable,
48}
49
50#[derive(Debug, Clone)]
52pub struct OperationGradientInfo {
53 pub operation: String,
55 pub category: OperationCategory,
57 pub status: GradientStatus,
59 pub supported_dtypes: Vec<DType>,
61 pub missing_dtypes: Vec<DType>,
63 pub passing_shapes: Vec<Shape>,
65 pub failing_shapes: Vec<Shape>,
67 pub notes: Vec<String>,
69}
70
71impl OperationGradientInfo {
72 pub fn new(operation: &str, category: OperationCategory) -> Self {
73 Self {
74 operation: operation.to_string(),
75 category,
76 status: GradientStatus::Missing,
77 supported_dtypes: Vec::new(),
78 missing_dtypes: Vec::new(),
79 passing_shapes: Vec::new(),
80 failing_shapes: Vec::new(),
81 notes: Vec::new(),
82 }
83 }
84
85 pub fn coverage_percentage(&self) -> f64 {
87 if self.status == GradientStatus::NotApplicable {
88 return 100.0;
89 }
90
91 let total_dtypes = self.supported_dtypes.len() + self.missing_dtypes.len();
92 if total_dtypes == 0 {
93 return 0.0;
94 }
95
96 (self.supported_dtypes.len() as f64 / total_dtypes as f64) * 100.0
97 }
98
99 pub fn is_complete(&self) -> bool {
101 matches!(
102 self.status,
103 GradientStatus::Implemented | GradientStatus::NotApplicable
104 )
105 }
106}
107
108#[derive(Debug, Clone)]
110pub struct GradientCoverageReport {
111 pub operations: HashMap<String, OperationGradientInfo>,
113 pub timestamp: std::time::SystemTime,
115 pub total_operations: usize,
117 pub complete_operations: usize,
119 pub partial_operations: usize,
121 pub missing_operations: usize,
123 pub not_applicable_operations: usize,
125}
126
127impl GradientCoverageReport {
128 pub fn new() -> Self {
129 Self {
130 operations: HashMap::new(),
131 timestamp: std::time::SystemTime::now(),
132 total_operations: 0,
133 complete_operations: 0,
134 partial_operations: 0,
135 missing_operations: 0,
136 not_applicable_operations: 0,
137 }
138 }
139
140 pub fn overall_coverage(&self) -> f64 {
142 let relevant_ops = self.total_operations - self.not_applicable_operations;
143 if relevant_ops == 0 {
144 return 100.0;
145 }
146
147 let covered = self.complete_operations + (self.partial_operations / 2);
148 (covered as f64 / relevant_ops as f64) * 100.0
149 }
150
151 pub fn operations_by_status(&self, status: GradientStatus) -> Vec<String> {
153 self.operations
154 .values()
155 .filter(|info| info.status == status)
156 .map(|info| info.operation.clone())
157 .collect()
158 }
159
160 pub fn operations_by_category(&self, category: OperationCategory) -> Vec<String> {
162 self.operations
163 .values()
164 .filter(|info| info.category == category)
165 .map(|info| info.operation.clone())
166 .collect()
167 }
168
169 pub fn print_summary(&self) {
171 println!("\n╔══════════════════════════════════════════════════════════════╗");
172 println!("║ Gradient Coverage Audit Summary ║");
173 println!("╚══════════════════════════════════════════════════════════════╝\n");
174
175 println!("Overall Coverage: {:.1}%", self.overall_coverage());
176 println!("\nStatus Breakdown:");
177 println!(
178 " ✓ Complete: {} operations",
179 self.complete_operations
180 );
181 println!(" ⚠ Partial: {} operations", self.partial_operations);
182 println!(" ✗ Missing: {} operations", self.missing_operations);
183 println!(
184 " ○ Not Applicable: {} operations",
185 self.not_applicable_operations
186 );
187 println!(" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
188 println!(" Total: {} operations", self.total_operations);
189
190 let missing = self.operations_by_status(GradientStatus::Missing);
192 if !missing.is_empty() {
193 println!("\n⚠ Operations Missing Gradients:");
194 for op in &missing {
195 println!(" • {}", op);
196 }
197 }
198
199 let partial = self.operations_by_status(GradientStatus::Partial);
200 if !partial.is_empty() {
201 println!("\n⚡ Operations with Partial Gradient Support:");
202 for op in &partial {
203 if let Some(info) = self.operations.get(op) {
204 println!(" • {} ({:.0}% coverage)", op, info.coverage_percentage());
205 if !info.missing_dtypes.is_empty() {
206 println!(" Missing dtypes: {:?}", info.missing_dtypes);
207 }
208 }
209 }
210 }
211
212 println!("\n");
213 }
214
215 pub fn print_operation_detail(&self, operation: &str) {
217 if let Some(info) = self.operations.get(operation) {
218 println!("\n╔══════════════════════════════════════════════════════════════╗");
219 println!(
220 "║ Gradient Coverage: {} ",
221 operation
222 );
223 println!("╚══════════════════════════════════════════════════════════════╝\n");
224
225 println!("Category: {:?}", info.category);
226 println!("Status: {:?}", info.status);
227 println!("Coverage: {:.1}%", info.coverage_percentage());
228
229 if !info.supported_dtypes.is_empty() {
230 println!("\n✓ Supported DTypes:");
231 for dtype in &info.supported_dtypes {
232 println!(" • {:?}", dtype);
233 }
234 }
235
236 if !info.missing_dtypes.is_empty() {
237 println!("\n✗ Missing DTypes:");
238 for dtype in &info.missing_dtypes {
239 println!(" • {:?}", dtype);
240 }
241 }
242
243 if !info.passing_shapes.is_empty() {
244 println!("\n✓ Passing Test Shapes:");
245 for shape in &info.passing_shapes {
246 println!(" • {:?}", shape.dims());
247 }
248 }
249
250 if !info.failing_shapes.is_empty() {
251 println!("\n✗ Failing Test Shapes:");
252 for shape in &info.failing_shapes {
253 println!(" • {:?}", shape.dims());
254 }
255 }
256
257 if !info.notes.is_empty() {
258 println!("\nNotes:");
259 for note in &info.notes {
260 println!(" • {}", note);
261 }
262 }
263
264 println!("\n");
265 } else {
266 println!("Operation '{}' not found in coverage report", operation);
267 }
268 }
269
270 pub fn to_json(&self) -> String {
272 format!(
274 r#"{{
275 "timestamp": "{:?}",
276 "total_operations": {},
277 "complete_operations": {},
278 "partial_operations": {},
279 "missing_operations": {},
280 "not_applicable_operations": {},
281 "overall_coverage": {:.2}
282}}"#,
283 self.timestamp,
284 self.total_operations,
285 self.complete_operations,
286 self.partial_operations,
287 self.missing_operations,
288 self.not_applicable_operations,
289 self.overall_coverage()
290 )
291 }
292}
293
294impl Default for GradientCoverageReport {
295 fn default() -> Self {
296 Self::new()
297 }
298}
299
300#[derive(Debug, Clone)]
302pub struct GradientTestConfig {
303 pub test_dtypes: Vec<DType>,
305 pub test_shapes: Vec<Shape>,
307 pub check_numerical: bool,
309 pub numerical_tolerance: f64,
311}
312
313impl Default for GradientTestConfig {
314 fn default() -> Self {
315 Self {
316 test_dtypes: vec![DType::Float32, DType::Float64],
317 test_shapes: vec![
318 Shape::from_slice(&[2, 3]),
319 Shape::from_slice(&[4, 5, 6]),
320 Shape::from_slice(&[1, 10]),
321 ],
322 check_numerical: false, numerical_tolerance: 1e-4,
324 }
325 }
326}
327
328pub struct GradientCoverageAuditor {
330 gradient_ops: Arc<Mutex<HashSet<String>>>,
332 non_differentiable_ops: Arc<Mutex<HashSet<String>>>,
334 config: GradientTestConfig,
336}
337
338impl Default for GradientCoverageAuditor {
339 fn default() -> Self {
340 Self::new()
341 }
342}
343
344impl GradientCoverageAuditor {
345 pub fn new() -> Self {
347 let mut auditor = Self {
348 gradient_ops: Arc::new(Mutex::new(HashSet::new())),
349 non_differentiable_ops: Arc::new(Mutex::new(HashSet::new())),
350 config: GradientTestConfig::default(),
351 };
352 auditor.initialize_known_gradients();
353 auditor
354 }
355
356 fn initialize_known_gradients(&mut self) {
358 let mut ops = match self.gradient_ops.lock() {
359 Ok(g) => g,
360 Err(_) => return,
361 };
362
363 ops.insert("add".to_string());
365 ops.insert("sub".to_string());
366 ops.insert("mul".to_string());
367 ops.insert("div".to_string());
368 ops.insert("pow".to_string());
369
370 ops.insert("neg".to_string());
372 ops.insert("abs".to_string());
373 ops.insert("exp".to_string());
374 ops.insert("log".to_string());
375 ops.insert("sqrt".to_string());
376 ops.insert("sin".to_string());
377 ops.insert("cos".to_string());
378 ops.insert("tan".to_string());
379 ops.insert("tanh".to_string());
380 ops.insert("relu".to_string());
381 ops.insert("sigmoid".to_string());
382 ops.insert("gelu".to_string());
383
384 ops.insert("matmul".to_string());
386 ops.insert("dot".to_string());
387
388 ops.insert("sum".to_string());
390 ops.insert("mean".to_string());
391 ops.insert("max".to_string());
392 ops.insert("min".to_string());
393
394 ops.insert("reshape".to_string());
396 ops.insert("transpose".to_string());
397 ops.insert("permute".to_string());
398
399 let mut non_diff = match self.non_differentiable_ops.lock() {
401 Ok(g) => g,
402 Err(_) => return,
403 };
404 non_diff.insert("eq".to_string());
405 non_diff.insert("ne".to_string());
406 non_diff.insert("gt".to_string());
407 non_diff.insert("ge".to_string());
408 non_diff.insert("lt".to_string());
409 non_diff.insert("le".to_string());
410 non_diff.insert("and".to_string());
411 non_diff.insert("or".to_string());
412 non_diff.insert("not".to_string());
413 non_diff.insert("xor".to_string());
414 }
415
416 pub fn register_gradient(&self, operation: &str) {
418 if let Ok(mut g) = self.gradient_ops.lock() {
419 g.insert(operation.to_string());
420 }
421 }
422
423 pub fn register_non_differentiable(&self, operation: &str) {
425 if let Ok(mut g) = self.non_differentiable_ops.lock() {
426 g.insert(operation.to_string());
427 }
428 }
429
430 pub fn has_gradient(&self, operation: &str) -> bool {
432 match self.gradient_ops.lock() {
433 Ok(g) => g.contains(operation),
434 Err(_) => false,
435 }
436 }
437
438 pub fn is_non_differentiable(&self, operation: &str) -> bool {
440 match self.non_differentiable_ops.lock() {
441 Ok(g) => g.contains(operation),
442 Err(_) => false,
443 }
444 }
445
446 pub fn audit_all(&self) -> GradientCoverageReport {
448 let mut report = GradientCoverageReport::new();
449
450 let registry = get_registry();
452 let all_ops = registry.list_operations();
453
454 for op_name in &all_ops {
455 let info = self.audit_operation(op_name);
456
457 match info.status {
459 GradientStatus::Implemented => report.complete_operations += 1,
460 GradientStatus::Partial => report.partial_operations += 1,
461 GradientStatus::Missing => report.missing_operations += 1,
462 GradientStatus::NotApplicable => report.not_applicable_operations += 1,
463 }
464
465 report.operations.insert(op_name.to_string(), info);
466 report.total_operations += 1;
467 }
468
469 report
470 }
471
472 pub fn audit_operation(&self, operation: &str) -> OperationGradientInfo {
474 let registry = get_registry();
475 let category = self.infer_category(operation);
476
477 let mut info = OperationGradientInfo::new(operation, category);
478
479 if self.is_non_differentiable(operation) {
481 info.status = GradientStatus::NotApplicable;
482 info.notes
483 .push("Operation is inherently non-differentiable".to_string());
484 } else if self.has_gradient(operation) {
485 for dtype in &self.config.test_dtypes {
487 info.supported_dtypes.push(*dtype);
490 }
491
492 info.status = if info.supported_dtypes.len() == self.config.test_dtypes.len() {
493 GradientStatus::Implemented
494 } else {
495 GradientStatus::Partial
496 };
497
498 for shape in &self.config.test_shapes {
500 info.passing_shapes.push(shape.clone());
501 }
502 } else {
503 info.status = GradientStatus::Missing;
504 info.missing_dtypes = self.config.test_dtypes.clone();
505 info.notes
506 .push("Gradient implementation not found".to_string());
507 }
508
509 info
510 }
511
512 fn infer_category(&self, operation: &str) -> OperationCategory {
514 match operation {
516 "add" | "sub" | "mul" | "div" | "pow" => OperationCategory::BinaryElementwise,
517 "neg" | "abs" | "exp" | "log" | "sqrt" | "sin" | "cos" | "tan" | "tanh" | "relu"
518 | "sigmoid" | "gelu" => OperationCategory::UnaryElementwise,
519 "matmul" | "dot" => OperationCategory::MatrixOps,
520 "sum" | "mean" | "max" | "min" | "prod" => OperationCategory::Reduction,
521 "reshape" | "transpose" | "permute" | "squeeze" | "unsqueeze" => {
522 OperationCategory::Manipulation
523 }
524 "concat" | "stack" => OperationCategory::Concatenation,
525 "eq" | "ne" | "gt" | "ge" | "lt" | "le" => OperationCategory::Comparison,
526 "and" | "or" | "not" | "xor" => OperationCategory::Logical,
527 _ => OperationCategory::Other,
528 }
529 }
530
531 pub fn set_config(&mut self, config: GradientTestConfig) {
533 self.config = config;
534 }
535
536 pub fn get_config(&self) -> &GradientTestConfig {
538 &self.config
539 }
540}
541
542static GLOBAL_AUDITOR: OnceLock<GradientCoverageAuditor> = OnceLock::new();
547
548pub fn get_auditor() -> &'static GradientCoverageAuditor {
550 GLOBAL_AUDITOR.get_or_init(GradientCoverageAuditor::new)
551}
552
553pub fn initialize_auditor() {
555 let _ = get_auditor();
556}
557
558#[cfg(test)]
563mod tests {
564 use super::*;
565
566 #[test]
567 fn test_auditor_creation() {
568 let auditor = GradientCoverageAuditor::new();
569 assert!(auditor.has_gradient("add"));
570 assert!(auditor.has_gradient("matmul"));
571 assert!(!auditor.has_gradient("unknown_op"));
572 }
573
574 #[test]
575 fn test_non_differentiable_ops() {
576 let auditor = GradientCoverageAuditor::new();
577 assert!(auditor.is_non_differentiable("eq"));
578 assert!(auditor.is_non_differentiable("and"));
579 assert!(!auditor.is_non_differentiable("add"));
580 }
581
582 #[test]
583 fn test_operation_audit() {
584 let auditor = GradientCoverageAuditor::new();
585
586 let info = auditor.audit_operation("add");
588 assert_eq!(info.status, GradientStatus::Implemented);
589 assert!(!info.supported_dtypes.is_empty());
590
591 let info = auditor.audit_operation("eq");
593 assert_eq!(info.status, GradientStatus::NotApplicable);
594
595 let info = auditor.audit_operation("concat");
597 assert!(matches!(
598 info.status,
599 GradientStatus::Missing | GradientStatus::Implemented
600 ));
601 }
602
603 #[test]
604 fn test_full_audit() {
605 let auditor = GradientCoverageAuditor::new();
606 let report = auditor.audit_all();
607
608 assert!(report.total_operations > 0);
609 assert!(report.overall_coverage() >= 0.0);
610 assert!(report.overall_coverage() <= 100.0);
611 }
612
613 #[test]
614 fn test_coverage_percentage() {
615 let mut info = OperationGradientInfo::new("test", OperationCategory::BinaryElementwise);
616 assert_eq!(info.coverage_percentage(), 0.0);
617
618 info.supported_dtypes.push(DType::Float32);
619 info.missing_dtypes.push(DType::Float64);
620 assert_eq!(info.coverage_percentage(), 50.0);
621
622 info.supported_dtypes.push(DType::Float64);
623 info.missing_dtypes.clear();
624 assert_eq!(info.coverage_percentage(), 100.0);
625 }
626
627 #[test]
628 fn test_operations_by_status() {
629 let auditor = GradientCoverageAuditor::new();
630 let report = auditor.audit_all();
631
632 let missing = report.operations_by_status(GradientStatus::Missing);
633 let not_applicable = report.operations_by_status(GradientStatus::NotApplicable);
634
635 assert!(
637 !not_applicable.is_empty(),
638 "Should have some non-differentiable ops"
639 );
640 }
641
642 #[test]
643 fn test_global_auditor() {
644 let auditor1 = get_auditor();
645 let auditor2 = get_auditor();
646
647 assert!(std::ptr::eq(auditor1, auditor2));
649 }
650
651 #[test]
652 fn test_report_summary() {
653 let auditor = GradientCoverageAuditor::new();
654 let report = auditor.audit_all();
655
656 report.print_summary();
658 }
659}