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