""" 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))