use std::collections::BTreeMap;
use wasm4pm::conformance::token_replay_pure;
use wasm4pm::models::{
AttributeValue, Event, EventLog, PetriNet, PetriNetArc, PetriNetPlace, PetriNetTransition,
Trace,
};
fn make_log(traces: &[&[&str]]) -> EventLog {
EventLog {
attributes: BTreeMap::new(),
traces: traces
.iter()
.map(|activities| Trace {
attributes: BTreeMap::new(),
events: activities
.iter()
.map(|&a| {
let mut attrs = BTreeMap::new();
attrs.insert(
"concept:name".to_string(),
AttributeValue::String(a.to_string()),
);
Event { attributes: attrs }
})
.collect(),
})
.collect(),
}
}
fn arc(from: &str, to: &str) -> PetriNetArc {
PetriNetArc {
from: from.to_string(),
to: to.to_string(),
weight: None,
}
}
fn trans(id: &str, label: &str) -> PetriNetTransition {
PetriNetTransition {
id: id.to_string(),
label: label.to_string(),
is_invisible: None,
}
}
fn place(id: &str) -> PetriNetPlace {
PetriNetPlace {
id: id.to_string(),
label: id.to_string(),
marking: None,
}
}
#[test]
fn ec_empty_log_no_traces() {
let net = PetriNet {
places: vec![place("i"), place("p"), place("f")],
transitions: vec![trans("A", "A"), trans("B", "B")],
arcs: vec![arc("i", "A"), arc("A", "p"), arc("p", "B"), arc("B", "f")],
initial_marking: {
let mut m = BTreeMap::new();
m.insert("i".to_string(), 1);
m
},
final_markings: vec![{
let mut m = BTreeMap::new();
m.insert("f".to_string(), 1);
m
}],
};
let log = make_log(&[]);
let result = token_replay_pure(&log, &net, "concept:name");
assert!(
result.avg_fitness >= 0.0 && result.avg_fitness <= 1.0,
"fitness {} out of bounds [0.0, 1.0]",
result.avg_fitness
);
assert_eq!(
result.avg_fitness, 1.0,
"empty log should have fitness 1.0 (vacuous), got {}",
result.avg_fitness
);
assert_eq!(
result.case_fitness.len(),
0,
"empty log should have no case results"
);
}
#[test]
fn ec_single_event_log() {
let net = PetriNet {
places: vec![place("i"), place("p"), place("f")],
transitions: vec![trans("A", "A"), trans("B", "B")],
arcs: vec![arc("i", "A"), arc("A", "p"), arc("p", "B"), arc("B", "f")],
initial_marking: {
let mut m = BTreeMap::new();
m.insert("i".to_string(), 1);
m
},
final_markings: vec![{
let mut m = BTreeMap::new();
m.insert("f".to_string(), 1);
m
}],
};
let log = make_log(&[&["A"]]);
let result = token_replay_pure(&log, &net, "concept:name");
assert!(
result.avg_fitness >= 0.0 && result.avg_fitness <= 1.0,
"fitness {} out of bounds [0.0, 1.0]",
result.avg_fitness
);
assert_eq!(result.case_fitness.len(), 1);
assert!(!result.avg_fitness.is_nan(), "fitness should not be NaN");
}
#[test]
fn ec_single_activity_net() {
let net = PetriNet {
places: vec![place("i"), place("f")],
transitions: vec![trans("A", "A")],
arcs: vec![arc("i", "A"), arc("A", "f")],
initial_marking: {
let mut m = BTreeMap::new();
m.insert("i".to_string(), 1);
m
},
final_markings: vec![{
let mut m = BTreeMap::new();
m.insert("f".to_string(), 1);
m
}],
};
let log_perfect = make_log(&[&["A"]]);
let result_perfect = token_replay_pure(&log_perfect, &net, "concept:name");
assert!(result_perfect.avg_fitness >= 0.0 && result_perfect.avg_fitness <= 1.0);
assert!(!result_perfect.avg_fitness.is_nan());
let log_extra = make_log(&[&["A", "A"]]);
let result_extra = token_replay_pure(&log_extra, &net, "concept:name");
assert!(result_extra.avg_fitness >= 0.0 && result_extra.avg_fitness <= 1.0);
assert!(!result_extra.avg_fitness.is_nan());
}
#[test]
fn ec_all_unknown_activities() {
let net = PetriNet {
places: vec![place("i"), place("p"), place("f")],
transitions: vec![trans("A", "A"), trans("B", "B")],
arcs: vec![arc("i", "A"), arc("A", "p"), arc("p", "B"), arc("B", "f")],
initial_marking: {
let mut m = BTreeMap::new();
m.insert("i".to_string(), 1);
m
},
final_markings: vec![{
let mut m = BTreeMap::new();
m.insert("f".to_string(), 1);
m
}],
};
let log = make_log(&[&["X", "Y", "Z"]]);
let result = token_replay_pure(&log, &net, "concept:name");
assert!(
result.avg_fitness >= 0.0 && result.avg_fitness <= 1.0,
"fitness {} out of bounds [0.0, 1.0] for all-unknown trace",
result.avg_fitness
);
assert!(result.avg_fitness < 1.0);
assert!(result.case_fitness[0].tokens_missing > 0);
}
#[test]
fn ec_empty_trace_in_log() {
let net = PetriNet {
places: vec![place("i"), place("p"), place("f")],
transitions: vec![trans("A", "A"), trans("B", "B")],
arcs: vec![arc("i", "A"), arc("A", "p"), arc("p", "B"), arc("B", "f")],
initial_marking: {
let mut m = BTreeMap::new();
m.insert("i".to_string(), 1);
m
},
final_markings: vec![{
let mut m = BTreeMap::new();
m.insert("f".to_string(), 1);
m
}],
};
let log = make_log(&[&[]]);
let result = token_replay_pure(&log, &net, "concept:name");
assert!(
result.avg_fitness >= 0.0 && result.avg_fitness <= 1.0,
"fitness {} out of bounds for empty trace",
result.avg_fitness
);
assert!(result.avg_fitness.is_finite());
assert_eq!(result.case_fitness.len(), 1);
}
#[test]
fn ec_heavy_deviations_clamped() {
let net = PetriNet {
places: vec![place("i"), place("f")],
transitions: vec![trans("A", "A")],
arcs: vec![arc("i", "A"), arc("A", "f")],
initial_marking: {
let mut m = BTreeMap::new();
m.insert("i".to_string(), 1);
m
},
final_markings: vec![{
let mut m = BTreeMap::new();
m.insert("f".to_string(), 1);
m
}],
};
let log = make_log(&[&["X", "Y", "Z", "W", "V", "U"]]);
let result = token_replay_pure(&log, &net, "concept:name");
assert!(
result.avg_fitness >= 0.0 && result.avg_fitness <= 1.0,
"fitness {} out of bounds even with heavy deviations",
result.avg_fitness
);
}
#[test]
fn ec_mixed_traces_with_empty() {
let net = PetriNet {
places: vec![place("i"), place("p"), place("f")],
transitions: vec![trans("A", "A"), trans("B", "B")],
arcs: vec![arc("i", "A"), arc("A", "p"), arc("p", "B"), arc("B", "f")],
initial_marking: {
let mut m = BTreeMap::new();
m.insert("i".to_string(), 1);
m
},
final_markings: vec![{
let mut m = BTreeMap::new();
m.insert("f".to_string(), 1);
m
}],
};
let log = make_log(&[&["A", "B"], &[], &["X"]]);
let result = token_replay_pure(&log, &net, "concept:name");
assert!(
result.avg_fitness >= 0.0 && result.avg_fitness <= 1.0,
"avg_fitness {} out of bounds for mixed traces",
result.avg_fitness
);
assert_eq!(result.case_fitness.len(), 3);
for cf in &result.case_fitness {
assert!(
cf.trace_fitness >= 0.0 && cf.trace_fitness <= 1.0,
"case fitness {} out of bounds",
cf.trace_fitness
);
}
}
#[test]
fn ec_fitness_never_nan_or_inf() {
let net = PetriNet {
places: vec![place("i"), place("f")],
transitions: vec![trans("A", "A")],
arcs: vec![arc("i", "A"), arc("A", "f")],
initial_marking: {
let mut m = BTreeMap::new();
m.insert("i".to_string(), 1);
m
},
final_markings: vec![{
let mut m = BTreeMap::new();
m.insert("f".to_string(), 1);
m
}],
};
for (name, traces) in &[
("empty_log", vec![]),
("single_event", vec![vec!["A"]]),
("all_unknown", vec![vec!["X", "Y"]]),
] {
let log = make_log(&traces.iter().map(|t| t.as_slice()).collect::<Vec<_>>());
let result = token_replay_pure(&log, &net, "concept:name");
assert!(
result.avg_fitness.is_finite(),
"{}: avg_fitness is not finite ({})",
name,
result.avg_fitness
);
for (i, cf) in result.case_fitness.iter().enumerate() {
assert!(
cf.trace_fitness.is_finite(),
"{} trace {}: fitness is not finite ({})",
name,
i,
cf.trace_fitness
);
}
}
}
#[test]
fn ec_fitness_1_0_only_for_perfect_traces() {
let net = PetriNet {
places: vec![place("i"), place("p"), place("f")],
transitions: vec![trans("A", "A"), trans("B", "B")],
arcs: vec![arc("i", "A"), arc("A", "p"), arc("p", "B"), arc("B", "f")],
initial_marking: {
let mut m = BTreeMap::new();
m.insert("i".to_string(), 1);
m
},
final_markings: vec![{
let mut m = BTreeMap::new();
m.insert("f".to_string(), 1);
m
}],
};
let log_perfect = make_log(&[&["A", "B"]]);
let result_perfect = token_replay_pure(&log_perfect, &net, "concept:name");
assert!(
result_perfect.avg_fitness > 0.5,
"perfect trace should have fitness > 0.5"
);
let log_imperfect = make_log(&[&["A"]]);
let result_imperfect = token_replay_pure(&log_imperfect, &net, "concept:name");
assert!(
result_perfect.avg_fitness > result_imperfect.avg_fitness,
"perfect trace fitness ({}) should exceed imperfect ({})",
result_perfect.avg_fitness,
result_imperfect.avg_fitness
);
}
#[test]
fn ec_monotonic_fitness_with_added_events() {
let net = PetriNet {
places: vec![place("i"), place("p"), place("f")],
transitions: vec![trans("A", "A"), trans("B", "B")],
arcs: vec![arc("i", "A"), arc("A", "p"), arc("p", "B"), arc("B", "f")],
initial_marking: {
let mut m = BTreeMap::new();
m.insert("i".to_string(), 1);
m
},
final_markings: vec![{
let mut m = BTreeMap::new();
m.insert("f".to_string(), 1);
m
}],
};
let log_a = make_log(&[&["A"]]);
let result_a = token_replay_pure(&log_a, &net, "concept:name");
let log_ab = make_log(&[&["A", "B"]]);
let result_ab = token_replay_pure(&log_ab, &net, "concept:name");
assert!(
result_ab.avg_fitness >= result_a.avg_fitness,
"adding correct event B should improve fitness: {} >= {}",
result_ab.avg_fitness,
result_a.avg_fitness
);
}