# Adaptive Isogrid Plate Lightweighting — Technical Specification ## System Architecture: "Python Brain + NX Hands + Atomizer Manager" **Author:** Atomaste Solution **Date:** February 2026 **Status:** Architecture Locked — Ready for Implementation --- ## 1. Project Summary ### What We're Building A semi-automated tool that takes a plate with holes, generates an optimally lightweighted isogrid pattern, and produces a manufacturing-ready geometry. The isogrid density varies across the plate based on hole importance, edge proximity, and optimization-driven meta-parameters. ### Architecture Decision Record After extensive brainstorming, the following decisions are locked: | Decision | Choice | Rationale | | ------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------- | | Geometry generation | External Python (Constrained Delaunay) | Full access to scipy/triangle/gmsh, debuggable, fast | | FEA strategy | Reserved-region monolithic remesh | Keep load/BC topology stable while allowing local rib updates | | FEA solver | NX Simcenter + Nastran (2D shell) | Existing expertise, handles complex BCs, extensible to modal/buckling | | NX role | Extract sandbox faces, reimport profile, remesh + solve | Reserved regions preserve associations; no assembly merge pipeline needed | | Optimization | Atomizer (Optuna TPE), pure parametric v1 | One FEA per trial, ~2 min/iteration, stress feedback deferred to v2 | | Geometry transfer | JSON-only round trip | Deterministic, scriptable, no DXF/STEP conversion drift | | Plate type | Flat, 200–600 mm scale, 6–15 mm thick, 16–30 holes | Shell elements appropriate, fast solves | ### System Diagram ``` ONE-TIME SETUP ══════════════ User in NX: ├── Partition plate mid-surface into regions: │ ├── sandbox face(s): ISOGRID_SANDBOX = sandbox_1, sandbox_2, ... │ └── reserved regions: edges/functional zones to stay untouched ├── Assign hole weights (interactive UI or table) └── Verify baseline solve and saved solution setup OPTIMIZATION LOOP (~2 min/iteration) ════════════════════════════════════ Atomizer (Optuna TPE) │ │ Samples meta-parameters │ (η₀, α, β, p, R₀, κ, s_min, s_max, t_min, t₀, γ, w_frame, r_f, ...) │ ▼ NXOpen Extractor (~seconds) ├── Find all sandbox faces by attribute ├── Extract each sandbox loop set to local 2D └── Write geometry_sandbox_n.json │ ▼ External Python — "The Brain" (~1-3 sec) ├── Load geometry_sandbox_n.json ├── Compute density field η(x) ├── Generate constrained Delaunay triangulation ├── Apply manufacturing constraints └── Write rib_profile_sandbox_n.json │ ▼ NXOpen Import + Solve (~60-90 sec) ├── Replace sandbox geometry only ├── Keep reserved regions unchanged (loads/BCs persist) ├── Remesh full plate as monolithic model ├── Solve (Nastran) └── Extract results.json (stress/disp/strain/mass) │ ▼ Atomizer updates surrogate, samples next trial Repeat until convergence (~500-2000 trials) ``` ### Key Architectural Insight: Why Reserved Regions The plate lightweighting problem has a natural separation: **What should stay fixed:** load/BC attachment topology around edges, holes, and functional interfaces. **What should evolve:** the rib network inside the designable interior. The reserved-region workflow maps directly to this separation. By only replacing sandbox faces, all references in reserved zones remain valid while the interior rib topology changes every iteration. This means: - Loads and BCs persist naturally because reserved geometry is untouched - No multi-model coupling layer or interface node reconciliation workflow - Full-plate remesh remains straightforward and robust - Geometry extraction runs each iteration, enabling hole-position optimization --- ## 2. One-Time Setup: Geometry Extraction from NX ### 2.1 What the User Does The user opens their plate model in NX and runs a setup script (NXOpen Python). The interaction is: 1. **Select the plate face** — click the top (or bottom) face of the plate. The tool reads this face's topology. 2. **Hole classification** — the tool lists all inner loops (holes) found on the face, showing each hole's center, diameter, and a preview highlight. The user assigns each hole a weight from 0.0 (ignore — just avoid it) to 1.0 (critical — maximum reinforcement). Grouping by class (A/B/C) is optional; raw per-hole weights work fine since they're not optimization variables. 3. **Review** — the tool displays the extracted boundary and holes overlaid on the model for visual confirmation. 4. **Export** — writes `geometry.json` containing everything the Python brain needs. ### 2.2 Geometry Extraction Logic (NXOpen) The plate face in NX is a B-rep face bounded by edge loops. Extraction pseudocode: ```python # NXOpen extraction script (runs inside NX) import NXOpen import json def extract_plate_geometry(face, hole_weights): """ face: NXOpen.Face — the selected plate face hole_weights: dict — {loop_index: weight} from user input Returns: geometry dict for export """ # Get all edge loops on this face loops = face.GetLoops() geometry = { 'outer_boundary': None, 'holes': [], 'face_normal': None, 'thickness': None # can be read from plate body } for loop in loops: edges = loop.GetEdges() # Sample each edge as a polyline points = [] for edge in edges: # Get edge curve, sample at intervals pts = sample_edge(edge, tolerance=0.1) # 0.1 mm chord tol points.extend(pts) if loop.IsOuter(): geometry['outer_boundary'] = points else: # Inner loop = hole center, diameter = fit_circle(points) # for circular holes hole_idx = len(geometry['holes']) geometry['holes'].append({ 'index': hole_idx, 'boundary': points, # actual boundary polyline 'center': center, # [x, y] 'diameter': diameter, # mm (None if non-circular) 'is_circular': is_circle(points, tolerance=0.5), 'weight': hole_weights.get(hole_idx, 0.0) }) # Get plate thickness from body geometry['thickness'] = measure_plate_thickness(face) # Get face normal and establish XY coordinate system geometry['face_normal'] = get_face_normal(face) geometry['transform'] = get_face_to_xy_transform(face) return geometry def export_geometry(geometry, filepath='geometry.json'): with open(filepath, 'w') as f: json.dump(geometry, f, indent=2) ``` ### 2.3 What geometry.json Contains ```json { "plate_id": "bracket_v3", "units": "mm", "thickness": 10.0, "material": "AL6061-T6", "outer_boundary": [[0,0], [400,0], [400,300], [0,300]], "holes": [ { "index": 0, "center": [50, 50], "diameter": 12.0, "is_circular": true, "boundary": [[44,50], [44.2,51.8], ...], "weight": 1.0 }, { "index": 1, "center": [200, 150], "diameter": 8.0, "is_circular": true, "boundary": [...], "weight": 0.3 } ] } ``` Non-circular holes (slots, irregular cutouts) carry their full boundary polyline and weight, but `diameter` and `is_circular` are null/false. The density field uses point-to-polygon distance instead of point-to-center distance for these. --- ## 3. The Python Brain: Density Field + Geometry Generation ### 3.1 Density Field Formulation The density field η(x) is the core of the method. It maps every point on the plate to a value between 0 (minimum density — remove maximum material) and 1 (maximum density — retain material). **Hole influence term:** ``` I(x) = Σᵢ wᵢ · exp( -(dᵢ(x) / Rᵢ)^p ) ``` Where: - `dᵢ(x)` = distance from point x to hole i (center-to-point for circular, boundary-to-point for non-circular) - `Rᵢ = R₀ · (1 + κ · wᵢ)` = influence radius, scales with hole importance - `p` = decay exponent (controls transition sharpness) - `wᵢ` = user-assigned hole weight (fixed, not optimized) **Edge reinforcement term:** ``` E(x) = exp( -(d_edge(x) / R_edge)^p_edge ) ``` Where `d_edge(x)` is the distance from x to the nearest plate boundary edge. **Combined density field:** ``` η(x) = clamp(0, 1, η₀ + α · I(x) + β · E(x)) ``` **Density to local target spacing:** ``` s(x) = s_max - (s_max - s_min) · η(x) ``` Where s(x) is the target triangle edge length at point x. High density → small spacing (more ribs). Low density → large spacing (fewer ribs). **Density to local rib thickness:** ``` t(x) = clamp(t_min, t_max, t₀ · (1 + γ · η(x))) ``` Where t₀ is the nominal rib thickness and γ controls how much density affects thickness. ### 3.2 Geometry Generation: Gmsh Frontal-Delaunay Pipeline The geometry generation pipeline converts the density field into a manufacturable 2D rib profile. **Production implementation uses Gmsh's Frontal-Delaunay algorithm** (Python binding: `gmsh`) for superior adaptive meshing with background size fields. **Why Gmsh over Triangle library:** - **Frontal-Delaunay** advances from boundaries inward → better boundary conformance, more regular triangles - **Background size fields** handle density variation in ONE pass (no iterative refinement) - **Boolean geometry operations** → cleaner hole handling than PSLG workarounds - **Better triangle quality** → min angles 30-35° vs 25-30°, tighter distribution around equilateral (60°) - **Manufacturable patterns** → more uniform rib widths, smoother pocket shapes **Step 1 — Define the Planar Straight Line Graph (PSLG):** The PSLG is the input to Triangle. It consists of: - The outer boundary as a polygon (vertices + segments) - Each hole boundary as a polygon (vertices + segments) - Hole markers (points inside each hole, telling Triangle to leave these regions empty) ```python import triangle import numpy as np def build_pslg(geometry, keepout_distance): """ Build PSLG from plate geometry. keepout_distance: extra clearance around holes (mm) """ vertices = [] segments = [] holes_markers = [] # Outer boundary outer = offset_inward(geometry['outer_boundary'], keepout_distance) v_start = len(vertices) vertices.extend(outer) for i in range(len(outer)): segments.append([v_start + i, v_start + (i+1) % len(outer)]) # Each hole boundary (offset outward for keepout) for hole in geometry['holes']: hole_boundary = offset_outward(hole['boundary'], keepout_distance) v_start = len(vertices) vertices.extend(hole_boundary) for i in range(len(hole_boundary)): segments.append([v_start + i, v_start + (i+1) % len(hole_boundary)]) holes_markers.append(hole['center']) # point inside hole return { 'vertices': np.array(vertices), 'segments': np.array(segments), 'holes': np.array(holes_markers) } ``` **Step 2 — Compute area constraints from density field:** Triangle supports per-region area constraints via a callback or a maximum area parameter. For spatially varying area, we use an iterative refinement approach: ```python def compute_max_area(x, y, params): """ Target triangle area at point (x,y) based on density field. Smaller area = denser triangulation = more ribs. """ eta = evaluate_density_field(x, y, params) s = params['s_max'] - (params['s_max'] - params['s_min']) * eta # Area of equilateral triangle with side length s target_area = (np.sqrt(3) / 4) * s**2 return target_area ``` **Step 3 — Run constrained Delaunay triangulation:** ```python def generate_triangulation(pslg, params): """ Generate adaptive triangulation using Triangle library. """ # Initial triangulation with global max area global_max_area = (np.sqrt(3) / 4) * params['s_max']**2 # Triangle options: # 'p' = triangulate PSLG # 'q30' = minimum angle 30° (quality mesh) # 'a' = area constraint # 'D' = conforming Delaunay result = triangle.triangulate(pslg, f'pq30Da{global_max_area}') # Iterative refinement based on density field for iteration in range(3): # 2-3 refinement passes # For each triangle, check if area exceeds local target triangles = result['triangles'] vertices = result['vertices'] areas = compute_triangle_areas(vertices, triangles) centroids = compute_centroids(vertices, triangles) # Build per-triangle area constraints max_areas = np.array([ compute_max_area(cx, cy, params) for cx, cy in centroids ]) # If all triangles satisfy constraints, done if np.all(areas <= max_areas * 1.2): # 20% tolerance break # Refine: set area constraint and re-triangulate # Triangle supports this via the 'r' (refine) flag result = triangle.triangulate( result, f'rpq30Da', # per-triangle area constraints via triangle_max_area ) return result ``` **Step 4 — Extract ribs and compute thicknesses:** ```python def extract_ribs(triangulation, params, geometry): """ Convert triangulation edges to rib definitions. Each rib = (start_point, end_point, thickness, midpoint_density) """ vertices = triangulation['vertices'] triangles = triangulation['triangles'] # Get unique edges from triangle connectivity edges = set() for tri in triangles: for i in range(3): edge = tuple(sorted([tri[i], tri[(i+1)%3]])) edges.add(edge) ribs = [] for v1_idx, v2_idx in edges: p1 = vertices[v1_idx] p2 = vertices[v2_idx] midpoint = (p1 + p2) / 2 # Skip edges on the boundary (these aren't interior ribs) if is_boundary_edge(v1_idx, v2_idx, triangulation): continue # Compute local density and rib thickness eta = evaluate_density_field(midpoint[0], midpoint[1], params) thickness = compute_rib_thickness(eta, params) ribs.append({ 'start': p1.tolist(), 'end': p2.tolist(), 'midpoint': midpoint.tolist(), 'thickness': thickness, 'density': eta }) return ribs ``` **Step 5 — Generate pocket profiles:** Each triangle in the triangulation defines a pocket. The pocket profile is the triangle inset by half the local rib thickness on each edge, with fillet radii at corners. ```python def generate_pocket_profiles(triangulation, ribs, params): """ For each triangle, compute the pocket outline (triangle boundary inset by half-rib-width on each edge). """ vertices = triangulation['vertices'] triangles = triangulation['triangles'] pockets = [] for tri_idx, tri in enumerate(triangles): # Get the three edge thicknesses edge_thicknesses = get_triangle_edge_thicknesses( tri, ribs, vertices ) # Inset each edge by half its rib thickness inset_polygon = inset_triangle( vertices[tri[0]], vertices[tri[1]], vertices[tri[2]], edge_thicknesses[0]/2, edge_thicknesses[1]/2, edge_thicknesses[2]/2 ) if inset_polygon is None: # Triangle too small for pocket — skip (solid region) continue # Check minimum pocket size inscribed_r = compute_inscribed_radius(inset_polygon) if inscribed_r < params.get('min_pocket_radius', 1.5): continue # pocket too small to manufacture # Apply fillet to pocket corners filleted = fillet_polygon(inset_polygon, params['r_f']) pockets.append({ 'triangle_index': tri_idx, 'vertices': filleted, 'area': polygon_area(filleted) }) return pockets ``` **Step 6 — Assemble the ribbed plate profile:** The final output is the plate boundary minus all pocket regions, plus the hole cutouts. This is a 2D profile that NX will mesh as shells. ```python def assemble_profile(geometry, pockets, params): """ Create the final 2D ribbed plate profile. Plate boundary - pockets - holes = ribbed plate """ from shapely.geometry import Polygon, MultiPolygon from shapely.ops import unary_union # Plate outline (with optional perimeter frame) plate = Polygon(geometry['outer_boundary']) # Inset plate by frame width if params['w_frame'] > 0: inner_plate = plate.buffer(-params['w_frame']) else: inner_plate = plate # Union all pocket polygons pocket_polys = [Polygon(p['vertices']) for p in pockets] all_pockets = unary_union(pocket_polys) # Clip pockets to inner plate (don't cut into frame) clipped_pockets = all_pockets.intersection(inner_plate) # Subtract pockets from plate ribbed_plate = plate.difference(clipped_pockets) # Subtract holes (with original hole boundaries) for hole in geometry['holes']: hole_poly = Polygon(hole['boundary']) ribbed_plate = ribbed_plate.difference(hole_poly) return ribbed_plate ``` **Step 7 — Validate and export:** ```python def validate_and_export(ribbed_plate, params, output_path): """ Check manufacturability and export for NXOpen. """ checks = { 'min_web_width': check_minimum_web(ribbed_plate, params['t_min']), 'no_islands': check_no_floating_islands(ribbed_plate), 'no_self_intersections': ribbed_plate.is_valid, 'mass_estimate': estimate_mass(ribbed_plate, params), } valid = all([ checks['min_web_width'], checks['no_islands'], checks['no_self_intersections'] ]) # Export as JSON (coordinate arrays for NXOpen) profile_data = { 'valid': valid, 'checks': checks, 'outer_boundary': list(ribbed_plate.exterior.coords), 'pockets': [list(interior.coords) for interior in ribbed_plate.interiors if is_pocket(interior)], # pocket cutouts only 'hole_boundaries': [list(interior.coords) for interior in ribbed_plate.interiors if is_hole(interior)], # original hole cutouts 'mass_estimate': checks['mass_estimate'], 'num_pockets': len([i for i in ribbed_plate.interiors if is_pocket(i)]), 'parameters_used': params } with open(output_path, 'w') as f: json.dump(profile_data, f) return valid, checks ``` ### 3.3 Manufacturing Constraint Summary These constraints are enforced during geometry generation, not as FEA post-checks: | Constraint | Value | Enforcement Point | |---|---|---| | Minimum rib width | t_min (param, ≥ 2.0 mm) | Rib thickness computation + validation | | Minimum pocket inscribed radius | 1.5 mm (waterjet pierce requirement) | Pocket generation — skip small pockets | | Corner fillet radius | r_f (param, ≥ 0.5 mm for waterjet, ≥ tool_radius for CNC) | Pocket profile filleting | | Hole keepout | d_keep,hole (param, typically 1.5× hole diameter) | PSLG construction | | Edge keepout / frame | w_frame (param) | Profile assembly | | Minimum triangle quality | q_min = 30° minimum angle | Triangle library quality flag | | No floating islands | — | Validation step | | No self-intersections | — | Shapely validity check | --- ## 4. The NX Hands: Reserved-Region FEM Architecture ### 4.1 Core Concept: Sandbox + Reserved Regions The FEA workflow uses direct reserved-region remeshing rather than interface-coupling constructs. Instead, each plate mid-surface is explicitly partitioned in NX into: - **Sandbox region(s)** — where the adaptive isogrid is allowed to change each iteration - **Reserved regions** — geometry that must remain untouched (edges, functional features, local areas around critical holes/BC interfaces) Each sandbox face is tagged with an NX user attribute: - **Title:** `ISOGRID_SANDBOX` - **Value:** `sandbox_1`, `sandbox_2`, ... Multiple sandbox faces are supported on the same plate. ### 4.2 Iterative Geometry Round-Trip (JSON-only) Per optimization iteration, NXOpen performs a full geometry round-trip for each sandbox: 1. **Extract current sandbox geometry** from NX into `geometry_sandbox_n.json` - outer loop boundary - inner loops (holes/cutouts) - local 2D transform (face-local XY frame) 2. **Python Brain generates isogrid profile** inside sandbox boundary - outputs `rib_profile_sandbox_n.json` 3. **NXOpen imports profile** and replaces only sandbox geometry - reserved faces remain unchanged 4. **Full plate remesh** (single monolithic mesh) 5. **Solve existing Simcenter solution setup** 6. **Extract field + scalar results** to `results.json` This JSON-only flow replaces DXF/STEP transfer for v1 and keeps CAD/CAE handoff deterministic. ### 4.3 Why Reserved-Region Is Robust This architecture addresses the load/BC persistence problem by preserving topology where constraints live: - Loads and boundary conditions are assigned to entities in reserved regions that do not change between iterations - Only sandbox geometry is replaced, so reserved-region references remain valid - The solver always runs on one connected, remeshed plate model (no multi-model merge operations) - Hole positions can be optimized because geometry extraction happens **every iteration** from the latest NX model state ### 4.4 NXOpen Phase-2 Script Responsibilities #### A) `extract_sandbox.py` - Discover all faces with `ISOGRID_SANDBOX` attribute - For each sandbox face: - read outer + inner loops - sample edges to polyline (chord tolerance 0.1 mm) - fit circles on inner loops when circular - project 3D points to face-local 2D - write `geometry_sandbox_n.json` using Python Brain input schema #### B) `import_profile.py` - Read `rib_profile_sandbox_n.json` - Rebuild NX curves from coordinate arrays - Create/update sheet body for the sandbox zone - Replace sandbox face geometry while preserving surrounding reserved faces - Sew/unite resulting geometry into a watertight plate body #### C) `solve_and_extract.py` - Regenerate FE mesh for full plate - Trigger solve using existing solution object(s) - Extract from sandbox-associated nodes/elements: - nodal Von Mises stress - nodal displacement magnitude - elemental strain - total mass - Write `results.json`: ```json { "nodes_xy": [[...], [...]], "stress_values": [...], "disp_values": [...], "strain_values": [...], "mass": 0.0 } ``` ### 4.5 Results Extraction Strategy Two output backends are acceptable: 1. **Direct NXOpen post-processing API** (preferred when available) 2. **Simcenter CSV export + parser** (robust fallback) Both must produce consistent arrays for downstream optimization and optional stress-feedback loops. --- ## 5. Atomizer Integration ### 5.1 Parameter Space Definition ```python # Atomizer/Optuna parameter space PARAM_SPACE = { # Density field parameters 'eta_0': {'type': 'float', 'low': 0.0, 'high': 0.4, 'desc': 'Baseline density offset'}, 'alpha': {'type': 'float', 'low': 0.3, 'high': 2.0, 'desc': 'Hole influence scale'}, 'R_0': {'type': 'float', 'low': 10.0, 'high': 100.0, 'desc': 'Base influence radius (mm)'}, 'kappa': {'type': 'float', 'low': 0.0, 'high': 3.0, 'desc': 'Weight-to-radius coupling'}, 'p': {'type': 'float', 'low': 1.0, 'high': 4.0, 'desc': 'Decay exponent'}, 'beta': {'type': 'float', 'low': 0.0, 'high': 1.0, 'desc': 'Edge influence scale'}, 'R_edge': {'type': 'float', 'low': 5.0, 'high': 40.0, 'desc': 'Edge influence radius (mm)'}, # Spacing parameters 's_min': {'type': 'float', 'low': 8.0, 'high': 20.0, 'desc': 'Min cell size (mm)'}, 's_max': {'type': 'float', 'low': 25.0, 'high': 60.0, 'desc': 'Max cell size (mm)'}, # Rib thickness parameters 't_min': {'type': 'float', 'low': 2.0, 'high': 4.0, 'desc': 'Min rib thickness (mm)'}, 't_0': {'type': 'float', 'low': 2.0, 'high': 6.0, 'desc': 'Nominal rib thickness (mm)'}, 'gamma': {'type': 'float', 'low': 0.0, 'high': 3.0, 'desc': 'Density-thickness coupling'}, # Manufacturing / frame parameters 'w_frame': {'type': 'float', 'low': 3.0, 'high': 20.0, 'desc': 'Perimeter frame width (mm)'}, 'r_f': {'type': 'float', 'low': 0.5, 'high': 3.0, 'desc': 'Pocket fillet radius (mm)'}, 'd_keep': {'type': 'float', 'low': 1.0, 'high': 3.0, 'desc': 'Hole keepout multiplier (× diameter)'}, } ``` **Total: 15 continuous parameters.** This is a comfortable range for Optuna TPE. Can easily expand to 20-25 if needed (e.g., adding per-class influence overrides, smoothing length, separate edge/hole decay exponents). ### 5.2 Objective Function ```python def objective(trial, geometry, sim_template): # Sample parameters params = {} for name, spec in PARAM_SPACE.items(): params[name] = trial.suggest_float(name, spec['low'], spec['high']) # --- Python Brain: generate geometry --- profile_path = f'/tmp/isogrid_trial_{trial.number}.json' valid, checks = generate_isogrid(geometry, params, profile_path) if not valid: # Geometry failed validation — penalize return float('inf') # --- NX Hands: mesh and solve --- results = nx_import_and_solve(profile_path, sim_template) if results['status'] != 'solved': return float('inf') # --- Evaluate --- mass = results['mass'] max_stress = results['max_von_mises'] max_disp = results['max_displacement'] # Constraint penalties penalty = 0.0 SIGMA_ALLOW = 150.0 # MPa (example for AL6061-T6 with SF) DELTA_MAX = 0.1 # mm (example) if max_stress > SIGMA_ALLOW: penalty += 1e4 * ((max_stress / SIGMA_ALLOW) - 1.0) ** 2 if max_disp > DELTA_MAX: penalty += 1e4 * ((max_disp / DELTA_MAX) - 1.0) ** 2 # Store fields for visualization (not used by optimizer) trial.set_user_attr('stress_field', results.get('stress_field')) trial.set_user_attr('displacement_field', results.get('displacement_field')) trial.set_user_attr('mass', mass) trial.set_user_attr('max_stress', max_stress) trial.set_user_attr('max_disp', max_disp) trial.set_user_attr('num_pockets', checks.get('num_pockets', 0)) return mass + penalty ``` ### 5.3 Convergence and Stopping With ~2 min/iteration and 15 parameters, expect: | Trials | Wall Time | Expected Outcome | |---|---|---| | 50 | ~1.5 hours | Random exploration, baseline understanding | | 200 | ~7 hours | Surrogate learning, good solutions emerging | | 500 | ~17 hours | Near-optimal, diminishing returns starting | | 1000 | ~33 hours | Refined optimum, convergence likely | | 2000 | ~67 hours | Exhaustive, marginal improvement | **Recommendation:** Start with 500 trials overnight. Review results. If the Pareto front is still moving, extend to 1000. --- ## 6. V2 Roadmap: Stress-Feedback Enhancement Once v1 is running and producing good results, the stress-feedback enhancement adds the FEA stress/displacement fields as inputs to the density field: ``` η(x) = clamp(0, 1, η₀ + α·I(x) + β·E(x) + λ·S_prev(x)) ``` Where `S_prev(x)` is the normalized, smoothed stress field from the previous FEA of the same trial. This creates a local feedback loop within each Atomizer trial: 1. Generate isogrid from density field (hole-based only, no stress data) 2. Run FEA → get stress field 3. Regenerate isogrid from updated density field (now including stress) 4. Run FEA → get updated stress field 5. Check convergence (stress field stable?) → if not, repeat from 3 6. Report final metrics to Atomizer Atomizer then optimizes the meta-parameters including λ (stress feedback strength) and a smoothing kernel size for the stress field. **New parameters for v2:** λ (stress feedback weight), σ_smooth (stress field smoothing kernel size, mm), n_inner_max (max inner iterations). This is more expensive (2-5× FEA per trial) but produces designs that are truly structurally adapted, not just geometrically adapted. --- ## 7. Implementation Sequence ### Phase 1 — Python Brain Standalone (1-2 weeks) Build and test the geometry generator independently of NX: - Density field evaluation with exponential kernel - Constrained Delaunay triangulation (using `triangle` library) - Rib thickening and pocket profile generation (using `shapely`) - Manufacturing constraint validation - Matplotlib visualization (density field heatmap, rib pattern overlay, pocket profiles) - Test with 3-5 different plate geometries and 50+ parameter sets **Deliverable:** A Python module that takes `geometry.json` + parameters → outputs `rib_profile.json` + visualization plots. ### Phase 2 — NX Sandbox Extraction + Profile Import Scripts (1-2 weeks) Build the NXOpen scripts for reserved-region geometry handling: - Sandbox discovery via NX attribute (`ISOGRID_SANDBOX = sandbox_n`) - Per-sandbox extraction script → `geometry_sandbox_n.json` - Local 2D projection + loop sampling (outer + inner loops) - Rib profile import script (`rib_profile_sandbox_n.json`) - Sandbox-only geometry replacement + sew/unite with reserved regions - End-to-end JSON round-trip validation on multi-sandbox plates **Deliverable:** Complete NX geometry round-trip pipeline (extract/import) with reserved regions preserved. ### Phase 3 — NX Iteration Solve + Results Extraction (1-2 weeks) Build the NXOpen per-iteration analysis script: - Extract sandbox geometry every iteration (supports moving/optimized holes) - Import regenerated rib profile into sandbox region(s) - Remesh the **full** plate as one monolithic model - Trigger existing Simcenter solution setup - Extract nodal stress/displacement + elemental strain + mass - Serialize standardized `results.json` for Atomizer - End-to-end test: Python Brain → NX scripts → `results.json` vs manual benchmark **Deliverable:** Production-ready per-iteration NX pipeline with stable result export for optimization. ### Phase 4 — Atomizer Integration (1 week) Wire Atomizer to orchestrate the pipeline: - Parameter sampling → Python Brain → NX journal trigger → result extraction - Objective function with constraint penalties - Study creation, execution, result logging - Failed iteration handling (geometry validation failures, solve failures, merge warnings) - Convergence monitoring (plot best mass vs. trial number) **Deliverable:** Full automated optimization loop, ready for production runs. ### Phase 5 — Validation + First Real Project (1-2 weeks) Run on an actual client plate: - Full optimization campaign (500+ trials) - Compare optimized mass vs. original solid plate and vs. uniform isogrid - Manufacturing review (waterjet quote/feasibility from shop) - Verify optimal design with refined mesh / higher-fidelity analysis - Iterate on parameter bounds and manufacturing constraints based on feedback **Deliverable:** First optimized plate design, manufacturing-ready. --- ## Appendix A: Python Dependencies ``` numpy >= 1.24 scipy >= 1.10 shapely >= 2.0 triangle >= 20230923 # Python binding for Shewchuk's Triangle matplotlib >= 3.7 ``` Optional for v2: `gmsh` (alternative mesher), `plotly` (interactive viz). ## Appendix B: Key Reference Material - Shewchuk, J.R. "Triangle: A Two-Dimensional Quality Mesh Generator" — the engine behind the constrained Delaunay step - NASA CR-124075 "Isogrid Design Handbook" — classical isogrid design equations - Optuna documentation — TPE sampler configuration and multi-objective support - NXOpen Python API Reference — for geometry creation and Simcenter automation