From 4243a332a3db7378191c8be5008f3bdcdb6a9f32 Mon Sep 17 00:00:00 2001 From: Antoine Date: Wed, 11 Feb 2026 14:39:10 +0000 Subject: [PATCH] Iteration archival: solve on master model, archive outputs to studies/iterations/iterNNN/ - Each iteration gets: params.json, results.json, OP2, F06, mass files - Model directory stays clean (no solver output buildup) - Study folder is self-contained with full trial history --- .../studies/01_doe_landscape/nx_interface.py | 99 +++++++++++++++---- 1 file changed, 80 insertions(+), 19 deletions(-) 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 f5f534b4..9e7b0306 100644 --- a/projects/hydrotech-beam/studies/01_doe_landscape/nx_interface.py +++ b/projects/hydrotech-beam/studies/01_doe_landscape/nx_interface.py @@ -153,15 +153,18 @@ class AtomizerNXSolver: if not model_dir.exists(): raise FileNotFoundError(f"Model directory not found: {model_dir}") - self.model_dir = model_dir + self.model_dir = model_dir.resolve() self.nx_version = nx_version self.timeout = timeout self.use_iteration_folders = use_iteration_folders self._iteration = 0 - # Set up iteration base directory - self.iterations_dir = model_dir.parent / "2_iterations" + # Iteration outputs go inside the study folder, not models/ + # study_dir = the directory where run_doe.py lives + self.study_dir = Path(__file__).parent.resolve() + self.iterations_dir = self.study_dir / "iterations" self.iterations_dir.mkdir(parents=True, exist_ok=True) + logger.info("Iterations dir: %s", self.iterations_dir) # Find the .sim file sim_files = list(model_dir.glob("*.sim")) @@ -277,25 +280,34 @@ class AtomizerNXSolver: trial.hole_count, ) - try: - # Step 1: Create iteration folder with fresh model copies + .exp file - if self.use_iteration_folders: - iter_dir = self._nx_solver.create_iteration_folder( - iterations_base_dir=self.iterations_dir, - iteration_number=self._iteration, - expression_updates=expressions, - ) - sim_file = iter_dir / self.sim_file.name - prt_file = iter_dir / self.prt_file.name - else: - iter_dir = self.model_dir - sim_file = self.sim_file - prt_file = self.prt_file + # Create iteration output folder inside the study + iter_dir = self.iterations_dir / f"iter{self._iteration:03d}" + iter_dir.mkdir(parents=True, exist_ok=True) - # Step 2: Run NX journal (update expressions + solve) + try: + # Step 1: Solve directly on master model (no file copying) + # NX file references stay intact — expressions updated in-place by journal + sim_file = self.sim_file + prt_file = self.prt_file + + # Save trial params to iteration folder + import json + params_file = iter_dir / "params.json" + params_file.write_text(json.dumps({ + "iteration": self._iteration, + "expressions": expressions, + "trial_input": { + "beam_half_core_thickness": trial.beam_half_core_thickness, + "beam_face_thickness": trial.beam_face_thickness, + "holes_diameter": trial.holes_diameter, + "hole_count": trial.hole_count, + }, + }, indent=2)) + + # Step 2: Run NX journal (update expressions + solve) on master model solve_result = self._nx_solver.run_simulation( sim_file=sim_file, - working_dir=iter_dir, + working_dir=self.model_dir, expression_updates=expressions, ) @@ -348,6 +360,9 @@ class AtomizerNXSolver: logger.warning("Stress extraction failed: %s", e) max_vm_stress = float("nan") + # Step 6: Copy solver outputs to iteration folder for archival + self._archive_iteration(iter_dir, op2_path, mass_kg, tip_displacement, max_vm_stress) + elapsed = time.time() - start_time logger.info( "Trial %d complete: mass=%.2f kg, disp=%.3f mm, stress=%.1f MPa (%.1fs)", @@ -373,6 +388,52 @@ class AtomizerNXSolver: iteration_dir=str(iter_dir) if 'iter_dir' in locals() else None, ) + def _archive_iteration( + self, + iter_dir: Path, + op2_path: Path, + mass: float, + displacement: float, + stress: float, + ) -> None: + """Copy solver outputs to iteration folder for archival. + + Keeps the models/ directory clean — solver outputs go to the study's + iterations/ folder. Each iteration gets: OP2, F06, mass file, and + a results summary JSON. + """ + import json + import shutil + + # Copy OP2 and F06 files + for suffix in [".op2", ".f06", ".log"]: + src = op2_path.with_suffix(suffix) + if src.exists(): + try: + shutil.copy2(src, iter_dir / src.name) + except Exception as e: + logger.warning("Could not copy %s: %s", src.name, e) + + # Copy mass temp file if it exists + for fname in ["_temp_mass.txt", "_temp_part_properties.json"]: + src = self.model_dir / fname + if src.exists(): + try: + shutil.copy2(src, iter_dir / fname) + except Exception as e: + logger.warning("Could not copy %s: %s", fname, e) + + # Write results summary + results_file = iter_dir / "results.json" + results_file.write_text(json.dumps({ + "mass_kg": mass, + "tip_displacement_mm": displacement, + "max_von_mises_mpa": stress, + "op2_file": op2_path.name, + }, indent=2)) + + logger.info("Archived iteration %d to %s", self._iteration, iter_dir.name) + def close(self) -> None: """Clean up NX solver resources.""" logger.info("AtomizerNXSolver closed. %d iterations completed.", self._iteration)