pub mod base_case;
pub mod choice_graph;
pub mod cuts;
pub mod fall_through;
pub mod from_dfg;
pub mod from_partial_orders;
pub mod ocel;
pub mod variants;
use crate::models::EventLog;
use crate::powl_arena::PowlArena;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum DiscoveryVariant {
#[default]
DecisionGraphCyclic,
DecisionGraphCyclicStrict,
DecisionGraphMax,
DecisionGraphClustering,
DynamicClustering,
Maximal,
Tree,
BruteForce,
}
impl DiscoveryVariant {
pub fn as_str(&self) -> &'static str {
match self {
DiscoveryVariant::DecisionGraphCyclic => "decision_graph_cyclic",
DiscoveryVariant::DecisionGraphCyclicStrict => "decision_graph_cyclic_strict",
DiscoveryVariant::DecisionGraphMax => "decision_graph_max",
DiscoveryVariant::DecisionGraphClustering => "decision_graph_clustering",
DiscoveryVariant::DynamicClustering => "dynamic_clustering",
DiscoveryVariant::Maximal => "maximal",
DiscoveryVariant::Tree => "tree",
DiscoveryVariant::BruteForce => "brute_force",
}
}
pub fn from_variant_str(s: &str) -> Option<Self> {
match s {
"decision_graph_cyclic" => Some(DiscoveryVariant::DecisionGraphCyclic),
"decision_graph_cyclic_strict" => Some(DiscoveryVariant::DecisionGraphCyclicStrict),
"decision_graph_max" => Some(DiscoveryVariant::DecisionGraphMax),
"decision_graph_clustering" => Some(DiscoveryVariant::DecisionGraphClustering),
"dynamic_clustering" => Some(DiscoveryVariant::DynamicClustering),
"maximal" => Some(DiscoveryVariant::Maximal),
"tree" => Some(DiscoveryVariant::Tree),
"brute_force" => Some(DiscoveryVariant::BruteForce),
_ => None,
}
}
pub fn uses_decision_graph_directly(&self) -> bool {
matches!(
self,
DiscoveryVariant::DecisionGraphCyclic
| DiscoveryVariant::DecisionGraphCyclicStrict
| DiscoveryVariant::DecisionGraphMax
| DiscoveryVariant::DecisionGraphClustering
)
}
pub fn allows_concurrency(&self) -> bool {
!matches!(self, DiscoveryVariant::Tree)
}
pub fn allows_loops(&self) -> bool {
!matches!(self, DiscoveryVariant::Tree | DiscoveryVariant::Maximal)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DiscoveryConfig {
pub activity_key: String,
pub variant: DiscoveryVariant,
pub min_trace_count: usize,
pub noise_threshold: f64,
pub from_dfg: bool,
pub fall_through_fired: bool,
}
impl Default for DiscoveryConfig {
fn default() -> Self {
Self {
activity_key: "concept:name".to_string(),
variant: DiscoveryVariant::default(),
min_trace_count: 1,
noise_threshold: 0.0,
from_dfg: false,
fall_through_fired: false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CutType {
Concurrency,
Sequence,
Loop,
Xor,
MaximalPartialOrder,
DynamicClusteringPartialOrder,
BruteForcePartialOrder,
}
#[derive(Clone, Debug)]
pub struct CutFilter {
pub cut_order: Vec<CutType>,
pub decision_graph_fall_through: bool,
pub strict_mode: bool,
}
impl CutFilter {
pub fn for_variant(variant: DiscoveryVariant) -> Self {
match variant {
DiscoveryVariant::Tree => Self {
cut_order: vec![CutType::Xor, CutType::Sequence],
decision_graph_fall_through: false,
strict_mode: false,
},
DiscoveryVariant::Maximal => Self {
cut_order: vec![
CutType::Xor,
CutType::Sequence,
CutType::Loop,
CutType::Concurrency,
CutType::MaximalPartialOrder,
],
decision_graph_fall_through: false,
strict_mode: false,
},
DiscoveryVariant::DynamicClustering => Self {
cut_order: vec![
CutType::Xor,
CutType::Loop,
CutType::DynamicClusteringPartialOrder,
],
decision_graph_fall_through: false,
strict_mode: false,
},
DiscoveryVariant::BruteForce => Self {
cut_order: vec![
CutType::Xor,
CutType::Sequence,
CutType::Loop,
CutType::Concurrency,
CutType::BruteForcePartialOrder,
],
decision_graph_fall_through: false,
strict_mode: false,
},
DiscoveryVariant::DecisionGraphMax => Self {
cut_order: vec![
CutType::Xor,
CutType::Sequence,
CutType::Concurrency,
CutType::Loop,
CutType::MaximalPartialOrder,
],
decision_graph_fall_through: true,
strict_mode: false,
},
DiscoveryVariant::DecisionGraphClustering => Self {
cut_order: vec![
CutType::Sequence,
CutType::Xor,
CutType::Loop,
CutType::DynamicClusteringPartialOrder,
],
decision_graph_fall_through: true,
strict_mode: false,
},
DiscoveryVariant::DecisionGraphCyclic => Self {
cut_order: vec![
CutType::Xor,
CutType::Sequence,
CutType::Concurrency,
CutType::Loop,
],
decision_graph_fall_through: true,
strict_mode: false,
},
DiscoveryVariant::DecisionGraphCyclicStrict => Self {
cut_order: vec![
CutType::Xor,
CutType::Sequence,
CutType::Concurrency,
CutType::Loop,
],
decision_graph_fall_through: true,
strict_mode: true,
},
}
}
}
pub fn discover_powl(log: &EventLog, config: &DiscoveryConfig) -> Result<(PowlArena, u32), String> {
let mut arena = PowlArena::new();
let root = if config.from_dfg {
discover_from_dfg(log, config, &mut arena)?
} else {
discover_from_traces(log, config, &mut arena)?
};
Ok((arena, root))
}
fn discover_from_traces(
log: &EventLog,
config: &DiscoveryConfig,
arena: &mut PowlArena,
) -> Result<u32, String> {
let activities = log.get_activities(&config.activity_key);
if activities.is_empty() {
return base_case::handle_empty_log(arena);
}
if activities.len() == 1 {
return base_case::handle_single_activity(arena, &activities[0]);
}
let traces = group_traces_by_activity_sequence(log, &config.activity_key);
let cut_filter = CutFilter::for_variant(config.variant);
inductive_miner(&traces, arena, config, &cut_filter)
}
fn discover_from_dfg(
log: &EventLog,
config: &DiscoveryConfig,
arena: &mut PowlArena,
) -> Result<u32, String> {
let _dfg_edges = log.get_directly_follows(&config.activity_key);
discover_from_traces(log, config, arena)
}
pub(crate) fn inductive_miner(
traces: &[Vec<String>],
arena: &mut PowlArena,
config: &DiscoveryConfig,
cut_filter: &CutFilter,
) -> Result<u32, String> {
for cut_type in &cut_filter.cut_order {
let result = match cut_type {
CutType::Concurrency => cuts::detect_concurrency_cut(traces, arena, config),
CutType::Sequence => cuts::detect_sequence_cut(traces, arena, config),
CutType::Loop => cuts::detect_loop_cut(traces, arena, config),
CutType::Xor => cuts::detect_xor_cut(traces, arena, config),
CutType::MaximalPartialOrder => {
cuts::detect_maximal_partial_order_cut(traces, arena, config)
}
CutType::DynamicClusteringPartialOrder => {
cuts::detect_dynamic_clustering_cut(traces, arena, config)
}
CutType::BruteForcePartialOrder => {
cuts::detect_brute_force_partial_order_cut(traces, arena, config)
}
};
if result.is_ok() {
return result;
}
}
if cut_filter.decision_graph_fall_through {
fall_through::decision_graph_fall_through(traces, arena, config)
} else {
fall_through::flower_model_fall_through(traces, arena, config)
}
}
pub(crate) fn group_traces_by_activity_sequence(
log: &EventLog,
activity_key: &str,
) -> Vec<Vec<String>> {
let mut unique_traces = std::collections::HashMap::new();
for trace in &log.traces {
let activities: Vec<String> = trace
.events
.iter()
.filter_map(|event| event.attributes.get(activity_key))
.filter_map(|v| v.as_string())
.map(|s| s.to_string())
.collect();
let count = unique_traces.entry(activities.clone()).or_insert(0);
*count += 1;
}
unique_traces.into_keys().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_variant_serialization_roundtrip() {
let variants = [
DiscoveryVariant::DecisionGraphCyclic,
DiscoveryVariant::DecisionGraphCyclicStrict,
DiscoveryVariant::DecisionGraphMax,
DiscoveryVariant::DecisionGraphClustering,
DiscoveryVariant::DynamicClustering,
DiscoveryVariant::Maximal,
DiscoveryVariant::Tree,
DiscoveryVariant::BruteForce,
];
for variant in variants {
let s = variant.as_str();
assert_eq!(DiscoveryVariant::from_variant_str(s), Some(variant));
}
assert_eq!(DiscoveryVariant::from_variant_str("invalid"), None);
}
#[test]
fn test_cut_filter_variant_configurations() {
assert!(!CutFilter::for_variant(DiscoveryVariant::Tree)
.cut_order
.contains(&CutType::Concurrency)); assert!(CutFilter::for_variant(DiscoveryVariant::Maximal)
.cut_order
.contains(&CutType::MaximalPartialOrder)); assert!(CutFilter::for_variant(DiscoveryVariant::BruteForce)
.cut_order
.contains(&CutType::BruteForcePartialOrder)); assert!(
CutFilter::for_variant(DiscoveryVariant::DecisionGraphCyclic)
.decision_graph_fall_through
); assert!(CutFilter::for_variant(DiscoveryVariant::DecisionGraphCyclicStrict).strict_mode);
}
#[test]
fn test_variants_produce_different_configurations() {
let cf_tree = CutFilter::for_variant(DiscoveryVariant::Tree);
let cf_max = CutFilter::for_variant(DiscoveryVariant::Maximal);
let cf_dg = CutFilter::for_variant(DiscoveryVariant::DecisionGraphCyclic);
assert_ne!(cf_tree.cut_order, cf_max.cut_order);
assert_ne!(cf_tree.cut_order, cf_dg.cut_order);
assert_ne!(cf_max.cut_order, cf_dg.cut_order);
}
}