1use crate::types::{EventLog, PartialOrderResult, Trace};
10use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
11use std::collections::{HashMap, HashSet};
12
13#[derive(Debug, Clone)]
22pub struct PartialOrderAnalysis {
23 metadata: KernelMetadata,
24}
25
26impl Default for PartialOrderAnalysis {
27 fn default() -> Self {
28 Self::new()
29 }
30}
31
32impl PartialOrderAnalysis {
33 #[must_use]
35 pub fn new() -> Self {
36 Self {
37 metadata: KernelMetadata::batch("procint/partial-order", Domain::ProcessIntelligence)
38 .with_description("Partial order and concurrency analysis")
39 .with_throughput(50_000)
40 .with_latency_us(100.0),
41 }
42 }
43
44 pub fn analyze(log: &EventLog, config: &PartialOrderConfig) -> PartialOrderResult {
46 let activities: Vec<String> = log
47 .activities()
48 .into_iter()
49 .map(|s| s.to_string())
50 .collect();
51
52 if activities.is_empty() {
53 return PartialOrderResult {
54 concurrent_pairs: Vec::new(),
55 sequential_pairs: Vec::new(),
56 exclusive_pairs: Vec::new(),
57 parallelism_score: 0.0,
58 };
59 }
60
61 let mut cooccurrence: HashMap<(String, String), u64> = HashMap::new();
63 let mut before_count: HashMap<(String, String), u64> = HashMap::new();
64 let mut after_count: HashMap<(String, String), u64> = HashMap::new();
65
66 for trace in log.traces.values() {
67 let mut sorted_events: Vec<_> = trace.events.iter().collect();
68 sorted_events.sort_by_key(|e| e.timestamp);
69
70 let trace_activities: HashSet<_> =
72 sorted_events.iter().map(|e| e.activity.clone()).collect();
73
74 for a1 in &trace_activities {
76 for a2 in &trace_activities {
77 if a1 != a2 {
78 let key = (a1.clone(), a2.clone());
79 *cooccurrence.entry(key).or_insert(0) += 1;
80 }
81 }
82 }
83
84 for i in 0..sorted_events.len() {
86 for j in (i + 1)..sorted_events.len() {
87 let a1 = &sorted_events[i].activity;
88 let a2 = &sorted_events[j].activity;
89
90 if a1 != a2 {
91 *before_count.entry((a1.clone(), a2.clone())).or_insert(0) += 1;
92 *after_count.entry((a2.clone(), a1.clone())).or_insert(0) += 1;
93 }
94 }
95 }
96 }
97
98 let trace_count = log.trace_count() as u64;
99 let mut concurrent_pairs = Vec::new();
100 let mut sequential_pairs = Vec::new();
101 let mut exclusive_pairs = Vec::new();
102
103 for i in 0..activities.len() {
105 for j in (i + 1)..activities.len() {
106 let a1 = &activities[i];
107 let a2 = &activities[j];
108
109 let co = cooccurrence
110 .get(&(a1.clone(), a2.clone()))
111 .copied()
112 .unwrap_or(0);
113 let ab = before_count
114 .get(&(a1.clone(), a2.clone()))
115 .copied()
116 .unwrap_or(0);
117 let ba = before_count
118 .get(&(a2.clone(), a1.clone()))
119 .copied()
120 .unwrap_or(0);
121
122 if co == 0 && trace_count > 0 {
124 exclusive_pairs.push((a1.clone(), a2.clone()));
125 continue;
126 }
127
128 let seq_threshold = (config.sequence_threshold * co as f64) as u64;
130
131 if ab >= seq_threshold && ba == 0 {
132 sequential_pairs.push((a1.clone(), a2.clone()));
133 } else if ba >= seq_threshold && ab == 0 {
134 sequential_pairs.push((a2.clone(), a1.clone()));
135 } else if ab > 0 && ba > 0 {
136 let concurrent_ratio = (ab.min(ba) as f64) / (ab.max(ba) as f64);
138 if concurrent_ratio >= config.concurrency_threshold {
139 concurrent_pairs.push((a1.clone(), a2.clone()));
140 }
141 }
142 }
143 }
144
145 let total_pairs = activities.len() * (activities.len() - 1) / 2;
147 let parallelism_score = if total_pairs > 0 {
148 concurrent_pairs.len() as f64 / total_pairs as f64
149 } else {
150 0.0
151 };
152
153 PartialOrderResult {
154 concurrent_pairs,
155 sequential_pairs,
156 exclusive_pairs,
157 parallelism_score,
158 }
159 }
160
161 pub fn analyze_trace(trace: &Trace) -> TracePartialOrder {
163 let mut sorted_events: Vec<_> = trace.events.iter().collect();
164 sorted_events.sort_by_key(|e| e.timestamp);
165
166 let mut ordering_graph: HashMap<String, HashSet<String>> = HashMap::new();
167 let mut concurrent_with: HashMap<String, HashSet<String>> = HashMap::new();
168
169 for i in 0..sorted_events.len() {
171 for j in (i + 1)..sorted_events.len() {
172 let a1 = &sorted_events[i].activity;
173 let a2 = &sorted_events[j].activity;
174
175 if a1 != a2 {
176 let time_diff = sorted_events[j]
178 .timestamp
179 .saturating_sub(sorted_events[i].timestamp);
180
181 if time_diff == 0 {
182 concurrent_with
184 .entry(a1.clone())
185 .or_default()
186 .insert(a2.clone());
187 concurrent_with
188 .entry(a2.clone())
189 .or_default()
190 .insert(a1.clone());
191 } else {
192 ordering_graph
194 .entry(a1.clone())
195 .or_default()
196 .insert(a2.clone());
197 }
198 }
199 }
200 }
201
202 TracePartialOrder {
203 ordering_graph,
204 concurrent_with,
205 }
206 }
207
208 pub fn detect_loops(log: &EventLog) -> Vec<LoopPattern> {
210 let mut loop_patterns: HashMap<Vec<String>, u64> = HashMap::new();
211
212 for trace in log.traces.values() {
213 let mut sorted_events: Vec<_> = trace.events.iter().collect();
214 sorted_events.sort_by_key(|e| e.timestamp);
215
216 let activities: Vec<String> =
217 sorted_events.iter().map(|e| e.activity.clone()).collect();
218
219 for window_size in 2..=activities.len().min(5) {
221 for start in 0..activities.len().saturating_sub(window_size * 2 - 1) {
222 let pattern: Vec<String> = activities[start..start + window_size].to_vec();
223
224 let next_start = start + window_size;
226 if next_start + window_size <= activities.len() {
227 let next_pattern: Vec<String> =
228 activities[next_start..next_start + window_size].to_vec();
229 if pattern == next_pattern {
230 *loop_patterns.entry(pattern).or_insert(0) += 1;
231 }
232 }
233 }
234 }
235 }
236
237 loop_patterns
238 .into_iter()
239 .filter(|(_, count)| *count >= 2)
240 .map(|(activities, count)| LoopPattern {
241 activities,
242 occurrence_count: count,
243 })
244 .collect()
245 }
246
247 pub fn calculate_independence(log: &EventLog) -> HashMap<(String, String), f64> {
249 let activities: Vec<String> = log
250 .activities()
251 .into_iter()
252 .map(|s| s.to_string())
253 .collect();
254 let mut independence_scores: HashMap<(String, String), f64> = HashMap::new();
255
256 if activities.is_empty() || log.trace_count() == 0 {
257 return independence_scores;
258 }
259
260 let mut cooccurrence_count: HashMap<(String, String), u64> = HashMap::new();
262 let mut activity_count: HashMap<String, u64> = HashMap::new();
263
264 for trace in log.traces.values() {
265 let trace_activities: HashSet<_> =
266 trace.events.iter().map(|e| e.activity.clone()).collect();
267
268 for activity in &trace_activities {
269 *activity_count.entry(activity.clone()).or_insert(0) += 1;
270 }
271
272 for a1 in &trace_activities {
273 for a2 in &trace_activities {
274 if a1 < a2 {
275 *cooccurrence_count
276 .entry((a1.clone(), a2.clone()))
277 .or_insert(0) += 1;
278 }
279 }
280 }
281 }
282
283 let trace_count = log.trace_count() as f64;
284
285 for i in 0..activities.len() {
287 for j in (i + 1)..activities.len() {
288 let a1 = &activities[i];
289 let a2 = &activities[j];
290
291 let key = if a1 < a2 {
292 (a1.clone(), a2.clone())
293 } else {
294 (a2.clone(), a1.clone())
295 };
296
297 let p_a1 = activity_count.get(a1).copied().unwrap_or(0) as f64 / trace_count;
298 let p_a2 = activity_count.get(a2).copied().unwrap_or(0) as f64 / trace_count;
299 let p_joint =
300 cooccurrence_count.get(&key).copied().unwrap_or(0) as f64 / trace_count;
301
302 let expected = p_a1 * p_a2;
304 let independence = if expected > 0.0 && p_joint > 0.0 {
305 1.0 - (p_joint / expected).min(1.0)
306 } else if p_joint == 0.0 {
307 1.0 } else {
309 0.0
310 };
311
312 independence_scores.insert((a1.clone(), a2.clone()), independence);
313 }
314 }
315
316 independence_scores
317 }
318}
319
320impl GpuKernel for PartialOrderAnalysis {
321 fn metadata(&self) -> &KernelMetadata {
322 &self.metadata
323 }
324}
325
326#[derive(Debug, Clone)]
328pub struct PartialOrderConfig {
329 pub concurrency_threshold: f64,
332 pub sequence_threshold: f64,
335}
336
337impl Default for PartialOrderConfig {
338 fn default() -> Self {
339 Self {
340 concurrency_threshold: 0.5,
341 sequence_threshold: 0.8,
342 }
343 }
344}
345
346#[derive(Debug, Clone)]
348pub struct TracePartialOrder {
349 pub ordering_graph: HashMap<String, HashSet<String>>,
351 pub concurrent_with: HashMap<String, HashSet<String>>,
353}
354
355#[derive(Debug, Clone)]
357pub struct LoopPattern {
358 pub activities: Vec<String>,
360 pub occurrence_count: u64,
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use crate::types::ProcessEvent;
368
369 fn create_test_log() -> EventLog {
370 let mut log = EventLog::new("test_log".to_string());
371
372 for (i, activity) in ["A", "B", "C", "D"].iter().enumerate() {
374 log.add_event(ProcessEvent {
375 id: i as u64,
376 case_id: "case1".to_string(),
377 activity: activity.to_string(),
378 timestamp: (i as u64 + 1) * 1000,
379 resource: None,
380 attributes: HashMap::new(),
381 });
382 }
383
384 for (i, activity) in ["A", "B", "C", "D"].iter().enumerate() {
386 log.add_event(ProcessEvent {
387 id: (i + 10) as u64,
388 case_id: "case2".to_string(),
389 activity: activity.to_string(),
390 timestamp: (i as u64 + 1) * 1000,
391 resource: None,
392 attributes: HashMap::new(),
393 });
394 }
395
396 for (i, activity) in ["A", "C", "B", "D"].iter().enumerate() {
398 log.add_event(ProcessEvent {
399 id: (i + 20) as u64,
400 case_id: "case3".to_string(),
401 activity: activity.to_string(),
402 timestamp: (i as u64 + 1) * 1000,
403 resource: None,
404 attributes: HashMap::new(),
405 });
406 }
407
408 log
409 }
410
411 fn create_exclusive_log() -> EventLog {
412 let mut log = EventLog::new("exclusive_log".to_string());
413
414 for (i, activity) in ["A", "B", "C"].iter().enumerate() {
416 log.add_event(ProcessEvent {
417 id: i as u64,
418 case_id: "case1".to_string(),
419 activity: activity.to_string(),
420 timestamp: (i as u64 + 1) * 1000,
421 resource: None,
422 attributes: HashMap::new(),
423 });
424 }
425
426 for (i, activity) in ["A", "D", "E"].iter().enumerate() {
428 log.add_event(ProcessEvent {
429 id: (i + 10) as u64,
430 case_id: "case2".to_string(),
431 activity: activity.to_string(),
432 timestamp: (i as u64 + 1) * 1000,
433 resource: None,
434 attributes: HashMap::new(),
435 });
436 }
437
438 log
439 }
440
441 #[test]
442 fn test_partial_order_metadata() {
443 let kernel = PartialOrderAnalysis::new();
444 assert_eq!(kernel.metadata().id, "procint/partial-order");
445 assert_eq!(kernel.metadata().domain, Domain::ProcessIntelligence);
446 }
447
448 #[test]
449 fn test_concurrent_detection() {
450 let log = create_test_log();
451 let config = PartialOrderConfig::default();
452 let result = PartialOrderAnalysis::analyze(&log, &config);
453
454 let bc_concurrent = result
456 .concurrent_pairs
457 .iter()
458 .any(|(a, b)| (a == "B" && b == "C") || (a == "C" && b == "B"));
459 assert!(bc_concurrent, "B and C should be concurrent");
460 }
461
462 #[test]
463 fn test_sequential_detection() {
464 let log = create_test_log();
465 let config = PartialOrderConfig::default();
466 let result = PartialOrderAnalysis::analyze(&log, &config);
467
468 let a_before_d = result
470 .sequential_pairs
471 .iter()
472 .any(|(a, b)| a == "A" && b == "D");
473 assert!(a_before_d, "A should be sequential before D");
474 }
475
476 #[test]
477 fn test_exclusive_detection() {
478 let log = create_exclusive_log();
479 let config = PartialOrderConfig::default();
480 let result = PartialOrderAnalysis::analyze(&log, &config);
481
482 let bd_exclusive = result
484 .exclusive_pairs
485 .iter()
486 .any(|(a, b)| (a == "B" && b == "D") || (a == "D" && b == "B"));
487 assert!(bd_exclusive, "B and D should be exclusive");
488 }
489
490 #[test]
491 fn test_parallelism_score() {
492 let log = create_test_log();
493 let config = PartialOrderConfig::default();
494 let result = PartialOrderAnalysis::analyze(&log, &config);
495
496 assert!(result.parallelism_score >= 0.0 && result.parallelism_score <= 1.0);
498 }
499
500 #[test]
501 fn test_empty_log() {
502 let log = EventLog::new("empty".to_string());
503 let config = PartialOrderConfig::default();
504 let result = PartialOrderAnalysis::analyze(&log, &config);
505
506 assert!(result.concurrent_pairs.is_empty());
507 assert!(result.sequential_pairs.is_empty());
508 assert!(result.exclusive_pairs.is_empty());
509 assert_eq!(result.parallelism_score, 0.0);
510 }
511
512 #[test]
513 fn test_trace_partial_order() {
514 let trace = crate::types::Trace {
515 case_id: "test".to_string(),
516 events: vec![
517 ProcessEvent {
518 id: 1,
519 case_id: "test".to_string(),
520 activity: "A".to_string(),
521 timestamp: 1000,
522 resource: None,
523 attributes: HashMap::new(),
524 },
525 ProcessEvent {
526 id: 2,
527 case_id: "test".to_string(),
528 activity: "B".to_string(),
529 timestamp: 2000,
530 resource: None,
531 attributes: HashMap::new(),
532 },
533 ProcessEvent {
534 id: 3,
535 case_id: "test".to_string(),
536 activity: "C".to_string(),
537 timestamp: 3000,
538 resource: None,
539 attributes: HashMap::new(),
540 },
541 ],
542 attributes: HashMap::new(),
543 };
544
545 let result = PartialOrderAnalysis::analyze_trace(&trace);
546
547 assert!(
549 result
550 .ordering_graph
551 .get("A")
552 .is_some_and(|s| s.contains("B"))
553 );
554 assert!(
555 result
556 .ordering_graph
557 .get("A")
558 .is_some_and(|s| s.contains("C"))
559 );
560 }
561
562 #[test]
563 fn test_loop_detection() {
564 let mut log = EventLog::new("loop_log".to_string());
565
566 for (i, activity) in ["A", "B", "C", "B", "C", "D"].iter().enumerate() {
568 log.add_event(ProcessEvent {
569 id: i as u64,
570 case_id: "case1".to_string(),
571 activity: activity.to_string(),
572 timestamp: (i as u64 + 1) * 1000,
573 resource: None,
574 attributes: HashMap::new(),
575 });
576 }
577
578 for (i, activity) in ["A", "B", "C", "B", "C", "D"].iter().enumerate() {
580 log.add_event(ProcessEvent {
581 id: (i + 10) as u64,
582 case_id: "case2".to_string(),
583 activity: activity.to_string(),
584 timestamp: (i as u64 + 1) * 1000,
585 resource: None,
586 attributes: HashMap::new(),
587 });
588 }
589
590 let loops = PartialOrderAnalysis::detect_loops(&log);
591
592 let bc_loop = loops.iter().any(|l| l.activities == vec!["B", "C"]);
594 assert!(bc_loop, "Should detect B -> C loop pattern");
595 }
596
597 #[test]
598 fn test_independence_calculation() {
599 let log = create_exclusive_log();
600 let independence = PartialOrderAnalysis::calculate_independence(&log);
601
602 let bd_key = ("B".to_string(), "D".to_string());
604 if let Some(&score) = independence.get(&bd_key) {
605 assert_eq!(score, 1.0, "B and D should be fully independent");
606 }
607 }
608
609 #[test]
610 fn test_config_thresholds() {
611 let log = create_test_log();
612
613 let strict_config = PartialOrderConfig {
615 concurrency_threshold: 0.9,
616 sequence_threshold: 0.95,
617 };
618 let strict_result = PartialOrderAnalysis::analyze(&log, &strict_config);
619
620 let loose_config = PartialOrderConfig {
622 concurrency_threshold: 0.3,
623 sequence_threshold: 0.5,
624 };
625 let loose_result = PartialOrderAnalysis::analyze(&log, &loose_config);
626
627 assert!(loose_result.concurrent_pairs.len() >= strict_result.concurrent_pairs.len());
629 }
630}