memstead_base/ops/
labelling.rs1use std::collections::{BTreeMap, HashMap};
15
16use crate::entity::EntityId;
17use crate::store::Store;
18use memstead_schema::{LabellingDef, ReachDirection, SupportWalk};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum Label {
23 Accepted,
24 Defeated,
25 Undecided,
26}
27
28impl Label {
29 pub fn wire(&self) -> &'static str {
30 match self {
31 Label::Accepted => "accepted",
32 Label::Defeated => "defeated",
33 Label::Undecided => "undecided",
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
43pub struct MemLabelling {
44 pub labels: BTreeMap<String, Label>,
46 pub attackers: BTreeMap<String, Vec<String>>,
48 pub cross_mem_edges_excluded: usize,
52}
53
54impl MemLabelling {
55 pub fn accepted_attackers_of(&self, id: &str) -> Vec<String> {
58 self.direct_attackers_with(id, Label::Accepted)
59 }
60
61 pub fn undecided_attackers_of(&self, id: &str) -> Vec<String> {
64 self.direct_attackers_with(id, Label::Undecided)
65 }
66
67 fn direct_attackers_with(&self, id: &str, label: Label) -> Vec<String> {
68 self.attackers
69 .get(id)
70 .map(|atts| {
71 atts.iter()
72 .filter(|a| self.labels.get(a.as_str()) == Some(&label))
73 .cloned()
74 .collect()
75 })
76 .unwrap_or_default()
77 }
78}
79
80pub fn compute_mem_labelling(store: &Store, mem: &str, attack: &[String]) -> MemLabelling {
82 let mut node_ids: Vec<String> = store
84 .all_entities()
85 .filter(|e| e.mem == mem && !e.stub)
86 .map(|e| e.id.0.clone())
87 .collect();
88 node_ids.sort();
89 let node_set: std::collections::HashSet<&str> = node_ids.iter().map(String::as_str).collect();
90
91 let mut attackers: BTreeMap<String, Vec<String>> = BTreeMap::new();
96 let mut cross_mem_edges_excluded = 0usize;
97 for id_str in &node_ids {
98 let id = EntityId(id_str.clone());
99 let mut atts: Vec<String> = Vec::new();
100 for edge in store.incoming(&id) {
101 if !attack.iter().any(|n| n == &edge.rel_type) {
102 continue;
103 }
104 if edge.from.mem() != mem {
105 cross_mem_edges_excluded += 1;
106 continue;
107 }
108 if node_set.contains(edge.from.0.as_str()) {
109 atts.push(edge.from.0.clone());
110 }
111 }
112 for edge in store.outgoing(&id) {
113 if !attack.iter().any(|n| n == &edge.rel_type) {
114 continue;
115 }
116 if edge.target.mem() != mem {
117 cross_mem_edges_excluded += 1;
118 }
119 }
120 atts.sort();
121 atts.dedup();
122 attackers.insert(id_str.clone(), atts);
123 }
124
125 let mut labels: HashMap<&str, Label> = HashMap::new();
130 loop {
131 let mut changed = false;
132 for id in &node_ids {
133 if labels.contains_key(id.as_str()) {
134 continue;
135 }
136 let atts = &attackers[id.as_str()];
137 if atts
138 .iter()
139 .all(|a| labels.get(a.as_str()) == Some(&Label::Defeated))
140 {
141 labels.insert(id.as_str(), Label::Accepted);
142 changed = true;
143 } else if atts
144 .iter()
145 .any(|a| labels.get(a.as_str()) == Some(&Label::Accepted))
146 {
147 labels.insert(id.as_str(), Label::Defeated);
148 changed = true;
149 }
150 }
151 if !changed {
152 break;
153 }
154 }
155
156 let labels: BTreeMap<String, Label> = node_ids
157 .iter()
158 .map(|id| {
159 (
160 id.clone(),
161 labels.get(id.as_str()).copied().unwrap_or(Label::Undecided),
162 )
163 })
164 .collect();
165
166 MemLabelling {
167 labels,
168 attackers,
169 cross_mem_edges_excluded,
170 }
171}
172
173#[derive(Debug, Clone, PartialEq)]
178pub struct ShapeStats {
179 pub depth: u64,
182 pub branching: f64,
185 pub terminal_share: Option<f64>,
188 pub defeated_in_support: u64,
191 pub undecided_in_support: u64,
193}
194
195pub fn compute_shape(
200 store: &Store,
201 start: &EntityId,
202 walk: &SupportWalk,
203 label_of: &dyn Fn(&EntityId) -> Option<Label>,
204) -> ShapeStats {
205 let successors = |id: &EntityId| -> Vec<EntityId> {
206 let mut next: Vec<EntityId> = match walk.direction {
207 ReachDirection::Out => store
208 .outgoing(id)
209 .iter()
210 .filter(|e| walk.relationships.iter().any(|n| n == &e.rel_type))
211 .map(|e| e.target.clone())
212 .collect(),
213 ReachDirection::In => store
214 .incoming(id)
215 .iter()
216 .filter(|e| walk.relationships.iter().any(|n| n == &e.rel_type))
217 .map(|e| e.from.clone())
218 .collect(),
219 };
220 next.sort_by(|a, b| a.0.cmp(&b.0));
221 next.dedup();
222 next
223 };
224
225 let mut visited: std::collections::HashSet<EntityId> = std::iter::once(start.clone()).collect();
228 let mut frontier = vec![start.clone()];
229 let mut depth: u64 = 0;
230 let mut subtree: Vec<EntityId> = Vec::new();
231 let mut successor_counts: Vec<usize> = Vec::new();
232 let start_succ = successors(start).len();
234 if start_succ > 0 {
235 successor_counts.push(start_succ);
236 }
237 while !frontier.is_empty() {
238 let mut next_frontier = Vec::new();
239 for current in frontier {
240 for next in successors(¤t) {
241 if visited.insert(next.clone()) {
242 subtree.push(next.clone());
243 next_frontier.push(next);
244 }
245 }
246 }
247 if !next_frontier.is_empty() {
248 depth += 1;
249 }
250 frontier = next_frontier;
251 }
252
253 let mut leaves_total = 0u64;
254 let mut leaves_terminal = 0u64;
255 let mut defeated_in_support = 0u64;
256 let mut undecided_in_support = 0u64;
257 for node in &subtree {
258 let succ = successors(node);
259 if succ.is_empty() {
260 leaves_total += 1;
261 if store
262 .get(node)
263 .is_some_and(|e| !e.stub && walk.terminal_types.iter().any(|t| t == &e.entity_type))
264 {
265 leaves_terminal += 1;
266 }
267 } else {
268 successor_counts.push(succ.len());
269 }
270 match label_of(node) {
271 Some(Label::Defeated) => defeated_in_support += 1,
272 Some(Label::Undecided) => undecided_in_support += 1,
273 _ => {}
274 }
275 }
276
277 let branching = if successor_counts.is_empty() {
278 0.0
279 } else {
280 successor_counts.iter().sum::<usize>() as f64 / successor_counts.len() as f64
281 };
282 let terminal_share = if leaves_total == 0 {
283 None
284 } else {
285 Some(leaves_terminal as f64 / leaves_total as f64)
286 };
287
288 ShapeStats {
289 depth,
290 branching,
291 terminal_share,
292 defeated_in_support,
293 undecided_in_support,
294 }
295}
296
297#[derive(Debug, Clone)]
300pub struct LabellingView {
301 pub label: Label,
302 pub defeated_by: Vec<String>,
303 pub undecided_by: Vec<String>,
304 pub shape: Option<ShapeStats>,
305}
306
307impl LabellingView {
308 pub fn to_json(&self) -> serde_json::Value {
311 let mut v = serde_json::json!({
312 "label": self.label.wire(),
313 "defeated_by": self.defeated_by,
314 "undecided_by": self.undecided_by,
315 });
316 if let Some(shape) = &self.shape {
317 v["shape"] = serde_json::json!({
318 "depth": shape.depth,
319 "branching": shape.branching,
320 "terminal_share": shape.terminal_share,
321 "defeated_in_support": shape.defeated_in_support,
322 "undecided_in_support": shape.undecided_in_support,
323 });
324 }
325 v
326 }
327}
328
329pub fn labelling_of(schema: &memstead_schema::Schema) -> Option<&LabellingDef> {
331 schema.manifest.relationships.labelling.as_ref()
332}