use wasm4pm_compat::powl::{ChoiceGraph, ChoiceGraphNode};
use std::collections::{HashMap, HashSet};
fn is_reachable(dfg: &HashSet<(String, String)>, from: &str, to: &str) -> bool {
if from == to {
return true;
}
let mut visited = HashSet::new();
let mut queue = vec![from.to_string()];
while let Some(current) = queue.pop() {
if visited.contains(¤t) {
continue;
}
visited.insert(current.clone());
for (src, tgt) in dfg {
if src == ¤t && tgt == to {
return true;
}
if src == ¤t && !visited.contains(tgt) {
queue.push(tgt.clone());
}
}
}
false
}
fn find_cycles(
dfg: &HashSet<(String, String)>,
activities: &HashSet<String>,
) -> Vec<(String, String)> {
let mut cycles = Vec::new();
for a1 in activities {
for a2 in activities {
if a1 != a2 && is_reachable(dfg, a1, a2) && is_reachable(dfg, a2, a1) {
cycles.push((a1.clone(), a2.clone()));
}
}
}
cycles
}
struct UnionFind {
parent: HashMap<String, String>,
}
impl UnionFind {
fn new(activities: &HashSet<String>) -> Self {
let parent = activities.iter().map(|a| (a.clone(), a.clone())).collect();
UnionFind { parent }
}
fn find(&mut self, x: &str) -> String {
let mut curr = x.to_string();
while let Some(p) = self.parent.get(&curr) {
if p == &curr {
break;
}
curr = p.clone();
}
if curr != x {
self.parent.insert(x.to_string(), curr.clone());
}
curr
}
fn union(&mut self, x: &str, y: &str) {
let root_x = self.find(x);
let root_y = self.find(y);
if root_x != root_y {
self.parent.insert(root_x, root_y);
}
}
fn get_partitions(&mut self) -> Vec<HashSet<String>> {
let mut partitions: HashMap<String, HashSet<String>> = HashMap::new();
let keys: Vec<String> = self.parent.keys().cloned().collect();
for k in keys {
let root = self.find(&k);
partitions.entry(root).or_default().insert(k);
}
partitions.into_values().collect()
}
}
fn build_partition_edges(
dfg: &HashSet<(String, String)>,
partitions: &[HashSet<String>],
) -> HashSet<(usize, usize)> {
let mut edges = HashSet::new();
for (i, partition_i) in partitions.iter().enumerate() {
for (j, partition_j) in partitions.iter().enumerate() {
if i != j {
for a_i in partition_i {
for a_j in partition_j {
if is_reachable(dfg, a_i, a_j) {
edges.insert((i, j));
break;
}
}
if edges.contains(&(i, j)) {
break;
}
}
}
}
}
edges
}
pub fn discover_choice_graph(
dfg: &HashSet<(String, String)>,
activities: &HashSet<String>,
_start_activities: &HashSet<String>,
_end_activities: &HashSet<String>,
_has_empty_trace: bool,
) -> Option<(Vec<HashSet<String>>, HashSet<(usize, usize)>)> {
let mut uf = UnionFind::new(activities);
let cycles = find_cycles(dfg, activities);
for (a1, a2) in cycles {
uf.union(&a1, &a2);
}
let partitions = uf.get_partitions();
if partitions.len() <= 1 {
return None;
}
let edges = build_partition_edges(dfg, &partitions);
Some((partitions, edges))
}
#[derive(Clone, Debug)]
pub struct ChoiceGraphCut {
pub partition: Vec<HashSet<String>>,
pub graph: ChoiceGraph,
pub partition_for_node: Vec<Option<usize>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NoCutFound {
InsufficientPartitions,
InvalidGraph(String),
}
impl core::fmt::Display for NoCutFound {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
NoCutFound::InsufficientPartitions => {
write!(f, "MineDG produced < 2 partitions; no choice graph cut")
}
NoCutFound::InvalidGraph(s) => write!(f, "invalid choice graph: {}", s),
}
}
}
impl std::error::Error for NoCutFound {}
pub fn discover_choice_graph_v2(
activities: &HashSet<String>,
dfg: &HashSet<(String, String)>,
start_activities: &HashSet<String>,
end_activities: &HashSet<String>,
has_empty_trace: bool,
) -> Result<ChoiceGraphCut, NoCutFound> {
let mut uf = UnionFind::new(activities);
let cycles = find_cycles(dfg, activities);
for (a1, a2) in cycles {
uf.union(&a1, &a2);
}
let partition = uf.get_partitions();
if partition.len() < 2 {
return Err(NoCutFound::InsufficientPartitions);
}
let mut nodes: Vec<ChoiceGraphNode> = Vec::new();
let mut partition_for_node: Vec<Option<usize>> = Vec::new();
nodes.push(ChoiceGraphNode::Start);
partition_for_node.push(None);
let start_idx_node = 0usize;
let mut part_node_idx: Vec<usize> = Vec::with_capacity(partition.len());
for (p_idx, part) in partition.iter().enumerate() {
let repr = part
.iter()
.min()
.cloned()
.unwrap_or_else(|| format!("part_{}", p_idx));
part_node_idx.push(nodes.len());
nodes.push(ChoiceGraphNode::Activity(repr));
partition_for_node.push(Some(p_idx));
}
let end_idx_node = nodes.len();
nodes.push(ChoiceGraphNode::End);
partition_for_node.push(None);
let mut edges: Vec<(usize, usize)> = Vec::new();
let mut edge_set: HashSet<(usize, usize)> = HashSet::new();
if has_empty_trace {
edges.push((start_idx_node, end_idx_node));
edge_set.insert((start_idx_node, end_idx_node));
}
for (i, part_i) in partition.iter().enumerate() {
let ni = part_node_idx[i];
if part_i.iter().any(|a| start_activities.contains(a)) {
let e = (start_idx_node, ni);
if edge_set.insert(e) {
edges.push(e);
}
}
if part_i.iter().any(|a| end_activities.contains(a)) {
let e = (ni, end_idx_node);
if edge_set.insert(e) {
edges.push(e);
}
}
for (j, part_j) in partition.iter().enumerate() {
if i == j {
continue;
}
let nj = part_node_idx[j];
let has_dfg_edge = part_i
.iter()
.any(|a| part_j.iter().any(|b| dfg.contains(&(a.clone(), b.clone()))));
if has_dfg_edge {
let e = (ni, nj);
if edge_set.insert(e) {
edges.push(e);
}
}
}
}
let graph = ChoiceGraph::new(nodes, edges);
Ok(ChoiceGraphCut {
partition,
graph,
partition_for_node,
})
}
#[cfg(test)]
mod v2_tests {
use super::*;
use std::collections::HashSet;
#[test]
fn linear_chain_yields_3_part_cut() {
let activities: HashSet<String> = ["A", "B", "C"].iter().map(|s| s.to_string()).collect();
let dfg: HashSet<(String, String)> = [
("A".to_string(), "B".to_string()),
("B".to_string(), "C".to_string()),
]
.iter()
.cloned()
.collect();
let starts: HashSet<String> = ["A"].iter().map(|s| s.to_string()).collect();
let ends: HashSet<String> = ["C"].iter().map(|s| s.to_string()).collect();
let cut = discover_choice_graph_v2(&activities, &dfg, &starts, &ends, false)
.expect("expected a cut");
assert_eq!(cut.partition.len(), 3);
assert!(cut.graph.has_empty_path() == false);
}
#[test]
fn empty_trace_adds_direct_start_end_edge() {
let activities: HashSet<String> = ["A"].iter().map(|s| s.to_string()).collect();
let dfg: HashSet<(String, String)> = HashSet::new();
let starts: HashSet<String> = ["A"].iter().map(|s| s.to_string()).collect();
let ends: HashSet<String> = ["A"].iter().map(|s| s.to_string()).collect();
let err = discover_choice_graph_v2(&activities, &dfg, &starts, &ends, true).unwrap_err();
assert_eq!(err, NoCutFound::InsufficientPartitions);
}
}