feat(canvas): Add 'Run Baseline' FEA simulation feature to IntrospectionPanel
Backend:
- Add POST /api/optimization/studies/{study_id}/nx/run-baseline endpoint
- Creates trial_baseline folder in 2_iterations/
- Copies all model files and runs NXSolver
- Returns paths to result files (.op2, .f06, .bdf) for extractor testing
Frontend:
- Add 'Run Baseline Simulation' button to IntrospectionPanel
- Show progress spinner during simulation
- Display result files when complete (OP2, F06, BDF)
- Show error messages if simulation fails
This enables:
- Testing custom extractors against real FEA results
- Validating the simulation pipeline before optimization
- Inspecting boundary conditions and loads
This commit is contained in:
@@ -4577,6 +4577,153 @@ async def introspect_nx_model(study_id: str, force: bool = False):
|
|||||||
raise HTTPException(status_code=500, detail=f"Failed to introspect: {str(e)}")
|
raise HTTPException(status_code=500, detail=f"Failed to introspect: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/studies/{study_id}/nx/run-baseline")
|
||||||
|
async def run_baseline_simulation(study_id: str):
|
||||||
|
"""
|
||||||
|
Run a baseline FEA simulation with current/default parameter values.
|
||||||
|
|
||||||
|
This creates a 'baseline' trial folder in 2_iterations/ with:
|
||||||
|
- All NX model files copied
|
||||||
|
- Simulation run with baseline parameters
|
||||||
|
- Result files (.op2, .f06, .bdf) for extractor testing
|
||||||
|
|
||||||
|
Use this to:
|
||||||
|
1. Verify the FEA pipeline works before optimization
|
||||||
|
2. Get result files for testing custom extractors
|
||||||
|
3. Validate boundary conditions and loads are correct
|
||||||
|
|
||||||
|
Args:
|
||||||
|
study_id: Study identifier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON with baseline run status and paths to result files
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
study_dir = resolve_study_path(study_id)
|
||||||
|
print(f"[run-baseline] study_id={study_id}, study_dir={study_dir}")
|
||||||
|
|
||||||
|
if not study_dir.exists():
|
||||||
|
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||||
|
|
||||||
|
# Find model directory
|
||||||
|
model_dir = None
|
||||||
|
for possible_dir in [
|
||||||
|
study_dir / "1_setup" / "model",
|
||||||
|
study_dir / "1_model",
|
||||||
|
study_dir / "0_model",
|
||||||
|
study_dir / "model",
|
||||||
|
study_dir / "1_setup",
|
||||||
|
]:
|
||||||
|
if possible_dir.exists() and list(possible_dir.glob("*.sim")):
|
||||||
|
model_dir = possible_dir
|
||||||
|
break
|
||||||
|
|
||||||
|
if model_dir is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"No model directory with .sim file found for {study_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find .sim file
|
||||||
|
sim_files = list(model_dir.glob("*.sim"))
|
||||||
|
if not sim_files:
|
||||||
|
raise HTTPException(status_code=404, detail=f"No .sim file found in {model_dir}")
|
||||||
|
|
||||||
|
sim_file = sim_files[0]
|
||||||
|
print(f"[run-baseline] sim_file={sim_file}")
|
||||||
|
|
||||||
|
# Create baseline trial folder
|
||||||
|
iterations_dir = study_dir / "2_iterations"
|
||||||
|
iterations_dir.mkdir(exist_ok=True)
|
||||||
|
baseline_dir = iterations_dir / "trial_baseline"
|
||||||
|
|
||||||
|
# Clean up existing baseline if present
|
||||||
|
if baseline_dir.exists():
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
shutil.rmtree(baseline_dir)
|
||||||
|
baseline_dir.mkdir()
|
||||||
|
|
||||||
|
# Copy all model files to baseline folder
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
model_extensions = [".prt", ".fem", ".afm", ".sim", ".exp"]
|
||||||
|
copied_files = []
|
||||||
|
for ext in model_extensions:
|
||||||
|
for src_file in model_dir.glob(f"*{ext}"):
|
||||||
|
dst_file = baseline_dir / src_file.name
|
||||||
|
shutil.copy2(src_file, dst_file)
|
||||||
|
copied_files.append(src_file.name)
|
||||||
|
|
||||||
|
print(f"[run-baseline] Copied {len(copied_files)} files to baseline folder")
|
||||||
|
|
||||||
|
# Find the copied sim file in baseline dir
|
||||||
|
baseline_sim = baseline_dir / sim_file.name
|
||||||
|
|
||||||
|
# Try to run the solver
|
||||||
|
try:
|
||||||
|
from optimization_engine.nx.solver import NXSolver
|
||||||
|
|
||||||
|
solver = NXSolver(
|
||||||
|
nastran_version="2512",
|
||||||
|
timeout=600,
|
||||||
|
use_journal=True,
|
||||||
|
enable_session_management=True,
|
||||||
|
study_name=study_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"[run-baseline] Starting solver...")
|
||||||
|
result = solver.run_simulation(
|
||||||
|
sim_file=baseline_sim,
|
||||||
|
working_dir=baseline_dir,
|
||||||
|
cleanup=False, # Keep all files for inspection
|
||||||
|
expression_updates=None, # Use baseline values
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find result files
|
||||||
|
op2_files = list(baseline_dir.glob("*.op2"))
|
||||||
|
f06_files = list(baseline_dir.glob("*.f06"))
|
||||||
|
bdf_files = list(baseline_dir.glob("*.bdf")) + list(baseline_dir.glob("*.dat"))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": result.get("success", False),
|
||||||
|
"study_id": study_id,
|
||||||
|
"baseline_dir": str(baseline_dir),
|
||||||
|
"sim_file": str(baseline_sim),
|
||||||
|
"elapsed_time": result.get("elapsed_time"),
|
||||||
|
"result_files": {
|
||||||
|
"op2": [f.name for f in op2_files],
|
||||||
|
"f06": [f.name for f in f06_files],
|
||||||
|
"bdf": [f.name for f in bdf_files],
|
||||||
|
},
|
||||||
|
"errors": result.get("errors", []),
|
||||||
|
"message": "Baseline simulation complete"
|
||||||
|
if result.get("success")
|
||||||
|
else "Simulation failed",
|
||||||
|
}
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"study_id": study_id,
|
||||||
|
"baseline_dir": str(baseline_dir),
|
||||||
|
"error": f"NXSolver not available: {str(e)}",
|
||||||
|
"message": "Model files copied but solver not available. Run manually in NX.",
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"study_id": study_id,
|
||||||
|
"baseline_dir": str(baseline_dir),
|
||||||
|
"error": str(e),
|
||||||
|
"message": f"Solver execution failed: {str(e)}",
|
||||||
|
}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to run baseline: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/studies/{study_id}/nx/expressions")
|
@router.get("/studies/{study_id}/nx/expressions")
|
||||||
async def get_nx_expressions(study_id: str):
|
async def get_nx_expressions(study_id: str):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -83,6 +83,23 @@ interface IntrospectionResult {
|
|||||||
linked_parts?: Record<string, unknown>;
|
linked_parts?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Baseline run result interface
|
||||||
|
interface BaselineRunResult {
|
||||||
|
success: boolean;
|
||||||
|
study_id: string;
|
||||||
|
baseline_dir: string;
|
||||||
|
sim_file?: string;
|
||||||
|
elapsed_time?: number;
|
||||||
|
result_files?: {
|
||||||
|
op2: string[];
|
||||||
|
f06: string[];
|
||||||
|
bdf: string[];
|
||||||
|
};
|
||||||
|
errors?: string[];
|
||||||
|
error?: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function IntrospectionPanel({ filePath, studyId, onClose }: IntrospectionPanelProps) {
|
export function IntrospectionPanel({ filePath, studyId, onClose }: IntrospectionPanelProps) {
|
||||||
const [result, setResult] = useState<IntrospectionResult | null>(null);
|
const [result, setResult] = useState<IntrospectionResult | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -92,6 +109,10 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
|
|||||||
);
|
);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
// Baseline run state
|
||||||
|
const [isRunningBaseline, setIsRunningBaseline] = useState(false);
|
||||||
|
const [baselineResult, setBaselineResult] = useState<BaselineRunResult | null>(null);
|
||||||
|
|
||||||
const { addNode, nodes } = useCanvasStore();
|
const { addNode, nodes } = useCanvasStore();
|
||||||
|
|
||||||
const runIntrospection = useCallback(async () => {
|
const runIntrospection = useCallback(async () => {
|
||||||
@@ -136,6 +157,37 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
|
|||||||
runIntrospection();
|
runIntrospection();
|
||||||
}, [runIntrospection]);
|
}, [runIntrospection]);
|
||||||
|
|
||||||
|
// Run baseline FEA simulation
|
||||||
|
const runBaseline = useCallback(async () => {
|
||||||
|
if (!studyId) {
|
||||||
|
setError('Study ID required to run baseline');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRunningBaseline(true);
|
||||||
|
setBaselineResult(null);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/optimization/studies/${studyId}/nx/run-baseline`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
setBaselineResult(data);
|
||||||
|
|
||||||
|
if (!data.success && data.error) {
|
||||||
|
setError(`Baseline run failed: ${data.error}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e instanceof Error ? e.message : 'Failed to run baseline';
|
||||||
|
setError(msg);
|
||||||
|
console.error('Baseline run error:', e);
|
||||||
|
} finally {
|
||||||
|
setIsRunningBaseline(false);
|
||||||
|
}
|
||||||
|
}, [studyId]);
|
||||||
|
|
||||||
const toggleSection = (section: string) => {
|
const toggleSection = (section: string) => {
|
||||||
setExpandedSections((prev) => {
|
setExpandedSections((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
@@ -243,6 +295,87 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Run Baseline Button */}
|
||||||
|
{studyId && (
|
||||||
|
<div className="px-4 py-2 border-b border-dark-700">
|
||||||
|
<button
|
||||||
|
onClick={runBaseline}
|
||||||
|
disabled={isRunningBaseline}
|
||||||
|
className="w-full flex items-center justify-center gap-2 px-3 py-2
|
||||||
|
bg-emerald-600 hover:bg-emerald-500 disabled:bg-dark-700
|
||||||
|
text-white disabled:text-dark-400 text-sm font-medium
|
||||||
|
rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
{isRunningBaseline ? (
|
||||||
|
<>
|
||||||
|
<RefreshCw size={14} className="animate-spin" />
|
||||||
|
Running Baseline FEA...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Cpu size={14} />
|
||||||
|
Run Baseline Simulation
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<p className="text-xs text-dark-500 mt-1.5 text-center">
|
||||||
|
Creates result files (.op2, .f06) for testing extractors
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Baseline Run Result */}
|
||||||
|
{baselineResult && (
|
||||||
|
<div className={`mx-2 my-2 p-3 rounded-lg border ${
|
||||||
|
baselineResult.success
|
||||||
|
? 'bg-emerald-500/10 border-emerald-500/30'
|
||||||
|
: 'bg-red-500/10 border-red-500/30'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
{baselineResult.success ? (
|
||||||
|
<div className="w-2 h-2 rounded-full bg-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<AlertTriangle size={14} className="text-red-400" />
|
||||||
|
)}
|
||||||
|
<span className={`text-xs font-medium ${
|
||||||
|
baselineResult.success ? 'text-emerald-400' : 'text-red-400'
|
||||||
|
}`}>
|
||||||
|
{baselineResult.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{baselineResult.elapsed_time && (
|
||||||
|
<p className="text-xs text-dark-400 mb-1">
|
||||||
|
Completed in {baselineResult.elapsed_time.toFixed(1)}s
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{baselineResult.result_files && (
|
||||||
|
<div className="space-y-1">
|
||||||
|
{baselineResult.result_files.op2.length > 0 && (
|
||||||
|
<p className="text-xs text-dark-300">
|
||||||
|
<span className="text-emerald-400">OP2:</span> {baselineResult.result_files.op2.join(', ')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{baselineResult.result_files.f06.length > 0 && (
|
||||||
|
<p className="text-xs text-dark-300">
|
||||||
|
<span className="text-blue-400">F06:</span> {baselineResult.result_files.f06.join(', ')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{baselineResult.result_files.bdf.length > 0 && (
|
||||||
|
<p className="text-xs text-dark-300">
|
||||||
|
<span className="text-amber-400">BDF:</span> {baselineResult.result_files.bdf.join(', ')}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{baselineResult.error && (
|
||||||
|
<p className="text-xs text-red-400 mt-1">{baselineResult.error}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user