1use std::collections::{BTreeSet, HashSet};
10
11use ocel::Ocel;
12use serde::Serialize;
13
14use crate::trace;
15
16const MAX_ACTIVITIES: usize = 20;
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
23#[serde(rename_all = "camelCase")]
24pub struct Place {
25 pub id: String,
26 pub inputs: Vec<String>,
27 pub outputs: Vec<String>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub struct PetriNet {
34 pub object_type: String,
35 pub transitions: Vec<String>,
36 pub places: Vec<Place>,
37 pub warnings: Vec<String>,
39}
40
41fn extend_independent(
42 from: usize,
43 n: usize,
44 independent: &[Vec<bool>],
45 current: &mut Vec<u16>,
46 out: &mut Vec<Vec<u16>>,
47) {
48 for next in from..n {
49 if independent[next][next]
52 && current
53 .iter()
54 .all(|&a| independent[a as usize][next] && independent[next][a as usize])
55 {
56 current.push(u16::try_from(next).expect("checked size"));
57 out.push(current.clone());
58 extend_independent(next + 1, n, independent, current, out);
59 current.pop();
60 }
61 }
62}
63
64fn independent_subsets(n: usize, independent: &[Vec<bool>]) -> Vec<Vec<u16>> {
66 let mut out = Vec::new();
67 let mut current: Vec<u16> = Vec::new();
68 extend_independent(0, n, independent, &mut current, &mut out);
69 out
70}
71
72fn maximal_pairs(
75 n: usize,
76 causal: &[Vec<bool>],
77 independent: &[Vec<bool>],
78) -> Vec<(Vec<u16>, Vec<u16>)> {
79 let mut pairs: Vec<(Vec<u16>, Vec<u16>)> = Vec::new();
80 for a_set in independent_subsets(n, independent) {
81 let common: Vec<usize> = (0..n)
82 .filter(|&b| a_set.iter().all(|&a| causal[a as usize][b]))
83 .collect();
84 if common.is_empty() {
85 continue;
86 }
87 let sub_independent: Vec<Vec<bool>> = common
88 .iter()
89 .map(|&x| common.iter().map(|&y| independent[x][y]).collect())
90 .collect();
91 for b_local in independent_subsets(common.len(), &sub_independent) {
92 let b_set: Vec<u16> = b_local
93 .iter()
94 .map(|&i| u16::try_from(common[i as usize]).expect("checked size"))
95 .collect();
96 pairs.push((a_set.clone(), b_set));
97 }
98 }
99
100 let as_sets: Vec<(HashSet<u16>, HashSet<u16>)> = pairs
101 .iter()
102 .map(|(a, b)| {
103 (
104 a.iter().copied().collect::<HashSet<u16>>(),
105 b.iter().copied().collect::<HashSet<u16>>(),
106 )
107 })
108 .collect();
109 pairs
110 .iter()
111 .enumerate()
112 .filter(|&(i, _)| {
113 !as_sets.iter().enumerate().any(|(j, (a2, b2))| {
114 j != i
115 && as_sets[i].0.is_subset(a2)
116 && as_sets[i].1.is_subset(b2)
117 && (as_sets[i].0.len() < a2.len() || as_sets[i].1.len() < b2.len())
118 })
119 })
120 .map(|(_, p)| p.clone())
121 .collect()
122}
123
124#[must_use]
126pub fn alpha(log: &Ocel, object_type: &str) -> PetriNet {
127 let traces = trace::build(log, object_type);
128 let n = traces.activity_names.len();
129 let mut warnings: Vec<String> = Vec::new();
130
131 let mut follows = vec![vec![false; n]; n];
132 let mut starts: BTreeSet<u16> = BTreeSet::new();
133 let mut ends: BTreeSet<u16> = BTreeSet::new();
134 for steps in &traces.steps {
135 let (Some(&(first, _)), Some(&(last, _))) = (steps.first(), steps.last()) else {
136 continue;
137 };
138 starts.insert(first);
139 ends.insert(last);
140 for pair in steps.windows(2) {
141 follows[pair[0].0 as usize][pair[1].0 as usize] = true;
142 }
143 }
144
145 let name = |id: u16| traces.activity_names[id as usize].to_owned();
146 let transitions: Vec<String> = {
147 let mut t: Vec<String> = (0..n)
148 .map(|i| traces.activity_names[i].to_owned())
149 .collect();
150 t.sort_unstable();
151 t
152 };
153
154 for (i, row) in follows.iter().enumerate() {
155 if row[i] {
156 warnings.push(format!(
157 "self-loop on '{}' cannot be modeled by the alpha algorithm; use the inductive miner",
158 traces.activity_names[i]
159 ));
160 }
161 }
162 if n > MAX_ACTIVITIES {
163 warnings.push(format!(
164 "{n} activities exceed the alpha pair-enumeration cutoff ({MAX_ACTIVITIES}); returning transitions only"
165 ));
166 return PetriNet {
167 object_type: object_type.to_owned(),
168 transitions,
169 places: Vec::new(),
170 warnings,
171 };
172 }
173
174 let mut causal = vec![vec![false; n]; n];
175 let mut independent = vec![vec![false; n]; n];
176 for a in 0..n {
177 for b in 0..n {
178 causal[a][b] = follows[a][b] && !follows[b][a];
179 independent[a][b] = !follows[a][b] && !follows[b][a];
180 }
181 }
182
183 let maximal = maximal_pairs(n, &causal, &independent);
184
185 let mut places: Vec<Place> = maximal
186 .iter()
187 .map(|(a_set, b_set)| Place {
188 id: String::new(),
189 inputs: a_set.iter().map(|&a| name(a)).collect(),
190 outputs: b_set.iter().map(|&b| name(b)).collect(),
191 })
192 .collect();
193 for place in &mut places {
194 place.inputs.sort_unstable();
195 place.outputs.sort_unstable();
196 }
197 places.sort_unstable_by(|a, b| (&a.inputs, &a.outputs).cmp(&(&b.inputs, &b.outputs)));
198 for (i, place) in places.iter_mut().enumerate() {
199 place.id = format!("p{}", i + 1);
200 }
201 places.insert(
202 0,
203 Place {
204 id: "source".into(),
205 inputs: Vec::new(),
206 outputs: {
207 let mut v: Vec<String> = starts.iter().map(|&a| name(a)).collect();
208 v.sort_unstable();
209 v
210 },
211 },
212 );
213 places.push(Place {
214 id: "sink".into(),
215 inputs: {
216 let mut v: Vec<String> = ends.iter().map(|&a| name(a)).collect();
217 v.sort_unstable();
218 v
219 },
220 outputs: Vec::new(),
221 });
222
223 PetriNet {
224 object_type: object_type.to_owned(),
225 transitions,
226 places,
227 warnings,
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::test_util::log_from_sequences;
235
236 #[test]
237 fn textbook_l1_yields_the_known_net() {
238 let log = log_from_sequences(&[
240 &["a", "b", "c", "d"],
241 &["a", "b", "c", "d"],
242 &["a", "b", "c", "d"],
243 &["a", "c", "b", "d"],
244 &["a", "c", "b", "d"],
245 &["a", "e", "d"],
246 ]);
247 let net = alpha(&log, "case");
248 assert!(net.warnings.is_empty());
249 assert_eq!(net.transitions, ["a", "b", "c", "d", "e"]);
250
251 let shapes: Vec<(Vec<&str>, Vec<&str>)> = net
252 .places
253 .iter()
254 .map(|p| {
255 (
256 p.inputs.iter().map(String::as_str).collect(),
257 p.outputs.iter().map(String::as_str).collect(),
258 )
259 })
260 .collect();
261 let expected: Vec<(Vec<&str>, Vec<&str>)> = vec![
262 (vec![], vec!["a"]), (vec!["a"], vec!["b", "e"]),
264 (vec!["a"], vec!["c", "e"]),
265 (vec!["b", "e"], vec!["d"]),
266 (vec!["c", "e"], vec!["d"]),
267 (vec!["d"], vec![]), ];
269 assert_eq!(shapes, expected);
270 }
271
272 #[test]
273 fn self_loops_produce_warnings_and_disconnect() {
274 let log = log_from_sequences(&[&["a", "b", "b", "c"]]);
275 let net = alpha(&log, "case");
276 assert!(net.warnings.iter().any(|w| w.contains("'b'")));
277 assert!(net
281 .places
282 .iter()
283 .all(|p| !p.inputs.contains(&"b".to_owned()) && !p.outputs.contains(&"b".to_owned())));
284 }
285
286 #[test]
287 fn empty_type_yields_empty_net() {
288 let log = log_from_sequences(&[]);
289 let net = alpha(&log, "case");
290 assert!(net.transitions.is_empty());
291 assert_eq!(net.places.len(), 2);
293 }
294}