feat: Add substudy system with live history tracking and workflow fixes
Major Features: - Hierarchical substudy system (like NX Solutions/Subcases) * Shared model files across all substudies * Independent configuration per substudy * Continuation support from previous substudies * Real-time incremental history updates - Live history tracking with optimization_history_incremental.json - Complete bracket_displacement_maximizing study with substudy examples Core Fixes: - Fixed expression update workflow to pass design_vars through simulation_runner * Restored working NX journal expression update mechanism * OP2 timestamp verification instead of file deletion * Resolved issue where all trials returned identical objective values - Fixed LLMOptimizationRunner to pass design variables to simulation runner - Enhanced NXSolver with timestamp-based file regeneration verification New Components: - optimization_engine/llm_optimization_runner.py - LLM-driven optimization runner - optimization_engine/optimization_setup_wizard.py - Phase 3.3 setup wizard - studies/bracket_displacement_maximizing/ - Complete substudy example * run_substudy.py - Substudy runner with continuation * run_optimization.py - Standalone optimization runner * config/substudy_template.json - Template for new substudies * substudies/coarse_exploration/ - 20-trial coarse search * substudies/fine_tuning/ - 50-trial refinement (continuation example) * SUBSTUDIES_README.md - Complete substudy documentation Technical Improvements: - Incremental history saving after each trial (optimization_history_incremental.json) - Expression update workflow: .prt update → NX journal receives values → geometry update → FEM update → solve - Trial indexing fix in substudy result saving - Updated README with substudy system documentation Testing: - Successfully ran 20-trial coarse_exploration substudy - Verified different objective values across trials (workflow fix validated) - Confirmed live history updates in real-time - Tested shared model file usage across substudies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -194,22 +194,16 @@ class NXSolver:
|
||||
print(f" Working dir: {working_dir}")
|
||||
print(f" Mode: {'Journal' if self.use_journal else 'Direct'}")
|
||||
|
||||
# Delete old result files (.op2, .log, .f06) to force fresh solve
|
||||
# (.dat file is needed by NX, don't delete it!)
|
||||
# (Otherwise NX may reuse cached results)
|
||||
files_to_delete = [op2_file, log_file, f06_file]
|
||||
# Record timestamps of old files BEFORE solving
|
||||
# We'll verify files are regenerated by checking timestamps AFTER solve
|
||||
# This is more reliable than deleting (which can fail due to file locking on Windows)
|
||||
old_op2_time = op2_file.stat().st_mtime if op2_file.exists() else None
|
||||
old_f06_time = f06_file.stat().st_mtime if f06_file.exists() else None
|
||||
old_log_time = log_file.stat().st_mtime if log_file.exists() else None
|
||||
|
||||
deleted_count = 0
|
||||
for old_file in files_to_delete:
|
||||
if old_file.exists():
|
||||
try:
|
||||
old_file.unlink()
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
print(f" Warning: Could not delete {old_file.name}: {e}")
|
||||
|
||||
if deleted_count > 0:
|
||||
print(f" Deleted {deleted_count} old result file(s) to force fresh solve")
|
||||
if old_op2_time:
|
||||
print(f" Found existing OP2 (modified: {time.ctime(old_op2_time)})")
|
||||
print(f" Will verify NX regenerates it with newer timestamp")
|
||||
|
||||
# Build command based on mode
|
||||
if self.use_journal and sim_file.suffix == '.sim':
|
||||
@@ -308,19 +302,41 @@ sys.argv = ['', {argv_str}] # Set argv for the main function
|
||||
for line in result.stderr.strip().split('\n')[:5]:
|
||||
print(f" {line}")
|
||||
|
||||
# Wait for output files to appear (journal mode runs solve in background)
|
||||
# Wait for output files to appear AND be regenerated (journal mode runs solve in background)
|
||||
if self.use_journal:
|
||||
max_wait = 30 # seconds - background solves can take time
|
||||
wait_start = time.time()
|
||||
print("[NX SOLVER] Waiting for solve to complete...")
|
||||
while not (f06_file.exists() and op2_file.exists()) and (time.time() - wait_start) < max_wait:
|
||||
|
||||
# Wait for files to exist AND have newer timestamps than before
|
||||
while (time.time() - wait_start) < max_wait:
|
||||
files_exist = f06_file.exists() and op2_file.exists()
|
||||
|
||||
if files_exist:
|
||||
# Verify files were regenerated (newer timestamps)
|
||||
new_op2_time = op2_file.stat().st_mtime
|
||||
new_f06_time = f06_file.stat().st_mtime
|
||||
|
||||
# If no old files, or new files are newer, we're done!
|
||||
if (old_op2_time is None or new_op2_time > old_op2_time) and \
|
||||
(old_f06_time is None or new_f06_time > old_f06_time):
|
||||
elapsed = time.time() - wait_start
|
||||
print(f"[NX SOLVER] Fresh output files detected after {elapsed:.1f}s")
|
||||
if old_op2_time:
|
||||
print(f" OP2 regenerated: {time.ctime(old_op2_time)} -> {time.ctime(new_op2_time)}")
|
||||
break
|
||||
|
||||
time.sleep(0.5)
|
||||
if (time.time() - wait_start) % 2 < 0.5: # Print every 2 seconds
|
||||
elapsed = time.time() - wait_start
|
||||
print(f" Waiting... ({elapsed:.0f}s)")
|
||||
print(f" Waiting for fresh results... ({elapsed:.0f}s)")
|
||||
|
||||
if f06_file.exists() and op2_file.exists():
|
||||
print(f"[NX SOLVER] Output files detected after {time.time() - wait_start:.1f}s")
|
||||
# Final check - warn if files weren't regenerated
|
||||
if op2_file.exists():
|
||||
current_op2_time = op2_file.stat().st_mtime
|
||||
if old_op2_time and current_op2_time <= old_op2_time:
|
||||
print(f" WARNING: OP2 file was NOT regenerated! (Still has old timestamp)")
|
||||
print(f" Old: {time.ctime(old_op2_time)}, Current: {time.ctime(current_op2_time)}")
|
||||
|
||||
# Check for completion
|
||||
success = self._check_solution_success(f06_file, log_file)
|
||||
|
||||
Reference in New Issue
Block a user