1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
//! Pattern Analysis for YAWL Pattern Dispatch
//!
//! This module analyzes trace structure to dynamically select the most appropriate
//! YAWL workflow pattern for process mining operations. It quantitatively evaluates
//! trace characteristics (concurrency, loops, choice points) to dispatch to the
//! optimal pattern handler.
use crate::pattern_dispatch::PatternType;
use std::collections::{HashMap, HashSet};
/// Trace structure analysis results
#[derive(Debug, Clone)]
pub struct TraceStructureAnalysis {
/// Primary pattern type detected
#[cfg(feature = "discovery_advanced")]
pub primary_pattern: PatternType,
/// Confidence score [0, 1] for pattern selection
pub confidence: f64,
/// Quantitative characteristics of the trace structure
pub characteristics: StructureCharacteristics,
}
/// Quantitative trace characteristics
///
/// These metrics are computed from raw traces and activity frequencies
/// to enable data-driven pattern selection.
#[derive(Debug, Clone)]
pub struct StructureCharacteristics {
// Basic metrics
/// Average number of events per trace
pub avg_trace_length: f64,
/// Number of unique activities across all traces
pub unique_activity_count: usize,
// Parallelism indicators
/// Maximum number of distinct outgoing edges from any activity
pub max_out_degree: usize,
/// Concurrency score [0, 1] — higher means more parallel execution paths
pub concurrency_score: f64,
// Loop indicators
/// Rework score [0, 1] — higher means more repetitive activity
pub rework_score: f64,
/// Whether any trace contains repeated activities
pub has_repetitions: bool,
// Choice indicators
/// Number of branch points (activities with multiple outgoing edges)
pub branch_points: usize,
/// Choice score [0, 1] — higher means more decision points
pub choice_score: f64,
}
/// Analyze trace structure from traces and activity frequencies
///
/// This function computes quantitative characteristics of the event log
/// and selects the most appropriate YAWL pattern based on those characteristics.
///
/// # Arguments
///
/// * `traces` - Vector of traces, where each trace is a vector of activity names
///
/// # Returns
///
/// A `TraceStructureAnalysis` containing the selected pattern, confidence score,
/// and detailed characteristics.
///
/// # Examples
///
/// ```
/// # use wasm4pm::pattern_analysis::analyze_trace_structure;
/// # use wasm4pm::pattern_dispatch::PatternType;
/// let traces = vec![
/// vec!["A".to_string(), "B".to_string(), "C".to_string()],
/// vec!["A".to_string(), "B".to_string(), "C".to_string()],
/// ];
///
/// let analysis = analyze_trace_structure(&traces);
/// assert_eq!(analysis.primary_pattern, PatternType::Sequence);
/// ```
pub fn analyze_trace_structure(traces: &[Vec<String>]) -> TraceStructureAnalysis {
let chars = compute_characteristics(traces);
let (pattern, confidence) = select_pattern(&chars);
TraceStructureAnalysis {
primary_pattern: pattern,
confidence,
characteristics: chars,
}
}
/// Compute quantitative characteristics from traces
fn compute_characteristics(traces: &[Vec<String>]) -> StructureCharacteristics {
// Basic metrics
let avg_trace_length = if !traces.is_empty() {
traces.iter().map(|t| t.len()).sum::<usize>() as f64 / traces.len() as f64
} else {
0.0
};
let unique_activities: HashSet<String> =
traces.iter().flat_map(|t| t.iter().cloned()).collect();
// Detect concurrency (activities appearing in different orders)
let (concurrency_score, max_out_degree) = detect_concurrency(traces);
// Detect loops (repetitions within traces)
let (rework_score, has_repetitions) = detect_loops(traces);
// Detect choice points (branching)
let (branch_points, choice_score) = detect_choice_points(traces);
StructureCharacteristics {
avg_trace_length,
unique_activity_count: unique_activities.len(),
max_out_degree,
concurrency_score,
rework_score,
has_repetitions,
branch_points,
choice_score,
}
}
/// Detect concurrency by analyzing activity ordering across traces
///
/// Returns (concurrency_score, max_out_degree)
/// Concurrency score is [0, 1] where higher means more parallel paths
fn detect_concurrency(traces: &[Vec<String>]) -> (f64, usize) {
if traces.len() < 2 {
return (0.0, 0);
}
// Build orderings from each trace
let mut orderings: HashSet<(String, String)> = HashSet::new();
let mut all_activities: HashSet<String> = HashSet::new();
for trace in traces {
for i in 0..trace.len().saturating_sub(1) {
orderings.insert((trace[i].clone(), trace[i + 1].clone()));
all_activities.insert(trace[i].clone());
all_activities.insert(trace[i + 1].clone());
}
}
// Count concurrent pairs (A before B in some, B before A in others)
let mut concurrent_count = 0;
let activities: Vec<&String> = all_activities.iter().collect();
for i in 0..activities.len() {
for j in (i + 1)..activities.len() {
let a = activities[i];
let b = activities[j];
let ab = orderings.contains(&(a.clone(), b.clone()));
let ba = orderings.contains(&(b.clone(), a.clone()));
if ab && ba {
concurrent_count += 1;
}
}
}
let total_possible = activities.len() * (activities.len() - 1) / 2;
let concurrency_score = if total_possible > 0 {
concurrent_count as f64 / total_possible as f64
} else {
0.0
};
(concurrency_score, activities.len())
}
/// Detect loops by analyzing repetitions within traces
///
/// Returns (rework_score, has_repetitions)
/// Rework score is [0, 1] where higher means more repetitive activity
fn detect_loops(traces: &[Vec<String>]) -> (f64, bool) {
let mut total_repetitions = 0;
let mut traces_with_repetitions = 0;
for trace in traces {
let mut seen: HashSet<&str> = HashSet::new();
let mut has_repetition = false;
for activity in trace {
if seen.contains(activity.as_str()) {
has_repetition = true;
total_repetitions += 1;
}
seen.insert(activity.as_str());
}
if has_repetition {
traces_with_repetitions += 1;
}
}
let rework_score = if !traces.is_empty() {
(traces_with_repetitions as f64 / traces.len() as f64)
* (total_repetitions as f64 / traces.len() as f64)
} else {
0.0
};
(rework_score, traces_with_repetitions > 0)
}
/// Detect choice points by analyzing branching structure
///
/// Returns (branch_points, choice_score)
/// Branch points is the count of activities with multiple DIFFERENT outgoing edges
/// Choice score is [0, 1] where higher means more decision points
fn detect_choice_points(traces: &[Vec<String>]) -> (usize, f64) {
// Track unique successors for each activity
let mut outgoing: HashMap<String, HashSet<String>> = HashMap::new();
for trace in traces {
for i in 0..trace.len().saturating_sub(1) {
outgoing
.entry(trace[i].clone())
.or_default()
.insert(trace[i + 1].clone());
}
}
// Count activities with multiple unique successors (real branch points)
let branch_points = outgoing.values().filter(|set| set.len() > 1).count();
// Choice score based on branch point diversity relative to total activities
let choice_score = if !traces.is_empty() && !outgoing.is_empty() {
branch_points as f64 / outgoing.len() as f64
} else {
0.0
};
(branch_points, choice_score)
}
/// Select the best pattern based on computed characteristics
///
/// Returns (pattern_type, confidence_score)
/// Confidence is [0, 1] where higher means more confident in the selection
fn select_pattern(chars: &StructureCharacteristics) -> (PatternType, f64) {
// Scoring for each pattern type
let sequence_score =
if chars.concurrency_score < 0.2 && !chars.has_repetitions && chars.choice_score < 0.3 {
0.9
} else {
0.0
};
let parallel_score = chars.concurrency_score;
let loop_score = if chars.has_repetitions {
chars.rework_score
} else {
0.0
};
let choice_score = chars.choice_score;
// Select pattern with highest score
let scores = vec![
(PatternType::Sequence, sequence_score),
(PatternType::ParallelSplit, parallel_score),
(PatternType::StructuredLoop, loop_score),
(PatternType::ExclusiveChoice, choice_score),
];
let (pattern, confidence) = scores
.into_iter()
.max_by(|a, b| a.1.total_cmp(&b.1))
.unwrap_or((PatternType::Sequence, 0.5));
(pattern, confidence)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sequence_detection() {
let traces = vec![
vec!["A".to_string(), "B".to_string(), "C".to_string()],
vec!["A".to_string(), "B".to_string(), "C".to_string()],
vec!["A".to_string(), "B".to_string(), "C".to_string()],
];
let analysis = analyze_trace_structure(&traces);
assert_eq!(analysis.primary_pattern, PatternType::Sequence);
assert!(!analysis.characteristics.has_repetitions);
assert!(analysis.characteristics.concurrency_score < 0.2);
}
#[test]
fn test_loop_detection() {
let traces = vec![vec![
"A".to_string(),
"B".to_string(),
"A".to_string(),
"C".to_string(),
]];
let analysis = analyze_trace_structure(&traces);
assert!(analysis.characteristics.has_repetitions);
assert!(analysis.characteristics.rework_score > 0.0);
}
#[test]
fn test_concurrency_detection() {
let traces = vec![
vec!["A".to_string(), "B".to_string(), "C".to_string()],
vec!["A".to_string(), "C".to_string(), "B".to_string()],
];
let analysis = analyze_trace_structure(&traces);
assert!(analysis.characteristics.concurrency_score > 0.0);
}
#[test]
fn test_empty_traces() {
let traces: Vec<Vec<String>> = vec![];
let analysis = analyze_trace_structure(&traces);
// Should default to Sequence with low confidence
assert_eq!(analysis.primary_pattern, PatternType::Sequence);
}
}