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:
@@ -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():
|
||||
|
||||
221
atomizer-dashboard/backend/api/routes/insights.py
Normal file
221
atomizer-dashboard/backend/api/routes/insights.py
Normal 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))
|
||||
Reference in New Issue
Block a user