---
title: Thinning-Aware Union-Find in Lifting E-Graphs
url: https://www.emergentmind.com/topics/thinning-aware-union-find
type: topic
---

# Thinning-Aware Union-Find in Lifting E-Graphs

Thinning-aware union-find is a union-find whose parent edges are labeled by thinning bitvectors rather than being bare pointers, and whose elements are “fat” identifiers of the form $(\text{Thin}, \text{id})$. In the formulation introduced for lifting e-graphs, thinnings are used to track how variables are used across contexts, so that rigid $\alpha$-canonical variables and lifting are first-class rather than external conventions [2606.22734]. The resulting structure is designed for e-graphs in which context and variable usage are represented explicitly, allowing equality saturation to relate terms that differ by lifting while preserving injectivity, scope safety, and rigid $\alpha$-equivalence.

## 1. Definition and motivation

A thinning is a strictly monotone map between two totally ordered finite sets, representable as a bitvector. Operationally, it is “a recipe for extracting a subsequence of arguments/variables.” In implementation terms, a thinning is represented as `list[bool]`, with `True` meaning “keep this variable” and `False` meaning “drop it” [2606.22734].

The central lifting combinator is parametrized by a thinning bitvector $t$ with a 1 for arguments to keep and 0 for arguments to drop. Type-wise,
\[
\text{lift}_t : (\mathbb{R}^{\text{popcnt}(t)} \to \mathbb{R}) \to (\mathbb{R}^{\text{len}(t)} \to \mathbb{R}).
\]
This makes it possible to relate functions in different variable contexts by thinning their argument lists rather than by treating variables as incidental syntax [2606.22734].

The motivation for a thinning-aware union-find arises in lifting e-graphs. Standard e-graphs are typically ground and use a plain union-find on integer e-class identifiers. That is insufficient when lifting and rigid $\alpha$-canonical variables are part of the representation itself. With only standard union-find on bare integers, one cannot track relationships between e-classes that differ only by lifting or thinning, exploit sharing between differently contextualized terms such as $\sin_1(x)$ and $\sin_2(x)$, or maintain rigid $\alpha$-equivalence while performing equality saturation. The proposed remedy is to fatten identifiers with thinning bitvectors and to annotate union-find structure with thinning transformations [2606.22734].

The approach is explicitly inspired by Co-de Bruijn syntax and slotted e-graphs. Co-de Bruijn syntax pulls context information and index manipulations into an explicit thinning-like structure, while slotted e-graphs parameterize e-classes by slots and attach mappings between context variables and slots to e-node edges. Thinning-aware union-find applies these ideas in an e-graph setting where slots are totally ordered and the mapping is a thinning bitvector [2606.22734].

## 2. Core representation and invariants

The basic identifier is a fat identifier:
```python
type FatId = (Thin, int)
```
The integer is the underlying e-class index, while the thinning records how that e-class is lifted relative to the current context. E-nodes remain structurally ordinary except that their children are fat identifiers:
```python
class ENode:
    f    : str
    args : list[FatId]
```
This same fat-identifier representation is used both for hashing, via lift-pulling smart constructors, and for union-find [2606.22734].

The union-find itself is represented as:
```python
type Id = tuple[Thin, int]

@dataclass
class ThinUF:
    parents : list[Id] = field(default_factory=list)
```
Here `parents[i] = (thiny, yid)` means that node `i` points to parent `yid` through an edge labeled by thinning `thiny`. A root is characterized by a self-loop whose thinning is all-True, so the root is canonical for its own scope [2606.22734].

Several invariants organize the structure.

First, **root identity thinning** requires that for any root `r`, `parents[r] = (thiny, r)` and `all(thiny)` holds. This ensures that the root’s representation is canonical for its scope.

Second, **edge thinning consistency** requires parent-edge thinnings to compose correctly with the thinnings already carried by fat identifiers. In code terms, the composition `comp(thiny, thin)` must be well-typed by matching domain and codomain sizes.

Third, **injectivity / rigid $\alpha$-equivalence** is built into the representation. Thinnings represent injective, strictly monotone maps of variable positions, and “it does not even type check to union two objects with different numbers of variables.” This means scope mismatches are not merely disallowed by policy; they are excluded by the typing discipline of the operations themselves [2606.22734].

Fourth, the structure relies on a **weakest common thinning**,
```python
def wct(f : Thin, g : Thin) -> Thin:
    assert dom(f) == dom(g)
    return [a and b for a,b in zip(f,g)]
```
which keeps exactly the variables kept in both thinnings. This is the maximal common sub-context available to both sides of a merge [2606.22734].

Fifth, it uses a **division** operation,
```python
def div(f : Thin, g : Thin) -> Thin:
    assert dom(f) == dom(g)
    assert all(not a for a,b in zip(f,g) if not b)
    return [a for a,b in zip(f,g) if b]
```
whose precondition expresses that one thinning is thinner than the other. Division computes the thinning required to factor a current context through the common context produced by `wct` [2606.22734].

## 3. Find and union in the thinning-aware setting

The `find` operation takes a fat identifier `(thin, xid)` and walks parent pointers until reaching a root:
```python
def find(self, x : Id) -> Id:
    thin, xid = x
    while True:
        (thiny, yid) = self.parents[xid]
        if xid == yid:
            assert all(thiny)
            return (thin, xid)
        thin = comp(thiny, thin)
        xid = yid
```
Unlike standard union-find, which returns only a root identifier, this version returns both a root id and an accumulated thinning. The accumulated thinning expresses the total transformation from the root context to the original context of the queried identifier. The paper notes that no path compression is implemented in the snippet, though it could be added by updating parent pointers and thinnings with composition [2606.22734].

The `union` operation first canonicalizes both arguments by calling `find`, then compares both the roots and the resulting thinnings:
```python
def union(self, x : Id, y : Id) -> Id | None:
    thinx, xid = self.find(x)
    thiny, yid = self.find(y)
    if xid != yid or thinx != thiny:
        thinz = wct(thinx,thiny)
        (_, z) = self.makeset(cod(thinz))
        self.parents[xid] = (div(thinz,thinx), z)
        self.parents[yid] = (div(thinz,thiny), z)
        return (thinz, z)
    else:
        return None
```
If both identifiers are already identical as contextualized representatives, nothing changes. Otherwise, the operation computes the weakest common thinning `thinz`, creates a fresh root in the corresponding common scope, and attaches the previous roots to that fresh root using division thinnings [2606.22734].

This differs sharply from classical parent redirection. Union may create a fresh representative rather than orienting one existing root beneath another. The paper compares this to factor union-find, where a fresh variable can be introduced as the best solution. In thinning-aware union-find, this fresh representative captures the canonical common context of the merged class [2606.22734].

Three cases from the paper illustrate the behavior.

For **ordinary union with common lifting**, both sides carry identical thinnings, so the common lifting can be peeled and the operation reduces to ordinary union on the underlying e-class ids.

For **redundant variables and forced orientation**, as in $(x \mapsto x * 0) = (x \mapsto 0)$, one side has a redundant variable. The orientation is forced so that the thinner, dependency-free representation is preferred as parent. The union-find mechanism supplies the general context machinery, while the forced orientation is a policy choice intended to preserve coherent thinnings and contexts.

For **irreconcilable liftings**, as in $(x,y \mapsto x * 0) = (x,y \mapsto 0 * y)$, neither side is expressible as a thinning of the other. The operation therefore creates a fresh id representing the common context, then attaches both prior representatives through appropriate thinning maps. This is the case most characteristic of thinning-aware union-find proper [2606.22734].

## 4. Interaction with lifting and e-graph normalization

The union-find is not an isolated data structure; it is coupled to lift-pulling smart constructors. These constructors inspect the fat identifiers of the arguments, compute their common lifting, peel it off, intern the underlying e-node in minimized context, and then reattach the lifting to the resulting fat identifier. This implements the rewriting pattern
\[
f(\text{lift}_i(X), \text{lift}_i(Y)) \to \text{lift}_i(f(X,Y)).
\]
As a result, e-ids are stored in a maximally pulled form in which lifts occur high in the syntax tree and thinnings on fat ids are canonicalized as much as possible [2606.22734].

Within this representation, the lifting combinator is governed by two equational properties:
\[
\forall X,\ \text{lift}_i(\text{lift}_j(X)) = \text{lift}_{i \cdot j}(X)
\]
and
\[
\forall X,Y,\ f(\text{lift}_i(X), \text{lift}_i(Y)) = \text{lift}_i(f(X,Y)).
\]
These laws are “baked into” the system through smart constructors on one side and thinning operations such as `comp`, `wct`, and `div` on the other [2606.22734].

The paper’s examples show how this normalization works on concrete terms. For instance, the expression $x,y,z,w \mapsto x + z$ is represented in maximally pulled form as
\[
\text{lift}_{1010}\big(\text{lift}_{10}(\operatorname{var}) + \text{lift}_{01}(\operatorname{var})\big).
\]
Similarly, the equality $x * 0 = 0 * y$ in a two-variable context becomes a relation between `lift_{10}(e_92)` and `lift_{01}(e_13)`, whose merge yields a fresh 0-scope representative. In that form, the shared fact is that both sides denote a constant, not merely that they are syntactically reducible in separate contexts [2606.22734].

The visualization described in the paper “thicken[s] all edges into thinnings.” That makes variable usage explicit at every connection in the e-graph. A plausible implication is that the union-find is best understood not as a mere equivalence-maintenance layer, but as part of the contextual semantics of the e-graph itself.

## 5. Algebraic properties, correctness considerations, and complexity

The correctness discussion is informal rather than theorem-driven, but several structural properties are emphasized. The first is **injectivity**: liftings are injective, so from $\text{lift}_i(a) = \text{lift}_i(b)$ one can safely infer $a = b$. The union-find relies on that fact when it strips shared lifting and merges underlying classes [2606.22734].

The second is **type and scope safety**. Because thinning composition, weakest common thinning, and division all respect domain and codomain sizes, ill-typed merges are ruled out. The system “does not even type check to union two objects with different numbers of variables.” This makes scope consistency a data-structural invariant rather than a post hoc sanity check [2606.22734].

The third is the handling of **rigid $\alpha$-canonical variables**. By storing context-transforming thinnings on union-find edges, the representation encodes $\alpha$-equivalence structurally in a Co-de Bruijn-like form. Variable renaming is not treated as extrinsic normalization over names; it is built into the identity of e-classes and their relations [2606.22734].

The paper also stresses that thinnings behave categorically: they have identity and composition, and path composition in `find` is therefore well-founded. At the same time, thinnings do not form a group, because there are no inverses in general. This is central to one of the paper’s broader claims: thinning union-find “refutes” the misunderstanding that group axioms are necessary for union-find edge annotations. Group-labeled variants such as offset union-find rely on invertible labels, whereas thinning-aware union-find works with labels that form a category but not a group [2606.22734].

Asymptotically, the paper states that operations remain nearly the same as ordinary union-find. `find` is $O(\text{tree height})$ with composition of bitvector operations, and `union` is $O(1)$ for `makeset` plus bitvector operations and pointer updates. The overhead comes from `wct`, `div`, and `comp`, but thinnings are “sweetened by the fact that liftings/thinnings can be represented as compact bitvectors.” The implementation discussion points to `microeggpy` and a Rust module `thin.rs`, suggesting packed bitvector representations in practice [2606.22734].

## 6. Relation to other union-find generalizations and other senses of “thinning”

Relative to classical union-find, thinning-aware union-find preserves the parent-pointer forest organization and the `find`/`union` interface, while changing three essentials: each edge carries a thinning label, each element is represented as `(Thin, id)` rather than by an integer alone, and union may create a fresh root whose context is determined by a weakest common thinning rather than simply redirecting one tree beneath another [2606.22734].

The paper compares this design to offset union-find, Monus union-find, factor union-find, labeled union-find, and slotted e-graphs. Offset and related variants use labels with group or monoid structure; thinning-aware union-find instead uses partial injections represented as bitvectors. Slotted e-graphs provide the closest conceptual analogue: both attach context-transforming maps to e-graph structure, but thinning e-graphs specialize to totally ordered slots and sparse thinnings [2606.22734].

The phrase “thinning-aware” also appears in other research contexts, but with different meanings. In dynamic connectivity, one can speak of “thinning” updates by doing less work per insertion. Lower bounds show that if incremental connectivity supports edge insertions in worst-case time
\[
t_u = o\left(\frac{\log n}{\log\log n}\right),
\]
then worst-case query time must satisfy
\[
t_q \ge n^{1-o(1)}.
\]
That use of “thinning-aware” refers to reducing update work in a connectivity structure, not to annotating union-find edges with thinnings as contextual maps [1102.1783].

In union-find decoders for the surface code, “thinning-aware” refers again to a different design space: pruning or selectively thinning clusters while preserving the geometric and parity conditions needed for threshold proofs. The relevant framework requires local growth from active detectors, merging on geometric contact, and preservation of parity-based validity conditions [2602.20238]. Likewise, in cubical persistent homology, union-find is combined with pruning and lookup tables so that zero-persistence direct pairs and other locally classifiable cells can be removed before global processing; there, thinning means local topological simplification on regular grids rather than context-sensitive identity management [2606.04801].

Taken narrowly, however, thinning-aware union-find denotes the union-find developed for lifting e-graphs: a parent-labeled, context-sensitive equivalence structure over fat identifiers, designed to preserve rigid $\alpha$-equivalence and maximize sharing across variable contexts [2606.22734].

Source: https://www.emergentmind.com/topics/thinning-aware-union-find