Coverage Report

Created: 2026-01-25 15:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/trueno/src/simulation/jidoka.rs
Line
Count
Source
1
//! Jidoka Guard (Built-in Quality: Stop on Defect)
2
//!
3
//! Implements Toyota Production System's Jidoka principle:
4
//! stop production when a defect is detected.
5
6
/// Jidoka condition that triggers stop
7
#[derive(Debug, Clone, PartialEq)]
8
pub enum JidokaCondition {
9
    /// NaN detected in output
10
    NanDetected,
11
    /// Infinity detected in output
12
    InfDetected,
13
    /// Cross-backend divergence exceeds tolerance
14
    BackendDivergence {
15
        /// Tolerance threshold
16
        tolerance: f32,
17
    },
18
    /// Performance regression exceeds threshold
19
    PerformanceRegression {
20
        /// Threshold percentage
21
        threshold_pct: f32,
22
    },
23
    /// Determinism failure (same seed, different output)
24
    DeterminismFailure,
25
}
26
27
/// Jidoka action on condition trigger
28
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29
pub enum JidokaAction {
30
    /// Stop immediately and report
31
    Stop,
32
    /// Log and continue (soft Jidoka)
33
    LogAndContinue,
34
    /// Trigger visual diff report
35
    VisualReport,
36
}
37
38
/// Jidoka error types
39
#[derive(Debug, Clone)]
40
pub enum JidokaError {
41
    /// NaN values detected
42
    NanDetected {
43
        /// Context description
44
        context: String,
45
        /// Indices of NaN values
46
        indices: Vec<usize>,
47
    },
48
    /// Infinity values detected
49
    InfDetected {
50
        /// Context description
51
        context: String,
52
        /// Indices of infinite values
53
        indices: Vec<usize>,
54
    },
55
    /// Backend divergence detected
56
    BackendDivergence {
57
        /// Context description
58
        context: String,
59
        /// Maximum difference found
60
        max_diff: f32,
61
        /// Tolerance threshold
62
        tolerance: f32,
63
    },
64
    /// Performance regression detected
65
    PerformanceRegression {
66
        /// Context description
67
        context: String,
68
        /// Actual regression percentage
69
        regression_pct: f32,
70
        /// Threshold percentage
71
        threshold_pct: f32,
72
    },
73
    /// Determinism failure detected
74
    DeterminismFailure {
75
        /// Context description
76
        context: String,
77
        /// First differing index
78
        first_diff_index: usize,
79
    },
80
}
81
82
impl std::fmt::Display for JidokaError {
83
0
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84
0
        match self {
85
0
            Self::NanDetected { context, indices } => {
86
0
                write!(
87
0
                    f,
88
0
                    "Jidoka: NaN detected at {context} (indices: {indices:?})"
89
                )
90
            }
91
0
            Self::InfDetected { context, indices } => {
92
0
                write!(
93
0
                    f,
94
0
                    "Jidoka: Infinity detected at {context} (indices: {indices:?})"
95
                )
96
            }
97
            Self::BackendDivergence {
98
0
                context,
99
0
                max_diff,
100
0
                tolerance,
101
            } => {
102
0
                write!(
103
0
                    f,
104
0
                    "Jidoka: Backend divergence at {context} (max_diff: {max_diff}, tolerance: {tolerance})"
105
                )
106
            }
107
            Self::PerformanceRegression {
108
0
                context,
109
0
                regression_pct,
110
0
                threshold_pct,
111
            } => {
112
0
                write!(
113
0
                    f,
114
0
                    "Jidoka: Performance regression at {context} ({regression_pct:.2}% > {threshold_pct:.2}%)"
115
                )
116
            }
117
            Self::DeterminismFailure {
118
0
                context,
119
0
                first_diff_index,
120
            } => {
121
0
                write!(
122
0
                    f,
123
0
                    "Jidoka: Determinism failure at {context} (first diff at index {first_diff_index})"
124
                )
125
            }
126
        }
127
0
    }
128
}
129
130
impl std::error::Error for JidokaError {}
131
132
/// Jidoka guard for simulation tests
133
///
134
/// Implements Toyota Production System's Jidoka principle:
135
/// stop production when a defect is detected.
136
#[derive(Debug, Clone)]
137
pub struct JidokaGuard {
138
    /// Condition that triggers stop
139
    pub condition: JidokaCondition,
140
    /// Action to take on trigger
141
    pub action: JidokaAction,
142
    /// Context for debugging
143
    pub context: String,
144
}
145
146
impl JidokaGuard {
147
    /// Create a new Jidoka guard
148
    #[must_use]
149
0
    pub fn new(
150
0
        condition: JidokaCondition,
151
0
        action: JidokaAction,
152
0
        context: impl Into<String>,
153
0
    ) -> Self {
154
0
        Self {
155
0
            condition,
156
0
            action,
157
0
            context: context.into(),
158
0
        }
159
0
    }
160
161
    /// Create a NaN detection guard
162
    #[must_use]
163
0
    pub fn nan_guard(context: impl Into<String>) -> Self {
164
0
        Self::new(JidokaCondition::NanDetected, JidokaAction::Stop, context)
165
0
    }
166
167
    /// Create an infinity detection guard
168
    #[must_use]
169
0
    pub fn inf_guard(context: impl Into<String>) -> Self {
170
0
        Self::new(JidokaCondition::InfDetected, JidokaAction::Stop, context)
171
0
    }
172
173
    /// Create a backend divergence guard
174
    #[must_use]
175
0
    pub fn divergence_guard(tolerance: f32, context: impl Into<String>) -> Self {
176
0
        Self::new(
177
0
            JidokaCondition::BackendDivergence { tolerance },
178
0
            JidokaAction::Stop,
179
0
            context,
180
        )
181
0
    }
182
183
    /// Check output for NaN/Inf and return error if found
184
    ///
185
    /// # Errors
186
    ///
187
    /// Returns `JidokaError` if the condition is triggered
188
0
    pub fn check_output(&self, output: &[f32]) -> Result<(), JidokaError> {
189
0
        match &self.condition {
190
            JidokaCondition::NanDetected => {
191
0
                let nan_indices: Vec<usize> = output
192
0
                    .iter()
193
0
                    .enumerate()
194
0
                    .filter(|(_, x)| x.is_nan())
195
0
                    .map(|(i, _)| i)
196
0
                    .collect();
197
198
0
                if !nan_indices.is_empty() {
199
0
                    return Err(JidokaError::NanDetected {
200
0
                        context: self.context.clone(),
201
0
                        indices: nan_indices,
202
0
                    });
203
0
                }
204
            }
205
            JidokaCondition::InfDetected => {
206
0
                let inf_indices: Vec<usize> = output
207
0
                    .iter()
208
0
                    .enumerate()
209
0
                    .filter(|(_, x)| x.is_infinite())
210
0
                    .map(|(i, _)| i)
211
0
                    .collect();
212
213
0
                if !inf_indices.is_empty() {
214
0
                    return Err(JidokaError::InfDetected {
215
0
                        context: self.context.clone(),
216
0
                        indices: inf_indices,
217
0
                    });
218
0
                }
219
            }
220
0
            _ => {} // Other conditions handled by compare methods
221
        }
222
0
        Ok(())
223
0
    }
224
225
    /// Compare two outputs for backend divergence
226
    ///
227
    /// # Errors
228
    ///
229
    /// Returns `JidokaError` if divergence exceeds tolerance
230
0
    pub fn check_divergence(&self, a: &[f32], b: &[f32]) -> Result<(), JidokaError> {
231
0
        if let JidokaCondition::BackendDivergence { tolerance } = &self.condition {
232
0
            let max_diff = a
233
0
                .iter()
234
0
                .zip(b.iter())
235
0
                .map(|(x, y)| (x - y).abs())
236
0
                .fold(0.0_f32, f32::max);
237
238
0
            if max_diff > *tolerance {
239
0
                return Err(JidokaError::BackendDivergence {
240
0
                    context: self.context.clone(),
241
0
                    max_diff,
242
0
                    tolerance: *tolerance,
243
0
                });
244
0
            }
245
0
        }
246
0
        Ok(())
247
0
    }
248
249
    /// Check for determinism (same inputs should produce same outputs)
250
    ///
251
    /// # Errors
252
    ///
253
    /// Returns `JidokaError` if outputs differ
254
0
    pub fn check_determinism(&self, a: &[f32], b: &[f32]) -> Result<(), JidokaError> {
255
0
        if let JidokaCondition::DeterminismFailure = &self.condition {
256
0
            for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
257
                // Use bitwise comparison for exact equality
258
0
                if x.to_bits() != y.to_bits() {
259
0
                    return Err(JidokaError::DeterminismFailure {
260
0
                        context: self.context.clone(),
261
0
                        first_diff_index: i,
262
0
                    });
263
0
                }
264
            }
265
0
        }
266
0
        Ok(())
267
0
    }
268
}
269
270
#[cfg(test)]
271
mod tests {
272
    use super::*;
273
274
    #[test]
275
    fn test_jidoka_nan_detection() {
276
        // Falsifiable claim B-027
277
        let guard = JidokaGuard::nan_guard("test_operation");
278
        let output_with_nan = vec![1.0, 2.0, f32::NAN, 4.0];
279
280
        let result = guard.check_output(&output_with_nan);
281
        assert!(result.is_err());
282
283
        if let Err(JidokaError::NanDetected { indices, .. }) = result {
284
            assert_eq!(indices, vec![2]);
285
        } else {
286
            panic!("Expected NanDetected error");
287
        }
288
    }
289
290
    #[test]
291
    fn test_jidoka_nan_no_false_positive() {
292
        let guard = JidokaGuard::nan_guard("test_operation");
293
        let clean_output = vec![1.0, 2.0, 3.0, 4.0];
294
295
        let result = guard.check_output(&clean_output);
296
        assert!(result.is_ok());
297
    }
298
299
    #[test]
300
    fn test_jidoka_inf_detection() {
301
        // Falsifiable claim B-028
302
        let guard = JidokaGuard::inf_guard("test_operation");
303
        let output_with_inf = vec![1.0, f32::INFINITY, 3.0, f32::NEG_INFINITY];
304
305
        let result = guard.check_output(&output_with_inf);
306
        assert!(result.is_err());
307
308
        if let Err(JidokaError::InfDetected { indices, .. }) = result {
309
            assert_eq!(indices, vec![1, 3]);
310
        } else {
311
            panic!("Expected InfDetected error");
312
        }
313
    }
314
315
    #[test]
316
    fn test_jidoka_divergence_detection() {
317
        // Falsifiable claim A-004
318
        let guard = JidokaGuard::divergence_guard(1e-5, "cross_backend");
319
        let a = vec![1.0, 2.0, 3.0, 4.0];
320
        let b = vec![1.0, 2.0, 3.1, 4.0]; // 0.1 diff at index 2
321
322
        let result = guard.check_divergence(&a, &b);
323
        assert!(result.is_err());
324
325
        if let Err(JidokaError::BackendDivergence { max_diff, .. }) = result {
326
            assert!((max_diff - 0.1).abs() < 1e-6);
327
        } else {
328
            panic!("Expected BackendDivergence error");
329
        }
330
    }
331
332
    #[test]
333
    fn test_jidoka_divergence_within_tolerance() {
334
        let guard = JidokaGuard::divergence_guard(1e-5, "cross_backend");
335
        let a = vec![1.0, 2.0, 3.0, 4.0];
336
        let b = vec![1.0, 2.0, 3.0 + 1e-7, 4.0]; // Within tolerance
337
338
        let result = guard.check_divergence(&a, &b);
339
        assert!(result.is_ok());
340
    }
341
342
    #[test]
343
    fn test_jidoka_determinism_check() {
344
        // Falsifiable claim B-017
345
        let guard = JidokaGuard::new(
346
            JidokaCondition::DeterminismFailure,
347
            JidokaAction::Stop,
348
            "determinism_test",
349
        );
350
351
        let a = vec![1.0, 2.0, 3.0, 4.0];
352
        let b = vec![1.0, 2.0, 3.0, 4.0];
353
354
        let result = guard.check_determinism(&a, &b);
355
        assert!(result.is_ok());
356
    }
357
358
    #[test]
359
    fn test_jidoka_determinism_failure() {
360
        let guard = JidokaGuard::new(
361
            JidokaCondition::DeterminismFailure,
362
            JidokaAction::Stop,
363
            "determinism_test",
364
        );
365
366
        let a: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0];
367
        let b: Vec<f32> = vec![1.0, 2.0, 3.000_001, 4.0]; // Different bit pattern
368
369
        // Verify they actually have different bits
370
        assert_ne!(a[2].to_bits(), b[2].to_bits(), "Test values must differ");
371
372
        let result = guard.check_determinism(&a, &b);
373
        assert!(result.is_err());
374
375
        if let Err(JidokaError::DeterminismFailure {
376
            first_diff_index, ..
377
        }) = result
378
        {
379
            assert_eq!(first_diff_index, 2);
380
        } else {
381
            panic!("Expected DeterminismFailure error");
382
        }
383
    }
384
385
    #[test]
386
    fn test_jidoka_error_display() {
387
        let err = JidokaError::NanDetected {
388
            context: "test".to_string(),
389
            indices: vec![0, 2],
390
        };
391
        let display = format!("{err}");
392
        assert!(display.contains("NaN"));
393
        assert!(display.contains("test"));
394
395
        let err2 = JidokaError::BackendDivergence {
396
            context: "cross".to_string(),
397
            max_diff: 0.01,
398
            tolerance: 0.001,
399
        };
400
        let display2 = format!("{err2}");
401
        assert!(display2.contains("divergence"));
402
    }
403
404
    #[test]
405
    fn test_empty_output_checks() {
406
        let guard = JidokaGuard::nan_guard("empty_test");
407
        let result = guard.check_output(&[]);
408
        assert!(result.is_ok());
409
    }
410
411
    #[test]
412
    fn test_single_element_checks() {
413
        let guard = JidokaGuard::nan_guard("single_test");
414
415
        assert!(guard.check_output(&[1.0]).is_ok());
416
        assert!(guard.check_output(&[f32::NAN]).is_err());
417
    }
418
419
    #[test]
420
    fn test_jidoka_condition_clone() {
421
        let condition = JidokaCondition::BackendDivergence { tolerance: 1e-5 };
422
        let cloned = condition.clone();
423
        assert_eq!(condition, cloned);
424
    }
425
426
    #[test]
427
    fn test_jidoka_action_eq() {
428
        assert_eq!(JidokaAction::Stop, JidokaAction::Stop);
429
        assert_ne!(JidokaAction::Stop, JidokaAction::LogAndContinue);
430
    }
431
}