use std::collections::{BTreeMap, HashMap};
use crate::entity::EntityId;
use crate::store::Store;
use memstead_schema::{LabellingDef, ReachDirection, SupportWalk};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Label {
Accepted,
Defeated,
Undecided,
}
impl Label {
pub fn wire(&self) -> &'static str {
match self {
Label::Accepted => "accepted",
Label::Defeated => "defeated",
Label::Undecided => "undecided",
}
}
}
#[derive(Debug, Clone)]
pub struct MemLabelling {
pub labels: BTreeMap<String, Label>,
pub attackers: BTreeMap<String, Vec<String>>,
pub cross_mem_edges_excluded: usize,
}
impl MemLabelling {
pub fn accepted_attackers_of(&self, id: &str) -> Vec<String> {
self.direct_attackers_with(id, Label::Accepted)
}
pub fn undecided_attackers_of(&self, id: &str) -> Vec<String> {
self.direct_attackers_with(id, Label::Undecided)
}
fn direct_attackers_with(&self, id: &str, label: Label) -> Vec<String> {
self.attackers
.get(id)
.map(|atts| {
atts.iter()
.filter(|a| self.labels.get(a.as_str()) == Some(&label))
.cloned()
.collect()
})
.unwrap_or_default()
}
}
pub fn compute_mem_labelling(store: &Store, mem: &str, attack: &[String]) -> MemLabelling {
let mut node_ids: Vec<String> = store
.all_entities()
.filter(|e| e.mem == mem && !e.stub)
.map(|e| e.id.0.clone())
.collect();
node_ids.sort();
let node_set: std::collections::HashSet<&str> = node_ids.iter().map(String::as_str).collect();
let mut attackers: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut cross_mem_edges_excluded = 0usize;
for id_str in &node_ids {
let id = EntityId(id_str.clone());
let mut atts: Vec<String> = Vec::new();
for edge in store.incoming(&id) {
if !attack.iter().any(|n| n == &edge.rel_type) {
continue;
}
if edge.from.mem() != mem {
cross_mem_edges_excluded += 1;
continue;
}
if node_set.contains(edge.from.0.as_str()) {
atts.push(edge.from.0.clone());
}
}
for edge in store.outgoing(&id) {
if !attack.iter().any(|n| n == &edge.rel_type) {
continue;
}
if edge.target.mem() != mem {
cross_mem_edges_excluded += 1;
}
}
atts.sort();
atts.dedup();
attackers.insert(id_str.clone(), atts);
}
let mut labels: HashMap<&str, Label> = HashMap::new();
loop {
let mut changed = false;
for id in &node_ids {
if labels.contains_key(id.as_str()) {
continue;
}
let atts = &attackers[id.as_str()];
if atts
.iter()
.all(|a| labels.get(a.as_str()) == Some(&Label::Defeated))
{
labels.insert(id.as_str(), Label::Accepted);
changed = true;
} else if atts
.iter()
.any(|a| labels.get(a.as_str()) == Some(&Label::Accepted))
{
labels.insert(id.as_str(), Label::Defeated);
changed = true;
}
}
if !changed {
break;
}
}
let labels: BTreeMap<String, Label> = node_ids
.iter()
.map(|id| {
(
id.clone(),
labels.get(id.as_str()).copied().unwrap_or(Label::Undecided),
)
})
.collect();
MemLabelling {
labels,
attackers,
cross_mem_edges_excluded,
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct ShapeStats {
pub depth: u64,
pub branching: f64,
pub terminal_share: Option<f64>,
pub defeated_in_support: u64,
pub undecided_in_support: u64,
}
pub fn compute_shape(
store: &Store,
start: &EntityId,
walk: &SupportWalk,
label_of: &dyn Fn(&EntityId) -> Option<Label>,
) -> ShapeStats {
let successors = |id: &EntityId| -> Vec<EntityId> {
let mut next: Vec<EntityId> = match walk.direction {
ReachDirection::Out => store
.outgoing(id)
.iter()
.filter(|e| walk.relationships.iter().any(|n| n == &e.rel_type))
.map(|e| e.target.clone())
.collect(),
ReachDirection::In => store
.incoming(id)
.iter()
.filter(|e| walk.relationships.iter().any(|n| n == &e.rel_type))
.map(|e| e.from.clone())
.collect(),
};
next.sort_by(|a, b| a.0.cmp(&b.0));
next.dedup();
next
};
let mut visited: std::collections::HashSet<EntityId> = std::iter::once(start.clone()).collect();
let mut frontier = vec![start.clone()];
let mut depth: u64 = 0;
let mut subtree: Vec<EntityId> = Vec::new();
let mut successor_counts: Vec<usize> = Vec::new();
let start_succ = successors(start).len();
if start_succ > 0 {
successor_counts.push(start_succ);
}
while !frontier.is_empty() {
let mut next_frontier = Vec::new();
for current in frontier {
for next in successors(¤t) {
if visited.insert(next.clone()) {
subtree.push(next.clone());
next_frontier.push(next);
}
}
}
if !next_frontier.is_empty() {
depth += 1;
}
frontier = next_frontier;
}
let mut leaves_total = 0u64;
let mut leaves_terminal = 0u64;
let mut defeated_in_support = 0u64;
let mut undecided_in_support = 0u64;
for node in &subtree {
let succ = successors(node);
if succ.is_empty() {
leaves_total += 1;
if store
.get(node)
.is_some_and(|e| !e.stub && walk.terminal_types.iter().any(|t| t == &e.entity_type))
{
leaves_terminal += 1;
}
} else {
successor_counts.push(succ.len());
}
match label_of(node) {
Some(Label::Defeated) => defeated_in_support += 1,
Some(Label::Undecided) => undecided_in_support += 1,
_ => {}
}
}
let branching = if successor_counts.is_empty() {
0.0
} else {
successor_counts.iter().sum::<usize>() as f64 / successor_counts.len() as f64
};
let terminal_share = if leaves_total == 0 {
None
} else {
Some(leaves_terminal as f64 / leaves_total as f64)
};
ShapeStats {
depth,
branching,
terminal_share,
defeated_in_support,
undecided_in_support,
}
}
#[derive(Debug, Clone)]
pub struct LabellingView {
pub label: Label,
pub defeated_by: Vec<String>,
pub undecided_by: Vec<String>,
pub shape: Option<ShapeStats>,
}
impl LabellingView {
pub fn to_json(&self) -> serde_json::Value {
let mut v = serde_json::json!({
"label": self.label.wire(),
"defeated_by": self.defeated_by,
"undecided_by": self.undecided_by,
});
if let Some(shape) = &self.shape {
v["shape"] = serde_json::json!({
"depth": shape.depth,
"branching": shape.branching,
"terminal_share": shape.terminal_share,
"defeated_in_support": shape.defeated_in_support,
"undecided_in_support": shape.undecided_in_support,
});
}
v
}
}
pub fn labelling_of(schema: &memstead_schema::Schema) -> Option<&LabellingDef> {
schema.manifest.relationships.labelling.as_ref()
}