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)]
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 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 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 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}