use crate::models::*;
use crate::state::{get_or_init_state, StoredObject};
use crate::utilities::to_js_str;
use rustc_hash::FxHashMap;
use serde_json::json;
use std::collections::HashSet;
use wasm_bindgen::prelude::*;
struct CandidatePlace {
input_acts: Vec<u32>,
output_acts: Vec<u32>,
}
pub fn compute_simplicity(places: usize, transitions: usize, arcs: usize) -> f64 {
if places == 0 || transitions == 0 || arcs == 0 {
return 1.0; }
let n = transitions.saturating_sub(1).max(1); let min_places = n + 1;
let min_transitions = n;
let min_arcs = 2 * n;
let place_ratio = (min_places as f64 / places as f64).min(1.0);
let transition_ratio = (min_transitions as f64 / transitions as f64).min(1.0);
let arc_ratio = (min_arcs as f64 / arcs as f64).min(1.0);
(place_ratio * transition_ratio * arc_ratio).cbrt()
}
#[wasm_bindgen]
pub fn wasm_compute_simplicity(places: usize, transitions: usize, arcs: usize) -> f64 {
compute_simplicity(places, transitions, arcs)
}
pub fn discover_ilp_petri_net_from_log(log: &EventLog, activity_key: &str) -> (PetriNet, f64, f64) {
let col_owned = log.to_columnar_owned(activity_key);
let col = ColumnarLog::from_owned(&col_owned);
let n = col.vocab.len() as u32;
if n == 0 || col.trace_offsets.len() <= 1 {
return (PetriNet::new(), 0.0, 0.0);
}
let mut df: FxHashMap<(u32, u32), usize> = FxHashMap::default();
let mut start_acts: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
let mut end_acts: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
let trace_count = col.trace_offsets.len() - 1;
for t in 0..trace_count {
let s = col.trace_offsets[t];
let e = col.trace_offsets[t + 1];
let trace = &col.events[s..e];
if trace.is_empty() {
continue;
}
start_acts.insert(trace[0]);
end_acts.insert(*trace.last().unwrap());
for w in trace.windows(2) {
*df.entry((w[0], w[1])).or_default() += 1;
}
}
let mut causal_pairs: Vec<(u32, u32)> = Vec::new();
let mut loop1_acts: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
let mut parallel_pairs: HashSet<(u32, u32)> = HashSet::new();
for &(a, b) in df.keys() {
if a == b {
loop1_acts.insert(a);
} else if df.contains_key(&(b, a)) {
if a < b {
parallel_pairs.insert((a, b));
parallel_pairs.insert((b, a));
}
} else {
causal_pairs.push((a, b));
}
}
causal_pairs.sort_unstable();
let mut candidates: Vec<CandidatePlace> = Vec::new();
for &(a, b) in &causal_pairs {
candidates.push(CandidatePlace {
input_acts: vec![a],
output_acts: vec![b],
});
}
let mut causes_of: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
for &(a, b) in &causal_pairs {
causes_of.entry(a).or_default().push(b);
}
for (&a, outputs) in &causes_of {
for i in 0..outputs.len() {
for j in i + 1..outputs.len() {
let b = outputs[i];
let c = outputs[j];
if parallel_pairs.contains(&(b, c)) {
let (lo, hi) = if b <= c { (b, c) } else { (c, b) };
candidates.push(CandidatePlace {
input_acts: vec![a],
output_acts: vec![lo, hi],
});
}
}
}
}
let mut caused_by: FxHashMap<u32, Vec<u32>> = FxHashMap::default();
for &(a, b) in &causal_pairs {
caused_by.entry(b).or_default().push(a);
}
for (&c, inputs) in &caused_by {
for i in 0..inputs.len() {
for j in i + 1..inputs.len() {
let a = inputs[i];
let b = inputs[j];
if parallel_pairs.contains(&(a, b)) {
let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
candidates.push(CandidatePlace {
input_acts: vec![lo, hi],
output_acts: vec![c],
});
}
}
}
}
let valid_candidates: Vec<CandidatePlace> = candidates
.into_iter()
.filter(|place| {
let in_set: HashSet<u32> = place.input_acts.iter().copied().collect();
let out_set: HashSet<u32> = place.output_acts.iter().copied().collect();
let mut fires_ever = false;
for t in 0..trace_count {
let s = col.trace_offsets[t];
let e = col.trace_offsets[t + 1];
let mut tokens: i64 = 0;
for &ev in &col.events[s..e] {
if in_set.contains(&ev) {
tokens += 1;
fires_ever = true;
}
if out_set.contains(&ev) {
tokens -= 1;
if tokens < 0 {
return false; }
}
}
}
fires_ever
})
.collect();
let causal_set: HashSet<(u32, u32)> = causal_pairs.iter().copied().collect();
let selected = ilp_greedy_cover(valid_candidates, &causal_set);
build_ilp_petri_net(
&selected,
&col,
log,
activity_key,
&start_acts,
&end_acts,
&loop1_acts,
)
}
fn ilp_greedy_cover(
candidates: Vec<CandidatePlace>,
causal_pairs: &HashSet<(u32, u32)>,
) -> Vec<CandidatePlace> {
let mut uncovered: HashSet<(u32, u32)> = causal_pairs.clone();
let mut remaining = candidates;
let mut selected: Vec<CandidatePlace> = Vec::new();
while !uncovered.is_empty() && !remaining.is_empty() {
let best_idx = remaining
.iter()
.enumerate()
.max_by_key(|(_, c)| {
let in_set: HashSet<u32> = c.input_acts.iter().copied().collect();
let out_set: HashSet<u32> = c.output_acts.iter().copied().collect();
uncovered
.iter()
.filter(|(a, b)| in_set.contains(a) && out_set.contains(b))
.count()
})
.map(|(i, _)| i);
if let Some(idx) = best_idx {
let candidate = remaining.remove(idx);
let in_set: HashSet<u32> = candidate.input_acts.iter().copied().collect();
let out_set: HashSet<u32> = candidate.output_acts.iter().copied().collect();
let covers_any = uncovered
.iter()
.any(|(a, b)| in_set.contains(a) && out_set.contains(b));
if covers_any {
uncovered.retain(|(a, b)| !(in_set.contains(a) && out_set.contains(b)));
selected.push(candidate);
} else {
break;
}
} else {
break;
}
}
selected
}
fn build_ilp_petri_net(
selected: &[CandidatePlace],
col: &ColumnarLog<'_>,
log: &EventLog,
activity_key: &str,
start_acts: &std::collections::BTreeSet<u32>,
end_acts: &std::collections::BTreeSet<u32>,
loop1_acts: &std::collections::BTreeSet<u32>,
) -> (PetriNet, f64, f64) {
let mut petri_net = PetriNet::new();
let mut act_to_trans: FxHashMap<u32, String> = FxHashMap::default();
for (id, &name) in col.vocab.iter().enumerate() {
let trans_id = format!("t_{}", name);
act_to_trans.insert(id as u32, trans_id.clone());
petri_net.transitions.push(PetriNetTransition {
id: trans_id,
label: name.to_string(),
is_invisible: Some(false),
});
}
let source = "p_source".to_string();
let sink = "p_sink".to_string();
petri_net.places.push(PetriNetPlace {
id: source.clone(),
label: "source".to_string(),
marking: Some(1),
});
petri_net.places.push(PetriNetPlace {
id: sink.clone(),
label: "sink".to_string(),
marking: Some(0),
});
petri_net.initial_marking.insert(source.clone(), 1);
petri_net
.final_markings
.push(std::collections::BTreeMap::from([(sink.clone(), 1)]));
for &sa in start_acts {
if let Some(t) = act_to_trans.get(&sa) {
petri_net.arcs.push(PetriNetArc {
from: source.clone(),
to: t.clone(),
weight: Some(1),
});
}
}
for &ea in end_acts {
if let Some(t) = act_to_trans.get(&ea) {
petri_net.arcs.push(PetriNetArc {
from: t.clone(),
to: sink.clone(),
weight: Some(1),
});
}
}
for &a in loop1_acts {
if let Some(t) = act_to_trans.get(&a) {
if let Some(name) = col.vocab.get(a as usize) {
let pid = format!("p_loop_{}", name);
petri_net.places.push(PetriNetPlace {
id: pid.clone(),
label: format!("loop_{}", name),
marking: Some(0),
});
petri_net.arcs.push(PetriNetArc {
from: t.clone(),
to: pid.clone(),
weight: Some(1),
});
petri_net.arcs.push(PetriNetArc {
from: pid,
to: t.clone(),
weight: Some(1),
});
}
}
}
for (idx, place) in selected.iter().enumerate() {
let pid = format!("p{}", idx);
let input_labels: Vec<String> = place
.input_acts
.iter()
.filter_map(|&a| col.vocab.get(a as usize).map(|s| s.to_string()))
.collect();
let output_labels: Vec<String> = place
.output_acts
.iter()
.filter_map(|&a| col.vocab.get(a as usize).map(|s| s.to_string()))
.collect();
let label = format!("{}->{}", input_labels.join(","), output_labels.join(","));
petri_net.places.push(PetriNetPlace {
id: pid.clone(),
label,
marking: Some(0),
});
for &in_act in &place.input_acts {
if let Some(t) = act_to_trans.get(&in_act) {
petri_net.arcs.push(PetriNetArc {
from: t.clone(),
to: pid.clone(),
weight: Some(1),
});
}
}
for &out_act in &place.output_acts {
if let Some(t) = act_to_trans.get(&out_act) {
petri_net.arcs.push(PetriNetArc {
from: pid.clone(),
to: t.clone(),
weight: Some(1),
});
}
}
}
let conformance = crate::conformance::token_replay_pure(log, &petri_net, activity_key);
let fitness = conformance.avg_fitness;
let precision = calculate_precision(&petri_net, log, activity_key);
(petri_net, fitness, precision)
}
#[wasm_bindgen]
pub fn discover_ilp_petri_net(
eventlog_handle: &str,
activity_key: &str,
) -> Result<JsValue, JsValue> {
let log_owned = get_or_init_state().with_event_log(eventlog_handle, |log| Ok(log.clone()))?;
let (petri_net, fitness, precision) = discover_ilp_petri_net_from_log(&log_owned, activity_key);
let simplicity = compute_simplicity(
petri_net.places.len(),
petri_net.transitions.len(),
petri_net.arcs.len(),
);
let handle = get_or_init_state()
.store_object(StoredObject::PetriNet(petri_net.clone()))
.map_err(|_e| crate::error::js_val("Failed to store Petri net"))?;
to_js_str(&json!({
"handle": handle,
"algorithm": "ilp_petri_net",
"places": petri_net.places.len(),
"transitions": petri_net.transitions.len(),
"arcs": petri_net.arcs.len(),
"fitness": fitness,
"precision": precision,
"simplicity": simplicity,
"f_measure": 2.0 * (fitness * precision) / (fitness + precision + 0.001),
}))
}
pub fn discover_optimized_dfg_from_log(
log: &EventLog,
activity_key: &str,
fitness_weight: f64,
simplicity_weight: f64,
) -> DFG {
let activities = log.get_activities(activity_key);
let mut dfg = DFG::new();
for activity in &activities {
dfg.nodes.push(DFGNode {
id: activity.clone(),
label: activity.clone(),
frequency: 0,
});
}
let node_index: FxHashMap<&str, usize> = activities
.iter()
.enumerate()
.map(|(i, a)| (a.as_str(), i))
.collect();
let mut edge_counts: FxHashMap<(String, String), usize> = FxHashMap::default();
for trace in &log.traces {
for event in &trace.events {
if let Some(AttributeValue::String(activity)) = event.attributes.get(activity_key) {
if let Some(&idx) = node_index.get(activity.as_str()) {
dfg.nodes[idx].frequency += 1;
}
}
}
for window in trace.events.windows(2) {
if let (Some(AttributeValue::String(act1)), Some(AttributeValue::String(act2))) = (
window[0].attributes.get(activity_key),
window[1].attributes.get(activity_key),
) {
*edge_counts.entry((act1.clone(), act2.clone())).or_default() += 1;
}
}
}
let max_freq = edge_counts.values().max().copied().unwrap_or(1);
for ((from, to), count) in edge_counts {
let normalized_freq = count as f64 / max_freq as f64;
let score = (fitness_weight * normalized_freq) - (simplicity_weight * 0.1);
if score > 0.1 {
dfg.edges.push(DirectlyFollowsRelation {
from,
to,
frequency: count,
});
}
}
for trace in &log.traces {
if !trace.events.is_empty() {
if let Some(AttributeValue::String(first_act)) =
trace.events[0].attributes.get(activity_key)
{
*dfg.start_activities.entry(first_act.clone()).or_default() += 1;
}
if let Some(AttributeValue::String(last_act)) = trace.events[trace.events.len() - 1]
.attributes
.get(activity_key)
{
*dfg.end_activities.entry(last_act.clone()).or_default() += 1;
}
}
}
dfg
}
#[wasm_bindgen]
pub fn discover_optimized_dfg(
eventlog_handle: &str,
activity_key: &str,
fitness_weight: f64,
simplicity_weight: f64,
) -> Result<JsValue, JsValue> {
let log = get_or_init_state().with_event_log(eventlog_handle, |log| Ok(log.clone()))?;
let dfg =
discover_optimized_dfg_from_log(&log, activity_key, fitness_weight, simplicity_weight);
let n_nodes = dfg.nodes.len();
let n_edges = dfg.edges.len();
let handle = get_or_init_state()
.store_object(StoredObject::DFG(dfg))
.map_err(|_e| crate::error::js_val("Failed to store DFG"))?;
to_js_str(&json!({
"handle": handle,
"algorithm": "optimized_dfg",
"nodes": n_nodes,
"edges": n_edges,
"fitness_weight": fitness_weight,
"simplicity_weight": simplicity_weight,
}))
}
#[inline]
fn calculate_precision(petri_net: &PetriNet, log: &EventLog, activity_key: &str) -> f64 {
let log_activities: HashSet<String> = log
.traces
.iter()
.flat_map(|trace| {
trace.events.iter().filter_map(|e| {
e.attributes
.get(activity_key)?
.as_string()
.map(str::to_owned)
})
})
.collect();
let model_activities: HashSet<String> = petri_net
.transitions
.iter()
.filter(|t| !t.is_invisible.unwrap_or(false))
.map(|t| t.label.clone())
.collect();
if model_activities.is_empty() {
return 0.0;
}
let covered = log_activities.intersection(&model_activities).count();
covered as f64 / model_activities.len() as f64
}
#[wasm_bindgen]
pub fn ilp_discovery_info() -> String {
json!({
"status": "ilp_discovery_available",
"algorithms": [
{
"name": "discover_ilp_petri_net",
"description": "Finds optimal Petri net using constraint-based optimization",
"parameters": ["activity_key"],
"returns": ["fitness", "precision", "simplicity", "f_measure"],
"better_for": "Finding optimal process models with balanced fit and complexity"
},
{
"name": "discover_optimized_dfg",
"description": "Discovers DFG with weighted fitness-simplicity optimization",
"parameters": ["activity_key", "fitness_weight", "simplicity_weight"],
"returns": ["nodes", "edges"],
"better_for": "Balancing detail and readability based on importance weights"
}
]
})
.to_string()
}