Skip to main content

tensorlogic_adapters/
hierarchy_viz.rs

1//! Domain hierarchy visualization and analysis.
2//!
3//! Renders the domain subtype hierarchy as ASCII trees, DOT (Graphviz) format,
4//! and computes structural metrics (depth, breadth, root count).
5//!
6//! # Example
7//!
8//! ```rust
9//! use tensorlogic_adapters::{SymbolTable, DomainInfo, DomainHierarchy};
10//! use tensorlogic_adapters::hierarchy_viz::{render_hierarchy_ascii, hierarchy_stats};
11//!
12//! let mut table = SymbolTable::new();
13//! table.add_domain(DomainInfo::new("Entity", 500)).unwrap();
14//! table.add_domain(DomainInfo::new("Person", 100)).unwrap();
15//! table.add_domain(DomainInfo::new("Student", 50)).unwrap();
16//!
17//! let mut hierarchy = DomainHierarchy::new();
18//! hierarchy.add_subtype("Person", "Entity");
19//! hierarchy.add_subtype("Student", "Person");
20//!
21//! let ascii = render_hierarchy_ascii(&table, &hierarchy);
22//! assert!(ascii.contains("Entity"));
23//! assert!(ascii.contains("Person"));
24//!
25//! let stats = hierarchy_stats(&table, &hierarchy);
26//! assert_eq!(stats.total_domains, 3);
27//! ```
28
29use std::collections::{BTreeMap, BTreeSet};
30use std::fmt::Write;
31
32use crate::{DomainHierarchy, SymbolTable};
33
34/// A node in the domain hierarchy tree.
35#[derive(Debug, Clone)]
36pub struct HierarchyNode {
37    /// Domain name.
38    pub name: String,
39    /// Child nodes in the hierarchy.
40    pub children: Vec<HierarchyNode>,
41    /// Depth in the tree (root = 0).
42    pub depth: usize,
43    /// Cardinality of the domain from the symbol table.
44    pub domain_size: usize,
45}
46
47/// Statistics about the domain hierarchy.
48#[derive(Debug, Clone, Default)]
49pub struct HierarchyStats {
50    /// Number of root domains (no parent).
51    pub root_count: usize,
52    /// Total number of domains.
53    pub total_domains: usize,
54    /// Maximum depth of the hierarchy tree.
55    pub max_depth: usize,
56    /// Maximum branching factor (max children of any single node).
57    pub max_breadth: usize,
58    /// Number of leaf domains (no children).
59    pub leaf_count: usize,
60}
61
62impl HierarchyStats {
63    /// Returns `true` if the hierarchy is flat (max depth <= 1).
64    pub fn is_flat(&self) -> bool {
65        self.max_depth <= 1
66    }
67
68    /// Returns a one-line summary string.
69    pub fn summary(&self) -> String {
70        format!(
71            "{} domains, depth {}, {} roots, {} leaves",
72            self.total_domains, self.max_depth, self.root_count, self.leaf_count
73        )
74    }
75}
76
77/// Build the hierarchy tree from a [`SymbolTable`] and [`DomainHierarchy`].
78///
79/// Domains present in the symbol table but absent from the hierarchy are treated
80/// as roots. The returned vector contains one [`HierarchyNode`] per root,
81/// sorted alphabetically by name for deterministic output.
82pub fn build_hierarchy(table: &SymbolTable, hierarchy: &DomainHierarchy) -> Vec<HierarchyNode> {
83    // Collect all domain names from the symbol table.
84    let domain_names: BTreeSet<String> = table.domains.keys().cloned().collect();
85
86    if domain_names.is_empty() {
87        return Vec::new();
88    }
89
90    // Build a map of parent -> sorted children (only for domains in the table).
91    let mut children_map: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
92    let mut has_parent: BTreeSet<String> = BTreeSet::new();
93
94    for name in &domain_names {
95        if let Some(parent) = hierarchy.get_parent(name) {
96            // Only track the relationship if the parent is also in the table.
97            if domain_names.contains(parent) {
98                children_map
99                    .entry(parent.to_string())
100                    .or_default()
101                    .insert(name.clone());
102                has_parent.insert(name.clone());
103            }
104        }
105    }
106
107    // Root domains: present in table but not a child of any other domain in the table.
108    let roots: Vec<String> = domain_names
109        .iter()
110        .filter(|name| !has_parent.contains(*name))
111        .cloned()
112        .collect();
113
114    // Recursively build nodes.
115    roots
116        .into_iter()
117        .map(|name| build_node(&name, 0, table, &children_map))
118        .collect()
119}
120
121/// Recursively construct a [`HierarchyNode`].
122fn build_node(
123    name: &str,
124    depth: usize,
125    table: &SymbolTable,
126    children_map: &BTreeMap<String, BTreeSet<String>>,
127) -> HierarchyNode {
128    let domain_size = table.get_domain(name).map(|d| d.cardinality).unwrap_or(0);
129
130    let children: Vec<HierarchyNode> = children_map
131        .get(name)
132        .map(|kids| {
133            kids.iter()
134                .map(|child| build_node(child, depth + 1, table, children_map))
135                .collect()
136        })
137        .unwrap_or_default();
138
139    HierarchyNode {
140        name: name.to_string(),
141        children,
142        depth,
143        domain_size,
144    }
145}
146
147/// Compute hierarchy statistics from a [`SymbolTable`] and [`DomainHierarchy`].
148pub fn hierarchy_stats(table: &SymbolTable, hierarchy: &DomainHierarchy) -> HierarchyStats {
149    let roots = build_hierarchy(table, hierarchy);
150
151    if roots.is_empty() {
152        return HierarchyStats::default();
153    }
154
155    let mut stats = HierarchyStats {
156        root_count: roots.len(),
157        total_domains: 0,
158        max_depth: 0,
159        max_breadth: 0,
160        leaf_count: 0,
161    };
162
163    for root in &roots {
164        collect_stats(root, &mut stats);
165    }
166
167    stats
168}
169
170/// Walk the tree collecting statistics.
171fn collect_stats(node: &HierarchyNode, stats: &mut HierarchyStats) {
172    stats.total_domains += 1;
173
174    let effective_depth = node.depth + 1; // depth is 0-based, we want max level count
175    if effective_depth > stats.max_depth {
176        stats.max_depth = effective_depth;
177    }
178
179    let breadth = node.children.len();
180    if breadth > stats.max_breadth {
181        stats.max_breadth = breadth;
182    }
183
184    if node.children.is_empty() {
185        stats.leaf_count += 1;
186    }
187
188    for child in &node.children {
189        collect_stats(child, stats);
190    }
191}
192
193/// Render the domain hierarchy as an ASCII tree.
194///
195/// # Example output
196///
197/// ```text
198/// Entity (500)
199/// ├── Organization (200)
200/// │   └── University (20)
201/// └── Person (100)
202///     ├── Student (50)
203///     └── Teacher (30)
204/// ```
205pub fn render_hierarchy_ascii(table: &SymbolTable, hierarchy: &DomainHierarchy) -> String {
206    let roots = build_hierarchy(table, hierarchy);
207    let mut out = String::new();
208
209    if roots.is_empty() {
210        out.push_str("(empty hierarchy)\n");
211        return out;
212    }
213
214    for (i, root) in roots.iter().enumerate() {
215        let is_last = i == roots.len() - 1;
216        render_node_ascii(&mut out, root, "", is_last, true);
217    }
218
219    out
220}
221
222/// Recursive ASCII renderer for a single node.
223fn render_node_ascii(
224    out: &mut String,
225    node: &HierarchyNode,
226    prefix: &str,
227    is_last: bool,
228    is_root: bool,
229) {
230    let connector = if is_root {
231        ""
232    } else if is_last {
233        "└── "
234    } else {
235        "├── "
236    };
237
238    let _ = writeln!(
239        out,
240        "{}{}{} ({})",
241        prefix, connector, node.name, node.domain_size
242    );
243
244    let child_prefix = if is_root {
245        String::new()
246    } else if is_last {
247        format!("{}    ", prefix)
248    } else {
249        format!("{}│   ", prefix)
250    };
251
252    for (i, child) in node.children.iter().enumerate() {
253        let child_is_last = i == node.children.len() - 1;
254        render_node_ascii(out, child, &child_prefix, child_is_last, false);
255    }
256}
257
258/// Render hierarchy as DOT (Graphviz) format.
259///
260/// The output can be piped to `dot -Tpng` to produce an image.
261pub fn render_hierarchy_dot(table: &SymbolTable, hierarchy: &DomainHierarchy) -> String {
262    let roots = build_hierarchy(table, hierarchy);
263    let mut dot = String::new();
264    let _ = writeln!(dot, "digraph DomainHierarchy {{");
265    let _ = writeln!(dot, "  rankdir=TB;");
266    let _ = writeln!(dot, "  node [shape=box];");
267
268    for root in &roots {
269        render_dot_node(&mut dot, root);
270    }
271
272    let _ = writeln!(dot, "}}");
273    dot
274}
275
276/// Recursive DOT renderer for a single node and its children.
277fn render_dot_node(dot: &mut String, node: &HierarchyNode) {
278    let _ = writeln!(
279        dot,
280        "  \"{}\" [label=\"{}\\n(size={})\"];",
281        node.name, node.name, node.domain_size
282    );
283    for child in &node.children {
284        let _ = writeln!(dot, "  \"{}\" -> \"{}\";", node.name, child.name);
285        render_dot_node(dot, child);
286    }
287}
288
289/// Find all ancestors of a domain (parent chain up to a root).
290///
291/// Returns an empty vector if the domain has no parent in the hierarchy.
292/// The result order is parent-first (immediate parent, then grandparent, etc.).
293pub fn ancestors(hierarchy: &DomainHierarchy, domain: &str) -> Vec<String> {
294    hierarchy.get_ancestors(domain)
295}
296
297/// Find all descendants of a domain (all children recursively).
298///
299/// Returns an empty vector if the domain has no children.
300pub fn descendants(hierarchy: &DomainHierarchy, domain: &str) -> Vec<String> {
301    hierarchy.get_descendants(domain)
302}
303
304/// Render a compact indented listing (simpler than ASCII tree).
305///
306/// Each level is indented by two spaces. Useful for logging.
307pub fn render_hierarchy_indented(table: &SymbolTable, hierarchy: &DomainHierarchy) -> String {
308    let roots = build_hierarchy(table, hierarchy);
309    let mut out = String::new();
310
311    if roots.is_empty() {
312        out.push_str("(empty hierarchy)\n");
313        return out;
314    }
315
316    for root in &roots {
317        render_indented_node(&mut out, root);
318    }
319
320    out
321}
322
323/// Recursive indented renderer.
324fn render_indented_node(out: &mut String, node: &HierarchyNode) {
325    let indent = "  ".repeat(node.depth);
326    let _ = writeln!(out, "{}{} ({})", indent, node.name, node.domain_size);
327    for child in &node.children {
328        render_indented_node(out, child);
329    }
330}
331
332/// Find the path from one domain to another through the hierarchy.
333///
334/// Returns `None` if no path exists (i.e., neither is an ancestor of the other).
335pub fn path_between(hierarchy: &DomainHierarchy, from: &str, to: &str) -> Option<Vec<String>> {
336    if from == to {
337        return Some(vec![from.to_string()]);
338    }
339
340    // Check if `to` is an ancestor of `from`.
341    let from_ancestors = hierarchy.get_ancestors(from);
342    if let Some(pos) = from_ancestors.iter().position(|a| a == to) {
343        let mut path = vec![from.to_string()];
344        path.extend(from_ancestors[..=pos].to_vec());
345        return Some(path);
346    }
347
348    // Check if `from` is an ancestor of `to`.
349    let to_ancestors = hierarchy.get_ancestors(to);
350    if let Some(pos) = to_ancestors.iter().position(|a| a == from) {
351        let mut path: Vec<String> = to_ancestors[..=pos].iter().rev().cloned().collect();
352        path.push(to.to_string());
353        // Reverse so it goes from `from` downward.
354        // Actually, to_ancestors is [parent, grandparent, ...], position found means
355        // from is at index `pos`. Build path from->...->to.
356        let mut result = vec![from.to_string()];
357        for ancestor in to_ancestors[..pos].iter().rev() {
358            result.push(ancestor.clone());
359        }
360        result.push(to.to_string());
361        return Some(result);
362    }
363
364    None
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370    use crate::DomainInfo;
371
372    /// Helper: create a symbol table with the given (name, cardinality) pairs.
373    fn make_table(domains: &[(&str, usize)]) -> SymbolTable {
374        let mut table = SymbolTable::new();
375        for &(name, card) in domains {
376            table
377                .add_domain(DomainInfo::new(name, card))
378                .expect("add_domain should succeed");
379        }
380        table
381    }
382
383    #[test]
384    fn test_hierarchy_empty_table() {
385        let table = SymbolTable::new();
386        let hierarchy = DomainHierarchy::new();
387        let ascii = render_hierarchy_ascii(&table, &hierarchy);
388        assert_eq!(ascii, "(empty hierarchy)\n");
389    }
390
391    #[test]
392    fn test_hierarchy_flat_domains() {
393        let table = make_table(&[("Alpha", 10), ("Beta", 20), ("Gamma", 30)]);
394        let hierarchy = DomainHierarchy::new();
395        let ascii = render_hierarchy_ascii(&table, &hierarchy);
396        // All three should appear as roots, each on its own line.
397        assert!(ascii.contains("Alpha"));
398        assert!(ascii.contains("Beta"));
399        assert!(ascii.contains("Gamma"));
400        // No tree connectors for roots.
401        assert!(!ascii.contains("├"));
402        assert!(!ascii.contains("└"));
403    }
404
405    #[test]
406    fn test_hierarchy_stats_flat() {
407        let table = make_table(&[("A", 1), ("B", 2), ("C", 3)]);
408        let hierarchy = DomainHierarchy::new();
409        let stats = hierarchy_stats(&table, &hierarchy);
410        assert_eq!(stats.max_depth, 1);
411        assert!(stats.is_flat());
412        assert_eq!(stats.root_count, 3);
413        assert_eq!(stats.leaf_count, 3);
414    }
415
416    #[test]
417    fn test_hierarchy_stats_summary() {
418        let table = make_table(&[("A", 1), ("B", 2)]);
419        let hierarchy = DomainHierarchy::new();
420        let summary = hierarchy_stats(&table, &hierarchy).summary();
421        assert!(summary.contains("2 domains"));
422        assert!(summary.contains("2 roots"));
423    }
424
425    #[test]
426    fn test_hierarchy_node_depth() {
427        let table = make_table(&[("Entity", 500), ("Person", 100), ("Student", 50)]);
428        let mut hierarchy = DomainHierarchy::new();
429        hierarchy.add_subtype("Person", "Entity");
430        hierarchy.add_subtype("Student", "Person");
431
432        let roots = build_hierarchy(&table, &hierarchy);
433        assert_eq!(roots.len(), 1);
434        assert_eq!(roots[0].depth, 0);
435        assert_eq!(roots[0].name, "Entity");
436
437        // Person is child of Entity.
438        assert_eq!(roots[0].children.len(), 1);
439        assert_eq!(roots[0].children[0].depth, 1);
440
441        // Student is child of Person.
442        assert_eq!(roots[0].children[0].children.len(), 1);
443        assert_eq!(roots[0].children[0].children[0].depth, 2);
444    }
445
446    #[test]
447    fn test_hierarchy_ascii_contains_names() {
448        let table = make_table(&[("Entity", 500), ("Person", 100)]);
449        let mut hierarchy = DomainHierarchy::new();
450        hierarchy.add_subtype("Person", "Entity");
451
452        let ascii = render_hierarchy_ascii(&table, &hierarchy);
453        assert!(ascii.contains("Entity"));
454        assert!(ascii.contains("Person"));
455        assert!(ascii.contains("500"));
456        assert!(ascii.contains("100"));
457    }
458
459    #[test]
460    fn test_hierarchy_ascii_tree_connectors() {
461        let table = make_table(&[("Entity", 500), ("Person", 100), ("Org", 200)]);
462        let mut hierarchy = DomainHierarchy::new();
463        hierarchy.add_subtype("Person", "Entity");
464        hierarchy.add_subtype("Org", "Entity");
465
466        let ascii = render_hierarchy_ascii(&table, &hierarchy);
467        // With two children we expect both connectors.
468        let has_branch = ascii.contains('├') || ascii.contains('└');
469        assert!(has_branch, "Expected tree connectors in:\n{}", ascii);
470    }
471
472    #[test]
473    fn test_hierarchy_dot_contains_digraph() {
474        let table = make_table(&[("A", 1)]);
475        let hierarchy = DomainHierarchy::new();
476        let dot = render_hierarchy_dot(&table, &hierarchy);
477        assert!(dot.starts_with("digraph DomainHierarchy {"));
478    }
479
480    #[test]
481    fn test_hierarchy_dot_contains_edges() {
482        let table = make_table(&[("Parent", 10), ("Child", 5)]);
483        let mut hierarchy = DomainHierarchy::new();
484        hierarchy.add_subtype("Child", "Parent");
485
486        let dot = render_hierarchy_dot(&table, &hierarchy);
487        assert!(dot.contains("->"), "Expected edges in DOT output:\n{}", dot);
488        assert!(dot.contains("\"Parent\" -> \"Child\""));
489    }
490
491    #[test]
492    fn test_hierarchy_stats_default() {
493        let stats = HierarchyStats::default();
494        assert_eq!(stats.root_count, 0);
495        assert_eq!(stats.total_domains, 0);
496        assert_eq!(stats.max_depth, 0);
497        assert_eq!(stats.max_breadth, 0);
498        assert_eq!(stats.leaf_count, 0);
499    }
500
501    #[test]
502    fn test_hierarchy_stats_leaf_count() {
503        let table = make_table(&[
504            ("Entity", 500),
505            ("Person", 100),
506            ("Student", 50),
507            ("Teacher", 30),
508            ("Org", 200),
509        ]);
510        let mut hierarchy = DomainHierarchy::new();
511        hierarchy.add_subtype("Person", "Entity");
512        hierarchy.add_subtype("Student", "Person");
513        hierarchy.add_subtype("Teacher", "Person");
514        hierarchy.add_subtype("Org", "Entity");
515
516        let stats = hierarchy_stats(&table, &hierarchy);
517        // Leaves: Student, Teacher, Org
518        assert_eq!(stats.leaf_count, 3);
519    }
520
521    #[test]
522    fn test_hierarchy_stats_root_count() {
523        let table = make_table(&[("A", 1), ("B", 2), ("C", 3), ("D", 4)]);
524        let mut hierarchy = DomainHierarchy::new();
525        hierarchy.add_subtype("B", "A");
526        // C and D are roots, A is a root.
527        let stats = hierarchy_stats(&table, &hierarchy);
528        assert_eq!(stats.root_count, 3); // A, C, D
529    }
530
531    #[test]
532    fn test_ancestors_empty() {
533        let hierarchy = DomainHierarchy::new();
534        let result = ancestors(&hierarchy, "Root");
535        assert!(result.is_empty());
536    }
537
538    #[test]
539    fn test_descendants_leaf() {
540        let hierarchy = DomainHierarchy::new();
541        // A domain not in the hierarchy at all has no descendants.
542        let result = descendants(&hierarchy, "Leaf");
543        assert!(result.is_empty());
544    }
545
546    #[test]
547    fn test_hierarchy_render_deterministic() {
548        let table = make_table(&[
549            ("Entity", 500),
550            ("Person", 100),
551            ("Student", 50),
552            ("Org", 200),
553        ]);
554        let mut hierarchy = DomainHierarchy::new();
555        hierarchy.add_subtype("Person", "Entity");
556        hierarchy.add_subtype("Student", "Person");
557        hierarchy.add_subtype("Org", "Entity");
558
559        let ascii1 = render_hierarchy_ascii(&table, &hierarchy);
560        let ascii2 = render_hierarchy_ascii(&table, &hierarchy);
561        assert_eq!(ascii1, ascii2, "Rendering should be deterministic");
562    }
563
564    #[test]
565    fn test_hierarchy_multiple_roots() {
566        let table = make_table(&[("TreeA", 10), ("TreeB", 20), ("ChildA", 5)]);
567        let mut hierarchy = DomainHierarchy::new();
568        hierarchy.add_subtype("ChildA", "TreeA");
569
570        let roots = build_hierarchy(&table, &hierarchy);
571        assert_eq!(roots.len(), 2); // TreeA and TreeB
572        let names: Vec<&str> = roots.iter().map(|r| r.name.as_str()).collect();
573        assert!(names.contains(&"TreeA"));
574        assert!(names.contains(&"TreeB"));
575    }
576
577    #[test]
578    fn test_hierarchy_stats_max_breadth() {
579        let table = make_table(&[("Root", 100), ("A", 10), ("B", 20), ("C", 30)]);
580        let mut hierarchy = DomainHierarchy::new();
581        hierarchy.add_subtype("A", "Root");
582        hierarchy.add_subtype("B", "Root");
583        hierarchy.add_subtype("C", "Root");
584
585        let stats = hierarchy_stats(&table, &hierarchy);
586        assert_eq!(stats.max_breadth, 3);
587    }
588
589    #[test]
590    fn test_hierarchy_node_domain_size() {
591        let table = make_table(&[("Entity", 500), ("Person", 100)]);
592        let mut hierarchy = DomainHierarchy::new();
593        hierarchy.add_subtype("Person", "Entity");
594
595        let roots = build_hierarchy(&table, &hierarchy);
596        assert_eq!(roots[0].domain_size, 500);
597        assert_eq!(roots[0].children[0].domain_size, 100);
598    }
599}