#!/usr/bin/env python3 """ Plot circle packing results from metrics.json file """ import json import matplotlib.pyplot as plt import matplotlib.patches as patches import numpy as np import re from pathlib import Path def parse_centers_from_string(centers_str): """Parse center coordinates from the centers_str field""" centers = [] # Pattern to match: centers[i] = (x, y) pattern = r'centers\[\d+\] = \(([0-9.]+), ([0-9.]+)\)' matches = re.findall(pattern, centers_str) for x, y in matches: centers.append((float(x), float(y))) return np.array(centers) def calculate_radii_from_centers(centers, target_sum): """ Estimate radii assuming roughly equal radii for all circles This is just for visualization - actual radii would come from the solution """ n = len(centers) # Start with uniform distribution uniform_radius = target_sum / n # Calculate actual minimum distances and adjust radii = [] for i in range(n): # Calculate minimum distance to boundaries x, y = centers[i] min_boundary_dist = min(x, y, 1.0 - x, 1.0 - y) # Calculate minimum distance to other circles (divided by 2 for radius) min_circle_dist = float('inf') for j in range(n): if i != j: dist = np.sqrt(np.sum((centers[i] - centers[j]) ** 2)) min_circle_dist = min(min_circle_dist, dist) # Estimate radius as smaller of boundary constraint and half of nearest circle estimated_r = min(min_boundary_dist, min_circle_dist / 2.0, uniform_radius * 1.5) radii.append(estimated_r) # Scale to match target sum radii = np.array(radii) radii = radii * (target_sum / np.sum(radii)) return radii def plot_circle_packing(metrics_file): """Plot circle packing from metrics.json file""" # Load metrics with open(metrics_file, 'r') as f: data = json.load(f) # Extract information combined_score = data['combined_score'] is_correct = data['correct'] generation = data.get('generation', 'N/A') num_circles = data['primary']['public']['num_circles'] centers_str = data['primary']['public']['centers_str'] # Try to load actual radii from extra.npz extra_file = Path(metrics_file).parent / 'extra.npz' if extra_file.exists(): extra_data = np.load(extra_file) centers = extra_data['centers'] radii = extra_data['radii'] print(f"Loaded {len(centers)} circles with actual radii from extra.npz") print(f"Sum of radii: {np.sum(radii):.6f} (target: {combined_score:.6f})") else: # Fallback: Parse centers from string and estimate radii centers = parse_centers_from_string(centers_str) print(f"Parsed {len(centers)} circle centers") radii = calculate_radii_from_centers(centers, combined_score) print(f"Estimated sum of radii: {np.sum(radii):.6f} (target: {combined_score:.6f})") print("WARNING: Using estimated radii. Actual radii not found in extra.npz") # Create figure with multiple subplots fig = plt.figure(figsize=(16, 6)) # Main packing plot ax1 = plt.subplot(1, 3, 1) ax1.set_xlim(-0.05, 1.05) ax1.set_ylim(-0.05, 1.05) ax1.set_aspect('equal') ax1.set_title(f'Circle Packing (Generation {generation})', fontsize=14, fontweight='bold') ax1.set_xlabel('X') ax1.set_ylabel('Y') # Draw unit square square = patches.Rectangle((0, 0), 1, 1, linewidth=2, edgecolor='black', facecolor='none') ax1.add_patch(square) # Draw circles with color based on radius colors = plt.cm.viridis(radii / np.max(radii)) for i, (center, radius) in enumerate(zip(centers, radii)): circle = patches.Circle(center, radius, linewidth=1, edgecolor='black', facecolor=colors[i], alpha=0.6) ax1.add_patch(circle) # Add circle number ax1.text(center[0], center[1], str(i), ha='center', va='center', fontsize=6, fontweight='bold') # Add grid ax1.grid(True, alpha=0.3, linestyle='--') # Radii distribution plot ax2 = plt.subplot(1, 3, 2) ax2.hist(radii, bins=15, color='steelblue', edgecolor='black', alpha=0.7) ax2.set_xlabel('Radius') ax2.set_ylabel('Frequency') ax2.set_title('Radius Distribution', fontsize=12, fontweight='bold') ax2.grid(True, alpha=0.3) # Statistics text ax2.axvline(np.mean(radii), color='red', linestyle='--', linewidth=2, label=f'Mean: {np.mean(radii):.4f}') ax2.axvline(np.median(radii), color='green', linestyle='--', linewidth=2, label=f'Median: {np.median(radii):.4f}') ax2.legend() # Metrics summary ax3 = plt.subplot(1, 3, 3) ax3.axis('off') # Calculate some additional metrics for display total_area = np.sum(np.pi * radii ** 2) packing_efficiency = total_area / 1.0 # Unit square area = 1 # Check for overlaps (approximate) overlaps = 0 min_gap = float('inf') for i in range(len(centers)): for j in range(i+1, len(centers)): dist = np.sqrt(np.sum((centers[i] - centers[j]) ** 2)) gap = dist - (radii[i] + radii[j]) if gap < 0: overlaps += 1 elif gap < min_gap: min_gap = gap # Check boundary violations boundary_violations = 0 for i in range(len(centers)): x, y = centers[i] r = radii[i] if x - r < 0 or x + r > 1 or y - r < 0 or y + r > 1: boundary_violations += 1 metrics_text = f""" CIRCLE PACKING METRICS {'='*40} Primary Metrics: • Sum of Radii: {combined_score:.6f} • Number of Circles: {num_circles} • Valid Solution: {'✓ Yes' if is_correct else '✗ No'} • Generation: {generation} Radius Statistics: • Mean Radius: {np.mean(radii):.6f} • Std Dev: {np.std(radii):.6f} • Min Radius: {np.min(radii):.6f} • Max Radius: {np.max(radii):.6f} • Median Radius: {np.median(radii):.6f} Packing Efficiency: • Total Circle Area: {total_area:.6f} • Area Ratio: {packing_efficiency:.2%} • Avg Area per Circle: {total_area/num_circles:.6f} Validation (Approximate): • Overlaps Detected: {overlaps} • Boundary Violations: {boundary_violations} • Min Gap (non-touching): {min_gap:.6f} Spatial Distribution: • Center of Mass: ({np.mean(centers[:, 0]):.4f}, {np.mean(centers[:, 1]):.4f}) • X-spread (std): {np.std(centers[:, 0]):.4f} • Y-spread (std): {np.std(centers[:, 1]):.4f} """ ax3.text(0.1, 0.95, metrics_text, transform=ax3.transAxes, fontsize=10, verticalalignment='top', fontfamily='monospace', bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.3)) plt.tight_layout() # Save figure output_dir = Path(metrics_file).parent output_file = output_dir / 'circle_packing_visualization.png' plt.savefig(output_file, dpi=150, bbox_inches='tight') print(f"\nVisualization saved to: {output_file}") plt.show() if __name__ == '__main__': metrics_file = '/home/tengxiao/pj/ShinkaEvolve/examples/circle_packing/results/results_full_gen200_period10_20260206_062935/best/results/metrics.json' plot_circle_packing(metrics_file)