Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Extract combinatorics and optimal angles from maximal volume configuration. | |
| """ | |
| import sys | |
| from pathlib import Path | |
| sys.path.insert(0, str(Path(__file__).parent)) | |
| import numpy as np | |
| import json | |
| from scipy.spatial import Delaunay | |
| from ideal_poly_volume_toolkit.rivin_delaunay import check_delaunay_realizability, build_edge_adjacency | |
| def extract_combinatorics_and_angles(json_file): | |
| """Extract triangulation and optimal angles from optimization result.""" | |
| # Load configuration | |
| with open(json_file, 'r') as f: | |
| data = json.load(f) | |
| # Get vertex positions | |
| real = np.array(data['best']['vertices_real']) | |
| imag = np.array(data['best']['vertices_imag']) | |
| points = np.column_stack([real, imag]) | |
| n_vertices = len(points) | |
| volume = data['best']['volume'] | |
| print(f"βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ") | |
| print(f"MAXIMAL VOLUME IDEAL POLYHEDRON - COMPLETE ANALYSIS") | |
| print(f"βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ") | |
| print(f"\nVolume: {volume:.12f}") | |
| print(f"Vertices: {n_vertices}") | |
| # Compute Delaunay triangulation | |
| tri = Delaunay(points) | |
| triangulation = [tuple(sorted(simplex)) for simplex in tri.simplices] | |
| triangulation = sorted(set(triangulation)) # Remove duplicates, sort | |
| print(f"Triangles: {len(triangulation)}") | |
| # Display triangulation | |
| print(f"\n{'β'*63}") | |
| print(f"TRIANGULATION (Combinatorial Structure)") | |
| print(f"{'β'*63}") | |
| print("\nTriangles (vertex indices):") | |
| for i, tri in enumerate(triangulation, 1): | |
| print(f" {i:2d}. {tri}") | |
| # Get optimal angles from Rivin LP | |
| print(f"\n{'β'*63}") | |
| print(f"OPTIMAL ANGLES FROM RIVIN LP") | |
| print(f"{'β'*63}") | |
| result = check_delaunay_realizability(triangulation, verbose=False, strict=False) | |
| if not result['realizable']: | |
| print("ERROR: Configuration not realizable!") | |
| return | |
| # Extract angles (convert from scaled units to radians) | |
| angles_scaled = result['angles'] | |
| angles_radians = angles_scaled * np.pi | |
| n_triangles = len(triangulation) | |
| angles_array = angles_radians.reshape((n_triangles, 3)) | |
| print("\nFace angles (interior angles of each triangle):") | |
| print(f"{'Tri#':>5} {'Vertices':>20} {'Vtx':>5} {'Angle (rad)':>14} {'Angle (deg)':>12} {'Angle/Ο':>10}") | |
| print(f"{'-'*70}") | |
| for i, tri in enumerate(triangulation): | |
| for j, vertex in enumerate(tri): | |
| angle_rad = angles_array[i, j] | |
| angle_deg = np.degrees(angle_rad) | |
| angle_normalized = angle_rad / np.pi | |
| tri_str = f"({tri[0]},{tri[1]},{tri[2]})" | |
| print(f" {i+1:3d}. {tri_str:>18s} v{vertex:2d} {angle_rad:13.10f} {angle_deg:11.6f}Β° {angle_normalized:9.6f}Ο") | |
| # Compute ALL dihedral angles | |
| print(f"\n{'='*78}") | |
| print(f"ALL DIHEDRAL ANGLES (Complete Ideal Polyhedron)") | |
| print(f"{'='*78}") | |
| from scipy.spatial import ConvexHull | |
| from fractions import Fraction | |
| # Find boundary (convex hull) | |
| hull = ConvexHull(points) | |
| boundary_edges = set() | |
| boundary_vertices = set() | |
| for simplex in hull.simplices: | |
| edge = tuple(sorted([simplex[0], simplex[1]])) | |
| boundary_edges.add(edge) | |
| boundary_vertices.add(simplex[0]) | |
| boundary_vertices.add(simplex[1]) | |
| edge_adjacency = build_edge_adjacency(triangulation) | |
| interior_edges = [e for e, ops in edge_adjacency.items() if len(ops) == 2] | |
| print(f"\nTopology:") | |
| print(f" Total vertices: {n_vertices}") | |
| print(f" Finite triangles: {len(triangulation)}") | |
| print(f" Boundary vertices: {len(boundary_vertices)}") | |
| print(f" Interior edges: {len(interior_edges)}") | |
| print(f" Boundary edges: {len(boundary_edges)}") | |
| print(f" Total edges: {len(interior_edges)} + {len(boundary_edges)} + {len(boundary_vertices)} = {len(interior_edges) + len(boundary_edges) + len(boundary_vertices)}") | |
| print(f" Expected (3V-6): {3*n_vertices - 6}") | |
| # TYPE 1: Interior edge dihedrals | |
| print(f"\n{'β'*78}") | |
| print(f"TYPE 1: Interior Edge Dihedrals ({len(interior_edges)} edges)") | |
| print(f"(Edges shared by two finite triangles)") | |
| print(f"{'β'*78}") | |
| all_dihedrals = [] | |
| for edge, opposite_corners in sorted(edge_adjacency.items()): | |
| if len(opposite_corners) == 2: | |
| angle1 = angles_array[opposite_corners[0][0], opposite_corners[0][1]] | |
| angle2 = angles_array[opposite_corners[1][0], opposite_corners[1][1]] | |
| dihedral = angle1 + angle2 | |
| normalized = dihedral / np.pi | |
| frac = Fraction(normalized).limit_denominator(20) | |
| rational = f"{frac.numerator}Ο/{frac.denominator}" if frac.denominator > 1 else f"{frac.numerator}Ο" | |
| all_dihedrals.append({ | |
| 'type': 'interior', | |
| 'edge': edge, | |
| 'dihedral_rad': dihedral, | |
| 'dihedral_deg': np.degrees(dihedral), | |
| 'normalized': normalized, | |
| 'rational': rational, | |
| 'p': frac.numerator, | |
| 'q': frac.denominator, | |
| }) | |
| # Print first 10 | |
| for d in all_dihedrals[:10]: | |
| print(f" {str(d['edge']):>12} {d['dihedral_deg']:7.3f}Β° = {d['normalized']:8.6f}Ο β {d['rational']:>8}") | |
| if len(all_dihedrals) > 10: | |
| print(f" ... ({len(all_dihedrals) - 10} more)") | |
| # TYPE 2: Boundary edge dihedrals | |
| print(f"\n{'β'*78}") | |
| print(f"TYPE 2: Boundary Edge Dihedrals ({len(boundary_edges)} edges)") | |
| print(f"(Angle opposite each boundary edge in the triangle containing it)") | |
| print(f"{'β'*78}") | |
| boundary_dihedrals = [] | |
| for edge in sorted(boundary_edges): | |
| # Find the triangle containing this edge | |
| v1, v2 = edge | |
| for i, tri in enumerate(triangulation): | |
| if v1 in tri and v2 in tri: | |
| # Find the third vertex (opposite to the edge) | |
| v3 = [v for v in tri if v != v1 and v != v2][0] | |
| v3_idx = tri.index(v3) | |
| dihedral = angles_array[i, v3_idx] | |
| normalized = dihedral / np.pi | |
| frac = Fraction(normalized).limit_denominator(20) | |
| rational = f"{frac.numerator}Ο/{frac.denominator}" if frac.denominator > 1 else f"{frac.numerator}Ο" | |
| boundary_dihedrals.append({ | |
| 'type': 'boundary', | |
| 'edge': edge, | |
| 'dihedral_rad': dihedral, | |
| 'dihedral_deg': np.degrees(dihedral), | |
| 'normalized': normalized, | |
| 'rational': rational, | |
| 'p': frac.numerator, | |
| 'q': frac.denominator, | |
| }) | |
| print(f" {str(edge):>12} {np.degrees(dihedral):7.3f}Β° = {normalized:8.6f}Ο β {rational:>8}") | |
| break | |
| all_dihedrals.extend(boundary_dihedrals) | |
| # TYPE 3: Vertex-to-β dihedrals | |
| print(f"\n{'β'*78}") | |
| print(f"TYPE 3: Vertex-to-β Dihedrals ({len(boundary_vertices)} edges)") | |
| print(f"(Sum of all angles at each boundary vertex)") | |
| print(f"{'β'*78}") | |
| vertex_dihedrals = [] | |
| for v in sorted(boundary_vertices): | |
| # Sum all angles at this vertex | |
| total_angle = 0 | |
| for i, tri in enumerate(triangulation): | |
| if v in tri: | |
| v_idx = tri.index(v) | |
| total_angle += angles_array[i, v_idx] | |
| normalized = total_angle / np.pi | |
| frac = Fraction(normalized).limit_denominator(20) | |
| rational = f"{frac.numerator}Ο/{frac.denominator}" if frac.denominator > 1 else f"{frac.numerator}Ο" | |
| vertex_dihedrals.append({ | |
| 'type': 'to_infinity', | |
| 'edge': (v, 'inf'), | |
| 'dihedral_rad': total_angle, | |
| 'dihedral_deg': np.degrees(total_angle), | |
| 'normalized': normalized, | |
| 'rational': rational, | |
| 'p': frac.numerator, | |
| 'q': frac.denominator, | |
| }) | |
| print(f" v{v}ββ {np.degrees(total_angle):11.3f}Β° = {normalized:8.6f}Ο β {rational:>8} {'< Ο β' if total_angle < np.pi else 'β₯ Ο β'}") | |
| all_dihedrals.extend(vertex_dihedrals) | |
| dihedrals = all_dihedrals # For later use | |
| # Summary of rational patterns | |
| print(f"\n{'β'*63}") | |
| print(f"RATIONAL ANGLE SUMMARY") | |
| print(f"{'β'*63}") | |
| from collections import Counter | |
| pattern_counts = Counter(d['rational'] for d in dihedrals) | |
| print(f"\n{'Pattern':>10} {'Count':>8} {'Degrees':>12}") | |
| print(f"{'-'*32}") | |
| for pattern, count in pattern_counts.most_common(): | |
| # Get representative angle | |
| angle_deg = next(d['dihedral_deg'] for d in dihedrals if d['rational'] == pattern) | |
| print(f" {pattern:>8} {count:7d} {angle_deg:11.3f}Β°") | |
| # Determine the dominant denominator | |
| denominator_counts = Counter(d['q'] for d in dihedrals) | |
| dominant_q = denominator_counts.most_common(1)[0][0] | |
| print(f"\n{'β'*63}") | |
| if len(denominator_counts) == 1 and dominant_q > 1: | |
| print(f"VERIFICATION: All angles are exact multiples of Ο/{dominant_q}!") | |
| else: | |
| print(f"VERIFICATION: All angles are exact rational multiples of Ο!") | |
| print(f"{'β'*63}") | |
| # Export to JSON | |
| output_data = { | |
| 'metadata': { | |
| 'source_file': str(json_file), | |
| 'volume': float(volume), | |
| 'n_vertices': int(n_vertices), | |
| 'n_triangles': len(triangulation), | |
| }, | |
| 'combinatorics': { | |
| 'triangles': [[int(v) for v in tri] for tri in triangulation], | |
| }, | |
| 'optimal_angles': { | |
| 'face_angles_radians': angles_array.tolist(), | |
| 'face_angles_degrees': np.degrees(angles_array).tolist(), | |
| 'dihedral_angles': [ | |
| { | |
| 'type': d['type'], | |
| 'edge': [int(d['edge'][0]), str(d['edge'][1])], # Handle 'inf' for vertex-to-β | |
| 'angle_radians': float(d['dihedral_rad']), | |
| 'angle_degrees': float(d['dihedral_deg']), | |
| 'rational_form': d['rational'], | |
| 'p': int(d['p']), | |
| 'q': int(d['q']), | |
| } | |
| for d in dihedrals | |
| ], | |
| }, | |
| 'vertex_positions': { | |
| 'real': real.tolist(), | |
| 'imag': imag.tolist(), | |
| }, | |
| } | |
| # Generate output filename based on input file | |
| input_name = Path(json_file).stem | |
| output_file = Path(f'results/data/{input_name}_combinatorics.json') | |
| output_file.parent.mkdir(parents=True, exist_ok=True) | |
| with open(output_file, 'w') as f: | |
| json.dump(output_data, f, indent=2) | |
| print(f"\nβ Exported to: {output_file}") | |
| if __name__ == '__main__': | |
| import argparse | |
| parser = argparse.ArgumentParser(description='Extract combinatorics and optimal angles from maximal volume config') | |
| parser.add_argument('json_file', help='Path to optimization result JSON file') | |
| args = parser.parse_args() | |
| extract_combinatorics_and_angles(args.json_file) | |