use wasm4pm_compat::powl::{ChoiceGraph, ChoiceGraphNode};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
use crate::models::EventLog as ModelsEventLog;
use crate::powl::analysis::complexity::measure;
use crate::powl::analysis::diff::diff as model_diff;
use crate::powl::conformance::dg_soundness::get_soundness_report as dg_soundness_report;
use crate::powl::conformance::footprints_conf::check as footprints_conformance_check;
use crate::powl::conformance::soundness::check_soundness;
use crate::powl::conformance::token_replay::compute_fitness;
use crate::powl::conversion::from_petri_net;
use crate::powl::conversion::from_process_tree;
use crate::powl::conversion::to_bpmn;
use crate::powl::conversion::to_petri_net;
use crate::powl::conversion::to_process_tree;
use crate::powl::discovery::{discover_powl, DiscoveryConfig, DiscoveryVariant};
use crate::powl::footprints::apply as footprints_apply;
use crate::powl::simplify::{simplify, simplify_using_frequent_transitions};
use crate::powl::visualization::process_tree_svg::render_process_tree_svg;
use crate::powl_arena::PowlArena;
use crate::powl_event_log::EventLog;
use crate::powl_models::PowlPetriNetResult;
use crate::powl_parser::parse_powl_model_string;
fn to_js(val: &impl serde::Serialize) -> Result<JsValue, JsValue> {
let s = serde_json::to_string(val)
.map_err(|e| crate::error::js_val(&format!("serde error: {}", e)))?;
Ok(JsValue::from_str(&s))
}
fn wasm_err(msg: &str) -> JsValue {
crate::error::js_val(msg)
}
fn parse_model(s: &str) -> Result<(PowlArena, u32), JsValue> {
let mut arena = PowlArena::new();
let root = parse_powl_model_string(s.trim(), &mut arena)
.map_err(|e| wasm_err(&format!("parse error: {}", e)))?;
Ok((arena, root))
}
#[wasm_bindgen]
pub fn parse_powl(s: &str) -> Result<JsValue, JsValue> {
let (arena, root) = parse_model(s)?;
let repr = arena.to_repr(root);
to_js(&serde_json::json!({
"root": root,
"node_count": arena.len(),
"repr": repr,
}))
}
#[wasm_bindgen]
pub fn validate_partial_orders(s: &str) -> Result<JsValue, JsValue> {
let (arena, root) = parse_model(s)?;
arena
.validate_partial_orders(root)
.map_err(|e| wasm_err(&e.to_string()))?;
to_js(&serde_json::json!({ "valid": true }))
}
#[wasm_bindgen]
pub fn powl_to_string(s: &str) -> Result<String, JsValue> {
let (arena, root) = parse_model(s)?;
Ok(arena.to_repr(root))
}
#[wasm_bindgen]
pub fn simplify_powl(s: &str) -> Result<JsValue, JsValue> {
let (mut arena, root) = parse_model(s)?;
let new_root = simplify(&mut arena, root);
let repr = arena.to_repr(new_root);
to_js(&serde_json::json!({
"root": new_root,
"node_count": arena.len(),
"repr": repr,
}))
}
#[wasm_bindgen]
pub fn simplify_frequent_transitions(s: &str) -> Result<JsValue, JsValue> {
let (mut arena, root) = parse_model(s)?;
let new_root = simplify_using_frequent_transitions(&mut arena, root);
let repr = arena.to_repr(new_root);
to_js(&serde_json::json!({
"root": new_root,
"node_count": arena.len(),
"repr": repr,
}))
}
#[wasm_bindgen]
pub fn node_to_string(s: &str, arena_idx: u32) -> Result<String, JsValue> {
let (arena, _root) = parse_model(s)?;
Ok(arena.to_repr(arena_idx))
}
#[wasm_bindgen]
pub fn get_children(s: &str, arena_idx: u32) -> Result<JsValue, JsValue> {
let (arena, _root) = parse_model(s)?;
let children = match arena.get(arena_idx) {
Some(crate::powl_arena::PowlNode::OperatorPowl(op)) => op.children.clone(),
Some(crate::powl_arena::PowlNode::StrictPartialOrder(spo)) => spo.children.clone(),
Some(crate::powl_arena::PowlNode::DecisionGraph(dg)) => dg.children.clone(),
Some(crate::powl_arena::PowlNode::ChoiceGraph(cg)) => cg
.graph
.nodes
.iter()
.filter_map(|n| match n {
ChoiceGraphNode::SubModel(idx) => Some(*idx),
_ => None,
})
.collect(),
_ => vec![],
};
to_js(&serde_json::json!({ "children": children }))
}
#[wasm_bindgen]
pub fn node_info_json(s: &str, arena_idx: u32) -> Result<String, JsValue> {
let (arena, _root) = parse_model(s)?;
let info = match arena.get(arena_idx) {
Some(crate::powl_arena::PowlNode::Transition(t)) => serde_json::json!({
"type": "transition",
"label": t.label,
"id": t.id,
}),
Some(crate::powl_arena::PowlNode::FrequentTransition(t)) => serde_json::json!({
"type": "frequent_transition",
"label": t.label,
"activity": t.activity,
"min_freq": t.min_freq,
"max_freq": t.max_freq,
"is_skippable": t.is_skippable(),
"is_repeatable": t.is_repeatable(),
"is_unbounded": t.is_unbounded(),
"skippable": t.skippable,
"selfloop": t.selfloop,
"id": t.id,
}),
Some(crate::powl_arena::PowlNode::StrictPartialOrder(spo)) => {
let edges: Vec<Vec<usize>> = spo
.order
.edge_list()
.into_iter()
.map(|(a, b)| vec![a, b])
.collect();
serde_json::json!({
"type": "strict_partial_order",
"children": spo.children,
"edges": edges,
"node_count": spo.order.n,
})
}
Some(crate::powl_arena::PowlNode::OperatorPowl(op)) => serde_json::json!({
"type": "operator",
"operator": op.operator.as_str(),
"children": op.children,
}),
Some(crate::powl_arena::PowlNode::DecisionGraph(dg)) => {
let edges: Vec<Vec<usize>> = dg
.order
.edge_list()
.into_iter()
.map(|(a, b)| vec![a, b])
.collect();
serde_json::json!({
"type": "decision_graph",
"children": dg.children,
"edges": edges,
"start_nodes": dg.start_nodes,
"end_nodes": dg.end_nodes,
"empty_path": dg.empty_path,
"node_count": dg.order.n,
})
}
Some(crate::powl_arena::PowlNode::ChoiceGraph(cg)) => {
let edges: Vec<Vec<usize>> = cg.graph.edges.iter().map(|(a, b)| vec![*a, *b]).collect();
let node_kinds: Vec<String> = cg
.graph
.nodes
.iter()
.map(|n| match n {
ChoiceGraphNode::Start => "Start".into(),
ChoiceGraphNode::End => "End".into(),
ChoiceGraphNode::Activity(l) => format!("Activity({})", l),
ChoiceGraphNode::SubModel(i) => format!("SubModel({})", i),
})
.collect();
serde_json::json!({
"type": "choice_graph",
"nodes": node_kinds,
"edges": edges,
"start_idx": cg.graph.start_idx,
"end_idx": cg.graph.end_idx,
})
}
None => serde_json::json!({ "error": "invalid index" }),
};
serde_json::to_string(&info)
.map_err(|e| crate::error::js_val(&format!("Failed to serialize model info: {}", e)))
}
#[wasm_bindgen]
pub fn powl_to_petri_net(s: &str) -> Result<String, JsValue> {
let (arena, root) = parse_model(s)?;
let result: PowlPetriNetResult = to_petri_net::apply(&arena, root);
serde_json::to_string_pretty(&result).map_err(|e| wasm_err(&format!("json error: {}", e)))
}
#[wasm_bindgen]
pub fn powl_to_process_tree(s: &str) -> Result<String, JsValue> {
let (arena, root) = parse_model(s)?;
if arena.is_empty() || root >= arena.len() as u32 {
return Err(wasm_err("POWL arena is empty or root index out of bounds"));
}
let tree = to_process_tree::apply(&arena, root);
serde_json::to_string_pretty(&tree).map_err(|e| wasm_err(&format!("json error: {}", e)))
}
#[wasm_bindgen]
pub fn process_tree_to_powl(tree_json: &str) -> Result<JsValue, JsValue> {
let (arena, root) =
from_process_tree::process_tree_to_powl(tree_json).map_err(|e| wasm_err(&e))?;
let repr = arena.to_repr(root);
to_js(&serde_json::json!({
"root": root,
"node_count": arena.len(),
"repr": repr,
}))
}
#[wasm_bindgen]
pub fn petri_net_to_powl(pn_json: &str) -> Result<JsValue, JsValue> {
let (arena, root) = from_petri_net::petri_net_to_powl(pn_json).map_err(|e| wasm_err(&e))?;
let repr = arena.to_repr(root);
to_js(&serde_json::json!({
"root": root,
"node_count": arena.len(),
"repr": repr,
}))
}
#[wasm_bindgen]
pub fn powl_to_bpmn(s: &str) -> Result<String, JsValue> {
let (arena, root) = parse_model(s)?;
let xml = to_bpmn::to_bpmn_xml(&arena, root);
Ok(xml)
}
#[wasm_bindgen]
pub fn token_replay_fitness(powl_str: &str, log_json: &str) -> Result<String, JsValue> {
let (arena, root) = parse_model(powl_str)?;
let pn_result: PowlPetriNetResult = to_petri_net::apply(&arena, root);
let log: EventLog =
serde_json::from_str(log_json).map_err(|e| wasm_err(&format!("log parse error: {}", e)))?;
let fitness_result = compute_fitness(
&pn_result.net,
&pn_result.initial_marking,
&pn_result.final_marking,
&log,
);
serde_json::to_string_pretty(&fitness_result)
.map_err(|e| wasm_err(&format!("json error: {}", e)))
}
#[wasm_bindgen]
pub fn check_powl_soundness(powl_str: &str) -> Result<String, JsValue> {
let (arena, root) = parse_model(powl_str)?;
let pn_result: PowlPetriNetResult = to_petri_net::apply(&arena, root);
let soundness_result = check_soundness(
&pn_result.net,
&pn_result.initial_marking,
&pn_result.final_marking,
);
serde_json::to_string_pretty(&soundness_result)
.map_err(|e| wasm_err(&format!("json error: {}", e)))
}
#[wasm_bindgen]
pub fn check_dg_soundness(powl_string: &str) -> Result<String, JsValue> {
let (arena, root) = parse_model(powl_string)?;
let report = dg_soundness_report(&arena, root);
serde_json::to_string_pretty(&report).map_err(|e| wasm_err(&format!("json error: {}", e)))
}
#[wasm_bindgen]
pub fn footprints_conformance(powl_str: &str, log_json: &str) -> Result<String, JsValue> {
let (arena, root) = parse_model(powl_str)?;
let model_fp = footprints_apply(&arena, root);
let log: EventLog =
serde_json::from_str(log_json).map_err(|e| wasm_err(&format!("log parse error: {}", e)))?;
let result = footprints_conformance_check(&log, &model_fp);
serde_json::to_string_pretty(&result).map_err(|e| wasm_err(&format!("json error: {}", e)))
}
#[wasm_bindgen]
pub fn measure_complexity(s: &str) -> Result<String, JsValue> {
let (arena, root) = parse_model(s)?;
let report = measure(&arena, root);
serde_json::to_string_pretty(&report).map_err(|e| wasm_err(&format!("json error: {}", e)))
}
#[wasm_bindgen]
pub fn powl_freq_analysis(s: &str) -> Result<String, JsValue> {
let (mut arena, root) = parse_model(s)?;
let root = crate::powl::simplify::simplify_using_frequent_transitions(&mut arena, root);
let mut nodes: Vec<serde_json::Value> = Vec::new();
collect_freq_nodes(&arena, root, &mut nodes);
let total = nodes.len();
let skippable_count = nodes
.iter()
.filter(|n| n["is_skippable"].as_bool().unwrap_or(false))
.count();
let repeatable_count = nodes
.iter()
.filter(|n| n["is_repeatable"].as_bool().unwrap_or(false))
.count();
let unbounded_count = nodes
.iter()
.filter(|n| n["is_unbounded"].as_bool().unwrap_or(false))
.count();
let freq_min_min: Option<i64> = nodes.iter().filter_map(|n| n["min_freq"].as_i64()).min();
let freq_max_max: serde_json::Value = if nodes
.iter()
.any(|n| n["is_unbounded"].as_bool().unwrap_or(false))
{
serde_json::Value::Null
} else {
nodes
.iter()
.filter_map(|n| n["max_freq"].as_i64())
.max()
.map_or(serde_json::Value::Null, serde_json::Value::from)
};
let result = serde_json::json!({
"total_frequent_transitions": total,
"skippable_count": skippable_count,
"repeatable_count": repeatable_count,
"unbounded_count": unbounded_count,
"freq_min_min": freq_min_min,
"freq_max_max": freq_max_max,
"nodes": nodes,
});
serde_json::to_string_pretty(&result).map_err(|e| wasm_err(&format!("json error: {}", e)))
}
fn collect_freq_nodes(arena: &PowlArena, idx: u32, out: &mut Vec<serde_json::Value>) {
match arena.get(idx) {
None | Some(crate::powl_arena::PowlNode::Transition(_)) => {}
Some(crate::powl_arena::PowlNode::FrequentTransition(t)) => {
out.push(serde_json::json!({
"activity": t.activity,
"min_freq": t.min_freq,
"max_freq": t.max_freq,
"is_skippable": t.is_skippable(),
"is_repeatable": t.is_repeatable(),
"is_unbounded": t.is_unbounded(),
}));
}
Some(crate::powl_arena::PowlNode::OperatorPowl(op)) => {
let children = op.children.clone();
for c in children {
collect_freq_nodes(arena, c, out);
}
}
Some(crate::powl_arena::PowlNode::StrictPartialOrder(spo)) => {
let children = spo.children.clone();
for c in children {
collect_freq_nodes(arena, c, out);
}
}
Some(crate::powl_arena::PowlNode::DecisionGraph(dg)) => {
let children = dg.children.clone();
for c in children {
collect_freq_nodes(arena, c, out);
}
}
Some(crate::powl_arena::PowlNode::ChoiceGraph(cg)) => {
let sub_idxs: Vec<u32> = cg
.graph
.nodes
.iter()
.filter_map(|n| match n {
ChoiceGraphNode::SubModel(i) => Some(*i),
_ => None,
})
.collect();
for c in sub_idxs {
collect_freq_nodes(arena, c, out);
}
}
}
}
#[wasm_bindgen]
pub fn diff_models(model_a_str: &str, model_b_str: &str) -> Result<String, JsValue> {
let (arena_a, root_a) = parse_model(model_a_str)?;
let (arena_b, root_b) = parse_model(model_b_str)?;
let result = model_diff(&arena_a, root_a, &arena_b, root_b);
serde_json::to_string_pretty(&result).map_err(|e| wasm_err(&format!("json error: {}", e)))
}
#[wasm_bindgen]
pub fn powl_footprints(s: &str) -> Result<String, JsValue> {
let (arena, root) = parse_model(s)?;
let fp = crate::powl::footprints::apply(&arena, root);
serde_json::to_string_pretty(&fp).map_err(|e| wasm_err(&format!("json error: {}", e)))
}
#[wasm_bindgen]
pub fn discover_powl_from_log(log_json: &str, variant: &str) -> Result<JsValue, JsValue> {
let log: ModelsEventLog =
serde_json::from_str(log_json).map_err(|e| wasm_err(&format!("log parse error: {}", e)))?;
let discovery_variant = DiscoveryVariant::from_variant_str(variant)
.unwrap_or(DiscoveryVariant::DecisionGraphCyclic);
let config = DiscoveryConfig {
activity_key: "concept:name".to_string(),
variant: discovery_variant,
min_trace_count: 1,
noise_threshold: 0.0,
from_dfg: false,
fall_through_fired: false,
};
let (arena, root) =
discover_powl(&log, &config).map_err(|e| wasm_err(&format!("discovery error: {}", e)))?;
let repr = arena.to_repr(root);
to_js(&serde_json::json!({
"root": root,
"node_count": arena.len(),
"repr": repr,
"variant": variant,
}))
}
#[wasm_bindgen]
pub fn discover_powl_from_log_config(
log_json: &str,
activity_key: &str,
variant: &str,
min_trace_count: usize,
noise_threshold: f64,
) -> Result<JsValue, JsValue> {
let log: ModelsEventLog =
serde_json::from_str(log_json).map_err(|e| wasm_err(&format!("log parse error: {}", e)))?;
let discovery_variant = DiscoveryVariant::from_variant_str(variant)
.unwrap_or(DiscoveryVariant::DecisionGraphCyclic);
let config = DiscoveryConfig {
activity_key: activity_key.to_string(),
variant: discovery_variant,
min_trace_count,
noise_threshold,
from_dfg: false,
fall_through_fired: false,
};
let (arena, root) =
discover_powl(&log, &config).map_err(|e| wasm_err(&format!("discovery error: {}", e)))?;
let repr = arena.to_repr(root);
to_js(&serde_json::json!({
"root": root,
"node_count": arena.len(),
"repr": repr,
"variant": variant,
"config": {
"activity_key": activity_key,
"min_trace_count": min_trace_count,
"noise_threshold": noise_threshold,
}
}))
}
#[wasm_bindgen]
pub fn discover_powl_from_partial_orders(
log_json: &str,
variant: &str,
) -> Result<JsValue, JsValue> {
let log: ModelsEventLog =
serde_json::from_str(log_json).map_err(|e| wasm_err(&format!("log parse error: {}", e)))?;
let discovery_variant = DiscoveryVariant::from_variant_str(variant)
.unwrap_or(DiscoveryVariant::DecisionGraphCyclic);
let config = DiscoveryConfig {
activity_key: "concept:name".to_string(),
variant: discovery_variant,
min_trace_count: 1,
noise_threshold: 0.0,
from_dfg: false,
fall_through_fired: false,
};
let mut arena = PowlArena::new();
let root = crate::powl::discovery::from_partial_orders::discover_from_partial_orders(
&log, &config, &mut arena,
)
.map_err(|e| wasm_err(&format!("partial order discovery error: {}", e)))?;
let repr = arena.to_repr(root);
to_js(&serde_json::json!({
"root": root,
"node_count": arena.len(),
"repr": repr,
"variant": variant,
"partial_order": true,
}))
}
#[wasm_bindgen]
pub fn discover_ocel_powl(ocel_json: &str, variant: &str) -> Result<JsValue, JsValue> {
let log: ModelsEventLog = serde_json::from_str(ocel_json)
.map_err(|e| wasm_err(&format!("ocel log parse error: {}", e)))?;
let ocel_variant = crate::powl::discovery::ocel::OcelVariant::from_variant_str(variant)
.ok_or_else(|| wasm_err("invalid OCEL variant: use 'flattening' or 'oc_powl'"))?;
let config = DiscoveryConfig {
activity_key: "concept:name".to_string(),
variant: DiscoveryVariant::DecisionGraphCyclic,
min_trace_count: 1,
noise_threshold: 0.0,
from_dfg: false,
fall_through_fired: false,
};
let mut arena = PowlArena::new();
let root =
crate::powl::discovery::ocel::discover_ocel_powl(&log, &config, &mut arena, ocel_variant)
.map_err(|e| wasm_err(&format!("ocel discovery error: {}", e)))?;
let repr = arena.to_repr(root);
to_js(&serde_json::json!({
"root": root,
"node_count": arena.len(),
"repr": repr,
"ocel_variant": variant,
}))
}
#[wasm_bindgen]
pub fn powl_to_svg(s: &str) -> Result<String, JsValue> {
let (arena, root) = parse_model(s)?;
let svg = render_process_tree_svg(&arena, root);
Ok(svg)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::powl_event_log::Trace;
fn parse_test(s: &str) -> Result<(PowlArena, u32), String> {
let mut arena = PowlArena::new();
let root = parse_powl_model_string(s.trim(), &mut arena).map_err(|e| e.to_string())?;
Ok((arena, root))
}
fn bicycle_powl() -> &'static str {
"PO=(nodes={create_process, X(accept_order, reject_order), \
prepare_assembly, assemble_bicycle, ship_bicycle, finish_process}, \
order={create_process-->X(accept_order, reject_order), \
X(accept_order, reject_order)-->prepare_assembly, \
prepare_assembly-->assemble_bicycle, \
assemble_bicycle-->ship_bicycle, \
ship_bicycle-->finish_process})"
}
#[test]
fn paper_bicycle_parse_and_validate() {
let (arena, root) = parse_test(bicycle_powl()).unwrap();
assert!(arena.validate_partial_orders(root).is_ok());
assert!(arena.to_repr(root).contains("create_process"));
}
#[test]
fn paper_bicycle_to_petri_net() {
let (arena, root) = parse_test(bicycle_powl()).unwrap();
let result: PowlPetriNetResult = to_petri_net::apply(&arena, root);
assert!(result.net.places.len() > 3);
assert!(result.net.transitions.len() > 3);
let labels: Vec<&str> = result
.net
.transitions
.iter()
.filter_map(|t| t.label.as_deref())
.collect();
assert!(labels.contains(&"create_process"));
assert!(labels.contains(&"accept_order"));
assert!(labels.contains(&"reject_order"));
assert!(labels.contains(&"assemble_bicycle"));
assert!(labels.contains(&"ship_bicycle"));
}
#[test]
fn paper_bicycle_to_bpmn() {
let (arena, root) = parse_test(bicycle_powl()).unwrap();
let xml = to_bpmn::to_bpmn_xml(&arena, root);
assert!(xml.contains("<definitions"));
assert!(xml.contains("exclusiveGateway") || xml.contains("parallelGateway"));
assert!(xml.contains("</definitions>"));
}
#[test]
fn paper_bicycle_complexity() {
let (arena, root) = parse_test(bicycle_powl()).unwrap();
let report = measure(&arena, root);
assert!(report.cyclomatic >= 2);
assert!(report.cfc >= 1);
assert!(report.halstead.volume > 0.0);
assert!(report.activity_count >= 5);
}
#[test]
fn paper_bicycle_footprints() {
let (arena, root) = parse_test(bicycle_powl()).unwrap();
let fp = crate::powl::footprints::apply(&arena, root);
assert!(fp.start_activities.contains("create_process"));
assert!(fp.end_activities.contains("finish_process"));
assert!(!fp.parallel.is_empty() || !fp.sequence.is_empty());
}
fn online_shop_powl() -> &'static str {
"PO=(nodes={login, PO=(nodes={select_items, set_payment_method}, order={}), \
reward_selection, \
X(pay, installment), \
delivery, \
*(return_exchange, return_choice)}, \
order={login-->PO=(nodes={select_items, set_payment_method}, order={}), \
PO=(nodes={select_items, set_payment_method}, order={})-->reward_selection, \
reward_selection-->X(pay,installment), \
X(pay,installment)-->delivery, \
delivery-->*(return_exchange, return_choice)})"
}
#[test]
fn paper_online_shop_parse_and_validate() {
let (arena, root) = parse_test(online_shop_powl()).unwrap();
assert!(arena.validate_partial_orders(root).is_ok());
}
#[test]
fn paper_online_shop_to_petri_net() {
let (arena, root) = parse_test(online_shop_powl()).unwrap();
let result: PowlPetriNetResult = to_petri_net::apply(&arena, root);
let labels: Vec<&str> = result
.net
.transitions
.iter()
.filter_map(|t| t.label.as_deref())
.collect();
assert!(labels.contains(&"select_items"));
assert!(labels.contains(&"pay"));
assert!(labels.contains(&"delivery"));
assert!(labels.contains(&"return_exchange"));
}
#[test]
fn paper_online_shop_conformance_perfect() {
let (arena, root) = parse_test(online_shop_powl()).unwrap();
let pn_result: PowlPetriNetResult = to_petri_net::apply(&arena, root);
let log = EventLog {
traces: vec![Trace {
case_id: "c1".to_string(),
events: vec![
crate::powl_event_log::Event {
name: "login".into(),
timestamp: None,
lifecycle: None,
attributes: std::collections::HashMap::new(),
},
crate::powl_event_log::Event {
name: "select_items".into(),
timestamp: None,
lifecycle: None,
attributes: std::collections::HashMap::new(),
},
crate::powl_event_log::Event {
name: "set_payment_method".into(),
timestamp: None,
lifecycle: None,
attributes: std::collections::HashMap::new(),
},
crate::powl_event_log::Event {
name: "reward_selection".into(),
timestamp: None,
lifecycle: None,
attributes: std::collections::HashMap::new(),
},
crate::powl_event_log::Event {
name: "pay".into(),
timestamp: None,
lifecycle: None,
attributes: std::collections::HashMap::new(),
},
crate::powl_event_log::Event {
name: "delivery".into(),
timestamp: None,
lifecycle: None,
attributes: std::collections::HashMap::new(),
},
],
}],
};
let fitness = compute_fitness(
&pn_result.net,
&pn_result.initial_marking,
&pn_result.final_marking,
&log,
);
assert!(fitness.percentage > 0.0);
}
fn hotel_powl() -> &'static str {
"PO=(nodes={take_order, PO=(nodes={submit_kitchen, fetch_wine}, order={}), \
assign_waiter, \
PO=(nodes={ready_cart, prepare_wine}, order={}), \
deliver, \
*(tip_optional, redo_tip), \
debit_account}, \
order={take_order-->PO=(nodes={submit_kitchen, fetch_wine}, order={}), \
PO=(nodes={submit_kitchen, fetch_wine}, order={})-->assign_waiter, \
assign_waiter-->PO=(nodes={ready_cart, prepare_wine}, order={}), \
PO=(nodes={ready_cart, prepare_wine}, order={})-->deliver, \
deliver-->*(tip_optional, redo_tip), \
*(tip_optional, redo_tip)-->debit_account})"
}
#[test]
fn paper_hotel_parse_and_validate() {
let (arena, root) = parse_test(hotel_powl()).unwrap();
assert!(arena.validate_partial_orders(root).is_ok());
}
#[test]
fn paper_hotel_sound_bpmn_export() {
let (arena, root) = parse_test(hotel_powl()).unwrap();
let xml = to_bpmn::to_bpmn_xml(&arena, root);
assert!(xml.contains("<definitions"));
assert!(xml.contains("</definitions>"));
assert!(xml.contains("Gateway"));
}
#[test]
fn paper_hotel_conformance() {
let (arena, root) = parse_test(hotel_powl()).unwrap();
let pn_result: PowlPetriNetResult = to_petri_net::apply(&arena, root);
let make_event = |name: &str| crate::powl_event_log::Event {
name: name.into(),
timestamp: None,
lifecycle: None,
attributes: std::collections::HashMap::new(),
};
let log = EventLog {
traces: vec![Trace {
case_id: "h1".to_string(),
events: vec![
make_event("take_order"),
make_event("submit_kitchen"),
make_event("fetch_wine"),
make_event("assign_waiter"),
make_event("ready_cart"),
make_event("prepare_wine"),
make_event("deliver"),
make_event("tip_optional"),
make_event("debit_account"),
],
}],
};
let fitness = compute_fitness(
&pn_result.net,
&pn_result.initial_marking,
&pn_result.final_marking,
&log,
);
assert!(fitness.percentage > 0.0);
assert!(fitness.total_traces == 1);
}
#[test]
fn paper_diff_after_feedback() {
let before = "X ( A, B )";
let after = "X ( A, B, C )";
let (arena_a, root_a) = parse_test(before).unwrap();
let (arena_b, root_b) = parse_test(after).unwrap();
let d = model_diff(&arena_a, root_a, &arena_b, root_b);
assert!(d.added_activities.contains(&"C".to_string()));
assert!(d.severity == crate::powl::analysis::diff::Severity::Moderate);
}
#[test]
fn paper_simplification_flattens_nested_xor() {
let model_str = "X ( X ( A, B ), C )";
let (mut arena, root) = parse_test(model_str).unwrap();
let new_root = simplify(&mut arena, root);
let repr = arena.to_repr(new_root);
assert_eq!(repr, "X ( A, B, C )");
}
#[test]
fn end_to_end_pipeline() {
let model_str = "PO=(nodes={A, B, C}, order={A-->B, A-->C})";
let (arena, root) = parse_test(model_str).unwrap();
assert!(arena.validate_partial_orders(root).is_ok());
let pn: PowlPetriNetResult = to_petri_net::apply(&arena, root);
assert!(pn.net.places.len() >= 2);
let bpmn = to_bpmn::to_bpmn_xml(&arena, root);
assert!(bpmn.contains("<definitions"));
let tree = to_process_tree::apply(&arena, root);
assert!(tree.to_repr().contains("+"));
let complexity = measure(&arena, root);
assert_eq!(complexity.activity_count, 3);
let fp = crate::powl::footprints::apply(&arena, root);
assert!(fp.start_activities.contains("A"));
let log = EventLog {
traces: vec![
Trace {
case_id: "c1".into(),
events: vec![
crate::powl_event_log::Event {
name: "A".into(),
timestamp: None,
lifecycle: None,
attributes: std::collections::HashMap::new(),
},
crate::powl_event_log::Event {
name: "B".into(),
timestamp: None,
lifecycle: None,
attributes: std::collections::HashMap::new(),
},
crate::powl_event_log::Event {
name: "C".into(),
timestamp: None,
lifecycle: None,
attributes: std::collections::HashMap::new(),
},
],
},
Trace {
case_id: "c2".into(),
events: vec![
crate::powl_event_log::Event {
name: "A".into(),
timestamp: None,
lifecycle: None,
attributes: std::collections::HashMap::new(),
},
crate::powl_event_log::Event {
name: "C".into(),
timestamp: None,
lifecycle: None,
attributes: std::collections::HashMap::new(),
},
],
},
],
};
let fitness = compute_fitness(&pn.net, &pn.initial_marking, &pn.final_marking, &log);
assert_eq!(fitness.total_traces, 2);
assert!(fitness.percentage > 0.0);
assert!(fitness.avg_trace_fitness > 0.0);
}
}