Data Model Invariants

Why invariants?

They ensure a valid, simulation-ready Purkinje graph: geometry is on the surface, indices are valid, and the connectivity is a tree without self-loops.

Checklist

  • On-surface nodes: every node lies on the endocardial surface within tolerance.

  • Valid indices: all connectivity indices are in [0, len(nodes)-1].

  • No self-edges: no edge connects a node to itself.

  • No duplicate edges: undirected edges appear once (or consistently ordered).

  • Acyclic: the graph is a tree (|E| = |V| - C where C is number of components).

  • Terminals: all end nodes in end_nodes have degree = 1.

  • No isolated nodes: every node appears in at least one edge (except permissible root cases).

  • Finite geometry: node coordinates are finite (no NaN/Inf).

  • PMJs (if present): are a subset of graph nodes.

Minimal validator (example)

import math

def validate_tree(nodes, connectivity, end_nodes):
    n = len(nodes)
    # Indices
    for (u, v) in connectivity:
        assert 0 <= u < n and 0 <= v < n, "out-of-range index"
        assert u != v, "self-edge found"
    # Duplicates (undirected)
    seen = set()
    for (u, v) in connectivity:
        e = (u, v) if u < v else (v, u)
        assert e not in seen, "duplicate edge"
        seen.add(e)
    # Degrees
    deg = [0] * n
    for (u, v) in connectivity:
        deg[u] += 1; deg[v] += 1
    for k in end_nodes:
        assert deg[k] == 1, f"endpoint degree != 1 at {k}"
    # Components and acyclicity via union-find
    parent = list(range(n))
    def find(x):
        while x != parent[x]:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x
    def unite(a, b):
        ra, rb = find(a), find(b)
        if ra == rb:
            return False
        parent[rb] = ra; return True
    merges = 0
    for (u, v) in seen:
        merges += 1 if unite(u, v) else 0
    components = len({find(i) for i in range(n) if deg[i] > 0})
    assert len(seen) == sum(deg[i] > 0 for i in range(n)) - components, "not a forest"
    # Geometry finite
    for x in nodes:
        assert all(math.isfinite(float(c)) for c in x), "non-finite coordinate"

Operational tips

  • Validate after growth and before activation/export.

  • If you reindex or merge nodes, re-validate.

  • Keep tolerances centralized (distance thresholds, collision spacing).