Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| Gradio GUI for Ideal Polyhedron Volume Toolkit | |
| Interactive interface for: | |
| - Optimizing polyhedra | |
| - Analyzing distributions | |
| - 3D visualization in sphere and Poincaré ball models | |
| Usage: | |
| python bin/gui.py # Local only (127.0.0.1:7860) | |
| python bin/gui.py --share # Create shareable public link | |
| python bin/gui.py --port 8080 # Custom port | |
| """ | |
| import gradio as gr | |
| import numpy as np | |
| import json | |
| import io | |
| import matplotlib.pyplot as plt | |
| import argparse | |
| from datetime import datetime | |
| from pathlib import Path | |
| from PIL import Image | |
| from ideal_poly_volume_toolkit.geometry import ( | |
| delaunay_triangulation_indices, | |
| triangle_volume_from_points_torch, | |
| triangle_volume_bloch_wigner, | |
| ideal_poly_volume_via_delaunay, | |
| ) | |
| from ideal_poly_volume_toolkit.visualization import ( | |
| plot_polyhedron_klein, | |
| plot_polyhedron_poincare, | |
| plot_delaunay_2d, | |
| create_polyhedron_mesh, | |
| ) | |
| from ideal_poly_volume_toolkit.rivin_holonomy import ( | |
| Triangulation, | |
| generators_from_triangulation, | |
| ) | |
| from ideal_poly_volume_toolkit.rivin_delaunay import ( | |
| check_delaunay_realizability, | |
| optimize_hyperbolic_volume, | |
| realize_angles_as_points, | |
| extract_boundary_vertices, | |
| ) | |
| from ideal_poly_volume_toolkit.symmetry import ( | |
| compute_symmetry_group, | |
| format_symmetry_report, | |
| ) | |
| import torch | |
| from scipy.optimize import differential_evolution | |
| # Note: GPU is slower than CPU for this problem due to small tensor sizes | |
| # and transfer overhead, so we use CPU explicitly | |
| DEVICE = torch.device('cpu') | |
| # ============================================================================ | |
| # Optimization Functions | |
| # ============================================================================ | |
| def spherical_to_complex(theta, phi): | |
| """Convert spherical coordinates to complex via stereographic projection.""" | |
| return np.tan(theta/2) * np.exp(1j * phi) | |
| def compute_volume(params, n_vertices): | |
| """Compute volume for a polyhedron with n_vertices. | |
| Performance optimizations: | |
| - Reduced series_terms to 64 (good balance of speed/accuracy) | |
| - Single torch tensor conversion | |
| - Parallel evaluation via differential_evolution workers | |
| Args: | |
| params: Optimization parameters (theta, phi pairs) | |
| n_vertices: Total number of vertices | |
| Returns: | |
| Negative volume (for minimization) | |
| Note: | |
| The polishing step (L-BFGS-B) uses finite differences for gradients. | |
| PyTorch autodiff could be used but the Delaunay triangulation is | |
| not differentiable, making gradients unreliable near topology changes. | |
| """ | |
| complex_points = [complex(0, 0), complex(1, 0)] | |
| n_params = n_vertices - 3 # n_vertices includes 0, 1, ∞, plus (n_vertices-3) free | |
| for i in range(n_params): | |
| theta = params[2*i] | |
| phi = params[2*i + 1] | |
| z = spherical_to_complex(theta, phi) | |
| complex_points.append(z) | |
| Z_np = np.array(complex_points, dtype=np.complex128) | |
| try: | |
| idx = delaunay_triangulation_indices(Z_np) | |
| except: | |
| return 1000.0 | |
| # Single torch conversion (CPU is faster than GPU for small tensors) | |
| Z_torch = torch.tensor(Z_np, dtype=torch.complex128, device=DEVICE) | |
| total_volume = 0 | |
| for (i, j, k) in idx: | |
| try: | |
| # Use Bloch-Wigner dilogarithm (faster and more accurate than Lobachevsky series) | |
| vol = triangle_volume_bloch_wigner( | |
| Z_torch[i], Z_torch[j], Z_torch[k] | |
| ) | |
| total_volume += vol.item() | |
| except: | |
| return 1000.0 | |
| return -total_volume | |
| def run_optimization(n_vertices, n_trials, max_iter, pop_size, seed, progress=gr.Progress()): | |
| """Run optimization with progress tracking. | |
| Uses parallel workers (all CPU cores) for faster optimization. | |
| """ | |
| import os | |
| from functools import partial | |
| # Validate and convert inputs | |
| try: | |
| n_vertices = int(n_vertices) | |
| if n_vertices < 4: | |
| return "Error: Number of vertices must be at least 4", None | |
| if n_vertices > 100: | |
| return "Error: Number of vertices limited to 100 for practical computation time", None | |
| except (ValueError, TypeError): | |
| return "Error: Number of vertices must be an integer", None | |
| n_cpus = os.cpu_count() | |
| # Limit workers to avoid "too many open files" error | |
| # Each worker opens file descriptors for IPC | |
| # Conservative limit: use at most 32 workers | |
| max_workers = min(32, n_cpus) if n_cpus else 1 | |
| progress(0, desc=f"Starting optimization (using {max_workers} workers)...") | |
| n_free_vertices = n_vertices - 3 | |
| n_params = n_free_vertices * 2 | |
| bounds = [(0.1, np.pi - 0.1), (0, 2*np.pi)] * n_free_vertices | |
| # Adaptive settings for large vertex counts | |
| # For high dimensions, reduce popsize to avoid excessive evaluations | |
| adaptive_popsize = min(pop_size, max(10, 15 - (n_vertices - 7) // 3)) | |
| best_volume = 0 | |
| best_params = None | |
| all_volumes = [] | |
| # Create picklable objective function using partial | |
| # (lambdas can't be pickled for multiprocessing) | |
| objective_func = partial(compute_volume, n_vertices=n_vertices) | |
| for trial in range(n_trials): | |
| progress((trial + 1) / n_trials, desc=f"Trial {trial + 1}/{n_trials}") | |
| result = differential_evolution( | |
| objective_func, | |
| bounds, | |
| maxiter=max_iter, | |
| popsize=adaptive_popsize, | |
| seed=seed + trial, | |
| polish=True, | |
| disp=False, | |
| workers=max_workers, # Limited to avoid file descriptor exhaustion | |
| updating='deferred' # Better for parallel workers | |
| ) | |
| volume = -result.fun | |
| all_volumes.append(volume) | |
| if volume > best_volume: | |
| best_volume = volume | |
| best_params = result.x | |
| # Reconstruct best configuration | |
| complex_points = [complex(0, 0), complex(1, 0)] | |
| for i in range(n_free_vertices): | |
| theta = best_params[2*i] | |
| phi = best_params[2*i + 1] | |
| z = spherical_to_complex(theta, phi) | |
| complex_points.append(z) | |
| Z_np = np.array(complex_points, dtype=np.complex128) | |
| idx = delaunay_triangulation_indices(Z_np) | |
| # Compute statistics | |
| stats = { | |
| 'n_vertices': n_vertices, | |
| 'n_faces': len(idx), | |
| 'best_volume': float(best_volume), | |
| 'mean_volume': float(np.mean(all_volumes)), | |
| 'std_volume': float(np.std(all_volumes)), | |
| 'all_volumes': [float(v) for v in all_volumes], | |
| } | |
| # Save result | |
| timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| output_file = f"results/data/{n_vertices}vertex_optimization_{timestamp}.json" | |
| Path(output_file).parent.mkdir(parents=True, exist_ok=True) | |
| with open(output_file, 'w') as f: | |
| json.dump({ | |
| 'metadata': {'timestamp': datetime.now().isoformat()}, | |
| 'best': { | |
| 'volume': stats['best_volume'], | |
| 'params': best_params.tolist(), | |
| 'vertices_real': Z_np.real.tolist(), | |
| 'vertices_imag': Z_np.imag.tolist(), | |
| }, | |
| 'statistics': stats, | |
| }, f, indent=2) | |
| # Create summary text | |
| summary = f""" | |
| ## Optimization Results | |
| **Configuration:** | |
| - Vertices: {n_vertices} | |
| - Trials: {n_trials} | |
| - Iterations per trial: {max_iter} | |
| **Best Result:** | |
| - Volume: {best_volume:.8f} | |
| - Faces: {len(idx)} | |
| **Statistics over all trials:** | |
| - Mean: {stats['mean_volume']:.8f} | |
| - Std Dev: {stats['std_volume']:.8f} | |
| - Best/Mean: {best_volume/stats['mean_volume']:.4f} | |
| **Saved to:** `{output_file}` | |
| """ | |
| # Return data as dict for state management | |
| opt_data = { | |
| 'vertices': Z_np, | |
| 'triangulation': idx, | |
| 'stats': stats | |
| } | |
| return summary, opt_data | |
| # ============================================================================ | |
| # Distribution Analysis Functions | |
| # ============================================================================ | |
| def analyze_volume_distribution(n_vertices, n_samples, seed, progress=gr.Progress()): | |
| """Analyze volume distribution with progress tracking. | |
| Note: n_vertices is the number of random vertices to add. | |
| Finite vertices = n_vertices random + 2 fixed (0, 1) = n_vertices + 2 | |
| Total with infinity = n_vertices + 3 | |
| """ | |
| np.random.seed(seed) | |
| # Fixed vertices: 0, 1 (infinity is implicit in the volume computation) | |
| fixed_vertices = [complex(0, 0), complex(1, 0)] | |
| n_random = n_vertices | |
| total_vertices = n_vertices + 2 # 0, 1 + n_vertices random (∞ is implicit, not counted) | |
| volumes = [] | |
| def sample_random_vertex(): | |
| vec = np.random.randn(3) | |
| vec = vec / np.linalg.norm(vec) | |
| x, y, z = vec | |
| if z > 0.999: | |
| return None | |
| w = complex(x/(1-z), y/(1-z)) | |
| return w | |
| for i in range(n_samples): | |
| if (i + 1) % 100 == 0: | |
| progress((i + 1) / n_samples, desc=f"Sampling: {i + 1}/{n_samples}") | |
| vertices = fixed_vertices.copy() | |
| for _ in range(n_random): | |
| v = sample_random_vertex() | |
| if v is None or any(abs(v - existing) < 0.01 for existing in vertices): | |
| continue | |
| vertices.append(v) | |
| if len(vertices) != total_vertices: | |
| continue | |
| try: | |
| vertices_np = np.array(vertices, dtype=np.complex128) | |
| vol = ideal_poly_volume_via_delaunay(vertices_np, mode='fast', series_terms=96) | |
| if np.isfinite(vol) and 0 < vol < 1000: | |
| volumes.append(vol) | |
| except Exception as e: | |
| # Silently skip failed computations | |
| pass | |
| volumes = np.array(volumes) | |
| # Check if we have any valid volumes | |
| if len(volumes) == 0: | |
| error_msg = f""" | |
| ## Distribution Analysis Failed | |
| **Error:** No valid volume computations succeeded. | |
| **Possible causes:** | |
| - Random vertices may be generating degenerate configurations | |
| - Try increasing the number of samples | |
| - Try a different number of vertices | |
| **Configuration:** | |
| - Random vertices requested: {n_random} | |
| - Samples attempted: {n_samples} | |
| - Valid samples: 0 | |
| """ | |
| return error_msg, None | |
| # Create histogram | |
| fig, ax = plt.subplots(figsize=(10, 6)) | |
| ax.hist(volumes, bins=50, density=True, alpha=0.7, color='steelblue', edgecolor='black') | |
| ax.axvline(np.mean(volumes), color='red', linestyle='--', linewidth=2, | |
| label=f'Mean: {np.mean(volumes):.4f}') | |
| ax.axvline(np.median(volumes), color='green', linestyle='--', linewidth=2, | |
| label=f'Median: {np.median(volumes):.4f}') | |
| ax.set_xlabel('Volume', fontsize=12) | |
| ax.set_ylabel('Density', fontsize=12) | |
| ax.set_title(f'{total_vertices}-Vertex Volume Distribution ({len(volumes)} samples)', fontsize=14) | |
| ax.legend() | |
| ax.grid(True, alpha=0.3) | |
| # Save plot to BytesIO and convert to PIL Image for Gradio | |
| buf = io.BytesIO() | |
| plt.tight_layout() | |
| plt.savefig(buf, format='png', dpi=150) | |
| buf.seek(0) | |
| plt.close() | |
| # Convert BytesIO to PIL Image for Gradio | |
| img = Image.open(buf) | |
| # Statistics summary | |
| summary = f""" | |
| ## Distribution Analysis Results | |
| **Configuration:** | |
| - Random vertices: {n_random} | |
| - Fixed vertices: 2 (at 0, 1) | |
| - **Finite vertices: {total_vertices}** | |
| - **Total (with ∞): {total_vertices + 1}** | |
| - Samples requested: {n_samples} | |
| - Valid samples: {len(volumes)} | |
| **Statistics:** | |
| - Mean: {np.mean(volumes):.8f} | |
| - Median: {np.median(volumes):.8f} | |
| - Std Dev: {np.std(volumes):.8f} | |
| - Min: {np.min(volumes):.8f} | |
| - Max: {np.max(volumes):.8f} | |
| - Q25: {np.percentile(volumes, 25):.8f} | |
| - Q75: {np.percentile(volumes, 75):.8f} | |
| """ | |
| return summary, img | |
| # ============================================================================ | |
| # Visualization Functions | |
| # ============================================================================ | |
| def visualize_configuration(vertices_real, vertices_imag, vis_type, subdivisions): | |
| """Create visualization based on user selection.""" | |
| if not vertices_real or not vertices_imag: | |
| return None, "Please provide vertices" | |
| try: | |
| # Parse vertices | |
| real_parts = [float(x.strip()) for x in vertices_real.split(',')] | |
| imag_parts = [float(x.strip()) for x in vertices_imag.split(',')] | |
| if len(real_parts) != len(imag_parts): | |
| return None, "Real and imaginary parts must have same length" | |
| vertices = np.array([complex(r, i) for r, i in zip(real_parts, imag_parts)]) | |
| if vis_type == "Delaunay (2D)": | |
| idx = delaunay_triangulation_indices(vertices) | |
| fig = plot_delaunay_2d(vertices, idx) | |
| return fig, f"Showing Delaunay triangulation with {len(idx)} faces" | |
| elif vis_type == "Klein Ball (3D)": | |
| fig = plot_polyhedron_klein(vertices, subdivisions=subdivisions) | |
| return fig, f"Showing Klein model (subdivision level: {subdivisions})" | |
| elif vis_type == "Poincaré Ball (3D)": | |
| fig = plot_polyhedron_poincare(vertices, subdivisions=subdivisions) | |
| return fig, f"Showing Poincaré ball model (subdivision level: {subdivisions})" | |
| except Exception as e: | |
| return None, f"Error: {str(e)}" | |
| def load_from_optimization(opt_data): | |
| """Load vertices from optimization results.""" | |
| if opt_data is None: | |
| return "", "" | |
| # opt_data is a dict with vertices | |
| vertices = opt_data.get('vertices', None) | |
| if vertices is None: | |
| return "", "" | |
| real_str = ", ".join(f"{v.real:.6f}" for v in vertices) | |
| imag_str = ", ".join(f"{v.imag:.6f}" for v in vertices) | |
| return real_str, imag_str | |
| # ============================================================================ | |
| # Holonomy/Arithmeticity Functions | |
| # ============================================================================ | |
| def compute_holonomy_analysis(vertices_real, vertices_imag, progress=gr.Progress()): | |
| """Compute holonomy generators and check arithmeticity.""" | |
| if not vertices_real or not vertices_imag: | |
| return "Please provide vertices", None | |
| try: | |
| # Parse vertices | |
| real_parts = [float(x.strip()) for x in vertices_real.split(',')] | |
| imag_parts = [float(x.strip()) for x in vertices_imag.split(',')] | |
| if len(real_parts) != len(imag_parts): | |
| return "Real and imaginary parts must have same length", None | |
| vertices = np.array([complex(r, i) for r, i in zip(real_parts, imag_parts)]) | |
| progress(0.2, desc="Computing Delaunay triangulation...") | |
| # Get triangulation | |
| idx = delaunay_triangulation_indices(vertices) | |
| F = len(idx) | |
| progress(0.4, desc="Building adjacency structure...") | |
| # Build adjacency structure | |
| adjacency = {} | |
| edge_id_map = {} | |
| edge_id = 0 | |
| for i, tri_i in enumerate(idx): | |
| for side_i in range(3): | |
| v1_i, v2_i = tri_i[side_i], tri_i[(side_i + 1) % 3] | |
| edge = tuple(sorted([v1_i, v2_i])) | |
| # Find matching triangle | |
| for j, tri_j in enumerate(idx): | |
| if i == j: | |
| continue | |
| for side_j in range(3): | |
| v1_j, v2_j = tri_j[side_j], tri_j[(side_j + 1) % 3] | |
| if set([v1_j, v2_j]) == set([v1_i, v2_i]): | |
| if (i, side_i) not in adjacency: | |
| if edge not in edge_id_map: | |
| edge_id_map[edge] = edge_id | |
| edge_id += 1 | |
| adjacency[(i, side_i)] = (j, side_j, edge_id_map[edge]) | |
| progress(0.6, desc="Computing holonomy generators...") | |
| # Define order and orientation | |
| order = {t: [0, 1, 2] for t in range(F)} | |
| orientation = {} | |
| for edge, eid in edge_id_map.items(): | |
| for (t, s), (u, su, e) in adjacency.items(): | |
| if e == eid: | |
| orientation[eid] = ((t, s), (u, su)) | |
| break | |
| # Create triangulation | |
| T = Triangulation(F, adjacency, order, orientation) | |
| # Zero shears for ideal polyhedra | |
| Z = {eid: 0.0 for eid in range(edge_id)} | |
| # Compute generators | |
| gens = generators_from_triangulation(T, Z, root=0) | |
| progress(0.8, desc="Analyzing traces...") | |
| # Analyze traces | |
| trace_analysis = [] | |
| traces = [] | |
| integral_count = 0 | |
| for i, (u, v, tokens, M) in enumerate(gens): | |
| trace = M[0][0] + M[1][1] | |
| traces.append(trace) | |
| nearest_int = round(trace) | |
| distance = abs(trace - nearest_int) | |
| is_close = distance < 0.01 | |
| if is_close: | |
| integral_count += 1 | |
| trace_analysis.append({ | |
| 'generator': i, | |
| 'edge': (u, v), | |
| 'trace': float(trace), | |
| 'nearest_int': int(nearest_int), | |
| 'distance': float(distance), | |
| 'is_close': is_close | |
| }) | |
| progress(1.0, desc="Complete!") | |
| # Create summary | |
| summary = f""" | |
| ## Holonomy Analysis Results | |
| **Configuration:** | |
| - Vertices: {len(vertices)} | |
| - Triangular faces: {F} | |
| - Number of generators: {len(gens)} | |
| **Arithmeticity Test:** | |
| - Generators with integral traces: {integral_count}/{len(gens)} | |
| - Percentage: {100*integral_count/len(gens):.1f}% | |
| **Interpretation:** | |
| """ | |
| if integral_count == len(gens): | |
| summary += "✅ **ALL TRACES ARE INTEGERS!**\n\n" | |
| summary += "This polyhedron is **ARITHMETIC** - it has deep number-theoretic structure!\n" | |
| summary += "The holonomy lies in PSL(2,O_K) for some number field K." | |
| elif integral_count > len(gens) * 0.7: | |
| summary += "⚠️ **MOST TRACES ARE CLOSE TO INTEGERS**\n\n" | |
| summary += f"This suggests possible arithmetic structure with {integral_count}/{len(gens)} integral traces.\n" | |
| summary += "May be commensurable with an arithmetic group." | |
| else: | |
| summary += "❌ **NOT ARITHMETIC**\n\n" | |
| summary += "Only a few traces are close to integers. This is likely a generic configuration." | |
| summary += "\n\n## Trace Details:\n\n" | |
| summary += "| Generator | Edge | Trace | Nearest Int | Distance | Status |\n" | |
| summary += "|-----------|------|-------|-------------|----------|--------|\n" | |
| for ta in trace_analysis: | |
| status = "✅ INTEGRAL" if ta['is_close'] else "❌" | |
| summary += f"| {ta['generator']} | {ta['edge'][0]}-{ta['edge'][1]} | " | |
| summary += f"{ta['trace']:.6f} | {ta['nearest_int']} | " | |
| summary += f"{ta['distance']:.6f} | {status} |\n" | |
| # Create plot | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) | |
| # Plot traces | |
| gen_nums = [ta['generator'] for ta in trace_analysis] | |
| trace_vals = [ta['trace'] for ta in trace_analysis] | |
| colors = ['green' if ta['is_close'] else 'red' for ta in trace_analysis] | |
| ax1.bar(gen_nums, trace_vals, color=colors, alpha=0.7, edgecolor='black') | |
| ax1.axhline(y=0, color='k', linestyle='-', linewidth=0.5) | |
| ax1.set_xlabel('Generator', fontsize=12) | |
| ax1.set_ylabel('Trace', fontsize=12) | |
| ax1.set_title('Holonomy Generator Traces', fontsize=14) | |
| ax1.grid(True, alpha=0.3) | |
| # Plot distances from integers | |
| distances = [ta['distance'] for ta in trace_analysis] | |
| ax2.bar(gen_nums, distances, color=colors, alpha=0.7, edgecolor='black') | |
| ax2.axhline(y=0.01, color='orange', linestyle='--', linewidth=2, | |
| label='Threshold (0.01)') | |
| ax2.set_xlabel('Generator', fontsize=12) | |
| ax2.set_ylabel('Distance from nearest integer', fontsize=12) | |
| ax2.set_title('Integrality Test', fontsize=14) | |
| ax2.legend() | |
| ax2.grid(True, alpha=0.3) | |
| ax2.set_yscale('log') | |
| plt.tight_layout() | |
| # Save plot to BytesIO and convert to PIL Image for Gradio | |
| buf = io.BytesIO() | |
| plt.savefig(buf, format='png', dpi=150) | |
| buf.seek(0) | |
| plt.close() | |
| # Convert BytesIO to PIL Image for Gradio | |
| img = Image.open(buf) | |
| return summary, img | |
| except Exception as e: | |
| return f"Error: {str(e)}", None | |
| def compute_symmetry_analysis(vertices_real, vertices_imag): | |
| """Compute symmetry group of the polyhedron.""" | |
| if not vertices_real or not vertices_imag: | |
| return "Please provide vertices" | |
| try: | |
| # Parse vertices | |
| real_parts = [float(x.strip()) for x in vertices_real.split(',')] | |
| imag_parts = [float(x.strip()) for x in vertices_imag.split(',')] | |
| if len(real_parts) != len(imag_parts): | |
| return "Real and imaginary parts must have same length" | |
| vertices = np.array([complex(r, i) for r, i in zip(real_parts, imag_parts)]) | |
| # Lift to 3D (Klein model in the ball) | |
| from ideal_poly_volume_toolkit.visualization import lift_to_sphere_with_inf | |
| vertices_3d = lift_to_sphere_with_inf(vertices) | |
| # Compute symmetry group | |
| sym_info = compute_symmetry_group(vertices_3d) | |
| # Format report | |
| report = format_symmetry_report(sym_info) | |
| return report | |
| except Exception as e: | |
| return f"Error: {str(e)}" | |
| # ============================================================================ | |
| # Fixed Combinatorics Optimization Functions | |
| # ============================================================================ | |
| def generate_random_sphere_points(n_points, seed=None): | |
| """ | |
| Generate n random points uniformly on the unit sphere, plus north pole (infinity). | |
| Returns complex points via stereographic projection (north pole -> infinity). | |
| """ | |
| if seed is not None: | |
| np.random.seed(seed) | |
| # Generate n uniform random points on sphere using Gaussian method | |
| points_3d = [] | |
| for _ in range(n_points): | |
| vec = np.random.randn(3) | |
| vec = vec / np.linalg.norm(vec) | |
| points_3d.append(vec) | |
| points_3d = np.array(points_3d) | |
| # Stereographic projection from north pole (0, 0, 1) to complex plane | |
| # For point (x, y, z), the projection is w = (x + iy) / (1 - z) | |
| complex_points = [] | |
| for x, y, z in points_3d: | |
| if z > 0.9999: # Very close to north pole - treat as infinity | |
| complex_points.append(complex(np.inf, np.inf)) | |
| else: | |
| w = complex(x / (1 - z), y / (1 - z)) | |
| complex_points.append(w) | |
| return np.array(complex_points), points_3d | |
| def inverse_stereographic_to_sphere(complex_points): | |
| """ | |
| Map complex points back to sphere via inverse stereographic projection. | |
| For w = x + iy, the sphere point is: | |
| X = 2x / (|w|^2 + 1) | |
| Y = 2y / (|w|^2 + 1) | |
| Z = (|w|^2 - 1) / (|w|^2 + 1) | |
| """ | |
| points_3d = [] | |
| for w in complex_points: | |
| if not np.isfinite(w): | |
| points_3d.append([0, 0, 1]) # North pole | |
| else: | |
| r2 = w.real**2 + w.imag**2 | |
| denom = r2 + 1 | |
| X = 2 * w.real / denom | |
| Y = 2 * w.imag / denom | |
| Z = (r2 - 1) / denom | |
| points_3d.append([X, Y, Z]) | |
| return np.array(points_3d) | |
| def optimize_fixed_combinatorics(n_points, seed, progress=gr.Progress()): | |
| """ | |
| Generate random points, extract combinatorics, optimize volume for fixed combinatorics. | |
| This uses the Rivin-Delaunay algorithm to find the maximal volume configuration | |
| while keeping the combinatorial structure (triangulation) fixed. | |
| """ | |
| progress(0.1, desc="Generating random points on sphere...") | |
| # Generate random points | |
| complex_points, sphere_points = generate_random_sphere_points(n_points, seed) | |
| # Filter out any infinite points for Delaunay computation | |
| finite_mask = np.isfinite(complex_points) | |
| finite_points = complex_points[finite_mask] | |
| if len(finite_points) < 3: | |
| return "Error: Need at least 3 finite points", None, None | |
| progress(0.2, desc="Computing Delaunay triangulation...") | |
| # Compute Delaunay triangulation to get combinatorics | |
| triangulation_indices = delaunay_triangulation_indices(finite_points) | |
| triangles = [tuple(tri) for tri in triangulation_indices] | |
| n_triangles = len(triangles) | |
| n_vertices = len(finite_points) | |
| progress(0.3, desc="Checking Delaunay realizability...") | |
| # Check realizability (should always be realizable since we started from Delaunay) | |
| realizability = check_delaunay_realizability(triangles, verbose=False) | |
| if not realizability['realizable']: | |
| return f"Error: Triangulation not realizable: {realizability['message']}", None, None | |
| progress(0.5, desc="Optimizing hyperbolic volume...") | |
| # Optimize volume for this fixed combinatorics | |
| opt_result = optimize_hyperbolic_volume( | |
| triangles, | |
| initial_angles=realizability['angles_radians'], | |
| verbose=False | |
| ) | |
| if not opt_result['success']: | |
| return f"Warning: Optimization may not have converged: {opt_result['message']}", None, None | |
| optimal_volume = opt_result['volume'] | |
| optimal_angles = opt_result['angles'] | |
| progress(0.7, desc="Reconstructing geometry from optimal angles...") | |
| # Realize the optimal angles as point positions | |
| realization = realize_angles_as_points( | |
| triangles, | |
| optimal_angles, | |
| verbose=False | |
| ) | |
| if not realization['success']: | |
| # Even if realization didn't fully succeed, we may have useful data | |
| pass | |
| # Get the optimized vertices | |
| if realization['points'] is not None: | |
| # Map from realization indices to complex numbers | |
| vertex_list = realization['vertex_list'] | |
| points_2d = realization['points'] | |
| # Create complex array in original vertex order | |
| optimized_complex = np.zeros(len(vertex_list), dtype=complex) | |
| for i, v in enumerate(vertex_list): | |
| optimized_complex[v] = complex(points_2d[i, 0], points_2d[i, 1]) | |
| else: | |
| optimized_complex = finite_points # Fall back to original | |
| progress(0.9, desc="Computing initial volume for comparison...") | |
| # Compute initial volume for comparison | |
| initial_volume = ideal_poly_volume_via_delaunay(finite_points, use_bloch_wigner=True) | |
| progress(1.0, desc="Complete!") | |
| # Build summary | |
| summary = f""" | |
| ## Fixed Combinatorics Optimization Results | |
| **Input Configuration:** | |
| - Random points generated: {n_points} | |
| - Finite vertices (after projection): {n_vertices} | |
| - Triangular faces: {n_triangles} | |
| - Random seed: {seed} | |
| **Optimization Results:** | |
| - Initial volume (random): {initial_volume:.8f} | |
| - Optimal volume (Rivin): {optimal_volume:.8f} | |
| - Volume improvement: {(optimal_volume/initial_volume - 1)*100:.2f}% | |
| **Geometry Reconstruction:** | |
| - Success: {'Yes' if realization['success'] else 'Partial'} | |
| - Triangulation preserved: {'Yes' if realization.get('triangulation_preserved', False) else 'No'} | |
| - Angle error: {realization.get('angle_error_degrees', 'N/A'):.4f}° (if applicable) | |
| **Interpretation:** | |
| The Rivin-Delaunay algorithm finds the unique maximal volume configuration | |
| for the given combinatorial triangulation structure. | |
| """ | |
| # Prepare data for visualization and holonomy check | |
| opt_data = { | |
| 'vertices': optimized_complex, | |
| 'triangulation': triangulation_indices, | |
| 'volume': optimal_volume, | |
| 'initial_volume': initial_volume, | |
| 'angles': optimal_angles, | |
| 'triangles': triangles, | |
| 'n_vertices': n_vertices, | |
| 'n_faces': n_triangles, | |
| } | |
| return summary, opt_data, optimized_complex | |
| def batch_fixed_combinatorics_test(n_points, n_trials, seed, progress=gr.Progress()): | |
| """ | |
| Run multiple trials of fixed combinatorics optimization and check arithmeticity. | |
| """ | |
| results = [] | |
| all_arithmetic = True | |
| for trial in range(n_trials): | |
| progress((trial + 0.5) / n_trials, desc=f"Trial {trial + 1}/{n_trials}...") | |
| trial_seed = seed + trial | |
| # Generate and optimize | |
| summary, opt_data, optimized_complex = optimize_fixed_combinatorics( | |
| n_points, trial_seed, progress=gr.Progress() | |
| ) | |
| if opt_data is None: | |
| results.append({ | |
| 'trial': trial + 1, | |
| 'seed': trial_seed, | |
| 'success': False, | |
| 'error': summary | |
| }) | |
| continue | |
| # Check arithmeticity via holonomy | |
| vertices = opt_data['vertices'] | |
| triangles = opt_data['triangles'] | |
| # Build holonomy structure | |
| try: | |
| F = len(triangles) | |
| adjacency = {} | |
| edge_id_map = {} | |
| edge_id = 0 | |
| for i, tri_i in enumerate(triangles): | |
| for side_i in range(3): | |
| v1_i, v2_i = tri_i[side_i], tri_i[(side_i + 1) % 3] | |
| edge = tuple(sorted([v1_i, v2_i])) | |
| for j, tri_j in enumerate(triangles): | |
| if i == j: | |
| continue | |
| for side_j in range(3): | |
| v1_j, v2_j = tri_j[side_j], tri_j[(side_j + 1) % 3] | |
| if set([v1_j, v2_j]) == set([v1_i, v2_i]): | |
| if (i, side_i) not in adjacency: | |
| if edge not in edge_id_map: | |
| edge_id_map[edge] = edge_id | |
| edge_id += 1 | |
| adjacency[(i, side_i)] = (j, side_j, edge_id_map[edge]) | |
| order = {t: [0, 1, 2] for t in range(F)} | |
| orientation = {} | |
| for edge, eid in edge_id_map.items(): | |
| for (t, s), (u, su, e) in adjacency.items(): | |
| if e == eid: | |
| orientation[eid] = ((t, s), (u, su)) | |
| break | |
| T = Triangulation(F, adjacency, order, orientation) | |
| Z = {eid: 0.0 for eid in range(edge_id)} | |
| gens = generators_from_triangulation(T, Z, root=0) | |
| # Check traces | |
| integral_count = 0 | |
| traces = [] | |
| for u, v, tokens, M in gens: | |
| trace = M[0][0] + M[1][1] | |
| traces.append(trace) | |
| if abs(trace - round(trace)) < 0.01: | |
| integral_count += 1 | |
| is_arithmetic = (integral_count == len(gens)) if len(gens) > 0 else False | |
| if not is_arithmetic: | |
| all_arithmetic = False | |
| results.append({ | |
| 'trial': trial + 1, | |
| 'seed': trial_seed, | |
| 'success': True, | |
| 'volume': opt_data['volume'], | |
| 'initial_volume': opt_data['initial_volume'], | |
| 'n_generators': len(gens), | |
| 'integral_traces': integral_count, | |
| 'is_arithmetic': is_arithmetic, | |
| 'traces': traces, | |
| }) | |
| except Exception as e: | |
| results.append({ | |
| 'trial': trial + 1, | |
| 'seed': trial_seed, | |
| 'success': False, | |
| 'error': str(e) | |
| }) | |
| all_arithmetic = False | |
| # Build summary report | |
| summary = f""" | |
| ## Batch Fixed Combinatorics Test Results | |
| **Configuration:** | |
| - Points per trial: {n_points} | |
| - Number of trials: {n_trials} | |
| - Starting seed: {seed} | |
| **Results Summary:** | |
| """ | |
| successful = [r for r in results if r.get('success', False)] | |
| arithmetic_count = sum(1 for r in successful if r.get('is_arithmetic', False)) | |
| summary += f"- Successful trials: {len(successful)}/{n_trials}\n" | |
| summary += f"- Arithmetic configurations: {arithmetic_count}/{len(successful)}\n\n" | |
| if all_arithmetic and len(successful) == n_trials: | |
| summary += "**ALL CONFIGURATIONS ARE ARITHMETIC!**\n\n" | |
| summary += "| Trial | Seed | Volume | Init Vol | Improvement | Generators | Integral | Arithmetic |\n" | |
| summary += "|-------|------|--------|----------|-------------|------------|----------|------------|\n" | |
| for r in results: | |
| if r.get('success', False): | |
| improvement = (r['volume'] / r['initial_volume'] - 1) * 100 | |
| arith_str = "YES" if r['is_arithmetic'] else "NO" | |
| summary += f"| {r['trial']} | {r['seed']} | {r['volume']:.6f} | {r['initial_volume']:.6f} | {improvement:+.1f}% | {r['n_generators']} | {r['integral_traces']}/{r['n_generators']} | {arith_str} |\n" | |
| else: | |
| summary += f"| {r['trial']} | {r['seed']} | ERROR | - | - | - | - | - |\n" | |
| # Create plot of volumes | |
| if successful: | |
| fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5)) | |
| trials = [r['trial'] for r in successful] | |
| volumes = [r['volume'] for r in successful] | |
| init_vols = [r['initial_volume'] for r in successful] | |
| ax1.bar(np.array(trials) - 0.2, init_vols, 0.4, label='Initial (random)', alpha=0.7) | |
| ax1.bar(np.array(trials) + 0.2, volumes, 0.4, label='Optimized (Rivin)', alpha=0.7) | |
| ax1.set_xlabel('Trial') | |
| ax1.set_ylabel('Volume') | |
| ax1.set_title('Volume Comparison') | |
| ax1.legend() | |
| ax1.grid(True, alpha=0.3) | |
| # Plot integral trace fractions | |
| fractions = [r['integral_traces'] / r['n_generators'] if r['n_generators'] > 0 else 0 | |
| for r in successful] | |
| colors = ['green' if f == 1.0 else 'red' for f in fractions] | |
| ax2.bar(trials, fractions, color=colors, alpha=0.7) | |
| ax2.axhline(y=1.0, color='green', linestyle='--', linewidth=2, label='100% integral') | |
| ax2.set_xlabel('Trial') | |
| ax2.set_ylabel('Fraction of Integral Traces') | |
| ax2.set_title('Arithmeticity Check') | |
| ax2.set_ylim(0, 1.1) | |
| ax2.legend() | |
| ax2.grid(True, alpha=0.3) | |
| plt.tight_layout() | |
| buf = io.BytesIO() | |
| plt.savefig(buf, format='png', dpi=150) | |
| buf.seek(0) | |
| plt.close() | |
| img = Image.open(buf) | |
| else: | |
| img = None | |
| return summary, img | |
| # ============================================================================ | |
| # Gradio Interface | |
| # ============================================================================ | |
| def create_gui(): | |
| """Create the main Gradio interface.""" | |
| with gr.Blocks(title="Ideal Polyhedron Volume Toolkit", theme=gr.themes.Soft()) as demo: | |
| # Shared state for passing optimization results to visualization | |
| opt_result_state = gr.State(None) | |
| gr.Markdown(""" | |
| # 🔺 Ideal Polyhedron Volume Toolkit | |
| Interactive GUI for computing and optimizing volumes of ideal hyperbolic polyhedra. | |
| """) | |
| with gr.Tabs(): | |
| # ================================================================ | |
| # Tab 1: Optimization | |
| # ================================================================ | |
| with gr.Tab("🎯 Optimization"): | |
| gr.Markdown("Find maximal volume configurations for ideal polyhedra") | |
| with gr.Row(): | |
| with gr.Column(): | |
| opt_vertices = gr.Number(value=7, label="Number of Vertices", | |
| minimum=4, maximum=100, | |
| info="Recommended: 4-30 (higher is much slower)") | |
| opt_trials = gr.Slider(1, 50, value=10, step=1, | |
| label="Number of Trials") | |
| opt_maxiter = gr.Slider(50, 500, value=150, step=50, | |
| label="Max Iterations per Trial", | |
| info="150-200 is usually sufficient") | |
| opt_popsize = gr.Slider(10, 30, value=15, step=5, | |
| label="Population Size") | |
| opt_seed = gr.Number(value=42, label="Random Seed") | |
| opt_button = gr.Button("Run Optimization", variant="primary") | |
| with gr.Column(): | |
| opt_output = gr.Markdown("Results will appear here...") | |
| opt_button.click( | |
| run_optimization, | |
| inputs=[opt_vertices, opt_trials, opt_maxiter, opt_popsize, opt_seed], | |
| outputs=[opt_output, opt_result_state] | |
| ) | |
| # ================================================================ | |
| # Tab 2: Distribution Analysis | |
| # ================================================================ | |
| with gr.Tab("📊 Distribution Analysis"): | |
| gr.Markdown(""" | |
| Analyze volume distributions by sampling random configurations. | |
| **Note:** Vertices are added to fixed base (0, 1, ∞). | |
| So 4 random vertices = 7 total vertices (4 + 0, 1, ∞). | |
| """) | |
| with gr.Row(): | |
| with gr.Column(): | |
| dist_vertices = gr.Slider(1, 10, value=4, step=1, | |
| label="Number of Random Vertices") | |
| dist_samples = gr.Slider(100, 50000, value=5000, step=100, | |
| label="Number of Samples") | |
| dist_seed = gr.Number(value=42, label="Random Seed") | |
| dist_button = gr.Button("Analyze Distribution", variant="primary") | |
| with gr.Column(): | |
| dist_output = gr.Markdown("Results will appear here...") | |
| dist_plot = gr.Image(label="Distribution") | |
| dist_button.click( | |
| analyze_volume_distribution, | |
| inputs=[dist_vertices, dist_samples, dist_seed], | |
| outputs=[dist_output, dist_plot] | |
| ) | |
| # ================================================================ | |
| # Tab 3: Fixed Combinatorics Optimization (Rivin-Delaunay) | |
| # ================================================================ | |
| with gr.Tab("📐 Fixed Combinatorics"): | |
| gr.Markdown(""" | |
| Optimize volume for a **fixed combinatorial triangulation** using the Rivin-Delaunay algorithm. | |
| This finds the unique maximal-volume configuration while preserving the combinatorial structure. | |
| All such maximal configurations are **arithmetic** (have integral holonomy traces). | |
| """) | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### Single Trial") | |
| fc_n_points = gr.Slider(4, 50, value=10, step=1, | |
| label="Number of Random Points", | |
| info="Points on sphere (+ infinity)") | |
| fc_seed = gr.Number(value=42, label="Random Seed") | |
| fc_single_button = gr.Button("Run Single Optimization", variant="primary") | |
| gr.Markdown("---") | |
| gr.Markdown("### Batch Test (with Arithmeticity Check)") | |
| fc_batch_trials = gr.Slider(1, 20, value=5, step=1, | |
| label="Number of Trials") | |
| fc_batch_button = gr.Button("Run Batch Test", variant="secondary") | |
| with gr.Column(): | |
| fc_output = gr.Markdown("Results will appear here...") | |
| fc_plot = gr.Image(label="Results") | |
| # State for storing optimization results | |
| fc_result_state = gr.State(None) | |
| def run_single_fc(n_points, seed): | |
| summary, opt_data, vertices = optimize_fixed_combinatorics( | |
| int(n_points), int(seed) | |
| ) | |
| return summary, opt_data | |
| fc_single_button.click( | |
| run_single_fc, | |
| inputs=[fc_n_points, fc_seed], | |
| outputs=[fc_output, fc_result_state] | |
| ) | |
| fc_batch_button.click( | |
| batch_fixed_combinatorics_test, | |
| inputs=[fc_n_points, fc_batch_trials, fc_seed], | |
| outputs=[fc_output, fc_plot] | |
| ) | |
| # ================================================================ | |
| # Tab 4: 3D Visualization | |
| # ================================================================ | |
| with gr.Tab("🔮 Visualization"): | |
| gr.Markdown("Visualize polyhedra in different models") | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### Input Vertices") | |
| gr.Markdown("Enter vertices as comma-separated values in the complex plane") | |
| vis_real = gr.Textbox( | |
| label="Real parts", | |
| value="0, 1, 0, 0.5", | |
| placeholder="0, 1, 0.5, -0.5" | |
| ) | |
| vis_imag = gr.Textbox( | |
| label="Imaginary parts", | |
| value="0, 0, 1, 0.866", | |
| placeholder="0, 0, 0.866, 0.866" | |
| ) | |
| with gr.Row(): | |
| load_opt_button = gr.Button("Load from Optimization", size="sm") | |
| gr.Markdown("### Visualization Options") | |
| vis_type = gr.Radio( | |
| ["Delaunay (2D)", "Klein Ball (3D)", "Poincaré Ball (3D)"], | |
| value="Klein Ball (3D)", | |
| label="Visualization Type" | |
| ) | |
| vis_subdivisions = gr.Slider(0, 5, value=3, step=1, | |
| label="Subdivision Level (3D only)", | |
| info="Higher = smoother curves (slower rendering)") | |
| vis_button = gr.Button("Generate Visualization", variant="primary") | |
| with gr.Column(): | |
| vis_plot = gr.Plot(label="Visualization") | |
| vis_status = gr.Textbox(label="Status", interactive=False) | |
| vis_button.click( | |
| visualize_configuration, | |
| inputs=[vis_real, vis_imag, vis_type, vis_subdivisions], | |
| outputs=[vis_plot, vis_status] | |
| ) | |
| load_opt_button.click( | |
| load_from_optimization, | |
| inputs=[opt_result_state], | |
| outputs=[vis_real, vis_imag] | |
| ) | |
| # ================================================================ | |
| # Tab 5: Arithmeticity / Holonomy | |
| # ================================================================ | |
| with gr.Tab("🔬 Arithmeticity"): | |
| gr.Markdown("Check if a polyhedron is arithmetic using Penner-Rivin holonomy") | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### Input Vertices") | |
| arith_real = gr.Textbox( | |
| label="Real parts", | |
| value="0, 1, 0, 0.5", | |
| placeholder="0, 1, 0.5, -0.5" | |
| ) | |
| arith_imag = gr.Textbox( | |
| label="Imaginary parts", | |
| value="0, 0, 1, 0.866", | |
| placeholder="0, 0, 0.866, 0.866" | |
| ) | |
| with gr.Row(): | |
| load_opt_arith_button = gr.Button("Load from Optimization", size="sm") | |
| arith_button = gr.Button("Compute Holonomy & Check Arithmeticity", variant="primary") | |
| symmetry_button = gr.Button("Compute Symmetry Group", variant="secondary") | |
| with gr.Column(): | |
| arith_output = gr.Markdown("Results will appear here...") | |
| arith_plot = gr.Image(label="Trace Analysis") | |
| arith_button.click( | |
| compute_holonomy_analysis, | |
| inputs=[arith_real, arith_imag], | |
| outputs=[arith_output, arith_plot] | |
| ) | |
| symmetry_button.click( | |
| compute_symmetry_analysis, | |
| inputs=[arith_real, arith_imag], | |
| outputs=[arith_output] | |
| ) | |
| load_opt_arith_button.click( | |
| load_from_optimization, | |
| inputs=[opt_result_state], | |
| outputs=[arith_real, arith_imag] | |
| ) | |
| gr.Markdown(""" | |
| ### About Arithmeticity | |
| A hyperbolic 3-manifold is **arithmetic** if its holonomy representation lies in PSL(2, O_K) | |
| where O_K is the ring of integers in a number field K. | |
| For ideal polyhedra, this can be tested by computing: | |
| 1. Holonomy generators (Penner-Rivin algorithm) | |
| 2. Traces of these generators | |
| 3. Checking if traces are integers (or lie in a number field) | |
| **Arithmetic polyhedra have deep number-theoretic significance!** | |
| If all traces are integers, the configuration is arithmetic and related to | |
| special lattices in hyperbolic space. | |
| """) | |
| # ================================================================ | |
| # Tab 6: About | |
| # ================================================================ | |
| with gr.Tab("ℹ️ About"): | |
| gr.Markdown(""" | |
| ## About This Tool | |
| This GUI provides an interactive interface to the **Ideal Polyhedron Volume Toolkit**. | |
| ### Features | |
| - **Optimization**: Find maximal volume configurations using differential evolution | |
| - **Distribution Analysis**: Sample random configurations and analyze volume distributions | |
| - **Fixed Combinatorics**: Optimize volume while keeping triangulation fixed (Rivin-Delaunay) | |
| - **3D Visualization**: View polyhedra in multiple models: | |
| - Delaunay triangulation in complex plane (2D) | |
| - Stereographic projection on unit sphere (3D) | |
| - Poincaré ball model (3D hyperbolic geometry) | |
| - **Arithmeticity Testing**: Check if polyhedra have arithmetic holonomy (Penner-Rivin) | |
| ### Mathematical Background | |
| Ideal polyhedra are polyhedra in hyperbolic 3-space with all vertices at infinity. | |
| Their volumes can be computed using: | |
| - Delaunay triangulation of vertex positions | |
| - Lobachevsky's formula for ideal tetrahedra | |
| - Stereographic projection from the complex plane | |
| ### Usage Tips | |
| 1. Start with **Optimization** to find interesting configurations | |
| 2. Use **Load from Optimization** in the Visualization tab to see results | |
| 3. Adjust **subdivision level** for smoother 3D visualizations | |
| 4. Compare Klein ball and Poincaré ball models to understand hyperbolic geometry | |
| ### Performance | |
| - **Parallel optimization**: Uses up to 32 CPU cores automatically | |
| - **7-vertex**: ~10 seconds for 10 trials | |
| - **20-vertex**: ~2-3 minutes for 10 trials | |
| **Note**: Worker limit set to 32 to avoid file descriptor exhaustion. | |
| To use more cores, increase system limit: `ulimit -n 4096` | |
| ### Documentation | |
| - See `README.md` for installation and Python API | |
| - See `bin/README.md` for command-line tools | |
| - See `examples/` for research scripts | |
| --- | |
| **Version:** 0.3.0 | |
| **License:** MIT | |
| """) | |
| gr.Markdown("---") | |
| gr.Markdown("*Ideal Polyhedron Volume Toolkit GUI*") | |
| return demo | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser( | |
| description="Launch Gradio GUI for Ideal Polyhedron Volume Toolkit", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| %(prog)s # Launch locally on 127.0.0.1:7860 | |
| %(prog)s --share # Create shareable public link | |
| %(prog)s --port 8080 # Use custom port | |
| %(prog)s --share --port 8080 # Share with custom port | |
| """ | |
| ) | |
| parser.add_argument('--share', action='store_true', | |
| help='Create a shareable public Gradio link') | |
| parser.add_argument('--port', type=int, default=7860, | |
| help='Port to run the server on (default: 7860)') | |
| parser.add_argument('--server-name', type=str, default="127.0.0.1", | |
| help='Server name/IP to bind to (default: 127.0.0.1)') | |
| args = parser.parse_args() | |
| demo = create_gui() | |
| print("=" * 70) | |
| print("🎨 Ideal Polyhedron Volume Toolkit - GUI") | |
| print("=" * 70) | |
| if args.share: | |
| print("Creating shareable public link...") | |
| print("⚠️ WARNING: Public links expose your local server to the internet") | |
| else: | |
| print(f"Launching local server at http://{args.server_name}:{args.port}") | |
| print("=" * 70) | |
| demo.launch( | |
| share=args.share, | |
| server_name=args.server_name, | |
| server_port=args.port | |
| ) | |