feat: Add MLP surrogate with Turbo Mode for 100x faster optimization

Neural Acceleration (MLP Surrogate):
- Add run_nn_optimization.py with hybrid FEA/NN workflow
- MLP architecture: 4-layer (64->128->128->64) with BatchNorm/Dropout
- Three workflow modes:
  - --all: Sequential export->train->optimize->validate
  - --hybrid-loop: Iterative Train->NN->Validate->Retrain cycle
  - --turbo: Aggressive single-best validation (RECOMMENDED)
- Turbo mode: 5000 NN trials + 50 FEA validations in ~12 minutes
- Separate nn_study.db to avoid overloading dashboard

Performance Results (bracket_pareto_3obj study):
- NN prediction errors: mass 1-5%, stress 1-4%, stiffness 5-15%
- Found minimum mass designs at boundary (angle~30deg, thick~30mm)
- 100x speedup vs pure FEA exploration

Protocol Operating System:
- Add .claude/skills/ with Bootstrap, Cheatsheet, Context Loader
- Add docs/protocols/ with operations (OP_01-06) and system (SYS_10-14)
- Update SYS_14_NEURAL_ACCELERATION.md with MLP Turbo Mode docs

NX Automation:
- Add optimization_engine/hooks/ for NX CAD/CAE automation
- Add study_wizard.py for guided study creation
- Fix FEM mesh update: load idealized part before UpdateFemodel()

New Study:
- bracket_pareto_3obj: 3-objective Pareto (mass, stress, stiffness)
- 167 FEA trials + 5000 NN trials completed
- Demonstrates full hybrid workflow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Antoine
2025-12-06 20:01:59 -05:00
parent 0cb2808c44
commit 602560c46a
70 changed files with 31018 additions and 289 deletions

View File

@@ -676,7 +676,13 @@ def solve_assembly_fem_workflow(theSession, sim_file_path, solution_name, expres
def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_updates, working_dir):
"""
Simple workflow for single-part simulations or when no expression updates needed.
Workflow for single-part simulations with optional expression updates.
For single-part FEMs (Bracket.prt -> Bracket_fem1.fem -> Bracket_sim1.sim):
1. Open the .sim file (this loads .fem and .prt)
2. If expression_updates: find the geometry .prt, update expressions, rebuild
3. Update the FEM mesh
4. Solve
"""
print(f"[JOURNAL] Opening simulation: {sim_file_path}")
@@ -688,6 +694,192 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
partLoadStatus1.Dispose()
workSimPart = theSession.Parts.BaseWork
# =========================================================================
# STEP 1: UPDATE EXPRESSIONS IN GEOMETRY PART (if any)
# =========================================================================
if expression_updates:
print(f"[JOURNAL] STEP 1: Updating expressions in geometry part...")
# List all loaded parts for debugging
print(f"[JOURNAL] Currently loaded parts:")
for part in theSession.Parts:
print(f"[JOURNAL] - {part.Name} (type: {type(part).__name__})")
# NX doesn't automatically load the geometry .prt when opening a SIM file
# We need to find and load it explicitly from the working directory
geom_part = None
# First, try to find an already loaded geometry part
for part in theSession.Parts:
part_name = part.Name.lower()
part_type = type(part).__name__
# Skip FEM and SIM parts by type
if 'fem' in part_type.lower() or 'sim' in part_type.lower():
continue
# Skip parts with _fem or _sim in name
if '_fem' in part_name or '_sim' in part_name:
continue
geom_part = part
print(f"[JOURNAL] Found geometry part (already loaded): {part.Name}")
break
# If not found, try to load the geometry .prt file from working directory
if geom_part is None:
print(f"[JOURNAL] Geometry part not loaded, searching for .prt file...")
for filename in os.listdir(working_dir):
if filename.endswith('.prt') and '_fem' not in filename.lower() and '_sim' not in filename.lower():
prt_path = os.path.join(working_dir, filename)
print(f"[JOURNAL] Loading geometry part: {filename}")
try:
geom_part, partLoadStatus = theSession.Parts.Open(prt_path)
partLoadStatus.Dispose()
print(f"[JOURNAL] Geometry part loaded: {geom_part.Name}")
break
except Exception as e:
print(f"[JOURNAL] WARNING: Could not load {filename}: {e}")
if geom_part:
try:
# Switch to the geometry part for expression editing
markId_expr = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Visible, "Update Expressions")
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
geom_part,
NXOpen.DisplayPartOption.AllowAdditional,
NXOpen.PartDisplayPartWorkPartOption.UseLast
)
partLoadStatus.Dispose()
# Switch to modeling application for expression editing
theSession.ApplicationSwitchImmediate("UG_APP_MODELING")
workPart = theSession.Parts.Work
# Write expressions to temp file and import
exp_file_path = os.path.join(working_dir, "_temp_expressions.exp")
with open(exp_file_path, 'w') as f:
for expr_name, expr_value in expression_updates.items():
# Determine unit based on name
if 'angle' in expr_name.lower():
unit_str = "Degrees"
else:
unit_str = "MilliMeter"
f.write(f"[{unit_str}]{expr_name}={expr_value}\n")
print(f"[JOURNAL] {expr_name} = {expr_value} ({unit_str})")
print(f"[JOURNAL] Importing expressions...")
expModified, errorMessages = workPart.Expressions.ImportFromFile(
exp_file_path,
NXOpen.ExpressionCollection.ImportMode.Replace
)
print(f"[JOURNAL] Expressions modified: {expModified}")
if errorMessages:
print(f"[JOURNAL] Import messages: {errorMessages}")
# Update geometry
print(f"[JOURNAL] Rebuilding geometry...")
markId_update = theSession.SetUndoMark(NXOpen.Session.MarkVisibility.Invisible, "NX update")
nErrs = theSession.UpdateManager.DoUpdate(markId_update)
theSession.DeleteUndoMark(markId_update, "NX update")
print(f"[JOURNAL] Geometry rebuilt ({nErrs} errors)")
# Save geometry part
print(f"[JOURNAL] Saving geometry part...")
partSaveStatus_geom = workPart.Save(NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue)
partSaveStatus_geom.Dispose()
# Clean up temp file
try:
os.remove(exp_file_path)
except:
pass
except Exception as e:
print(f"[JOURNAL] ERROR updating expressions: {e}")
import traceback
traceback.print_exc()
else:
print(f"[JOURNAL] WARNING: Could not find geometry part for expression updates!")
# =========================================================================
# STEP 2: UPDATE FEM MESH (if expressions were updated)
# =========================================================================
if expression_updates:
print(f"[JOURNAL] STEP 2: Updating FEM mesh...")
# First, load the idealized part if it exists (required for mesh update chain)
# The chain is: .prt (geometry) -> _i.prt (idealized) -> .fem (mesh)
idealized_part = None
for filename in os.listdir(working_dir):
if '_i.prt' in filename.lower():
idealized_path = os.path.join(working_dir, filename)
print(f"[JOURNAL] Loading idealized part: {filename}")
try:
idealized_part, partLoadStatus = theSession.Parts.Open(idealized_path)
partLoadStatus.Dispose()
print(f"[JOURNAL] Idealized part loaded: {idealized_part.Name}")
except Exception as e:
print(f"[JOURNAL] WARNING: Could not load idealized part: {e}")
break
# Find the FEM part
fem_part = None
for part in theSession.Parts:
if '_fem' in part.Name.lower() or part.Name.lower().endswith('.fem'):
fem_part = part
print(f"[JOURNAL] Found FEM part: {part.Name}")
break
if fem_part:
try:
# Switch to FEM part - CRITICAL: Use SameAsDisplay to make FEM the work part
# This is required for UpdateFemodel() to properly regenerate the mesh
# Reference: tests/journal_with_regenerate.py line 76
print(f"[JOURNAL] Switching to FEM part: {fem_part.Name}")
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
fem_part,
NXOpen.DisplayPartOption.AllowAdditional,
NXOpen.PartDisplayPartWorkPartOption.SameAsDisplay # Critical fix!
)
partLoadStatus.Dispose()
# Switch to FEM application
theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
# Update the FE model
workFemPart = theSession.Parts.BaseWork
feModel = workFemPart.FindObject("FEModel")
print(f"[JOURNAL] Updating FE model...")
feModel.UpdateFemodel()
print(f"[JOURNAL] FE model updated")
# Save FEM
partSaveStatus_fem = workFemPart.Save(NXOpen.BasePart.SaveComponents.TrueValue, NXOpen.BasePart.CloseAfterSave.FalseValue)
partSaveStatus_fem.Dispose()
print(f"[JOURNAL] FEM saved")
except Exception as e:
print(f"[JOURNAL] ERROR updating FEM: {e}")
import traceback
traceback.print_exc()
# =========================================================================
# STEP 3: SWITCH BACK TO SIM AND SOLVE
# =========================================================================
print(f"[JOURNAL] STEP 3: Solving simulation...")
# Switch back to sim part
status, partLoadStatus = theSession.Parts.SetActiveDisplay(
workSimPart,
NXOpen.DisplayPartOption.AllowAdditional,
NXOpen.PartDisplayPartWorkPartOption.UseLast
)
partLoadStatus.Dispose()
theSession.ApplicationSwitchImmediate("UG_APP_SFEM")
theSession.Post.UpdateUserGroupsFromSimPart(workSimPart)
@@ -710,7 +902,7 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
psolutions1,
NXOpen.CAE.SimSolution.SolveOption.Solve,
NXOpen.CAE.SimSolution.SetupCheckOption.CompleteCheckAndOutputErrors,
NXOpen.CAE.SimSolution.SolveMode.Background
NXOpen.CAE.SimSolution.SolveMode.Foreground # Use Foreground to wait for completion
)
theSession.DeleteUndoMark(markId_solve2, None)
@@ -718,14 +910,11 @@ def solve_simple_workflow(theSession, sim_file_path, solution_name, expression_u
print(f"[JOURNAL] Solve completed: {numsolved} solved, {numfailed} failed, {numskipped} skipped")
# Save
# Save all
try:
partSaveStatus = workSimPart.Save(
NXOpen.BasePart.SaveComponents.TrueValue,
NXOpen.BasePart.CloseAfterSave.FalseValue
)
anyPartsModified, partSaveStatus = theSession.Parts.SaveAll()
partSaveStatus.Dispose()
print(f"[JOURNAL] Saved!")
print(f"[JOURNAL] Saved all parts!")
except:
pass