use crate::powl::discovery::DiscoveryConfig;
use crate::powl_arena::{Operator, PowlArena};
use std::collections::{HashMap, HashSet};
fn build_efg(traces: &[Vec<String>]) -> HashSet<(String, String)> {
let mut efg = HashSet::new();
for trace in traces {
let activities: Vec<&String> = trace.iter().collect();
for i in 0..activities.len() {
for j in (i + 1)..activities.len() {
efg.insert((activities[i].clone(), activities[j].clone()));
}
}
}
efg
}
fn build_dfg(traces: &[Vec<String>]) -> HashSet<(String, String)> {
let mut dfg = HashSet::new();
for trace in traces {
for i in 0..trace.len().saturating_sub(1) {
dfg.insert((trace[i].clone(), trace[i + 1].clone()));
}
}
dfg
}
pub fn detect_concurrency_cut(
traces: &[Vec<String>],
arena: &mut PowlArena,
_config: &DiscoveryConfig,
) -> Result<u32, String> {
let unique_activities: HashSet<&str> = traces
.iter()
.flat_map(|trace| trace.iter().map(|s| s.as_str()))
.collect();
if unique_activities.len() < 2 {
return Err("Not enough activities for concurrency cut".to_string());
}
let orderings: HashSet<Vec<&str>> = traces
.iter()
.map(|trace| trace.iter().map(|s| s.as_str()).collect())
.collect();
if orderings.len() < 2 {
return Err("Only one ordering found, not concurrent".to_string());
}
let activity_set: Vec<String> = unique_activities
.into_iter()
.map(|s| s.to_string())
.collect();
let mut activity_to_idx: HashMap<String, usize> = HashMap::new();
for (i, act) in activity_set.iter().enumerate() {
activity_to_idx.insert(act.clone(), i);
}
let mut child_indices: Vec<u32> = Vec::new();
for activity in &activity_set {
let child_idx = arena.add_transition(Some(activity.clone()));
child_indices.push(child_idx);
}
let spo_idx = arena.add_strict_partial_order(child_indices.clone());
for trace in traces {
for i in 0..trace.len().saturating_sub(1) {
let src = &trace[i];
let tgt = &trace[i + 1];
if let (Some(&src_idx), Some(&tgt_idx)) =
(activity_to_idx.get(src), activity_to_idx.get(tgt))
{
arena.add_order_edge(spo_idx, src_idx, tgt_idx).ok();
}
}
}
Ok(spo_idx)
}
#[allow(clippy::needless_range_loop)] pub fn detect_sequence_cut(
traces: &[Vec<String>],
arena: &mut PowlArena,
_config: &DiscoveryConfig,
) -> Result<u32, String> {
if traces.is_empty() {
return Err("No traces for sequence cut".to_string());
}
let dfg = build_dfg(traces);
let efg = build_efg(traces);
let mut activities: Vec<String> = traces
.iter()
.flat_map(|t| t.iter().cloned())
.collect::<HashSet<_>>()
.into_iter()
.collect::<Vec<_>>();
activities.sort();
if activities.is_empty() {
return Err("No activities in traces".to_string());
}
let mut act_to_idx: HashMap<String, usize> = HashMap::new();
for (i, a) in activities.iter().enumerate() {
act_to_idx.insert(a.clone(), i);
}
let n = activities.len();
let mut directed: Vec<Vec<usize>> = vec![Vec::new(); n];
for (a, b) in &dfg {
if let (Some(&ai), Some(&bi)) = (act_to_idx.get(a), act_to_idx.get(b)) {
if !directed[ai].contains(&bi) {
directed[ai].push(bi);
}
}
}
let sccs = tarjan_sccs(&directed);
if sccs.len() < 2 {
return Err("SCC decomposition produced fewer than 2 components".to_string());
}
let mut scc_to_idx: HashMap<usize, usize> = HashMap::new();
for (i, scc) in sccs.iter().enumerate() {
for &node in scc {
scc_to_idx.insert(node, i);
}
}
let num_sccs = sccs.len();
let mut scc_efg: Vec<Vec<bool>> = vec![vec![false; num_sccs]; num_sccs];
for (a, b) in &efg {
if let (Some(&ai), Some(&bi)) = (act_to_idx.get(a), act_to_idx.get(b)) {
let scc_a = scc_to_idx[&ai];
let scc_b = scc_to_idx[&bi];
if scc_a != scc_b {
scc_efg[scc_a][scc_b] = true;
}
}
}
let mut scc_order: Vec<Vec<bool>> = vec![vec![false; num_sccs]; num_sccs];
for i in 0..num_sccs {
for j in (i + 1)..num_sccs {
let ij = scc_efg[i][j];
let ji = scc_efg[j][i];
if ij && !ji {
scc_order[i][j] = true;
} else if ji && !ij {
scc_order[j][i] = true;
} else if ij && ji {
return Err("SCCs have bidirectional edges, not sequential".to_string());
}
}
}
for i in 0..num_sccs {
for j in (i + 1)..num_sccs {
if !scc_order[i][j] && !scc_order[j][i] {
return Err("SCCs do not form a total order".to_string());
}
}
}
let mut child_indices: Vec<u32> = Vec::new();
for scc in &sccs {
let scc_activities: Vec<String> = scc.iter().map(|&idx| activities[idx].clone()).collect();
if scc_activities.len() == 1 {
let idx = arena.add_transition(Some(scc_activities[0].clone()));
child_indices.push(idx);
} else {
let scc_activity_set: HashSet<String> = scc_activities.iter().cloned().collect();
let filtered_traces: Vec<Vec<String>> = traces
.iter()
.map(|trace| {
trace
.iter()
.filter(|a| scc_activity_set.contains(*a))
.cloned()
.collect()
})
.filter(|t: &Vec<String>| !t.is_empty())
.collect();
if filtered_traces.is_empty() {
return Err("Filtered traces for SCC are empty".to_string());
}
let scc_dfg = build_dfg(&filtered_traces);
let mut scc_child_indices: Vec<u32> = Vec::new();
for activity in &scc_activities {
let idx = arena.add_transition(Some(activity.clone()));
scc_child_indices.push(idx);
}
if scc_child_indices.len() == 1 {
child_indices.push(scc_child_indices[0]);
} else {
let scc_spo_idx = arena.add_strict_partial_order(scc_child_indices.clone());
let scc_act_to_idx: HashMap<String, usize> = scc_activities
.iter()
.enumerate()
.map(|(i, a)| (a.clone(), i))
.collect();
for (a, b) in &scc_dfg {
if let (Some(&ai), Some(&bi)) = (scc_act_to_idx.get(a), scc_act_to_idx.get(b)) {
arena.add_order_edge(scc_spo_idx, ai, bi).ok();
}
}
child_indices.push(scc_spo_idx);
}
}
}
if child_indices.len() == 1 {
Ok(child_indices[0])
} else {
let spo_idx = arena.add_strict_partial_order(child_indices.clone());
for i in 0..child_indices.len().saturating_sub(1) {
arena.add_order_edge(spo_idx, i, i + 1).ok();
}
Ok(spo_idx)
}
}
fn tarjan_sccs(graph: &[Vec<usize>]) -> Vec<Vec<usize>> {
let n = graph.len();
let mut indices: Vec<Option<usize>> = vec![None; n];
let mut lowlinks: Vec<usize> = vec![0; n];
let mut on_stack: Vec<bool> = vec![false; n];
let mut stack: Vec<usize> = Vec::new();
let mut sccs: Vec<Vec<usize>> = Vec::new();
let mut index_counter = 0;
fn strongconnect(
v: usize,
graph: &[Vec<usize>],
indices: &mut [Option<usize>],
lowlinks: &mut [usize],
on_stack: &mut [bool],
stack: &mut Vec<usize>,
sccs: &mut Vec<Vec<usize>>,
index_counter: &mut usize,
) {
indices[v] = Some(*index_counter);
lowlinks[v] = *index_counter;
*index_counter += 1;
stack.push(v);
on_stack[v] = true;
for &w in &graph[v] {
if indices[w].is_none() {
strongconnect(
w,
graph,
indices,
lowlinks,
on_stack,
stack,
sccs,
index_counter,
);
lowlinks[v] = lowlinks[v].min(lowlinks[w]);
} else if on_stack[w] {
lowlinks[v] = lowlinks[v].min(indices[w].unwrap());
}
}
if lowlinks[v] == indices[v].unwrap() {
let mut scc = Vec::new();
loop {
let w = stack.pop().unwrap();
on_stack[w] = false;
scc.push(w);
if w == v {
break;
}
}
scc.sort();
sccs.push(scc);
}
}
for v in 0..n {
if indices[v].is_none() {
strongconnect(
v,
graph,
&mut indices,
&mut lowlinks,
&mut on_stack,
&mut stack,
&mut sccs,
&mut index_counter,
);
}
}
sccs.reverse();
sccs
}
pub fn detect_loop_cut(
traces: &[Vec<String>],
arena: &mut PowlArena,
_config: &DiscoveryConfig,
) -> Result<u32, String> {
if traces.is_empty() {
return Err("No traces for loop cut".to_string());
}
let dfg = build_dfg(traces);
let _efg = build_efg(traces);
let mut start_activities: HashSet<String> = HashSet::new();
for trace in traces {
if !trace.is_empty() {
start_activities.insert(trace[0].clone());
}
}
let mut end_activities: HashSet<String> = HashSet::new();
for trace in traces {
if !trace.is_empty() {
end_activities.insert(trace[trace.len() - 1].clone());
}
}
if start_activities.is_empty() {
return Err("No start activities found".to_string());
}
let mut all_activities: Vec<String> = traces
.iter()
.flat_map(|t| t.iter().cloned())
.collect::<HashSet<_>>()
.into_iter()
.collect::<Vec<_>>();
all_activities.sort();
let mut act_to_idx: HashMap<String, usize> = HashMap::new();
for (i, a) in all_activities.iter().enumerate() {
act_to_idx.insert(a.clone(), i);
}
let n = all_activities.len();
let mut dfg_reachable: Vec<Vec<bool>> = vec![vec![false; n]; n];
for (a, b) in &dfg {
if let (Some(&ai), Some(&bi)) = (act_to_idx.get(a), act_to_idx.get(b)) {
dfg_reachable[ai][bi] = true;
}
}
let mut reachable = dfg_reachable.clone();
for k in 0..n {
for i in 0..n {
for j in 0..n {
if reachable[i][k] && reachable[k][j] {
reachable[i][j] = true;
}
}
}
}
let mut do_part: HashSet<String> = HashSet::new();
for start_act in &start_activities {
if let Some(&start_idx) = act_to_idx.get(start_act) {
do_part.insert(start_act.clone());
for j in 0..n {
if reachable[start_idx][j] {
do_part.insert(all_activities[j].clone());
}
}
}
}
let mut redo_part: HashSet<String> = HashSet::new();
for trace in traces {
let mut start_indices: Vec<usize> = Vec::new();
for (i, activity) in trace.iter().enumerate() {
if start_activities.contains(activity) {
start_indices.push(i);
}
}
if start_indices.len() >= 2 {
let first_start_idx = start_indices[0];
let second_start_idx = start_indices[1];
for activity in &trace[(first_start_idx + 1)..second_start_idx] {
redo_part.insert(activity.clone());
}
}
}
if redo_part.is_empty() {
return Err("No redo activities found, not a loop pattern".to_string());
}
let mut valid_redo = false;
for redo_act in &redo_part {
if let Some(&redo_idx) = act_to_idx.get(redo_act) {
for start_act in &start_activities {
if let Some(&start_idx) = act_to_idx.get(start_act) {
if dfg_reachable[redo_idx][start_idx] || dfg_reachable[start_idx][redo_idx] {
valid_redo = true;
break;
}
}
}
}
if valid_redo {
break;
}
}
if !valid_redo && !redo_part.is_empty() {
}
let mut do_traces: Vec<Vec<String>> = Vec::new();
let mut redo_traces: Vec<Vec<String>> = Vec::new();
for trace in traces {
let first_start_idx = trace.iter().position(|a| start_activities.contains(a));
if let Some(start_pos) = first_start_idx {
let second_start_idx = trace[start_pos + 1..]
.iter()
.position(|a| start_activities.contains(a))
.map(|i| i + start_pos + 1);
if let Some(redo_start_pos) = second_start_idx {
let do_trace: Vec<String> = trace[0..redo_start_pos].to_vec();
let redo_trace: Vec<String> = trace[redo_start_pos..].to_vec();
if !do_trace.is_empty() {
do_traces.push(do_trace);
}
if !redo_trace.is_empty() {
redo_traces.push(redo_trace);
}
} else {
do_traces.push(trace.clone());
}
}
}
let do_model_idx = if do_part.len() == 1 {
let activity = {
let mut v: Vec<_> = do_part.iter().cloned().collect();
v.sort_unstable();
v.into_iter().next().unwrap()
};
arena.add_transition(Some(activity))
} else {
let mut do_children: Vec<u32> = Vec::new();
for activity in &do_part {
let idx = arena.add_transition(Some(activity.clone()));
do_children.push(idx);
}
let do_spo_idx = arena.add_strict_partial_order(do_children.clone());
let do_act_to_idx: HashMap<String, usize> = do_part
.iter()
.enumerate()
.map(|(i, a)| (a.clone(), i))
.collect();
for (a, b) in &dfg {
if let (Some(&ai), Some(&bi)) = (do_act_to_idx.get(a), do_act_to_idx.get(b)) {
arena.add_order_edge(do_spo_idx, ai, bi).ok();
}
}
do_spo_idx
};
let redo_model_idx = if redo_part.len() == 1 {
let activity = {
let mut v: Vec<_> = redo_part.iter().cloned().collect();
v.sort_unstable();
v.into_iter().next().unwrap()
};
arena.add_transition(Some(activity))
} else {
let mut redo_children: Vec<u32> = Vec::new();
let mut redo_order: Vec<String> = redo_part.iter().cloned().collect();
redo_order.sort();
for activity in &redo_order {
let idx = arena.add_transition(Some(activity.clone()));
redo_children.push(idx);
}
let redo_spo_idx = arena.add_strict_partial_order(redo_children.clone());
let redo_act_to_idx: HashMap<String, usize> = redo_order
.iter()
.enumerate()
.map(|(i, a)| (a.clone(), i))
.collect();
for (a, b) in &dfg {
if let (Some(&ai), Some(&bi)) = (redo_act_to_idx.get(a), redo_act_to_idx.get(b)) {
arena.add_order_edge(redo_spo_idx, ai, bi).ok();
}
}
redo_spo_idx
};
let loop_idx = arena.add_operator(Operator::Loop, vec![do_model_idx, redo_model_idx]);
Ok(loop_idx)
}
pub fn detect_xor_cut(
traces: &[Vec<String>],
arena: &mut PowlArena,
_config: &DiscoveryConfig,
) -> Result<u32, String> {
if traces.is_empty() || traces.len() < 2 {
return Err("Need at least 2 traces for XOR cut".to_string());
}
let dfg = build_dfg(traces);
let mut activities: Vec<String> = traces
.iter()
.flat_map(|t| t.iter().cloned())
.collect::<HashSet<_>>()
.into_iter()
.collect::<Vec<_>>();
activities.sort();
if activities.is_empty() {
return Err("No activities in traces".to_string());
}
let mut act_to_idx: HashMap<String, usize> = HashMap::new();
for (i, a) in activities.iter().enumerate() {
act_to_idx.insert(a.clone(), i);
}
let n = activities.len();
let mut undirected: Vec<Vec<usize>> = vec![Vec::new(); n];
for (a, b) in &dfg {
if let (Some(&ai), Some(&bi)) = (act_to_idx.get(a), act_to_idx.get(b)) {
if !undirected[ai].contains(&bi) {
undirected[ai].push(bi);
}
if !undirected[bi].contains(&ai) {
undirected[bi].push(ai);
}
}
}
let mut visited: Vec<bool> = vec![false; n];
let mut components: Vec<Vec<usize>> = Vec::new();
fn dfs(node: usize, graph: &[Vec<usize>], visited: &mut [bool], component: &mut Vec<usize>) {
visited[node] = true;
component.push(node);
for &neighbor in &graph[node] {
if !visited[neighbor] {
dfs(neighbor, graph, visited, component);
}
}
}
for i in 0..n {
if !visited[i] {
let mut component = Vec::new();
dfs(i, &undirected, &mut visited, &mut component);
component.sort();
components.push(component);
}
}
if components.len() < 2 {
return Err(
"Activities form a single connected component, not XOR alternatives".to_string(),
);
}
let component_id: Vec<usize> = {
let mut comp_id = vec![0; n];
for (comp_idx, component) in components.iter().enumerate() {
for &node in component {
comp_id[node] = comp_idx;
}
}
comp_id
};
for trace in traces {
let mut seen_components = HashSet::new();
for activity in trace {
if let Some(&act_idx) = act_to_idx.get(activity) {
seen_components.insert(component_id[act_idx]);
}
}
if seen_components.len() > 1 {
return Err("Trace contains activities from multiple components, not XOR".to_string());
}
}
let mut child_indices: Vec<u32> = Vec::new();
for component in &components {
let component_activities: Vec<String> = component
.iter()
.map(|&idx| activities[idx].clone())
.collect();
let component_activity_set: HashSet<String> =
component_activities.iter().cloned().collect();
let filtered_traces: Vec<Vec<String>> = traces
.iter()
.filter_map(|trace| {
let filtered: Vec<String> = trace
.iter()
.filter(|a| component_activity_set.contains(*a))
.cloned()
.collect();
if filtered.is_empty() {
None
} else {
Some(filtered)
}
})
.collect();
if filtered_traces.is_empty() {
return Err("Filtered traces for component are empty".to_string());
}
if component_activities.len() == 1 {
let idx = arena.add_transition(Some(component_activities[0].clone()));
child_indices.push(idx);
} else {
let component_dfg = build_dfg(&filtered_traces);
let mut comp_children: Vec<u32> = Vec::new();
for activity in &component_activities {
let idx = arena.add_transition(Some(activity.clone()));
comp_children.push(idx);
}
let comp_spo_idx = arena.add_strict_partial_order(comp_children.clone());
let comp_act_to_idx: HashMap<String, usize> = component_activities
.iter()
.enumerate()
.map(|(i, a)| (a.clone(), i))
.collect();
for (a, b) in &component_dfg {
if let (Some(&ai), Some(&bi)) = (comp_act_to_idx.get(a), comp_act_to_idx.get(b)) {
arena.add_order_edge(comp_spo_idx, ai, bi).ok();
}
}
child_indices.push(comp_spo_idx);
}
}
let xor_idx = arena.add_operator(Operator::Xor, child_indices);
Ok(xor_idx)
}
#[allow(clippy::needless_range_loop)]
pub fn detect_maximal_partial_order_cut(
traces: &[Vec<String>],
arena: &mut PowlArena,
_config: &DiscoveryConfig,
) -> Result<u32, String> {
let efg = build_efg(traces);
let dfg = build_dfg(traces);
let mut activities: Vec<String> = traces
.iter()
.flat_map(|t| t.iter().cloned())
.collect::<HashSet<_>>()
.into_iter()
.collect::<Vec<_>>();
activities.sort();
if activities.len() < 2 {
return Err("Not enough activities for maximal PO cut".to_string());
}
let mut act_to_idx: HashMap<String, usize> = HashMap::new();
for (i, a) in activities.iter().enumerate() {
act_to_idx.insert(a.clone(), i);
}
let n = activities.len();
let mut has_edge: Vec<Vec<bool>> = vec![vec![false; n]; n];
for i in 0..n {
for j in (i + 1)..n {
let a = &activities[i];
let b = &activities[j];
let ab = efg.contains(&(a.clone(), b.clone()));
let ba = efg.contains(&(b.clone(), a.clone()));
if ab && !ba {
has_edge[i][j] = true;
} else if ba && !ab {
has_edge[j][i] = true;
}
}
}
let mut pre_sets: Vec<HashSet<usize>> = vec![HashSet::new(); n];
let mut post_sets: Vec<HashSet<usize>> = vec![HashSet::new(); n];
for i in 0..n {
for j in 0..n {
if has_edge[i][j] {
pre_sets[j].insert(i);
post_sets[i].insert(j);
}
}
}
let mut clusters: Vec<Vec<usize>> = Vec::new();
let mut assigned: Vec<bool> = vec![false; n];
for i in 0..n {
if assigned[i] {
continue;
}
let mut cluster = vec![i];
assigned[i] = true;
for j in (i + 1)..n {
if !assigned[j] && pre_sets[i] == pre_sets[j] && post_sets[i] == post_sets[j] {
cluster.push(j);
assigned[j] = true;
}
}
clusters.push(cluster);
}
if clusters.len() < 2 {
return Err("Cannot form multiple clusters for maximal PO cut".to_string());
}
let mut has_incoming: HashSet<String> = HashSet::new();
let mut has_outgoing: HashSet<String> = HashSet::new();
for (a, b) in dfg.iter() {
has_outgoing.insert(a.clone());
has_incoming.insert(b.clone());
}
let start_activities: HashSet<&str> = activities
.iter()
.filter(|a| !has_incoming.contains(*a))
.map(|s| s.as_str())
.collect();
let end_activities: HashSet<&str> = activities
.iter()
.filter(|a| !has_outgoing.contains(*a))
.map(|s| s.as_str())
.collect();
let nc = clusters.len();
let mut cluster_pre: Vec<HashSet<usize>> = vec![HashSet::new(); nc];
let mut cluster_post: Vec<HashSet<usize>> = vec![HashSet::new(); nc];
for ci in 0..nc {
for cj in 0..nc {
if ci == cj {
continue;
}
for &ni in &clusters[ci] {
for &nj in &clusters[cj] {
if has_edge[ni][nj] {
cluster_pre[cj].insert(ci);
cluster_post[ci].insert(cj);
}
}
}
}
}
for ci in 0..nc {
let is_start_cluster = cluster_pre[ci].is_empty();
if is_start_cluster {
let has_start = clusters[ci]
.iter()
.any(|&idx| start_activities.contains(activities[idx].as_str()));
if !has_start && !start_activities.is_empty() {
return Err("Start cluster has no start activity".to_string());
}
}
let is_end_cluster = cluster_post[ci].is_empty();
if is_end_cluster {
let has_end = clusters[ci]
.iter()
.any(|&idx| end_activities.contains(activities[idx].as_str()));
if !has_end && !end_activities.is_empty() {
return Err("End cluster has no end activity".to_string());
}
}
}
let mut child_indices: Vec<u32> = Vec::new();
let mut cluster_to_child: Vec<usize> = Vec::new();
for cluster in &clusters {
if cluster.len() == 1 {
let idx = arena.add_transition(Some(activities[cluster[0]].clone()));
child_indices.push(idx);
cluster_to_child.push(child_indices.len() - 1);
} else {
let mut seq_children: Vec<u32> = Vec::new();
for &act_idx in cluster {
let idx = arena.add_transition(Some(activities[act_idx].clone()));
seq_children.push(idx);
}
let spo_idx = arena.add_strict_partial_order(seq_children.clone());
for k in 0..seq_children.len().saturating_sub(1) {
arena.add_order_edge(spo_idx, k, k + 1).ok();
}
child_indices.push(spo_idx);
cluster_to_child.push(child_indices.len() - 1);
}
}
let spo_idx = arena.add_strict_partial_order(child_indices.clone());
for ci in 0..nc {
for cj in 0..nc {
if ci != cj && cluster_post[ci].contains(&cj) {
arena.add_order_edge(spo_idx, ci, cj).ok();
}
}
}
Ok(spo_idx)
}
#[allow(clippy::needless_range_loop)]
pub fn detect_dynamic_clustering_cut(
traces: &[Vec<String>],
arena: &mut PowlArena,
_config: &DiscoveryConfig,
) -> Result<u32, String> {
let efg = build_efg(traces);
let _dfg = build_dfg(traces);
let mut activities: Vec<String> = traces
.iter()
.flat_map(|t| t.iter().cloned())
.collect::<HashSet<_>>()
.into_iter()
.collect::<Vec<_>>();
activities.sort();
if activities.len() < 2 {
return Err("Not enough activities for dynamic clustering cut".to_string());
}
let mut clusters: Vec<Vec<String>> = activities.iter().map(|a| vec![a.clone()]).collect();
let mut changed = true;
while changed {
changed = false;
let nc = clusters.len();
let mut cluster_efg_both: Vec<Vec<bool>> = vec![vec![false; nc]; nc];
let mut cluster_efg_ab: Vec<Vec<bool>> = vec![vec![false; nc]; nc];
let mut cluster_efg_ba: Vec<Vec<bool>> = vec![vec![false; nc]; nc];
for ci in 0..nc {
for cj in (ci + 1)..nc {
let mut has_ab = false;
let mut has_ba = false;
for a in &clusters[ci] {
for b in &clusters[cj] {
if efg.contains(&(a.clone(), b.clone())) {
has_ab = true;
}
if efg.contains(&(b.clone(), a.clone())) {
has_ba = true;
}
}
}
cluster_efg_ab[ci][cj] = has_ab;
cluster_efg_ba[ci][cj] = has_ba;
cluster_efg_ab[cj][ci] = has_ba;
cluster_efg_ba[cj][ci] = has_ab;
cluster_efg_both[ci][cj] = has_ab && has_ba;
cluster_efg_both[cj][ci] = has_ab && has_ba;
}
}
let mut new_clusters: Vec<Vec<String>> = Vec::new();
let mut merged: Vec<bool> = vec![false; nc];
for ci in 0..nc {
if merged[ci] {
continue;
}
let mut group = clusters[ci].clone();
for cj in (ci + 1)..nc {
if !merged[cj] && cluster_efg_both[ci][cj] {
group.extend(clusters[cj].iter().cloned());
merged[cj] = true;
changed = true;
}
}
new_clusters.push(group);
}
clusters = new_clusters;
}
if clusters.len() < 2 {
return Err("Dynamic clustering produced only one cluster".to_string());
}
let nc = clusters.len();
let mut cluster_has_edge: Vec<Vec<bool>> = vec![vec![false; nc]; nc];
for ci in 0..nc {
for cj in (ci + 1)..nc {
let mut has_ab = false;
let mut has_ba = false;
for a in &clusters[ci] {
for b in &clusters[cj] {
if efg.contains(&(a.clone(), b.clone())) {
has_ab = true;
}
if efg.contains(&(b.clone(), a.clone())) {
has_ba = true;
}
}
}
if has_ab && !has_ba {
cluster_has_edge[ci][cj] = true;
} else if has_ba && !has_ab {
cluster_has_edge[cj][ci] = true;
}
}
}
for ci in 0..nc {
for cj in (ci + 1)..nc {
if cluster_has_edge[ci][cj] && cluster_has_edge[cj][ci] {
return Err("Dynamic clustering produced cyclic order between clusters".to_string());
}
}
}
let mut child_indices: Vec<u32> = Vec::new();
for cluster in &clusters {
if cluster.len() == 1 {
let idx = arena.add_transition(Some(cluster[0].clone()));
child_indices.push(idx);
} else {
let mut seq_children: Vec<u32> = Vec::new();
for activity in cluster {
let idx = arena.add_transition(Some(activity.clone()));
seq_children.push(idx);
}
let spo_idx = arena.add_strict_partial_order(seq_children.clone());
for k in 0..seq_children.len().saturating_sub(1) {
arena.add_order_edge(spo_idx, k, k + 1).ok();
}
child_indices.push(spo_idx);
}
}
let spo_idx = arena.add_strict_partial_order(child_indices.clone());
for ci in 0..nc {
for cj in 0..nc {
if cluster_has_edge[ci][cj] {
arena.add_order_edge(spo_idx, ci, cj).ok();
}
}
}
Ok(spo_idx)
}
#[allow(clippy::needless_range_loop)]
pub fn detect_brute_force_partial_order_cut(
traces: &[Vec<String>],
arena: &mut PowlArena,
_config: &DiscoveryConfig,
) -> Result<u32, String> {
let efg = build_efg(traces);
let dfg = build_dfg(traces);
let mut activities: Vec<String> = traces
.iter()
.flat_map(|t| t.iter().cloned())
.collect::<HashSet<_>>()
.into_iter()
.collect::<Vec<_>>();
activities.sort();
if activities.len() < 2 {
return Err("Not enough activities for brute force PO cut".to_string());
}
if activities.len() > 8 {
return Err(format!(
"Brute force cut limited to 8 activities, got {}",
activities.len()
));
}
let mut has_incoming: HashSet<String> = HashSet::new();
let mut has_outgoing: HashSet<String> = HashSet::new();
for (a, b) in &dfg {
has_outgoing.insert(a.clone());
has_incoming.insert(b.clone());
}
let start_activities: HashSet<&str> = activities
.iter()
.filter(|a| !has_incoming.contains(*a))
.map(|s| s.as_str())
.collect();
let end_activities: HashSet<&str> = activities
.iter()
.filter(|a| !has_outgoing.contains(*a))
.map(|s| s.as_str())
.collect();
let n = activities.len();
let two_partitions = generate_partitions_of_size_k(&activities, 2);
for partition in &two_partitions {
if let Some(spo_idx) = try_partition_as_po(
partition,
&efg,
&activities,
&start_activities,
&end_activities,
arena,
) {
return Ok(spo_idx);
}
}
for k in 3..=n {
let partitions = generate_partitions_of_size_k(&activities, k);
for partition in &partitions {
if let Some(spo_idx) = try_partition_as_po(
partition,
&efg,
&activities,
&start_activities,
&end_activities,
arena,
) {
return Ok(spo_idx);
}
}
}
Err("No valid partition found for brute force PO cut".to_string())
}
#[allow(clippy::needless_range_loop)]
fn try_partition_as_po(
partition: &[Vec<String>],
efg: &HashSet<(String, String)>,
_all_activities: &[String],
start_activities: &HashSet<&str>,
end_activities: &HashSet<&str>,
arena: &mut PowlArena,
) -> Option<u32> {
let np = partition.len();
if np < 2 {
return None;
}
let mut has_edge: Vec<Vec<bool>> = vec![vec![false; np]; np];
let mut all_efg_ab: Vec<Vec<bool>> = vec![vec![true; np]; np];
let mut all_efg_ba: Vec<Vec<bool>> = vec![vec![true; np]; np];
for ci in 0..np {
for cj in (ci + 1)..np {
let mut any_ab = false;
let mut any_ba = false;
let mut all_ab = true;
let mut all_ba = true;
for a in &partition[ci] {
for b in &partition[cj] {
if efg.contains(&(a.clone(), b.clone())) {
any_ab = true;
} else {
all_ab = false;
}
if efg.contains(&(b.clone(), a.clone())) {
any_ba = true;
} else {
all_ba = false;
}
}
}
all_efg_ab[ci][cj] = all_ab;
all_efg_ba[ci][cj] = all_ba;
all_efg_ab[cj][ci] = all_ba;
all_efg_ba[cj][ci] = all_ab;
if any_ab && !any_ba {
has_edge[ci][cj] = true;
} else if any_ba && !any_ab {
has_edge[cj][ci] = true;
}
}
}
for ci in 0..np {
for cj in (ci + 1)..np {
if has_edge[ci][cj] && has_edge[cj][ci] {
return None; }
}
}
for ci in 0..np {
for cj in (ci + 1)..np {
if !has_edge[ci][cj]
&& !has_edge[cj][ci]
&& (!all_efg_ab[ci][cj] || !all_efg_ba[ci][cj])
{
return None;
}
}
}
let mut is_start_cluster: Vec<bool> = vec![true; np];
let mut is_end_cluster: Vec<bool> = vec![true; np];
for ci in 0..np {
for cj in 0..np {
if ci != cj {
if has_edge[ci][cj] {
is_start_cluster[cj] = false;
is_end_cluster[ci] = false;
}
if has_edge[cj][ci] {
is_start_cluster[ci] = false;
is_end_cluster[cj] = false;
}
}
}
}
for ci in 0..np {
if is_start_cluster[ci] {
let has_start = partition[ci]
.iter()
.any(|a| start_activities.contains(a.as_str()));
if !has_start && !start_activities.is_empty() {
return None;
}
}
if is_end_cluster[ci] {
let has_end = partition[ci]
.iter()
.any(|a| end_activities.contains(a.as_str()));
if !has_end && !end_activities.is_empty() {
return None;
}
}
}
let mut child_indices: Vec<u32> = Vec::new();
for cluster in partition {
if cluster.len() == 1 {
let idx = arena.add_transition(Some(cluster[0].clone()));
child_indices.push(idx);
} else {
let mut seq_children: Vec<u32> = Vec::new();
for activity in cluster {
let idx = arena.add_transition(Some(activity.clone()));
seq_children.push(idx);
}
let spo_idx = arena.add_strict_partial_order(seq_children.clone());
for k in 0..seq_children.len().saturating_sub(1) {
arena.add_order_edge(spo_idx, k, k + 1).ok();
}
child_indices.push(spo_idx);
}
}
let spo_idx = arena.add_strict_partial_order(child_indices.clone());
for ci in 0..np {
for cj in 0..np {
if has_edge[ci][cj] {
arena.add_order_edge(spo_idx, ci, cj).ok();
}
}
}
Some(spo_idx)
}
fn generate_partitions_of_size_k(items: &[String], k: usize) -> Vec<Vec<Vec<String>>> {
let n = items.len();
if k < 1 || k > n {
return Vec::new();
}
if k == n {
return vec![items.iter().map(|s| vec![s.clone()]).collect()];
}
let mut results = Vec::new();
let mut current: Vec<usize> = vec![0; n];
generate_rgs(&mut current, 0, k, items, &mut results);
results
}
fn generate_rgs(
current: &mut Vec<usize>,
pos: usize,
k: usize,
items: &[String],
results: &mut Vec<Vec<Vec<String>>>,
) {
let n = items.len();
if pos == n {
let max_group = *current.iter().max().unwrap_or(&0) + 1;
if max_group == k {
let mut partition: Vec<Vec<String>> = vec![Vec::new(); k];
for (i, &group) in current.iter().enumerate() {
partition[group].push(items[i].clone());
}
results.push(partition);
}
return;
}
let max_existing = if pos == 0 {
0
} else {
*current[..pos].iter().max().unwrap_or(&0) + 1
};
for group in 0..=max_existing.min(k - 1) {
if group < max_existing || (group == max_existing && max_existing < k) {
current[pos] = group;
generate_rgs(current, pos + 1, k, items, results);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_cuts_detect_correctly() {
let concurrent = vec![
vec!["A".to_string(), "B".to_string()],
vec!["B".to_string(), "A".to_string()],
];
let sequential = vec![
vec!["A".to_string(), "B".to_string(), "C".to_string()],
vec!["A".to_string(), "B".to_string(), "C".to_string()],
];
let alternatives = vec![vec!["A".to_string()], vec!["B".to_string()]];
let mut arena = PowlArena::new();
let config = DiscoveryConfig::default();
assert!(detect_concurrency_cut(&concurrent, &mut arena, &config).is_ok());
assert!(detect_sequence_cut(&sequential, &mut arena, &config).is_ok());
assert!(detect_xor_cut(&alternatives, &mut arena, &config).is_ok());
assert!(detect_xor_cut(&sequential, &mut arena, &config).is_err());
}
#[test]
fn test_partial_order_cuts_detect_clusters() {
let mixed = vec![
vec!["A".to_string(), "B".to_string(), "C".to_string()],
vec!["A".to_string(), "C".to_string(), "B".to_string()],
];
let mut arena = PowlArena::new();
let config = DiscoveryConfig::default();
assert!(detect_maximal_partial_order_cut(&mixed, &mut arena, &config).is_ok());
assert!(detect_dynamic_clustering_cut(&mixed, &mut arena, &config).is_ok());
let concurrent = vec![
vec!["A".to_string(), "B".to_string()],
vec!["B".to_string(), "A".to_string()],
];
assert!(detect_brute_force_partial_order_cut(&concurrent, &mut arena, &config).is_ok());
}
#[test]
fn test_cut_helpers_and_edge_cases() {
let items = vec!["A".to_string(), "B".to_string(), "C".to_string()];
assert_eq!(generate_partitions_of_size_k(&items, 2).len(), 3);
assert_eq!(generate_partitions_of_size_k(&items, 3).len(), 1);
let traces = vec![
vec!["A".to_string(), "B".to_string()],
vec!["B".to_string(), "A".to_string()],
];
let efg = build_efg(&traces);
assert!(efg.contains(&("A".to_string(), "B".to_string())));
assert!(efg.contains(&("B".to_string(), "A".to_string())));
let too_many = vec![vec![
"A".to_string(),
"B".to_string(),
"C".to_string(),
"D".to_string(),
"E".to_string(),
"F".to_string(),
"G".to_string(),
"H".to_string(),
"I".to_string(),
]];
let mut arena = PowlArena::new();
let config = DiscoveryConfig::default();
assert!(detect_brute_force_partial_order_cut(&too_many, &mut arena, &config).is_err());
}
#[test]
fn test_pw_d1_sequence_cut_scc_decomposition() {
let sequential = vec![
vec!["A".to_string(), "B".to_string(), "C".to_string()],
vec!["A".to_string(), "B".to_string(), "C".to_string()],
];
let mut arena = PowlArena::new();
let config = DiscoveryConfig::default();
let result = detect_sequence_cut(&sequential, &mut arena, &config);
assert!(
result.is_ok(),
"Sequence cut should detect strict total order of SCCs"
);
let single = vec![vec!["A".to_string()], vec!["A".to_string()]];
let mut arena2 = PowlArena::new();
let result2 = detect_sequence_cut(&single, &mut arena2, &config);
let complex = vec![
vec![
"A".to_string(),
"B".to_string(),
"C".to_string(),
"D".to_string(),
],
vec![
"B".to_string(),
"A".to_string(),
"C".to_string(),
"D".to_string(),
],
];
let mut arena3 = PowlArena::new();
let result3 = detect_sequence_cut(&complex, &mut arena3, &config);
assert!(
result3.is_ok(),
"Sequence cut should detect SCCs with cycles and total order"
);
}
#[test]
fn test_pw_d2_xor_cut_component_partitioning() {
let alternatives = vec![vec!["A".to_string()], vec!["B".to_string()]];
let mut arena = PowlArena::new();
let config = DiscoveryConfig::default();
let result = detect_xor_cut(&alternatives, &mut arena, &config);
assert!(
result.is_ok(),
"XOR cut should detect disconnected activities"
);
let connected = vec![
vec!["A".to_string(), "B".to_string()],
vec!["A".to_string(), "B".to_string()],
];
let mut arena2 = PowlArena::new();
let result2 = detect_xor_cut(&connected, &mut arena2, &config);
assert!(
result2.is_err(),
"XOR cut should reject single connected component"
);
let mixed = vec![
vec!["A".to_string(), "B".to_string()],
vec!["A".to_string(), "C".to_string()],
];
let mut arena3 = PowlArena::new();
let result3 = detect_xor_cut(&mixed, &mut arena3, &config);
assert!(
result3.is_err(),
"XOR cut should reject when traces mix components"
);
}
#[test]
fn test_pw_d3_loop_cut_do_redo_decomposition() {
let looping = vec![vec![
"A".to_string(),
"B".to_string(),
"A".to_string(),
"B".to_string(),
]];
let mut arena = PowlArena::new();
let config = DiscoveryConfig::default();
let result = detect_loop_cut(&looping, &mut arena, &config);
assert!(
result.is_ok(),
"Loop cut should detect activity repetition pattern"
);
let no_loop = vec![vec!["A".to_string(), "B".to_string(), "C".to_string()]];
let mut arena2 = PowlArena::new();
let result2 = detect_loop_cut(&no_loop, &mut arena2, &config);
assert!(
result2.is_err(),
"Loop cut should reject traces with no repetition"
);
let multi_start_loop = vec![
vec!["A".to_string(), "X".to_string(), "A".to_string()],
vec!["B".to_string(), "Y".to_string(), "B".to_string()],
];
let mut arena3 = PowlArena::new();
let result3 = detect_loop_cut(&multi_start_loop, &mut arena3, &config);
assert!(
result3.is_ok(),
"Loop cut should handle multiple start activities"
);
}
#[test]
fn test_sequence_cut_fitness_simple_log() {
let traces = vec![
vec![
"Register".to_string(),
"Examine".to_string(),
"Pay".to_string(),
],
vec![
"Register".to_string(),
"Examine".to_string(),
"Pay".to_string(),
],
vec![
"Register".to_string(),
"Examine".to_string(),
"Pay".to_string(),
],
];
let mut arena = PowlArena::new();
let config = DiscoveryConfig::default();
let result = detect_sequence_cut(&traces, &mut arena, &config);
assert!(result.is_ok(), "Sequence cut should detect linear traces");
}
#[test]
fn test_xor_cut_fitness_alternative_branches() {
let traces = vec![
vec![
"Start".to_string(),
"ApproveA".to_string(),
"End".to_string(),
],
vec![
"Start".to_string(),
"ApproveB".to_string(),
"End".to_string(),
],
vec![
"Start".to_string(),
"ApproveA".to_string(),
"End".to_string(),
],
];
let mut arena = PowlArena::new();
let config = DiscoveryConfig::default();
let _result = detect_xor_cut(&traces, &mut arena, &config);
}
#[test]
fn test_loop_cut_fitness_with_rework() {
let traces = vec![
vec![
"Submit".to_string(),
"Review".to_string(),
"Submit".to_string(),
"Approve".to_string(),
],
vec![
"Submit".to_string(),
"Review".to_string(),
"Submit".to_string(),
"Approve".to_string(),
],
];
let mut arena = PowlArena::new();
let config = DiscoveryConfig::default();
let result = detect_loop_cut(&traces, &mut arena, &config);
assert!(result.is_ok(), "Loop cut should detect rework patterns");
}
}