use anyhow::Context;
use std::collections::BTreeMap;
use wasm4pm::align_etconformance::{
compute_align_etconformance_precision, AlignETConformanceConfig,
};
use wasm4pm::conformance::token_replay_pure;
use wasm4pm::models::{
self as wm, AttributeValue as WmAttributeValue, PetriNet, PetriNetArc, PetriNetPlace,
PetriNetTransition,
};
use wasm4pm_compat::event_log::{
AttributeValue as CompatAttributeValue, EventLog as CompatEventLog,
};
use wasm4pm_compat::models::DFG;
const START_PLACE: &str = "__bridge_start__";
fn convert_attribute_value(v: &CompatAttributeValue) -> Option<WmAttributeValue> {
match v {
CompatAttributeValue::String(s) => Some(WmAttributeValue::String(s.clone())),
CompatAttributeValue::Int(i) => Some(WmAttributeValue::Int(*i)),
CompatAttributeValue::Float(f) => Some(WmAttributeValue::Float(*f)),
CompatAttributeValue::Boolean(b) => Some(WmAttributeValue::Boolean(*b)),
CompatAttributeValue::Date(d) => Some(WmAttributeValue::Date(d.to_rfc3339())),
_ => None,
}
}
pub fn convert_event_log(log: &CompatEventLog) -> wm::EventLog {
let mut out = wm::EventLog::new();
out.attributes = convert_attrs(&log.attributes);
for trace in &log.traces {
let mut wtrace = wm::Trace::new();
wtrace.attributes = convert_attrs(&trace.attributes);
for event in &trace.events {
let mut wevent = wm::Event::new();
wevent.attributes = convert_attrs(&event.attributes);
wtrace.events.push(wevent);
}
out.traces.push(wtrace);
}
out
}
fn convert_attrs(attrs: &wasm4pm_compat::event_log::Attributes) -> wm::Attributes {
let mut map = BTreeMap::new();
for attr in attrs {
if let Some(v) = convert_attribute_value(&attr.value) {
map.insert(attr.key.clone(), v);
}
}
map
}
pub fn convert_dfg_to_petri_net(dfg: &DFG) -> PetriNet {
let mut net = PetriNet::new();
net.places.push(PetriNetPlace {
id: START_PLACE.to_string(),
label: "start".to_string(),
marking: Some(1),
});
net.initial_marking.insert(START_PLACE.to_string(), 1);
net.final_markings.push(BTreeMap::new());
let transition_id = |activity: &str| format!("t::{activity}");
for node in &dfg.nodes {
net.transitions.push(PetriNetTransition {
id: transition_id(&node.activity),
label: node.activity.clone(),
is_invisible: Some(false),
});
}
for activity in &dfg.start_activities {
net.arcs.push(PetriNetArc {
from: START_PLACE.to_string(),
to: transition_id(activity),
weight: Some(1),
});
}
for edge in &dfg.edges {
let place_id = format!("p::{}::{}", edge.source, edge.target);
net.places.push(PetriNetPlace {
id: place_id.clone(),
label: format!("{}->{}", edge.source, edge.target),
marking: Some(0),
});
net.arcs.push(PetriNetArc {
from: transition_id(&edge.source),
to: place_id.clone(),
weight: Some(1),
});
net.arcs.push(PetriNetArc {
from: place_id,
to: transition_id(&edge.target),
weight: Some(1),
});
}
net
}
pub fn check_conformance_real(
log: &CompatEventLog,
dfg: &DFG,
activity_key: &str,
) -> anyhow::Result<(f64, Option<f64>)> {
let wm_log = convert_event_log(log);
let net = convert_dfg_to_petri_net(dfg);
let replay_result = token_replay_pure(&wm_log, &net, activity_key);
let precision_report =
compute_align_etconformance_precision(&wm_log, &net, &AlignETConformanceConfig::default())
.map_err(|e| anyhow::anyhow!(e))
.context("align-etconformance precision computation failed")?;
Ok((replay_result.avg_fitness, Some(precision_report.precision)))
}
#[cfg(test)]
mod tests {
use super::*;
use wasm4pm_compat::event_log::{
Attribute, AttributeValue as CAV, Event as CEvent, EventLog as CLog, Trace as CTrace,
XESEditableAttribute,
};
use wasm4pm_compat::models::{DFGEdge, DFGNode};
fn ev(activity: &str) -> CEvent {
CEvent::with_activity(activity)
}
fn trace(case_id: &str, activities: &[&str]) -> CTrace {
CTrace::new(
case_id.to_string(),
activities.iter().map(|a| ev(a)).collect(),
)
}
fn sequential_dfg() -> DFG {
DFG {
nodes: vec![
DFGNode::new("a".to_string(), 1),
DFGNode::new("b".to_string(), 1),
DFGNode::new("c".to_string(), 1),
],
edges: vec![
DFGEdge::new("a".to_string(), "b".to_string(), 1),
DFGEdge::new("b".to_string(), "c".to_string(), 1),
],
start_activities: vec!["a".to_string()],
end_activities: vec!["c".to_string()],
}
}
#[test]
fn perfectly_fitting_log_scores_fitness_one() {
let log = CLog::new(vec![trace("case-1", &["a", "b", "c"])], Vec::new());
let dfg = sequential_dfg();
let (fitness, precision) =
check_conformance_real(&log, &dfg, "concept:name").expect("conformance check runs");
assert_eq!(
fitness, 1.0,
"trace exactly matching the model must be perfectly fit"
);
assert!(precision.is_some());
let precision = precision.unwrap();
assert!((0.0..=1.0).contains(&precision));
}
#[test]
fn deviating_log_scores_less_than_perfect_fitness() {
let log = CLog::new(vec![trace("case-1", &["a", "c"])], Vec::new());
let dfg = sequential_dfg();
let (fitness, _precision) =
check_conformance_real(&log, &dfg, "concept:name").expect("conformance check runs");
assert!(
fitness < 1.0,
"trace skipping a required activity must not score perfect fitness, got {fitness}"
);
}
#[test]
fn convert_event_log_preserves_activity_names() {
let log = CLog::new(vec![trace("case-1", &["a", "b"])], Vec::new());
let converted = convert_event_log(&log);
assert_eq!(converted.traces.len(), 1);
assert_eq!(converted.traces[0].events.len(), 2);
let first = converted.traces[0].events[0]
.attributes
.get("concept:name")
.and_then(|v| v.as_string());
assert_eq!(first, Some("a"));
}
#[test]
fn convert_dfg_to_petri_net_has_expected_shape() {
let dfg = sequential_dfg();
let net = convert_dfg_to_petri_net(&dfg);
assert_eq!(net.transitions.len(), 3);
assert_eq!(net.places.len(), 3);
assert_eq!(net.initial_marking.get(START_PLACE), Some(&1));
}
}