FAILURE MAP
← Case archive

FA-88401 / Mesh topology invariants / Open access

Pinched boundary vertex silently merges or splits loops · case 01

A bowtie vertex yields a loop answer instead of "ambiguous".

Verified by executionVariant 1 · 7 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

A second outgoing boundary half-edge overwrites the first successor.

VERIFIED REPAIR

Return "ambiguous" when a vertex starts two boundary half-edges.

Unsuccessful approach: Skipping the second half-edge keeps an arbitrary loop and hides the pinch.

Case contract

Input: consistently oriented polygon faces. A half-edge (a,b) is boundary when (b,a) is not a half-edge. If any vertex starts two boundary half-edges return "ambiguous". Otherwise walk loops along the half-edge direction, each starting at its smallest vertex, and return them sorted by descending length then first vertex.

Why this case matters

Mesh processing pipelines (remeshing, export, simulation, printing) trust these topological counts and adjacency answers; a wrong invariant silently accepts broken meshes or rejects valid ones.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    faces=x
    he=[]
    for f in faces:
        n=len(f)
        for i in range(n):
            he.append((f[i],f[(i+1)%n]))
    hs=set(he)
    nxt={}
    for a,b in he:
        if (b,a) not in hs:
            nxt[a]=b
    loops=[]
    seen=set()
    for s in sorted(nxt):
        if s in seen: continue
        loop=[]
        v=s
        while v not in seen:
            seen.add(v)
            loop.append(v)
            v=nxt[v]
        loops.append(loop)
    return sorted(loops,key=lambda l:(-len(l),l[0]))
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['disk', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1]]], [[1, 2, 3, 4, 5]]], ['fan', [[[0, 1, 2], [0, 2, 3], [0, 3, 4]]], [[0, 1, 2, 3, 4]]], ['annulus', [[[0, 3, 4, 1], [1, 4, 5, 2], [2, 5, 3, 0]]], [[0, 2, 1], [3, 4, 5]]], ['quad grid', [[[0, 1, 4, 3], [1, 2, 5, 4], [3, 4, 7, 6], [4, 5, 8, 7]]], [[0, 1, 2, 5, 8, 7, 6, 3]]], ['square with hole', [[[0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['closed tetrahedron', [[[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]]], []], ['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous']], [['quad grid', [[[0, 1, 4, 3], [1, 2, 5, 4], [3, 4, 7, 6], [4, 5, 8, 7]]], [[0, 1, 2, 5, 8, 7, 6, 3]]], ['square with hole', [[[0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['closed tetrahedron', [[[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]]], []], ['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous'], ['two disks', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1], [10, 11, 12], [10, 12, 13], [10, 13, 14]]], [[1, 2, 3, 4, 5], [10, 11, 12, 13, 14]]], ['open cylinder', [[[0, 1, 5], [0, 5, 4], [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['single triangle', [[[4, 2, 7]]], [[2, 7, 4]]]], [['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous'], ['two disks', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1], [10, 11, 12], [10, 12, 13], [10, 13, 14]]], [[1, 2, 3, 4, 5], [10, 11, 12, 13, 14]]], ['open cylinder', [[[0, 1, 5], [0, 5, 4], [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['single triangle', [[[4, 2, 7]]], [[2, 7, 4]]], ['single quad', [[[3, 1, 0, 2]]], [[0, 2, 3, 1]]], ['triangle grid', [[[0, 1, 4], [0, 4, 3], [1, 2, 5], [1, 5, 4]]], [[0, 1, 2, 5, 4, 3]]], ['disk with small far island', [[[5, 6, 7], [5, 7, 8], [5, 8, 9], [5, 9, 10], [5, 10, 6], [0, 1, 2]]], [[6, 7, 8, 9, 10], [0, 1, 2]]]], [['disk', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1]]], [[1, 2, 3, 4, 5]]], ['fan', [[[0, 1, 2], [0, 2, 3], [0, 3, 4]]], [[0, 1, 2, 3, 4]]], ['annulus', [[[0, 3, 4, 1], [1, 4, 5, 2], [2, 5, 3, 0]]], [[0, 2, 1], [3, 4, 5]]], ['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous'], ['single quad', [[[3, 1, 0, 2]]], [[0, 2, 3, 1]]], ['triangle grid', [[[0, 1, 4], [0, 4, 3], [1, 2, 5], [1, 5, 4]]], [[0, 1, 2, 5, 4, 3]]], ['disk with small far island', [[[5, 6, 7], [5, 7, 8], [5, 8, 9], [5, 9, 10], [5, 10, 6], [0, 1, 2]]], [[6, 7, 8, 9, 10], [0, 1, 2]]]], [['disk', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1]]], [[1, 2, 3, 4, 5]]], ['fan', [[[0, 1, 2], [0, 2, 3], [0, 3, 4]]], [[0, 1, 2, 3, 4]]], ['annulus', [[[0, 3, 4, 1], [1, 4, 5, 2], [2, 5, 3, 0]]], [[0, 2, 1], [3, 4, 5]]], ['quad grid', [[[0, 1, 4, 3], [1, 2, 5, 4], [3, 4, 7, 6], [4, 5, 8, 7]]], [[0, 1, 2, 5, 8, 7, 6, 3]]], ['square with hole', [[[0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['closed tetrahedron', [[[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]]], []], ['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous']]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
disk[[1, 2, 3, 4, 5]][[1, 2, 3, 4, 5]]Passed
fan[[0, 1, 2, 3, 4]][[0, 1, 2, 3, 4]]Passed
annulus[[0, 2, 1], [3, 4, 5]][[0, 2, 1], [3, 4, 5]]Passed
quad grid[[0, 1, 2, 5, 8, 7, 6, 3]][[0, 1, 2, 5, 8, 7, 6, 3]]Passed
square with hole[[0, 1, 2, 3], [4, 7, 6, 5]][[0, 1, 2, 3], [4, 7, 6, 5]]Passed
closed tetrahedron[][]Passed
bowtie[[0, 3, 4], [1, 2]]ambiguousFailed

SHA-256 / d2ff5eaf8d2c93bd7441ae17023f44c5e83a0c7f03cc442ca060127edda63720

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    faces=x
    he=[]
    for f in faces:
        n=len(f)
        for i in range(n):
            he.append((f[i],f[(i+1)%n]))
    hs=set(he)
    nxt={}
    for a,b in he:
        if (b,a) not in hs:
            if a in nxt: continue
            nxt[a]=b
    loops=[]
    seen=set()
    for s in sorted(nxt):
        if s in seen: continue
        loop=[]
        v=s
        while v not in seen:
            seen.add(v)
            loop.append(v)
            v=nxt[v]
        loops.append(loop)
    return sorted(loops,key=lambda l:(-len(l),l[0]))
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['disk', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1]]], [[1, 2, 3, 4, 5]]], ['fan', [[[0, 1, 2], [0, 2, 3], [0, 3, 4]]], [[0, 1, 2, 3, 4]]], ['annulus', [[[0, 3, 4, 1], [1, 4, 5, 2], [2, 5, 3, 0]]], [[0, 2, 1], [3, 4, 5]]], ['quad grid', [[[0, 1, 4, 3], [1, 2, 5, 4], [3, 4, 7, 6], [4, 5, 8, 7]]], [[0, 1, 2, 5, 8, 7, 6, 3]]], ['square with hole', [[[0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['closed tetrahedron', [[[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]]], []], ['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous']], [['quad grid', [[[0, 1, 4, 3], [1, 2, 5, 4], [3, 4, 7, 6], [4, 5, 8, 7]]], [[0, 1, 2, 5, 8, 7, 6, 3]]], ['square with hole', [[[0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['closed tetrahedron', [[[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]]], []], ['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous'], ['two disks', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1], [10, 11, 12], [10, 12, 13], [10, 13, 14]]], [[1, 2, 3, 4, 5], [10, 11, 12, 13, 14]]], ['open cylinder', [[[0, 1, 5], [0, 5, 4], [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['single triangle', [[[4, 2, 7]]], [[2, 7, 4]]]], [['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous'], ['two disks', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1], [10, 11, 12], [10, 12, 13], [10, 13, 14]]], [[1, 2, 3, 4, 5], [10, 11, 12, 13, 14]]], ['open cylinder', [[[0, 1, 5], [0, 5, 4], [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['single triangle', [[[4, 2, 7]]], [[2, 7, 4]]], ['single quad', [[[3, 1, 0, 2]]], [[0, 2, 3, 1]]], ['triangle grid', [[[0, 1, 4], [0, 4, 3], [1, 2, 5], [1, 5, 4]]], [[0, 1, 2, 5, 4, 3]]], ['disk with small far island', [[[5, 6, 7], [5, 7, 8], [5, 8, 9], [5, 9, 10], [5, 10, 6], [0, 1, 2]]], [[6, 7, 8, 9, 10], [0, 1, 2]]]], [['disk', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1]]], [[1, 2, 3, 4, 5]]], ['fan', [[[0, 1, 2], [0, 2, 3], [0, 3, 4]]], [[0, 1, 2, 3, 4]]], ['annulus', [[[0, 3, 4, 1], [1, 4, 5, 2], [2, 5, 3, 0]]], [[0, 2, 1], [3, 4, 5]]], ['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous'], ['single quad', [[[3, 1, 0, 2]]], [[0, 2, 3, 1]]], ['triangle grid', [[[0, 1, 4], [0, 4, 3], [1, 2, 5], [1, 5, 4]]], [[0, 1, 2, 5, 4, 3]]], ['disk with small far island', [[[5, 6, 7], [5, 7, 8], [5, 8, 9], [5, 9, 10], [5, 10, 6], [0, 1, 2]]], [[6, 7, 8, 9, 10], [0, 1, 2]]]], [['disk', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1]]], [[1, 2, 3, 4, 5]]], ['fan', [[[0, 1, 2], [0, 2, 3], [0, 3, 4]]], [[0, 1, 2, 3, 4]]], ['annulus', [[[0, 3, 4, 1], [1, 4, 5, 2], [2, 5, 3, 0]]], [[0, 2, 1], [3, 4, 5]]], ['quad grid', [[[0, 1, 4, 3], [1, 2, 5, 4], [3, 4, 7, 6], [4, 5, 8, 7]]], [[0, 1, 2, 5, 8, 7, 6, 3]]], ['square with hole', [[[0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['closed tetrahedron', [[[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]]], []], ['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous']]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
disk[[1, 2, 3, 4, 5]][[1, 2, 3, 4, 5]]Passed
fan[[0, 1, 2, 3, 4]][[0, 1, 2, 3, 4]]Passed
annulus[[0, 2, 1], [3, 4, 5]][[0, 2, 1], [3, 4, 5]]Passed
quad grid[[0, 1, 2, 5, 8, 7, 6, 3]][[0, 1, 2, 5, 8, 7, 6, 3]]Passed
square with hole[[0, 1, 2, 3], [4, 7, 6, 5]][[0, 1, 2, 3], [4, 7, 6, 5]]Passed
closed tetrahedron[][]Passed
bowtie[[0, 1, 2], [3, 4]]ambiguousFailed

SHA-256 / b81b33aefd4bad51b849f70d85e3ca8d2730e9f72fc0a6e04162b1039e603238

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json
import math
N = 1
observations = []
def solve(x):
    faces=x
    he=[]
    for f in faces:
        n=len(f)
        for i in range(n):
            he.append((f[i],f[(i+1)%n]))
    hs=set(he)
    nxt={}
    for a,b in he:
        if (b,a) not in hs:
            if a in nxt: return 'ambiguous'
            nxt[a]=b
    loops=[]
    seen=set()
    for s in sorted(nxt):
        if s in seen: continue
        loop=[]
        v=s
        while v not in seen:
            seen.add(v)
            loop.append(v)
            v=nxt[v]
        loops.append(loop)
    return sorted(loops,key=lambda l:(-len(l),l[0]))
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
fixtures = [[['disk', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1]]], [[1, 2, 3, 4, 5]]], ['fan', [[[0, 1, 2], [0, 2, 3], [0, 3, 4]]], [[0, 1, 2, 3, 4]]], ['annulus', [[[0, 3, 4, 1], [1, 4, 5, 2], [2, 5, 3, 0]]], [[0, 2, 1], [3, 4, 5]]], ['quad grid', [[[0, 1, 4, 3], [1, 2, 5, 4], [3, 4, 7, 6], [4, 5, 8, 7]]], [[0, 1, 2, 5, 8, 7, 6, 3]]], ['square with hole', [[[0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['closed tetrahedron', [[[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]]], []], ['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous']], [['quad grid', [[[0, 1, 4, 3], [1, 2, 5, 4], [3, 4, 7, 6], [4, 5, 8, 7]]], [[0, 1, 2, 5, 8, 7, 6, 3]]], ['square with hole', [[[0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['closed tetrahedron', [[[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]]], []], ['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous'], ['two disks', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1], [10, 11, 12], [10, 12, 13], [10, 13, 14]]], [[1, 2, 3, 4, 5], [10, 11, 12, 13, 14]]], ['open cylinder', [[[0, 1, 5], [0, 5, 4], [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['single triangle', [[[4, 2, 7]]], [[2, 7, 4]]]], [['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous'], ['two disks', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1], [10, 11, 12], [10, 12, 13], [10, 13, 14]]], [[1, 2, 3, 4, 5], [10, 11, 12, 13, 14]]], ['open cylinder', [[[0, 1, 5], [0, 5, 4], [1, 2, 6], [1, 6, 5], [2, 3, 7], [2, 7, 6], [3, 0, 4], [3, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['single triangle', [[[4, 2, 7]]], [[2, 7, 4]]], ['single quad', [[[3, 1, 0, 2]]], [[0, 2, 3, 1]]], ['triangle grid', [[[0, 1, 4], [0, 4, 3], [1, 2, 5], [1, 5, 4]]], [[0, 1, 2, 5, 4, 3]]], ['disk with small far island', [[[5, 6, 7], [5, 7, 8], [5, 8, 9], [5, 9, 10], [5, 10, 6], [0, 1, 2]]], [[6, 7, 8, 9, 10], [0, 1, 2]]]], [['disk', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1]]], [[1, 2, 3, 4, 5]]], ['fan', [[[0, 1, 2], [0, 2, 3], [0, 3, 4]]], [[0, 1, 2, 3, 4]]], ['annulus', [[[0, 3, 4, 1], [1, 4, 5, 2], [2, 5, 3, 0]]], [[0, 2, 1], [3, 4, 5]]], ['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous'], ['single quad', [[[3, 1, 0, 2]]], [[0, 2, 3, 1]]], ['triangle grid', [[[0, 1, 4], [0, 4, 3], [1, 2, 5], [1, 5, 4]]], [[0, 1, 2, 5, 4, 3]]], ['disk with small far island', [[[5, 6, 7], [5, 7, 8], [5, 8, 9], [5, 9, 10], [5, 10, 6], [0, 1, 2]]], [[6, 7, 8, 9, 10], [0, 1, 2]]]], [['disk', [[[0, 1, 2], [0, 2, 3], [0, 3, 4], [0, 4, 5], [0, 5, 1]]], [[1, 2, 3, 4, 5]]], ['fan', [[[0, 1, 2], [0, 2, 3], [0, 3, 4]]], [[0, 1, 2, 3, 4]]], ['annulus', [[[0, 3, 4, 1], [1, 4, 5, 2], [2, 5, 3, 0]]], [[0, 2, 1], [3, 4, 5]]], ['quad grid', [[[0, 1, 4, 3], [1, 2, 5, 4], [3, 4, 7, 6], [4, 5, 8, 7]]], [[0, 1, 2, 5, 8, 7, 6, 3]]], ['square with hole', [[[0, 1, 5, 4], [1, 2, 6, 5], [2, 3, 7, 6], [3, 0, 4, 7]]], [[0, 1, 2, 3], [4, 7, 6, 5]]], ['closed tetrahedron', [[[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]]], []], ['bowtie', [[[0, 1, 2], [0, 3, 4]]], 'ambiguous']]]
for label, args, expected in fixtures[N-1]:
    check(label, solve(*args), expected)
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
disk[[1, 2, 3, 4, 5]][[1, 2, 3, 4, 5]]Passed
fan[[0, 1, 2, 3, 4]][[0, 1, 2, 3, 4]]Passed
annulus[[0, 2, 1], [3, 4, 5]][[0, 2, 1], [3, 4, 5]]Passed
quad grid[[0, 1, 2, 5, 8, 7, 6, 3]][[0, 1, 2, 5, 8, 7, 6, 3]]Passed
square with hole[[0, 1, 2, 3], [4, 7, 6, 5]][[0, 1, 2, 3], [4, 7, 6, 5]]Passed
closed tetrahedron[][]Passed
bowtieambiguousambiguousPassed

SHA-256 / e38aa4785122719bd8655fd518bc66b1804c5de130ce1ce980505df66094be4d

Verification & scope

Pure combinatorial teaching model over integer face lists with a stipulated contract; no geometry kernel or file format is implied. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:51:07.774838+00:00.

Case digest / b9c6f15528233d113762f960350588b681c426dcfc98de1106f3d5291d385007