use crate::models::PetriNet;
use crate::state::{get_or_init_state, StoredObject};
use serde_json::json;
use std::cmp::Ordering;
use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet};
use wasm_bindgen::prelude::*;
#[derive(Clone, Debug, PartialEq)]
struct AlignmentState {
trace_index: usize, marking: BTreeMap<String, usize>, cost: f64, path: Vec<String>, }
#[derive(Clone)]
struct PriorityAlignmentState {
f_score: f64,
state: AlignmentState,
}
impl PartialEq for PriorityAlignmentState {
fn eq(&self, other: &Self) -> bool {
(self.f_score - other.f_score).abs() < 1e-9 && self.state == other.state
}
}
impl Eq for PriorityAlignmentState {}
impl Ord for PriorityAlignmentState {
fn cmp(&self, other: &Self) -> Ordering {
other
.f_score
.partial_cmp(&self.f_score)
.unwrap_or(Ordering::Equal)
}
}
impl PartialOrd for PriorityAlignmentState {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn can_fire(petri_net: &PetriNet, marking: &BTreeMap<String, usize>, transition_id: &str) -> bool {
for arc in &petri_net.arcs {
if arc.to == transition_id {
let weight = arc.weight.unwrap_or(1);
let current = marking.get(&arc.from).copied().unwrap_or(0);
if current < weight {
return false;
}
}
}
true
}
fn fire_transition(
petri_net: &PetriNet,
marking: &BTreeMap<String, usize>,
transition_id: &str,
) -> Option<BTreeMap<String, usize>> {
if !can_fire(petri_net, marking, transition_id) {
return None;
}
let mut new_marking = marking.clone();
for arc in &petri_net.arcs {
if arc.to == transition_id {
let weight = arc.weight.unwrap_or(1);
*new_marking.entry(arc.from.clone()).or_insert(0) -= weight;
}
}
for arc in &petri_net.arcs {
if arc.from == transition_id {
let weight = arc.weight.unwrap_or(1);
*new_marking.entry(arc.to.clone()).or_default() += weight;
}
}
Some(new_marking)
}
fn heuristic(_trace_len: usize, _current_trace_index: usize) -> f64 {
0.0
}
fn compute_trace_alignment(
trace_activities: &[String],
petri_net: &PetriNet,
sync_cost: f64,
log_move_cost: f64,
model_move_cost: f64,
) -> (f64, Vec<String>, usize, usize, usize) {
let trace_len = trace_activities.len();
let mut open_set = BinaryHeap::new();
let mut closed_set = HashSet::new();
let initial_state = AlignmentState {
trace_index: 0,
marking: petri_net
.initial_marking
.iter()
.map(|(k, v)| (k.clone(), *v))
.collect(),
cost: 0.0,
path: Vec::new(),
};
let h0 = heuristic(trace_len, 0);
open_set.push(PriorityAlignmentState {
f_score: h0,
state: initial_state,
});
let mut best_solution: Option<(f64, Vec<String>, usize, usize, usize)> = None;
let mut iterations = 0;
let max_iterations = 100_000;
while let Some(PriorityAlignmentState { f_score: _, state }) = open_set.pop() {
iterations += 1;
if iterations > max_iterations {
break;
}
let state_key = (state.trace_index, state.marking.clone());
if closed_set.contains(&state_key) {
continue;
}
closed_set.insert(state_key);
if state.trace_index == trace_len
&& (petri_net.final_markings.is_empty()
|| petri_net.final_markings.iter().any(|fm| {
fm.len() == state.marking.len()
&& fm.iter().all(|(k, v)| state.marking.get(k) == Some(v))
}))
{
let (sync_count, log_count, model_count) = count_moves(&state.path);
best_solution = Some((
state.cost,
state.path.clone(),
sync_count,
log_count,
model_count,
));
break;
}
let mut successors = Vec::new();
if state.trace_index < trace_len {
let activity = &trace_activities[state.trace_index];
let mut new_path = state.path.clone();
new_path.push(format!("log:{}", activity));
successors.push((
AlignmentState {
trace_index: state.trace_index + 1,
marking: state.marking.clone(),
cost: state.cost + log_move_cost,
path: new_path,
},
log_move_cost,
));
}
if state.trace_index < trace_len {
let next_activity = &trace_activities[state.trace_index];
for transition in &petri_net.transitions {
if transition.label == *next_activity {
if let Some(new_marking) =
fire_transition(petri_net, &state.marking, &transition.id)
{
let mut new_path = state.path.clone();
new_path.push(format!("sync:{}", next_activity));
successors.push((
AlignmentState {
trace_index: state.trace_index + 1,
marking: new_marking,
cost: state.cost + sync_cost,
path: new_path,
},
sync_cost,
));
}
}
}
}
for transition in &petri_net.transitions {
if let Some(new_marking) = fire_transition(petri_net, &state.marking, &transition.id) {
let move_cost =
if transition.is_invisible.unwrap_or(false) || transition.label.is_empty() {
0.0
} else {
model_move_cost
};
let mut new_path = state.path.clone();
new_path.push(format!("model:{}", transition.label));
successors.push((
AlignmentState {
trace_index: state.trace_index,
marking: new_marking,
cost: state.cost + move_cost,
path: new_path,
},
move_cost,
));
}
}
for (succ_state, _edge_cost) in successors {
let h = heuristic(trace_len, succ_state.trace_index);
let f = succ_state.cost + h;
open_set.push(PriorityAlignmentState {
f_score: f,
state: succ_state,
});
}
}
best_solution.unwrap_or((f64::INFINITY, vec![], 0, 0, 0))
}
fn count_moves(path: &[String]) -> (usize, usize, usize) {
let mut sync = 0;
let mut log = 0;
let mut model = 0;
for move_str in path {
if move_str.starts_with("sync:") {
sync += 1;
} else if move_str.starts_with("log:") {
log += 1;
} else if move_str.starts_with("model:") {
model += 1;
}
}
(sync, log, model)
}
#[wasm_bindgen]
pub fn compute_optimal_alignments(
log_handle: &str,
petri_net_handle: &str,
activity_key: &str,
cost_config_json: &str, ) -> Result<JsValue, JsValue> {
let cost_config: HashMap<String, f64> = serde_json::from_str(cost_config_json)
.map_err(|_| crate::error::js_val("Invalid cost_config_json"))?;
let sync_cost = cost_config.get("sync_cost").copied().unwrap_or(0.0);
let log_move_cost = cost_config.get("log_move_cost").copied().unwrap_or(1.0);
let model_move_cost = cost_config.get("model_move_cost").copied().unwrap_or(1.0);
let petri_net = get_or_init_state().with_petri_net(petri_net_handle, |pn| Ok(pn.clone()))?;
let result_json = get_or_init_state().with_event_log(log_handle, |log| {
let mut alignments: Vec<serde_json::Value> = Vec::new();
let mut total_cost = 0.0;
for trace in &log.traces {
let case_id = trace
.attributes
.get("concept:name")
.and_then(|v| v.as_string())
.unwrap_or("unknown")
.to_string();
let acts: Vec<String> = trace
.events
.iter()
.filter_map(|e| {
e.attributes
.get(activity_key)
.and_then(|v| v.as_string())
.map(str::to_owned)
})
.collect();
let (cost, path, sync_count, log_count, model_count) = compute_trace_alignment(
&acts,
&petri_net,
sync_cost,
log_move_cost,
model_move_cost,
);
if cost.is_finite() {
total_cost += cost;
}
alignments.push(json!({
"case_id": case_id,
"alignment_found": cost.is_finite(),
"cost": if cost.is_finite() { cost } else { -1.0 }, "sync_moves": sync_count,
"log_moves": log_count,
"model_moves": model_count,
"path": path,
}));
}
let finite_count = alignments
.iter()
.filter(|a| a["cost"].as_f64().unwrap_or(-1.0) >= 0.0)
.count();
let avg_cost = if finite_count > 0 {
total_cost / finite_count as f64
} else {
0.0
};
serde_json::to_string(&json!({
"total_traces": log.traces.len(),
"avg_cost": avg_cost,
"alignments": alignments,
}))
.map_err(|e| crate::error::js_val(&e.to_string()))
})?;
Ok(crate::error::js_val(&result_json))
}
#[wasm_bindgen]
pub fn compute_alignments(
log_handle: &str,
dfg_handle: &str,
activity_key: &str,
) -> Result<JsValue, JsValue> {
let edge_map: std::collections::HashMap<(String, String), usize> = get_or_init_state()
.with_object(dfg_handle, |obj| match obj {
Some(StoredObject::DFG(dfg)) => Ok(dfg
.edges
.iter()
.map(|e| ((e.from.clone(), e.to.clone()), e.frequency))
.collect()),
Some(_) => Err(crate::error::js_val("Handle is not a DFG")),
None => Err(crate::error::js_val("DFG handle not found")),
})?;
let start_activities: std::collections::HashSet<String> = get_or_init_state()
.with_dfg(dfg_handle, |dfg| {
Ok(dfg.start_activities.keys().cloned().collect())
})?;
let result_json = get_or_init_state().with_event_log(log_handle, |log| {
let mut alignments: Vec<serde_json::Value> = Vec::new();
for trace in &log.traces {
let case_id = trace
.attributes
.get("concept:name")
.and_then(|v| v.as_string())
.unwrap_or("unknown")
.to_string();
let acts: Vec<String> = trace
.events
.iter()
.filter_map(|e| {
e.attributes
.get(activity_key)
.and_then(|v| v.as_string())
.map(str::to_owned)
})
.collect();
let mut moves: Vec<serde_json::Value> = Vec::new();
let mut sync_count = 0usize;
let mut log_move_count = 0usize;
if acts.is_empty() {
alignments.push(json!({
"case_id": case_id,
"fitness": 1.0,
"moves": moves,
}));
continue;
}
if start_activities.is_empty() || start_activities.contains(&acts[0]) {
moves.push(json!({"type": "sync", "activity": acts[0]}));
sync_count += 1;
} else {
moves.push(json!({"type": "log", "activity": acts[0]}));
log_move_count += 1;
}
for w in acts.windows(2) {
let edge = (w[0].clone(), w[1].clone());
if edge_map.contains_key(&edge) {
moves.push(json!({"type": "sync", "activity": w[1]}));
sync_count += 1;
} else {
moves.push(json!({"type": "log", "activity": w[1]}));
log_move_count += 1;
}
}
let total_moves = sync_count + log_move_count;
let fitness = if total_moves == 0 {
1.0
} else {
sync_count as f64 / total_moves as f64
};
alignments.push(json!({
"case_id": case_id,
"fitness": fitness,
"moves": moves,
}));
}
let avg_fitness = if alignments.is_empty() {
1.0
} else {
alignments
.iter()
.map(|a| a["fitness"].as_f64().unwrap_or(1.0))
.sum::<f64>()
/ alignments.len() as f64
};
serde_json::to_string(&json!({
"total_traces": log.traces.len(),
"avg_fitness": avg_fitness,
"alignments": alignments,
}))
.map_err(|e| crate::error::js_val(&e.to_string()))
})?;
Ok(crate::error::js_val(&result_json))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_count_moves() {
let path = vec![
"sync:A".to_string(),
"sync:B".to_string(),
"log:C".to_string(),
"model:D".to_string(),
];
let (sync, log, model) = count_moves(&path);
assert_eq!(sync, 2);
assert_eq!(log, 1);
assert_eq!(model, 1);
}
#[test]
fn test_count_moves_empty() {
let path: Vec<String> = vec![];
let (sync, log, model) = count_moves(&path);
assert_eq!(sync, 0);
assert_eq!(log, 0);
assert_eq!(model, 0);
}
#[test]
fn test_count_moves_only_sync() {
let path = vec!["sync:A".to_string(), "sync:B".to_string()];
let (sync, log, model) = count_moves(&path);
assert_eq!(sync, 2);
assert_eq!(log, 0);
assert_eq!(model, 0);
}
#[test]
fn test_heuristic_always_zero() {
let h = heuristic(100, 50);
assert_eq!(h, 0.0);
}
#[test]
fn test_alignment_state_equality() {
let state1 = AlignmentState {
trace_index: 5,
marking: BTreeMap::from([("p1".to_string(), 1)]),
cost: 2.0,
path: vec!["sync:A".to_string()],
};
let state2 = AlignmentState {
trace_index: 5,
marking: BTreeMap::from([("p1".to_string(), 1)]),
cost: 2.0, path: vec!["sync:A".to_string()],
};
assert_eq!(state1, state2);
let state3 = AlignmentState {
trace_index: 5,
marking: BTreeMap::from([("p1".to_string(), 1)]),
cost: 3.0, path: vec!["sync:A".to_string()],
};
assert_ne!(state1, state3);
}
}