use crate::models::DFG;
use crate::streaming::{
impl_activity_interner, ActivityInterner, Interner, StreamStats, StreamingAlgorithm,
};
use rustc_hash::FxHashMap;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Default)]
pub struct StreamingHeuristicBuilder {
pub interner: Interner,
pub activity_counts: Vec<usize>,
pub edge_counts: FxHashMap<(u32, u32), usize>,
pub start_counts: FxHashMap<u32, usize>,
pub end_counts: FxHashMap<u32, usize>,
pub event_count: usize,
pub trace_count: usize,
pub open_traces: BTreeMap<String, Vec<u32>>,
pub dependency_threshold: f64,
}
impl_activity_interner!(StreamingHeuristicBuilder);
impl StreamingHeuristicBuilder {
pub fn new() -> Self {
StreamingHeuristicBuilder {
interner: Interner::new(),
activity_counts: Vec::new(),
edge_counts: FxHashMap::default(),
start_counts: FxHashMap::default(),
end_counts: FxHashMap::default(),
event_count: 0,
trace_count: 0,
open_traces: BTreeMap::new(),
dependency_threshold: 0.0,
}
}
pub fn with_dependency_threshold(threshold: f64) -> Self {
StreamingHeuristicBuilder {
dependency_threshold: threshold,
..Self::new()
}
}
#[inline]
pub fn dependency_score(&self, from: u32, to: u32) -> f64 {
let forward = *self.edge_counts.get(&(from, to)).unwrap_or(&0);
let reverse = *self.edge_counts.get(&(to, from)).unwrap_or(&0);
if forward + reverse == 0 {
0.0
} else {
(forward as f64 - reverse as f64) / (forward as f64 + reverse as f64 + 1.0)
}
}
pub fn snapshot_with_threshold(&self, threshold: f64) -> DFG {
let mut dfg = DFG::new();
dfg.nodes = self
.interner
.vocab()
.iter()
.enumerate()
.map(|(i, name)| crate::models::DFGNode {
id: name.clone(),
label: name.clone(),
frequency: self.activity_counts.get(i).copied().unwrap_or(0),
})
.collect();
let mut edges: Vec<crate::models::DirectlyFollowsRelation> = self
.edge_counts
.iter()
.filter_map(|(&(from, to), &freq)| {
let dep_score = self.dependency_score(from, to);
#[cfg(feature = "bcinr")]
{
let pass = (dep_score.abs() >= threshold) as u64;
let mask = bcinr::mask::select_u64(pass, 1, 0);
if mask == 0 {
return None;
}
}
#[cfg(not(feature = "bcinr"))]
{
if dep_score.abs() < threshold {
return None;
}
}
Some(crate::models::DirectlyFollowsRelation {
from: self.interner.get(from).unwrap_or("").to_string(),
to: self.interner.get(to).unwrap_or("").to_string(),
frequency: freq,
})
})
.collect();
edges.sort_by_key(|x| (x.from.clone(), x.to.clone()));
dfg.edges = edges;
for (&id, &cnt) in &self.start_counts {
if let Some(name) = self.interner.get(id) {
dfg.start_activities.insert(name.to_string(), cnt);
}
}
for (&id, &cnt) in &self.end_counts {
if let Some(name) = self.interner.get(id) {
dfg.end_activities.insert(name.to_string(), cnt);
}
}
dfg
}
pub fn dependency_matrix(&self) -> FxHashMap<(String, String), f64> {
let mut matrix = FxHashMap::default();
for &(from, to) in self.edge_counts.keys() {
let score = self.dependency_score(from, to);
if let (Some(from_name), Some(to_name)) =
(self.interner.get(from), self.interner.get(to))
{
matrix.insert((from_name.to_string(), to_name.to_string()), score);
}
}
matrix
}
}
impl StreamingAlgorithm for StreamingHeuristicBuilder {
type Model = DFG;
fn new() -> Self {
Self::new()
}
#[inline]
fn add_event(&mut self, case_id: &str, activity: &str) {
let id = self.intern(activity);
self.open_traces
.entry(case_id.to_owned())
.or_default()
.push(id);
if id as usize >= self.activity_counts.len() {
self.activity_counts.resize(id as usize + 1, 0);
}
self.event_count += 1;
}
#[inline]
fn close_trace(&mut self, case_id: &str) -> bool {
let Some(events) = self.open_traces.remove(case_id) else {
return false;
};
if events.is_empty() {
return true;
}
for &id in &events {
self.activity_counts[id as usize] += 1;
}
for pair in events.windows(2) {
*self.edge_counts.entry((pair[0], pair[1])).or_default() += 1;
}
*self.start_counts.entry(events[0]).or_default() += 1;
if let Some(last) = events.last() {
*self.end_counts.entry(*last).or_default() += 1;
}
self.trace_count += 1;
true
}
fn snapshot(&self) -> Self::Model {
self.snapshot_with_threshold(self.dependency_threshold)
}
fn stats(&self) -> StreamStats {
let open_trace_events: usize = self.open_traces.values().map(|v| v.len()).sum();
let memory_bytes =
self.open_traces.len() * (std::mem::size_of::<String>() + std::mem::size_of::<Vec<u32>>()) +
open_trace_events * std::mem::size_of::<u32>() +
self.activity_counts.len() * std::mem::size_of::<usize>() +
self.edge_counts.len() * (std::mem::size_of::<(u32,u32)>() + std::mem::size_of::<usize>()) +
(self.start_counts.len() + self.end_counts.len()) * (std::mem::size_of::<u32>() + std::mem::size_of::<usize>());
StreamStats {
event_count: self.event_count,
trace_count: self.trace_count,
open_traces: self.open_traces.len(),
memory_bytes,
activities: self.interner.len(),
}
}
fn open_trace_ids(&self) -> Vec<String> {
self.open_traces.keys().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_heuristic() {
let mut stream = StreamingHeuristicBuilder::new();
stream.add_event("case1", "A");
stream.add_event("case1", "B");
stream.add_event("case1", "C");
stream.close_trace("case1");
let dfg = stream.snapshot();
assert_eq!(dfg.nodes.len(), 3);
assert_eq!(dfg.edges.len(), 2);
}
#[test]
fn test_dependency_score() {
let mut stream = StreamingHeuristicBuilder::new();
for i in 1..=100 {
stream.add_event(&format!("case{}", i), "A");
stream.add_event(&format!("case{}", i), "B");
stream.close_trace(&format!("case{}", i));
}
stream.add_event("case101", "B");
stream.add_event("case101", "A");
stream.close_trace("case101");
let id_a = stream.interner.vocab_map.get("A").unwrap();
let id_b = stream.interner.vocab_map.get("B").unwrap();
let dep_ab = stream.dependency_score(*id_a, *id_b);
assert!(dep_ab > 0.9);
let dep_ba = stream.dependency_score(*id_b, *id_a);
assert!(dep_ba < -0.9); }
#[test]
fn test_dependency_threshold() {
{
let mut stream = StreamingHeuristicBuilder::with_dependency_threshold(0.0);
stream.add_event("case1", "A");
stream.add_event("case1", "B");
stream.close_trace("case1");
let dfg = stream.snapshot();
assert_eq!(dfg.edges.len(), 1);
assert_eq!(dfg.edges[0].from, "A");
}
{
let mut stream = StreamingHeuristicBuilder::with_dependency_threshold(0.5);
for i in 1..=5 {
stream.add_event(&format!("case{}", i), "A");
stream.add_event(&format!("case{}", i), "B");
stream.close_trace(&format!("case{}", i));
}
for i in 6..=9 {
stream.add_event(&format!("case{}", i), "B");
stream.add_event(&format!("case{}", i), "A");
stream.close_trace(&format!("case{}", i));
}
let dfg = stream.snapshot();
assert_eq!(dfg.edges.len(), 0);
}
{
let mut stream = StreamingHeuristicBuilder::with_dependency_threshold(0.7);
for i in 1..=10 {
stream.add_event(&format!("case{}", i), "A");
stream.add_event(&format!("case{}", i), "B");
stream.close_trace(&format!("case{}", i));
}
stream.add_event("case11", "B");
stream.add_event("case11", "A");
stream.close_trace("case11");
let dfg = stream.snapshot();
assert_eq!(dfg.edges.len(), 2);
}
}
#[test]
fn test_dependency_matrix() {
let mut stream = StreamingHeuristicBuilder::new();
stream.add_event("case1", "A");
stream.add_event("case1", "B");
stream.add_event("case1", "C");
stream.close_trace("case1");
let matrix = stream.dependency_matrix();
assert!(matrix.contains_key(&(String::from("A"), String::from("B"))));
assert!(matrix.contains_key(&(String::from("B"), String::from("C"))));
}
#[test]
fn test_stats() {
let mut stream = StreamingHeuristicBuilder::new();
stream.add_event("case1", "A");
stream.add_event("case1", "B");
stream.add_event("case2", "A");
stream.close_trace("case1");
let stats = stream.stats();
assert_eq!(stats.event_count, 3);
assert_eq!(stats.trace_count, 1);
assert_eq!(stats.open_traces, 1);
assert_eq!(stats.activities, 2);
}
}