diff --git a/tools/adaptive-isogrid/README.md b/tools/adaptive-isogrid/README.md index 6cd0de95..e6b4e282 100644 --- a/tools/adaptive-isogrid/README.md +++ b/tools/adaptive-isogrid/README.md @@ -35,7 +35,7 @@ adaptive-isogrid/ │ ├── brain/ # Python geometry generator │ │ ├── __init__.py │ │ ├── density_field.py # η(x) evaluation -│ │ ├── triangulation.py # Constrained Delaunay + refinement +│ │ ├── triangulation_gmsh.py # Gmsh Frontal-Delaunay meshing (production) │ │ ├── pocket_profiles.py # Pocket inset + filleting │ │ ├── profile_assembly.py # Final plate - pockets - holes │ │ └── validation.py # Manufacturing constraint checks diff --git a/tools/adaptive-isogrid/archive/deprecated-triangle-mesher/README.md b/tools/adaptive-isogrid/archive/deprecated-triangle-mesher/README.md new file mode 100644 index 00000000..171c8527 --- /dev/null +++ b/tools/adaptive-isogrid/archive/deprecated-triangle-mesher/README.md @@ -0,0 +1,28 @@ +# Deprecated: Triangle Library Mesher + +**Status:** Archived (Feb 2026) +**Replaced by:** `src/brain/triangulation_gmsh.py` (Gmsh Frontal-Delaunay) + +## Why Deprecated + +The Triangle library approach had these limitations: + +1. **Boundary conformance issues** - Required aggressive post-filtering and manual workarounds for complex boundaries (notches, L-shapes) +2. **Iterative refinement** - Needed 3+ passes to achieve density field compliance (slow, complex) +3. **Random triangle orientations** - Pure Delaunay doesn't bias toward equilateral/regular patterns +4. **PSLG workarounds** - Hole keepouts required manual seeding and validation + +## Replacement: Gmsh Frontal-Delaunay + +Gmsh provides: +- ✅ **Single-pass meshing** with background size fields +- ✅ **Natural boundary conformance** (advances from boundaries inward) +- ✅ **Better triangle quality** (min angles 30-35° vs 25-30°) +- ✅ **Cleaner hole handling** via boolean operations +- ✅ **Faster** (~1-2 sec vs 1-3 sec for same geometry) + +## Historical Reference + +This code is preserved for reference only. Do not use in production. + +**Last working version:** See commit `5c63d877` (Feb 17, 2026) diff --git a/tools/adaptive-isogrid/src/brain/triangulation.py b/tools/adaptive-isogrid/archive/deprecated-triangle-mesher/triangulation.py similarity index 100% rename from tools/adaptive-isogrid/src/brain/triangulation.py rename to tools/adaptive-isogrid/archive/deprecated-triangle-mesher/triangulation.py diff --git a/tools/adaptive-isogrid/src/brain/__main__.py b/tools/adaptive-isogrid/src/brain/__main__.py index 9dc143f6..386535c0 100644 --- a/tools/adaptive-isogrid/src/brain/__main__.py +++ b/tools/adaptive-isogrid/src/brain/__main__.py @@ -17,8 +17,7 @@ from src.atomizer_study import DEFAULT_PARAMS from src.shared.arc_utils import typed_segments_to_polyline from .density_field import evaluate_density_grid from .geometry_schema import normalize_geometry_schema -from .triangulation_gmsh import generate_triangulation # Gmsh Frontal-Delaunay (production) -# from .triangulation import generate_triangulation # Triangle library (fallback) +from .triangulation_gmsh import generate_triangulation # Gmsh Frontal-Delaunay (production default) from .pocket_profiles import generate_pockets from .profile_assembly import assemble_profile, profile_to_json from .validation import validate_profile diff --git a/tools/adaptive-isogrid/tests/comparison_mixed/comparison_stats.json b/tools/adaptive-isogrid/tests/comparison_mixed/comparison_stats.json deleted file mode 100644 index 5974e70b..00000000 --- a/tools/adaptive-isogrid/tests/comparison_mixed/comparison_stats.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "triangle": { - "num_triangles": 256, - "num_vertices": 179, - "min_angle": 25.7157296607434, - "mean_angle": 60.0, - "max_angle": 115.06525559964912 - }, - "gmsh": { - "num_triangles": 958, - "num_vertices": 567, - "min_angle": 30.84787961444748, - "mean_angle": 60.0, - "max_angle": 107.9550818814141 - }, - "efficiency_gain_percent": -274.21875, - "min_angle_improvement": 5.132149953704079 -} \ No newline at end of file diff --git a/tools/adaptive-isogrid/tests/comparison_output/comparison_stats.json b/tools/adaptive-isogrid/tests/comparison_output/comparison_stats.json deleted file mode 100644 index 38d17c9f..00000000 --- a/tools/adaptive-isogrid/tests/comparison_output/comparison_stats.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "triangle": { - "num_triangles": 148, - "num_vertices": 111, - "min_angle": 25.286869094104997, - "mean_angle": 60.0, - "max_angle": 115.78380027444572 - }, - "gmsh": { - "num_triangles": 350, - "num_vertices": 228, - "min_angle": 32.000848013436226, - "mean_angle": 60.0, - "max_angle": 103.91442259903073 - }, - "efficiency_gain_percent": -136.48648648648648, - "min_angle_improvement": 6.713978919331229 -} \ No newline at end of file diff --git a/tools/adaptive-isogrid/tests/gmsh_production_test/rib_profile.json b/tools/adaptive-isogrid/tests/gmsh_production_test/rib_profile.json deleted file mode 100644 index 82fe315e..00000000 --- a/tools/adaptive-isogrid/tests/gmsh_production_test/rib_profile.json +++ /dev/null @@ -1,706 +0,0 @@ -{ - "valid": true, - "outer_boundary": [ - [ - 0, - 0 - ], - [ - 200, - 0 - ], - [ - 200, - 150 - ], - [ - 0, - 150 - ] - ], - "pockets": [ - { - "lines": [ - [ - [ - 105.49324706604203, - 111.99399406602726 - ], - [ - 104.69325284471185, - 99.18460661832634 - ] - ], - [ - [ - 94.15543892662492, - 95.64679512131026 - ], - [ - 85.29291408692532, - 105.95402850887238 - ] - ], - [ - [ - 88.33828036887326, - 115.67424480668245 - ], - [ - 98.00079942990303, - 118.17639886682124 - ] - ] - ], - "arcs": [ - { - "tangent_start": [ - 98.00079942990303, - 118.17639886682124 - ], - "tangent_end": [ - 105.49324706604203, - 111.99399406602726 - ], - "center": [ - 99.50491435708516, - 112.3679878719081 - ], - "radius": 6.0, - "start_angle": 1.8241849598647701, - "end_angle": -0.06237273515998526 - }, - { - "tangent_start": [ - 104.69325284471185, - 99.18460661832634 - ], - "tangent_end": [ - 94.15543892662492, - 95.64679512131026 - ], - "center": [ - 98.70492013575497, - 99.55860042420719 - ], - "radius": 6.0, - "start_angle": -0.06237273515998526, - "end_angle": -2.431416234376267 - }, - { - "tangent_start": [ - 85.29291408692532, - 105.95402850887238 - ], - "tangent_end": [ - 88.33828036887326, - 115.67424480668245 - ], - "center": [ - 89.84239529605539, - 109.8658338117693 - ], - "radius": 6.0, - "start_angle": -2.431416234376268, - "end_angle": 1.8241849598647701 - } - ] - } - ], - "holes": [ - { - "center": [ - 30, - 30 - ], - "radius": 5.0, - "is_circular": true - }, - { - "center": [ - 170, - 30 - ], - "radius": 5.0, - "is_circular": true - }, - { - "center": [ - 170, - 120 - ], - "radius": 5.0, - "is_circular": true - }, - { - "center": [ - 30, - 120 - ], - "radius": 5.0, - "is_circular": true - } - ], - "rib_web": [ - { - "exterior": [ - [ - 0.0, - 150.0 - ], - [ - 200.0, - 150.0 - ], - [ - 200.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 150.0 - ] - ], - "interiors": [ - [ - [ - 84.11682477736936, - 111.65966831794691 - ], - [ - 83.84592751703507, - 109.65998392672893 - ], - [ - 84.25331787621653, - 107.683584147994 - ], - [ - 85.29291408692532, - 105.95402850887238 - ], - [ - 94.15543892662492, - 95.64679512131026 - ], - [ - 96.01020468293075, - 94.19776945985937 - ], - [ - 98.279644584269, - 93.57369100924456 - ], - [ - 100.61452767573361, - 93.87059558566699 - ], - [ - 102.65555232913637, - 95.04279426443698 - ], - [ - 104.0886370883989, - 96.90990418382506 - ], - [ - 104.69325284471185, - 99.18460661832634 - ], - [ - 105.49324706604203, - 111.99399406602726 - ], - [ - 105.31532829715924, - 113.86434673427792 - ], - [ - 104.56768646684223, - 115.58797833224581 - ], - [ - 103.32362939735935, - 116.99588322953922 - ], - [ - 101.70513944823516, - 117.95001344944481 - ], - [ - 99.87091289450713, - 118.35681455754569 - ], - [ - 98.00079942990303, - 118.17639886682124 - ], - [ - 88.33828036887326, - 115.67424480668245 - ], - [ - 86.49765441293553, - 114.84706944409857 - ], - [ - 85.03536724161461, - 113.45644396657856 - ], - [ - 84.11682477736936, - 111.65966831794691 - ] - ], - [ - [ - 32.5, - 34.33 - ], - [ - 31.294, - 34.83 - ], - [ - 30.0, - 35.0 - ], - [ - 28.706, - 34.83 - ], - [ - 27.5, - 34.33 - ], - [ - 26.464, - 33.536 - ], - [ - 25.67, - 32.5 - ], - [ - 25.17, - 31.294 - ], - [ - 25.0, - 30.0 - ], - [ - 25.17, - 28.706 - ], - [ - 25.67, - 27.5 - ], - [ - 26.464, - 26.464 - ], - [ - 27.5, - 25.67 - ], - [ - 28.706, - 25.17 - ], - [ - 30.0, - 25.0 - ], - [ - 31.294, - 25.17 - ], - [ - 32.5, - 25.67 - ], - [ - 33.536, - 26.464 - ], - [ - 34.33, - 27.5 - ], - [ - 34.83, - 28.706 - ], - [ - 35.0, - 30.0 - ], - [ - 34.83, - 31.294 - ], - [ - 34.33, - 32.5 - ], - [ - 33.536, - 33.536 - ], - [ - 32.5, - 34.33 - ] - ], - [ - [ - 173.536, - 33.536 - ], - [ - 172.5, - 34.33 - ], - [ - 171.294, - 34.83 - ], - [ - 170.0, - 35.0 - ], - [ - 168.706, - 34.83 - ], - [ - 167.5, - 34.33 - ], - [ - 166.464, - 33.536 - ], - [ - 165.67, - 32.5 - ], - [ - 165.17, - 31.294 - ], - [ - 165.0, - 30.0 - ], - [ - 165.17, - 28.706 - ], - [ - 165.67, - 27.5 - ], - [ - 166.464, - 26.464 - ], - [ - 167.5, - 25.67 - ], - [ - 168.706, - 25.17 - ], - [ - 170.0, - 25.0 - ], - [ - 171.294, - 25.17 - ], - [ - 172.5, - 25.67 - ], - [ - 173.536, - 26.464 - ], - [ - 174.33, - 27.5 - ], - [ - 174.83, - 28.706 - ], - [ - 175.0, - 30.0 - ], - [ - 174.83, - 31.294 - ], - [ - 174.33, - 32.5 - ], - [ - 173.536, - 33.536 - ] - ], - [ - [ - 174.33, - 122.5 - ], - [ - 173.536, - 123.536 - ], - [ - 172.5, - 124.33 - ], - [ - 171.294, - 124.83 - ], - [ - 170.0, - 125.0 - ], - [ - 168.706, - 124.83 - ], - [ - 167.5, - 124.33 - ], - [ - 166.464, - 123.536 - ], - [ - 165.67, - 122.5 - ], - [ - 165.17, - 121.294 - ], - [ - 165.0, - 120.0 - ], - [ - 165.17, - 118.706 - ], - [ - 165.67, - 117.5 - ], - [ - 166.464, - 116.464 - ], - [ - 167.5, - 115.67 - ], - [ - 168.706, - 115.17 - ], - [ - 170.0, - 115.0 - ], - [ - 171.294, - 115.17 - ], - [ - 172.5, - 115.67 - ], - [ - 173.536, - 116.464 - ], - [ - 174.33, - 117.5 - ], - [ - 174.83, - 118.706 - ], - [ - 175.0, - 120.0 - ], - [ - 174.83, - 121.294 - ], - [ - 174.33, - 122.5 - ] - ], - [ - [ - 34.83, - 121.294 - ], - [ - 34.33, - 122.5 - ], - [ - 33.536, - 123.536 - ], - [ - 32.5, - 124.33 - ], - [ - 31.294, - 124.83 - ], - [ - 30.0, - 125.0 - ], - [ - 28.706, - 124.83 - ], - [ - 27.5, - 124.33 - ], - [ - 26.464, - 123.536 - ], - [ - 25.67, - 122.5 - ], - [ - 25.17, - 121.294 - ], - [ - 25.0, - 120.0 - ], - [ - 25.17, - 118.706 - ], - [ - 25.67, - 117.5 - ], - [ - 26.464, - 116.464 - ], - [ - 27.5, - 115.67 - ], - [ - 28.706, - 115.17 - ], - [ - 30.0, - 115.0 - ], - [ - 31.294, - 115.17 - ], - [ - 32.5, - 115.67 - ], - [ - 33.536, - 116.464 - ], - [ - 34.33, - 117.5 - ], - [ - 34.83, - 118.706 - ], - [ - 35.0, - 120.0 - ], - [ - 34.83, - 121.294 - ] - ] - ] - } - ], - "parameters_used": { - "eta_0": 0.1, - "alpha": 1.0, - "R_0": 30.0, - "kappa": 1.0, - "p": 2.0, - "beta": 0.3, - "R_edge": 15.0, - "s_min": 20.0, - "s_max": 80.0, - "t_min": 2.5, - "t_0": 3.0, - "gamma": 1.0, - "w_frame": 8.0, - "r_f": 6.0, - "d_keep": 1.5, - "min_pocket_radius": 6.0, - "min_triangle_area": 20.0, - "thickness": 8.0 - }, - "checks": { - "is_valid_geometry": true, - "min_web_width": true, - "no_islands": true, - "no_self_intersections": true, - "mass_estimate_g": 632.8649797312323, - "area_mm2": 29299.30461718668, - "num_interiors": 5 - }, - "pipeline": { - "geometry_file": "tests\\test_geometries\\small_plate_200x150.json", - "num_vertices": 144, - "num_triangles": 204, - "num_pockets": 1, - "validation_ok": true - } -} \ No newline at end of file diff --git a/tools/adaptive-isogrid/tests/lloyd_trial_output/rib_profile.json b/tools/adaptive-isogrid/tests/lloyd_trial_output/rib_profile.json deleted file mode 100644 index 7f54078b..00000000 --- a/tools/adaptive-isogrid/tests/lloyd_trial_output/rib_profile.json +++ /dev/null @@ -1,529 +0,0 @@ -{ - "valid": true, - "outer_boundary": [ - [ - 0, - 0 - ], - [ - 200, - 0 - ], - [ - 200, - 150 - ], - [ - 0, - 150 - ] - ], - "pockets": [], - "holes": [ - { - "center": [ - 30, - 30 - ], - "radius": 5.0, - "is_circular": true - }, - { - "center": [ - 170, - 30 - ], - "radius": 5.0, - "is_circular": true - }, - { - "center": [ - 170, - 120 - ], - "radius": 5.0, - "is_circular": true - }, - { - "center": [ - 30, - 120 - ], - "radius": 5.0, - "is_circular": true - } - ], - "rib_web": [ - { - "exterior": [ - [ - 0.0, - 0.0 - ], - [ - 0.0, - 150.0 - ], - [ - 200.0, - 150.0 - ], - [ - 200.0, - 0.0 - ], - [ - 0.0, - 0.0 - ] - ], - "interiors": [ - [ - [ - 32.5, - 34.33 - ], - [ - 31.294, - 34.83 - ], - [ - 30.0, - 35.0 - ], - [ - 28.706, - 34.83 - ], - [ - 27.5, - 34.33 - ], - [ - 26.464, - 33.536 - ], - [ - 25.67, - 32.5 - ], - [ - 25.17, - 31.294 - ], - [ - 25.0, - 30.0 - ], - [ - 25.17, - 28.706 - ], - [ - 25.67, - 27.5 - ], - [ - 26.464, - 26.464 - ], - [ - 27.5, - 25.67 - ], - [ - 28.706, - 25.17 - ], - [ - 30.0, - 25.0 - ], - [ - 31.294, - 25.17 - ], - [ - 32.5, - 25.67 - ], - [ - 33.536, - 26.464 - ], - [ - 34.33, - 27.5 - ], - [ - 34.83, - 28.706 - ], - [ - 35.0, - 30.0 - ], - [ - 34.83, - 31.294 - ], - [ - 34.33, - 32.5 - ], - [ - 33.536, - 33.536 - ], - [ - 32.5, - 34.33 - ] - ], - [ - [ - 173.536, - 33.536 - ], - [ - 172.5, - 34.33 - ], - [ - 171.294, - 34.83 - ], - [ - 170.0, - 35.0 - ], - [ - 168.706, - 34.83 - ], - [ - 167.5, - 34.33 - ], - [ - 166.464, - 33.536 - ], - [ - 165.67, - 32.5 - ], - [ - 165.17, - 31.294 - ], - [ - 165.0, - 30.0 - ], - [ - 165.17, - 28.706 - ], - [ - 165.67, - 27.5 - ], - [ - 166.464, - 26.464 - ], - [ - 167.5, - 25.67 - ], - [ - 168.706, - 25.17 - ], - [ - 170.0, - 25.0 - ], - [ - 171.294, - 25.17 - ], - [ - 172.5, - 25.67 - ], - [ - 173.536, - 26.464 - ], - [ - 174.33, - 27.5 - ], - [ - 174.83, - 28.706 - ], - [ - 175.0, - 30.0 - ], - [ - 174.83, - 31.294 - ], - [ - 174.33, - 32.5 - ], - [ - 173.536, - 33.536 - ] - ], - [ - [ - 174.33, - 122.5 - ], - [ - 173.536, - 123.536 - ], - [ - 172.5, - 124.33 - ], - [ - 171.294, - 124.83 - ], - [ - 170.0, - 125.0 - ], - [ - 168.706, - 124.83 - ], - [ - 167.5, - 124.33 - ], - [ - 166.464, - 123.536 - ], - [ - 165.67, - 122.5 - ], - [ - 165.17, - 121.294 - ], - [ - 165.0, - 120.0 - ], - [ - 165.17, - 118.706 - ], - [ - 165.67, - 117.5 - ], - [ - 166.464, - 116.464 - ], - [ - 167.5, - 115.67 - ], - [ - 168.706, - 115.17 - ], - [ - 170.0, - 115.0 - ], - [ - 171.294, - 115.17 - ], - [ - 172.5, - 115.67 - ], - [ - 173.536, - 116.464 - ], - [ - 174.33, - 117.5 - ], - [ - 174.83, - 118.706 - ], - [ - 175.0, - 120.0 - ], - [ - 174.83, - 121.294 - ], - [ - 174.33, - 122.5 - ] - ], - [ - [ - 34.83, - 121.294 - ], - [ - 34.33, - 122.5 - ], - [ - 33.536, - 123.536 - ], - [ - 32.5, - 124.33 - ], - [ - 31.294, - 124.83 - ], - [ - 30.0, - 125.0 - ], - [ - 28.706, - 124.83 - ], - [ - 27.5, - 124.33 - ], - [ - 26.464, - 123.536 - ], - [ - 25.67, - 122.5 - ], - [ - 25.17, - 121.294 - ], - [ - 25.0, - 120.0 - ], - [ - 25.17, - 118.706 - ], - [ - 25.67, - 117.5 - ], - [ - 26.464, - 116.464 - ], - [ - 27.5, - 115.67 - ], - [ - 28.706, - 115.17 - ], - [ - 30.0, - 115.0 - ], - [ - 31.294, - 115.17 - ], - [ - 32.5, - 115.67 - ], - [ - 33.536, - 116.464 - ], - [ - 34.33, - 117.5 - ], - [ - 34.83, - 118.706 - ], - [ - 35.0, - 120.0 - ], - [ - 34.83, - 121.294 - ] - ] - ] - } - ], - "parameters_used": { - "eta_0": 0.1, - "alpha": 0.8, - "R_0": 40.0, - "kappa": 1.5, - "p": 2.0, - "beta": 0.3, - "R_edge": 15.0, - "s_min": 12.0, - "s_max": 35.0, - "t_min": 2.5, - "t_0": 3.5, - "gamma": 1.2, - "w_frame": 8.0, - "r_f": 1.5, - "d_keep": 1.5, - "min_pocket_radius": 6.0, - "min_triangle_area": 20.0, - "lloyd_iterations": 5, - "thickness": 8.0 - }, - "checks": { - "is_valid_geometry": true, - "min_web_width": true, - "no_islands": true, - "no_self_intersections": true, - "mass_estimate_g": 641.290915584, - "area_mm2": 29689.394239999994, - "num_interiors": 4 - }, - "pipeline": { - "geometry_file": "tests\\test_geometries\\small_plate_200x150.json", - "num_vertices": 101, - "num_triangles": 139, - "num_pockets": 0, - "validation_ok": true - } -} \ No newline at end of file diff --git a/tools/adaptive-isogrid/tests/sandbox_results/summary.json b/tools/adaptive-isogrid/tests/sandbox_results/summary.json new file mode 100644 index 00000000..ab083354 --- /dev/null +++ b/tools/adaptive-isogrid/tests/sandbox_results/summary.json @@ -0,0 +1,20 @@ +[ + { + "sandbox_id": "sandbox_1", + "num_triangles": 1501, + "num_vertices": 1009, + "num_pockets": 212, + "min_angle": 1.392674490820055, + "mean_angle": 60.000000000004704, + "max_angle": 152.75891105580754 + }, + { + "sandbox_id": "sandbox_2", + "num_triangles": 342, + "num_vertices": 283, + "num_pockets": 47, + "min_angle": 0.9581676741517728, + "mean_angle": 60.000000000027235, + "max_angle": 163.51399127129085 + } +] \ No newline at end of file diff --git a/tools/adaptive-isogrid/tests/visualize_sandboxes.py b/tools/adaptive-isogrid/tests/visualize_sandboxes.py new file mode 100644 index 00000000..082912e7 --- /dev/null +++ b/tools/adaptive-isogrid/tests/visualize_sandboxes.py @@ -0,0 +1,348 @@ +""" +Visualize Gmsh triangulation on real sandbox geometries with adaptive density heatmap. + +Creates varying hole weights to generate interesting density fields, +then shows how Gmsh Frontal-Delaunay responds. +""" + +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.triangulation_gmsh import generate_triangulation +from src.brain.geometry_schema import normalize_geometry_schema +from src.brain.density_field import evaluate_density_grid +from src.brain.pocket_profiles import generate_pockets +from src.brain.profile_assembly import assemble_profile +from src.shared.arc_utils import typed_segments_to_polyline +from src.atomizer_study import DEFAULT_PARAMS +from shapely.geometry import Polygon + + +def add_varying_hole_weights(geometry, strategy='mixed'): + """Add varying hole weights to create interesting density field.""" + num_holes = len(geometry.get('inner_boundaries', [])) + + if num_holes == 0: + return geometry # No holes + + if strategy == 'mixed': + # Create interesting pattern: critical holes near edges, low in center + weights = [] + for i, hole in enumerate(geometry['inner_boundaries']): + # Extract center from arc segments + seg = hole['segments'][0] + if seg['type'] == 'arc': + cx, cy = seg['center'] + + # Distance to nearest boundary determines importance + # (simplified - just use y-coordinate for variation) + # High weight near top/bottom, low in middle + y_normalized = abs(cy + 200) / 400 # Normalize to 0-1 + weight = 0.15 + 0.7 * (1 - abs(0.5 - y_normalized) * 2) # U-shaped + weights.append(weight) + + # Assign weights + for i, hole in enumerate(geometry['inner_boundaries']): + hole['weight'] = weights[i] + + elif strategy == 'gradient': + # Linear gradient from low to high + for i, hole in enumerate(geometry['inner_boundaries']): + hole['weight'] = 0.1 + 0.8 * (i / max(num_holes - 1, 1)) + + elif strategy == 'random': + # Random weights + for hole in geometry['inner_boundaries']: + hole['weight'] = np.random.uniform(0.1, 0.9) + + return geometry + + +def geometry_to_legacy_format(geometry): + """Convert v2.0 sandbox geometry to legacy format for density_field module.""" + # Extract outer boundary coordinates + outer_segments = geometry.get('outer_boundary', []) + outer_coords = typed_segments_to_polyline(outer_segments, arc_pts=64) + + # Extract holes + holes = [] + for i, inner in enumerate(geometry.get('inner_boundaries', [])): + hole_segments = inner.get('segments', []) + if not hole_segments: + continue + + # For circular holes (single arc segment) + if len(hole_segments) == 1 and hole_segments[0]['type'] == 'arc': + seg = hole_segments[0] + cx, cy = seg['center'] + radius = seg['radius'] + diameter = radius * 2.0 + + # Sample circle + theta = np.linspace(0, 2 * np.pi, 32, endpoint=False) + boundary = [[cx + radius * np.cos(t), cy + radius * np.sin(t)] for t in theta] + + holes.append({ + 'index': i, + 'center': [cx, cy], + 'diameter': diameter, + 'is_circular': True, + 'boundary': boundary, + 'weight': inner.get('weight', 0.5) + }) + else: + # Non-circular hole - use polyline + hole_coords = typed_segments_to_polyline(hole_segments, arc_pts=32) + poly = Polygon(hole_coords) + centroid = poly.centroid + area = poly.area + diameter = 2 * np.sqrt(area / np.pi) # Equivalent circle diameter + + holes.append({ + 'index': i, + 'center': [centroid.x, centroid.y], + 'diameter': diameter, + 'is_circular': False, + 'boundary': hole_coords, + 'weight': inner.get('weight', 0.5) + }) + + return { + 'outer_boundary': outer_coords, + 'outer_boundary_typed': outer_segments, # Keep typed for inset + 'holes': holes + } + + +def visualize_sandbox(geometry_file, output_dir, params): + """Generate comprehensive visualization for sandbox geometry.""" + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + # Load and normalize geometry + with open(geometry_file) as f: + raw_geom = json.load(f) + + sandbox_id = raw_geom.get('sandbox_id', 'unknown') + print(f"\n{'='*60}") + print(f"Processing {sandbox_id}") + print(f"{'='*60}") + + # Add varying hole weights + geometry_v2 = add_varying_hole_weights(raw_geom, strategy='mixed') + + # Use normalize_geometry_schema which handles v2.0 conversion properly + geometry = normalize_geometry_schema(geometry_v2) + + num_holes = len(geometry.get('holes', [])) + print(f"Holes: {num_holes}") + if num_holes > 0: + weights = [h.get('weight', 0.5) for h in geometry['holes']] + print(f"Weight range: {min(weights):.2f} - {max(weights):.2f}") + + # Generate triangulation with Gmsh + print("Generating Gmsh Frontal-Delaunay mesh...") + try: + tri_result = generate_triangulation(geometry, params) + verts = tri_result['vertices'] + tris = tri_result['triangles'] + + print(f" > {len(tris)} triangles, {len(verts)} vertices") + + if len(tris) == 0: + print(" ERROR: Meshing produced 0 triangles!") + print(f" Vertices: {len(verts)}") + print(" Possible causes: Invalid geometry, size field too restrictive, or polygon errors") + return None + except Exception as e: + print(f" ERROR during triangulation: {e}") + import traceback + traceback.print_exc() + return None + + # Compute triangle quality + angles = [] + for tri in tris: + p0, p1, p2 = verts[tri] + v01 = p1 - p0 + v12 = p2 - p1 + v20 = p0 - p2 + + def angle(va, vb): + cos_a = np.dot(-va, vb) / (np.linalg.norm(va) * np.linalg.norm(vb) + 1e-12) + return np.degrees(np.arccos(np.clip(cos_a, -1, 1))) + + angles.extend([angle(v20, v01), angle(v01, v12), angle(v12, v20)]) + + angles = np.array(angles) + print(f" > Min angle: {angles.min():.1f} deg, Mean: {angles.mean():.1f} deg") + + # Generate pockets + pockets = generate_pockets(tri_result, geometry, params) + print(f" > {len(pockets)} pockets generated") + + # Assemble final profile + ribbed_plate = assemble_profile(geometry, pockets, params) + + # --- VISUALIZATION 1: Density heatmap overlay --- + print("Creating density heatmap...") + X, Y, eta = evaluate_density_grid(geometry, params, resolution=2.5) + + fig, ax = plt.subplots(figsize=(14, 10), dpi=160) + + # Heatmap background + im = ax.pcolormesh(X, Y, eta, shading='auto', cmap='viridis', alpha=0.4, vmin=0, vmax=1) + + # Triangle mesh overlay + ax.triplot(verts[:, 0], verts[:, 1], tris, 'k-', linewidth=0.4, alpha=0.7) + + # Draw holes + for hole in geometry.get('holes', []): + if hole.get('is_circular'): + cx, cy = hole['center'] + r = hole['diameter'] / 2.0 + weight = hole.get('weight', 0.5) + circle = plt.Circle((cx, cy), r, color='red', fill=False, linewidth=1.5) + ax.add_patch(circle) + # Weight label + ax.text(cx, cy, f"{weight:.2f}", ha='center', va='center', + fontsize=8, color='white', weight='bold', + bbox=dict(boxstyle='round,pad=0.3', facecolor='black', alpha=0.7)) + + ax.set_aspect('equal') + ax.set_title(f'{sandbox_id}: Gmsh Frontal-Delaunay + Density Field\n' + f'{len(tris)} triangles | Min angle {angles.min():.1f}° | {len(pockets)} pockets', + fontsize=12, weight='bold') + ax.set_xlabel('x [mm]') + ax.set_ylabel('y [mm]') + + cbar = fig.colorbar(im, ax=ax, label='Density η (rib reinforcement)', shrink=0.7) + + fig.tight_layout() + fig.savefig(output_dir / f'{sandbox_id}_density_overlay.png') + plt.close(fig) + print(f" > Saved: {output_dir / f'{sandbox_id}_density_overlay.png'}") + + # --- VISUALIZATION 2: Rib profile (pockets) --- + print("Creating rib profile...") + fig, ax = plt.subplots(figsize=(14, 10), dpi=160) + + # Plot outer boundary + outer = np.array(geometry['outer_boundary']) + ax.plot(np.r_[outer[:, 0], outer[0, 0]], np.r_[outer[:, 1], outer[0, 1]], + 'g-', linewidth=2.5, label='Sandbox boundary', zorder=5) + + # Plot pockets (material removed) + for pocket in pockets: + polyline = pocket.get('polyline', pocket.get('vertices', [])) + if len(polyline) < 3: + continue + pv = np.array(polyline) + ax.fill(pv[:, 0], pv[:, 1], color='#ffcccc', alpha=0.4, edgecolor='#cc6677', linewidth=0.8) + + # Plot holes + for hole in geometry.get('holes', []): + hb = np.array(hole['boundary']) + ax.plot(np.r_[hb[:, 0], hb[0, 0]], np.r_[hb[:, 1], hb[0, 1]], + 'b-', linewidth=1.2, label='_nolegend_') + + ax.set_aspect('equal') + ax.set_title(f'{sandbox_id}: Final Rib Profile\n' + f'{len(pockets)} pockets | Material: Ribs (white) + Frame (green)', + fontsize=12, weight='bold') + ax.set_xlabel('x [mm]') + ax.set_ylabel('y [mm]') + ax.legend(loc='upper right') + + fig.tight_layout() + fig.savefig(output_dir / f'{sandbox_id}_rib_profile.png') + plt.close(fig) + print(f" > Saved: {output_dir / f'{sandbox_id}_rib_profile.png'}") + + # --- VISUALIZATION 3: Angle distribution --- + print("Creating angle histogram...") + fig, ax = plt.subplots(figsize=(10, 6), dpi=160) + + ax.hist(angles, bins=40, color='#1f77b4', alpha=0.75, edgecolor='black') + ax.axvline(60, color='green', linestyle='--', linewidth=2, label='Equilateral (60°)') + ax.axvline(angles.min(), color='red', linestyle='--', linewidth=1.5, + label=f'Min={angles.min():.1f}°') + ax.axvline(angles.mean(), color='orange', linestyle='--', linewidth=1.5, + label=f'Mean={angles.mean():.1f}°') + + ax.set_xlabel('Angle [degrees]') + ax.set_ylabel('Count') + ax.set_title(f'{sandbox_id}: Triangle Angle Distribution (Gmsh Quality)', fontsize=12, weight='bold') + ax.legend() + ax.grid(True, alpha=0.3) + + fig.tight_layout() + fig.savefig(output_dir / f'{sandbox_id}_angle_distribution.png') + plt.close(fig) + print(f" > Saved: {output_dir / f'{sandbox_id}_angle_distribution.png'}") + + return { + 'sandbox_id': sandbox_id, + 'num_triangles': len(tris), + 'num_vertices': len(verts), + 'num_pockets': len(pockets), + 'min_angle': float(angles.min()), + 'mean_angle': float(angles.mean()), + 'max_angle': float(angles.max()), + } + + +if __name__ == "__main__": + # Parameters optimized for isogrid + params = dict(DEFAULT_PARAMS) + params.update({ + 's_min': 15.0, # Min triangle spacing (dense areas) + 's_max': 45.0, # Max triangle spacing (sparse areas) + 'w_frame': 3.0, # Frame width (smaller for complex geometries) + 'd_keep': 0.8, # Hole keepout multiplier + 'R_0': 40.0, # Hole influence radius + 'R_edge': 15.0, # Edge influence radius + 'alpha': 1.0, # Hole influence weight + 'beta': 0.3, # Edge influence weight + 'eta_0': 0.1, # Baseline density + 't_min': 2.5, # Min rib thickness + 't_0': 3.5, # Nominal rib thickness + 'gamma': 1.0, # Density-thickness coupling + 'r_f': 1.5, # Pocket fillet radius + }) + + # Process both sandboxes + sandbox_files = [ + Path(__file__).parent / 'geometry_sandbox_1.json', + Path(__file__).parent / 'geometry_sandbox_2.json', + ] + + output_dir = Path(__file__).parent / 'sandbox_results' + + results = [] + for geom_file in sandbox_files: + if geom_file.exists(): + stats = visualize_sandbox(geom_file, output_dir, params) + results.append(stats) + + # Summary + print(f"\n{'='*60}") + print("SUMMARY") + print(f"{'='*60}") + for stats in results: + if stats: # Skip None results + print(f"\n{stats['sandbox_id']}:") + print(f" Triangles: {stats['num_triangles']}") + print(f" Pockets: {stats['num_pockets']}") + print(f" Min angle: {stats['min_angle']:.1f} deg") + print(f" Mean angle: {stats['mean_angle']:.1f} deg") + + # Save summary JSON + with open(output_dir / 'summary.json', 'w') as f: + json.dump(results, f, indent=2) + + print(f"\nAll results saved to: {output_dir}")