Fix NX solve: backup/restore master model, archive outputs to iterations

NX .sim files store absolute internal references to .fem/.prt files.
Copying them to iteration folders breaks these references (Parts.Open
returns None). Instead:

1. Backup master model once at study start
2. Restore from backup before each trial (isolation)
3. Solve on master model in-place (NX references intact)
4. Archive solver outputs (OP2/F06) + params.exp to iterations/iterNNN/
5. params.exp in each iteration: import into NX to recreate any trial

iteration_manager.py kept for future use but not wired in.
This commit is contained in:
2026-02-11 15:05:18 +00:00
parent 815db0fb8d
commit 3718a8d5c8

View File

@@ -159,13 +159,28 @@ class AtomizerNXSolver:
self.use_iteration_folders = use_iteration_folders
self._iteration = 0
# Smart iteration manager — handles folder creation, model copies, retention
from iteration_manager import IterationManager
self.study_dir = Path(__file__).parent.resolve()
self._iter_mgr = IterationManager(
study_dir=self.study_dir,
master_model_dir=self.model_dir,
)
# Iteration output folders (solver outputs + params, NOT model copies)
self.iterations_dir = self.study_dir / "iterations"
self.iterations_dir.mkdir(parents=True, exist_ok=True)
# One-time backup of master model (restored before each trial for isolation)
# NX .sim files store absolute internal references to .fem/.prt — copying
# them to iteration folders breaks these references. Instead we solve on
# the master model in-place and archive outputs to iteration folders.
import shutil
self._backup_dir = self.study_dir / "_model_backup"
if not self._backup_dir.exists():
logger.info("Creating master model backup at %s", self._backup_dir)
self._backup_dir.mkdir(parents=True)
for f in model_dir.iterdir():
if f.is_file():
shutil.copy2(f, self._backup_dir / f.name)
n_backed = len(list(self._backup_dir.iterdir()))
logger.info("Backed up %d model files", n_backed)
else:
logger.info("Using existing model backup at %s", self._backup_dir)
# Find the .sim file
sim_files = list(model_dir.glob("*.sim"))
@@ -281,17 +296,22 @@ class AtomizerNXSolver:
trial.hole_count,
)
try:
# Step 0: Prepare iteration folder with fresh model copies
# All paths resolved to absolute — fixes NX reference issues
iter_dir = self._iter_mgr.prepare_iteration(self._iteration)
# Create iteration output folder
iter_dir = self.iterations_dir / f"iter{self._iteration:03d}"
iter_dir.mkdir(parents=True, exist_ok=True)
# Sim and prt files are now in the iteration folder
sim_file = iter_dir / self.sim_file.name
prt_file = iter_dir / self.prt_file.name
try:
# Step 0: Restore master model from backup (clean state)
import shutil
import json
restored = 0
for bf in self._backup_dir.iterdir():
if bf.is_file():
shutil.copy2(bf, self.model_dir / bf.name)
restored += 1
logger.info("Restored %d model files from backup", restored)
# Save trial params to iteration folder
import json
params_file = iter_dir / "params.json"
params_file.write_text(json.dumps({
"iteration": self._iteration,
@@ -304,11 +324,19 @@ class AtomizerNXSolver:
},
}, indent=2))
# Step 2: Run NX journal (update expressions + solve) in iteration folder
# All paths are absolute — critical for NX to resolve file references
# Also write .exp file to iteration folder (import into NX to recreate)
exp_file = iter_dir / "params.exp"
with open(exp_file, "w") as f:
for name, val in expressions.items():
unit = "Constant" if name in ("hole_count",) else "MilliMeter"
f.write(f'{name}={val} [{unit}]\n')
# Step 1: Solve on MASTER model (NX internal references intact)
sim_file = self.sim_file
prt_file = self.prt_file
solve_result = self._nx_solver.run_simulation(
sim_file=sim_file,
working_dir=iter_dir,
working_dir=self.model_dir,
expression_updates=expressions,
)
@@ -361,13 +389,22 @@ class AtomizerNXSolver:
logger.warning("Stress extraction failed: %s", e)
max_vm_stress = float("nan")
# Step 6: Record results + write summary to iteration folder
self._iter_mgr.record_result(
self._iteration,
mass=mass_kg,
displacement=tip_displacement,
stress=max_vm_stress,
)
# Step 6: Archive solver outputs to iteration folder
# Copy OP2, F06, and other solver outputs from models/ dir
for suffix in (".op2", ".f06", ".log", ".dat"):
for src in self.model_dir.glob(f"*{suffix}"):
try:
shutil.copy2(src, iter_dir / src.name)
except Exception as e:
logger.warning("Could not archive %s: %s", src.name, e)
# Copy temp files (mass extraction, etc.)
for pattern in ("_temp_*",):
for src in self.model_dir.glob(pattern):
try:
shutil.copy2(src, iter_dir / src.name)
except Exception:
pass
# Write results summary JSON
results_file = iter_dir / "results.json"
@@ -380,6 +417,8 @@ class AtomizerNXSolver:
"op2_file": op2_path.name if op2_path else None,
}, indent=2))
logger.info("Archived iter%03d: results + solver outputs", self._iteration)
elapsed = time.time() - start_time
logger.info(
"Trial %d complete: mass=%.2f kg, disp=%.3f mm, stress=%.1f MPa (%.1fs)",
@@ -406,13 +445,7 @@ class AtomizerNXSolver:
)
def close(self) -> None:
"""Clean up NX solver resources and run final retention."""
self._iter_mgr.apply_retention()
best = self._iter_mgr.get_best_iterations(3)
if best:
logger.info("Best iterations: %s",
[(f"iter{b.number:03d}", f"{b.mass:.1f}kg",
"" if b.feasible else "") for b in best])
"""Clean up NX solver resources."""
logger.info("AtomizerNXSolver closed. %d iterations completed.", self._iteration)