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:
@@ -83,6 +83,23 @@ interface IntrospectionResult {
|
||||
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) {
|
||||
const [result, setResult] = useState<IntrospectionResult | null>(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<BaselineRunResult | null>(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
|
||||
/>
|
||||
</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 */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
{isLoading ? (
|
||||
|
||||
Reference in New Issue
Block a user