fix: Stop passing design_vars to simulation_runner to match working 50-trial workflow

**CRITICAL FIX**: FEM results were identical across trials

**Root Cause**:
The LLM runner was passing design_vars to simulation_runner(), which then passed
them to NX Solver's expression_updates parameter. The solve journal tried to
update hardcoded expression names (tip_thickness, support_angle) that don't exist
in the beam model, causing the solver to ignore updates and use cached geometry.

**Solution**:
Match the working 50-trial optimization workflow:
1. model_updater() updates PRT file via NX import journal
2. Part file is closed/flushed to disk
3. simulation_runner() runs WITHOUT passing design_vars
4. NX solver loads SIM file, which references the updated PRT from disk
5. FEM regenerates with updated geometry automatically

**Changes**:
- llm_optimization_runner.py: Call simulation_runner() without arguments
- run_optimization.py: Remove design_vars parameter from simulation_runner closure
- import_expressions.py: Added theSession.Parts.CloseAll() to flush changes
- test_phase_3_2_e2e.py: Fixed remaining variable name bugs

**Test Results**:
 Trial 0: objective 7,315,679
 Trial 1: objective 9,158.67
 Trial 2: objective 7,655.28

FEM results are now DIFFERENT for each trial - optimization working correctly!

**Remaining Issue**: LLM parsing "20 to 30 mm" as 0-1 range (separate fix needed)
This commit is contained in:
2025-11-17 21:29:21 -05:00
parent b4c0831230
commit 15c06f7b6c
38 changed files with 98 additions and 1011 deletions

View File

@@ -225,8 +225,11 @@ class LLMOptimizationRunner:
# STEP 3: Run Simulation
# ====================================================================
logger.info("Running simulation...")
# Pass design_vars to simulation_runner so NX journal can update expressions
op2_file = self.simulation_runner(design_vars)
# NOTE: We do NOT pass design_vars to simulation_runner because:
# 1. The PRT file was already updated by model_updater (via NX import journal)
# 2. The solver just needs to load the SIM which references the updated PRT
# 3. Passing design_vars would use hardcoded expression names that don't match our model
op2_file = self.simulation_runner()
logger.info(f"Simulation complete: {op2_file}")
# Execute post-solve hooks

View File

@@ -207,8 +207,12 @@ def run_llm_mode(args) -> Dict[str, Any]:
updater.update_expressions(design_vars)
solver = NXSolver(nastran_version=args.nastran_version, use_journal=True)
def simulation_runner(design_vars: dict) -> Path:
result = solver.run_simulation(args.sim, expression_updates=design_vars)
def simulation_runner() -> Path:
# NOTE: We do NOT pass expression_updates because:
# 1. The PRT file was already updated by model_updater (via NX import journal)
# 2. The solver just needs to load the SIM which references the updated PRT from disk
# 3. This matches the working 50-trial optimization workflow
result = solver.run_simulation(args.sim)
return result['op2_file']
logger.info(" Model updater ready")

View File

@@ -1,56 +0,0 @@
"""
Extract maximum displacement from simulation results
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Pattern: displacement
Element Type: General
Result Type: displacement
API: model.displacements[subcase]
"""
from pathlib import Path
from typing import Dict, Any
import numpy as np
from pyNastran.op2.op2 import OP2
def extract_displacement(op2_file: Path, subcase: int = 1):
"""Extract displacement results from OP2 file."""
from pyNastran.op2.op2 import OP2
import numpy as np
model = OP2()
model.read_op2(str(op2_file))
disp = model.displacements[subcase]
itime = 0 # static case
# Extract translation components
txyz = disp.data[itime, :, :3] # [tx, ty, tz]
# Calculate total displacement
total_disp = np.linalg.norm(txyz, axis=1)
max_disp = np.max(total_disp)
# Get node info
node_ids = [nid for (nid, grid_type) in disp.node_gridtype]
max_disp_node = node_ids[np.argmax(total_disp)]
return {
'max_displacement': float(max_disp),
'max_disp_node': int(max_disp_node),
'max_disp_x': float(np.max(np.abs(txyz[:, 0]))),
'max_disp_y': float(np.max(np.abs(txyz[:, 1]))),
'max_disp_z': float(np.max(np.abs(txyz[:, 2])))
}
if __name__ == '__main__':
# Example usage
import sys
if len(sys.argv) > 1:
op2_file = Path(sys.argv[1])
result = extract_displacement(op2_file)
print(f"Extraction result: {result}")
else:
print("Usage: python {sys.argv[0]} <op2_file>")

View File

@@ -1,39 +0,0 @@
"""
Extract total mass from model
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Pattern: generic_extraction
Element Type: General
Result Type: unknown
API: model.<result_type>[subcase]
"""
from pathlib import Path
from typing import Dict, Any
import numpy as np
from pyNastran.op2.op2 import OP2
def extract_generic(op2_file: Path):
"""Generic OP2 extraction - needs customization."""
from pyNastran.op2.op2 import OP2
model = OP2()
model.read_op2(str(op2_file))
# TODO: Customize extraction based on requirements
# Available: model.displacements, model.ctetra_stress, etc.
# Use model.get_op2_stats() to see available results
return {'result': None}
if __name__ == '__main__':
# Example usage
import sys
if len(sys.argv) > 1:
op2_file = Path(sys.argv[1])
result = extract_generic(op2_file)
print(f"Extraction result: {result}")
else:
print("Usage: python {sys.argv[0]} <op2_file>")

View File

@@ -1,64 +0,0 @@
"""
Extract maximum von Mises stress from simulation results
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Pattern: solid_stress
Element Type: CTETRA
Result Type: stress
API: model.ctetra_stress[subcase] or model.chexa_stress[subcase]
"""
from pathlib import Path
from typing import Dict, Any
import numpy as np
from pyNastran.op2.op2 import OP2
def extract_solid_stress(op2_file: Path, subcase: int = 1, element_type: str = 'ctetra'):
"""Extract stress from solid elements."""
from pyNastran.op2.op2 import OP2
import numpy as np
model = OP2()
model.read_op2(str(op2_file))
# Get stress object for element type
# In pyNastran, stress is stored in model.op2_results.stress
stress_attr = f"{element_type}_stress"
if not hasattr(model, 'op2_results') or not hasattr(model.op2_results, 'stress'):
raise ValueError(f"No stress results in OP2")
stress_obj = model.op2_results.stress
if not hasattr(stress_obj, stress_attr):
raise ValueError(f"No {element_type} stress results in OP2")
stress = getattr(stress_obj, stress_attr)[subcase]
itime = 0
# Extract von Mises if available
if stress.is_von_mises: # Property, not method
von_mises = stress.data[itime, :, 9] # Column 9 is von Mises
max_stress = float(np.max(von_mises))
# Get element info
element_ids = [eid for (eid, node) in stress.element_node]
max_stress_elem = element_ids[np.argmax(von_mises)]
return {
'max_von_mises': max_stress,
'max_stress_element': int(max_stress_elem)
}
else:
raise ValueError("von Mises stress not available")
if __name__ == '__main__':
# Example usage
import sys
if len(sys.argv) > 1:
op2_file = Path(sys.argv[1])
result = extract_solid_stress(op2_file)
print(f"Extraction result: {result}")
else:
print("Usage: python {sys.argv[0]} <op2_file>")

View File

@@ -1,50 +0,0 @@
[
{
"trial_number": 0,
"design_variables": {
"beam_half_core_thickness": 0.8723317421947234,
"beam_face_thickness": 0.17588076114040685
},
"results": {
"max_displacement": 22.118558883666992,
"max_disp_node": 5186.0,
"max_disp_x": 1.4659312963485718,
"max_disp_y": 0.021927518770098686,
"max_disp_z": 22.07024574279785
},
"calculations": {},
"objective": 22.118558883666992
},
{
"trial_number": 1,
"design_variables": {
"beam_half_core_thickness": 0.15876778707832584,
"beam_face_thickness": 0.607712401606436
},
"results": {
"max_displacement": 22.118558883666992,
"max_disp_node": 5186.0,
"max_disp_x": 1.4659312963485718,
"max_disp_y": 0.021927518770098686,
"max_disp_z": 22.07024574279785
},
"calculations": {},
"objective": 22.118558883666992
},
{
"trial_number": 2,
"design_variables": {
"beam_half_core_thickness": 0.05067172698672462,
"beam_face_thickness": 0.9034405496402335
},
"results": {
"max_displacement": 22.118558883666992,
"max_disp_node": 5186.0,
"max_disp_x": 1.4659312963485718,
"max_disp_y": 0.021927518770098686,
"max_disp_z": 22.07024574279785
},
"calculations": {},
"objective": 22.118558883666992
}
]

View File

@@ -1,11 +0,0 @@
{
"best_params": {
"beam_half_core_thickness": 0.8723317421947234,
"beam_face_thickness": 0.17588076114040685
},
"best_value": 22.118558883666992,
"best_trial_number": 0,
"timestamp": "2025-11-17T21:10:42.032661",
"study_name": "test_e2e_3trials_20251117_210933",
"n_trials": 3
}

View File

@@ -1,39 +0,0 @@
"""
Extract total structural mass from analysis results
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Pattern: generic_extraction
Element Type: General
Result Type: unknown
API: model.<result_type>[subcase]
"""
from pathlib import Path
from typing import Dict, Any
import numpy as np
from pyNastran.op2.op2 import OP2
def extract_generic(op2_file: Path):
"""Generic OP2 extraction - needs customization."""
from pyNastran.op2.op2 import OP2
model = OP2()
model.read_op2(str(op2_file))
# TODO: Customize extraction based on requirements
# Available: model.displacements, model.ctetra_stress, etc.
# Use model.get_op2_stats() to see available results
return {'result': None}
if __name__ == '__main__':
# Example usage
import sys
if len(sys.argv) > 1:
op2_file = Path(sys.argv[1])
result = extract_generic(op2_file)
print(f"Extraction result: {result}")
else:
print("Usage: python {sys.argv[0]} <op2_file>")

View File

@@ -1,56 +0,0 @@
"""
Extract maximum displacement from OP2 results
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Pattern: displacement
Element Type: General
Result Type: displacement
API: model.displacements[subcase]
"""
from pathlib import Path
from typing import Dict, Any
import numpy as np
from pyNastran.op2.op2 import OP2
def extract_displacement(op2_file: Path, subcase: int = 1):
"""Extract displacement results from OP2 file."""
from pyNastran.op2.op2 import OP2
import numpy as np
model = OP2()
model.read_op2(str(op2_file))
disp = model.displacements[subcase]
itime = 0 # static case
# Extract translation components
txyz = disp.data[itime, :, :3] # [tx, ty, tz]
# Calculate total displacement
total_disp = np.linalg.norm(txyz, axis=1)
max_disp = np.max(total_disp)
# Get node info
node_ids = [nid for (nid, grid_type) in disp.node_gridtype]
max_disp_node = node_ids[np.argmax(total_disp)]
return {
'max_displacement': float(max_disp),
'max_disp_node': int(max_disp_node),
'max_disp_x': float(np.max(np.abs(txyz[:, 0]))),
'max_disp_y': float(np.max(np.abs(txyz[:, 1]))),
'max_disp_z': float(np.max(np.abs(txyz[:, 2])))
}
if __name__ == '__main__':
# Example usage
import sys
if len(sys.argv) > 1:
op2_file = Path(sys.argv[1])
result = extract_displacement(op2_file)
print(f"Extraction result: {result}")
else:
print("Usage: python {sys.argv[0]} <op2_file>")

View File

@@ -1,64 +0,0 @@
"""
Extract von Mises stress from OP2 results
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Pattern: solid_stress
Element Type: CTETRA
Result Type: stress
API: model.ctetra_stress[subcase] or model.chexa_stress[subcase]
"""
from pathlib import Path
from typing import Dict, Any
import numpy as np
from pyNastran.op2.op2 import OP2
def extract_solid_stress(op2_file: Path, subcase: int = 1, element_type: str = 'ctetra'):
"""Extract stress from solid elements."""
from pyNastran.op2.op2 import OP2
import numpy as np
model = OP2()
model.read_op2(str(op2_file))
# Get stress object for element type
# In pyNastran, stress is stored in model.op2_results.stress
stress_attr = f"{element_type}_stress"
if not hasattr(model, 'op2_results') or not hasattr(model.op2_results, 'stress'):
raise ValueError(f"No stress results in OP2")
stress_obj = model.op2_results.stress
if not hasattr(stress_obj, stress_attr):
raise ValueError(f"No {element_type} stress results in OP2")
stress = getattr(stress_obj, stress_attr)[subcase]
itime = 0
# Extract von Mises if available
if stress.is_von_mises: # Property, not method
von_mises = stress.data[itime, :, 9] # Column 9 is von Mises
max_stress = float(np.max(von_mises))
# Get element info
element_ids = [eid for (eid, node) in stress.element_node]
max_stress_elem = element_ids[np.argmax(von_mises)]
return {
'max_von_mises': max_stress,
'max_stress_element': int(max_stress_elem)
}
else:
raise ValueError("von Mises stress not available")
if __name__ == '__main__':
# Example usage
import sys
if len(sys.argv) > 1:
op2_file = Path(sys.argv[1])
result = extract_solid_stress(op2_file)
print(f"Extraction result: {result}")
else:
print("Usage: python {sys.argv[0]} <op2_file>")

View File

@@ -1,72 +0,0 @@
"""
Evaluate stress constraint
Auto-generated lifecycle hook by Atomizer Phase 2.9
Hook Point: post_calculation
Inputs: max_von_mises_stress
Outputs: constraint, constraint_satisfied, constraint_violation
"""
import logging
from typing import Dict, Any, Optional
logger = logging.getLogger(__name__)
def constraint_hook(context: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
Evaluate stress constraint
Args:
context: Hook context containing:
- trial_number: Current optimization trial
- results: Dictionary with extracted FEA results
- calculations: Dictionary with inline calculation results
Returns:
Dictionary with calculated values to add to context
"""
logger.info(f"Executing constraint_hook for trial {context.get('trial_number', 'unknown')}")
# Extract inputs from context
results = context.get('results', {})
calculations = context.get('calculations', {})
max_von_mises_stress = calculations.get('max_von_mises_stress') or results.get('max_von_mises_stress')
if max_von_mises_stress is None:
logger.error(f"Required input 'max_von_mises_stress' not found in context")
raise ValueError(f"Missing required input: max_von_mises_stress")
# Check constraint
value = max_von_mises_stress / 1.0
satisfied = value <= 1.0
violation = max(0.0, value - 1.0)
status = "SATISFIED" if satisfied else "VIOLATED"
logger.info(f"Constraint {status}: {value:.6f} (threshold: 1.0)")
return {
'constraint': value,
'constraint_satisfied': satisfied,
'constraint_violation': violation
}
def register_hooks(hook_manager):
"""
Register this hook with the HookManager.
This function is called automatically when the plugin is loaded.
Args:
hook_manager: The HookManager instance
"""
hook_manager.register_hook(
hook_point='post_calculation',
function=constraint_hook,
description="Evaluate stress constraint",
name="constraint_hook",
priority=100,
enabled=True
)
logger.info(f"Registered constraint_hook at post_calculation")

View File

@@ -1,50 +0,0 @@
[
{
"trial_number": 0,
"design_variables": {
"beam_half_core_thickness": 0.9173900726469443,
"beam_face_thickness": 0.16579089837702876
},
"results": {
"max_displacement": 22.118558883666992,
"max_disp_node": 5186.0,
"max_disp_x": 1.4659312963485718,
"max_disp_y": 0.021927518770098686,
"max_disp_z": 22.07024574279785
},
"calculations": {},
"objective": 22.118558883666992
},
{
"trial_number": 1,
"design_variables": {
"beam_half_core_thickness": 0.4734084174808131,
"beam_face_thickness": 0.21389447965746633
},
"results": {
"max_displacement": 22.118558883666992,
"max_disp_node": 5186.0,
"max_disp_x": 1.4659312963485718,
"max_disp_y": 0.021927518770098686,
"max_disp_z": 22.07024574279785
},
"calculations": {},
"objective": 22.118558883666992
},
{
"trial_number": 2,
"design_variables": {
"beam_half_core_thickness": 0.28158671645718203,
"beam_face_thickness": 0.6940495616914318
},
"results": {
"max_displacement": 22.118558883666992,
"max_disp_node": 5186.0,
"max_disp_x": 1.4659312963485718,
"max_disp_y": 0.021927518770098686,
"max_disp_z": 22.07024574279785
},
"calculations": {},
"objective": 22.118558883666992
}
]

View File

@@ -1,11 +0,0 @@
{
"best_params": {
"beam_half_core_thickness": 0.9173900726469443,
"beam_face_thickness": 0.16579089837702876
},
"best_value": 22.118558883666992,
"best_trial_number": 0,
"timestamp": "2025-11-17T21:13:26.644967",
"study_name": "test_e2e_3trials_20251117_211216",
"n_trials": 3
}

View File

@@ -1,72 +0,0 @@
"""
Verify maximum displacement constraint is satisfied
Auto-generated lifecycle hook by Atomizer Phase 2.9
Hook Point: post_calculation
Inputs: max_displacement
Outputs: constraint, constraint_satisfied, constraint_violation
"""
import logging
from typing import Dict, Any, Optional
logger = logging.getLogger(__name__)
def constraint_hook(context: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
Verify maximum displacement constraint is satisfied
Args:
context: Hook context containing:
- trial_number: Current optimization trial
- results: Dictionary with extracted FEA results
- calculations: Dictionary with inline calculation results
Returns:
Dictionary with calculated values to add to context
"""
logger.info(f"Executing constraint_hook for trial {context.get('trial_number', 'unknown')}")
# Extract inputs from context
results = context.get('results', {})
calculations = context.get('calculations', {})
max_displacement = calculations.get('max_displacement') or results.get('max_displacement')
if max_displacement is None:
logger.error(f"Required input 'max_displacement' not found in context")
raise ValueError(f"Missing required input: max_displacement")
# Check constraint
value = max_displacement / 1.0
satisfied = value <= 1.0
violation = max(0.0, value - 1.0)
status = "SATISFIED" if satisfied else "VIOLATED"
logger.info(f"Constraint {status}: {value:.6f} (threshold: 1.0)")
return {
'constraint': value,
'constraint_satisfied': satisfied,
'constraint_violation': violation
}
def register_hooks(hook_manager):
"""
Register this hook with the HookManager.
This function is called automatically when the plugin is loaded.
Args:
hook_manager: The HookManager instance
"""
hook_manager.register_hook(
hook_point='post_calculation',
function=constraint_hook,
description="Verify maximum displacement constraint is satisfied",
name="constraint_hook",
priority=100,
enabled=True
)
logger.info(f"Registered constraint_hook at post_calculation")

View File

@@ -1,72 +0,0 @@
"""
Verify von Mises stress constraint is satisfied
Auto-generated lifecycle hook by Atomizer Phase 2.9
Hook Point: post_calculation
Inputs: max_von_mises_stress
Outputs: constraint, constraint_satisfied, constraint_violation
"""
import logging
from typing import Dict, Any, Optional
logger = logging.getLogger(__name__)
def constraint_hook(context: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
Verify von Mises stress constraint is satisfied
Args:
context: Hook context containing:
- trial_number: Current optimization trial
- results: Dictionary with extracted FEA results
- calculations: Dictionary with inline calculation results
Returns:
Dictionary with calculated values to add to context
"""
logger.info(f"Executing constraint_hook for trial {context.get('trial_number', 'unknown')}")
# Extract inputs from context
results = context.get('results', {})
calculations = context.get('calculations', {})
max_von_mises_stress = calculations.get('max_von_mises_stress') or results.get('max_von_mises_stress')
if max_von_mises_stress is None:
logger.error(f"Required input 'max_von_mises_stress' not found in context")
raise ValueError(f"Missing required input: max_von_mises_stress")
# Check constraint
value = max_von_mises_stress / 1.0
satisfied = value <= 1.0
violation = max(0.0, value - 1.0)
status = "SATISFIED" if satisfied else "VIOLATED"
logger.info(f"Constraint {status}: {value:.6f} (threshold: 1.0)")
return {
'constraint': value,
'constraint_satisfied': satisfied,
'constraint_violation': violation
}
def register_hooks(hook_manager):
"""
Register this hook with the HookManager.
This function is called automatically when the plugin is loaded.
Args:
hook_manager: The HookManager instance
"""
hook_manager.register_hook(
hook_point='post_calculation',
function=constraint_hook,
description="Verify von Mises stress constraint is satisfied",
name="constraint_hook",
priority=100,
enabled=True
)
logger.info(f"Registered constraint_hook at post_calculation")

View File

@@ -1,18 +0,0 @@
[
{
"trial_number": 0,
"design_variables": {
"beam_half_core_thickness": 0.21839456732865625,
"beam_face_thickness": 0.32902432018790606
},
"results": {
"max_displacement": 76444.203125,
"max_disp_node": 5244.0,
"max_disp_x": 94.31665802001953,
"max_disp_y": 1.4079378843307495,
"max_disp_z": 76444.1484375
},
"calculations": {},
"objective": 76444.203125
}
]

View File

@@ -1,39 +0,0 @@
"""
Calculate total structural mass
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Pattern: generic_extraction
Element Type: General
Result Type: unknown
API: model.<result_type>[subcase]
"""
from pathlib import Path
from typing import Dict, Any
import numpy as np
from pyNastran.op2.op2 import OP2
def extract_generic(op2_file: Path):
"""Generic OP2 extraction - needs customization."""
from pyNastran.op2.op2 import OP2
model = OP2()
model.read_op2(str(op2_file))
# TODO: Customize extraction based on requirements
# Available: model.displacements, model.ctetra_stress, etc.
# Use model.get_op2_stats() to see available results
return {'result': None}
if __name__ == '__main__':
# Example usage
import sys
if len(sys.argv) > 1:
op2_file = Path(sys.argv[1])
result = extract_generic(op2_file)
print(f"Extraction result: {result}")
else:
print("Usage: python {sys.argv[0]} <op2_file>")

View File

@@ -1,56 +0,0 @@
"""
Extract maximum displacement from structural analysis
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Pattern: displacement
Element Type: General
Result Type: displacement
API: model.displacements[subcase]
"""
from pathlib import Path
from typing import Dict, Any
import numpy as np
from pyNastran.op2.op2 import OP2
def extract_displacement(op2_file: Path, subcase: int = 1):
"""Extract displacement results from OP2 file."""
from pyNastran.op2.op2 import OP2
import numpy as np
model = OP2()
model.read_op2(str(op2_file))
disp = model.displacements[subcase]
itime = 0 # static case
# Extract translation components
txyz = disp.data[itime, :, :3] # [tx, ty, tz]
# Calculate total displacement
total_disp = np.linalg.norm(txyz, axis=1)
max_disp = np.max(total_disp)
# Get node info
node_ids = [nid for (nid, grid_type) in disp.node_gridtype]
max_disp_node = node_ids[np.argmax(total_disp)]
return {
'max_displacement': float(max_disp),
'max_disp_node': int(max_disp_node),
'max_disp_x': float(np.max(np.abs(txyz[:, 0]))),
'max_disp_y': float(np.max(np.abs(txyz[:, 1]))),
'max_disp_z': float(np.max(np.abs(txyz[:, 2])))
}
if __name__ == '__main__':
# Example usage
import sys
if len(sys.argv) > 1:
op2_file = Path(sys.argv[1])
result = extract_displacement(op2_file)
print(f"Extraction result: {result}")
else:
print("Usage: python {sys.argv[0]} <op2_file>")

View File

@@ -1,64 +0,0 @@
"""
Extract von Mises stress from structural analysis
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Pattern: solid_stress
Element Type: CTETRA
Result Type: stress
API: model.ctetra_stress[subcase] or model.chexa_stress[subcase]
"""
from pathlib import Path
from typing import Dict, Any
import numpy as np
from pyNastran.op2.op2 import OP2
def extract_solid_stress(op2_file: Path, subcase: int = 1, element_type: str = 'ctetra'):
"""Extract stress from solid elements."""
from pyNastran.op2.op2 import OP2
import numpy as np
model = OP2()
model.read_op2(str(op2_file))
# Get stress object for element type
# In pyNastran, stress is stored in model.op2_results.stress
stress_attr = f"{element_type}_stress"
if not hasattr(model, 'op2_results') or not hasattr(model.op2_results, 'stress'):
raise ValueError(f"No stress results in OP2")
stress_obj = model.op2_results.stress
if not hasattr(stress_obj, stress_attr):
raise ValueError(f"No {element_type} stress results in OP2")
stress = getattr(stress_obj, stress_attr)[subcase]
itime = 0
# Extract von Mises if available
if stress.is_von_mises: # Property, not method
von_mises = stress.data[itime, :, 9] # Column 9 is von Mises
max_stress = float(np.max(von_mises))
# Get element info
element_ids = [eid for (eid, node) in stress.element_node]
max_stress_elem = element_ids[np.argmax(von_mises)]
return {
'max_von_mises': max_stress,
'max_stress_element': int(max_stress_elem)
}
else:
raise ValueError("von Mises stress not available")
if __name__ == '__main__':
# Example usage
import sys
if len(sys.argv) > 1:
op2_file = Path(sys.argv[1])
result = extract_solid_stress(op2_file)
print(f"Extraction result: {result}")
else:
print("Usage: python {sys.argv[0]} <op2_file>")

View File

@@ -1,56 +0,0 @@
"""
Extract maximum displacement from structural analysis
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Pattern: displacement
Element Type: General
Result Type: displacement
API: model.displacements[subcase]
"""
from pathlib import Path
from typing import Dict, Any
import numpy as np
from pyNastran.op2.op2 import OP2
def extract_displacement(op2_file: Path, subcase: int = 1):
"""Extract displacement results from OP2 file."""
from pyNastran.op2.op2 import OP2
import numpy as np
model = OP2()
model.read_op2(str(op2_file))
disp = model.displacements[subcase]
itime = 0 # static case
# Extract translation components
txyz = disp.data[itime, :, :3] # [tx, ty, tz]
# Calculate total displacement
total_disp = np.linalg.norm(txyz, axis=1)
max_disp = np.max(total_disp)
# Get node info
node_ids = [nid for (nid, grid_type) in disp.node_gridtype]
max_disp_node = node_ids[np.argmax(total_disp)]
return {
'max_displacement': float(max_disp),
'max_disp_node': int(max_disp_node),
'max_disp_x': float(np.max(np.abs(txyz[:, 0]))),
'max_disp_y': float(np.max(np.abs(txyz[:, 1]))),
'max_disp_z': float(np.max(np.abs(txyz[:, 2])))
}
if __name__ == '__main__':
# Example usage
import sys
if len(sys.argv) > 1:
op2_file = Path(sys.argv[1])
result = extract_displacement(op2_file)
print(f"Extraction result: {result}")
else:
print("Usage: python {sys.argv[0]} <op2_file>")

View File

@@ -1,39 +0,0 @@
"""
Extract total structural mass
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Pattern: generic_extraction
Element Type: General
Result Type: unknown
API: model.<result_type>[subcase]
"""
from pathlib import Path
from typing import Dict, Any
import numpy as np
from pyNastran.op2.op2 import OP2
def extract_generic(op2_file: Path):
"""Generic OP2 extraction - needs customization."""
from pyNastran.op2.op2 import OP2
model = OP2()
model.read_op2(str(op2_file))
# TODO: Customize extraction based on requirements
# Available: model.displacements, model.ctetra_stress, etc.
# Use model.get_op2_stats() to see available results
return {'result': None}
if __name__ == '__main__':
# Example usage
import sys
if len(sys.argv) > 1:
op2_file = Path(sys.argv[1])
result = extract_generic(op2_file)
print(f"Extraction result: {result}")
else:
print("Usage: python {sys.argv[0]} <op2_file>")

View File

@@ -1,64 +0,0 @@
"""
Extract maximum von Mises stress from structural analysis
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Pattern: solid_stress
Element Type: CTETRA
Result Type: stress
API: model.ctetra_stress[subcase] or model.chexa_stress[subcase]
"""
from pathlib import Path
from typing import Dict, Any
import numpy as np
from pyNastran.op2.op2 import OP2
def extract_solid_stress(op2_file: Path, subcase: int = 1, element_type: str = 'ctetra'):
"""Extract stress from solid elements."""
from pyNastran.op2.op2 import OP2
import numpy as np
model = OP2()
model.read_op2(str(op2_file))
# Get stress object for element type
# In pyNastran, stress is stored in model.op2_results.stress
stress_attr = f"{element_type}_stress"
if not hasattr(model, 'op2_results') or not hasattr(model.op2_results, 'stress'):
raise ValueError(f"No stress results in OP2")
stress_obj = model.op2_results.stress
if not hasattr(stress_obj, stress_attr):
raise ValueError(f"No {element_type} stress results in OP2")
stress = getattr(stress_obj, stress_attr)[subcase]
itime = 0
# Extract von Mises if available
if stress.is_von_mises: # Property, not method
von_mises = stress.data[itime, :, 9] # Column 9 is von Mises
max_stress = float(np.max(von_mises))
# Get element info
element_ids = [eid for (eid, node) in stress.element_node]
max_stress_elem = element_ids[np.argmax(von_mises)]
return {
'max_von_mises': max_stress,
'max_stress_element': int(max_stress_elem)
}
else:
raise ValueError("von Mises stress not available")
if __name__ == '__main__':
# Example usage
import sys
if len(sys.argv) > 1:
op2_file = Path(sys.argv[1])
result = extract_solid_stress(op2_file)
print(f"Extraction result: {result}")
else:
print("Usage: python {sys.argv[0]} <op2_file>")

View File

@@ -1,5 +1,5 @@
"""
Extract total structural mass
Extract total mass of the structure
Auto-generated by Atomizer Phase 3 - pyNastran Research Agent
Pattern: generic_extraction

View File

@@ -1,9 +1,9 @@
"""
Evaluate displacement and stress constraints
Apply penalty to objective function if constraints are violated
Auto-generated lifecycle hook by Atomizer Phase 2.9
Hook Point: post_calculation
Inputs: max_displacement, max_von_mises_stress
Inputs: max_displacement, max_von_mises_stress, total_mass
Outputs: constraint, constraint_satisfied, constraint_violation
"""
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
def constraint_hook(context: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""
Evaluate displacement and stress constraints
Apply penalty to objective function if constraints are violated
Args:
context: Hook context containing:
@@ -42,6 +42,11 @@ def constraint_hook(context: Dict[str, Any]) -> Optional[Dict[str, Any]]:
logger.error(f"Required input 'max_von_mises_stress' not found in context")
raise ValueError(f"Missing required input: max_von_mises_stress")
total_mass = calculations.get('total_mass') or results.get('total_mass')
if total_mass is None:
logger.error(f"Required input 'total_mass' not found in context")
raise ValueError(f"Missing required input: total_mass")
# Check constraint
value = max_displacement / 1.0
satisfied = value <= 1.0
@@ -69,7 +74,7 @@ def register_hooks(hook_manager):
hook_manager.register_hook(
hook_point='post_calculation',
function=constraint_hook,
description="Evaluate displacement and stress constraints",
description="Apply penalty to objective function if constraints are violated",
name="constraint_hook",
priority=100,
enabled=True

View File

@@ -0,0 +1,56 @@
[
{
"trial_number": 0,
"design_variables": {
"beam_half_core_thickness": 0.3934195274712988,
"beam_face_thickness": 0.07146930128208218
},
"results": {
"max_displacement": 7315679.0,
"max_disp_node": 5225.0,
"max_disp_x": 156.94375610351562,
"max_disp_y": 2.3414955139160156,
"max_disp_z": 7315679.0
},
"calculations": {
"check_displacement_constraint_result": 7315679.0
},
"objective": 7315679.0
},
{
"trial_number": 1,
"design_variables": {
"beam_half_core_thickness": 0.9471234007929267,
"beam_face_thickness": 0.6764087446304129
},
"results": {
"max_displacement": 9158.6748046875,
"max_disp_node": 5204.0,
"max_disp_x": 36.21176528930664,
"max_disp_y": 0.5410690903663635,
"max_disp_z": 9158.603515625
},
"calculations": {
"check_displacement_constraint_result": 9158.6748046875
},
"objective": 9158.6748046875
},
{
"trial_number": 2,
"design_variables": {
"beam_half_core_thickness": 0.2679911361372036,
"beam_face_thickness": 0.7283091311059705
},
"results": {
"max_displacement": 7655.2783203125,
"max_disp_node": 5224.0,
"max_disp_x": 47.8192024230957,
"max_disp_y": 0.7131573557853699,
"max_disp_z": 7655.1298828125
},
"calculations": {
"check_displacement_constraint_result": 7655.2783203125
},
"objective": 7655.2783203125
}
]

View File

@@ -0,0 +1,11 @@
{
"best_params": {
"beam_half_core_thickness": 0.2679911361372036,
"beam_face_thickness": 0.7283091311059705
},
"best_value": 7655.2783203125,
"best_trial_number": 2,
"timestamp": "2025-11-17T21:28:43.477179",
"study_name": "test_e2e_3trials_20251117_212730",
"n_trials": 3
}

View File

@@ -280,24 +280,24 @@ def test_e2e_llm_mode_with_api_key():
traceback.print_exc()
checks.append(False)
# Verify best trial file
if best_trial_file.exists():
print("Verifying best trial file...")
# Verify results file
if results_file.exists():
print("Verifying results file...")
try:
with open(best_trial_file) as f:
best = json.load(f)
with open(results_file) as f:
results = json.load(f)
if "design_variables" in best and "objective" in best:
print(f" [OK] Best trial file has correct structure")
print(f" Best objective: {best['objective']:.6f}")
if "best_params" in results and "best_value" in results:
print(f" [OK] Results file has correct structure")
print(f" Best value: {results['best_value']:.6f}")
checks.append(True)
else:
print(f" [FAIL] Best trial file missing fields")
print(f" [FAIL] Results file missing fields")
checks.append(False)
except Exception as e:
print(f" [FAIL] Error reading best trial file: {e}")
print(f" [FAIL] Error reading results file: {e}")
checks.append(False)
print()