Skip to main content

datasynth_eval/coherence/
manufacturing.rs

1//! Manufacturing evaluator.
2//!
3//! Validates manufacturing data coherence including yield rate consistency,
4//! cost variance, operation sequencing, quality inspection accuracy,
5//! and cycle count variance calculations.
6
7use crate::error::EvalResult;
8use serde::{Deserialize, Serialize};
9
10/// Thresholds for manufacturing evaluation.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ManufacturingThresholds {
13    /// Minimum yield rate consistency (fraction of orders with valid yield).
14    pub min_yield_consistency: f64,
15    /// Minimum fraction of orders with valid operation sequences.
16    pub min_sequence_valid: f64,
17    /// Minimum defect rate calculation accuracy.
18    pub min_defect_rate_accuracy: f64,
19    /// Minimum variance calculation accuracy for cycle counts.
20    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/// Production order data for validation.
35#[derive(Debug, Clone)]
36pub struct ProductionOrderData {
37    /// Order identifier.
38    pub order_id: String,
39    /// Actual output quantity.
40    pub actual_quantity: f64,
41    /// Scrap quantity.
42    pub scrap_quantity: f64,
43    /// Reported yield rate.
44    pub reported_yield: f64,
45    /// Planned cost.
46    pub planned_cost: f64,
47    /// Actual cost.
48    pub actual_cost: f64,
49}
50
51/// Routing operation data for sequence validation.
52#[derive(Debug, Clone)]
53pub struct RoutingOperationData {
54    /// Parent order identifier.
55    pub order_id: String,
56    /// Operation sequence number.
57    pub sequence_number: u32,
58    /// Operation start timestamp (epoch seconds).
59    pub start_timestamp: i64,
60}
61
62/// Quality inspection data.
63#[derive(Debug, Clone)]
64pub struct QualityInspectionData {
65    /// Inspection lot identifier.
66    pub lot_id: String,
67    /// Sample size.
68    pub sample_size: u32,
69    /// Defect count.
70    pub defect_count: u32,
71    /// Reported defect rate.
72    pub reported_defect_rate: f64,
73    /// Characteristics within specification limits.
74    pub characteristics_within_limits: u32,
75    /// Total characteristics inspected.
76    pub total_characteristics: u32,
77}
78
79/// Cycle count data.
80#[derive(Debug, Clone)]
81pub struct CycleCountData {
82    /// Count record identifier.
83    pub record_id: String,
84    /// Book quantity.
85    pub book_quantity: f64,
86    /// Counted quantity.
87    pub counted_quantity: f64,
88    /// Reported variance.
89    pub reported_variance: f64,
90}
91
92/// Results of manufacturing evaluation.
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ManufacturingEvaluation {
95    /// Yield rate consistency: fraction of orders with correct yield calculation.
96    pub yield_rate_consistency: f64,
97    /// Average cost variance ratio |actual - planned| / planned.
98    pub avg_cost_variance_ratio: f64,
99    /// Operation sequence validity: fraction with ascending sequence numbers and dates.
100    pub operation_sequence_valid: f64,
101    /// Defect rate accuracy: fraction of inspections with correct defect rate.
102    pub defect_rate_accuracy: f64,
103    /// Characteristics compliance: fraction of characteristics within limits.
104    pub characteristics_compliance: f64,
105    /// Variance calculation accuracy: fraction of cycle counts with correct variance.
106    pub variance_calculation_accuracy: f64,
107    /// Total production orders evaluated.
108    pub total_orders: usize,
109    /// Total inspections evaluated.
110    pub total_inspections: usize,
111    /// Total cycle counts evaluated.
112    pub total_cycle_counts: usize,
113    /// Overall pass/fail.
114    pub passes: bool,
115    /// Issues found.
116    pub issues: Vec<String>,
117}
118
119/// Evaluator for manufacturing coherence.
120pub struct ManufacturingEvaluator {
121    thresholds: ManufacturingThresholds,
122}
123
124impl ManufacturingEvaluator {
125    /// Create a new evaluator with default thresholds.
126    pub fn new() -> Self {
127        Self {
128            thresholds: ManufacturingThresholds::default(),
129        }
130    }
131
132    /// Create with custom thresholds.
133    pub fn with_thresholds(thresholds: ManufacturingThresholds) -> Self {
134        Self { thresholds }
135    }
136
137    /// Evaluate manufacturing data.
138    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        // 1. Yield rate consistency: yield = actual / (actual + scrap)
148        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; // Skip zero-output orders
154                }
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        // 2. Cost variance
166        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        // 3. Operation sequencing: group by order, check ascending
178        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                // Check sequence numbers are ascending and timestamps non-decreasing
190                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        // 4. Quality inspection: defect_rate = defect_count / sample_size
203        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        // Characteristics compliance
220        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        // 5. Cycle count variance: variance = counted - book
232        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        // Check thresholds
246        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// ---------------------------------------------------------------------------
296// Manufacturing GL Proof (v2.5 — cross-domain coherence)
297// ---------------------------------------------------------------------------
298
299/// Data for validating that manufacturing GL postings match production data.
300///
301/// Compares: sum of WIP/FG/COGS JE postings vs. production order cost totals.
302#[derive(Debug, Clone)]
303pub struct ManufacturingGLProofData {
304    /// Total cost posted to WIP GL accounts (1420) across all JEs.
305    pub gl_wip_total: rust_decimal::Decimal,
306    /// Total cost posted to Finished Goods GL accounts (1410).
307    pub gl_fg_total: rust_decimal::Decimal,
308    /// Total COGS posted to GL (5000).
309    pub gl_cogs_total: rust_decimal::Decimal,
310    /// Total actual cost from production orders.
311    pub production_order_total_cost: rust_decimal::Decimal,
312    /// Number of production orders.
313    pub production_order_count: usize,
314    /// Number of JEs with manufacturing document types.
315    pub manufacturing_je_count: usize,
316}
317
318/// Results of manufacturing GL proof validation.
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct ManufacturingGLProofEvaluation {
321    /// Whether WIP→FG→COGS flow is coherent (cost flows through correctly).
322    pub flow_coherent: bool,
323    /// Difference between production order costs and GL postings.
324    pub cost_gl_difference: rust_decimal::Decimal,
325    /// Whether every production order has corresponding GL postings.
326    pub all_orders_posted: bool,
327    /// Issues found.
328    pub issues: Vec<String>,
329}
330
331/// Validates manufacturing cost flow through the GL.
332pub struct ManufacturingGLProofEvaluator {
333    tolerance: rust_decimal::Decimal,
334}
335
336impl ManufacturingGLProofEvaluator {
337    /// Create with custom tolerance.
338    pub fn new(tolerance: rust_decimal::Decimal) -> Self {
339        Self { tolerance }
340    }
341
342    /// Validate manufacturing GL proof.
343    pub fn evaluate(
344        &self,
345        data: &ManufacturingGLProofData,
346    ) -> EvalResult<ManufacturingGLProofEvaluation> {
347        let mut issues = Vec::new();
348
349        // The cost flow should be: Raw Material → WIP → FG → COGS
350        // Total production cost should approximately equal GL postings
351        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)) // $100 tolerance for rounding across many orders
384    }
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, // 90 / (90+10) = 0.9
400            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, // Wrong, should be 0.9
449            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, // Later timestamp but earlier sequence
465            },
466            RoutingOperationData {
467                order_id: "PO001".to_string(),
468                sequence_number: 20,
469                start_timestamp: 1000, // Earlier timestamp but later sequence
470            },
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}