use std::collections::BTreeMap;
pub use crate::models::{DFGEdge, DFGNode, DFG};
use crate::ocel::OCEL;
pub fn discover_ocel_dfg(ocel: &OCEL) -> DFG {
let mut dfg = DFG::new();
for et in &ocel.event_types {
dfg.nodes.push(DFGNode::new(et.name.clone(), 0));
}
for event in &ocel.events {
if let Some(node) = dfg
.nodes
.iter_mut()
.find(|n| n.activity == event.event_type)
{
node.frequency += 1;
}
}
let mut events_by_object: BTreeMap<String, Vec<(i64, &str)>> = BTreeMap::new();
for event in &ocel.events {
let ts = event.time.timestamp_millis();
for rel in &event.relationships {
events_by_object
.entry(rel.object_id.clone())
.or_default()
.push((ts, event.event_type.as_str()));
}
}
for events in events_by_object.values_mut() {
events.sort_unstable_by_key(|(ts, _)| *ts);
}
let mut edge_map: BTreeMap<(&str, &str), usize> = BTreeMap::new();
for events in events_by_object.values() {
for pair in events.windows(2) {
*edge_map.entry((pair[0].1, pair[1].1)).or_default() += 1;
}
}
for ((from, to), frequency) in &edge_map {
dfg.edges
.push(DFGEdge::new(from.to_string(), to.to_string(), *frequency));
}
for events in events_by_object.values() {
if let Some((_, first)) = events.first() {
if !dfg.start_activities.contains(&first.to_string()) {
dfg.start_activities.push(first.to_string());
}
}
if let Some((_, last)) = events.last() {
if !dfg.end_activities.contains(&last.to_string()) {
dfg.end_activities.push(last.to_string());
}
}
}
dfg
}
pub fn extract_ocel_variants(ocel: &OCEL) -> Vec<Vec<String>> {
let mut events_by_object: BTreeMap<String, Vec<(i64, String)>> = BTreeMap::new();
for event in &ocel.events {
let ts = event.time.timestamp_millis();
for rel in &event.relationships {
events_by_object
.entry(rel.object_id.clone())
.or_default()
.push((ts, event.event_type.clone()));
}
}
events_by_object
.into_values()
.filter(|v| !v.is_empty())
.map(|mut v| {
v.sort_unstable_by_key(|(ts, _)| *ts);
v.into_iter().map(|(_, act)| act).collect()
})
.collect()
}
pub fn dfg_fitness(observed: &DFG, normative_arcs: &[(String, String)]) -> f64 {
if normative_arcs.is_empty() {
return 1.0;
}
let present = normative_arcs
.iter()
.filter(|(src, tgt)| {
observed
.edges
.iter()
.any(|e| e.source == *src && e.target == *tgt)
})
.count();
present as f64 / normative_arcs.len() as f64
}
pub fn dfg_precision(observed: &DFG, normative_arcs: &[(String, String)]) -> f64 {
if observed.edges.is_empty() {
return 1.0;
}
let normative: std::collections::HashSet<_> = normative_arcs.iter().collect();
let in_model = observed
.edges
.iter()
.filter(|e| normative.contains(&(e.source.clone(), e.target.clone())))
.count();
in_model as f64 / observed.edges.len() as f64
}
#[derive(Debug, Clone)]
pub struct DfgNode {
activity: String,
}
impl DfgNode {
pub fn new(activity: &str) -> Self {
DfgNode {
activity: activity.to_owned(),
}
}
pub fn activity(&self) -> &str {
&self.activity
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DfgWeight(pub u64);
impl DfgWeight {
pub fn count(&self) -> u64 {
self.0
}
}
#[derive(Debug, Clone)]
pub struct DfgEdge {
source: String,
target: String,
weight: DfgWeight,
}
impl DfgEdge {
pub fn new(source: &str, target: &str, count: u64) -> Self {
DfgEdge {
source: source.to_owned(),
target: target.to_owned(),
weight: DfgWeight(count),
}
}
pub fn source(&self) -> &str {
&self.source
}
pub fn target(&self) -> &str {
&self.target
}
pub fn weight(&self) -> &DfgWeight {
&self.weight
}
}
#[derive(Debug, Clone)]
pub struct Dfg {
nodes: Vec<DfgNode>,
edges: Vec<DfgEdge>,
}
impl Dfg {
pub fn new(
nodes: impl IntoIterator<Item = DfgNode>,
edges: impl IntoIterator<Item = DfgEdge>,
) -> Self {
Dfg {
nodes: nodes.into_iter().collect(),
edges: edges.into_iter().collect(),
}
}
pub fn nodes(&self) -> &[DfgNode] {
&self.nodes
}
pub fn edges(&self) -> &[DfgEdge] {
&self.edges
}
pub fn validate(&self) -> Result<(), DfgRefusal> {
if self.nodes.is_empty() {
return Err(DfgRefusal::EmptyGraph);
}
let activities: std::collections::HashSet<&str> =
self.nodes.iter().map(|n| n.activity.as_str()).collect();
for edge in &self.edges {
if !activities.contains(edge.source.as_str())
|| !activities.contains(edge.target.as_str())
{
return Err(DfgRefusal::DanglingEdge);
}
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DfgRefusal {
DanglingEdge,
EmptyGraph,
}
impl std::fmt::Display for DfgRefusal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DfgRefusal::DanglingEdge => write!(f, "DanglingEdge"),
DfgRefusal::EmptyGraph => write!(f, "EmptyGraph"),
}
}
}
impl std::error::Error for DfgRefusal {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DfgFrequency(pub u64);
#[derive(Debug, Clone)]
pub struct DfgEdgeFull {
source: String,
target: String,
frequency: DfgFrequency,
duration_ns: Option<u64>,
}
impl DfgEdgeFull {
pub fn new(source: &str, target: &str, frequency: u64) -> Self {
DfgEdgeFull {
source: source.to_owned(),
target: target.to_owned(),
frequency: DfgFrequency(frequency),
duration_ns: None,
}
}
pub fn source(&self) -> &str {
&self.source
}
pub fn target(&self) -> &str {
&self.target
}
pub fn frequency(&self) -> DfgFrequency {
self.frequency
}
pub fn duration_ns(&self) -> Option<u64> {
self.duration_ns
}
pub fn with_duration_ns(mut self, ns: u64) -> Self {
self.duration_ns = Some(ns);
self
}
}
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct ObjectCentricDfg {
pub per_type: HashMap<String, Dfg>,
keys: Vec<String>,
}
impl ObjectCentricDfg {
pub fn new() -> Self {
ObjectCentricDfg {
per_type: HashMap::new(),
keys: Vec::new(),
}
}
pub fn get(&self, object_type: &str) -> Option<&Dfg> {
self.per_type.get(object_type)
}
pub fn with_type_dfg(mut self, object_type: &str, dfg: Dfg) -> Self {
if !self.per_type.contains_key(object_type) {
self.keys.push(object_type.to_owned());
}
self.per_type.insert(object_type.to_owned(), dfg);
self
}
pub fn object_types(&self) -> impl Iterator<Item = &str> {
self.keys.iter().map(|s| s.as_str())
}
}