From 6f3325d86ffcaf67d88e76d6204ad8a9d999e24b Mon Sep 17 00:00:00 2001 From: Antoine Date: Wed, 11 Feb 2026 19:02:43 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20mass=20extraction=20NaN=20in=20Hydrotech?= =?UTF-8?q?=20Beam=20DOE=20=E2=80=94=20two=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug 1 — Journal (solve_simulation.py simple workflow): Expression lookup for p173 fails silently for derived/measurement expressions, so _temp_mass.txt was never written. Added MeasureManager fallback via extract_part_mass() (already used in assembly workflow). Bug 2 — Extractor (extract_mass_from_expression.py): Journal writes 'p173=' format but extractor tried float() on the whole content including 'p173='. Added key=value parsing. Defense in depth — nx_interface.py: Added stdout parsing fallback: if _temp_mass.txt still missing, parse mass from journal output captured via solver.py stdout passthrough. Files changed: - optimization_engine/nx/solve_simulation.py — MeasureManager fallback - optimization_engine/extractors/extract_mass_from_expression.py — key=value parse - optimization_engine/nx/solver.py — include stdout in result dict - projects/hydrotech-beam/studies/01_doe_landscape/nx_interface.py — stdout fallback Tags: hydrotech-beam, mass-extraction --- .../extract_mass_from_expression.py | 10 +++- optimization_engine/nx/solve_simulation.py | 47 ++++++++++++++----- optimization_engine/nx/solver.py | 3 +- .../studies/01_doe_landscape/nx_interface.py | 26 ++++++++++ 4 files changed, 70 insertions(+), 16 deletions(-) diff --git a/optimization_engine/extractors/extract_mass_from_expression.py b/optimization_engine/extractors/extract_mass_from_expression.py index cdf1206a..5bdb9af0 100644 --- a/optimization_engine/extractors/extract_mass_from_expression.py +++ b/optimization_engine/extractors/extract_mass_from_expression.py @@ -42,13 +42,19 @@ def extract_mass_from_expression(prt_file: Path, expression_name: str = "p173") # Read mass from file try: with open(mass_file, 'r') as f: - mass_kg = float(f.read().strip()) + content = f.read().strip() + + # Handle key=value format (e.g., "p173=1185.767") + if '=' in content: + content = content.split('=', 1)[1] + + mass_kg = float(content) print(f"[OK] Mass from {expression_name}: {mass_kg:.6f} kg ({mass_kg * 1000:.2f} g)") return mass_kg except ValueError as e: - raise ValueError(f"Could not parse mass from {mass_file}: {e}") + raise ValueError(f"Could not parse mass from {mass_file} (content: {content!r}): {e}") except Exception as e: raise RuntimeError(f"Failed to read mass file: {e}") diff --git a/optimization_engine/nx/solve_simulation.py b/optimization_engine/nx/solve_simulation.py index 0ee4acc2..6ebd09e2 100644 --- a/optimization_engine/nx/solve_simulation.py +++ b/optimization_engine/nx/solve_simulation.py @@ -1170,28 +1170,49 @@ def solve_simple_workflow( f"[JOURNAL] Solve completed: {numsolved} solved, {numfailed} failed, {numskipped} skipped" ) - # Extract mass from geometry part expression (p173) and write to temp file + # Extract mass and write to _temp_mass.txt + # Strategy: try expression lookup first, fall back to MeasureManager try: mass_value = None - # Find geometry part (Beam.prt) - for part in theSession.Parts: - part_type = type(part).__name__ - if "fem" not in part_type.lower() and "sim" not in part_type.lower(): - # This is the geometry part — look for mass expression - for expr in part.Expressions: - if expr.Name == "p173": - mass_value = expr.Value - print(f"[JOURNAL] Mass expression p173 = {mass_value}") - break - break + # Attempt 1: Read mass from expression p173 on geometry part + try: + for part in theSession.Parts: + part_type = type(part).__name__ + if "fem" not in part_type.lower() and "sim" not in part_type.lower(): + for expr in part.Expressions: + if expr.Name == "p173": + mass_value = expr.Value + print(f"[JOURNAL] Mass expression p173 = {mass_value}") + break + break + except Exception as expr_err: + print(f"[JOURNAL] Expression lookup failed: {expr_err}") + + # Attempt 2: Use MeasureManager (more reliable for derived/measurement expressions) + if mass_value is None: + print(f"[JOURNAL] Expression p173 not found, using MeasureManager fallback...") + geom_part = None + for part in theSession.Parts: + part_type = type(part).__name__ + if "fem" not in part_type.lower() and "sim" not in part_type.lower(): + geom_part = part + break + if geom_part is not None: + try: + mass_value = extract_part_mass(theSession, geom_part, working_dir) + print(f"[JOURNAL] MeasureManager mass = {mass_value}") + except Exception as mm_err: + print(f"[JOURNAL] MeasureManager failed: {mm_err}") + + # Write mass file (extract_part_mass may have already written it, but ensure consistency) if mass_value is not None: mass_file = os.path.join(working_dir, "_temp_mass.txt") with open(mass_file, "w") as f: f.write(f"p173={mass_value}\n") print(f"[JOURNAL] Wrote mass to {mass_file}") else: - print(f"[JOURNAL] WARNING: Could not find mass expression p173") + print(f"[JOURNAL] WARNING: Could not extract mass via expression or MeasureManager") except Exception as e: print(f"[JOURNAL] WARNING: Mass extraction failed: {e}") diff --git a/optimization_engine/nx/solver.py b/optimization_engine/nx/solver.py index edc8068d..8605cbab 100644 --- a/optimization_engine/nx/solver.py +++ b/optimization_engine/nx/solver.py @@ -605,7 +605,8 @@ sys.argv = ['', {argv_str}] # Set argv for the main function 'elapsed_time': elapsed_time, 'errors': errors, 'return_code': result.returncode, - 'solution_name': solution_name + 'solution_name': solution_name, + 'stdout': result.stdout if self.use_journal else '', } except subprocess.TimeoutExpired: 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 7e40da27..4761d507 100644 --- a/projects/hydrotech-beam/studies/01_doe_landscape/nx_interface.py +++ b/projects/hydrotech-beam/studies/01_doe_landscape/nx_interface.py @@ -363,8 +363,34 @@ class AtomizerNXSolver: op2_path = Path(op2_file) # Step 3: Extract mass from journal temp file + # The journal writes _temp_mass.txt to working_dir (= model_dir). + # The extractor looks in prt_file.parent (= model_dir). These MUST match. try: mass_kg = self._extract_mass(prt_file, expression_name=EXPR_MASS) + except FileNotFoundError: + # Fallback: parse mass from journal stdout captured in solve_result + # The journal prints "[JOURNAL] Mass expression p173 = " or + # "[JOURNAL] MeasureManager mass = " + mass_kg = float("nan") + stdout = solve_result.get("stdout", "") + if stdout: + import re + # Match either extraction method's output + m = re.search( + r'\[JOURNAL\]\s+(?:Mass expression p173|MeasureManager mass)\s*=\s*([0-9.eE+-]+)', + stdout, + ) + if m: + try: + mass_kg = float(m.group(1)) + logger.info("Mass recovered from journal stdout: %.6f kg", mass_kg) + except ValueError: + pass + if mass_kg != mass_kg: # NaN check + logger.warning( + "Mass temp file not found in %s and no mass in journal stdout", + self.model_dir, + ) except Exception as e: logger.warning("Mass extraction failed: %s", e) mass_kg = float("nan")