Distill a finite tiled half-edge graph back into a minimal :data:TilesetSpec.
The algorithm iteratively refines two equivalence classifiers:
- Faces are first classified by congruency (same shape).
- Half-edges are then classified by their local context: own class, reverse
class, neighbors, and the classes of their two incident faces.
- Faces are re-classified by the cyclic sequence of their edges' classes.
Steps 2-3 repeat until the number of distinct classes stops growing. The
fixpoint yields, for each tile in the spec, an exemplar face plus a labelling
of its outgoing edges.
spec_from_graph
spec_from_graph(G: EuclideanPositionHEG) -> TilesetSpec
Analyse a finite tiled Euclidean graph and produce a :data:TilesetSpec.
Source code in pleat/gjh/distill.py
| def spec_from_graph(G: EuclideanPositionHEG) -> TilesetSpec:
"""Analyse a finite tiled Euclidean graph and produce a :data:`TilesetSpec`."""
cc = congruency_classifier()
fs = list(G.faces)
face_dicts: list[dict[Face, int]] = [{f: cc.classify(f) for f in fs}]
hs = list(G.halfedges)
lengths = np.array([np.linalg.norm(h.orig["pos"] - h.dest["pos"]) for h in hs])
length_groups = fast_group_closeby(lengths[:, None], eps=1e-6)
edge_dicts: list[dict[HalfEdge, int]] = [{h: int(lg) for h, lg in zip(hs, length_groups)}]
before = 0
for _ in count():
hc = _EdgeClassifier(face_dicts[-1], edge_dicts[-1])
edge_dicts.append({h: hc.classify(h) for h in hs})
fc = _FaceClassifier(edge_dicts[-1])
face_dicts.append({f: fc.classify(f) for f in fs})
after = len(set(edge_dicts[-1].values())) + len(set(face_dicts[-1].values()))
if before >= after:
break
before = after
# Pick one exemplar face per class (skipping classes that touched the open border).
exemplar_fs: list[Face] = []
for key in set(face_dicts[-2].values()):
if key < 0:
continue
exemplar_fs.append(next(f for f in fs if face_dicts[-2][f] == key and face_dicts[-1][f] >= 0))
exemplar_fs.sort(key=lambda f: -f.order())
tile_names = {f: chr(97 + i) for i, f in enumerate(exemplar_fs)}
edge_names: dict[int, tuple[str, int]] = {}
for f in exemplar_fs:
for i, h in enumerate(f.halfedge_iter()):
edge_class = edge_dicts[-2][h]
if edge_class not in edge_names:
edge_names[edge_class] = (tile_names[f], i)
return {tile_names[f]: [edge_names[edge_dicts[-2][h.rev]] for h in f.halfedge_iter()] for f in exemplar_fs}
|