use crate::models::*;
use crate::state::get_or_init_state;
use crate::utilities::to_js_str;
use hashbrown::HashMap;
use itertools::Itertools;
use rustc_hash::FxHashMap;
use serde_json::json;
use std::collections::BTreeMap;
use std::collections::HashSet;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn analyze_variant_complexity(
eventlog_handle: &str,
activity_key: &str,
) -> Result<JsValue, JsValue> {
get_or_init_state().with_event_log(eventlog_handle, |log| {
let total = log.traces.len() as f64;
let variants = log
.traces
.iter()
.map(|trace| {
trace
.events
.iter()
.filter_map(|e| {
e.attributes
.get(activity_key)?
.as_string()
.map(str::to_owned)
})
.collect::<Vec<String>>()
})
.counts();
let entropy: f64 = variants.values().fold(0.0_f64, |acc, &count| {
let p = count as f64 / total;
p.log2().mul_add(-p, acc)
});
let mut variant_counts: Vec<usize> = variants.values().copied().collect();
variant_counts.sort_unstable_by(|a, b| b.cmp(a));
let coverage_top_10: f64 = variant_counts
.iter()
.take(10)
.map(|&v| v as f64 / total)
.sum();
let max_entropy = if variants.len() > 1 {
(variants.len() as f64).log2()
} else {
0.0
};
to_js_str(&json!({
"total_variants": variants.len(),
"entropy": entropy,
"max_entropy": max_entropy,
"normalized_entropy": if variants.len() <= 1 { 0.0 } else { entropy / max_entropy },
"top_10_coverage": coverage_top_10,
"predominant_variant_size": variant_counts.first().copied().unwrap_or(0),
}))
})
}
#[wasm_bindgen]
pub fn compute_activity_transition_matrix(
eventlog_handle: &str,
activity_key: &str,
) -> Result<JsValue, JsValue> {
get_or_init_state().with_event_log(eventlog_handle, |log| {
let activities = log.get_activities(activity_key);
let vocab: HashMap<String, u32> = activities
.iter()
.enumerate()
.map(|(i, a)| {
(
a.clone(),
u32::try_from(i).expect("activity vocab index fits u32"),
)
})
.collect();
let mut transitions: BTreeMap<(u32, u32), usize> = BTreeMap::new();
let mut activity_total: FxHashMap<u32, usize> = FxHashMap::default();
for activity_id in vocab.values() {
activity_total.insert(*activity_id, 0);
}
for trace in &log.traces {
trace.events.windows(2).for_each(|w| {
if let (Some(AttributeValue::String(a1)), Some(AttributeValue::String(a2))) = (
w[0].attributes.get(activity_key),
w[1].attributes.get(activity_key),
) {
if let (Some(&a1_id), Some(&a2_id)) = (vocab.get(a1), vocab.get(a2)) {
*transitions.entry((a1_id, a2_id)).or_default() += 1;
*activity_total.entry(a1_id).or_default() += 1;
}
}
});
}
let matrix_data: Vec<_> = transitions
.iter()
.filter_map(|((from, to), count)| {
activities.get(*from as usize).and_then(|from_name| {
activities.get(*to as usize).map(|to_name| {
let prob =
*count as f64 / activity_total.get(from).copied().unwrap_or(1) as f64;
json!({
"from": from_name,
"to": to_name,
"count": count,
"probability": prob
})
})
})
})
.collect();
to_js_str(&json!({
"matrix": matrix_data,
"num_activities": activities.len(),
}))
})
}
#[wasm_bindgen]
pub fn analyze_process_speedup(
eventlog_handle: &str,
timestamp_key: &str,
_window_size: usize,
) -> Result<JsValue, JsValue> {
get_or_init_state().with_event_log(eventlog_handle, |log| {
let mut time_gaps: Vec<f64> = Vec::new();
for trace in &log.traces {
let timestamps: Vec<&str> = trace
.events
.iter()
.filter_map(|e| e.attributes.get(timestamp_key)?.as_string())
.collect();
for pair in timestamps.windows(2) {
let gap = crate::parse_iso8601_duration(&pair[0], &pair[1]).abs();
time_gaps.push(gap);
}
}
if time_gaps.is_empty() {
return to_js_str(&json!({
"message": "No timestamps found",
"gaps": []
}));
}
time_gaps.sort_unstable_by(f64::total_cmp);
let mean: f64 = time_gaps.iter().sum::<f64>() / time_gaps.len() as f64;
let p25_idx = ((time_gaps.len() as f64 - 1.0) * 0.25).round() as usize;
let p75_idx = ((time_gaps.len() as f64 - 1.0) * 0.75).round() as usize;
let percentile_25 = time_gaps[p25_idx];
let percentile_75 = time_gaps[p75_idx];
to_js_str(&json!({
"avg_gap": mean,
"p25": percentile_25,
"p75": percentile_75,
"speedup_range": percentile_75 - percentile_25,
}))
})
}
#[wasm_bindgen]
pub fn compute_trace_similarity_matrix(
eventlog_handle: &str,
activity_key: &str,
) -> Result<JsValue, JsValue> {
get_or_init_state().with_event_log(eventlog_handle, |log| {
let mut similarities = Vec::new();
let trace_sets: Vec<HashSet<&str>> = log
.traces
.iter()
.map(|trace| {
trace
.events
.iter()
.filter_map(|e| e.attributes.get(activity_key)?.as_string())
.collect()
})
.collect();
for i in 0..log.traces.len() {
for j in (i + 1)..log.traces.len() {
let common = trace_sets[i].intersection(&trace_sets[j]).count();
let union = trace_sets[i].len() + trace_sets[j].len() - common;
let similarity = common as f64 / union.max(1) as f64;
if similarity > 0.5 {
similarities.push(json!({
"trace_i": i,
"trace_j": j,
"similarity": similarity
}));
}
}
}
to_js_str(&json!({
"similar_pairs": similarities,
"total_pairs": (log.traces.len() * (log.traces.len() - 1)) / 2,
}))
})
}
#[wasm_bindgen]
pub fn analyze_temporal_bottlenecks(
eventlog_handle: &str,
activity_key: &str,
timestamp_key: &str,
) -> Result<JsValue, JsValue> {
get_or_init_state().with_event_log(eventlog_handle, |log| {
let mut activity_durations: std::collections::BTreeMap<String, Vec<f64>> =
std::collections::BTreeMap::new();
for trace in &log.traces {
let activities: Vec<(String, String)> = trace
.events
.iter()
.filter_map(|e| {
if let (Some(AttributeValue::String(act)), Some(AttributeValue::String(ts))) = (
e.attributes.get(activity_key),
e.attributes.get(timestamp_key),
) {
Some((act.clone(), ts.clone()))
} else {
None
}
})
.collect();
for i in 0..activities.len().saturating_sub(1) {
let duration =
crate::parse_iso8601_duration(&activities[i].1, &activities[i + 1].1);
activity_durations
.entry(activities[i].0.clone())
.or_default()
.push(duration.abs());
}
}
let bottlenecks: Vec<_> = activity_durations
.iter()
.map(|(activity, durations)| {
let avg: f64 = durations.iter().sum::<f64>() / durations.len() as f64;
let max = durations.iter().copied().fold(f64::NEG_INFINITY, f64::max);
json!({
"activity": activity,
"avg_duration": avg,
"max_duration": max,
})
})
.collect();
to_js_str(&json!({
"bottlenecks": bottlenecks,
}))
})
}
#[wasm_bindgen]
pub fn extract_activity_ordering(
eventlog_handle: &str,
activity_key: &str,
) -> Result<JsValue, JsValue> {
get_or_init_state().with_event_log(eventlog_handle, |log| {
let mut mandatory_predecessors: std::collections::BTreeMap<
String,
std::collections::BTreeSet<String>,
> = std::collections::BTreeMap::new();
for trace in &log.traces {
let activities: Vec<&str> = trace
.events
.iter()
.filter_map(|e| e.attributes.get(activity_key)?.as_string())
.collect();
for (pos, &activity) in activities.iter().enumerate() {
let predecessors: std::collections::BTreeSet<String> =
activities.iter().take(pos).map(|&a| a.to_owned()).collect();
mandatory_predecessors
.entry(activity.to_owned())
.and_modify(|existing: &mut std::collections::BTreeSet<String>| {
existing.retain(|p| predecessors.contains(p.as_str()));
})
.or_insert(predecessors);
}
}
let result: Vec<_> = mandatory_predecessors
.iter()
.map(|(activity, preds)| {
json!({
"activity": activity,
"mandatory_predecessors": preds
})
})
.collect();
to_js_str(&json!({
"activity_ordering": result,
}))
})
}
#[wasm_bindgen]
pub fn final_analytics_info() -> String {
json!({
"status": "final_analytics_available",
"functions": [
{"name": "variant_complexity", "type": "entropy_analysis"},
{"name": "transition_matrix", "type": "markov_chain"},
{"name": "speedup_analysis", "type": "temporal"},
{"name": "trace_similarity", "type": "distance_metric"},
{"name": "temporal_bottlenecks", "type": "performance"},
{"name": "activity_ordering", "type": "constraints"},
]
})
.to_string()
}