/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 | | } |