use crate::models::EventLog;
use crate::models::PetriNet;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use wasm_bindgen::prelude::*;
pub type Marking = std::collections::BTreeMap<String, usize>;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PrecisionResult {
pub precision: f64,
pub total_escaping: u32,
pub total_consumed: u32,
pub total_traces: usize,
}
fn is_invisible(net: &PetriNet, trans_id: &str) -> bool {
net.transitions
.iter()
.find(|t| t.id == trans_id)
.and_then(|t| t.is_invisible)
.unwrap_or(false)
}
fn transition_has_label(net: &PetriNet, trans_id: &str, label: &str) -> bool {
net.transitions
.iter()
.find(|t| t.id == trans_id)
.is_some_and(|t| !t.label.is_empty() && t.label == label)
}
fn is_place(net: &PetriNet, name: &str) -> bool {
net.places.iter().any(|p| p.id == name)
}
fn preset(net: &PetriNet, trans_id: &str) -> Vec<String> {
net.arcs
.iter()
.filter(|a| a.to == trans_id)
.filter(|a| is_place(net, &a.from))
.map(|a| a.from.clone())
.collect()
}
fn postset(net: &PetriNet, trans_id: &str) -> Vec<String> {
net.arcs
.iter()
.filter(|a| a.from == trans_id)
.filter(|a| is_place(net, &a.to))
.map(|a| a.to.clone())
.collect()
}
fn is_enabled(marking: &Marking, pre: &[String]) -> bool {
pre.iter().all(|p| marking.get(p).copied().unwrap_or(0) > 0)
}
fn fire(marking: &mut Marking, pre: &[String], post: &[String]) {
for p in pre {
let entry = marking.entry(p.clone()).or_insert(0);
*entry = entry.saturating_sub(1);
}
for p in post {
*marking.entry(p.clone()).or_default() += 1;
}
}
fn fire_silent_enabled(net: &PetriNet, marking: &mut Marking) {
let budget = net.transitions.len() * 4 + 16;
let mut remaining = budget;
loop {
if remaining == 0 {
break;
}
let mut fired = false;
for trans in &net.transitions {
if !is_invisible(net, &trans.id) {
continue;
}
let pre = preset(net, &trans.id);
if !pre.is_empty() && is_enabled(marking, &pre) {
let post = postset(net, &trans.id);
fire(marking, &pre, &post);
remaining -= 1;
fired = true;
break; }
}
if !fired {
break;
}
}
}
fn event_activity(event: &crate::models::Event, activity_key: &str) -> Option<String> {
event
.attributes
.get(activity_key)
.and_then(|v| v.as_string())
.map(|s| s.to_string())
}
fn precision_for_trace(
net: &PetriNet,
initial_marking: &Marking,
final_marking: &Marking,
trace: &crate::models::Trace,
activity_key: &str,
) -> (u32, u32) {
let mut marking: Marking = initial_marking.clone();
let mut consumed: u32 = 0;
let mut escaping: u32 = 0;
fire_silent_enabled(net, &mut marking);
for event in &trace.events {
let Some(activity) = event_activity(event, activity_key) else {
continue;
};
let visible_candidates: Vec<String> = net
.transitions
.iter()
.filter(|t| transition_has_label(net, &t.id, &activity))
.map(|t| t.id.clone())
.collect();
if visible_candidates.is_empty() {
continue;
}
let chosen = if let Some(t) = visible_candidates.iter().find(|t| {
let pre = preset(net, t);
is_enabled(&marking, &pre)
}) {
t.clone()
} else {
let first = &visible_candidates[0];
for p in &preset(net, first) {
let have = marking.get(p).copied().unwrap_or(0);
if have == 0 {
*marking.entry(p.clone()).or_default() += 1;
}
}
first.clone()
};
let pre = preset(net, &chosen);
let post = postset(net, &chosen);
fire(&mut marking, &pre, &post);
consumed += pre.len() as u32;
fire_silent_enabled(net, &mut marking);
for trans in &net.transitions {
let trans_pre = preset(net, &trans.id);
if !trans_pre.is_empty() && is_enabled(&marking, &trans_pre) {
if !transition_has_label(net, &trans.id, &activity) {
escaping += trans_pre.len() as u32;
}
}
}
}
let final_consumed: u32 = final_marking.values().map(|&v| v as u32).sum();
consumed += final_consumed;
(escaping, consumed)
}
pub fn compute_precision(
net: &PetriNet,
initial_marking: &Marking,
final_marking: &Marking,
log: &EventLog,
activity_key: &str,
) -> PrecisionResult {
let mut total_escaping: u32 = 0;
let mut total_consumed: u32 = 0;
let total_traces = log.traces.len();
for trace in &log.traces {
let (escaping, consumed) =
precision_for_trace(net, initial_marking, final_marking, trace, activity_key);
total_escaping += escaping;
total_consumed += consumed;
}
let precision = if total_consumed == 0 && total_escaping == 0 {
1.0
} else {
let e = total_escaping as f64;
let c = total_consumed as f64;
(1.0 - e / (e + c)).clamp(0.0, 1.0)
};
PrecisionResult {
precision,
total_escaping,
total_consumed,
total_traces,
}
}
#[wasm_bindgen]
pub fn wasm_compute_precision(
eventlog_handle: &str,
petri_net_handle: &str,
activity_key: &str,
) -> Result<String, JsValue> {
use crate::state::{get_or_init_state, StoredObject};
let petri_net: Result<Option<PetriNet>, _> =
get_or_init_state().with_object(petri_net_handle, |obj| match obj {
Some(StoredObject::PetriNet(net)) => Ok(Some(net.clone())),
Some(_) => Ok(None),
None => Ok(None),
});
let Ok(Some(net)) = petri_net else {
return Err(crate::error::js_val(&format!(
r#"{{"error":"PetriNet '{}' not found or wrong type"}}"#,
petri_net_handle
)));
};
let initial_marking: Marking = net
.places
.iter()
.filter_map(|p| p.marking.map(|m| (p.id.clone(), m)))
.collect();
let final_marking: Marking = net
.final_markings
.first()
.cloned()
.ok_or_else(|| crate::error::js_val("No final marking defined in Petri net"))?;
let result = get_or_init_state().with_object(eventlog_handle, |obj| match obj {
Some(StoredObject::EventLog(log)) => {
let precision =
compute_precision(&net, &initial_marking, &final_marking, log, activity_key);
serde_json::to_string(&precision).map_err(|e| {
crate::error::js_val(&format!("Failed to serialize precision result: {}", e))
})
}
Some(_) => Err(crate::error::js_val("Object is not an EventLog")),
None => Err(crate::error::js_val(&format!(
"EventLog '{}' not found",
eventlog_handle
))),
})?;
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{AttributeValue, PetriNetArc, PetriNetPlace, PetriNetTransition};
fn sequential_net() -> (PetriNet, Marking, Marking) {
let mut net = PetriNet::new();
net.places.push(PetriNetPlace {
id: "p_start".into(),
label: "p_start".into(),
marking: Some(1),
});
net.places.push(PetriNetPlace {
id: "p1".into(),
label: "p1".into(),
marking: None,
});
net.places.push(PetriNetPlace {
id: "p_end".into(),
label: "p_end".into(),
marking: None,
});
net.transitions.push(PetriNetTransition {
id: "t_A".into(),
label: "A".into(),
is_invisible: Some(false),
});
net.transitions.push(PetriNetTransition {
id: "t_B".into(),
label: "B".into(),
is_invisible: Some(false),
});
net.arcs.push(PetriNetArc {
from: "p_start".into(),
to: "t_A".into(),
weight: Some(1),
});
net.arcs.push(PetriNetArc {
from: "t_A".into(),
to: "p1".into(),
weight: Some(1),
});
net.arcs.push(PetriNetArc {
from: "p1".into(),
to: "t_B".into(),
weight: Some(1),
});
net.arcs.push(PetriNetArc {
from: "t_B".into(),
to: "p_end".into(),
weight: Some(1),
});
let mut initial = Marking::default();
initial.insert("p_start".into(), 1);
let mut final_m = Marking::default();
final_m.insert("p_end".into(), 1);
(net, initial, final_m)
}
fn make_log(activity_key: &str, cases: &[&[&str]]) -> EventLog {
EventLog {
attributes: BTreeMap::new(),
traces: cases
.iter()
.enumerate()
.map(|(_, acts)| crate::models::Trace {
attributes: BTreeMap::new(),
events: acts
.iter()
.map(|&a| {
let mut attrs = std::collections::BTreeMap::new();
attrs.insert(
activity_key.to_string(),
AttributeValue::String(a.to_string()),
);
crate::models::Event { attributes: attrs }
})
.collect(),
})
.collect(),
}
}
#[test]
fn test_perfect_log_high_precision() {
let (net, initial, final_m) = sequential_net();
let log = make_log("concept:name", &[&["A", "B"], &["A", "B"]]);
let result = compute_precision(&net, &initial, &final_m, &log, "concept:name");
assert!(result.precision >= 0.5);
}
#[test]
fn test_precision_between_zero_and_one() {
let (net, initial, final_m) = sequential_net();
let log = make_log("concept:name", &[&["A", "B"]]);
let result = compute_precision(&net, &initial, &final_m, &log, "concept:name");
assert!(result.precision >= 0.0);
assert!(result.precision <= 1.0);
}
#[test]
fn test_empty_log_returns_one() {
let (net, initial, final_m) = sequential_net();
let log = make_log("concept:name", &[]);
let result = compute_precision(&net, &initial, &final_m, &log, "concept:name");
assert!((result.precision - 1.0).abs() < 1e-9);
assert_eq!(result.total_escaping, 0);
assert_eq!(result.total_consumed, 0);
assert_eq!(result.total_traces, 0);
}
#[test]
fn test_single_trace_count() {
let (net, initial, final_m) = sequential_net();
let log = make_log("concept:name", &[&["A", "B"]]);
let result = compute_precision(&net, &initial, &final_m, &log, "concept:name");
assert_eq!(result.total_traces, 1);
}
#[test]
fn test_precision_result_serialization() {
let result = PrecisionResult {
precision: 0.75,
total_escaping: 10,
total_consumed: 30,
total_traces: 5,
};
let json = serde_json::to_string(&result).unwrap();
let parsed: PrecisionResult = serde_json::from_str(&json).unwrap();
assert!((parsed.precision - 0.75).abs() < 1e-9);
assert_eq!(parsed.total_escaping, 10);
assert_eq!(parsed.total_consumed, 30);
assert_eq!(parsed.total_traces, 5);
}
#[test]
fn test_unknown_activity_skipped() {
let (net, initial, final_m) = sequential_net();
let log = make_log("concept:name", &[&["X", "Y"]]);
let result = compute_precision(&net, &initial, &final_m, &log, "concept:name");
assert!(result.total_consumed > 0);
}
#[test]
fn test_silent_transition_firing() {
let mut net = PetriNet::new();
net.places.push(PetriNetPlace {
id: "p1".into(),
label: "p1".into(),
marking: Some(1),
});
net.places.push(PetriNetPlace {
id: "p2".into(),
label: "p2".into(),
marking: None,
});
net.places.push(PetriNetPlace {
id: "p3".into(),
label: "p3".into(),
marking: None,
});
net.transitions.push(PetriNetTransition {
id: "tau".into(),
label: String::new(),
is_invisible: Some(true),
});
net.transitions.push(PetriNetTransition {
id: "tA".into(),
label: "A".into(),
is_invisible: Some(false),
});
net.arcs.push(PetriNetArc {
from: "p1".into(),
to: "tau".into(),
weight: Some(1),
});
net.arcs.push(PetriNetArc {
from: "tau".into(),
to: "p2".into(),
weight: Some(1),
});
net.arcs.push(PetriNetArc {
from: "p2".into(),
to: "tA".into(),
weight: Some(1),
});
net.arcs.push(PetriNetArc {
from: "tA".into(),
to: "p3".into(),
weight: Some(1),
});
let mut initial = Marking::default();
initial.insert("p1".into(), 1);
let final_m = Marking::default();
let log = make_log("concept:name", &[&["A"]]);
let result = compute_precision(&net, &initial, &final_m, &log, "concept:name");
assert!(result.total_consumed > 0);
assert!(result.precision >= 0.0);
assert!(result.precision <= 1.0);
}
#[test]
fn test_preset_postset_helpers() {
let (net, _, _) = sequential_net();
let pre = preset(&net, "t_A");
assert_eq!(pre, vec!["p_start"]);
let post = postset(&net, "t_A");
assert_eq!(post, vec!["p1"]);
}
#[test]
fn test_is_enabled() {
let mut marking = Marking::default();
marking.insert("p1".into(), 1);
assert!(is_enabled(&marking, &["p1".to_string()]));
assert!(!is_enabled(&marking, &["p2".to_string()]));
assert!(is_enabled(&marking, &[]));
}
#[test]
fn test_fire_transition() {
let mut marking = Marking::default();
marking.insert("p1".into(), 1);
marking.insert("p2".into(), 0);
fire(&mut marking, &["p1".to_string()], &["p2".to_string()]);
assert_eq!(marking.get("p1").copied().unwrap_or(0), 0);
assert_eq!(marking.get("p2").copied().unwrap_or(0), 1);
}
}