#![allow(clippy::needless_range_loop)]
use crate::models::PetriNet;
use crate::soundness::StructuralNet;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum PowlSpec {
Transition { label: String },
Silent,
PartialOrder {
children: Vec<PowlSpec>,
order: Vec<(usize, usize)>,
},
ChoiceGraph {
children: Vec<PowlSpec>,
edges: Vec<(usize, usize)>,
start: usize,
end: usize,
},
Irreducible { transitions: Vec<String> },
}
impl PowlSpec {
#[must_use]
pub fn has_irreducible(&self) -> bool {
match self {
PowlSpec::Irreducible { .. } => true,
PowlSpec::Transition { .. } | PowlSpec::Silent => false,
PowlSpec::PartialOrder { children, .. } | PowlSpec::ChoiceGraph { children, .. } => {
children.iter().any(PowlSpec::has_irreducible)
}
}
}
#[must_use]
pub fn repr(&self) -> String {
match self {
PowlSpec::Transition { label } => label.clone(),
PowlSpec::Silent => "tau".to_string(),
PowlSpec::PartialOrder { children, order } => {
let kids: Vec<String> = children.iter().map(PowlSpec::repr).collect();
let edges: Vec<String> = order.iter().map(|(i, j)| format!("{i}->{j}")).collect();
format!(
"PO(nodes=[{}], order=[{}])",
kids.join(", "),
edges.join(", ")
)
}
PowlSpec::ChoiceGraph {
children,
edges,
start,
end,
} => {
let kids: Vec<String> = children.iter().map(PowlSpec::repr).collect();
let es: Vec<String> = edges.iter().map(|(i, j)| format!("{i}->{j}")).collect();
format!(
"CG(nodes=[{}], edges=[{}], start={start}, end={end})",
kids.join(", "),
es.join(", ")
)
}
PowlSpec::Irreducible { transitions } => {
format!("IRREDUCIBLE[{}]", transitions.join(", "))
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WfToPowlResult {
pub is_wf_net: bool,
pub converted: bool,
pub powl: PowlSpec,
pub repr: String,
pub reason: String,
}
#[derive(Debug, Clone)]
struct WorkNet {
place_ids: Vec<String>,
t_label: Vec<Option<String>>,
t_pre: Vec<Vec<usize>>,
t_post: Vec<Vec<usize>>,
p_pre: Vec<Vec<usize>>,
p_post: Vec<Vec<usize>>,
source: usize,
sink: usize,
}
impl WorkNet {
fn from_petri_net(net: &PetriNet) -> Option<WorkNet> {
let snet = StructuralNet::from_petri_net(net);
let wf = snet.is_workflow_net();
if !wf.is_wf_net {
return None;
}
let place_ids = snet.places.clone();
let t_label: Vec<Option<String>> = snet
.transitions
.iter()
.enumerate()
.map(|(i, _)| {
let lbl = &snet.transition_labels[i];
if snet.transition_invisible[i] || lbl.is_empty() || lbl == "tau" || lbl == "τ" {
None
} else {
Some(lbl.clone())
}
})
.collect();
let source_id = wf.source.as_ref()?;
let sink_id = wf.sink.as_ref()?;
let source = place_ids.iter().position(|p| p == source_id)?;
let sink = place_ids.iter().position(|p| p == sink_id)?;
Some(WorkNet {
place_ids,
t_label,
t_pre: snet.t_pre.clone(),
t_post: snet.t_post.clone(),
p_pre: snet.p_pre.clone(),
p_post: snet.p_post.clone(),
source,
sink,
})
}
#[inline]
fn n_transitions(&self) -> usize {
self.t_label.len()
}
#[inline]
fn n_places(&self) -> usize {
self.place_ids.len()
}
fn language(&self) -> Option<BTreeSet<Vec<String>>> {
let np = self.n_places();
let mut initial = vec![0u32; np];
initial[self.source] = 1;
let mut goal = vec![0u32; np];
goal[self.sink] = 1;
const MAX_STATES: usize = 200_000;
let mut result: BTreeSet<Vec<String>> = BTreeSet::new();
let mut seen: HashSet<(Vec<u32>, Vec<String>)> = HashSet::new();
let mut q: VecDeque<(Vec<u32>, Vec<String>)> = VecDeque::new();
q.push_back((initial.clone(), Vec::new()));
seen.insert((initial, Vec::new()));
let mut explored = 0usize;
while let Some((marking, trace)) = q.pop_front() {
explored += 1;
if explored > MAX_STATES {
return None;
}
if marking == goal {
result.insert(trace.clone());
}
for t in 0..self.n_transitions() {
if !self.t_pre[t].iter().all(|&p| marking[p] >= 1) {
continue;
}
let mut next = marking.clone();
for &p in &self.t_pre[t] {
next[p] -= 1;
}
for &p in &self.t_post[t] {
next[p] += 1;
}
if next.iter().any(|&c| c > 1) {
continue; }
let mut ntrace = trace.clone();
if let Some(lbl) = &self.t_label[t] {
ntrace.push(lbl.clone());
}
let key = (next.clone(), ntrace.clone());
if seen.insert(key) {
q.push_back((next, ntrace));
}
}
}
Some(result)
}
fn transition_reachability(&self) -> Vec<Vec<bool>> {
let nt = self.n_transitions();
let mut reach = vec![vec![false; nt]; nt];
for t in 0..nt {
let mut seen = HashSet::new();
let mut q: VecDeque<usize> = VecDeque::new();
for &p in &self.t_post[t] {
for &t2 in &self.p_post[p] {
if seen.insert(t2) {
q.push_back(t2);
}
}
}
while let Some(t2) = q.pop_front() {
reach[t][t2] = true;
for &p in &self.t_post[t2] {
for &t3 in &self.p_post[p] {
if seen.insert(t3) {
q.push_back(t3);
}
}
}
}
}
reach
}
}
struct Dsu {
parent: Vec<usize>,
}
impl Dsu {
fn new(n: usize) -> Self {
Dsu {
parent: (0..n).collect(),
}
}
fn find(&mut self, x: usize) -> usize {
let mut r = x;
while self.parent[r] != r {
r = self.parent[r];
}
let mut cur = x;
while self.parent[cur] != r {
let nxt = self.parent[cur];
self.parent[cur] = r;
cur = nxt;
}
r
}
fn union(&mut self, a: usize, b: usize) {
let (ra, rb) = (self.find(a), self.find(b));
if ra != rb {
let (lo, hi) = if ra < rb { (ra, rb) } else { (rb, ra) };
self.parent[hi] = lo;
}
}
fn groups(&mut self, n: usize) -> Vec<Vec<usize>> {
let mut by_root: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
for x in 0..n {
let r = self.find(x);
by_root.entry(r).or_default().push(x);
}
by_root.into_values().collect()
}
}
fn partition_mg(net: &WorkNet) -> Vec<Vec<usize>> {
let nt = net.n_transitions();
let mut dsu = Dsu::new(nt);
let reach = net.transition_reachability();
for p in 0..net.n_places() {
let outs = &net.p_post[p];
if outs.len() <= 1 {
continue;
}
let mut group: Vec<usize> = Vec::new();
for t in 0..nt {
let reachable_from = |b: usize| b == t || reach[b][t];
let some = outs.iter().any(|&b| reachable_from(b));
let all = outs.iter().all(|&b| reachable_from(b));
if some && !all {
group.push(t);
}
}
if group.len() > 1 {
let first = group[0];
for &t in &group[1..] {
dsu.union(first, t);
}
}
}
for p in 0..net.n_places() {
let ins = &net.p_pre[p];
if ins.len() <= 1 {
continue;
}
let mut group: Vec<usize> = Vec::new();
for t in 0..nt {
let reaches = |b: usize| b == t || reach[t][b];
let some = ins.iter().any(|&b| reaches(b));
let all = ins.iter().all(|&b| reaches(b));
if some && !all {
group.push(t);
}
}
if group.len() > 1 {
let first = group[0];
for &t in &group[1..] {
dsu.union(first, t);
}
}
}
dsu.groups(nt)
}
fn forward_restricted_reachability(net: &WorkNet, p: usize, t_stop: usize) -> HashSet<usize> {
let mut result = HashSet::new();
let mut q: VecDeque<usize> = VecDeque::new();
for &t in &net.p_post[p] {
if t != t_stop && result.insert(t) {
q.push_back(t);
}
}
while let Some(t) = q.pop_front() {
for &p2 in &net.t_post[t] {
for &t2 in &net.p_post[p2] {
if t2 != t_stop && result.insert(t2) {
q.push_back(t2);
}
}
}
}
result
}
fn backward_restricted_reachability(net: &WorkNet, p: usize, t_stop: usize) -> HashSet<usize> {
let mut result = HashSet::new();
let mut q: VecDeque<usize> = VecDeque::new();
for &t in &net.p_pre[p] {
if t != t_stop && result.insert(t) {
q.push_back(t);
}
}
while let Some(t) = q.pop_front() {
for &p2 in &net.t_pre[t] {
for &t2 in &net.p_pre[p2] {
if t2 != t_stop && result.insert(t2) {
q.push_back(t2);
}
}
}
}
result
}
fn partition_sm(net: &WorkNet) -> Vec<Vec<usize>> {
let nt = net.n_transitions();
let mut dsu = Dsu::new(nt);
for t_split in 0..nt {
let outs = net.t_post[t_split].clone();
if outs.len() <= 1 {
continue;
}
let reach: Vec<HashSet<usize>> = outs
.iter()
.map(|&p| forward_restricted_reachability(net, p, t_split))
.collect();
let mut threads: Vec<usize> = Vec::new();
for t in 0..nt {
if t == t_split {
continue;
}
let in_some = reach.iter().any(|r| r.contains(&t));
let out_some = reach.iter().any(|r| !r.contains(&t));
if in_some && out_some {
threads.push(t);
}
}
let mut group = threads;
group.push(t_split);
if group.len() > 1 {
let first = group[0];
for &t in &group[1..] {
dsu.union(first, t);
}
}
}
for t_join in 0..nt {
let ins = net.t_pre[t_join].clone();
if ins.len() <= 1 {
continue;
}
let reach: Vec<HashSet<usize>> = ins
.iter()
.map(|&p| backward_restricted_reachability(net, p, t_join))
.collect();
let mut threads: Vec<usize> = Vec::new();
for t in 0..nt {
if t == t_join {
continue;
}
let in_some = reach.iter().any(|r| r.contains(&t));
let out_some = reach.iter().any(|r| !r.contains(&t));
if in_some && out_some {
threads.push(t);
}
}
let mut group = threads;
group.push(t_join);
if group.len() > 1 {
let first = group[0];
for &t in &group[1..] {
dsu.union(first, t);
}
}
}
dsu.groups(nt)
}
fn entry_points(net: &WorkNet, part: &BTreeSet<usize>) -> Vec<usize> {
(0..net.n_places())
.filter(|&p| {
let into_part = net.p_post[p].iter().any(|t| part.contains(t));
if !into_part {
return false;
}
p == net.source || net.p_pre[p].iter().any(|t| !part.contains(t))
})
.collect()
}
fn exit_points(net: &WorkNet, part: &BTreeSet<usize>) -> Vec<usize> {
(0..net.n_places())
.filter(|&p| {
let from_part = net.p_pre[p].iter().any(|t| part.contains(t));
if !from_part {
return false;
}
p == net.sink || net.p_post[p].iter().any(|t| !part.contains(t))
})
.collect()
}
fn place_equiv_wrt(net: &WorkNet, p: usize, q: usize, part: &BTreeSet<usize>) -> bool {
let pre_p: BTreeSet<usize> = net.p_pre[p]
.iter()
.copied()
.filter(|t| part.contains(t))
.collect();
let pre_q: BTreeSet<usize> = net.p_pre[q]
.iter()
.copied()
.filter(|t| part.contains(t))
.collect();
let post_p: BTreeSet<usize> = net.p_post[p]
.iter()
.copied()
.filter(|t| part.contains(t))
.collect();
let post_q: BTreeSet<usize> = net.p_post[q]
.iter()
.copied()
.filter(|t| part.contains(t))
.collect();
pre_p == pre_q && post_p == post_q
}
fn is_conflict_hiding(net: &WorkNet, parts: &[BTreeSet<usize>]) -> bool {
if parts.len() < 2 {
return false;
}
for p in 0..net.n_places() {
let entries = parts
.iter()
.filter(|part| entry_points(net, part).contains(&p))
.count();
if entries > 1 {
return false;
}
let exits = parts
.iter()
.filter(|part| exit_points(net, part).contains(&p))
.count();
if exits > 1 {
return false;
}
}
for part in parts {
let ent = entry_points(net, part);
for i in 0..ent.len() {
for j in (i + 1)..ent.len() {
if !place_equiv_wrt(net, ent[i], ent[j], part) {
return false;
}
}
}
let ex = exit_points(net, part);
for i in 0..ex.len() {
for j in (i + 1)..ex.len() {
if !place_equiv_wrt(net, ex[i], ex[j], part) {
return false;
}
}
}
}
true
}
fn is_concurrency_hiding(net: &WorkNet, parts: &[BTreeSet<usize>]) -> bool {
if parts.len() < 2 {
return false;
}
parts
.iter()
.all(|part| entry_points(net, part).len() == 1 && exit_points(net, part).len() == 1)
}
fn project_part(net: &WorkNet, part: &BTreeSet<usize>) -> PetriNet {
use crate::models::{PetriNetArc, PetriNetPlace, PetriNetTransition};
let entries = entry_points(net, part);
let exits = exit_points(net, part);
let entry_set: BTreeSet<usize> = entries.iter().copied().collect();
let exit_set: BTreeSet<usize> = exits.iter().copied().collect();
let boundary: BTreeSet<usize> = entry_set.union(&exit_set).copied().collect();
let mut places: Vec<PetriNetPlace> = Vec::new();
let mut place_name =
|idx: usize, net: &WorkNet| -> String { format!("{}__proj", net.place_ids[idx]) };
let mut kept_places: Vec<usize> = Vec::new();
for p in 0..net.n_places() {
if boundary.contains(&p) {
continue;
}
let touches = net.p_pre[p].iter().any(|t| part.contains(t))
|| net.p_post[p].iter().any(|t| part.contains(t));
if touches {
kept_places.push(p);
places.push(PetriNetPlace {
id: place_name(p, net),
label: net.place_ids[p].clone(),
marking: None,
});
}
}
let ps_id = "p_s__proj".to_string();
let pe_id = "p_e__proj".to_string();
places.push(PetriNetPlace {
id: ps_id.clone(),
label: "p_s".into(),
marking: None,
});
places.push(PetriNetPlace {
id: pe_id.clone(),
label: "p_e".into(),
marking: None,
});
let mut transitions: Vec<PetriNetTransition> = Vec::new();
let mut t_name = |t: usize| -> String { format!("t{t}__proj") };
for &t in part {
transitions.push(PetriNetTransition {
id: t_name(t),
label: net
.t_label
.get(t)
.and_then(|l| l.clone())
.unwrap_or_else(|| "tau".into()),
is_invisible: Some(net.t_label[t].is_none()),
});
}
let mut arcs: Vec<PetriNetArc> = Vec::new();
let arc = |from: String, to: String| PetriNetArc {
from,
to,
weight: Some(1),
};
for &t in part {
for &p in &net.t_pre[t] {
let from = if entry_set.contains(&p) {
ps_id.clone()
} else if kept_places.contains(&p) {
place_name(p, net)
} else if exit_set.contains(&p) {
ps_id.clone()
} else {
continue;
};
arcs.push(arc(from, t_name(t)));
}
for &p in &net.t_post[t] {
let to = if exit_set.contains(&p) {
pe_id.clone()
} else if kept_places.contains(&p) {
place_name(p, net)
} else if entry_set.contains(&p) {
pe_id.clone()
} else {
continue;
};
arcs.push(arc(t_name(t), to));
}
}
let mut initial_marking = std::collections::BTreeMap::new();
initial_marking.insert(ps_id.clone(), 1usize);
let mut final_mark = std::collections::BTreeMap::new();
final_mark.insert(pe_id.clone(), 1usize);
let raw = PetriNet {
places,
transitions,
arcs,
initial_marking,
final_markings: vec![final_mark],
};
normalize(raw, &ps_id, &pe_id)
}
fn normalize(mut net: PetriNet, ps_id: &str, pe_id: &str) -> PetriNet {
use crate::models::{PetriNetArc, PetriNetPlace, PetriNetTransition};
let has_incoming_ps = net.arcs.iter().any(|a| a.to == ps_id);
let has_outgoing_pe = net.arcs.iter().any(|a| a.from == pe_id);
if has_incoming_ps {
let new_src = "p_src__norm".to_string();
let tau_s = "tau_src__norm".to_string();
net.places.push(PetriNetPlace {
id: new_src.clone(),
label: "src".into(),
marking: None,
});
net.transitions.push(PetriNetTransition {
id: tau_s.clone(),
label: "tau".into(),
is_invisible: Some(true),
});
net.arcs.push(PetriNetArc {
from: new_src.clone(),
to: tau_s.clone(),
weight: Some(1),
});
net.arcs.push(PetriNetArc {
from: tau_s,
to: ps_id.to_string(),
weight: Some(1),
});
net.initial_marking.clear();
net.initial_marking.insert(new_src, 1usize);
}
if has_outgoing_pe {
let new_sink = "p_sink__norm".to_string();
let tau_e = "tau_sink__norm".to_string();
net.places.push(PetriNetPlace {
id: new_sink.clone(),
label: "sink".into(),
marking: None,
});
net.transitions.push(PetriNetTransition {
id: tau_e.clone(),
label: "tau".into(),
is_invisible: Some(true),
});
net.arcs.push(PetriNetArc {
from: pe_id.to_string(),
to: tau_e.clone(),
weight: Some(1),
});
net.arcs.push(PetriNetArc {
from: tau_e,
to: new_sink.clone(),
weight: Some(1),
});
let mut fm = std::collections::BTreeMap::new();
fm.insert(new_sink, 1usize);
net.final_markings = vec![fm];
}
net
}
fn execution_order(net: &WorkNet, parts: &[BTreeSet<usize>]) -> Vec<(usize, usize)> {
let n = parts.len();
let exits: Vec<BTreeSet<usize>> = parts
.iter()
.map(|p| exit_points(net, p).into_iter().collect())
.collect();
let entries: Vec<BTreeSet<usize>> = parts
.iter()
.map(|p| entry_points(net, p).into_iter().collect())
.collect();
let mut adj = vec![vec![false; n]; n];
for i in 0..n {
for j in 0..n {
if i == j {
continue;
}
if exits[i].intersection(&entries[j]).next().is_some() {
adj[i][j] = true;
}
}
}
for k in 0..n {
for i in 0..n {
if adj[i][k] {
for j in 0..n {
if adj[k][j] {
adj[i][j] = true;
}
}
}
}
}
let mut edges = Vec::new();
for i in 0..n {
for j in 0..n {
if adj[i][j] {
edges.push((i, j));
}
}
}
edges
}
fn execution_flow(net: &WorkNet, parts: &[BTreeSet<usize>]) -> (Vec<(usize, usize)>, usize, usize) {
let n = parts.len();
let start = n;
let end = n + 1;
let exits: Vec<Vec<usize>> = parts.iter().map(|p| exit_points(net, p)).collect();
let entries: Vec<Vec<usize>> = parts.iter().map(|p| entry_points(net, p)).collect();
let mut edges = Vec::new();
for i in 0..n {
for j in 0..n {
if i == j {
continue;
}
let exit_i: BTreeSet<usize> = exits[i].iter().copied().collect();
let entry_j: BTreeSet<usize> = entries[j].iter().copied().collect();
if exit_i.intersection(&entry_j).next().is_some() {
edges.push((i, j));
}
}
}
for i in 0..n {
if entries[i].contains(&net.source) {
edges.push((start, i));
}
if exits[i].contains(&net.sink) {
edges.push((i, end));
}
}
(edges, start, end)
}
const MAX_DEPTH: usize = 256;
fn convert_net(net: &WorkNet, depth: usize) -> PowlSpec {
if net.n_transitions() == 1 && net.n_places() == 2 {
let t = 0;
let pre = &net.t_pre[t];
let post = &net.t_post[t];
if pre.len() == 1 && post.len() == 1 && pre[0] == net.source && post[0] == net.sink {
return match &net.t_label[t] {
Some(l) => PowlSpec::Transition { label: l.clone() },
None => PowlSpec::Silent,
};
}
}
if depth >= MAX_DEPTH {
return irreducible_leaf(net);
}
let net_lang = net.language();
let mg = partition_mg(net);
let mg_parts: Vec<BTreeSet<usize>> = mg.iter().map(|g| g.iter().copied().collect()).collect();
if mg_parts.len() > 1 && is_conflict_hiding(net, &mg_parts) {
let progress = mg_parts.iter().all(|p| p.len() < net.n_transitions());
if progress {
if let Some(children) = project_and_recurse(net, &mg_parts, depth, true) {
let order = execution_order(net, &mg_parts);
let candidate = PowlSpec::PartialOrder { children, order };
if language_matches(&candidate, net_lang.as_ref()) {
return candidate;
}
}
}
}
let sm = partition_sm(net);
let sm_parts: Vec<BTreeSet<usize>> = sm.iter().map(|g| g.iter().copied().collect()).collect();
if sm_parts.len() > 1 && is_concurrency_hiding(net, &sm_parts) {
let progress = sm_parts.iter().all(|p| p.len() < net.n_transitions());
if progress {
if let Some(children) = project_and_recurse(net, &sm_parts, depth, false) {
let (edges, start, end) = execution_flow(net, &sm_parts);
let candidate = PowlSpec::ChoiceGraph {
children,
edges,
start,
end,
};
if language_matches(&candidate, net_lang.as_ref()) {
return candidate;
}
}
}
}
irreducible_leaf(net)
}
fn language_matches(candidate: &PowlSpec, net_lang: Option<&BTreeSet<Vec<String>>>) -> bool {
match net_lang {
None => true,
Some(target) => {
if candidate.has_irreducible() {
return false;
}
powl_language(candidate) == *target
}
}
}
fn project_and_recurse(
net: &WorkNet,
parts: &[BTreeSet<usize>],
depth: usize,
_is_mg: bool,
) -> Option<Vec<PowlSpec>> {
let mut children = Vec::with_capacity(parts.len());
for part in parts {
let child_pn = project_part(net, part);
let child_work = WorkNet::from_petri_net(&child_pn)?;
children.push(convert_net(&child_work, depth + 1));
}
Some(children)
}
fn irreducible_leaf(net: &WorkNet) -> PowlSpec {
let transitions: Vec<String> = net.t_label.iter().filter_map(|l| l.clone()).collect();
PowlSpec::Irreducible { transitions }
}
#[must_use]
pub fn wf_net_to_powl_spec(net: &PetriNet) -> WfToPowlResult {
let Some(work) = WorkNet::from_petri_net(net) else {
let labels: Vec<String> = net
.transitions
.iter()
.filter(|t| !t.is_invisible.unwrap_or(false))
.map(|t| t.label.clone())
.collect();
let powl = PowlSpec::Irreducible {
transitions: labels,
};
let repr = powl.repr();
return WfToPowlResult {
is_wf_net: false,
converted: false,
powl,
repr,
reason: "input is not a structural WF-net (Def 3.3): no unique source/sink \
or disconnected nodes"
.to_string(),
};
};
let powl = convert_net(&work, 0);
let irreducible = powl.has_irreducible();
let repr = powl.repr();
let reason = if irreducible {
"conversion incomplete: net is outside the separable class (Def 3.13); an \
irreducible fragment fell through (Section 4.4 fall-through)"
.to_string()
} else {
"converted to POWL 2.0; net is separable (Def 3.13), language preserved \
(Section 5)"
.to_string()
};
WfToPowlResult {
is_wf_net: true,
converted: !irreducible,
powl,
repr,
reason,
}
}
#[must_use]
pub fn powl_language(spec: &PowlSpec) -> BTreeSet<Vec<String>> {
match spec {
PowlSpec::Transition { label } => {
let mut s = BTreeSet::new();
s.insert(vec![label.clone()]);
s
}
PowlSpec::Silent => {
let mut s = BTreeSet::new();
s.insert(Vec::new());
s
}
PowlSpec::Irreducible { .. } => {
BTreeSet::new()
}
PowlSpec::PartialOrder { children, order } => {
let child_langs: Vec<BTreeSet<Vec<String>>> =
children.iter().map(powl_language).collect();
let n = children.len();
let mut prec = vec![vec![false; n]; n];
for &(i, j) in order {
if i < n && j < n {
prec[i][j] = true;
}
}
order_preserving_shuffle(&child_langs, &prec)
}
PowlSpec::ChoiceGraph {
children,
edges,
start,
end,
} => {
let child_langs: Vec<BTreeSet<Vec<String>>> =
children.iter().map(powl_language).collect();
choice_graph_language(&child_langs, edges, *start, *end, children.len())
}
}
}
fn order_preserving_shuffle(
child_langs: &[BTreeSet<Vec<String>>],
prec: &[Vec<bool>],
) -> BTreeSet<Vec<String>> {
let n = child_langs.len();
if n == 0 {
let mut s = BTreeSet::new();
s.insert(Vec::new());
return s;
}
let mut result = BTreeSet::new();
let selections = cartesian(child_langs);
for combo in selections {
interleave(&combo, prec, n, &mut result);
}
result
}
fn cartesian(child_langs: &[BTreeSet<Vec<String>>]) -> Vec<Vec<Vec<String>>> {
let mut acc: Vec<Vec<Vec<String>>> = vec![Vec::new()];
for lang in child_langs {
let mut next = Vec::new();
for prefix in &acc {
for seq in lang {
let mut p = prefix.clone();
p.push(seq.clone());
next.push(p);
}
}
acc = next;
}
acc
}
fn interleave(
combo: &[Vec<String>],
prec: &[Vec<bool>],
n: usize,
result: &mut BTreeSet<Vec<String>>,
) {
let progress: Vec<usize> = vec![0; n];
let mut current: Vec<String> = Vec::new();
interleave_rec(combo, prec, n, &progress, &mut current, result);
}
fn interleave_rec(
combo: &[Vec<String>],
prec: &[Vec<bool>],
n: usize,
progress: &[usize],
current: &mut Vec<String>,
result: &mut BTreeSet<Vec<String>>,
) {
if (0..n).all(|i| progress[i] >= combo[i].len()) {
result.insert(current.clone());
return;
}
for i in 0..n {
if progress[i] >= combo[i].len() {
continue;
}
let blocked = (0..n).any(|j| prec[j][i] && progress[j] < combo[j].len());
if blocked {
continue;
}
let tok = combo[i][progress[i]].clone();
current.push(tok);
let mut next = progress.to_vec();
next[i] += 1;
interleave_rec(combo, prec, n, &next, current, result);
current.pop();
}
}
fn choice_graph_language(
child_langs: &[BTreeSet<Vec<String>>],
edges: &[(usize, usize)],
start: usize,
end: usize,
n_children: usize,
) -> BTreeSet<Vec<String>> {
let max_node = edges
.iter()
.flat_map(|&(a, b)| [a, b])
.max()
.unwrap_or(end)
.max(end);
let mut adj: Vec<Vec<usize>> = vec![Vec::new(); max_node + 1];
for &(a, b) in edges {
adj[a].push(b);
}
let mut result = BTreeSet::new();
let mut prefix: Vec<String> = Vec::new();
let mut visits = vec![0usize; max_node + 1];
cg_dfs(
&adj,
child_langs,
start,
end,
n_children,
&mut prefix,
&mut visits,
&mut result,
);
result
}
const MAX_CYCLE_UNROLL: usize = 2;
#[allow(clippy::too_many_arguments)]
fn cg_dfs(
adj: &[Vec<usize>],
child_langs: &[BTreeSet<Vec<String>>],
node: usize,
end: usize,
n_children: usize,
prefix: &mut Vec<String>,
visits: &mut [usize],
result: &mut BTreeSet<Vec<String>>,
) {
if node == end {
result.insert(prefix.clone());
return;
}
if visits[node] > MAX_CYCLE_UNROLL {
return;
}
visits[node] += 1;
if node < n_children {
for seq in &child_langs[node] {
let added = seq.len();
prefix.extend(seq.iter().cloned());
for &nxt in &adj[node] {
cg_dfs(
adj,
child_langs,
nxt,
end,
n_children,
prefix,
visits,
result,
);
}
prefix.truncate(prefix.len() - added);
}
} else {
for &nxt in &adj[node] {
cg_dfs(
adj,
child_langs,
nxt,
end,
n_children,
prefix,
visits,
result,
);
}
}
visits[node] -= 1;
}
#[must_use]
pub fn wf_net_language(net: &PetriNet) -> Option<BTreeSet<Vec<String>>> {
WorkNet::from_petri_net(net)?.language()
}
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
fn wf_to_powl_json(net: &PetriNet) -> serde_json::Value {
let result = wf_net_to_powl_spec(net);
serde_json::json!({
"is_wf_net": result.is_wf_net,
"converted": result.converted,
"powl": result.powl,
"repr": result.repr,
"reason": result.reason,
})
}
#[cfg(not(target_arch = "wasm32"))]
#[must_use]
pub fn wf_net_to_powl_native(net: &PetriNet) -> String {
wf_to_powl_json(net).to_string()
}
#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
pub fn wf_net_to_powl(petri_net_handle: &str) -> Result<JsValue, JsValue> {
use crate::state::{get_or_init_state, StoredObject};
use crate::utilities::to_js_str;
let net = get_or_init_state().with_object(petri_net_handle, |obj| match obj {
Some(StoredObject::PetriNet(pn)) => Ok(pn.clone()),
Some(_) => Err(crate::error::js_val("Handle is not a PetriNet")),
None => Err(crate::error::js_val("PetriNet not found")),
})?;
to_js_str(&wf_to_powl_json(&net))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{PetriNetArc, PetriNetPlace, PetriNetTransition};
use std::collections::HashMap;
fn p(id: &str) -> PetriNetPlace {
PetriNetPlace {
id: id.into(),
label: id.into(),
marking: None,
}
}
fn t(id: &str, label: &str) -> PetriNetTransition {
PetriNetTransition {
id: id.into(),
label: label.into(),
is_invisible: Some(false),
}
}
fn a(from: &str, to: &str) -> PetriNetArc {
PetriNetArc {
from: from.into(),
to: to.into(),
weight: Some(1),
}
}
fn mk(places: &[&str], ts: &[(&str, &str)], arcs: &[(&str, &str)], src: &str) -> PetriNet {
let mut im: BTreeMap<String, usize> = BTreeMap::new();
im.insert(src.to_string(), 1usize);
PetriNet {
places: places.iter().map(|x| p(x)).collect(),
transitions: ts.iter().map(|(i, l)| t(i, l)).collect(),
arcs: arcs.iter().map(|(f, x)| a(f, x)).collect(),
initial_marking: im,
final_markings: Vec::new(),
}
}
#[test]
fn base_case_single_transition() {
let net = mk(
&["src", "sink"],
&[("tA", "A")],
&[("src", "tA"), ("tA", "sink")],
"src",
);
let work = WorkNet::from_petri_net(&net).expect("WF-net");
let spec = convert_net(&work, 0);
assert_eq!(spec, PowlSpec::Transition { label: "A".into() });
}
#[test]
fn dsu_groups_are_deterministic() {
let mut d = Dsu::new(4);
d.union(2, 0);
d.union(3, 1);
let g = d.groups(4);
assert_eq!(g, vec![vec![0, 2], vec![1, 3]]);
}
#[test]
fn transition_reachability_sequence() {
let net = mk(
&["src", "p1", "sink"],
&[("tA", "A"), ("tB", "B")],
&[("src", "tA"), ("tA", "p1"), ("p1", "tB"), ("tB", "sink")],
"src",
);
let work = WorkNet::from_petri_net(&net).unwrap();
let r = work.transition_reachability();
assert!(r[0][1], "tA reaches tB");
assert!(!r[1][0], "tB does not reach tA");
}
}