use crate::{
ebi_traits::ebi_trait_finite_stochastic_language::EbiTraitFiniteStochasticLanguage,
semantics::{labelled_petri_net_semantics::LPNMarking, semantics::Semantics},
stochastic_semantics::stochastic_semantics::StochasticSemantics,
techniques::tau_removal::TauRemoval,
};
use ebi_objects::{
Activity, ActivityKey, AutomatonState, HasActivityKey,
StochasticNondeterministicFiniteAutomaton,
anyhow::{Ok, Result, anyhow},
ebi_arithmetic::{Fraction, One, Recip, ebi_number::Zero},
ebi_objects::{
finite_stochastic_language::FiniteStochasticLanguage,
stochastic_labelled_petri_net::StochasticLabelledPetriNet,
},
};
use itertools::Itertools;
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use std::{cmp::max, collections::VecDeque, fmt::Display};
#[derive(Debug, Clone)]
pub struct MarkovianAbstraction {
pub activity_key: ActivityKey,
pub order: usize,
pub abstraction: FxHashMap<Vec<Activity>, Fraction>,
pub start_activity: Activity,
pub end_activity: Activity,
}
impl MarkovianAbstraction {
pub fn create_start_end(activity_key: &mut ActivityKey) -> (Activity, Activity) {
let max_len = activity_key
.activity2name
.iter()
.map(|name| name.len())
.max()
.unwrap_or(0);
let start_activity = activity_key.process_activity(&"+".repeat(max_len + 1));
let end_activity = activity_key.process_activity(&"-".repeat(max_len + 1));
(start_activity, end_activity)
}
pub fn harmonise_start_end(&mut self, other: &mut MarkovianAbstraction) {
{
let max_len_start = max(
self.activity_key
.get_activity_label(&self.start_activity)
.len(),
other
.activity_key
.get_activity_label(&other.start_activity)
.len(),
);
{
let self_name = self
.activity_key
.get_activity_label(&self.start_activity)
.to_string();
let self_id = self.activity_key.get_id_from_activity(self.start_activity);
self.activity_key.name2activity.remove(&self_name);
self.activity_key
.name2activity
.insert("+".repeat(max_len_start), self.start_activity);
self.activity_key.activity2name[self_id] = "+".repeat(max_len_start);
}
{
let other_name = other
.activity_key
.get_activity_label(&other.start_activity)
.to_string();
let other_id = other
.activity_key
.get_id_from_activity(other.start_activity);
other.activity_key.name2activity.remove(&other_name);
other
.activity_key
.name2activity
.insert("+".repeat(max_len_start), other.start_activity);
other.activity_key.activity2name[other_id] = "+".repeat(max_len_start);
}
}
{
let max_len_end = max(
self.activity_key
.get_activity_label(&self.end_activity)
.len(),
other
.activity_key
.get_activity_label(&other.end_activity)
.len(),
);
{
let self_name = self
.activity_key
.get_activity_label(&self.end_activity)
.to_string();
let self_id = self.activity_key.get_id_from_activity(self.end_activity);
self.activity_key.name2activity.remove(&self_name);
self.activity_key
.name2activity
.insert("-".repeat(max_len_end), self.end_activity);
self.activity_key.activity2name[self_id] = "-".repeat(max_len_end);
}
{
let other_name = other
.activity_key
.get_activity_label(&other.end_activity)
.to_string();
let other_id = other.activity_key.get_id_from_activity(other.end_activity);
other.activity_key.name2activity.remove(&other_name);
other
.activity_key
.name2activity
.insert("-".repeat(max_len_end), other.end_activity);
other.activity_key.activity2name[other_id] = "-".repeat(max_len_end);
}
}
}
}
impl Display for MarkovianAbstraction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (trace, probability) in &self.abstraction {
writeln!(
f,
"[{}]: {}",
trace
.iter()
.map(|activity| self.activity_key.deprocess_activity(activity))
.join(", "),
probability
)?;
}
write!(f, "")
}
}
impl From<MarkovianAbstraction> for FiniteStochasticLanguage {
fn from(ma: MarkovianAbstraction) -> Self {
let traces = ma.abstraction.into_iter().collect();
FiniteStochasticLanguage::new_raw(traces, ma.activity_key)
}
}
pub trait AbstractMarkovian {
fn abstract_markovian(&mut self, order: usize) -> Result<MarkovianAbstraction>;
}
impl AbstractMarkovian for StochasticLabelledPetriNet {
fn abstract_markovian(&mut self, order: usize) -> Result<MarkovianAbstraction> {
if order < 1 {
return Err(anyhow!(
"order must be at least 1 for Markovian abstraction"
));
}
let mut snfa = build_embedded_snfa(self)?;
snfa.abstract_markovian(order)
}
}
impl AbstractMarkovian for StochasticNondeterministicFiniteAutomaton {
fn abstract_markovian(&mut self, order: usize) -> Result<MarkovianAbstraction> {
self.remove_tau_transitions()?;
let (start_activity, end_activity) =
MarkovianAbstraction::create_start_end(&mut self.activity_key);
add_artificial_start_end_to_snfa(self, start_activity, end_activity)?;
let initial_state = if let Some(initial) = self.initial_state {
initial
} else {
return Ok(MarkovianAbstraction {
activity_key: self.activity_key.clone(),
order,
abstraction: FxHashMap::default(),
start_activity,
end_activity,
});
};
let n = self.termination_probabilities.len();
let delta = build_delta(&self);
let mut a_sparse: Vec<FxHashMap<usize, Fraction>> = vec![FxHashMap::default(); n];
for i in 0..n {
a_sparse[i].insert(i, Fraction::one());
for (&j, p) in &delta[i] {
a_sparse[j]
.entry(i)
.and_modify(|v| *v -= p)
.or_insert_with(|| -p);
}
}
let mut b = vec![Fraction::zero(); n];
b[initial_state] = Fraction::one();
let x = if n < 100 {
solve_sparse_linear_system(&mut a_sparse, b)?
} else {
solve_sparse_linear_system_optimized(&mut a_sparse, b)?
};
let phi_ids = compute_phi_ids(&self, order, start_activity, end_activity);
let translate = |ids: &Vec<usize>| -> Vec<Activity> {
let mut vec: Vec<Activity> = Vec::with_capacity(ids.len());
for id in ids.iter() {
vec.push(self.activity_key.get_activity_by_id(*id));
}
vec
};
let mut f_l_k: FxHashMap<Vec<Activity>, Fraction> = FxHashMap::default();
for (q, map) in phi_ids.iter().enumerate() {
for (gamma_ids, phi_val) in map {
let gamma = translate(gamma_ids);
let contribution = &x[q] * phi_val;
f_l_k
.entry(gamma)
.and_modify(|v| *v = &*v + &contribution)
.or_insert(contribution);
}
}
let total: Fraction = f_l_k.values().sum();
let mut abstraction = FxHashMap::default();
for (gamma, val) in f_l_k {
abstraction.insert(gamma, &val / &total);
}
Ok(MarkovianAbstraction {
activity_key: self.activity_key.clone(),
order,
abstraction,
start_activity,
end_activity,
})
}
}
impl AbstractMarkovian for dyn EbiTraitFiniteStochasticLanguage {
fn abstract_markovian(&mut self, order: usize) -> Result<MarkovianAbstraction> {
if order < 1 {
return Err(anyhow!(
"the order must be at least 1 for Markovian abstraction"
));
}
let mut f_l_k: FxHashMap<Vec<Activity>, Fraction> = FxHashMap::default();
let mut activity_key = self.activity_key().clone();
let (start_activity, end_activity) =
MarkovianAbstraction::create_start_end(&mut activity_key);
for (trace, probability) in self.iter_traces_probabilities() {
let m_sigma_k = compute_multiset_abstraction_for_trace_with_key(
&trace,
order,
start_activity,
end_activity,
);
for (subtrace, occurrences) in m_sigma_k {
let occurrences_as_fraction = Fraction::from(occurrences);
let contribution = {
let p_ref: &Fraction = probability;
let o_ref: &Fraction = &occurrences_as_fraction;
p_ref * o_ref };
f_l_k
.entry(subtrace)
.and_modify(|current| {
*current += &contribution; })
.or_insert(contribution); }
}
let total: Fraction = f_l_k.values().sum();
let mut abstraction = FxHashMap::default();
for (subtrace, count) in f_l_k {
let count_ref: &Fraction = &count;
let total_ref: &Fraction = &total;
abstraction.insert(subtrace, count_ref / total_ref);
}
Ok(MarkovianAbstraction {
activity_key,
order,
abstraction,
start_activity,
end_activity,
})
}
}
fn compute_multiset_abstraction_for_trace_with_key(
trace: &[Activity],
k: usize,
start_activity: Activity,
end_activity: Activity,
) -> FxHashMap<Vec<Activity>, usize> {
let mut augmented_ids = Vec::with_capacity(trace.len() + 2);
augmented_ids.push(start_activity);
augmented_ids.extend_from_slice(trace);
augmented_ids.push(end_activity);
let id_multiset = compute_multiset_k_trimmed_subtraces_iterative_ids(augmented_ids, k);
let mut result: FxHashMap<Vec<Activity>, usize> = FxHashMap::default();
result.reserve(id_multiset.len());
for (sub_ids, cnt) in id_multiset {
result.insert(sub_ids, cnt);
}
result
}
fn compute_multiset_k_trimmed_subtraces_iterative_ids(
trace: Vec<Activity>,
k: usize,
) -> FxHashMap<Vec<Activity>, usize> {
let mut result: FxHashMap<Vec<Activity>, usize> = FxHashMap::default();
if trace.len() <= k {
result.insert(trace, 1);
return result;
}
let mut ring = Vec::with_capacity(k);
ring.extend_from_slice(&trace[..k]);
let mut head: usize = 0;
let mut tmp = Vec::with_capacity(k);
let make_key = |ring: &Vec<Activity>, head: usize, tmp: &mut Vec<Activity>| -> Vec<Activity> {
tmp.clear();
tmp.extend_from_slice(&ring[head..]);
tmp.extend_from_slice(&ring[..head]);
tmp.clone()
};
result.insert(make_key(&ring, head, &mut tmp), 1);
for &next_id in &trace[k..] {
ring[head] = next_id; head = (head + 1) % k; let key = make_key(&ring, head, &mut tmp);
*result.entry(key).or_insert(0) += 1;
}
result
}
pub fn build_embedded_snfa(
net: &StochasticLabelledPetriNet,
) -> Result<StochasticNondeterministicFiniteAutomaton> {
let mut snfa = StochasticNondeterministicFiniteAutomaton::new();
snfa.activity_key = net.activity_key().clone();
let mut state2snfa_state: FxHashMap<LPNMarking, AutomatonState> = FxHashMap::default();
let mut queue: VecDeque<LPNMarking> = VecDeque::new();
let initial_state = if let Some(initial) = net.get_initial_state() {
initial
} else {
return Ok(snfa);
};
state2snfa_state.insert(initial_state.clone(), AutomatonState::zero());
queue.push_back(initial_state);
while let Some(state) = queue.pop_front() {
let snfa_state = *state2snfa_state.get(&state).unwrap();
let enabled_transitions = net.get_enabled_transitions(&state);
if enabled_transitions.is_empty() {
continue;
}
let weight_sum = net.get_total_weight_of_enabled_transitions(&state)?;
for &transition in &enabled_transitions {
let weight = net.get_transition_weight(&state, transition)?.clone();
let prob = &weight / &weight_sum;
let mut next_state = state.clone();
net.execute_transition(&mut next_state, transition)?;
let snfa_target = *state2snfa_state
.entry(next_state.clone())
.or_insert_with(|| {
let new_state = snfa.add_state();
queue.push_back(next_state);
new_state
});
let label = net.get_transition_label(transition);
snfa.add_transition(snfa_state, label, snfa_target, prob)?;
}
}
snfa.initial_state = Some(AutomatonState::zero());
Ok(snfa)
}
fn add_artificial_start_end_to_snfa(
snfa: &mut StochasticNondeterministicFiniteAutomaton,
start_activity: Activity,
end_activity: Activity,
) -> Result<()> {
let initial_state = if let Some(initial) = snfa.initial_state {
initial
} else {
return Ok(());
};
let q_minus = snfa.add_state();
for state in 0..snfa.termination_probabilities.len() - 1 {
if !snfa.termination_probabilities[state].is_zero() {
let final_prob = snfa.termination_probabilities[state].clone();
snfa.add_transition(
AutomatonState::of(state),
Some(end_activity),
q_minus,
final_prob,
)?;
}
}
let q_plus = snfa.add_state();
snfa.add_transition(q_plus, Some(start_activity), initial_state, Fraction::one())?;
snfa.initial_state = Some(q_plus);
Ok(())
}
fn build_delta(
snfa: &StochasticNondeterministicFiniteAutomaton,
) -> Vec<FxHashMap<AutomatonState, Fraction>> {
let n = snfa.termination_probabilities.len();
let mut delta = vec![FxHashMap::<AutomatonState, Fraction>::default(); n];
for transition in 0..snfa.sources.len() {
delta[snfa.sources[transition]]
.entry(snfa.targets[transition])
.and_modify(|v| *v += &snfa.probabilities[transition])
.or_insert_with(|| snfa.probabilities[transition].clone());
}
delta
}
fn solve_sparse_linear_system_optimized(
a_hash: &mut [FxHashMap<usize, Fraction>],
mut b: Vec<Fraction>,
) -> Result<Vec<Fraction>> {
fn to_vec_rows(a: &mut [FxHashMap<usize, Fraction>]) -> Vec<Vec<(usize, Fraction)>> {
a.iter_mut()
.map(|row| {
let mut v: Vec<(usize, Fraction)> = row.drain().collect();
v.sort_by_key(|(c, _)| *c);
v
})
.collect()
}
fn find_col(row: &[(usize, Fraction)], col: usize) -> Option<usize> {
row.binary_search_by_key(&col, |(c, _)| *c).ok()
}
fn saxpy_row(
target: &mut Vec<(usize, Fraction)>,
i: usize,
pivot: &[(usize, Fraction)],
factor: &Fraction,
) {
let mut out = Vec::with_capacity(target.len() + pivot.len());
let mut t = 0;
let mut p = 0;
while t < target.len() || p < pivot.len() {
match (target.get(t), pivot.get(p)) {
(Some(&(c_t, ref v_t)), Some(&(c_p, ref v_p))) if c_t == c_p => {
if c_t != i {
let mut new_val = v_t.clone();
new_val -= &(factor * v_p);
if !new_val.is_zero() {
out.push((c_t, new_val));
}
}
t += 1;
p += 1;
}
(Some(&(c_t, ref v_t)), Some(&(c_p, _))) if c_t < c_p => {
if c_t != i {
out.push((c_t, v_t.clone()));
}
t += 1;
}
(Some(&(c_t, ref v_t)), None) => {
if c_t != i {
out.push((c_t, v_t.clone()));
}
t += 1;
}
(None, Some(&(c_p, ref v_p))) | (Some(&(_, _)), Some(&(c_p, ref v_p))) => {
if c_p != i {
let new_val = -(factor * v_p);
if !new_val.is_zero() {
out.push((c_p, new_val));
}
}
p += 1;
}
(None, None) => panic!(),
}
}
out.shrink_to_fit();
*target = out;
}
let mut a: Vec<Vec<(usize, Fraction)>> = to_vec_rows(a_hash);
let n = b.len();
for i in 0..n {
let pivot = (i..n)
.find(|&r| find_col(&a[r], i).map_or(false, |idx| !a[r][idx].1.is_zero()))
.ok_or_else(|| anyhow!("Matrix is singular"))?;
if pivot != i {
a.swap(i, pivot);
b.swap(i, pivot);
}
let diag_idx = find_col(&a[i], i).expect("pivot exists");
let inv = a[i][diag_idx].1.clone().recip();
for &mut (_, ref mut v) in &mut a[i] {
*v *= &inv;
}
b[i] *= &inv;
let (left, rest) = a.split_at_mut(i);
let (pivot_row, below) = rest.split_first_mut().expect("pivot row");
let pivot_ref: &[(usize, Fraction)] = &pivot_row[..];
let pivot_b = b[i].clone();
let mut updates: Vec<(usize, Fraction)> = Vec::with_capacity(left.len() + below.len());
updates.extend(
left.par_iter_mut()
.enumerate()
.filter_map(|(r, row)| {
find_col(row, i)
.map(|idx| {
let factor = row[idx].1.clone();
row[idx].1 = Fraction::zero(); if factor.is_zero() {
None
} else {
saxpy_row(row, i, pivot_ref, &factor);
Some((r, factor))
}
})
.flatten()
})
.collect::<Vec<_>>(),
);
updates.extend(
below
.par_iter_mut()
.enumerate()
.filter_map(|(off, row)| {
let r = i + 1 + off;
find_col(row, i)
.map(|idx| {
let factor = row[idx].1.clone();
row[idx].1 = Fraction::zero();
if factor.is_zero() {
None
} else {
saxpy_row(row, i, pivot_ref, &factor);
Some((r, factor))
}
})
.flatten()
})
.collect::<Vec<_>>(),
);
for (r, factor) in updates {
b[r] -= &factor * &pivot_b;
}
}
Ok(b)
}
fn solve_sparse_linear_system(
a: &mut [FxHashMap<usize, Fraction>],
mut b: Vec<Fraction>,
) -> Result<Vec<Fraction>> {
let n = b.len();
for i in 0..n {
let mut pivot = i;
while pivot < n && a[pivot].get(&i).map_or(true, |v| v.is_zero()) {
pivot += 1;
}
if pivot == n {
return Err(anyhow!("Matrix is singular"));
}
if pivot != i {
a.swap(i, pivot);
b.swap(i, pivot);
}
let inv = a[i].get(&i).unwrap().clone().recip();
let keys: Vec<usize> = a[i].keys().cloned().collect();
for j in keys {
if let Some(val) = a[i].get_mut(&j) {
*val *= &inv;
}
}
b[i] *= &inv;
let pivot_b = b[i].clone();
for r in 0..n {
if r == i {
continue;
}
if let Some(factor_val) = a[r].get(&i).cloned() {
if !factor_val.is_zero() {
let keys: Vec<(usize, Fraction)> =
a[i].iter().map(|(k, v)| (*k, v.clone())).collect();
for (c, val_i) in keys {
let product = &factor_val * &val_i;
let entry = a[r].entry(c).or_insert_with(Fraction::zero);
*entry = &*entry - &product;
if entry.is_zero() {
a[r].remove(&c);
}
}
b[r] -= &factor_val * &pivot_b;
}
}
}
}
Ok(b.to_vec())
}
fn compute_phi_ids(
snfa: &StochasticNondeterministicFiniteAutomaton,
k: usize,
start_activity: Activity,
end_activity: Activity,
) -> Vec<FxHashMap<Vec<usize>, Fraction>> {
let n = snfa.termination_probabilities.len();
let id_plus = snfa.activity_key().get_id_from_activity(start_activity);
let id_minus = snfa.activity_key().get_id_from_activity(end_activity);
let mut phi: Vec<FxHashMap<Vec<usize>, Fraction>> = vec![FxHashMap::default(); n];
type SuffixMap = FxHashMap<Vec<usize>, Fraction>;
let mut cache: FxHashMap<(AutomatonState, usize), SuffixMap> = FxHashMap::default();
fn collect_suffixes(
state_idx: AutomatonState,
remaining: usize,
snfa: &StochasticNondeterministicFiniteAutomaton,
start_activity: Activity,
end_activity: Activity,
cache: &mut FxHashMap<(AutomatonState, usize), SuffixMap>,
) -> SuffixMap {
if let Some(m) = cache.get(&(state_idx, remaining)) {
return m.clone();
}
let mut result: SuffixMap = FxHashMap::default();
if remaining == 0 {
result.insert(Vec::<usize>::new(), Fraction::one());
} else {
for (_, target, label, probability) in snfa.outgoing_edges(state_idx) {
if label == &Some(end_activity) {
let arc = vec![snfa.activity_key().get_id_from_activity(end_activity)];
result
.entry(arc)
.and_modify(|v| *v += probability)
.or_insert_with(|| probability.clone());
} else {
let child_map = collect_suffixes(
*target,
remaining - 1,
snfa,
start_activity,
end_activity,
cache,
);
for (suf, w) in &child_map {
let mut vec = Vec::with_capacity(1 + suf.len());
let label = label.unwrap();
vec.push(snfa.activity_key().get_id_from_activity(label));
vec.extend_from_slice(&suf[..]);
let weight = probability * w;
result
.entry(vec)
.and_modify(|v| *v += &weight)
.or_insert(weight);
}
}
}
}
cache.insert((state_idx, remaining), result.clone());
result
}
for state in 0..n {
let suffixes = collect_suffixes(
AutomatonState::of(state),
k,
snfa,
start_activity,
end_activity,
&mut cache,
);
for (trace_slice, prob_suffix) in suffixes.iter() {
let is_exact_k = trace_slice.len() == k;
let is_short_with_end = !trace_slice.is_empty()
&& trace_slice.len() <= k
&& trace_slice.last().unwrap() == &id_minus
&& trace_slice.first().unwrap() == &id_plus;
if is_exact_k || is_short_with_end {
phi[state].insert(trace_slice.clone(), prob_suffix.clone());
}
}
}
phi
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ebi_traits::ebi_trait_finite_stochastic_language::EbiTraitFiniteStochasticLanguage,
techniques::stochastic_markovian_abstraction_conformance::{
DistanceMeasure, StochasticMarkovianConformance,
},
};
use ebi_objects::{
ActivityKeyTranslator, HasActivityKey, IntoRefTraceProbabilityIterator,
TranslateActivityKey,
ebi_objects::{
event_log::EventLog, finite_stochastic_language::FiniteStochasticLanguage,
stochastic_labelled_petri_net::StochasticLabelledPetriNet,
},
};
use std::fs;
#[test]
fn empty_slpn_abstract() {
let fin = fs::read_to_string("testfiles/empty.slpn").unwrap();
let mut slang = fin.parse::<StochasticLabelledPetriNet>().unwrap();
let abst = slang.abstract_markovian(2).unwrap();
abst.to_string();
}
#[test]
fn slpn_abstract() {
let fin = fs::read_to_string("testfiles/simple_markovian_abstraction.slpn").unwrap();
let mut slang = fin.parse::<StochasticLabelledPetriNet>().unwrap();
let abst = slang.abstract_markovian(2).unwrap();
abst.to_string();
}
#[test]
fn log_abstract() {
let fin = fs::read_to_string("testfiles/simple_log_markovian_abstraction.xes").unwrap();
let mut slang: Box<dyn EbiTraitFiniteStochasticLanguage> = Box::new(
FiniteStochasticLanguage::from(fin.parse::<EventLog>().unwrap()),
);
let abst = slang.abstract_markovian(2).unwrap();
abst.to_string();
}
#[test]
fn test_compute_abstraction_for_example_log() {
let file_content =
fs::read_to_string("testfiles/simple_log_markovian_abstraction.xes").unwrap();
let event_log = file_content.parse::<EventLog>().unwrap();
let mut finite_lang: Box<dyn EbiTraitFiniteStochasticLanguage> =
Box::new(FiniteStochasticLanguage::from(event_log));
let abstraction = finite_lang.abstract_markovian(2).unwrap();
assert_eq!(
abstraction.abstraction.len(),
8,
"Should be exactly 8 entries"
);
let mut check: std::collections::HashMap<String, Fraction> =
std::collections::HashMap::default();
for (subtrace, prob) in abstraction.abstraction.iter() {
let key = subtrace
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(",");
check.insert(key, prob.clone());
}
assert_eq!(
check[&format!("{},ac0", abstraction.start_activity)],
Fraction::from((4, 15))
);
assert_eq!(check["ac0,ac0"], Fraction::from((1, 10)));
let pair1 = [Fraction::from((7, 30)), Fraction::from((1, 30))];
let pair2 = [Fraction::from((1, 5)), Fraction::from((1, 15))];
let pair3 = [Fraction::from((1, 15)), Fraction::from((1, 30))];
fn assert_pair(
check: &std::collections::HashMap<String, Fraction>,
k1: &str,
k2: &str,
exp: [Fraction; 2],
) {
let v1 = check.get(k1).expect("missing key");
let v2 = check.get(k2).expect("missing key");
assert!(
(v1 == &exp[0] && v2 == &exp[1]) || (v1 == &exp[1] && v2 == &exp[0]),
"Pair {{ {}, {} }} has unexpected values {{ {}, {} }}",
k1,
k2,
v1,
v2
);
}
assert_pair(&check, "ac0,ac1", "ac0,ac2", pair1);
assert_pair(
&check,
&format!("ac1,{}", abstraction.end_activity),
&format!("ac2,{}", abstraction.end_activity),
pair2,
);
assert_pair(&check, "ac1,ac2", "ac2,ac1", pair3);
}
#[test]
fn test_compute_abstraction_for_petri_net() {
let file_content =
fs::read_to_string("testfiles/simple_markovian_abstraction.slpn").unwrap();
let mut petri_net = file_content.parse::<StochasticLabelledPetriNet>().unwrap();
let result = petri_net.clone().abstract_markovian(0);
assert!(result.is_err(), "Should reject k < 1");
let abstraction = petri_net.abstract_markovian(2).unwrap();
let language_of_model: FiniteStochasticLanguage = abstraction.clone().into();
let language1: Box<dyn EbiTraitFiniteStochasticLanguage> = Box::new(language_of_model);
let mut total = Fraction::from((0, 1));
for (_, probability1) in language1.iter_traces_probabilities() {
total += probability1;
}
assert_eq!(
total,
Fraction::from((1, 1)),
"Total probability should be 1"
);
let fin2 = fs::read_to_string("testfiles/markovian.slang").unwrap();
let expected_traces = fin2.parse::<FiniteStochasticLanguage>().unwrap();
let mut activity_key1 = language1.activity_key().clone();
let translator =
ActivityKeyTranslator::new(&expected_traces.activity_key(), &mut activity_key1);
for (trace1, probability1) in language1.iter_traces_probabilities() {
for (trace2, probability2) in expected_traces.iter_traces_probabilities() {
if trace1 == &translator.translate_trace(trace2) {
assert_eq!(
probability2, probability1,
"Probability mismatch for trace {:?}: expected {}, got {}",
trace1, probability2, probability1
);
}
}
}
}
#[test]
fn test_markovian_conformance_uemsc_log_vs_petri_net() {
let file_content =
fs::read_to_string("testfiles/simple_log_markovian_abstraction.xes").unwrap();
let event_log = file_content.parse::<EventLog>().unwrap();
let mut finite_lang: Box<dyn EbiTraitFiniteStochasticLanguage> =
Box::new(FiniteStochasticLanguage::from(event_log));
let finite_lang_abstraction = finite_lang.abstract_markovian(2).unwrap();
let file_content =
fs::read_to_string("testfiles/simple_markovian_abstraction.slpn").unwrap();
let mut petri_net = file_content.parse::<StochasticLabelledPetriNet>().unwrap();
petri_net.translate_using_activity_key(finite_lang.activity_key_mut());
let petri_net_abstraction = petri_net.abstract_markovian(2).unwrap();
let conformance = finite_lang_abstraction
.markovian_conformance(petri_net_abstraction, DistanceMeasure::UEMSC)
.unwrap();
assert_eq!(conformance, Fraction::from((4, 5))); }
#[test]
fn markovian_eduardo_process_science_journal_paper_test() {
let file_content = fs::read_to_string("testfiles/loop(a,tau)and(bc).slpn").unwrap();
let mut petri_net = file_content.parse::<StochasticLabelledPetriNet>().unwrap();
let fin2 = fs::read_to_string("testfiles/loop(a,tau)and(bc).snfa").unwrap();
let snfa2 = fin2
.parse::<StochasticNondeterministicFiniteAutomaton>()
.unwrap();
let snfa = build_embedded_snfa(&petri_net).unwrap();
assert_eq!(snfa.to_string(), snfa2.to_string());
let abstraction = petri_net.abstract_markovian(2).unwrap();
assert!(!abstraction.abstraction.is_empty());
}
}