datasynth_eval/coherence/
manufacturing.rs1use crate::error::EvalResult;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ManufacturingThresholds {
13 pub min_yield_consistency: f64,
15 pub min_sequence_valid: f64,
17 pub min_defect_rate_accuracy: f64,
19 pub min_variance_accuracy: f64,
21}
22
23impl Default for ManufacturingThresholds {
24 fn default() -> Self {
25 Self {
26 min_yield_consistency: 0.95,
27 min_sequence_valid: 0.99,
28 min_defect_rate_accuracy: 0.99,
29 min_variance_accuracy: 0.99,
30 }
31 }
32}
33
34#[derive(Debug, Clone)]
36pub struct ProductionOrderData {
37 pub order_id: String,
39 pub actual_quantity: f64,
41 pub scrap_quantity: f64,
43 pub reported_yield: f64,
45 pub planned_cost: f64,
47 pub actual_cost: f64,
49}
50
51#[derive(Debug, Clone)]
53pub struct RoutingOperationData {
54 pub order_id: String,
56 pub sequence_number: u32,
58 pub start_timestamp: i64,
60}
61
62#[derive(Debug, Clone)]
64pub struct QualityInspectionData {
65 pub lot_id: String,
67 pub sample_size: u32,
69 pub defect_count: u32,
71 pub reported_defect_rate: f64,
73 pub characteristics_within_limits: u32,
75 pub total_characteristics: u32,
77}
78
79#[derive(Debug, Clone)]
81pub struct CycleCountData {
82 pub record_id: String,
84 pub book_quantity: f64,
86 pub counted_quantity: f64,
88 pub reported_variance: f64,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ManufacturingEvaluation {
95 pub yield_rate_consistency: f64,
97 pub avg_cost_variance_ratio: f64,
99 pub operation_sequence_valid: f64,
101 pub defect_rate_accuracy: f64,
103 pub characteristics_compliance: f64,
105 pub variance_calculation_accuracy: f64,
107 pub total_orders: usize,
109 pub total_inspections: usize,
111 pub total_cycle_counts: usize,
113 pub passes: bool,
115 pub issues: Vec<String>,
117}
118
119pub struct ManufacturingEvaluator {
121 thresholds: ManufacturingThresholds,
122}
123
124impl ManufacturingEvaluator {
125 pub fn new() -> Self {
127 Self {
128 thresholds: ManufacturingThresholds::default(),
129 }
130 }
131
132 pub fn with_thresholds(thresholds: ManufacturingThresholds) -> Self {
134 Self { thresholds }
135 }
136
137 pub fn evaluate(
139 &self,
140 orders: &[ProductionOrderData],
141 operations: &[RoutingOperationData],
142 inspections: &[QualityInspectionData],
143 cycle_counts: &[CycleCountData],
144 ) -> EvalResult<ManufacturingEvaluation> {
145 let mut issues = Vec::new();
146
147 let yield_ok = orders
149 .iter()
150 .filter(|o| {
151 let total = o.actual_quantity + o.scrap_quantity;
152 if total <= 0.0 {
153 return true; }
155 let expected_yield = o.actual_quantity / total;
156 (o.reported_yield - expected_yield).abs() <= 0.001
157 })
158 .count();
159 let yield_rate_consistency = if orders.is_empty() {
160 1.0
161 } else {
162 yield_ok as f64 / orders.len() as f64
163 };
164
165 let cost_variances: Vec<f64> = orders
167 .iter()
168 .filter(|o| o.planned_cost > 0.0)
169 .map(|o| (o.actual_cost - o.planned_cost).abs() / o.planned_cost)
170 .collect();
171 let avg_cost_variance_ratio = if cost_variances.is_empty() {
172 0.0
173 } else {
174 cost_variances.iter().sum::<f64>() / cost_variances.len() as f64
175 };
176
177 let mut order_ops: std::collections::HashMap<&str, Vec<&RoutingOperationData>> =
179 std::collections::HashMap::new();
180 for op in operations {
181 order_ops.entry(op.order_id.as_str()).or_default().push(op);
182 }
183 let total_order_groups = order_ops.len();
184 let seq_valid = order_ops
185 .values()
186 .filter(|ops| {
187 let mut sorted = ops.to_vec();
188 sorted.sort_by_key(|o| o.sequence_number);
189 sorted.windows(2).all(|w| {
191 w[0].sequence_number < w[1].sequence_number
192 && w[0].start_timestamp <= w[1].start_timestamp
193 })
194 })
195 .count();
196 let operation_sequence_valid = if total_order_groups == 0 {
197 1.0
198 } else {
199 seq_valid as f64 / total_order_groups as f64
200 };
201
202 let defect_ok = inspections
204 .iter()
205 .filter(|insp| {
206 if insp.sample_size == 0 {
207 return true;
208 }
209 let expected_rate = insp.defect_count as f64 / insp.sample_size as f64;
210 (insp.reported_defect_rate - expected_rate).abs() <= 0.001
211 })
212 .count();
213 let defect_rate_accuracy = if inspections.is_empty() {
214 1.0
215 } else {
216 defect_ok as f64 / inspections.len() as f64
217 };
218
219 let total_chars: u32 = inspections.iter().map(|i| i.total_characteristics).sum();
221 let within_chars: u32 = inspections
222 .iter()
223 .map(|i| i.characteristics_within_limits)
224 .sum();
225 let characteristics_compliance = if total_chars == 0 {
226 1.0
227 } else {
228 within_chars as f64 / total_chars as f64
229 };
230
231 let variance_ok = cycle_counts
233 .iter()
234 .filter(|cc| {
235 let expected_variance = cc.counted_quantity - cc.book_quantity;
236 (cc.reported_variance - expected_variance).abs() <= 0.01
237 })
238 .count();
239 let variance_calculation_accuracy = if cycle_counts.is_empty() {
240 1.0
241 } else {
242 variance_ok as f64 / cycle_counts.len() as f64
243 };
244
245 if yield_rate_consistency < self.thresholds.min_yield_consistency {
247 issues.push(format!(
248 "Yield consistency {:.3} < {:.3}",
249 yield_rate_consistency, self.thresholds.min_yield_consistency
250 ));
251 }
252 if operation_sequence_valid < self.thresholds.min_sequence_valid {
253 issues.push(format!(
254 "Operation sequence validity {:.3} < {:.3}",
255 operation_sequence_valid, self.thresholds.min_sequence_valid
256 ));
257 }
258 if defect_rate_accuracy < self.thresholds.min_defect_rate_accuracy {
259 issues.push(format!(
260 "Defect rate accuracy {:.3} < {:.3}",
261 defect_rate_accuracy, self.thresholds.min_defect_rate_accuracy
262 ));
263 }
264 if variance_calculation_accuracy < self.thresholds.min_variance_accuracy {
265 issues.push(format!(
266 "Variance calculation accuracy {:.3} < {:.3}",
267 variance_calculation_accuracy, self.thresholds.min_variance_accuracy
268 ));
269 }
270
271 let passes = issues.is_empty();
272
273 Ok(ManufacturingEvaluation {
274 yield_rate_consistency,
275 avg_cost_variance_ratio,
276 operation_sequence_valid,
277 defect_rate_accuracy,
278 characteristics_compliance,
279 variance_calculation_accuracy,
280 total_orders: orders.len(),
281 total_inspections: inspections.len(),
282 total_cycle_counts: cycle_counts.len(),
283 passes,
284 issues,
285 })
286 }
287}
288
289impl Default for ManufacturingEvaluator {
290 fn default() -> Self {
291 Self::new()
292 }
293}
294
295#[derive(Debug, Clone)]
303pub struct ManufacturingGLProofData {
304 pub gl_wip_total: rust_decimal::Decimal,
306 pub gl_fg_total: rust_decimal::Decimal,
308 pub gl_cogs_total: rust_decimal::Decimal,
310 pub production_order_total_cost: rust_decimal::Decimal,
312 pub production_order_count: usize,
314 pub manufacturing_je_count: usize,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct ManufacturingGLProofEvaluation {
321 pub flow_coherent: bool,
323 pub cost_gl_difference: rust_decimal::Decimal,
325 pub all_orders_posted: bool,
327 pub issues: Vec<String>,
329}
330
331pub struct ManufacturingGLProofEvaluator {
333 tolerance: rust_decimal::Decimal,
334}
335
336impl ManufacturingGLProofEvaluator {
337 pub fn new(tolerance: rust_decimal::Decimal) -> Self {
339 Self { tolerance }
340 }
341
342 pub fn evaluate(
344 &self,
345 data: &ManufacturingGLProofData,
346 ) -> EvalResult<ManufacturingGLProofEvaluation> {
347 let mut issues = Vec::new();
348
349 let cost_gl_difference =
352 (data.production_order_total_cost - data.gl_cogs_total - data.gl_fg_total).abs();
353
354 if cost_gl_difference > self.tolerance && data.production_order_count > 0 {
355 issues.push(format!(
356 "Manufacturing cost/GL gap: production orders total={}, FG+COGS GL total={}, diff={}",
357 data.production_order_total_cost,
358 data.gl_fg_total + data.gl_cogs_total,
359 cost_gl_difference
360 ));
361 }
362
363 let all_orders_posted = data.manufacturing_je_count >= data.production_order_count
364 || data.production_order_count == 0;
365 if !all_orders_posted {
366 issues.push(format!(
367 "Not all production orders have GL postings: {} orders vs {} manufacturing JEs",
368 data.production_order_count, data.manufacturing_je_count
369 ));
370 }
371
372 Ok(ManufacturingGLProofEvaluation {
373 flow_coherent: issues.is_empty(),
374 cost_gl_difference,
375 all_orders_posted,
376 issues,
377 })
378 }
379}
380
381impl Default for ManufacturingGLProofEvaluator {
382 fn default() -> Self {
383 Self::new(rust_decimal::Decimal::new(100, 0)) }
385}
386
387#[cfg(test)]
388#[allow(clippy::unwrap_used)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn test_valid_manufacturing_data() {
394 let evaluator = ManufacturingEvaluator::new();
395 let orders = vec![ProductionOrderData {
396 order_id: "PO001".to_string(),
397 actual_quantity: 90.0,
398 scrap_quantity: 10.0,
399 reported_yield: 0.9, planned_cost: 10_000.0,
401 actual_cost: 10_500.0,
402 }];
403
404 let operations = vec![
405 RoutingOperationData {
406 order_id: "PO001".to_string(),
407 sequence_number: 10,
408 start_timestamp: 1000,
409 },
410 RoutingOperationData {
411 order_id: "PO001".to_string(),
412 sequence_number: 20,
413 start_timestamp: 2000,
414 },
415 ];
416
417 let inspections = vec![QualityInspectionData {
418 lot_id: "LOT001".to_string(),
419 sample_size: 100,
420 defect_count: 5,
421 reported_defect_rate: 0.05,
422 characteristics_within_limits: 95,
423 total_characteristics: 100,
424 }];
425
426 let cycle_counts = vec![CycleCountData {
427 record_id: "CC001".to_string(),
428 book_quantity: 100.0,
429 counted_quantity: 98.0,
430 reported_variance: -2.0,
431 }];
432
433 let result = evaluator
434 .evaluate(&orders, &operations, &inspections, &cycle_counts)
435 .unwrap();
436 assert!(result.passes);
437 assert_eq!(result.yield_rate_consistency, 1.0);
438 assert_eq!(result.defect_rate_accuracy, 1.0);
439 }
440
441 #[test]
442 fn test_wrong_yield() {
443 let evaluator = ManufacturingEvaluator::new();
444 let orders = vec![ProductionOrderData {
445 order_id: "PO001".to_string(),
446 actual_quantity: 90.0,
447 scrap_quantity: 10.0,
448 reported_yield: 0.5, planned_cost: 10_000.0,
450 actual_cost: 10_000.0,
451 }];
452
453 let result = evaluator.evaluate(&orders, &[], &[], &[]).unwrap();
454 assert!(!result.passes);
455 }
456
457 #[test]
458 fn test_out_of_order_operations() {
459 let evaluator = ManufacturingEvaluator::new();
460 let operations = vec![
461 RoutingOperationData {
462 order_id: "PO001".to_string(),
463 sequence_number: 10,
464 start_timestamp: 2000, },
466 RoutingOperationData {
467 order_id: "PO001".to_string(),
468 sequence_number: 20,
469 start_timestamp: 1000, },
471 ];
472
473 let result = evaluator.evaluate(&[], &operations, &[], &[]).unwrap();
474 assert_eq!(result.operation_sequence_valid, 0.0);
475 }
476
477 #[test]
478 fn test_empty_data() {
479 let evaluator = ManufacturingEvaluator::new();
480 let result = evaluator.evaluate(&[], &[], &[], &[]).unwrap();
481 assert!(result.passes);
482 }
483}