217 lines
8.1 KiB
Python
217 lines
8.1 KiB
Python
|
|
"""
|
||
|
|
Compare Triangle (pure Delaunay) vs Gmsh (Frontal-Delaunay) for isogrid meshing.
|
||
|
|
|
||
|
|
Generates side-by-side visualizations showing:
|
||
|
|
- Triangle count efficiency
|
||
|
|
- Minimum angle distribution
|
||
|
|
- Boundary conformance quality
|
||
|
|
- Density heatmap overlay
|
||
|
|
- Rib pattern visual comparison
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import numpy as np
|
||
|
|
import matplotlib.pyplot as plt
|
||
|
|
from pathlib import Path
|
||
|
|
import sys
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||
|
|
|
||
|
|
from src.brain import triangulation
|
||
|
|
from src.brain import triangulation_gmsh
|
||
|
|
from src.brain.geometry_schema import normalize_geometry_schema
|
||
|
|
from src.brain.density_field import evaluate_density_grid
|
||
|
|
from src.atomizer_study import DEFAULT_PARAMS
|
||
|
|
|
||
|
|
|
||
|
|
def compute_triangle_angles(vertices, triangles):
|
||
|
|
"""Compute all angles in all triangles (returns flat array of angles in degrees)."""
|
||
|
|
angles = []
|
||
|
|
for tri in triangles:
|
||
|
|
p0, p1, p2 = vertices[tri]
|
||
|
|
|
||
|
|
# Vectors
|
||
|
|
v01 = p1 - p0
|
||
|
|
v12 = p2 - p1
|
||
|
|
v20 = p0 - p2
|
||
|
|
|
||
|
|
# Angles using law of cosines
|
||
|
|
def angle_from_vectors(va, vb):
|
||
|
|
cos_angle = np.dot(-va, vb) / (np.linalg.norm(va) * np.linalg.norm(vb))
|
||
|
|
return np.degrees(np.arccos(np.clip(cos_angle, -1.0, 1.0)))
|
||
|
|
|
||
|
|
angles.append(angle_from_vectors(v20, v01))
|
||
|
|
angles.append(angle_from_vectors(v01, v12))
|
||
|
|
angles.append(angle_from_vectors(v12, v20))
|
||
|
|
|
||
|
|
return np.array(angles)
|
||
|
|
|
||
|
|
|
||
|
|
def plot_comparison(geometry, params, output_dir):
|
||
|
|
"""Generate comparison plots for Triangle vs Gmsh."""
|
||
|
|
output_dir = Path(output_dir)
|
||
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
# Generate both triangulations
|
||
|
|
print("Generating Triangle mesh...")
|
||
|
|
tri_result = triangulation.generate_triangulation(geometry, params)
|
||
|
|
tri_verts = tri_result['vertices']
|
||
|
|
tri_tris = tri_result['triangles']
|
||
|
|
|
||
|
|
print("Generating Gmsh mesh...")
|
||
|
|
gmsh_result = triangulation_gmsh.generate_triangulation_gmsh(geometry, params)
|
||
|
|
gmsh_verts = gmsh_result['vertices']
|
||
|
|
gmsh_tris = gmsh_result['triangles']
|
||
|
|
|
||
|
|
print(f"\nTriangle: {len(tri_tris)} triangles, {len(tri_verts)} vertices")
|
||
|
|
print(f"Gmsh: {len(gmsh_tris)} triangles, {len(gmsh_verts)} vertices")
|
||
|
|
print(f"Reduction: {100 * (1 - len(gmsh_tris) / max(len(tri_tris), 1)):.1f}%")
|
||
|
|
|
||
|
|
# Compute angle statistics
|
||
|
|
tri_angles = compute_triangle_angles(tri_verts, tri_tris)
|
||
|
|
gmsh_angles = compute_triangle_angles(gmsh_verts, gmsh_tris)
|
||
|
|
|
||
|
|
print(f"\nTriangle angles: min={tri_angles.min():.1f}°, mean={tri_angles.mean():.1f}°, max={tri_angles.max():.1f}°")
|
||
|
|
print(f"Gmsh angles: min={gmsh_angles.min():.1f}°, mean={gmsh_angles.mean():.1f}°, max={gmsh_angles.max():.1f}°")
|
||
|
|
|
||
|
|
# --- Plot 1: Side-by-side mesh comparison ---
|
||
|
|
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7), dpi=160)
|
||
|
|
|
||
|
|
# Triangle mesh
|
||
|
|
ax1.triplot(tri_verts[:, 0], tri_verts[:, 1], tri_tris, 'b-', linewidth=0.5, alpha=0.8)
|
||
|
|
ax1.set_title(f'Triangle (pure Delaunay)\n{len(tri_tris)} triangles, min angle={tri_angles.min():.1f}°', fontsize=11)
|
||
|
|
ax1.set_xlabel('x [mm]')
|
||
|
|
ax1.set_ylabel('y [mm]')
|
||
|
|
ax1.set_aspect('equal')
|
||
|
|
|
||
|
|
# Gmsh mesh
|
||
|
|
ax2.triplot(gmsh_verts[:, 0], gmsh_verts[:, 1], gmsh_tris, 'g-', linewidth=0.5, alpha=0.8)
|
||
|
|
ax2.set_title(f'Gmsh (Frontal-Delaunay)\n{len(gmsh_tris)} triangles, min angle={gmsh_angles.min():.1f}°', fontsize=11)
|
||
|
|
ax2.set_xlabel('x [mm]')
|
||
|
|
ax2.set_ylabel('y [mm]')
|
||
|
|
ax2.set_aspect('equal')
|
||
|
|
|
||
|
|
# Draw holes on both
|
||
|
|
for ax in [ax1, ax2]:
|
||
|
|
for hole in geometry.get('holes', []):
|
||
|
|
if hole.get('is_circular', False) and 'center' in hole:
|
||
|
|
cx, cy = hole['center']
|
||
|
|
r = float(hole['diameter']) / 2.0
|
||
|
|
circle = plt.Circle((cx, cy), r, color='red', fill=False, linewidth=1.5)
|
||
|
|
ax.add_patch(circle)
|
||
|
|
|
||
|
|
fig.tight_layout()
|
||
|
|
fig.savefig(output_dir / 'mesh_comparison.png')
|
||
|
|
plt.close(fig)
|
||
|
|
print(f"\nSaved: {output_dir / 'mesh_comparison.png'}")
|
||
|
|
|
||
|
|
# --- Plot 2: Angle distribution histograms ---
|
||
|
|
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5), dpi=160)
|
||
|
|
|
||
|
|
ax1.hist(tri_angles, bins=30, color='blue', alpha=0.7, edgecolor='black')
|
||
|
|
ax1.axvline(60, color='green', linestyle='--', linewidth=2, label='Equilateral (60°)')
|
||
|
|
ax1.axvline(tri_angles.min(), color='red', linestyle='--', linewidth=1.5, label=f'Min={tri_angles.min():.1f}°')
|
||
|
|
ax1.set_xlabel('Angle [degrees]')
|
||
|
|
ax1.set_ylabel('Count')
|
||
|
|
ax1.set_title('Triangle: Angle Distribution')
|
||
|
|
ax1.legend()
|
||
|
|
ax1.grid(True, alpha=0.3)
|
||
|
|
|
||
|
|
ax2.hist(gmsh_angles, bins=30, color='green', alpha=0.7, edgecolor='black')
|
||
|
|
ax2.axvline(60, color='green', linestyle='--', linewidth=2, label='Equilateral (60°)')
|
||
|
|
ax2.axvline(gmsh_angles.min(), color='red', linestyle='--', linewidth=1.5, label=f'Min={gmsh_angles.min():.1f}°')
|
||
|
|
ax2.set_xlabel('Angle [degrees]')
|
||
|
|
ax2.set_ylabel('Count')
|
||
|
|
ax2.set_title('Gmsh: Angle Distribution')
|
||
|
|
ax2.legend()
|
||
|
|
ax2.grid(True, alpha=0.3)
|
||
|
|
|
||
|
|
fig.tight_layout()
|
||
|
|
fig.savefig(output_dir / 'angle_distribution.png')
|
||
|
|
plt.close(fig)
|
||
|
|
print(f"Saved: {output_dir / 'angle_distribution.png'}")
|
||
|
|
|
||
|
|
# --- Plot 3: Density heatmap overlay ---
|
||
|
|
X, Y, eta = evaluate_density_grid(geometry, params, resolution=3.0)
|
||
|
|
|
||
|
|
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 7), dpi=160)
|
||
|
|
|
||
|
|
# Triangle + heatmap
|
||
|
|
ax1.pcolormesh(X, Y, eta, shading='auto', cmap='viridis', alpha=0.3, vmin=0, vmax=1)
|
||
|
|
ax1.triplot(tri_verts[:, 0], tri_verts[:, 1], tri_tris, 'k-', linewidth=0.4, alpha=0.6)
|
||
|
|
ax1.set_title('Triangle + Density Field')
|
||
|
|
ax1.set_xlabel('x [mm]')
|
||
|
|
ax1.set_ylabel('y [mm]')
|
||
|
|
ax1.set_aspect('equal')
|
||
|
|
|
||
|
|
# Gmsh + heatmap
|
||
|
|
im = ax2.pcolormesh(X, Y, eta, shading='auto', cmap='viridis', alpha=0.3, vmin=0, vmax=1)
|
||
|
|
ax2.triplot(gmsh_verts[:, 0], gmsh_verts[:, 1], gmsh_tris, 'k-', linewidth=0.4, alpha=0.6)
|
||
|
|
ax2.set_title('Gmsh + Density Field')
|
||
|
|
ax2.set_xlabel('x [mm]')
|
||
|
|
ax2.set_ylabel('y [mm]')
|
||
|
|
ax2.set_aspect('equal')
|
||
|
|
|
||
|
|
# Colorbar
|
||
|
|
fig.colorbar(im, ax=[ax1, ax2], label='Density η', shrink=0.8)
|
||
|
|
|
||
|
|
fig.tight_layout()
|
||
|
|
fig.savefig(output_dir / 'density_overlay.png')
|
||
|
|
plt.close(fig)
|
||
|
|
print(f"Saved: {output_dir / 'density_overlay.png'}")
|
||
|
|
|
||
|
|
# --- Summary stats ---
|
||
|
|
stats = {
|
||
|
|
'triangle': {
|
||
|
|
'num_triangles': int(len(tri_tris)),
|
||
|
|
'num_vertices': int(len(tri_verts)),
|
||
|
|
'min_angle': float(tri_angles.min()),
|
||
|
|
'mean_angle': float(tri_angles.mean()),
|
||
|
|
'max_angle': float(tri_angles.max()),
|
||
|
|
},
|
||
|
|
'gmsh': {
|
||
|
|
'num_triangles': int(len(gmsh_tris)),
|
||
|
|
'num_vertices': int(len(gmsh_verts)),
|
||
|
|
'min_angle': float(gmsh_angles.min()),
|
||
|
|
'mean_angle': float(gmsh_angles.mean()),
|
||
|
|
'max_angle': float(gmsh_angles.max()),
|
||
|
|
},
|
||
|
|
'efficiency_gain_percent': float(100 * (1 - len(gmsh_tris) / max(len(tri_tris), 1))),
|
||
|
|
'min_angle_improvement': float(gmsh_angles.min() - tri_angles.min()),
|
||
|
|
}
|
||
|
|
|
||
|
|
with open(output_dir / 'comparison_stats.json', 'w') as f:
|
||
|
|
json.dump(stats, f, indent=2)
|
||
|
|
print(f"Saved: {output_dir / 'comparison_stats.json'}")
|
||
|
|
|
||
|
|
return stats
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
# Test on small_plate_200x150
|
||
|
|
geometry_file = Path(__file__).parent / 'test_geometries' / 'small_plate_200x150.json'
|
||
|
|
|
||
|
|
with open(geometry_file) as f:
|
||
|
|
raw_geometry = json.load(f)
|
||
|
|
|
||
|
|
geometry = normalize_geometry_schema(raw_geometry)
|
||
|
|
|
||
|
|
params = dict(DEFAULT_PARAMS)
|
||
|
|
params.update({
|
||
|
|
's_min': 12.0,
|
||
|
|
's_max': 35.0,
|
||
|
|
'w_frame': 8.0,
|
||
|
|
'd_keep': 1.5,
|
||
|
|
'R_0': 40.0,
|
||
|
|
'R_edge': 15.0,
|
||
|
|
'lloyd_iterations': 0, # Disable Lloyd for fair comparison
|
||
|
|
})
|
||
|
|
|
||
|
|
output_dir = Path(__file__).parent / 'comparison_output'
|
||
|
|
stats = plot_comparison(geometry, params, output_dir)
|
||
|
|
|
||
|
|
print("\n=== Comparison Summary ===")
|
||
|
|
print(f"Triangle reduction: {stats['efficiency_gain_percent']:.1f}%")
|
||
|
|
print(f"Min angle improvement: {stats['min_angle_improvement']:.1f}°")
|
||
|
|
print(f"\nTriangle: {stats['triangle']['num_triangles']} tris, min angle {stats['triangle']['min_angle']:.1f}°")
|
||
|
|
print(f"Gmsh: {stats['gmsh']['num_triangles']} tris, min angle {stats['gmsh']['min_angle']:.1f}°")
|