use crate::models::DeclareConstraint;
use crate::streaming::{
impl_activity_interner, ActivityInterner, Interner, StreamStats, StreamingAlgorithm,
};
use rustc_hash::FxHashMap;
use std::collections::{HashMap, HashSet};
const DEFAULT_THRESHOLD: f64 = 0.6;
#[derive(Debug, Clone)]
pub struct StreamingDeclareBuilder {
pub interner: Interner,
pub activity_counts: Vec<usize>,
pub event_count: usize,
pub trace_count: usize,
pub open_traces: HashMap<String, Vec<u32>>,
pub response_counts: FxHashMap<(u32, u32), usize>,
pub precedence_counts: FxHashMap<(u32, u32), usize>,
pub coexistence_counts: FxHashMap<(u32, u32), usize>,
trace_activity_sets: Vec<HashSet<u32>>,
threshold: f64,
}
impl_activity_interner!(StreamingDeclareBuilder);
impl StreamingDeclareBuilder {
pub fn new() -> Self {
StreamingDeclareBuilder {
interner: Interner::new(),
activity_counts: Vec::new(),
event_count: 0,
trace_count: 0,
open_traces: HashMap::new(),
response_counts: FxHashMap::default(),
precedence_counts: FxHashMap::default(),
coexistence_counts: FxHashMap::default(),
trace_activity_sets: Vec::new(),
threshold: DEFAULT_THRESHOLD,
}
}
pub fn with_threshold(mut self, threshold: f64) -> Self {
self.threshold = threshold.clamp(0.0, 1.0);
self
}
pub fn to_declare(&self) -> crate::models::DeclareModel {
let mut model = crate::models::DeclareModel::new();
if self.trace_count == 0 {
return model;
}
let mut activity_trace_counts: Vec<usize> = vec![0; self.interner.len()];
for trace_set in &self.trace_activity_sets {
for &id in trace_set {
if (id as usize) < activity_trace_counts.len() {
activity_trace_counts[id as usize] += 1;
}
}
}
for (&(a, b), &count) in &self.response_counts {
let a_traces = activity_trace_counts.get(a as usize).copied().unwrap_or(0);
if a_traces > 0 {
let confidence = count as f64 / a_traces as f64;
if confidence >= self.threshold {
let a_name = self.interner.get(a).unwrap_or("?").to_string();
let b_name = self.interner.get(b).unwrap_or("?").to_string();
model.constraints.push(DeclareConstraint {
template: format!("response({}, {})", a_name, b_name),
activities: vec![a_name, b_name],
support: confidence,
confidence,
});
}
}
}
for (&(a, b), &count) in &self.precedence_counts {
let coexist = self.coexistence_counts.get(&(a, b)).copied().unwrap_or(0);
if coexist > 0 {
let confidence = count as f64 / coexist as f64;
if confidence >= self.threshold {
let a_name = self.interner.get(a).unwrap_or("?").to_string();
let b_name = self.interner.get(b).unwrap_or("?").to_string();
model.constraints.push(DeclareConstraint {
template: format!("precedence({}, {})", a_name, b_name),
activities: vec![a_name, b_name],
support: confidence,
confidence,
});
}
}
}
for (&(a, b), &count) in &self.coexistence_counts {
let confidence = count as f64 / self.trace_count as f64;
if confidence >= self.threshold {
let a_name = self.interner.get(a).unwrap_or("?").to_string();
let b_name = self.interner.get(b).unwrap_or("?").to_string();
model.constraints.push(DeclareConstraint {
template: format!("coexistence({}, {})", a_name, b_name),
activities: vec![a_name, b_name],
support: confidence,
confidence,
});
}
}
model
}
}
impl StreamingAlgorithm for StreamingDeclareBuilder {
type Model = crate::models::DeclareModel;
fn new() -> Self {
Self::new()
}
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;
}
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;
}
let activity_set: HashSet<u32> = events.iter().copied().collect();
for i in 0..events.len() {
for j in (i + 1)..events.len() {
let a = events[i];
let b = events[j];
*self.response_counts.entry((a, b)).or_insert(0) += 1;
*self.precedence_counts.entry((a, b)).or_insert(0) += 1;
*self.coexistence_counts.entry((a, b)).or_insert(0) += 1;
}
}
self.trace_activity_sets.push(activity_set);
self.trace_count += 1;
true
}
fn snapshot(&self) -> Self::Model {
self.to_declare()
}
fn stats(&self) -> StreamStats {
let open_trace_events: usize = self.open_traces.values().map(|v| v.len()).sum();
let memory_bytes = self.open_traces.capacity()
* (std::mem::size_of::<String>() + std::mem::size_of::<Vec<u32>>())
+ open_trace_events * std::mem::size_of::<u32>()
+ self.activity_counts.capacity() * std::mem::size_of::<usize>()
+ self.response_counts.capacity()
* (std::mem::size_of::<(u32, u32)>() + std::mem::size_of::<usize>())
+ self.precedence_counts.capacity()
* (std::mem::size_of::<(u32, u32)>() + std::mem::size_of::<usize>())
+ self.coexistence_counts.capacity()
* (std::mem::size_of::<(u32, u32)>() + std::mem::size_of::<usize>())
+ self.trace_activity_sets.capacity() * std::mem::size_of::<HashSet<u32>>();
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()
}
}
impl Default for StreamingDeclareBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_declare_basic() {
let mut stream = StreamingDeclareBuilder::new();
stream.add_event("case1", "A");
stream.add_event("case1", "B");
stream.add_event("case1", "C");
stream.close_trace("case1");
let stats = stream.stats();
assert_eq!(stats.event_count, 3);
assert_eq!(stats.trace_count, 1);
}
#[test]
fn test_declare_emits_constraints() {
let mut stream = StreamingDeclareBuilder::new().with_threshold(0.5);
for i in 0..5 {
stream.add_event(&format!("c{}", i), "A");
stream.add_event(&format!("c{}", i), "B");
stream.close_trace(&format!("c{}", i));
}
let model = stream.snapshot();
assert!(!model.constraints.is_empty());
let has_response = model
.constraints
.iter()
.any(|c| c.template.starts_with("response"));
assert!(
has_response,
"Should emit response constraint for A always before B"
);
}
#[test]
fn test_declare_empty() {
let stream = StreamingDeclareBuilder::new();
let model = stream.snapshot();
assert!(model.constraints.is_empty());
}
}