Skip to main content

datasynth_generators/manufacturing/
quality_inspection_generator.rs

1//! Quality inspection generator for manufacturing processes.
2//!
3//! Generates realistic quality inspections linked to production orders,
4//! with multiple inspection characteristics, defect tracking, and
5//! disposition assignment.
6
7use chrono::NaiveDate;
8use datasynth_core::models::{
9    InspectionCharacteristic, InspectionResult, InspectionType, QualityInspection,
10};
11use datasynth_core::uuid_factory::{DeterministicUuidFactory, GeneratorType};
12use rand::prelude::*;
13use rand_chacha::ChaCha8Rng;
14use rust_decimal::Decimal;
15
16/// Characteristic names used in quality inspections.
17const CHARACTERISTIC_NAMES: &[&str] = &[
18    "Dimension A",
19    "Weight",
20    "Surface Finish",
21    "Tensile Strength",
22    "Hardness",
23    "Thickness",
24    "Diameter",
25    "Flatness",
26    "Concentricity",
27    "Color Consistency",
28];
29
30/// Disposition actions for inspection results.
31const DISPOSITIONS_ACCEPTED: &[&str] = &["use_as_is", "stock"];
32const DISPOSITIONS_CONDITIONAL: &[&str] = &["use_as_is", "rework", "downgrade"];
33const DISPOSITIONS_REJECTED: &[&str] = &["return_to_vendor", "scrap", "rework"];
34
35/// Generates [`QualityInspection`] instances linked to production orders
36/// with realistic inspection characteristics and defect rates.
37pub struct QualityInspectionGenerator {
38    rng: ChaCha8Rng,
39    uuid_factory: DeterministicUuidFactory,
40}
41
42impl QualityInspectionGenerator {
43    /// Create a new quality inspection generator with the given seed.
44    pub fn new(seed: u64) -> Self {
45        Self {
46            rng: ChaCha8Rng::seed_from_u64(seed),
47            uuid_factory: DeterministicUuidFactory::new(seed, GeneratorType::QualityInspection),
48        }
49    }
50
51    /// Generate quality inspections for a set of production orders.
52    ///
53    /// # Arguments
54    ///
55    /// * `company_code` - Company code for all generated inspections.
56    /// * `production_orders` - Tuples of `(order_id, material_id, material_description)`.
57    /// * `inspection_date` - Date for all generated inspections.
58    pub fn generate(
59        &mut self,
60        company_code: &str,
61        production_orders: &[(String, String, String)],
62        inspection_date: NaiveDate,
63    ) -> Vec<QualityInspection> {
64        production_orders
65            .iter()
66            .map(|(order_id, material_id, material_desc)| {
67                self.generate_one(
68                    company_code,
69                    order_id,
70                    material_id,
71                    material_desc,
72                    inspection_date,
73                )
74            })
75            .collect()
76    }
77
78    /// Generate a single quality inspection for a production order.
79    fn generate_one(
80        &mut self,
81        company_code: &str,
82        order_id: &str,
83        material_id: &str,
84        material_description: &str,
85        inspection_date: NaiveDate,
86    ) -> QualityInspection {
87        let inspection_id = self.uuid_factory.next().to_string();
88
89        // Inspection type distribution: 40% Final, 25% InProcess, 20% Incoming, 10% Random, 5% Periodic
90        let inspection_type = self.pick_inspection_type();
91
92        // Lot size based on typical production order quantity
93        let lot_size_f64: f64 = self.rng.gen_range(50.0..=1000.0);
94        let lot_size = Decimal::from_f64_retain(lot_size_f64.round()).unwrap_or(Decimal::from(100));
95
96        // Sample size: 10-30% of lot
97        let sample_pct: f64 = self.rng.gen_range(0.10..=0.30);
98        let sample_size_f64 = (lot_size_f64 * sample_pct).round().max(1.0);
99        let sample_size = Decimal::from_f64_retain(sample_size_f64).unwrap_or(Decimal::from(10));
100
101        // Generate 2-5 inspection characteristics
102        let num_characteristics: usize = self.rng.gen_range(2..=5);
103        let characteristics = self.generate_characteristics(num_characteristics);
104
105        // Count defects (failed characteristics)
106        let defect_count = characteristics.iter().filter(|c| !c.passed).count() as u32;
107        let defect_rate = if sample_size_f64 > 0.0 {
108            defect_count as f64 / sample_size_f64
109        } else {
110            0.0
111        };
112
113        // Inspection result: 80% Accepted, 10% Conditionally, 7% Rejected, 3% Pending
114        let result = self.pick_result();
115
116        // Inspector
117        let inspector_id = Some(format!("QC-{:02}", self.rng.gen_range(1..=20)));
118
119        // Disposition based on result
120        let disposition = match result {
121            InspectionResult::Accepted => DISPOSITIONS_ACCEPTED
122                .choose(&mut self.rng)
123                .map(|s| s.to_string()),
124            InspectionResult::Conditionally => DISPOSITIONS_CONDITIONAL
125                .choose(&mut self.rng)
126                .map(|s| s.to_string()),
127            InspectionResult::Rejected => DISPOSITIONS_REJECTED
128                .choose(&mut self.rng)
129                .map(|s| s.to_string()),
130            InspectionResult::Pending => None,
131        };
132
133        // Notes for non-accepted results
134        let notes = match result {
135            InspectionResult::Rejected => Some(format!(
136                "{} defects found in {} characteristics. Material held for disposition.",
137                defect_count, num_characteristics
138            )),
139            InspectionResult::Conditionally => Some(format!(
140                "Minor deviations noted. {} characteristic(s) marginally out of spec.",
141                defect_count
142            )),
143            _ => None,
144        };
145
146        QualityInspection {
147            inspection_id,
148            company_code: company_code.to_string(),
149            reference_type: "production_order".to_string(),
150            reference_id: order_id.to_string(),
151            material_id: material_id.to_string(),
152            material_description: material_description.to_string(),
153            inspection_type,
154            inspection_date,
155            inspector_id,
156            lot_size,
157            sample_size,
158            defect_count,
159            defect_rate,
160            result,
161            characteristics,
162            disposition,
163            notes,
164        }
165    }
166
167    /// Pick an inspection type based on distribution.
168    fn pick_inspection_type(&mut self) -> InspectionType {
169        let roll: f64 = self.rng.gen();
170        if roll < 0.40 {
171            InspectionType::Final
172        } else if roll < 0.65 {
173            InspectionType::InProcess
174        } else if roll < 0.85 {
175            InspectionType::Incoming
176        } else if roll < 0.95 {
177            InspectionType::Random
178        } else {
179            InspectionType::Periodic
180        }
181    }
182
183    /// Pick an inspection result based on distribution.
184    fn pick_result(&mut self) -> InspectionResult {
185        let roll: f64 = self.rng.gen();
186        if roll < 0.80 {
187            InspectionResult::Accepted
188        } else if roll < 0.90 {
189            InspectionResult::Conditionally
190        } else if roll < 0.97 {
191            InspectionResult::Rejected
192        } else {
193            InspectionResult::Pending
194        }
195    }
196
197    /// Generate inspection characteristics with target/actual values and limits.
198    fn generate_characteristics(&mut self, count: usize) -> Vec<InspectionCharacteristic> {
199        // Shuffle and pick `count` characteristic names
200        let mut indices: Vec<usize> = (0..CHARACTERISTIC_NAMES.len()).collect();
201        indices.shuffle(&mut self.rng);
202        let selected_count = count.min(indices.len());
203
204        indices[..selected_count]
205            .iter()
206            .map(|&idx| {
207                let name = CHARACTERISTIC_NAMES[idx].to_string();
208
209                // Target value: random 10.0 - 100.0
210                let target_value: f64 = self.rng.gen_range(10.0..=100.0);
211
212                // Limits: ± 5-15% of target
213                let tolerance_pct: f64 = self.rng.gen_range(0.05..=0.15);
214                let lower_limit = target_value * (1.0 - tolerance_pct);
215                let upper_limit = target_value * (1.0 + tolerance_pct);
216
217                // Actual value: target * random(0.95 - 1.05)
218                let actual_factor: f64 = self.rng.gen_range(0.95..=1.05);
219                let actual_value = target_value * actual_factor;
220
221                let passed = actual_value >= lower_limit && actual_value <= upper_limit;
222
223                InspectionCharacteristic {
224                    name,
225                    target_value,
226                    actual_value,
227                    lower_limit,
228                    upper_limit,
229                    passed,
230                }
231            })
232            .collect()
233    }
234}
235
236// ---------------------------------------------------------------------------
237// Tests
238// ---------------------------------------------------------------------------
239
240#[cfg(test)]
241#[allow(clippy::unwrap_used)]
242mod tests {
243    use super::*;
244
245    fn sample_orders() -> Vec<(String, String, String)> {
246        vec![
247            (
248                "PO-001".to_string(),
249                "MAT-001".to_string(),
250                "Widget Alpha".to_string(),
251            ),
252            (
253                "PO-002".to_string(),
254                "MAT-002".to_string(),
255                "Widget Beta".to_string(),
256            ),
257            (
258                "PO-003".to_string(),
259                "MAT-003".to_string(),
260                "Widget Gamma".to_string(),
261            ),
262        ]
263    }
264
265    #[test]
266    fn test_basic_generation() {
267        let mut gen = QualityInspectionGenerator::new(42);
268        let orders = sample_orders();
269        let date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
270
271        let inspections = gen.generate("C001", &orders, date);
272
273        assert_eq!(inspections.len(), orders.len());
274        for insp in &inspections {
275            assert_eq!(insp.company_code, "C001");
276            assert_eq!(insp.inspection_date, date);
277            assert!(!insp.inspection_id.is_empty());
278            assert_eq!(insp.reference_type, "production_order");
279            assert!(insp.lot_size > Decimal::ZERO);
280            assert!(insp.sample_size > Decimal::ZERO);
281            assert!(insp.sample_size <= insp.lot_size);
282            assert!(!insp.characteristics.is_empty());
283            assert!(insp.characteristics.len() >= 2 && insp.characteristics.len() <= 5);
284            assert!(insp.inspector_id.is_some());
285        }
286    }
287
288    #[test]
289    fn test_deterministic() {
290        let orders = sample_orders();
291        let date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
292
293        let mut gen1 = QualityInspectionGenerator::new(12345);
294        let insp1 = gen1.generate("C001", &orders, date);
295
296        let mut gen2 = QualityInspectionGenerator::new(12345);
297        let insp2 = gen2.generate("C001", &orders, date);
298
299        assert_eq!(insp1.len(), insp2.len());
300        for (i1, i2) in insp1.iter().zip(insp2.iter()) {
301            assert_eq!(i1.inspection_id, i2.inspection_id);
302            assert_eq!(i1.lot_size, i2.lot_size);
303            assert_eq!(i1.sample_size, i2.sample_size);
304            assert_eq!(i1.defect_count, i2.defect_count);
305            assert_eq!(i1.characteristics.len(), i2.characteristics.len());
306        }
307    }
308
309    #[test]
310    fn test_characteristics_limits() {
311        let mut gen = QualityInspectionGenerator::new(99);
312        let orders = sample_orders();
313        let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
314
315        let inspections = gen.generate("C001", &orders, date);
316
317        for insp in &inspections {
318            for char in &insp.characteristics {
319                // Lower limit must be below target
320                assert!(
321                    char.lower_limit < char.target_value,
322                    "Lower limit {} should be below target {}",
323                    char.lower_limit,
324                    char.target_value,
325                );
326                // Upper limit must be above target
327                assert!(
328                    char.upper_limit > char.target_value,
329                    "Upper limit {} should be above target {}",
330                    char.upper_limit,
331                    char.target_value,
332                );
333                // Passed should be consistent with limits
334                let within_limits =
335                    char.actual_value >= char.lower_limit && char.actual_value <= char.upper_limit;
336                assert_eq!(
337                    char.passed, within_limits,
338                    "Passed flag ({}) inconsistent with limits for {}",
339                    char.passed, char.name,
340                );
341            }
342        }
343    }
344}