# Tree Data Structure Redesign ## Problem The current nested-tuple tree representation requires full recursive traversal for every structural query (weight, arity, leaves, min_leaf, subtree_degree). Since these queries are called repeatedly in performance-critical paths—especially the bar/cobar differentials `_d2_on_basis`—this causes O(v²) or worse overhead per differential computation. ## Current Representation Trees are encoded as nested Python tuples: - **Leaf**: `int` in `{1, ..., n}` - **Internal vertex**: `(decoration, child_1, ..., child_k)` where `decoration` is an operad/cooperad basis key (a tuple) and each `child_j` is a leaf or subtree. Example: `((1, 2), ((1,), 1, 2), 3)` — arity 3, weight 2. ### Bottlenecks 1. **`subtree_degree` recomputation in `_d2_on_basis`**: For each internal edge, the degrees of all sibling subtrees are recomputed from scratch. This is O(v² × lookup) per tree. 2. **`min_leaf` recomputation in shuffle normalization**: Called O(k) times per vertex during `to_shuffle_tree_bar/cobar`, each O(n) traversal. 3. **`leaves()` recomputation**: O(n) set union called in `after_cobar_deg`, `tree_arity`, `validate_tree`, and others. 4. **`weight()` recomputation**: O(v) recursive count called in enumeration. 5. **Full recursive copy on every structural operation**: `relabel_leaves`, `graft`, `contract_edge`, `expand_vertex` all rebuild the entire tree. ## New Design: `RootedTree` Class ### Core Idea Replace nested tuples with an immutable `RootedTree` class that **eagerly computes and caches structural properties** at construction time. Since every tree operation already rebuilds the tree from scratch anyway, the per-node overhead of caching is amortized over the many subsequent queries. ### Invariants - Leaves remain as plain `int` values (no wrapping). - `RootedTree` is immutable and hashable — works as SageMath `@cached_method` arguments and `CombinatorialFreeModule` basis keys. - Identity comparison (`is`) continues to work for DFS-extracted vertices. ### Cached Properties (computed at construction, O(1) access) | Property | Type | Old cost | New cost | |----------|------|----------|----------| | `weight` | `int` | O(v) | O(1) | | `tree_arity` | `int` | O(n) | O(1) | | `leaves` | `frozenset[int]` | O(n) | O(1) | | `min_leaf` | `int` | O(n) | O(1) | | `arity` (vertex) | `int` | O(1) | O(1) | | `decoration` | `tuple` | O(1) | O(1) | | `children` | `tuple` | O(1) | O(1) | ### Construction Cost Each `RootedTree(decoration, children)` call computes cached properties by merging child properties in O(k) time (where k = number of children). Since every tree modification already takes O(tree_size) for the recursive rebuild, this adds at most a constant factor per node during construction. ### Class API ```python class RootedTree: __slots__ = ( '_decoration', '_children', '_weight', '_tree_arity', '_leaves', '_min_leaf', '_arity', '_hash', ) def __init__(self, decoration: tuple, *children): # children: tuple of (RootedTree | int) # Eagerly compute: weight, leaves, min_leaf, tree_arity ``` ### Comparison & Hashing - `__hash__`: Based on `(decoration, children)` tuple — structurally equivalent trees hash identically. - `__eq__`: Recursive structural comparison (with short-circuit on hash mismatch). - `__lt__`: Lexicographic comparison for SageMath basis ordering. ### Migration Strategy 1. Add `RootedTree` class to `trees.py`. 2. Update all functions in `trees.py` to accept and return `RootedTree`. 3. Add `RootedTree.from_tuple()` for backward compatibility. 4. Update bar/cobar constructions, tree_module, and morphisms. 5. Update tests. ### Operations on `RootedTree` All existing operations are preserved with the same semantics: - `relabel_leaves(tree, mapping)` → returns new `RootedTree` - `graft(tree_top, i, tree_bot)` → returns new `RootedTree` - `contract_edge(tree, parent, pos, dec)` → returns new `RootedTree` - `expand_vertex(tree, target, pos, ...)` → returns new `RootedTree` - `split_at_vertex(tree, target)` → returns `(RootedTree, int, RootedTree)` - `vertices_dfs(tree)` → returns list of `RootedTree` references (identity-safe) ### Future Optimizations (Not in This PR) - **Degree caching**: Store context-specific degrees (bar, cobar, free) via a `degree_cache` dict on each node. This would make `subtree_degree` O(1). - **Persistent/structural sharing**: Use path-copying so that `contract_edge` only rebuilds nodes on the root-to-target path, sharing unchanged subtrees. This is already partially achieved since `RootedTree` children are references. - **Flat array representation**: For very large trees, store nodes in a flat DFS-ordered array with parent/child indices for cache-friendly traversal. --- ## Status Update (2026-04): Planar Coinvariant Enumeration In parallel with tree-level optimization, profiling showed a separate dominant cost in cofree coalgebra basis enumeration: explicit `S_n` orbit-sum construction for invariants. The current implementation now enumerates planar representatives directly and canonicalizes boundary output back to planar keys. ### Mathematical Setup For invariants of the form `X ⊗_{S_n} Y` with `X = X_pl ⊗ k[S_n]`, we use the canonical identification `(X_pl ⊗ k[S_n]) ⊗_{S_n} Y ≅ X_pl ⊗ Y` to avoid explicit orbit sums in basis enumeration. ### Boundary Strategies Compared Two methods were tested. 1. Orbit-sum method (reference): - Build orbit-sum representative. - Apply boundary. - Renormalize to planar representatives. 2. Planar-first method: - Keep only planar representative in the basis. - Apply boundary there. - Canonicalize non-planar output terms to planar keys. ### Result The two methods are not coefficient-identical in general when the module tuple `m` has repeated entries. The difference is a stabilizer factor `|Stab(m)|`. - Over `QQ`, this appears as multiplicative factors (for example factor `2` in binary symmetry cases). - Over positive characteristic, this factor can vanish (for example `2 = 0` in `GF(2)`), so matrix entries can differ by zero/nonzero behavior. Despite this, the two complexes are chain-isomorphic under the coinvariant identification above. The planar-first implementation satisfies `d^2 = 0` and computes the same homology. ### Implementation Notes - `basis_iter` / `basis_weight_iter` enumerate planar terms `term((c_pl, m))` directly (no orbit-sum construction during enumeration). - Boundary matrix assembly canonicalizes non-planar terms via a full canonicalization routine that returns all planar contributions. - This fixes a subtle lossy behavior from single-term canonicalization when planarization has multiple terms (for example with Lie-type factors). ### Validation Summary - `d^2 = 0` checked at weights 2-4 over both `QQ` and `GF(2)`. - Regression tests added for planar basis counting/canonical keys and boundary canonicalization behavior. - Observed speedup on heavy layers around `2.8x` (example: 226s to 80s), with full test suite passing.