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