feat: Add Insights tab to dashboard for physics visualizations

Dashboard integration for Study Insights module (SYS_16):
- Backend: New /api/insights/ routes for generating and viewing insights
- Frontend: New Insights.tsx page with Plotly visualization
- Navigation: Added Insights tab between Analysis and Results

Available insight types:
- Zernike WFE (wavefront error for mirrors)
- Stress Field (Von Mises stress contours)
- Modal Analysis (natural frequencies/mode shapes)
- Thermal Field (temperature distribution)
- Design Space (parameter-objective exploration)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-21 13:28:51 -05:00
parent 9aa5f6eb8c
commit d089003ced
5 changed files with 680 additions and 2 deletions

View File

@@ -12,7 +12,7 @@ import sys
# Add parent directory to path to import optimization_engine
sys.path.append(str(Path(__file__).parent.parent.parent.parent))
from api.routes import optimization, claude, terminal
from api.routes import optimization, claude, terminal, insights
from api.websocket import optimization_stream
# Create FastAPI app
@@ -36,6 +36,7 @@ app.include_router(optimization.router, prefix="/api/optimization", tags=["optim
app.include_router(optimization_stream.router, prefix="/api/ws", tags=["websocket"])
app.include_router(claude.router, prefix="/api/claude", tags=["claude"])
app.include_router(terminal.router, prefix="/api/terminal", tags=["terminal"])
app.include_router(insights.router, prefix="/api/insights", tags=["insights"])
@app.get("/")
async def root():

View File

@@ -0,0 +1,221 @@
"""
Study Insights API endpoints
Provides physics-focused visualizations for completed optimization trials
"""
from fastapi import APIRouter, HTTPException
from fastapi.responses import JSONResponse, HTMLResponse
from pathlib import Path
from typing import List, Dict, Optional
import json
import sys
# Add project root to path
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
router = APIRouter()
# Base studies directory
STUDIES_DIR = Path(__file__).parent.parent.parent.parent.parent / "studies"
def resolve_study_path(study_id: str) -> Path:
"""Find study folder by scanning all topic directories."""
# First check direct path (backwards compatibility)
direct_path = STUDIES_DIR / study_id
if direct_path.exists() and direct_path.is_dir():
if (direct_path / "1_setup").exists() or (direct_path / "optimization_config.json").exists():
return direct_path
# Scan topic folders for nested structure
for topic_dir in STUDIES_DIR.iterdir():
if topic_dir.is_dir() and not topic_dir.name.startswith('.'):
study_dir = topic_dir / study_id
if study_dir.exists() and study_dir.is_dir():
if (study_dir / "1_setup").exists() or (study_dir / "optimization_config.json").exists():
return study_dir
raise HTTPException(status_code=404, detail=f"Study not found: {study_id}")
@router.get("/studies/{study_id}/insights/available")
async def list_available_insights(study_id: str):
"""List all insight types that can be generated for this study."""
try:
study_path = resolve_study_path(study_id)
# Import insights module
from optimization_engine.insights import list_available_insights as get_available
available = get_available(study_path)
return {
"study_id": study_id,
"insights": available
}
except HTTPException:
raise
except ImportError as e:
return {
"study_id": study_id,
"insights": [],
"error": f"Insights module not available: {str(e)}"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/studies/{study_id}/insights/all")
async def list_all_insights():
"""List all registered insight types (regardless of availability for any study)."""
try:
from optimization_engine.insights import list_insights
return {
"insights": list_insights()
}
except ImportError as e:
return {
"insights": [],
"error": f"Insights module not available: {str(e)}"
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/studies/{study_id}/insights/generate/{insight_type}")
async def generate_insight(study_id: str, insight_type: str, trial_id: Optional[int] = None):
"""Generate a specific insight visualization.
Args:
study_id: Study identifier
insight_type: Type of insight (e.g., 'zernike_wfe', 'stress_field', 'design_space')
trial_id: Optional specific trial to analyze (defaults to best trial)
Returns:
JSON with plotly_figure data and summary statistics
"""
try:
study_path = resolve_study_path(study_id)
from optimization_engine.insights import get_insight, InsightConfig
insight = get_insight(insight_type, study_path)
if insight is None:
raise HTTPException(status_code=404, detail=f"Unknown insight type: {insight_type}")
if not insight.can_generate():
raise HTTPException(
status_code=400,
detail=f"Cannot generate {insight_type}: required data not found"
)
# Configure insight
config = InsightConfig(trial_id=trial_id)
# Generate
result = insight.generate(config)
if not result.success:
raise HTTPException(status_code=500, detail=result.error or "Generation failed")
return {
"success": True,
"insight_type": insight_type,
"study_id": study_id,
"html_path": str(result.html_path) if result.html_path else None,
"plotly_figure": result.plotly_figure,
"summary": result.summary
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/studies/{study_id}/insights/view/{insight_type}")
async def view_insight_html(study_id: str, insight_type: str):
"""Get the HTML content for an insight (for iframe embedding).
Returns the most recent generated HTML file for this insight type,
or generates one if none exists.
"""
try:
study_path = resolve_study_path(study_id)
insights_dir = study_path / "3_insights"
# Look for existing HTML files
if insights_dir.exists():
pattern = f"{insight_type}_*.html"
existing = list(insights_dir.glob(pattern))
if existing:
# Return most recent
newest = max(existing, key=lambda p: p.stat().st_mtime)
return HTMLResponse(content=newest.read_text(encoding='utf-8'))
# No existing file - generate one
from optimization_engine.insights import get_insight, InsightConfig
insight = get_insight(insight_type, study_path)
if insight is None:
raise HTTPException(status_code=404, detail=f"Unknown insight type: {insight_type}")
if not insight.can_generate():
raise HTTPException(
status_code=400,
detail=f"Cannot generate {insight_type}: required data not found"
)
result = insight.generate(InsightConfig())
if result.success and result.html_path:
return HTMLResponse(content=result.html_path.read_text(encoding='utf-8'))
else:
raise HTTPException(status_code=500, detail=result.error or "Generation failed")
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/studies/{study_id}/insights/generated")
async def list_generated_insights(study_id: str):
"""List all previously generated insight HTML files for a study."""
try:
study_path = resolve_study_path(study_id)
insights_dir = study_path / "3_insights"
if not insights_dir.exists():
return {"study_id": study_id, "files": []}
files = []
for html_file in insights_dir.glob("*.html"):
# Parse insight type from filename (e.g., "zernike_wfe_20251220_143022.html")
name = html_file.stem
parts = name.rsplit('_', 2) # Split from right to get type and timestamp
insight_type = parts[0] if len(parts) >= 3 else name
timestamp = f"{parts[-2]}_{parts[-1]}" if len(parts) >= 3 else None
files.append({
"filename": html_file.name,
"insight_type": insight_type,
"timestamp": timestamp,
"size_kb": round(html_file.stat().st_size / 1024, 1),
"modified": html_file.stat().st_mtime
})
# Sort by modification time (newest first)
files.sort(key=lambda x: x['modified'], reverse=True)
return {
"study_id": study_id,
"files": files
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))