"""Reduced dual: construction and verification. Test input is the icosahedron G (the unique 5-regular triangulation, n=12). Its dual G' is the dodecahedron (a cubic plane graph, 20 vertices). We pick a degree-5 vertex v of G -- equivalently a pentagonal face F_v of G' -- and apply the reduced-dual construction of Definition 2.1: 1. delete the 5 dual vertices on the boundary of F_v (and incident edges), leaving 5 degree-2 vertices on a new face F; 2. order those 5 vertices clockwise around F as A_0..A_4; 3. add a vertex v_n joined to A_i, A_{i+1}, A_{i+2}; 4. add an edge A_{i+3} A_{i+4}. We verify the result is again a cubic plane graph, and report the triangulation it is the dual of (its face count = the primal vertex count), to see how the vertex count changes relative to n. The dodecahedron is built directly in its concentric "Schlegel" layout with F_v the inner pentagon, so the figures (draw_reduced_dual_steps.py) are clean. """ import math import networkx as nx # --------------------------------------------------------------------------- # Build G' = dodecahedron with concentric positions; F_v = inner pentagon. # Vertex families a (inner pentagon), b, c, d (outer pentagon), 5 each. # Angles increase *clockwise* (90 - 72*i deg) so index order is clockwise. # --------------------------------------------------------------------------- def build_dual(): pos = {} R = {'a': 1.0, 'b': 2.2, 'c': 3.6, 'd': 4.8} for i in range(5): for fam in ('a', 'b'): th = math.radians(90 - 72 * i) pos[(fam, i)] = (R[fam] * math.cos(th), R[fam] * math.sin(th)) for fam in ('c', 'd'): th = math.radians(90 - 72 * i - 36) # offset half a step pos[(fam, i)] = (R[fam] * math.cos(th), R[fam] * math.sin(th)) Gp = nx.Graph() Gp.add_nodes_from(pos) for i in range(5): Gp.add_edge(('a', i), ('a', (i + 1) % 5)) # inner pentagon Gp.add_edge(('a', i), ('b', i)) # spokes a-b Gp.add_edge(('b', i), ('c', i)) # b-c Gp.add_edge(('b', i), ('c', (i - 1) % 5)) # b-c (other side) Gp.add_edge(('c', i), ('d', i)) # spokes c-d Gp.add_edge(('d', i), ('d', (i + 1) % 5)) # outer pentagon Fv_boundary = [('a', i) for i in range(5)] # inner pentagon return Gp, pos, Fv_boundary # --------------------------------------------------------------------------- # Face / dual helpers. # --------------------------------------------------------------------------- def faces_of(G): """Return the list of faces (each a list of vertices) of a plane graph.""" ok, emb = nx.check_planarity(G) assert ok, "graph is not planar" seen, faces = set(), [] for u in emb: for v in emb[u]: if (u, v) not in seen: faces.append(emb.traverse_face(u, v, mark_half_edges=seen)) return faces def dual_of(G): """Combinatorial dual (all faces, including outer) of a plane graph.""" faces = faces_of(G) edge_faces = {} for fi, face in enumerate(faces): for j in range(len(face)): e = frozenset((face[j], face[(j + 1) % len(face)])) edge_faces.setdefault(e, []).append(fi) D = nx.MultiGraph() D.add_nodes_from(range(len(faces))) for e, fs in edge_faces.items(): if len(fs) == 2: D.add_edge(fs[0], fs[1]) elif len(fs) == 1: # shouldn't happen for 2-connected G pass return D, faces # --------------------------------------------------------------------------- # The reduced-dual construction. # --------------------------------------------------------------------------- def clockwise_order(verts, pos): """Order verts clockwise around their centroid, starting from the topmost.""" cx = sum(pos[v][0] for v in verts) / len(verts) cy = sum(pos[v][1] for v in verts) / len(verts) ang = {v: math.atan2(pos[v][1] - cy, pos[v][0] - cx) for v in verts} ccw = sorted(verts, key=lambda v: ang[v]) # counterclockwise cw = list(reversed(ccw)) # clockwise start = max(range(len(cw)), key=lambda k: pos[cw[k]][1]) # topmost first return cw[start:] + cw[:start] def apply_reduction(Gp, pos, Fv_boundary, i=0): """Apply Definition 2.1 and return a dict capturing each stage.""" Ghat = Gp.copy() npos = dict(pos) # (1) delete the 5 boundary dual vertices of F_v Ghat.remove_nodes_from(Fv_boundary) deg2 = [v for v in Ghat if Ghat.degree(v) == 2] assert len(deg2) == 5, f"expected 5 degree-2 vertices, got {len(deg2)}" # (2) order them clockwise around the new face F A = clockwise_order(deg2, pos) # (3) new vertex v_n joined to A_i, A_{i+1}, A_{i+2} apex_nbrs = [A[(i + k) % 5] for k in range(3)] ax = sum(npos[v][0] for v in apex_nbrs) / 3 ay = sum(npos[v][1] for v in apex_nbrs) / 3 v_n = 'v_n' npos[v_n] = (ax * 0.55, ay * 0.55) # pull toward the 3 nbrs Ghat.add_node(v_n) for u in apex_nbrs: Ghat.add_edge(v_n, u) # (4) chord between the remaining two chord = (A[(i + 3) % 5], A[(i + 4) % 5]) Ghat.add_edge(*chord) return { 'Ghat': Ghat, 'pos': npos, 'A': A, 'v_n': v_n, 'apex_nbrs': apex_nbrs, 'chord': chord, 'deleted': list(Fv_boundary), } def main(): Gp, pos, Fv = build_dual() # --- verify G' is the dodecahedron = dual of the icosahedron --- assert nx.check_planarity(Gp)[0] assert all(d == 3 for _, d in Gp.degree()), "G' not cubic" assert nx.is_isomorphic(Gp, nx.dodecahedral_graph()), "G' is not dodecahedron" Dico, _ = dual_of(Gp) Dico = nx.Graph(Dico) print(f"G (icosahedron) : dual of G' has {Dico.number_of_nodes()} vertices, " f"degrees {sorted({d for _, d in Dico.degree()})}") print(f"G' (dodecahedron): {Gp.number_of_nodes()} vertices, " f"{Gp.number_of_edges()} edges, " f"{len(faces_of(Gp))} faces; cubic={all(d==3 for _,d in Gp.degree())}") # --- apply the reduced-dual construction --- res = apply_reduction(Gp, pos, Fv, i=0) Ghat = res['Ghat'] cubic = all(d == 3 for _, d in Ghat.degree()) planar = nx.check_planarity(Ghat)[0] ghat_simple = (nx.number_of_selfloops(Ghat) == 0) # Graph: no parallels nfaces = len(faces_of(Ghat)) print() print(f"reduced dual G^_v,i : {Ghat.number_of_nodes()} vertices, " f"{Ghat.number_of_edges()} edges, {nfaces} faces") print(f" cubic : {cubic}") print(f" planar : {planar}") print(f" simple : {ghat_simple}") # --- the triangulation it is dual to --- Dred_multi, _ = dual_of(Ghat) Dred = nx.Graph(Dred_multi) dred_simple = (Dred.number_of_edges() == Dred_multi.number_of_edges()) is_tri = all(len(f) == 3 for f in faces_of(Dred)) if planar else None print() print(f"dual of reduced dual : {Dred.number_of_nodes()} vertices " f"(= faces of G^), degree seq " f"{sorted((d for _, d in Dred.degree()), reverse=True)}") print(f" is a triangulation : {is_tri}") print(f" simple : {dred_simple}") n = Dico.number_of_nodes() print() print(f"VERTEX COUNT: G has n = {n}; reduced triangulation has " f"{Dred.number_of_nodes()} (change = " f"{Dred.number_of_nodes() - n}).") if __name__ == '__main__': main()