datasynth_generators/manufacturing/
quality_inspection_generator.rs1use 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
16const 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
30const 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
35pub struct QualityInspectionGenerator {
38 rng: ChaCha8Rng,
39 uuid_factory: DeterministicUuidFactory,
40}
41
42impl QualityInspectionGenerator {
43 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 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 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 let inspection_type = self.pick_inspection_type();
91
92 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 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 let num_characteristics: usize = self.rng.gen_range(2..=5);
103 let characteristics = self.generate_characteristics(num_characteristics);
104
105 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 let result = self.pick_result();
115
116 let inspector_id = Some(format!("QC-{:02}", self.rng.gen_range(1..=20)));
118
119 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 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 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 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 fn generate_characteristics(&mut self, count: usize) -> Vec<InspectionCharacteristic> {
199 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 let target_value: f64 = self.rng.gen_range(10.0..=100.0);
211
212 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 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#[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 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 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 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}