1use crate::{Result, Tensor, TensorError};
44use scirs2_core::numeric::{Float, FromPrimitive};
45use std::marker::PhantomData;
46
47#[derive(Debug, Clone)]
49pub struct GradientCheckConfig {
50 pub epsilon: f64,
52 pub rtol: f64,
54 pub atol: f64,
56 pub check_elementwise: bool,
58 pub use_central_difference: bool,
60 pub max_samples: Option<usize>,
62 pub random_seed: Option<u64>,
64}
65
66impl Default for GradientCheckConfig {
67 fn default() -> Self {
68 Self {
69 epsilon: 1e-5,
70 rtol: 1e-3,
71 atol: 1e-5,
72 check_elementwise: false,
73 use_central_difference: true,
74 max_samples: None,
75 random_seed: None,
76 }
77 }
78}
79
80impl GradientCheckConfig {
81 pub fn strict() -> Self {
83 Self {
84 epsilon: 1e-6,
85 rtol: 1e-4,
86 atol: 1e-6,
87 check_elementwise: true,
88 use_central_difference: true,
89 max_samples: None,
90 random_seed: None,
91 }
92 }
93
94 pub fn relaxed() -> Self {
96 Self {
97 epsilon: 1e-4,
98 rtol: 1e-2,
99 atol: 1e-4,
100 check_elementwise: false,
101 use_central_difference: true,
102 max_samples: Some(100),
103 random_seed: Some(42),
104 }
105 }
106
107 pub fn fast() -> Self {
109 Self {
110 epsilon: 1e-5,
111 rtol: 1e-3,
112 atol: 1e-5,
113 check_elementwise: false,
114 use_central_difference: false,
115 max_samples: Some(50),
116 random_seed: Some(42),
117 }
118 }
119}
120
121#[derive(Debug, Clone)]
123pub struct GradientCheckResult {
124 pub passed: bool,
126 pub max_relative_error: f64,
128 pub max_absolute_error: f64,
130 pub num_elements_checked: usize,
132 pub num_failures: usize,
134 pub failed_indices: Vec<usize>,
136 pub error_message: Option<String>,
138}
139
140impl GradientCheckResult {
141 pub fn is_ok(&self) -> bool {
143 self.passed
144 }
145
146 pub fn failure_rate(&self) -> f64 {
148 if self.num_elements_checked == 0 {
149 0.0
150 } else {
151 (self.num_failures as f64 / self.num_elements_checked as f64) * 100.0
152 }
153 }
154
155 pub fn summary(&self) -> String {
157 if self.passed {
158 format!(
159 "✓ Gradient check passed\n\
160 Elements checked: {}\n\
161 Max relative error: {:.2e}\n\
162 Max absolute error: {:.2e}",
163 self.num_elements_checked, self.max_relative_error, self.max_absolute_error
164 )
165 } else {
166 format!(
167 "✗ Gradient check FAILED\n\
168 Elements checked: {}\n\
169 Failures: {} ({:.2}%)\n\
170 Max relative error: {:.2e}\n\
171 Max absolute error: {:.2e}\n\
172 {}",
173 self.num_elements_checked,
174 self.num_failures,
175 self.failure_rate(),
176 self.max_relative_error,
177 self.max_absolute_error,
178 self.error_message.as_deref().unwrap_or("")
179 )
180 }
181 }
182}
183
184pub struct NumericalGradientChecker<T> {
186 config: GradientCheckConfig,
187 _phantom: PhantomData<T>,
188}
189
190impl<T> NumericalGradientChecker<T>
191where
192 T: Float + FromPrimitive + Clone + Send + Sync + Default + 'static,
193{
194 pub fn new(config: GradientCheckConfig) -> Self {
196 Self {
197 config,
198 _phantom: PhantomData,
199 }
200 }
201
202 pub fn compute_numerical_gradient<F>(&self, input: &Tensor<T>, func: F) -> Result<Tensor<T>>
204 where
205 F: Fn(&Tensor<T>) -> Result<Tensor<T>>,
206 {
207 let input_data = input.data();
208 let input_shape = input.shape();
209 let mut gradient_data = Vec::with_capacity(input_data.len());
210
211 let epsilon = T::from_f64(self.config.epsilon).ok_or_else(|| {
212 TensorError::invalid_operation_simple("Failed to convert epsilon".to_string())
213 })?;
214
215 for i in 0..input_data.len() {
216 let grad = if self.config.use_central_difference {
217 let mut input_plus = input_data.to_vec();
219 let mut input_minus = input_data.to_vec();
220
221 input_plus[i] = input_plus[i] + epsilon;
222 input_minus[i] = input_minus[i] - epsilon;
223
224 let x_plus = Tensor::from_array(
225 scirs2_core::ndarray::Array::from_shape_vec(
226 input_shape.dims().to_vec(),
227 input_plus.to_vec(),
228 )
229 .map_err(|e| TensorError::invalid_argument(format!("Shape mismatch: {}", e)))?
230 .into_dyn(),
231 );
232
233 let x_minus = Tensor::from_array(
234 scirs2_core::ndarray::Array::from_shape_vec(
235 input_shape.dims().to_vec(),
236 input_minus.to_vec(),
237 )
238 .map_err(|e| TensorError::invalid_argument(format!("Shape mismatch: {}", e)))?
239 .into_dyn(),
240 );
241
242 let f_plus = func(&x_plus)?;
243 let f_minus = func(&x_minus)?;
244
245 let two_epsilon = epsilon + epsilon;
247 let diff = f_plus.data()[i] - f_minus.data()[i];
248 diff / two_epsilon
249 } else {
250 let mut input_plus = input_data.to_vec();
252 input_plus[i] = input_plus[i] + epsilon;
253
254 let x_plus = Tensor::from_array(
255 scirs2_core::ndarray::Array::from_shape_vec(
256 input_shape.dims().to_vec(),
257 input_plus.to_vec(),
258 )
259 .map_err(|e| TensorError::invalid_argument(format!("Shape mismatch: {}", e)))?
260 .into_dyn(),
261 );
262
263 let f_plus = func(&x_plus)?;
264 let f_x = func(input)?;
265
266 let diff = f_plus.data()[i] - f_x.data()[i];
268 diff / epsilon
269 };
270
271 gradient_data.push(grad);
272 }
273
274 let gradient_array =
275 scirs2_core::ndarray::Array::from_shape_vec(input_shape.dims().to_vec(), gradient_data)
276 .map_err(|e| TensorError::invalid_argument(format!("Shape mismatch: {}", e)))?
277 .into_dyn();
278
279 Ok(Tensor::from_array(gradient_array))
280 }
281
282 pub fn compare_gradients(
284 &self,
285 numerical: &Tensor<T>,
286 analytical: &Tensor<T>,
287 ) -> Result<GradientCheckResult> {
288 if numerical.shape() != analytical.shape() {
289 return Err(TensorError::invalid_argument(format!(
290 "Shape mismatch: numerical {:?} vs analytical {:?}",
291 numerical.shape(),
292 analytical.shape()
293 )));
294 }
295
296 let num_data = numerical.data();
297 let ana_data = analytical.data();
298
299 let rtol = self.config.rtol;
300 let atol = self.config.atol;
301
302 let mut max_rel_error = 0.0;
303 let mut max_abs_error = 0.0;
304 let mut num_failures = 0;
305 let mut failed_indices = Vec::new();
306
307 for i in 0..num_data.len() {
308 let num_val = num_data[i].to_f64().unwrap_or(0.0);
309 let ana_val = ana_data[i].to_f64().unwrap_or(0.0);
310
311 let abs_error = (num_val - ana_val).abs();
312 let rel_error = if ana_val.abs() > 1e-10 {
313 abs_error / ana_val.abs()
314 } else {
315 abs_error
316 };
317
318 max_rel_error = max_rel_error.max(rel_error);
319 max_abs_error = max_abs_error.max(abs_error);
320
321 if rel_error > rtol && abs_error > atol {
322 num_failures += 1;
323 if self.config.check_elementwise {
324 failed_indices.push(i);
325 }
326 }
327 }
328
329 let passed = num_failures == 0;
330 let error_message = if !passed {
331 Some(format!(
332 "Gradient mismatch: {} of {} elements exceed tolerance (rtol={}, atol={})",
333 num_failures,
334 num_data.len(),
335 rtol,
336 atol
337 ))
338 } else {
339 None
340 };
341
342 Ok(GradientCheckResult {
343 passed,
344 max_relative_error: max_rel_error,
345 max_absolute_error: max_abs_error,
346 num_elements_checked: num_data.len(),
347 num_failures,
348 failed_indices,
349 error_message,
350 })
351 }
352
353 pub fn check<F, G>(
355 &self,
356 input: &Tensor<T>,
357 forward: F,
358 gradient: G,
359 ) -> Result<GradientCheckResult>
360 where
361 F: Fn(&Tensor<T>) -> Result<Tensor<T>>,
362 G: Fn(&Tensor<T>) -> Result<Tensor<T>>,
363 {
364 let numerical_grad = self.compute_numerical_gradient(input, forward)?;
365 let analytical_grad = gradient(input)?;
366
367 self.compare_gradients(&numerical_grad, &analytical_grad)
368 }
369}
370
371pub fn check_gradients<T, F, G>(
373 input: &Tensor<T>,
374 forward: F,
375 gradient: G,
376 config: &GradientCheckConfig,
377) -> Result<GradientCheckResult>
378where
379 T: Float + FromPrimitive + Clone + Send + Sync + Default + 'static,
380 F: Fn(&Tensor<T>) -> Result<Tensor<T>>,
381 G: Fn(&Tensor<T>) -> Result<Tensor<T>>,
382{
383 let checker = NumericalGradientChecker::new(config.clone());
384 checker.check(input, forward, gradient)
385}
386
387pub fn quick_check_gradients<T, F, G>(
389 input: &Tensor<T>,
390 forward: F,
391 gradient: G,
392) -> Result<GradientCheckResult>
393where
394 T: Float + FromPrimitive + Clone + Send + Sync + Default + 'static,
395 F: Fn(&Tensor<T>) -> Result<Tensor<T>>,
396 G: Fn(&Tensor<T>) -> Result<Tensor<T>>,
397{
398 check_gradients(input, forward, gradient, &GradientCheckConfig::default())
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404 use scirs2_core::ndarray::array;
405
406 #[test]
407 fn test_gradient_check_linear() {
408 let input = Tensor::from_array(array![1.0, 2.0, 3.0].into_dyn());
410
411 let forward = |x: &Tensor<f32>| {
412 let data: Vec<f32> = x.data().iter().map(|&v| v * 2.0).collect();
413 let result_array = scirs2_core::ndarray::Array::from_vec(data).into_dyn();
414 Ok(Tensor::from_array(result_array))
415 };
416
417 let gradient = |_x: &Tensor<f32>| {
418 let grad = array![2.0, 2.0, 2.0].into_dyn();
419 Ok(Tensor::from_array(grad))
420 };
421
422 let config = GradientCheckConfig::relaxed();
424 let result = check_gradients(&input, forward, gradient, &config)
425 .expect("test: check_gradients should succeed");
426
427 assert!(
428 result.passed,
429 "Gradient check should pass for linear function: {}",
430 result.summary()
431 );
432 }
433
434 #[test]
435 fn test_gradient_check_square() {
436 let input = Tensor::from_array(array![1.0, 2.0, 3.0].into_dyn());
438
439 let forward = |x: &Tensor<f32>| {
440 let data: Vec<f32> = x.data().iter().map(|&v| v * v).collect();
441 let result_array = scirs2_core::ndarray::Array::from_vec(data).into_dyn();
442 Ok(Tensor::from_array(result_array))
443 };
444
445 let gradient = |x: &Tensor<f32>| {
446 let data: Vec<f32> = x.data().iter().map(|&v| 2.0 * v).collect();
447 let grad_array = scirs2_core::ndarray::Array::from_vec(data).into_dyn();
448 Ok(Tensor::from_array(grad_array))
449 };
450
451 let config = GradientCheckConfig::relaxed();
453 let result = check_gradients(&input, forward, gradient, &config)
454 .expect("test: check_gradients should succeed");
455
456 assert!(
457 result.passed,
458 "Gradient check should pass for square function: {}",
459 result.summary()
460 );
461 }
462
463 #[test]
464 fn test_gradient_check_incorrect_gradient() {
465 let input = Tensor::from_array(array![1.0, 2.0, 3.0].into_dyn());
467
468 let forward = |x: &Tensor<f32>| {
469 let data: Vec<f32> = x.data().iter().map(|&v| v * v).collect();
470 let result_array = scirs2_core::ndarray::Array::from_vec(data).into_dyn();
471 Ok(Tensor::from_array(result_array))
472 };
473
474 let wrong_gradient = |x: &Tensor<f32>| {
475 let data: Vec<f32> = x.data().iter().map(|&v| 3.0 * v).collect();
477 let grad_array = scirs2_core::ndarray::Array::from_vec(data).into_dyn();
478 Ok(Tensor::from_array(grad_array))
479 };
480
481 let config = GradientCheckConfig::default();
482 let result = check_gradients(&input, forward, wrong_gradient, &config)
483 .expect("test: check_gradients should succeed");
484
485 assert!(
486 !result.passed,
487 "Gradient check should fail for incorrect gradient"
488 );
489 assert!(result.num_failures > 0);
490 }
491
492 #[test]
493 fn test_gradient_check_config_tolerances() {
494 let input = Tensor::from_array(array![1.0].into_dyn());
495
496 let forward = |x: &Tensor<f32>| {
497 let data: Vec<f32> = x.data().iter().map(|&v| v * v).collect();
498 let result_array = scirs2_core::ndarray::Array::from_vec(data).into_dyn();
499 Ok(Tensor::from_array(result_array))
500 };
501
502 let slightly_off_gradient = |x: &Tensor<f32>| {
503 let data: Vec<f32> = x.data().iter().map(|&v| 2.0 * v * 1.01).collect();
505 let grad_array = scirs2_core::ndarray::Array::from_vec(data).into_dyn();
506 Ok(Tensor::from_array(grad_array))
507 };
508
509 let relaxed = GradientCheckConfig::relaxed();
511 let result = check_gradients(&input, forward, slightly_off_gradient, &relaxed)
512 .expect("test: check_gradients should succeed");
513 assert!(result.passed, "Should pass with relaxed tolerances");
514
515 let strict = GradientCheckConfig::strict();
517 let result = check_gradients(&input, forward, slightly_off_gradient, &strict)
518 .expect("test: check_gradients should succeed");
519 assert!(!result.passed, "Should fail with strict tolerances");
520 }
521
522 #[test]
523 fn test_gradient_check_result_summary() {
524 let result = GradientCheckResult {
525 passed: false,
526 max_relative_error: 0.05,
527 max_absolute_error: 0.01,
528 num_elements_checked: 100,
529 num_failures: 10,
530 failed_indices: vec![],
531 error_message: Some("Test error".to_string()),
532 };
533
534 let summary = result.summary();
535 assert!(summary.contains("FAILED"));
536 assert!(summary.contains("10.00%"));
537
538 assert_eq!(result.failure_rate(), 10.0);
539 }
540}