use std::collections::{BTreeMap, HashMap};
use std::fmt::Write as _;
use chrono::{DateTime, FixedOffset, TimeZone};
use serde::{Deserialize, Serialize};
use crate::models::{PetriNet, PetriNetArc, PetriNetPlace, PetriNetTransition};
use crate::wf_to_powl::{wf_net_language, wf_net_to_powl_spec, PowlSpec, WfToPowlResult};
use wasm4pm_compat::ocel::{
OCELAttributeValue, OCELEvent, OCELEventAttribute, OCELObject, OCELRelationship, OCELType,
OCELTypeAttribute, ObjectTypeCardinality, OCEL,
};
pub const OBJECT_TYPES: [&str; 7] = [
"Customer", "Order", "Item", "Package", "Invoice", "Payment", "Employee",
];
pub const EVENT_TYPES: [&str; 9] = [
"Create Order",
"Confirm Order",
"Pick Item",
"Pack Package",
"Ship Package",
"Send Invoice",
"Receive Payment",
"Send Reminder",
"Cancel Order",
];
#[must_use]
pub fn field_net() -> PetriNet {
let places = [
"src",
"p_open",
"p_split",
"p_pick",
"p_packready",
"p_shipready",
"p_shipdone",
"p_inv",
"p_payready",
"p_paydone",
"sink",
];
let transitions: &[(&str, &str, bool)] = &[
("t_create", "Create Order", false),
("t_confirm", "Confirm Order", false),
("t_split", "τ_split", true),
("t_pick", "Pick Item", false),
("t_pack", "Pack Package", false),
("t_ship", "Ship Package", false),
("t_invoice", "Send Invoice", false),
("t_pay", "Receive Payment", false),
("t_join", "τ_join", true),
];
let arcs: &[(&str, &str)] = &[
("src", "t_create"),
("t_create", "p_open"),
("p_open", "t_confirm"),
("t_confirm", "p_split"),
("p_split", "t_split"),
("t_split", "p_pick"),
("t_split", "p_inv"),
("p_pick", "t_pick"),
("t_pick", "p_packready"),
("p_packready", "t_pack"),
("t_pack", "p_shipready"),
("p_shipready", "t_ship"),
("t_ship", "p_shipdone"),
("p_inv", "t_invoice"),
("t_invoice", "p_payready"),
("p_payready", "t_pay"),
("t_pay", "p_paydone"),
("p_shipdone", "t_join"),
("p_paydone", "t_join"),
("t_join", "sink"),
];
let mut initial: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
initial.insert("src".to_string(), 1usize);
let mut final_marking = BTreeMap::new();
final_marking.insert("sink".to_string(), 1usize);
PetriNet {
places: places
.iter()
.map(|p| PetriNetPlace {
id: (*p).to_string(),
label: (*p).to_string(),
marking: None,
})
.collect(),
transitions: transitions
.iter()
.map(|(id, label, invisible)| PetriNetTransition {
id: (*id).to_string(),
label: (*label).to_string(),
is_invisible: Some(*invisible),
})
.collect(),
arcs: arcs
.iter()
.map(|(from, to)| PetriNetArc {
from: (*from).to_string(),
to: (*to).to_string(),
weight: Some(1),
})
.collect(),
initial_marking: initial,
final_markings: vec![final_marking],
}
}
#[must_use]
pub fn field_powl() -> WfToPowlResult {
wf_net_to_powl_spec(&field_net())
}
#[must_use]
pub fn positive_order_traces() -> Vec<Vec<String>> {
let mut traces: Vec<Vec<String>> = wf_net_language(&field_net())
.map(|set| set.into_iter().collect())
.unwrap_or_default();
traces.sort();
traces
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum FieldTree {
Leaf { label: String },
Tau,
Sequence { children: Vec<FieldTree> },
Xor { children: Vec<FieldTree> },
Parallel { children: Vec<FieldTree> },
}
impl FieldTree {
#[must_use]
pub fn repr(&self) -> String {
match self {
FieldTree::Leaf { label } => label.clone(),
FieldTree::Tau => "τ".to_string(),
FieldTree::Sequence { children } => render_op("→", children),
FieldTree::Xor { children } => render_op("×", children),
FieldTree::Parallel { children } => render_op("∧", children),
}
}
}
fn render_op(op: &str, children: &[FieldTree]) -> String {
let inner: Vec<String> = children.iter().map(FieldTree::repr).collect();
format!("{op}( {} )", inner.join(", "))
}
#[must_use]
pub fn field_process_tree() -> FieldTree {
powl_to_field_tree(&field_powl().powl)
}
fn powl_to_field_tree(spec: &PowlSpec) -> FieldTree {
match spec {
PowlSpec::Transition { label } => FieldTree::Leaf {
label: label.clone(),
},
PowlSpec::Silent => FieldTree::Tau,
PowlSpec::Irreducible { .. } => FieldTree::Tau,
PowlSpec::ChoiceGraph { children, .. } => FieldTree::Xor {
children: children.iter().map(powl_to_field_tree).collect(),
},
PowlSpec::PartialOrder { children, order } => partial_order_to_tree(children, order),
}
}
fn partial_order_to_tree(children: &[PowlSpec], order: &[(usize, usize)]) -> FieldTree {
let n = children.len();
if n == 0 {
return FieldTree::Tau;
}
let mut prec = vec![vec![false; n]; n];
for &(i, j) in order {
if i < n && j < n {
prec[i][j] = true;
}
}
let members: Vec<usize> = (0..n).collect();
sp_decompose(children, &prec, &members)
}
fn sp_decompose(children: &[PowlSpec], prec: &[Vec<bool>], members: &[usize]) -> FieldTree {
if members.len() == 1 {
return powl_to_field_tree(&children[members[0]]);
}
if let Some(blocks) = series_blocks(prec, members) {
if blocks.len() >= 2 {
let block_trees: Vec<FieldTree> = blocks
.iter()
.map(|b| sp_decompose(children, prec, b))
.collect();
return FieldTree::Sequence {
children: block_trees,
};
}
}
let comps = comparability_components(prec, members);
if comps.len() >= 2 {
let comp_trees: Vec<FieldTree> = comps
.iter()
.map(|c| sp_decompose(children, prec, c))
.collect();
return FieldTree::Parallel {
children: comp_trees,
};
}
layered_sequence(children, prec, members)
}
fn series_blocks(prec: &[Vec<bool>], members: &[usize]) -> Option<Vec<Vec<usize>>> {
let order = topo_order(prec, members)?;
let m = order.len();
let mut blocks: Vec<Vec<usize>> = Vec::new();
let mut current: Vec<usize> = Vec::new();
for (idx, &node) in order.iter().enumerate() {
current.push(node);
let rest = &order[idx + 1..];
let clean_cut = rest.iter().all(|&r| current.iter().all(|&c| prec[c][r]));
if clean_cut && idx + 1 < m {
blocks.push(std::mem::take(&mut current));
}
}
if !current.is_empty() {
blocks.push(current);
}
Some(blocks)
}
fn topo_order(prec: &[Vec<bool>], members: &[usize]) -> Option<Vec<usize>> {
let set: std::collections::BTreeSet<usize> = members.iter().copied().collect();
let mut indeg: std::collections::BTreeMap<usize, usize> =
members.iter().map(|&m| (m, 0usize)).collect();
for &i in members {
for &j in members {
if prec[i][j] {
*indeg
.get_mut(&j)
.expect("invariant: j from members, all members are keys in indeg") += 1;
}
}
}
let mut ready: Vec<usize> = indeg
.iter()
.filter(|(_, &d)| d == 0)
.map(|(&n, _)| n)
.collect();
ready.sort_unstable();
let mut out = Vec::with_capacity(members.len());
while let Some(&next) = ready.first() {
ready.remove(0);
out.push(next);
for &j in &set {
if prec[next][j] {
let d = indeg
.get_mut(&j)
.expect("invariant: j from members, all members are keys in indeg");
*d -= 1;
if *d == 0 {
let pos = ready.binary_search(&j).unwrap_or_else(|e| e);
ready.insert(pos, j);
}
}
}
}
if out.len() == members.len() {
Some(out)
} else {
None
}
}
fn comparability_components(prec: &[Vec<bool>], members: &[usize]) -> Vec<Vec<usize>> {
let mut comp: std::collections::BTreeMap<usize, usize> = std::collections::BTreeMap::new();
let mut num = 0usize;
for &start in members {
if comp.contains_key(&start) {
continue;
}
let mut stack = vec![start];
comp.insert(start, num);
while let Some(u) = stack.pop() {
for &v in members {
if !comp.contains_key(&v) && (prec[u][v] || prec[v][u]) {
comp.insert(v, num);
stack.push(v);
}
}
}
num += 1;
}
let mut out: Vec<Vec<usize>> = vec![Vec::new(); num];
for (&node, &c) in &comp {
out[c].push(node);
}
for v in &mut out {
v.sort_unstable();
}
out
}
fn layered_sequence(children: &[PowlSpec], prec: &[Vec<bool>], members: &[usize]) -> FieldTree {
let mut remaining: Vec<usize> = members.to_vec();
let mut layers: Vec<Vec<usize>> = Vec::new();
while !remaining.is_empty() {
let mut layer: Vec<usize> = remaining
.iter()
.copied()
.filter(|&j| !remaining.iter().any(|&i| i != j && prec[i][j]))
.collect();
if layer.is_empty() {
layer = remaining.clone();
}
remaining.retain(|j| !layer.contains(j));
layers.push(layer);
}
let layer_trees: Vec<FieldTree> = layers
.iter()
.map(|layer| {
if layer.len() == 1 {
powl_to_field_tree(&children[layer[0]])
} else {
FieldTree::Parallel {
children: layer
.iter()
.map(|&i| powl_to_field_tree(&children[i]))
.collect(),
}
}
})
.collect();
if layer_trees.len() == 1 {
layer_trees
.into_iter()
.next()
.expect("invariant: layer_trees.len() == 1 checked above")
} else {
FieldTree::Sequence {
children: layer_trees,
}
}
}
#[must_use]
pub fn tree_language(tree: &FieldTree) -> std::collections::BTreeSet<Vec<String>> {
use std::collections::BTreeSet;
match tree {
FieldTree::Leaf { label } => {
let mut s = BTreeSet::new();
s.insert(vec![label.clone()]);
s
}
FieldTree::Tau => {
let mut s = BTreeSet::new();
s.insert(Vec::new());
s
}
FieldTree::Xor { children } => {
let mut s = BTreeSet::new();
for c in children {
s.extend(tree_language(c));
}
s
}
FieldTree::Sequence { children } => {
let mut acc: BTreeSet<Vec<String>> = BTreeSet::new();
acc.insert(Vec::new());
for c in children {
let lang = tree_language(c);
let mut next = BTreeSet::new();
for prefix in &acc {
for suffix in &lang {
let mut combined = prefix.clone();
combined.extend(suffix.iter().cloned());
next.insert(combined);
}
}
acc = next;
}
acc
}
FieldTree::Parallel { children } => {
let child_langs: Vec<BTreeSet<Vec<String>>> =
children.iter().map(tree_language).collect();
interleave(&child_langs)
}
}
}
fn interleave(
langs: &[std::collections::BTreeSet<Vec<String>>],
) -> std::collections::BTreeSet<Vec<String>> {
use std::collections::BTreeSet;
let mut acc: BTreeSet<Vec<String>> = BTreeSet::new();
acc.insert(Vec::new());
for lang in langs {
let mut next = BTreeSet::new();
for prefix in &acc {
for word in lang {
next.extend(interleave_two(prefix, word));
}
}
acc = next;
}
acc
}
fn interleave_two(a: &[String], b: &[String]) -> std::collections::BTreeSet<Vec<String>> {
use std::collections::BTreeSet;
let mut out = BTreeSet::new();
fn rec(
a: &[String],
b: &[String],
i: usize,
j: usize,
cur: &mut Vec<String>,
out: &mut BTreeSet<Vec<String>>,
) {
if i == a.len() && j == b.len() {
out.insert(cur.clone());
return;
}
if i < a.len() {
cur.push(a[i].clone());
rec(a, b, i + 1, j, cur, out);
cur.pop();
}
if j < b.len() {
cur.push(b[j].clone());
rec(a, b, i, j + 1, cur, out);
cur.pop();
}
}
rec(a, b, 0, 0, &mut Vec::new(), &mut out);
out
}
fn ts(day: u32, hour: u32) -> DateTime<FixedOffset> {
FixedOffset::east_opt(0)
.unwrap()
.with_ymd_and_hms(2026, 5, day, hour, 0, 0)
.single()
.expect("valid manufactured timestamp")
}
fn str_attr(name: &str, value: &str) -> OCELEventAttribute {
OCELEventAttribute {
name: name.to_string(),
value: OCELAttributeValue::String(value.to_string()),
}
}
fn e2o(object_id: &str, qualifier: &str) -> OCELRelationship {
OCELRelationship {
object_id: object_id.to_string(),
qualifier: qualifier.to_string(),
}
}
#[must_use]
pub fn field_ocel() -> OCEL {
let object_type = |name: &str| OCELType {
name: name.to_string(),
attributes: Vec::new(),
};
let event_type = |name: &str| OCELType {
name: name.to_string(),
attributes: vec![OCELTypeAttribute {
name: "resource".to_string(),
value_type: "string".to_string(),
}],
};
let objects = vec![
OCELObject {
id: "cust-1".to_string(),
object_type: "Customer".to_string(),
attributes: Vec::new(),
relationships: Vec::new(),
},
OCELObject {
id: "emp-1".to_string(),
object_type: "Employee".to_string(),
attributes: Vec::new(),
relationships: Vec::new(),
},
OCELObject {
id: "order-1".to_string(),
object_type: "Order".to_string(),
attributes: Vec::new(),
relationships: vec![
e2o("cust-1", "placed_by"),
e2o("item-1", "contains"),
e2o("pkg-1", "shipped_in"),
e2o("inv-1", "billed_by"),
],
},
OCELObject {
id: "item-1".to_string(),
object_type: "Item".to_string(),
attributes: Vec::new(),
relationships: Vec::new(),
},
OCELObject {
id: "pkg-1".to_string(),
object_type: "Package".to_string(),
attributes: Vec::new(),
relationships: vec![e2o("item-1", "packs")],
},
OCELObject {
id: "inv-1".to_string(),
object_type: "Invoice".to_string(),
attributes: Vec::new(),
relationships: vec![e2o("pay-1", "settled_by")],
},
OCELObject {
id: "pay-1".to_string(),
object_type: "Payment".to_string(),
attributes: Vec::new(),
relationships: Vec::new(),
},
];
let events = vec![
OCELEvent {
id: "e1".to_string(),
event_type: "Create Order".to_string(),
time: ts(1, 9),
attributes: vec![str_attr("resource", "emp-1")],
relationships: vec![e2o("order-1", "order"), e2o("cust-1", "customer")],
},
OCELEvent {
id: "e2".to_string(),
event_type: "Confirm Order".to_string(),
time: ts(1, 10),
attributes: vec![str_attr("resource", "emp-1")],
relationships: vec![e2o("order-1", "order")],
},
OCELEvent {
id: "e3".to_string(),
event_type: "Pick Item".to_string(),
time: ts(1, 11),
attributes: vec![str_attr("resource", "emp-1")],
relationships: vec![e2o("order-1", "order"), e2o("item-1", "item")],
},
OCELEvent {
id: "e4".to_string(),
event_type: "Pack Package".to_string(),
time: ts(1, 12),
attributes: vec![str_attr("resource", "emp-1")],
relationships: vec![
e2o("order-1", "order"),
e2o("pkg-1", "package"),
e2o("item-1", "item"),
],
},
OCELEvent {
id: "e5".to_string(),
event_type: "Ship Package".to_string(),
time: ts(1, 13),
attributes: vec![str_attr("resource", "emp-1")],
relationships: vec![e2o("order-1", "order"), e2o("pkg-1", "package")],
},
OCELEvent {
id: "e6".to_string(),
event_type: "Send Invoice".to_string(),
time: ts(2, 9),
attributes: vec![str_attr("resource", "emp-1")],
relationships: vec![e2o("order-1", "order"), e2o("inv-1", "invoice")],
},
OCELEvent {
id: "e7".to_string(),
event_type: "Receive Payment".to_string(),
time: ts(3, 9),
attributes: vec![str_attr("resource", "emp-1")],
relationships: vec![
e2o("order-1", "order"),
e2o("inv-1", "invoice"),
e2o("pay-1", "payment"),
],
},
];
OCEL {
event_types: EVENT_TYPES.iter().map(|n| event_type(n)).collect(),
object_types: OBJECT_TYPES.iter().map(|n| object_type(n)).collect(),
events,
objects,
}
}
#[must_use]
pub fn field_cardinality() -> BTreeMap<String, ObjectTypeCardinality> {
let mut card = BTreeMap::new();
card.insert(
"Order".to_string(),
ObjectTypeCardinality {
created_by: vec!["Create Order".to_string()],
terminated_by: vec!["Receive Payment".to_string(), "Cancel Order".to_string()],
schema: None,
min_count: Some(1),
max_count: Some(1),
},
);
card.insert(
"Payment".to_string(),
ObjectTypeCardinality {
created_by: vec!["Receive Payment".to_string()],
terminated_by: Vec::new(),
schema: None,
min_count: Some(0),
max_count: Some(1),
},
);
card
}
#[must_use]
fn order_flattened_trace() -> Vec<(String, String, DateTime<FixedOffset>)> {
let ocel = field_ocel();
let mut rows: Vec<(String, String, DateTime<FixedOffset>)> = ocel
.events
.iter()
.filter(|e| e.relationships.iter().any(|r| r.object_id == "order-1"))
.map(|e| (e.id.clone(), e.event_type.clone(), e.time))
.collect();
rows.sort_by_key(|x| (x.2, x.0.clone()));
rows
}
#[must_use]
pub fn field_xes() -> String {
let rows = order_flattened_trace();
let mut xes = String::new();
xes.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xes.push_str("<log xes.version=\"2.0\" xmlns=\"http://www.xes-standard.org/\">\n");
xes.push_str(" <trace>\n");
xes.push_str(" <string key=\"concept:name\" value=\"order-1\"/>\n");
for (id, ty, time) in &rows {
xes.push_str(" <event>\n");
let _ = write!(xes, " <string key=\"concept:name\" value=\"{ty}\"/>\n");
let _ = write!(
xes,
" <date key=\"time:timestamp\" value=\"{}\"/>\n",
time.to_rfc3339()
);
let _ = write!(xes, " <string key=\"id\" value=\"{id}\"/>\n");
xes.push_str(" </event>\n");
}
xes.push_str(" </trace>\n");
xes.push_str("</log>\n");
xes
}
#[must_use]
pub fn field_csv() -> String {
let rows = order_flattened_trace();
let mut csv = String::from("case_id,activity,timestamp,event_id\n");
for (id, ty, time) in &rows {
let _ = write!(csv, "order-1,{ty},{},{id}\n", time.to_rfc3339());
}
csv
}
#[must_use]
pub fn ocpq_query_fixtures() -> Vec<(String, serde_json::Value)> {
vec![
(
"confirmed_order_exists".to_string(),
serde_json::json!({
"box": {
"v0": { "kind": "Object", "object_type": "Order" },
"e1": { "kind": "Event", "event_type": "Confirm Order" }
},
"predicates": [
{ "kind": "E2O", "event": "e1", "object": "v0", "qualifier": "*" }
],
"constr": { "kind": "CBS", "activity": "Confirm Order", "min": 1, "max": 1 }
}),
),
(
"confirmed_order_paid_within_4w".to_string(),
serde_json::json!({
"box": {
"v0": { "kind": "Object", "object_type": "Order" },
"e1": { "kind": "Event", "event_type": "Confirm Order" }
},
"predicates": [
{ "kind": "E2O", "event": "e1", "object": "v0", "qualifier": "*" }
],
"constr": { "kind": "CBS", "activity": "Receive Payment", "min": 1, "max": 1 },
"child": {
"box": {
"e2": { "kind": "Event", "event_type": "Receive Payment" }
},
"predicates": [
{ "kind": "E2O", "event": "e2", "object": "v0", "qualifier": "*" },
{ "kind": "TBE", "from": "e1", "to": "e2", "tmin_s": 0, "tmax_s": 2419200 }
]
}
}),
),
]
}
#[must_use]
pub fn world_artifacts() -> Vec<(String, String)> {
let ocel = field_ocel();
let card = field_cardinality();
let powl = field_powl();
let tree = field_process_tree();
let net = field_net();
let positives = positive_order_traces();
let ocpq = ocpq_query_fixtures();
let pretty = |v: &serde_json::Value| serde_json::to_string_pretty(v).unwrap_or_default();
let mut files = vec![
(
"ocel-v2.json".to_string(),
serde_json::to_string_pretty(&ocel).unwrap_or_default(),
),
(
"object-types-cardinality.json".to_string(),
serde_json::to_string_pretty(&card).unwrap_or_default(),
),
(
"powl.json".to_string(),
serde_json::to_string_pretty(&powl.powl).unwrap_or_default(),
),
(
"process-tree.json".to_string(),
serde_json::to_string_pretty(&tree).unwrap_or_default(),
),
(
"wf-net.json".to_string(),
serde_json::to_string_pretty(&net).unwrap_or_default(),
),
(
"positive-traces.json".to_string(),
serde_json::to_string_pretty(&positives).unwrap_or_default(),
),
("order.xes".to_string(), field_xes()),
("order.csv".to_string(), field_csv()),
];
for (name, q) in &ocpq {
files.push((format!("ocpq-{name}.json"), pretty(q)));
}
files
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn field_net_has_all_happy_path_activities() {
let net = field_net();
let labels: std::collections::HashSet<&str> = net
.transitions
.iter()
.filter(|t| t.is_invisible != Some(true))
.map(|t| t.label.as_str())
.collect();
for a in [
"Create Order",
"Confirm Order",
"Pick Item",
"Pack Package",
"Ship Package",
"Send Invoice",
"Receive Payment",
] {
assert!(labels.contains(a), "field net missing activity {a}");
}
}
#[test]
fn positive_traces_nonempty_and_visible_only() {
let traces = positive_order_traces();
assert!(!traces.is_empty(), "positive corpus must not be empty");
for t in &traces {
assert!(
!t.iter().any(|a| a.starts_with('τ')),
"τ leaked into a trace: {t:?}"
);
}
}
}