refactor: Archive experimental LLM features for MVP stability (Phase 1.1)
Moved experimental LLM integration code to optimization_engine/future/: - llm_optimization_runner.py - Runtime LLM API runner - llm_workflow_analyzer.py - Workflow analysis - inline_code_generator.py - Auto-generate calculations - hook_generator.py - Auto-generate hooks - report_generator.py - LLM report generation - extractor_orchestrator.py - Extractor orchestration Added comprehensive optimization_engine/future/README.md explaining: - MVP LLM strategy (Claude Code skills, not runtime LLM) - Why files were archived - When to revisit post-MVP - Production architecture reference Production runner confirmed: optimization_engine/runner.py is sole active runner. This establishes clear separation between: - Production code (stable, no runtime LLM dependencies) - Experimental code (archived for post-MVP exploration) Part of Phase 1: Core Stabilization & Organization for MVP Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -3,12 +3,15 @@ Optimization API endpoints
|
||||
Handles study status, history retrieval, and control operations
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
|
||||
from fastapi.responses import JSONResponse, FileResponse
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional
|
||||
import json
|
||||
import sys
|
||||
import sqlite3
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
|
||||
# Add project root to path
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
|
||||
@@ -307,12 +310,40 @@ async def get_optimization_history(study_id: str, limit: Optional[int] = None):
|
||||
except (ValueError, TypeError):
|
||||
params[param_name] = param_value
|
||||
|
||||
# Get user attributes (extracted results: mass, frequency, stress, displacement, etc.)
|
||||
cursor.execute("""
|
||||
SELECT key, value_json
|
||||
FROM trial_user_attributes
|
||||
WHERE trial_id = ?
|
||||
""", (trial_id,))
|
||||
user_attrs = {}
|
||||
for key, value_json in cursor.fetchall():
|
||||
try:
|
||||
user_attrs[key] = json.loads(value_json)
|
||||
except (ValueError, TypeError):
|
||||
user_attrs[key] = value_json
|
||||
|
||||
# Extract relevant metrics for results (mass, frequency, stress, displacement, etc.)
|
||||
results = {}
|
||||
if "mass" in user_attrs:
|
||||
results["mass"] = user_attrs["mass"]
|
||||
if "frequency" in user_attrs:
|
||||
results["frequency"] = user_attrs["frequency"]
|
||||
if "max_stress" in user_attrs:
|
||||
results["max_stress"] = user_attrs["max_stress"]
|
||||
if "max_displacement" in user_attrs:
|
||||
results["max_displacement"] = user_attrs["max_displacement"]
|
||||
# Fallback to first frequency from objectives if available
|
||||
if not results and len(values) > 0:
|
||||
results["first_frequency"] = values[0]
|
||||
|
||||
trials.append({
|
||||
"trial_number": trial_num,
|
||||
"objective": values[0] if len(values) > 0 else None, # Primary objective
|
||||
"objectives": values if len(values) > 1 else None, # All objectives for multi-objective
|
||||
"design_variables": params,
|
||||
"results": {"first_frequency": values[0]} if len(values) > 0 else {},
|
||||
"results": results,
|
||||
"user_attrs": user_attrs, # Include all user attributes
|
||||
"start_time": start_time,
|
||||
"end_time": end_time
|
||||
})
|
||||
@@ -488,3 +519,268 @@ async def get_pareto_front(study_id: str):
|
||||
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get Pareto front: {str(e)}")
|
||||
|
||||
@router.post("/studies")
|
||||
async def create_study(
|
||||
config: str = Form(...),
|
||||
prt_file: Optional[UploadFile] = File(None),
|
||||
sim_file: Optional[UploadFile] = File(None),
|
||||
fem_file: Optional[UploadFile] = File(None)
|
||||
):
|
||||
"""
|
||||
Create a new optimization study
|
||||
Accepts:
|
||||
- config: JSON string with study configuration
|
||||
- prt_file: NX part file (optional if using existing study)
|
||||
- sim_file: NX simulation file (optional)
|
||||
- fem_file: NX FEM file (optional)
|
||||
"""
|
||||
try:
|
||||
# Parse config
|
||||
config_data = json.loads(config)
|
||||
study_name = config_data.get("name") # Changed from study_name to name to match frontend
|
||||
|
||||
if not study_name:
|
||||
raise HTTPException(status_code=400, detail="name is required in config")
|
||||
|
||||
# Create study directory structure
|
||||
study_dir = STUDIES_DIR / study_name
|
||||
if study_dir.exists():
|
||||
raise HTTPException(status_code=400, detail=f"Study {study_name} already exists")
|
||||
|
||||
setup_dir = study_dir / "1_setup"
|
||||
model_dir = setup_dir / "model"
|
||||
results_dir = study_dir / "2_results"
|
||||
|
||||
setup_dir.mkdir(parents=True, exist_ok=True)
|
||||
model_dir.mkdir(parents=True, exist_ok=True)
|
||||
results_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Save config file
|
||||
config_file = setup_dir / "optimization_config.json"
|
||||
with open(config_file, 'w') as f:
|
||||
json.dump(config_data, f, indent=2)
|
||||
|
||||
# Save uploaded files
|
||||
files_saved = {}
|
||||
if prt_file:
|
||||
prt_path = model_dir / prt_file.filename
|
||||
with open(prt_path, 'wb') as f:
|
||||
content = await prt_file.read()
|
||||
f.write(content)
|
||||
files_saved['prt_file'] = str(prt_path)
|
||||
|
||||
if sim_file:
|
||||
sim_path = model_dir / sim_file.filename
|
||||
with open(sim_path, 'wb') as f:
|
||||
content = await sim_file.read()
|
||||
f.write(content)
|
||||
files_saved['sim_file'] = str(sim_path)
|
||||
|
||||
if fem_file:
|
||||
fem_path = model_dir / fem_file.filename
|
||||
with open(fem_path, 'wb') as f:
|
||||
content = await fem_file.read()
|
||||
f.write(content)
|
||||
files_saved['fem_file'] = str(fem_path)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=201,
|
||||
content={
|
||||
"status": "created",
|
||||
"study_id": study_name,
|
||||
"study_path": str(study_dir),
|
||||
"config_path": str(config_file),
|
||||
"files_saved": files_saved,
|
||||
"message": f"Study {study_name} created successfully. Ready to run optimization."
|
||||
}
|
||||
)
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid JSON in config: {str(e)}")
|
||||
except Exception as e:
|
||||
# Clean up on error
|
||||
if 'study_dir' in locals() and study_dir.exists():
|
||||
shutil.rmtree(study_dir)
|
||||
raise HTTPException(status_code=500, detail=f"Failed to create study: {str(e)}")
|
||||
|
||||
@router.post("/studies/{study_id}/convert-mesh")
|
||||
async def convert_study_mesh(study_id: str):
|
||||
"""
|
||||
Convert study mesh to GLTF for 3D visualization
|
||||
Creates a web-viewable 3D model with FEA results as vertex colors
|
||||
"""
|
||||
try:
|
||||
study_dir = STUDIES_DIR / study_id
|
||||
if not study_dir.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||
|
||||
# Import mesh converter
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
|
||||
from optimization_engine.mesh_converter import convert_study_mesh
|
||||
|
||||
# Convert mesh
|
||||
output_path = convert_study_mesh(study_dir)
|
||||
|
||||
if output_path and output_path.exists():
|
||||
return {
|
||||
"status": "success",
|
||||
"gltf_path": str(output_path),
|
||||
"gltf_url": f"/api/optimization/studies/{study_id}/mesh/model.gltf",
|
||||
"metadata_url": f"/api/optimization/studies/{study_id}/mesh/model.json",
|
||||
"message": "Mesh converted successfully"
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Mesh conversion failed")
|
||||
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to convert mesh: {str(e)}")
|
||||
|
||||
@router.get("/studies/{study_id}/mesh/{filename}")
|
||||
async def get_mesh_file(study_id: str, filename: str):
|
||||
"""
|
||||
Serve GLTF mesh files and metadata
|
||||
Supports .gltf, .bin, and .json files
|
||||
"""
|
||||
try:
|
||||
# Validate filename to prevent directory traversal
|
||||
if '..' in filename or '/' in filename or '\\' in filename:
|
||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||
|
||||
study_dir = STUDIES_DIR / study_id
|
||||
visualization_dir = study_dir / "3_visualization"
|
||||
|
||||
file_path = visualization_dir / filename
|
||||
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"File {filename} not found")
|
||||
|
||||
# Determine content type
|
||||
suffix = file_path.suffix.lower()
|
||||
content_types = {
|
||||
'.gltf': 'model/gltf+json',
|
||||
'.bin': 'application/octet-stream',
|
||||
'.json': 'application/json',
|
||||
'.glb': 'model/gltf-binary'
|
||||
}
|
||||
|
||||
content_type = content_types.get(suffix, 'application/octet-stream')
|
||||
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
media_type=content_type,
|
||||
filename=filename
|
||||
)
|
||||
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"File not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to serve mesh file: {str(e)}")
|
||||
|
||||
@router.post("/studies/{study_id}/generate-report")
|
||||
async def generate_report(
|
||||
study_id: str,
|
||||
format: str = "markdown",
|
||||
include_llm_summary: bool = False
|
||||
):
|
||||
"""
|
||||
Generate an optimization report in the specified format
|
||||
|
||||
Args:
|
||||
study_id: Study identifier
|
||||
format: Report format ('markdown', 'html', or 'pdf')
|
||||
include_llm_summary: Whether to include LLM-generated executive summary
|
||||
|
||||
Returns:
|
||||
Information about the generated report including download URL
|
||||
"""
|
||||
try:
|
||||
study_dir = STUDIES_DIR / study_id
|
||||
if not study_dir.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||
|
||||
# Validate format
|
||||
valid_formats = ['markdown', 'md', 'html', 'pdf']
|
||||
if format.lower() not in valid_formats:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid format. Must be one of: {', '.join(valid_formats)}")
|
||||
|
||||
# Import report generator
|
||||
sys.path.append(str(Path(__file__).parent.parent.parent.parent.parent))
|
||||
from optimization_engine.report_generator import generate_study_report
|
||||
|
||||
# Generate report
|
||||
output_path = generate_study_report(
|
||||
study_dir=study_dir,
|
||||
output_format=format.lower(),
|
||||
include_llm_summary=include_llm_summary
|
||||
)
|
||||
|
||||
if output_path and output_path.exists():
|
||||
# Get relative path for URL
|
||||
rel_path = output_path.relative_to(study_dir)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"format": format,
|
||||
"file_path": str(output_path),
|
||||
"download_url": f"/api/optimization/studies/{study_id}/reports/{output_path.name}",
|
||||
"file_size": output_path.stat().st_size,
|
||||
"message": f"Report generated successfully in {format} format"
|
||||
}
|
||||
else:
|
||||
raise HTTPException(status_code=500, detail="Report generation failed")
|
||||
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to generate report: {str(e)}")
|
||||
|
||||
@router.get("/studies/{study_id}/reports/{filename}")
|
||||
async def download_report(study_id: str, filename: str):
|
||||
"""
|
||||
Download a generated report file
|
||||
|
||||
Args:
|
||||
study_id: Study identifier
|
||||
filename: Report filename
|
||||
|
||||
Returns:
|
||||
Report file for download
|
||||
"""
|
||||
try:
|
||||
# Validate filename to prevent directory traversal
|
||||
if '..' in filename or '/' in filename or '\\' in filename:
|
||||
raise HTTPException(status_code=400, detail="Invalid filename")
|
||||
|
||||
study_dir = STUDIES_DIR / study_id
|
||||
results_dir = study_dir / "2_results"
|
||||
|
||||
file_path = results_dir / filename
|
||||
|
||||
if not file_path.exists():
|
||||
raise HTTPException(status_code=404, detail=f"Report file {filename} not found")
|
||||
|
||||
# Determine content type
|
||||
suffix = file_path.suffix.lower()
|
||||
content_types = {
|
||||
'.md': 'text/markdown',
|
||||
'.html': 'text/html',
|
||||
'.pdf': 'application/pdf',
|
||||
'.json': 'application/json'
|
||||
}
|
||||
|
||||
content_type = content_types.get(suffix, 'application/octet-stream')
|
||||
|
||||
return FileResponse(
|
||||
path=str(file_path),
|
||||
media_type=content_type,
|
||||
filename=filename,
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||
)
|
||||
|
||||
except FileNotFoundError:
|
||||
raise HTTPException(status_code=404, detail=f"Report file not found")
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to download report: {str(e)}")
|
||||
|
||||
@@ -3,33 +3,53 @@ import {
|
||||
LineChart, Line, ScatterChart, Scatter,
|
||||
XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Cell
|
||||
} from 'recharts';
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import { Card } from '../components/Card';
|
||||
import { MetricCard } from '../components/MetricCard';
|
||||
import { StudyCard } from '../components/StudyCard';
|
||||
import { useOptimizationWebSocket } from '../hooks/useWebSocket';
|
||||
import { apiClient } from '../api/client';
|
||||
import { Card } from '../components/common/Card';
|
||||
import { MetricCard } from '../components/dashboard/MetricCard';
|
||||
import { StudyCard } from '../components/dashboard/StudyCard';
|
||||
import { OptimizerPanel } from '../components/OptimizerPanel';
|
||||
import { ParetoPlot } from '../components/ParetoPlot';
|
||||
import { ParallelCoordinatesPlot } from '../components/ParallelCoordinatesPlot';
|
||||
import type { Study, Trial, ConvergenceDataPoint, ParameterSpaceDataPoint } from '../types';
|
||||
|
||||
interface DashboardProps {
|
||||
studies: Study[];
|
||||
selectedStudyId: string | null;
|
||||
onStudySelect: (studyId: string) => void;
|
||||
}
|
||||
|
||||
export default function Dashboard({ studies, selectedStudyId, onStudySelect }: DashboardProps) {
|
||||
const [trials, setTrials] = useState<Trial[]>([]);
|
||||
export default function Dashboard() {
|
||||
const [studies, setStudies] = useState<Study[]>([]);
|
||||
const [selectedStudyId, setSelectedStudyId] = useState<string | null>(null);
|
||||
const [allTrials, setAllTrials] = useState<Trial[]>([]);
|
||||
const [displayedTrials, setDisplayedTrials] = useState<Trial[]>([]);
|
||||
const [bestValue, setBestValue] = useState<number>(Infinity);
|
||||
const [prunedCount, setPrunedCount] = useState<number>(0);
|
||||
const [alerts, setAlerts] = useState<Array<{ id: number; type: 'success' | 'warning'; message: string }>>([]);
|
||||
const [alertIdCounter, setAlertIdCounter] = useState(0);
|
||||
const [expandedTrials, setExpandedTrials] = useState<Set<number>>(new Set());
|
||||
const [sortBy, setSortBy] = useState<'performance' | 'chronological'>('performance');
|
||||
|
||||
// Protocol 13: New state for metadata and Pareto front
|
||||
const [studyMetadata, setStudyMetadata] = useState<any>(null);
|
||||
const [paretoFront, setParetoFront] = useState<any[]>([]);
|
||||
|
||||
// Load studies on mount
|
||||
useEffect(() => {
|
||||
apiClient.getStudies()
|
||||
.then(data => {
|
||||
setStudies(data.studies);
|
||||
if (data.studies.length > 0) {
|
||||
// Check LocalStorage for last selected study
|
||||
const savedStudyId = localStorage.getItem('lastSelectedStudyId');
|
||||
const studyExists = data.studies.find(s => s.id === savedStudyId);
|
||||
|
||||
if (savedStudyId && studyExists) {
|
||||
setSelectedStudyId(savedStudyId);
|
||||
} else {
|
||||
const running = data.studies.find(s => s.status === 'running');
|
||||
setSelectedStudyId(running?.id || data.studies[0].id);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
}, []);
|
||||
|
||||
const showAlert = (type: 'success' | 'warning', message: string) => {
|
||||
const id = alertIdCounter;
|
||||
setAlertIdCounter(prev => prev + 1);
|
||||
@@ -40,54 +60,50 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
|
||||
};
|
||||
|
||||
// WebSocket connection
|
||||
const { isConnected } = useWebSocket({
|
||||
const { connectionStatus } = useOptimizationWebSocket({
|
||||
studyId: selectedStudyId,
|
||||
onTrialCompleted: (trial) => {
|
||||
setTrials(prev => [trial, ...prev].slice(0, 20));
|
||||
setAllTrials(prev => [...prev, trial]);
|
||||
if (trial.objective < bestValue) {
|
||||
setBestValue(trial.objective);
|
||||
showAlert('success', `New best: ${trial.objective.toFixed(4)} (Trial #${trial.trial_number})`);
|
||||
onMessage: (msg) => {
|
||||
if (msg.type === 'trial_completed') {
|
||||
const trial = msg.data as Trial;
|
||||
setAllTrials(prev => [...prev, trial]);
|
||||
if (trial.objective !== null && trial.objective !== undefined && trial.objective < bestValue) {
|
||||
setBestValue(trial.objective);
|
||||
showAlert('success', `New best: ${trial.objective.toFixed(4)} (Trial #${trial.trial_number})`);
|
||||
}
|
||||
} else if (msg.type === 'trial_pruned') {
|
||||
setPrunedCount(prev => prev + 1);
|
||||
showAlert('warning', `Trial pruned: ${msg.data.pruning_cause}`);
|
||||
}
|
||||
},
|
||||
onNewBest: (trial) => {
|
||||
console.log('New best trial:', trial);
|
||||
},
|
||||
onTrialPruned: (pruned) => {
|
||||
setPrunedCount(prev => prev + 1);
|
||||
showAlert('warning', `Trial #${pruned.trial_number} pruned: ${pruned.pruning_cause}`);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
// Load initial trial history when study changes
|
||||
useEffect(() => {
|
||||
if (selectedStudyId) {
|
||||
setTrials([]);
|
||||
setAllTrials([]);
|
||||
setBestValue(Infinity);
|
||||
setPrunedCount(0);
|
||||
setExpandedTrials(new Set());
|
||||
|
||||
// Fetch full history
|
||||
fetch(`/api/optimization/studies/${selectedStudyId}/history`)
|
||||
.then(res => res.json())
|
||||
// Save to LocalStorage
|
||||
localStorage.setItem('lastSelectedStudyId', selectedStudyId);
|
||||
|
||||
apiClient.getStudyHistory(selectedStudyId)
|
||||
.then(data => {
|
||||
const sortedTrials = data.trials.sort((a: Trial, b: Trial) => a.trial_number - b.trial_number);
|
||||
setAllTrials(sortedTrials);
|
||||
setTrials(sortedTrials.slice(-20).reverse());
|
||||
if (sortedTrials.length > 0) {
|
||||
const minObj = Math.min(...sortedTrials.map((t: Trial) => t.objective));
|
||||
const validTrials = data.trials.filter(t => t.objective !== null && t.objective !== undefined);
|
||||
setAllTrials(validTrials);
|
||||
if (validTrials.length > 0) {
|
||||
const minObj = Math.min(...validTrials.map(t => t.objective));
|
||||
setBestValue(minObj);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Failed to load history:', err));
|
||||
.catch(console.error);
|
||||
|
||||
// Fetch pruning count
|
||||
fetch(`/api/optimization/studies/${selectedStudyId}/pruning`)
|
||||
.then(res => res.json())
|
||||
apiClient.getStudyPruning(selectedStudyId)
|
||||
.then(data => {
|
||||
setPrunedCount(data.pruned_trials?.length || 0);
|
||||
})
|
||||
.catch(err => console.error('Failed to load pruning data:', err));
|
||||
.catch(console.error);
|
||||
|
||||
// Protocol 13: Fetch metadata
|
||||
fetch(`/api/optimization/studies/${selectedStudyId}/metadata`)
|
||||
@@ -97,12 +113,12 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
|
||||
})
|
||||
.catch(err => console.error('Failed to load metadata:', err));
|
||||
|
||||
// Protocol 13: Fetch Pareto front
|
||||
// Protocol 13: Fetch Pareto front (raw format for Protocol 13 components)
|
||||
fetch(`/api/optimization/studies/${selectedStudyId}/pareto-front`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.is_multi_objective) {
|
||||
setParetoFront(data.pareto_front);
|
||||
.then(paretoData => {
|
||||
if (paretoData.is_multi_objective && paretoData.pareto_front) {
|
||||
setParetoFront(paretoData.pareto_front);
|
||||
} else {
|
||||
setParetoFront([]);
|
||||
}
|
||||
@@ -111,42 +127,92 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
|
||||
}
|
||||
}, [selectedStudyId]);
|
||||
|
||||
// Prepare chart data
|
||||
const convergenceData: ConvergenceDataPoint[] = allTrials.map((trial, idx) => ({
|
||||
trial_number: trial.trial_number,
|
||||
objective: trial.objective,
|
||||
best_so_far: Math.min(...allTrials.slice(0, idx + 1).map(t => t.objective)),
|
||||
}));
|
||||
// Sort trials based on selected sort order
|
||||
useEffect(() => {
|
||||
let sorted = [...allTrials];
|
||||
if (sortBy === 'performance') {
|
||||
// Sort by objective (best first)
|
||||
sorted.sort((a, b) => {
|
||||
const aObj = a.objective ?? Infinity;
|
||||
const bObj = b.objective ?? Infinity;
|
||||
return aObj - bObj;
|
||||
});
|
||||
} else {
|
||||
// Chronological (newest first)
|
||||
sorted.sort((a, b) => b.trial_number - a.trial_number);
|
||||
}
|
||||
setDisplayedTrials(sorted);
|
||||
}, [allTrials, sortBy]);
|
||||
|
||||
const parameterSpaceData: ParameterSpaceDataPoint[] = allTrials.map(trial => {
|
||||
const params = Object.values(trial.design_variables);
|
||||
return {
|
||||
trial_number: trial.trial_number,
|
||||
x: params[0] || 0,
|
||||
y: params[1] || 0,
|
||||
objective: trial.objective,
|
||||
isBest: trial.objective === bestValue,
|
||||
};
|
||||
});
|
||||
// Auto-refresh polling (every 3 seconds) for trial history
|
||||
useEffect(() => {
|
||||
if (!selectedStudyId) return;
|
||||
|
||||
const refreshInterval = setInterval(() => {
|
||||
apiClient.getStudyHistory(selectedStudyId)
|
||||
.then(data => {
|
||||
const validTrials = data.trials.filter(t => t.objective !== null && t.objective !== undefined);
|
||||
setAllTrials(validTrials);
|
||||
if (validTrials.length > 0) {
|
||||
const minObj = Math.min(...validTrials.map(t => t.objective));
|
||||
setBestValue(minObj);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('Auto-refresh failed:', err));
|
||||
}, 3000); // Poll every 3 seconds
|
||||
|
||||
return () => clearInterval(refreshInterval);
|
||||
}, [selectedStudyId]);
|
||||
|
||||
// Prepare chart data with proper null/undefined handling
|
||||
const convergenceData: ConvergenceDataPoint[] = allTrials
|
||||
.filter(t => t.objective !== null && t.objective !== undefined)
|
||||
.sort((a, b) => a.trial_number - b.trial_number)
|
||||
.map((trial, idx, arr) => {
|
||||
const previousTrials = arr.slice(0, idx + 1);
|
||||
const validObjectives = previousTrials.map(t => t.objective).filter(o => o !== null && o !== undefined);
|
||||
return {
|
||||
trial_number: trial.trial_number,
|
||||
objective: trial.objective,
|
||||
best_so_far: validObjectives.length > 0 ? Math.min(...validObjectives) : trial.objective,
|
||||
};
|
||||
});
|
||||
|
||||
const parameterSpaceData: ParameterSpaceDataPoint[] = allTrials
|
||||
.filter(t => t.objective !== null && t.objective !== undefined && t.design_variables)
|
||||
.map(trial => {
|
||||
const params = Object.values(trial.design_variables);
|
||||
return {
|
||||
trial_number: trial.trial_number,
|
||||
x: params[0] || 0,
|
||||
y: params[1] || 0,
|
||||
objective: trial.objective,
|
||||
isBest: trial.objective === bestValue,
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate average objective
|
||||
const avgObjective = allTrials.length > 0
|
||||
? allTrials.reduce((sum, t) => sum + t.objective, 0) / allTrials.length
|
||||
const validObjectives = allTrials.filter(t => t.objective !== null && t.objective !== undefined).map(t => t.objective);
|
||||
const avgObjective = validObjectives.length > 0
|
||||
? validObjectives.reduce((sum, obj) => sum + obj, 0) / validObjectives.length
|
||||
: 0;
|
||||
|
||||
// Get parameter names
|
||||
const paramNames = allTrials.length > 0 ? Object.keys(allTrials[0].design_variables) : [];
|
||||
const paramNames = allTrials.length > 0 && allTrials[0].design_variables
|
||||
? Object.keys(allTrials[0].design_variables)
|
||||
: [];
|
||||
|
||||
// Helper: Format parameter label with unit from metadata
|
||||
const getParamLabel = (paramName: string, index: number): string => {
|
||||
if (!studyMetadata?.design_variables) {
|
||||
return paramName || `Parameter ${index + 1}`;
|
||||
}
|
||||
const dv = studyMetadata.design_variables.find((v: any) => v.name === paramName);
|
||||
if (dv && dv.unit) {
|
||||
return `${paramName} (${dv.unit})`;
|
||||
}
|
||||
return paramName || `Parameter ${index + 1}`;
|
||||
// Toggle trial expansion
|
||||
const toggleTrialExpansion = (trialNumber: number) => {
|
||||
setExpandedTrials(prev => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(trialNumber)) {
|
||||
newSet.delete(trialNumber);
|
||||
} else {
|
||||
newSet.add(trialNumber);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
// Export functions
|
||||
@@ -169,7 +235,7 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
|
||||
const rows = allTrials.map(t => [
|
||||
t.trial_number,
|
||||
t.objective,
|
||||
...paramNames.map(k => t.design_variables[k])
|
||||
...paramNames.map(k => t.design_variables?.[k] ?? '')
|
||||
].join(','));
|
||||
const csv = [headers, ...rows].join('\n');
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
@@ -183,7 +249,7 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="container mx-auto">
|
||||
{/* Alerts */}
|
||||
<div className="fixed top-4 right-4 z-50 space-y-2">
|
||||
{alerts.map(alert => (
|
||||
@@ -201,12 +267,24 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<header className="mb-8 flex items-center justify-between border-b border-dark-500 pb-4">
|
||||
<header className="mb-8 flex items-center justify-between border-b border-dark-600 pb-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-primary-400">Atomizer Dashboard</h1>
|
||||
<p className="text-dark-200 mt-2">Real-time optimization monitoring</p>
|
||||
<h1 className="text-3xl font-bold text-primary-400">Live Dashboard</h1>
|
||||
<p className="text-dark-300 mt-1">Real-time optimization monitoring</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (selectedStudyId) {
|
||||
window.open(`http://localhost:8080?study=${selectedStudyId}`, '_blank');
|
||||
}
|
||||
}}
|
||||
className="btn-secondary"
|
||||
disabled={!selectedStudyId}
|
||||
title="Open Optuna Dashboard (make sure it's running on port 8080)"
|
||||
>
|
||||
Optuna Dashboard
|
||||
</button>
|
||||
<button onClick={exportJSON} className="btn-secondary" disabled={allTrials.length === 0}>
|
||||
Export JSON
|
||||
</button>
|
||||
@@ -226,7 +304,7 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
|
||||
key={study.id}
|
||||
study={study}
|
||||
isActive={study.id === selectedStudyId}
|
||||
onClick={() => onStudySelect(study.id)}
|
||||
onClick={() => setSelectedStudyId(study.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -248,14 +326,6 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
|
||||
value={avgObjective > 0 ? avgObjective.toFixed(4) : '-'}
|
||||
valueColor="text-blue-400"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Connection"
|
||||
value={isConnected ? 'Connected' : 'Disconnected'}
|
||||
valueColor={isConnected ? 'text-green-400' : 'text-red-400'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4 mb-6">
|
||||
<MetricCard
|
||||
label="Pruned"
|
||||
value={prunedCount}
|
||||
@@ -264,25 +334,38 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
|
||||
</div>
|
||||
|
||||
{/* Protocol 13: Intelligent Optimizer & Pareto Front */}
|
||||
{selectedStudyId && (
|
||||
{selectedStudyId && paretoFront.length > 0 && studyMetadata && studyMetadata.objectives && (
|
||||
<div className="grid grid-cols-2 gap-6 mb-6">
|
||||
<OptimizerPanel studyId={selectedStudyId} />
|
||||
{paretoFront.length > 0 && studyMetadata && (
|
||||
<ParetoPlot
|
||||
paretoData={paretoFront}
|
||||
objectives={studyMetadata.objectives || []}
|
||||
/>
|
||||
)}
|
||||
<Card title="Optimizer Strategy">
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm text-dark-300">
|
||||
<span className="font-semibold text-dark-100">Algorithm:</span> {studyMetadata.sampler || 'NSGA-II'}
|
||||
</div>
|
||||
<div className="text-sm text-dark-300">
|
||||
<span className="font-semibold text-dark-100">Type:</span> Multi-objective
|
||||
</div>
|
||||
<div className="text-sm text-dark-300">
|
||||
<span className="font-semibold text-dark-100">Objectives:</span> {studyMetadata.objectives?.length || 2}
|
||||
</div>
|
||||
<div className="text-sm text-dark-300">
|
||||
<span className="font-semibold text-dark-100">Design Variables:</span> {studyMetadata.design_variables?.length || 0}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<ParetoPlot
|
||||
paretoData={paretoFront}
|
||||
objectives={studyMetadata.objectives}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Parallel Coordinates (full width for multi-objective) */}
|
||||
{paretoFront.length > 0 && studyMetadata && (
|
||||
{paretoFront.length > 0 && studyMetadata && studyMetadata.objectives && studyMetadata.design_variables && (
|
||||
<div className="mb-6">
|
||||
<ParallelCoordinatesPlot
|
||||
paretoData={paretoFront}
|
||||
objectives={studyMetadata.objectives || []}
|
||||
designVariables={studyMetadata.design_variables || []}
|
||||
objectives={studyMetadata.objectives}
|
||||
designVariables={studyMetadata.design_variables}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -344,14 +427,14 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
|
||||
dataKey="x"
|
||||
stroke="#94a3b8"
|
||||
name={paramNames[0] || 'X'}
|
||||
label={{ value: getParamLabel(paramNames[0], 0), position: 'insideBottom', offset: -5, fill: '#94a3b8' }}
|
||||
label={{ value: paramNames[0] || 'Parameter 1', position: 'insideBottom', offset: -5, fill: '#94a3b8' }}
|
||||
/>
|
||||
<YAxis
|
||||
type="number"
|
||||
dataKey="y"
|
||||
stroke="#94a3b8"
|
||||
name={paramNames[1] || 'Y'}
|
||||
label={{ value: getParamLabel(paramNames[1], 1), angle: -90, position: 'insideLeft', fill: '#94a3b8' }}
|
||||
label={{ value: paramNames[1] || 'Parameter 2', angle: -90, position: 'insideLeft', fill: '#94a3b8' }}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ strokeDasharray: '3 3' }}
|
||||
@@ -381,38 +464,149 @@ export default function Dashboard({ studies, selectedStudyId, onStudySelect }: D
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Trial Feed */}
|
||||
<Card title="Recent Trials">
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{trials.length > 0 ? (
|
||||
trials.map(trial => (
|
||||
<div
|
||||
key={trial.trial_number}
|
||||
className={`p-3 rounded-lg transition-all duration-200 ${
|
||||
trial.objective === bestValue
|
||||
? 'bg-green-900 border-l-4 border-green-400'
|
||||
: 'bg-dark-500 hover:bg-dark-400'
|
||||
{/* Trial History with Sort Controls */}
|
||||
<Card
|
||||
title={
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span>Trial History ({displayedTrials.length} trials)</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setSortBy('performance')}
|
||||
className={`px-3 py-1 rounded text-sm ${
|
||||
sortBy === 'performance'
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-dark-500 text-dark-200 hover:bg-dark-400'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="font-semibold text-primary-400">
|
||||
Trial #{trial.trial_number}
|
||||
</span>
|
||||
<span className={`font-mono text-lg ${
|
||||
trial.objective === bestValue ? 'text-green-400 font-bold' : 'text-dark-100'
|
||||
}`}>
|
||||
{trial.objective.toFixed(4)}
|
||||
</span>
|
||||
Best First
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSortBy('chronological')}
|
||||
className={`px-3 py-1 rounded text-sm ${
|
||||
sortBy === 'chronological'
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-dark-500 text-dark-200 hover:bg-dark-400'
|
||||
}`}
|
||||
>
|
||||
Newest First
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="space-y-2 max-h-[600px] overflow-y-auto">
|
||||
{displayedTrials.length > 0 ? (
|
||||
displayedTrials.map(trial => {
|
||||
const isExpanded = expandedTrials.has(trial.trial_number);
|
||||
const isBest = trial.objective === bestValue;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={trial.trial_number}
|
||||
className={`rounded-lg transition-all duration-200 cursor-pointer ${
|
||||
isBest
|
||||
? 'bg-green-900 border-l-4 border-green-400'
|
||||
: 'bg-dark-500 hover:bg-dark-400'
|
||||
}`}
|
||||
onClick={() => toggleTrialExpansion(trial.trial_number)}
|
||||
>
|
||||
{/* Collapsed View */}
|
||||
<div className="p-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="font-semibold text-primary-400">
|
||||
Trial #{trial.trial_number}
|
||||
{isBest && <span className="ml-2 text-xs bg-green-700 text-green-100 px-2 py-1 rounded">BEST</span>}
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`font-mono text-lg ${
|
||||
isBest ? 'text-green-400 font-bold' : 'text-dark-100'
|
||||
}`}>
|
||||
{trial.objective !== null && trial.objective !== undefined
|
||||
? trial.objective.toFixed(4)
|
||||
: 'N/A'}
|
||||
</span>
|
||||
<span className="text-dark-400 text-sm">
|
||||
{isExpanded ? '▼' : '▶'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Preview */}
|
||||
{!isExpanded && trial.results && Object.keys(trial.results).length > 0 && (
|
||||
<div className="text-xs text-primary-300 flex flex-wrap gap-3 mt-2">
|
||||
{trial.results.mass && (
|
||||
<span>Mass: {trial.results.mass.toFixed(2)}g</span>
|
||||
)}
|
||||
{trial.results.frequency && (
|
||||
<span>Freq: {trial.results.frequency.toFixed(2)}Hz</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Expanded View */}
|
||||
{isExpanded && (
|
||||
<div className="px-3 pb-3 space-y-3">
|
||||
{/* Design Variables */}
|
||||
{trial.design_variables && Object.keys(trial.design_variables).length > 0 && (
|
||||
<div className="border-t border-dark-400 pt-3">
|
||||
<h4 className="text-sm font-semibold text-dark-200 mb-2">Design Variables</h4>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
{Object.entries(trial.design_variables).map(([key, val]) => (
|
||||
<div key={key} className="flex justify-between bg-dark-600 px-2 py-1 rounded">
|
||||
<span className="text-dark-300">{key}:</span>
|
||||
<span className="text-dark-100 font-mono">{val.toFixed(4)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results */}
|
||||
{trial.results && Object.keys(trial.results).length > 0 && (
|
||||
<div className="border-t border-dark-400 pt-3">
|
||||
<h4 className="text-sm font-semibold text-dark-200 mb-2">Extracted Results</h4>
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
{Object.entries(trial.results).map(([key, val]) => (
|
||||
<div key={key} className="flex justify-between bg-dark-600 px-2 py-1 rounded">
|
||||
<span className="text-dark-300">{key}:</span>
|
||||
<span className="text-primary-300 font-mono">
|
||||
{typeof val === 'number' ? val.toFixed(4) : String(val)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* All User Attributes */}
|
||||
{trial.user_attrs && Object.keys(trial.user_attrs).length > 0 && (
|
||||
<div className="border-t border-dark-400 pt-3">
|
||||
<h4 className="text-sm font-semibold text-dark-200 mb-2">All Attributes</h4>
|
||||
<div className="max-h-48 overflow-y-auto">
|
||||
<pre className="text-xs text-dark-300 bg-dark-700 p-2 rounded">
|
||||
{JSON.stringify(trial.user_attrs, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Timestamps */}
|
||||
{trial.start_time && trial.end_time && (
|
||||
<div className="border-t border-dark-400 pt-3 text-xs text-dark-400">
|
||||
<div className="flex justify-between">
|
||||
<span>Duration:</span>
|
||||
<span>
|
||||
{((new Date(trial.end_time).getTime() - new Date(trial.start_time).getTime()) / 1000).toFixed(1)}s
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-dark-200 flex flex-wrap gap-3">
|
||||
{Object.entries(trial.design_variables).map(([key, val]) => (
|
||||
<span key={key}>
|
||||
<span className="text-dark-400">{key}:</span> {val.toFixed(3)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-center py-8 text-dark-300">
|
||||
No trials yet. Waiting for optimization to start...
|
||||
|
||||
Reference in New Issue
Block a user