1#![allow(dead_code)]
5
6use std::collections::HashMap;
7
8use oxihuman_core::parser::target::TargetFile;
9
10#[derive(Debug, Clone)]
12pub struct VertexInfluence {
13 pub vertex_id: u32,
14 pub influences: Vec<(String, f32)>,
16}
17
18impl VertexInfluence {
19 pub fn total_magnitude(&self) -> f32 {
21 self.influences.iter().map(|(_, m)| m).sum()
22 }
23
24 pub fn dominant_target(&self) -> Option<&str> {
26 self.influences.first().map(|(name, _)| name.as_str())
27 }
28}
29
30#[derive(Debug)]
33pub struct InfluenceMap {
34 pub vertex_count: usize,
35 influences: HashMap<u32, VertexInfluence>,
36}
37
38impl InfluenceMap {
39 pub fn build(targets: &[(&str, &TargetFile)]) -> Self {
41 let mut map: HashMap<u32, Vec<(String, f32)>> = HashMap::new();
42
43 for (name, target) in targets {
44 for delta in &target.deltas {
45 let mag = (delta.dx * delta.dx + delta.dy * delta.dy + delta.dz * delta.dz).sqrt();
46 map.entry(delta.vid)
47 .or_default()
48 .push((name.to_string(), mag));
49 }
50 }
51
52 let mut influences: HashMap<u32, VertexInfluence> = HashMap::new();
53 for (vid, mut infl_list) in map {
54 infl_list.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
55 influences.insert(
56 vid,
57 VertexInfluence {
58 vertex_id: vid,
59 influences: infl_list,
60 },
61 );
62 }
63
64 let vertex_count = influences.len();
65 Self {
66 vertex_count,
67 influences,
68 }
69 }
70
71 pub fn get(&self, vertex_id: u32) -> Option<&VertexInfluence> {
73 self.influences.get(&vertex_id)
74 }
75
76 pub fn affected_vertex_count(&self) -> usize {
78 self.influences.len()
79 }
80
81 pub fn iter(&self) -> impl Iterator<Item = &VertexInfluence> {
83 self.influences.values()
84 }
85
86 pub fn top_vertices(&self, n: usize) -> Vec<&VertexInfluence> {
88 let mut all: Vec<&VertexInfluence> = self.influences.values().collect();
89 all.sort_by(|a, b| {
90 b.total_magnitude()
91 .partial_cmp(&a.total_magnitude())
92 .unwrap_or(std::cmp::Ordering::Equal)
93 });
94 all.truncate(n);
95 all
96 }
97
98 pub fn vertices_for_target(&self, target_name: &str) -> Vec<u32> {
100 let mut result: Vec<u32> = self
101 .influences
102 .values()
103 .filter(|vi| vi.influences.iter().any(|(name, _)| name == target_name))
104 .map(|vi| vi.vertex_id)
105 .collect();
106 result.sort_unstable();
107 result
108 }
109
110 pub fn targets_for_vertex(&self, vertex_id: u32) -> Vec<(&str, f32)> {
112 self.influences
113 .get(&vertex_id)
114 .map(|vi| {
115 vi.influences
116 .iter()
117 .map(|(name, mag)| (name.as_str(), *mag))
118 .collect()
119 })
120 .unwrap_or_default()
121 }
122
123 pub fn target_stats(&self) -> Vec<(String, usize, f32)> {
125 let mut stats: HashMap<String, (usize, f32)> = HashMap::new();
126 for vi in self.influences.values() {
127 for (name, mag) in &vi.influences {
128 let entry = stats.entry(name.clone()).or_insert((0, 0.0));
129 entry.0 += 1;
130 entry.1 += mag;
131 }
132 }
133 let mut result: Vec<(String, usize, f32)> = stats
134 .into_iter()
135 .map(|(name, (count, total))| (name, count, total))
136 .collect();
137 result.sort_by(|a, b| a.0.cmp(&b.0));
138 result
139 }
140
141 pub fn isolated_vertices(&self) -> Vec<u32> {
143 let mut result: Vec<u32> = self
144 .influences
145 .values()
146 .filter(|vi| vi.influences.len() == 1)
147 .map(|vi| vi.vertex_id)
148 .collect();
149 result.sort_unstable();
150 result
151 }
152
153 pub fn shared_vertices(&self, min_targets: usize) -> Vec<u32> {
155 let mut result: Vec<u32> = self
156 .influences
157 .values()
158 .filter(|vi| vi.influences.len() >= min_targets)
159 .map(|vi| vi.vertex_id)
160 .collect();
161 result.sort_unstable();
162 result
163 }
164}
165
166#[derive(Debug, Clone)]
168pub struct InfluenceMapStats {
169 pub affected_vertices: usize,
171 pub target_count: usize,
173 pub avg_targets_per_vertex: f32,
175 pub max_targets_per_vertex: usize,
177 pub total_magnitude: f32,
179}
180
181pub fn build_influence_map(targets: &[(&str, &TargetFile)]) -> InfluenceMap {
183 InfluenceMap::build(targets)
184}
185
186pub fn top_influences_for_vertex(
189 map: &InfluenceMap,
190 vertex_id: u32,
191 n: usize,
192) -> Vec<(String, f32)> {
193 map.get(vertex_id)
194 .map(|vi| vi.influences.iter().take(n).cloned().collect())
195 .unwrap_or_default()
196}
197
198pub fn target_vertex_coverage(map: &InfluenceMap, target_name: &str, vertex_ids: &[u32]) -> f32 {
201 if vertex_ids.is_empty() {
202 return 0.0;
203 }
204 let covered = vertex_ids
205 .iter()
206 .filter(|&&vid| {
207 map.get(vid)
208 .map(|vi| vi.influences.iter().any(|(n, _)| n == target_name))
209 .unwrap_or(false)
210 })
211 .count();
212 covered as f32 / vertex_ids.len() as f32
213}
214
215pub fn vertex_target_overlap(map: &InfluenceMap, target_a: &str, target_b: &str) -> f32 {
218 let set_a: std::collections::HashSet<u32> =
219 map.vertices_for_target(target_a).into_iter().collect();
220 let set_b: std::collections::HashSet<u32> =
221 map.vertices_for_target(target_b).into_iter().collect();
222
223 let intersection = set_a.intersection(&set_b).count();
224 let union = set_a.union(&set_b).count();
225
226 if union == 0 {
227 0.0
228 } else {
229 intersection as f32 / union as f32
230 }
231}
232
233pub fn influence_map_stats(map: &InfluenceMap) -> InfluenceMapStats {
235 let affected_vertices = map.affected_vertex_count();
236
237 let mut total_targets_sum: usize = 0;
238 let mut max_targets_per_vertex: usize = 0;
239
240 for vi in map.iter() {
241 let cnt = vi.influences.len();
242 total_targets_sum += cnt;
243 if cnt > max_targets_per_vertex {
244 max_targets_per_vertex = cnt;
245 }
246 }
247
248 let stats = map.target_stats();
249 let target_count = stats.len();
250 let total_magnitude: f32 = stats.iter().map(|(_, _, m)| m).sum();
251
252 let avg_targets_per_vertex = if affected_vertices == 0 {
253 0.0
254 } else {
255 total_targets_sum as f32 / affected_vertices as f32
256 };
257
258 InfluenceMapStats {
259 affected_vertices,
260 target_count,
261 avg_targets_per_vertex,
262 max_targets_per_vertex,
263 total_magnitude,
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use oxihuman_core::parser::target::{Delta, TargetFile};
271
272 fn make_target(name: &str, deltas: Vec<Delta>) -> TargetFile {
273 TargetFile {
274 name: name.to_string(),
275 deltas,
276 }
277 }
278
279 fn delta(vid: u32, dx: f32, dy: f32, dz: f32) -> Delta {
280 Delta { vid, dx, dy, dz }
281 }
282
283 #[test]
286 fn build_empty_no_vertices() {
287 let map = InfluenceMap::build(&[]);
288 assert_eq!(map.vertex_count, 0);
289 assert_eq!(map.affected_vertex_count(), 0);
290 }
291
292 #[test]
293 fn build_single_target_single_vertex() {
294 let t = make_target("height", vec![delta(10, 1.0, 0.0, 0.0)]);
295 let map = InfluenceMap::build(&[("height", &t)]);
296 assert_eq!(map.vertex_count, 1);
297 let vi = map.get(10).expect("should succeed");
298 assert_eq!(vi.vertex_id, 10);
299 assert_eq!(vi.influences.len(), 1);
300 assert_eq!(vi.influences[0].0, "height");
301 assert!((vi.influences[0].1 - 1.0).abs() < 1e-6);
302 }
303
304 #[test]
305 fn build_multiple_targets_same_vertex() {
306 let t1 = make_target("m1", vec![delta(42, 1.0, 0.0, 0.0)]);
307 let t2 = make_target("m2", vec![delta(42, 0.0, 1.0, 0.0)]);
308 let t3 = make_target("m3", vec![delta(42, 0.0, 0.0, 1.0)]);
309 let map = InfluenceMap::build(&[("m1", &t1), ("m2", &t2), ("m3", &t3)]);
310 assert_eq!(map.vertex_count, 1);
311 let vi = map.get(42).expect("should succeed");
312 assert_eq!(vi.influences.len(), 3);
313 }
314
315 #[test]
318 fn vertex_influence_total_magnitude() {
319 let t1 = make_target("a", vec![delta(5, 3.0, 4.0, 0.0)]); let t2 = make_target("b", vec![delta(5, 0.0, 0.0, 2.0)]); let map = InfluenceMap::build(&[("a", &t1), ("b", &t2)]);
322 let vi = map.get(5).expect("should succeed");
323 assert!((vi.total_magnitude() - 7.0).abs() < 1e-5);
324 }
325
326 #[test]
327 fn vertex_influence_dominant_target() {
328 let t1 = make_target("small", vec![delta(7, 0.0, 0.0, 1.0)]); let t2 = make_target("large", vec![delta(7, 3.0, 4.0, 0.0)]); let map = InfluenceMap::build(&[("small", &t1), ("large", &t2)]);
331 let vi = map.get(7).expect("should succeed");
332 assert_eq!(vi.dominant_target(), Some("large"));
333 }
334
335 #[test]
336 fn dominant_target_none_for_empty() {
337 let vi = VertexInfluence {
338 vertex_id: 0,
339 influences: vec![],
340 };
341 assert_eq!(vi.dominant_target(), None);
342 assert!((vi.total_magnitude() - 0.0).abs() < 1e-9);
343 }
344
345 #[test]
348 fn affected_vertex_count_correct() {
349 let t = make_target(
350 "t",
351 vec![
352 delta(1, 0.1, 0.0, 0.0),
353 delta(2, 0.2, 0.0, 0.0),
354 delta(3, 0.3, 0.0, 0.0),
355 ],
356 );
357 let map = InfluenceMap::build(&[("t", &t)]);
358 assert_eq!(map.affected_vertex_count(), 3);
359 }
360
361 #[test]
362 fn top_vertices_sorted_desc() {
363 let t = make_target(
364 "t",
365 vec![
366 delta(1, 1.0, 0.0, 0.0), delta(2, 3.0, 4.0, 0.0), delta(3, 0.0, 2.0, 0.0), ],
370 );
371 let map = InfluenceMap::build(&[("t", &t)]);
372 let top = map.top_vertices(2);
373 assert_eq!(top.len(), 2);
374 assert_eq!(top[0].vertex_id, 2); assert_eq!(top[1].vertex_id, 3); }
377
378 #[test]
379 fn top_vertices_clamps_to_available() {
380 let t = make_target("t", vec![delta(0, 1.0, 0.0, 0.0)]);
381 let map = InfluenceMap::build(&[("t", &t)]);
382 let top = map.top_vertices(100);
383 assert_eq!(top.len(), 1);
384 }
385
386 #[test]
389 fn vertices_for_target_correct() {
390 let t1 = make_target(
391 "alpha",
392 vec![delta(10, 1.0, 0.0, 0.0), delta(20, 1.0, 0.0, 0.0)],
393 );
394 let t2 = make_target(
395 "beta",
396 vec![delta(20, 0.5, 0.0, 0.0), delta(30, 0.5, 0.0, 0.0)],
397 );
398 let map = InfluenceMap::build(&[("alpha", &t1), ("beta", &t2)]);
399 let verts_alpha = map.vertices_for_target("alpha");
400 assert_eq!(verts_alpha, vec![10, 20]);
401 let verts_beta = map.vertices_for_target("beta");
402 assert_eq!(verts_beta, vec![20, 30]);
403 }
404
405 #[test]
406 fn vertices_for_unknown_target_empty() {
407 let t = make_target("real", vec![delta(1, 1.0, 0.0, 0.0)]);
408 let map = InfluenceMap::build(&[("real", &t)]);
409 assert!(map.vertices_for_target("ghost").is_empty());
410 }
411
412 #[test]
413 fn targets_for_vertex_returns_all() {
414 let t1 = make_target("x", vec![delta(99, 1.0, 0.0, 0.0)]);
415 let t2 = make_target("y", vec![delta(99, 0.0, 2.0, 0.0)]);
416 let t3 = make_target("z", vec![delta(99, 0.0, 0.0, 3.0)]);
417 let map = InfluenceMap::build(&[("x", &t1), ("y", &t2), ("z", &t3)]);
418 let targets = map.targets_for_vertex(99);
419 assert_eq!(targets.len(), 3);
420 let names: Vec<&str> = targets.iter().map(|(n, _)| *n).collect();
421 assert!(names.contains(&"x"));
422 assert!(names.contains(&"y"));
423 assert!(names.contains(&"z"));
424 }
425
426 #[test]
427 fn targets_for_unknown_vertex_empty() {
428 let map = InfluenceMap::build(&[]);
429 assert!(map.targets_for_vertex(999).is_empty());
430 }
431
432 #[test]
435 fn target_stats_vertex_count_correct() {
436 let t1 = make_target("aa", vec![delta(1, 1.0, 0.0, 0.0), delta(2, 2.0, 0.0, 0.0)]);
437 let t2 = make_target(
438 "bb",
439 vec![
440 delta(2, 0.5, 0.0, 0.0),
441 delta(3, 0.5, 0.0, 0.0),
442 delta(4, 0.5, 0.0, 0.0),
443 ],
444 );
445 let map = InfluenceMap::build(&[("aa", &t1), ("bb", &t2)]);
446 let stats = map.target_stats();
447 let aa = stats
448 .iter()
449 .find(|(n, _, _)| n == "aa")
450 .expect("should succeed");
451 let bb = stats
452 .iter()
453 .find(|(n, _, _)| n == "bb")
454 .expect("should succeed");
455 assert_eq!(aa.1, 2); assert_eq!(bb.1, 3); assert!((aa.2 - 3.0).abs() < 1e-5); assert!((bb.2 - 1.5).abs() < 1e-5); }
460
461 #[test]
464 fn isolated_vertices_single_target() {
465 let t1 = make_target("only", vec![delta(5, 1.0, 0.0, 0.0)]);
466 let t2 = make_target(
467 "shared",
468 vec![delta(5, 0.5, 0.0, 0.0), delta(6, 0.5, 0.0, 0.0)],
469 );
470 let map = InfluenceMap::build(&[("only", &t1), ("shared", &t2)]);
471 let isolated = map.isolated_vertices();
472 assert_eq!(isolated, vec![6]);
474 }
475
476 #[test]
477 fn shared_vertices_min_two() {
478 let t1 = make_target("p", vec![delta(1, 1.0, 0.0, 0.0), delta(2, 1.0, 0.0, 0.0)]);
479 let t2 = make_target("q", vec![delta(2, 1.0, 0.0, 0.0), delta(3, 1.0, 0.0, 0.0)]);
480 let t3 = make_target("r", vec![delta(2, 1.0, 0.0, 0.0)]);
481 let map = InfluenceMap::build(&[("p", &t1), ("q", &t2), ("r", &t3)]);
482 let shared2 = map.shared_vertices(2);
483 assert_eq!(shared2, vec![2]);
485 let shared3 = map.shared_vertices(3);
486 assert_eq!(shared3, vec![2]);
487 let shared4 = map.shared_vertices(4);
488 assert!(shared4.is_empty());
489 }
490
491 #[test]
494 fn influences_sorted_by_magnitude() {
495 let t_big = make_target("big", vec![delta(0, 3.0, 4.0, 0.0)]); let t_mid = make_target("mid", vec![delta(0, 0.0, 2.0, 0.0)]); let t_small = make_target("small", vec![delta(0, 1.0, 0.0, 0.0)]); let map = InfluenceMap::build(&[("small", &t_small), ("big", &t_big), ("mid", &t_mid)]);
499 let vi = map.get(0).expect("should succeed");
500 assert_eq!(vi.influences.len(), 3);
501 assert_eq!(vi.influences[0].0, "big");
502 assert!((vi.influences[0].1 - 5.0).abs() < 1e-5);
503 assert_eq!(vi.influences[1].0, "mid");
504 assert!((vi.influences[1].1 - 2.0).abs() < 1e-5);
505 assert_eq!(vi.influences[2].0, "small");
506 assert!((vi.influences[2].1 - 1.0).abs() < 1e-5);
507 }
508
509 #[test]
512 fn build_influence_map_fn_equivalent() {
513 let t = make_target("t", vec![delta(1, 1.0, 0.0, 0.0)]);
514 let map = build_influence_map(&[("t", &t)]);
515 assert_eq!(map.vertex_count, 1);
516 assert!(map.get(1).is_some());
517 }
518
519 #[test]
520 fn top_influences_for_vertex_returns_n() {
521 let t1 = make_target("big", vec![delta(0, 3.0, 4.0, 0.0)]); let t2 = make_target("mid", vec![delta(0, 0.0, 2.0, 0.0)]); let t3 = make_target("small", vec![delta(0, 1.0, 0.0, 0.0)]); let map = InfluenceMap::build(&[("big", &t1), ("mid", &t2), ("small", &t3)]);
525 let top2 = top_influences_for_vertex(&map, 0, 2);
526 assert_eq!(top2.len(), 2);
527 assert_eq!(top2[0].0, "big");
528 assert_eq!(top2[1].0, "mid");
529 }
530
531 #[test]
532 fn top_influences_for_unknown_vertex_empty() {
533 let map = InfluenceMap::build(&[]);
534 assert!(top_influences_for_vertex(&map, 999, 5).is_empty());
535 }
536
537 #[test]
538 fn target_vertex_coverage_fraction() {
539 let t1 = make_target(
540 "cover",
541 vec![delta(1, 1.0, 0.0, 0.0), delta(2, 1.0, 0.0, 0.0)],
542 );
543 let map = InfluenceMap::build(&[("cover", &t1)]);
544 let cov = target_vertex_coverage(&map, "cover", &[1, 2, 3]);
546 assert!((cov - 2.0 / 3.0).abs() < 1e-5);
547 }
548
549 #[test]
550 fn target_vertex_coverage_empty_returns_zero() {
551 let map = InfluenceMap::build(&[]);
552 assert!((target_vertex_coverage(&map, "any", &[])).abs() < 1e-9);
553 }
554
555 #[test]
556 fn vertex_target_overlap_identical_sets() {
557 let t = make_target("t", vec![delta(1, 1.0, 0.0, 0.0), delta(2, 1.0, 0.0, 0.0)]);
558 let map = InfluenceMap::build(&[("t", &t)]);
559 let overlap = vertex_target_overlap(&map, "t", "t");
561 assert!((overlap - 1.0).abs() < 1e-5);
562 }
563
564 #[test]
565 fn vertex_target_overlap_disjoint_sets() {
566 let t1 = make_target("a", vec![delta(1, 1.0, 0.0, 0.0)]);
567 let t2 = make_target("b", vec![delta(2, 1.0, 0.0, 0.0)]);
568 let map = InfluenceMap::build(&[("a", &t1), ("b", &t2)]);
569 let overlap = vertex_target_overlap(&map, "a", "b");
570 assert!((overlap - 0.0).abs() < 1e-5);
571 }
572
573 #[test]
574 fn vertex_target_overlap_partial() {
575 let t1 = make_target("a", vec![delta(1, 1.0, 0.0, 0.0), delta(2, 1.0, 0.0, 0.0)]);
577 let t2 = make_target("b", vec![delta(2, 1.0, 0.0, 0.0), delta(3, 1.0, 0.0, 0.0)]);
578 let map = InfluenceMap::build(&[("a", &t1), ("b", &t2)]);
579 let overlap = vertex_target_overlap(&map, "a", "b");
580 assert!((overlap - 1.0 / 3.0).abs() < 1e-5);
581 }
582
583 #[test]
584 fn influence_map_stats_basic() {
585 let t1 = make_target("x", vec![delta(1, 1.0, 0.0, 0.0), delta(2, 1.0, 0.0, 0.0)]);
586 let t2 = make_target("y", vec![delta(2, 0.0, 1.0, 0.0), delta(3, 0.0, 0.0, 1.0)]);
587 let map = InfluenceMap::build(&[("x", &t1), ("y", &t2)]);
588 let stats = influence_map_stats(&map);
589 assert_eq!(stats.affected_vertices, 3); assert_eq!(stats.target_count, 2); assert!((stats.total_magnitude - 4.0).abs() < 1e-5);
593 assert_eq!(stats.max_targets_per_vertex, 2);
595 }
596
597 #[test]
598 fn influence_map_stats_empty() {
599 let map = InfluenceMap::build(&[]);
600 let stats = influence_map_stats(&map);
601 assert_eq!(stats.affected_vertices, 0);
602 assert_eq!(stats.target_count, 0);
603 assert!((stats.total_magnitude).abs() < 1e-9);
604 assert!((stats.avg_targets_per_vertex).abs() < 1e-9);
605 }
606
607 #[test]
608 fn iter_visits_all_vertices() {
609 let t = make_target(
610 "t",
611 vec![
612 delta(10, 1.0, 0.0, 0.0),
613 delta(20, 2.0, 0.0, 0.0),
614 delta(30, 3.0, 0.0, 0.0),
615 ],
616 );
617 let map = InfluenceMap::build(&[("t", &t)]);
618 let mut vids: Vec<u32> = map.iter().map(|vi| vi.vertex_id).collect();
619 vids.sort_unstable();
620 assert_eq!(vids, vec![10, 20, 30]);
621 }
622}