diff --git a/projects/hydrotech-beam/studies/01_doe_landscape/nx_interface.py b/projects/hydrotech-beam/studies/01_doe_landscape/nx_interface.py index 5d26fa42..1e6dd013 100644 --- a/projects/hydrotech-beam/studies/01_doe_landscape/nx_interface.py +++ b/projects/hydrotech-beam/studies/01_doe_landscape/nx_interface.py @@ -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)