datasynth_ocpm/generator/
event_generator.rs1use chrono::{DateTime, Duration, Utc};
6use rand::{Rng, SeedableRng};
7use rand_chacha::ChaCha8Rng;
8use uuid::Uuid;
9
10use crate::models::{
11 ActivityType, CaseTrace, EventLifecycle, EventObjectRef, ObjectAttributeValue, ObjectInstance,
12 ObjectQualifier, ObjectRelationship, ObjectType, OcpmEvent,
13};
14use datasynth_core::models::BusinessProcess;
15
16#[derive(Debug, Clone)]
18pub struct OcpmGeneratorConfig {
19 pub generate_p2p: bool,
21 pub generate_o2c: bool,
23 pub happy_path_rate: f64,
25 pub exception_path_rate: f64,
27 pub error_path_rate: f64,
29 pub add_duration_variability: bool,
31 pub duration_std_dev_factor: f64,
33}
34
35impl Default for OcpmGeneratorConfig {
36 fn default() -> Self {
37 Self {
38 generate_p2p: true,
39 generate_o2c: true,
40 happy_path_rate: 0.75,
41 exception_path_rate: 0.20,
42 error_path_rate: 0.05,
43 add_duration_variability: true,
44 duration_std_dev_factor: 0.3,
45 }
46 }
47}
48
49pub struct OcpmEventGenerator {
51 rng: ChaCha8Rng,
53 config: OcpmGeneratorConfig,
55 p2p_activities: Vec<ActivityType>,
57 o2c_activities: Vec<ActivityType>,
59 case_counter: u64,
61}
62
63impl OcpmEventGenerator {
64 pub fn new(seed: u64) -> Self {
66 Self {
67 rng: ChaCha8Rng::seed_from_u64(seed),
68 config: OcpmGeneratorConfig::default(),
69 p2p_activities: ActivityType::p2p_activities(),
70 o2c_activities: ActivityType::o2c_activities(),
71 case_counter: 0,
72 }
73 }
74
75 pub fn with_config(seed: u64, config: OcpmGeneratorConfig) -> Self {
77 Self {
78 rng: ChaCha8Rng::seed_from_u64(seed),
79 config,
80 p2p_activities: ActivityType::p2p_activities(),
81 o2c_activities: ActivityType::o2c_activities(),
82 case_counter: 0,
83 }
84 }
85
86 pub fn new_case_id(&mut self) -> Uuid {
88 self.case_counter += 1;
89 Uuid::new_v4()
90 }
91
92 pub fn select_variant_type(&mut self) -> VariantType {
94 let r: f64 = self.rng.gen();
95 if r < self.config.happy_path_rate {
96 VariantType::HappyPath
97 } else if r < self.config.happy_path_rate + self.config.exception_path_rate {
98 VariantType::ExceptionPath
99 } else {
100 VariantType::ErrorPath
101 }
102 }
103
104 pub fn calculate_event_time(
106 &mut self,
107 base_time: DateTime<Utc>,
108 activity: &ActivityType,
109 ) -> DateTime<Utc> {
110 if let Some(typical_minutes) = activity.typical_duration_minutes {
111 let std_dev = activity.duration_std_dev.unwrap_or(typical_minutes * 0.3);
112
113 if self.config.add_duration_variability {
114 let variability: f64 = self.rng.gen_range(-2.0..2.0) * std_dev;
116 let actual_minutes = (typical_minutes + variability).max(1.0);
117 base_time + Duration::minutes(actual_minutes as i64)
118 } else {
119 base_time + Duration::minutes(typical_minutes as i64)
120 }
121 } else {
122 base_time + Duration::minutes(5) }
124 }
125
126 pub fn create_event(
128 &mut self,
129 activity: &ActivityType,
130 timestamp: DateTime<Utc>,
131 resource_id: &str,
132 company_code: &str,
133 case_id: Uuid,
134 ) -> OcpmEvent {
135 OcpmEvent::new(
136 &activity.activity_id,
137 &activity.name,
138 timestamp,
139 resource_id,
140 company_code,
141 )
142 .with_case(case_id)
143 .with_lifecycle(if activity.is_automated {
144 EventLifecycle::Atomic
145 } else {
146 EventLifecycle::Complete
147 })
148 }
149
150 pub fn create_object(
152 &self,
153 object_type: &ObjectType,
154 external_id: &str,
155 company_code: &str,
156 created_at: DateTime<Utc>,
157 ) -> ObjectInstance {
158 ObjectInstance::new(&object_type.type_id, external_id, company_code)
159 .with_state("active")
160 .with_created_at(created_at)
161 }
162
163 pub fn create_object_ref(
165 &self,
166 object: &ObjectInstance,
167 qualifier: ObjectQualifier,
168 ) -> EventObjectRef {
169 EventObjectRef::new(object.object_id, &object.object_type_id, qualifier)
170 .with_external_id(&object.external_id)
171 }
172
173 pub fn add_event_attribute(event: &mut OcpmEvent, key: &str, value: ObjectAttributeValue) {
175 event.attributes.insert(key.into(), value);
176 }
177
178 pub fn create_case_trace(
180 &self,
181 _case_id: Uuid,
182 events: &[OcpmEvent],
183 business_process: BusinessProcess,
184 primary_object_id: Uuid,
185 primary_object_type: &str,
186 company_code: &str,
187 ) -> CaseTrace {
188 let activity_sequence: Vec<String> = events.iter().map(|e| e.activity_id.clone()).collect();
189
190 let start_time = events.first().map(|e| e.timestamp).unwrap_or_else(Utc::now);
191 let end_time = events.last().map(|e| e.timestamp);
192
193 let mut trace = CaseTrace::new(
194 business_process,
195 primary_object_id,
196 primary_object_type,
197 company_code,
198 );
199 trace.activity_sequence = activity_sequence;
200 trace.event_ids = events.iter().map(|e| e.event_id).collect();
201 trace.start_time = start_time;
202 trace.end_time = end_time;
203 trace
204 }
205
206 pub fn select_resource(
208 &mut self,
209 activity: &ActivityType,
210 available_users: &[String],
211 ) -> String {
212 if activity.is_automated {
213 "SYSTEM".into()
214 } else if available_users.is_empty() {
215 format!("USER{:04}", self.rng.gen_range(1..100))
216 } else {
217 let idx = self.rng.gen_range(0..available_users.len());
218 available_users[idx].clone()
219 }
220 }
221
222 pub fn p2p_activities(&self) -> &[ActivityType] {
224 &self.p2p_activities
225 }
226
227 pub fn o2c_activities(&self) -> &[ActivityType] {
229 &self.o2c_activities
230 }
231
232 pub fn generate_inter_activity_delay(
234 &mut self,
235 min_minutes: i64,
236 max_minutes: i64,
237 ) -> Duration {
238 let minutes = self.rng.gen_range(min_minutes..=max_minutes);
239 Duration::minutes(minutes)
240 }
241
242 pub fn should_skip_activity(&mut self, skip_probability: f64) -> bool {
244 self.rng.gen::<f64>() < skip_probability
245 }
246
247 pub fn random_bool(&mut self, probability: f64) -> bool {
249 self.rng.gen::<f64>() < probability
250 }
251}
252
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum VariantType {
256 HappyPath,
258 ExceptionPath,
260 ErrorPath,
262}
263
264impl VariantType {
265 pub fn description(&self) -> &'static str {
267 match self {
268 Self::HappyPath => "Standard process execution",
269 Self::ExceptionPath => "Process with exceptions or variations",
270 Self::ErrorPath => "Process failed or aborted",
271 }
272 }
273}
274
275#[derive(Debug)]
277pub struct CaseGenerationResult {
278 pub events: Vec<OcpmEvent>,
280 pub objects: Vec<ObjectInstance>,
282 pub relationships: Vec<ObjectRelationship>,
284 pub case_trace: CaseTrace,
286 pub variant_type: VariantType,
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn test_generator_creation() {
296 let generator = OcpmEventGenerator::new(42);
297 assert!(!generator.p2p_activities.is_empty());
298 assert!(!generator.o2c_activities.is_empty());
299 }
300
301 #[test]
302 fn test_variant_selection() {
303 let mut generator = OcpmEventGenerator::new(42);
304
305 let mut happy = 0;
307 let mut exception = 0;
308 let mut error = 0;
309
310 for _ in 0..1000 {
311 match generator.select_variant_type() {
312 VariantType::HappyPath => happy += 1,
313 VariantType::ExceptionPath => exception += 1,
314 VariantType::ErrorPath => error += 1,
315 }
316 }
317
318 assert!(happy > 600 && happy < 850);
320 assert!(exception > 100 && exception < 300);
321 assert!(error > 10 && error < 100);
322 }
323
324 #[test]
325 fn test_case_id_generation() {
326 let mut generator = OcpmEventGenerator::new(42);
327 let id1 = generator.new_case_id();
328 let id2 = generator.new_case_id();
329 assert_ne!(id1, id2);
330 }
331
332 #[test]
333 fn test_event_creation() {
334 let mut generator = OcpmEventGenerator::new(42);
335 let activity = ActivityType::create_po();
336 let case_id = generator.new_case_id();
337
338 let event = generator.create_event(&activity, Utc::now(), "user001", "1000", case_id);
339
340 assert_eq!(event.activity_id, "create_po");
341 assert_eq!(event.case_id, Some(case_id));
342 }
343}