homology
triwarp.homology
Homology generators: the loops that make a surface's topology, found by tree-cotree.
On a closed genus-g surface there are 2 * g independent loops that cannot be contracted to a
point — two per handle, going around it and through it. They are what a cutting tool needs (cut
along them and the surface opens into a disk), and what tells a parametrizer where its seams have to
go.
The construction is Erickson & Whittlesey's tree-cotree decomposition, and it is a counting
argument as much as an algorithm: take a spanning tree of the vertex graph, then a spanning tree of
the dual graph that avoids the primal tree's edges, and every remaining edge closes exactly one
independent loop. Euler's formula fixes how many are left: E - (V - 1) - (F - 1) = 2 - chi,
which is 2 * g.
Unlike the rest of this package there is no reference implementation to compare against —
potpourri3d does not expose geometry-central's homology code — so the tests check structural
invariants instead: the loop count against
euler_characteristic, and that every loop is a closed
walk along real mesh edges that visits no vertex twice.
homology_generators
homology_generators(
vertices: array[vec3],
faces: array[int32],
*,
copy: bool = False,
) -> list[wp.array[wp.int32]]
Non-contractible loops of the surface, as ordered vertex-index cycles.
Returns 2 * g loops for a closed genus-g surface and none for a sphere. Each loop is a
closed walk along mesh edges: consecutive entries share an edge, and so do the last and first
(the start vertex is not repeated, matching
boundary_loops).
The loops are a basis, not canonical: any generating set is as valid as any other, and this
one falls out of the spanning trees the construction happens to build. They are also as long
and as jagged as those trees, which is what
shorten_loop is for.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
copy
|
bool
|
Return independent buffers instead of views into one packed allocation. |
False
|
Returns:
| Type | Description |
|---|---|
list[array[int32]]
|
One |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the mesh has a boundary, since a surface with boundary has a different homology basis (every boundary loop contributes one, and the tree-cotree count no longer applies), or if its referenced vertices are not all in one connected component, since the count is then a sum over components that a single pair of spanning trees does not produce. |
RuntimeError
|
If |
See Also
shorten_loop
Shortens these loops within their homotopy class, keeping them on mesh edges.
tree_cotree
euler_characteristic
boundary_loops
Source code in triwarp/homology.py
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 | |
tree_cotree
tree_cotree(
vertices: array[vec3], faces: array[int32]
) -> tuple[
twt.Array2dInt32, twt.Array2dInt32, wp.array[wp.int32]
]
Tree-cotree decomposition: the edges a primal and a dual spanning tree both leave alone.
Three pieces of structure, and the building block behind
homology_generators:
- a breadth-first spanning tree of the vertex graph, whose
parentsthe loop tracing walks, - a spanning forest of the dual (face-adjacency) graph, restricted to dual edges whose primal edge is not already in the vertex tree,
- whatever edges belong to neither — exactly
2 * gof them on a closed genus-gsurface.
The dual side is a forest rather than a traversal because only its edge set is read, never its
shape. That matters for cost as well as tidiness: the dual graph restricted to non-primal-tree
edges is already nearly a tree, so a breadth-first traversal of it would run its diameter, which
can be very large, and graph.bfs costs one pass of kernels per level whatever the node
count. A Boruvka forest instead needs only O(log n_faces) rounds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
vertices
|
array[vec3]
|
|
required |
faces
|
array[int32]
|
Length- |
required |
Returns:
| Name | Type | Description |
|---|---|---|
unique_edges |
Array2dInt32
|
|
generator_edges |
Array2dInt32
|
|
parents |
array[int32]
|
Length |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the mesh has a boundary, or is not connected (see
|
RuntimeError
|
If |
See Also
Source code in triwarp/homology.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 | |