use std::collections::BTreeMap;
use wasm4pm::network_metrics::{NetworkEdge, NetworkNode, SocialNetwork};
use wasm4pm_compat::event_log::EventLog as CompatEventLog;
#[derive(Debug, Clone, serde::Serialize)]
pub struct SocialNetworkMetrics {
pub degree: BTreeMap<String, f64>,
pub betweenness: BTreeMap<String, f64>,
pub closeness: BTreeMap<String, f64>,
}
pub fn compute_social_network(
log: &CompatEventLog,
resource_key: &str,
) -> anyhow::Result<SocialNetworkMetrics> {
let native_log: wasm4pm::models::EventLog = log.clone().into();
let mut handovers: BTreeMap<(String, String), usize> = BTreeMap::new();
let mut all_resources: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
for trace in &native_log.traces {
let resources: Vec<Option<String>> = trace
.events
.iter()
.map(|e| {
e.attributes
.get(resource_key)
.and_then(|v| v.as_string())
.map(str::to_owned)
})
.collect();
for r in resources.iter().filter_map(|r| r.as_ref()) {
all_resources.insert(r.clone());
}
for i in 0..resources.len().saturating_sub(1) {
if let (Some(r1), Some(r2)) = (&resources[i], &resources[i + 1]) {
if r1 != r2 {
*handovers.entry((r1.clone(), r2.clone())).or_default() += 1;
}
}
}
}
let nodes: Vec<NetworkNode> = all_resources
.iter()
.map(|id| NetworkNode {
id: id.clone(),
label: Some(id.clone()),
workload: None,
})
.collect();
let edges: Vec<NetworkEdge> = handovers
.iter()
.map(|((from, to), weight)| NetworkEdge {
from: from.clone(),
to: to.clone(),
weight: *weight,
})
.collect();
let network = SocialNetwork { nodes, edges };
Ok(SocialNetworkMetrics {
degree: network.degree_centrality(),
betweenness: network.betweenness_centrality(),
closeness: network.closeness_centrality(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use wasm4pm_compat::event_log::{
Attribute, AttributeValue, Event, Trace, XESEditableAttribute,
};
fn event_with_activity_and_resource(activity: &str, resource: &str) -> Event {
let mut event = Event::with_activity(activity);
event.attributes.add_attribute(Attribute::new(
"org:resource".to_string(),
AttributeValue::String(resource.to_string()),
));
event
}
fn build_small_log() -> CompatEventLog {
let trace1 = Trace::new(
"case-1".to_string(),
vec![
event_with_activity_and_resource("register", "alice"),
event_with_activity_and_resource("approve", "bob"),
],
);
let trace2 = Trace::new(
"case-2".to_string(),
vec![
event_with_activity_and_resource("register", "alice"),
event_with_activity_and_resource("approve", "bob"),
],
);
let trace3 = Trace::new(
"case-3".to_string(),
vec![event_with_activity_and_resource("audit", "carol")],
);
CompatEventLog {
attributes: Vec::new(),
traces: vec![trace1, trace2, trace3],
extensions: None,
classifiers: None,
global_trace_attrs: None,
global_event_attrs: None,
}
}
#[test]
fn compute_social_network_finds_real_handover_edge_between_alice_and_bob() {
let log = build_small_log();
let metrics = compute_social_network(&log, "org:resource")
.expect("social network computation should not fail on a small valid log");
assert!(
metrics.degree.contains_key("alice") && metrics.degree.contains_key("bob"),
"expected alice and bob in degree centrality map, got {:?}",
metrics.degree
);
assert!(
metrics.degree["alice"] > 0.0,
"expected alice to have nonzero degree centrality from the alice->bob handover, got {:?}",
metrics.degree
);
assert!(
metrics.degree["bob"] > 0.0,
"expected bob to have nonzero degree centrality from the alice->bob handover, got {:?}",
metrics.degree
);
assert_eq!(
metrics.degree.get("carol").copied().unwrap_or(0.0),
0.0,
"carol has no handover partner in any trace, expected zero degree centrality, got {:?}",
metrics.degree
);
}
}