datasynth_generators/manufacturing/
quality_inspection_generator.rs1use 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
19const 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
33const 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
38pub struct QualityInspectionGenerator {
41 rng: ChaCha8Rng,
42 uuid_factory: DeterministicUuidFactory,
43}
44
45impl QualityInspectionGenerator {
46 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 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 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 let inspection_type = self.pick_inspection_type();
95
96 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 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 let num_characteristics: usize = self.rng.random_range(2..=5);
107 let characteristics = self.generate_characteristics(num_characteristics);
108
109 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 let result = self.pick_result();
119
120 let inspector_id = Some(format!("QC-{:02}", self.rng.random_range(1..=20)));
122
123 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 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 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 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 fn generate_characteristics(
201 &mut self,
202 count: usize,
203 ) -> SmallVec<[InspectionCharacteristic; 4]> {
204 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 let target_value: f64 = self.rng.random_range(10.0..=100.0);
216
217 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 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#[cfg(test)]
246#[allow(clippy::unwrap_used)]
247mod tests {
248 use super::*;
249
250 fn sample_orders() -> Vec<(String, String, String)> {
251 vec![
252 (
253 "PO-001".to_string(),
254 "MAT-001".to_string(),
255 "Widget Alpha".to_string(),
256 ),
257 (
258 "PO-002".to_string(),
259 "MAT-002".to_string(),
260 "Widget Beta".to_string(),
261 ),
262 (
263 "PO-003".to_string(),
264 "MAT-003".to_string(),
265 "Widget Gamma".to_string(),
266 ),
267 ]
268 }
269
270 #[test]
271 fn test_basic_generation() {
272 let mut gen = QualityInspectionGenerator::new(42);
273 let orders = sample_orders();
274 let date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
275
276 let inspections = gen.generate("C001", &orders, date);
277
278 assert_eq!(inspections.len(), orders.len());
279 for insp in &inspections {
280 assert_eq!(insp.company_code, "C001");
281 assert_eq!(insp.inspection_date, date);
282 assert!(!insp.inspection_id.is_empty());
283 assert_eq!(insp.reference_type, "production_order");
284 assert!(insp.lot_size > Decimal::ZERO);
285 assert!(insp.sample_size > Decimal::ZERO);
286 assert!(insp.sample_size <= insp.lot_size);
287 assert!(!insp.characteristics.is_empty());
288 assert!(insp.characteristics.len() >= 2 && insp.characteristics.len() <= 5);
289 assert!(insp.inspector_id.is_some());
290 }
291 }
292
293 #[test]
294 fn test_deterministic() {
295 let orders = sample_orders();
296 let date = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap();
297
298 let mut gen1 = QualityInspectionGenerator::new(12345);
299 let insp1 = gen1.generate("C001", &orders, date);
300
301 let mut gen2 = QualityInspectionGenerator::new(12345);
302 let insp2 = gen2.generate("C001", &orders, date);
303
304 assert_eq!(insp1.len(), insp2.len());
305 for (i1, i2) in insp1.iter().zip(insp2.iter()) {
306 assert_eq!(i1.inspection_id, i2.inspection_id);
307 assert_eq!(i1.lot_size, i2.lot_size);
308 assert_eq!(i1.sample_size, i2.sample_size);
309 assert_eq!(i1.defect_count, i2.defect_count);
310 assert_eq!(i1.characteristics.len(), i2.characteristics.len());
311 }
312 }
313
314 #[test]
315 fn test_characteristics_limits() {
316 let mut gen = QualityInspectionGenerator::new(99);
317 let orders = sample_orders();
318 let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();
319
320 let inspections = gen.generate("C001", &orders, date);
321
322 for insp in &inspections {
323 for char in &insp.characteristics {
324 assert!(
326 char.lower_limit < char.target_value,
327 "Lower limit {} should be below target {}",
328 char.lower_limit,
329 char.target_value,
330 );
331 assert!(
333 char.upper_limit > char.target_value,
334 "Upper limit {} should be above target {}",
335 char.upper_limit,
336 char.target_value,
337 );
338 let within_limits =
340 char.actual_value >= char.lower_limit && char.actual_value <= char.upper_limit;
341 assert_eq!(
342 char.passed, within_limits,
343 "Passed flag ({}) inconsistent with limits for {}",
344 char.passed, char.name,
345 );
346 }
347 }
348 }
349}