From 89694088a2c72db71cc68ecc6008712e1297949c Mon Sep 17 00:00:00 2001 From: Anto01 Date: Tue, 20 Jan 2026 14:50:50 -0500 Subject: [PATCH] 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 --- .../backend/api/routes/optimization.py | 147 ++++++++++++++++++ .../canvas/panels/IntrospectionPanel.tsx | 133 ++++++++++++++++ 2 files changed, 280 insertions(+) diff --git a/atomizer-dashboard/backend/api/routes/optimization.py b/atomizer-dashboard/backend/api/routes/optimization.py index af870ee8..e1234f64 100644 --- a/atomizer-dashboard/backend/api/routes/optimization.py +++ b/atomizer-dashboard/backend/api/routes/optimization.py @@ -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)}") +@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") async def get_nx_expressions(study_id: str): """ diff --git a/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx b/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx index 087f7b33..471177f9 100644 --- a/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx +++ b/atomizer-dashboard/frontend/src/components/canvas/panels/IntrospectionPanel.tsx @@ -83,6 +83,23 @@ interface IntrospectionResult { linked_parts?: Record; } +// 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) { const [result, setResult] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -91,6 +108,10 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection new Set(['expressions', 'extractors']) ); const [searchTerm, setSearchTerm] = useState(''); + + // Baseline run state + const [isRunningBaseline, setIsRunningBaseline] = useState(false); + const [baselineResult, setBaselineResult] = useState(null); const { addNode, nodes } = useCanvasStore(); @@ -136,6 +157,37 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection 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) => { setExpandedSections((prev) => { const next = new Set(prev); @@ -243,6 +295,87 @@ export function IntrospectionPanel({ filePath, studyId, onClose }: Introspection /> + {/* Run Baseline Button */} + {studyId && ( +
+ +

+ Creates result files (.op2, .f06) for testing extractors +

+
+ )} + + {/* Baseline Run Result */} + {baselineResult && ( +
+
+ {baselineResult.success ? ( +
+ ) : ( + + )} + + {baselineResult.message} + +
+ + {baselineResult.elapsed_time && ( +

+ Completed in {baselineResult.elapsed_time.toFixed(1)}s +

+ )} + + {baselineResult.result_files && ( +
+ {baselineResult.result_files.op2.length > 0 && ( +

+ OP2: {baselineResult.result_files.op2.join(', ')} +

+ )} + {baselineResult.result_files.f06.length > 0 && ( +

+ F06: {baselineResult.result_files.f06.join(', ')} +

+ )} + {baselineResult.result_files.bdf.length > 0 && ( +

+ BDF: {baselineResult.result_files.bdf.join(', ')} +

+ )} +
+ )} + + {baselineResult.error && ( +

{baselineResult.error}

+ )} +
+ )} + {/* Content */}
{isLoading ? (