Files
Atomizer/atomizer-dashboard/backend/api/routes/optimization.py

841 lines
31 KiB
Python
Raw Normal View History

feat: Implement Protocol 13 - Real-Time Dashboard Tracking Complete implementation of Protocol 13 featuring real-time web dashboard for monitoring multi-objective optimization studies. ## New Features ### Backend (Python) - Real-time tracking system with per-trial JSON writes - New API endpoints for metadata, optimizer state, and Pareto fronts - Unit inference from objective descriptions - Multi-objective support using Optuna's best_trials API ### Frontend (React + TypeScript) - OptimizerPanel: Real-time optimizer state (phase, strategy, progress) - ParetoPlot: Pareto front visualization with normalization toggle - 3 modes: Raw, Min-Max [0-1], Z-Score standardization - Pareto front line connecting optimal points - ParallelCoordinatesPlot: High-dimensional interactive visualization - Objectives + design variables on parallel axes - Click-to-select, hover-to-highlight - Color-coded feasibility - Dynamic units throughout all visualizations ### Documentation - Comprehensive Protocol 13 guide with architecture, data flow, usage ## Files Added - `docs/PROTOCOL_13_DASHBOARD.md` - `atomizer-dashboard/frontend/src/components/OptimizerPanel.tsx` - `atomizer-dashboard/frontend/src/components/ParetoPlot.tsx` - `atomizer-dashboard/frontend/src/components/ParallelCoordinatesPlot.tsx` - `optimization_engine/realtime_tracking.py` ## Files Modified - `atomizer-dashboard/frontend/src/pages/Dashboard.tsx` - `atomizer-dashboard/backend/api/routes/optimization.py` - `optimization_engine/intelligent_optimizer.py` ## Testing - Tested with bracket_stiffness_optimization_V2 (30 trials, 20 Pareto solutions) - Dashboard running on localhost:3001 - All P1 and P2 features verified working 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 15:58:00 -05:00
"""
Optimization API endpoints
Handles study status, history retrieval, and control operations
"""
from fastapi import APIRouter, HTTPException, UploadFile, File, Form
from fastapi.responses import JSONResponse, FileResponse
feat: Implement Protocol 13 - Real-Time Dashboard Tracking Complete implementation of Protocol 13 featuring real-time web dashboard for monitoring multi-objective optimization studies. ## New Features ### Backend (Python) - Real-time tracking system with per-trial JSON writes - New API endpoints for metadata, optimizer state, and Pareto fronts - Unit inference from objective descriptions - Multi-objective support using Optuna's best_trials API ### Frontend (React + TypeScript) - OptimizerPanel: Real-time optimizer state (phase, strategy, progress) - ParetoPlot: Pareto front visualization with normalization toggle - 3 modes: Raw, Min-Max [0-1], Z-Score standardization - Pareto front line connecting optimal points - ParallelCoordinatesPlot: High-dimensional interactive visualization - Objectives + design variables on parallel axes - Click-to-select, hover-to-highlight - Color-coded feasibility - Dynamic units throughout all visualizations ### Documentation - Comprehensive Protocol 13 guide with architecture, data flow, usage ## Files Added - `docs/PROTOCOL_13_DASHBOARD.md` - `atomizer-dashboard/frontend/src/components/OptimizerPanel.tsx` - `atomizer-dashboard/frontend/src/components/ParetoPlot.tsx` - `atomizer-dashboard/frontend/src/components/ParallelCoordinatesPlot.tsx` - `optimization_engine/realtime_tracking.py` ## Files Modified - `atomizer-dashboard/frontend/src/pages/Dashboard.tsx` - `atomizer-dashboard/backend/api/routes/optimization.py` - `optimization_engine/intelligent_optimizer.py` ## Testing - Tested with bracket_stiffness_optimization_V2 (30 trials, 20 Pareto solutions) - Dashboard running on localhost:3001 - All P1 and P2 features verified working 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 15:58:00 -05:00
from pathlib import Path
from typing import List, Dict, Optional
import json
import sys
import sqlite3
import shutil
from datetime import datetime
feat: Implement Protocol 13 - Real-Time Dashboard Tracking Complete implementation of Protocol 13 featuring real-time web dashboard for monitoring multi-objective optimization studies. ## New Features ### Backend (Python) - Real-time tracking system with per-trial JSON writes - New API endpoints for metadata, optimizer state, and Pareto fronts - Unit inference from objective descriptions - Multi-objective support using Optuna's best_trials API ### Frontend (React + TypeScript) - OptimizerPanel: Real-time optimizer state (phase, strategy, progress) - ParetoPlot: Pareto front visualization with normalization toggle - 3 modes: Raw, Min-Max [0-1], Z-Score standardization - Pareto front line connecting optimal points - ParallelCoordinatesPlot: High-dimensional interactive visualization - Objectives + design variables on parallel axes - Click-to-select, hover-to-highlight - Color-coded feasibility - Dynamic units throughout all visualizations ### Documentation - Comprehensive Protocol 13 guide with architecture, data flow, usage ## Files Added - `docs/PROTOCOL_13_DASHBOARD.md` - `atomizer-dashboard/frontend/src/components/OptimizerPanel.tsx` - `atomizer-dashboard/frontend/src/components/ParetoPlot.tsx` - `atomizer-dashboard/frontend/src/components/ParallelCoordinatesPlot.tsx` - `optimization_engine/realtime_tracking.py` ## Files Modified - `atomizer-dashboard/frontend/src/pages/Dashboard.tsx` - `atomizer-dashboard/backend/api/routes/optimization.py` - `optimization_engine/intelligent_optimizer.py` ## Testing - Tested with bracket_stiffness_optimization_V2 (30 trials, 20 Pareto solutions) - Dashboard running on localhost:3001 - All P1 and P2 features verified working 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 15:58:00 -05:00
# 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"
@router.get("/studies")
async def list_studies():
"""List all available optimization studies"""
try:
studies = []
if not STUDIES_DIR.exists():
return {"studies": []}
for study_dir in STUDIES_DIR.iterdir():
if not study_dir.is_dir():
continue
# Look for optimization config (check multiple locations)
config_file = study_dir / "optimization_config.json"
if not config_file.exists():
config_file = study_dir / "1_setup" / "optimization_config.json"
if not config_file.exists():
continue
# Load config
with open(config_file) as f:
config = json.load(f)
# Check if results directory exists
results_dir = study_dir / "2_results"
# Check for Optuna database (Protocol 10) or JSON history (other protocols)
study_db = results_dir / "study.db"
history_file = results_dir / "optimization_history_incremental.json"
status = "not_started"
trial_count = 0
best_value = None
# Protocol 10: Read from Optuna SQLite database
if study_db.exists():
try:
conn = sqlite3.connect(str(study_db))
cursor = conn.cursor()
# Get trial count and status
cursor.execute("SELECT COUNT(*) FROM trials WHERE state = 'COMPLETE'")
trial_count = cursor.fetchone()[0]
# Get best trial (for single-objective, or first objective for multi-objective)
if trial_count > 0:
cursor.execute("""
SELECT value FROM trial_values
WHERE trial_id IN (
SELECT trial_id FROM trials WHERE state = 'COMPLETE'
)
ORDER BY value ASC
LIMIT 1
""")
result = cursor.fetchone()
if result:
best_value = result[0]
conn.close()
# Determine status
total_trials = config.get('optimization_settings', {}).get('n_trials', 50)
if trial_count >= total_trials:
status = "completed"
else:
status = "running" # Simplified - would need process check
except Exception as e:
print(f"Warning: Failed to read Optuna database for {study_dir.name}: {e}")
status = "error"
# Legacy: Read from JSON history
elif history_file.exists():
with open(history_file) as f:
history = json.load(f)
trial_count = len(history)
if history:
# Find best trial
best_trial = min(history, key=lambda x: x['objective'])
best_value = best_trial['objective']
# Determine status
total_trials = config.get('trials', {}).get('n_trials', 50)
if trial_count >= total_trials:
status = "completed"
else:
status = "running" # Simplified - would need process check
# Get total trials from config (supports both formats)
total_trials = (
config.get('optimization_settings', {}).get('n_trials') or
config.get('trials', {}).get('n_trials', 50)
)
studies.append({
"id": study_dir.name,
"name": study_dir.name.replace("_", " ").title(),
"status": status,
"progress": {
"current": trial_count,
"total": total_trials
},
"best_value": best_value,
"target": config.get('target', {}).get('value'),
"path": str(study_dir)
})
return {"studies": studies}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to list studies: {str(e)}")
@router.get("/studies/{study_id}/status")
async def get_study_status(study_id: str):
"""Get detailed status of a specific study"""
try:
study_dir = STUDIES_DIR / study_id
if not study_dir.exists():
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
# Load config (check multiple locations)
config_file = study_dir / "optimization_config.json"
if not config_file.exists():
config_file = study_dir / "1_setup" / "optimization_config.json"
with open(config_file) as f:
config = json.load(f)
# Check for results
results_dir = study_dir / "2_results"
study_db = results_dir / "study.db"
history_file = results_dir / "optimization_history_incremental.json"
# Protocol 10: Read from Optuna database
if study_db.exists():
conn = sqlite3.connect(str(study_db))
cursor = conn.cursor()
# Get trial counts by state
cursor.execute("SELECT COUNT(*) FROM trials WHERE state = 'COMPLETE'")
trial_count = cursor.fetchone()[0]
cursor.execute("SELECT COUNT(*) FROM trials WHERE state = 'PRUNED'")
pruned_count = cursor.fetchone()[0]
# Get best trial (first objective for multi-objective)
best_trial = None
if trial_count > 0:
cursor.execute("""
SELECT t.trial_id, t.number, tv.value
FROM trials t
JOIN trial_values tv ON t.trial_id = tv.trial_id
WHERE t.state = 'COMPLETE'
ORDER BY tv.value ASC
LIMIT 1
""")
result = cursor.fetchone()
if result:
trial_id, trial_number, best_value = result
# Get parameters for this trial
cursor.execute("""
SELECT param_name, param_value
FROM trial_params
WHERE trial_id = ?
""", (trial_id,))
params = {row[0]: row[1] for row in cursor.fetchall()}
best_trial = {
"trial_number": trial_number,
"objective": best_value,
"design_variables": params,
"results": {"first_frequency": best_value}
}
conn.close()
total_trials = config.get('optimization_settings', {}).get('n_trials', 50)
status = "completed" if trial_count >= total_trials else "running"
return {
"study_id": study_id,
"status": status,
"progress": {
"current": trial_count,
"total": total_trials,
"percentage": (trial_count / total_trials * 100) if total_trials > 0 else 0
},
"best_trial": best_trial,
"pruned_trials": pruned_count,
"config": config
}
# Legacy: Read from JSON history
if not history_file.exists():
return {
"study_id": study_id,
"status": "not_started",
"progress": {"current": 0, "total": config.get('trials', {}).get('n_trials', 50)},
"config": config
}
with open(history_file) as f:
history = json.load(f)
trial_count = len(history)
total_trials = config.get('trials', {}).get('n_trials', 50)
# Find best trial
best_trial = None
if history:
best_trial = min(history, key=lambda x: x['objective'])
# Check for pruning data
pruning_file = results_dir / "pruning_history.json"
pruned_count = 0
if pruning_file.exists():
with open(pruning_file) as f:
pruning_history = json.load(f)
pruned_count = len(pruning_history)
status = "completed" if trial_count >= total_trials else "running"
return {
"study_id": study_id,
"status": status,
"progress": {
"current": trial_count,
"total": total_trials,
"percentage": (trial_count / total_trials * 100) if total_trials > 0 else 0
},
"best_trial": best_trial,
"pruned_trials": pruned_count,
"config": config
}
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 get study status: {str(e)}")
@router.get("/studies/{study_id}/history")
async def get_optimization_history(study_id: str, limit: Optional[int] = None):
"""Get optimization history (all trials)"""
try:
study_dir = STUDIES_DIR / study_id
results_dir = study_dir / "2_results"
study_db = results_dir / "study.db"
history_file = results_dir / "optimization_history_incremental.json"
# Protocol 10: Read from Optuna database
if study_db.exists():
conn = sqlite3.connect(str(study_db))
cursor = conn.cursor()
# Get all completed trials
cursor.execute("""
SELECT trial_id, number, datetime_start, datetime_complete
FROM trials
WHERE state = 'COMPLETE'
ORDER BY number DESC
""" + (f" LIMIT {limit}" if limit else ""))
trial_rows = cursor.fetchall()
trials = []
for trial_id, trial_num, start_time, end_time in trial_rows:
# Get objectives for this trial
cursor.execute("""
SELECT value
FROM trial_values
WHERE trial_id = ?
ORDER BY objective
""", (trial_id,))
values = [row[0] for row in cursor.fetchall()]
# Get parameters for this trial
cursor.execute("""
SELECT param_name, param_value
FROM trial_params
WHERE trial_id = ?
""", (trial_id,))
params = {}
for param_name, param_value in cursor.fetchall():
try:
params[param_name] = float(param_value) if param_value is not None else 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]
# CRITICAL: Extract design_vars from user_attrs if stored there
# The optimization code does: trial.set_user_attr("design_vars", design_vars)
design_vars_from_attrs = user_attrs.get("design_vars", {})
# Merge with params (prefer user_attrs design_vars if available)
final_design_vars = {**params, **design_vars_from_attrs} if design_vars_from_attrs else params
feat: Implement Protocol 13 - Real-Time Dashboard Tracking Complete implementation of Protocol 13 featuring real-time web dashboard for monitoring multi-objective optimization studies. ## New Features ### Backend (Python) - Real-time tracking system with per-trial JSON writes - New API endpoints for metadata, optimizer state, and Pareto fronts - Unit inference from objective descriptions - Multi-objective support using Optuna's best_trials API ### Frontend (React + TypeScript) - OptimizerPanel: Real-time optimizer state (phase, strategy, progress) - ParetoPlot: Pareto front visualization with normalization toggle - 3 modes: Raw, Min-Max [0-1], Z-Score standardization - Pareto front line connecting optimal points - ParallelCoordinatesPlot: High-dimensional interactive visualization - Objectives + design variables on parallel axes - Click-to-select, hover-to-highlight - Color-coded feasibility - Dynamic units throughout all visualizations ### Documentation - Comprehensive Protocol 13 guide with architecture, data flow, usage ## Files Added - `docs/PROTOCOL_13_DASHBOARD.md` - `atomizer-dashboard/frontend/src/components/OptimizerPanel.tsx` - `atomizer-dashboard/frontend/src/components/ParetoPlot.tsx` - `atomizer-dashboard/frontend/src/components/ParallelCoordinatesPlot.tsx` - `optimization_engine/realtime_tracking.py` ## Files Modified - `atomizer-dashboard/frontend/src/pages/Dashboard.tsx` - `atomizer-dashboard/backend/api/routes/optimization.py` - `optimization_engine/intelligent_optimizer.py` ## Testing - Tested with bracket_stiffness_optimization_V2 (30 trials, 20 Pareto solutions) - Dashboard running on localhost:3001 - All P1 and P2 features verified working 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 15:58:00 -05:00
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": final_design_vars, # Use merged design vars
"results": results,
"user_attrs": user_attrs, # Include all user attributes
feat: Implement Protocol 13 - Real-Time Dashboard Tracking Complete implementation of Protocol 13 featuring real-time web dashboard for monitoring multi-objective optimization studies. ## New Features ### Backend (Python) - Real-time tracking system with per-trial JSON writes - New API endpoints for metadata, optimizer state, and Pareto fronts - Unit inference from objective descriptions - Multi-objective support using Optuna's best_trials API ### Frontend (React + TypeScript) - OptimizerPanel: Real-time optimizer state (phase, strategy, progress) - ParetoPlot: Pareto front visualization with normalization toggle - 3 modes: Raw, Min-Max [0-1], Z-Score standardization - Pareto front line connecting optimal points - ParallelCoordinatesPlot: High-dimensional interactive visualization - Objectives + design variables on parallel axes - Click-to-select, hover-to-highlight - Color-coded feasibility - Dynamic units throughout all visualizations ### Documentation - Comprehensive Protocol 13 guide with architecture, data flow, usage ## Files Added - `docs/PROTOCOL_13_DASHBOARD.md` - `atomizer-dashboard/frontend/src/components/OptimizerPanel.tsx` - `atomizer-dashboard/frontend/src/components/ParetoPlot.tsx` - `atomizer-dashboard/frontend/src/components/ParallelCoordinatesPlot.tsx` - `optimization_engine/realtime_tracking.py` ## Files Modified - `atomizer-dashboard/frontend/src/pages/Dashboard.tsx` - `atomizer-dashboard/backend/api/routes/optimization.py` - `optimization_engine/intelligent_optimizer.py` ## Testing - Tested with bracket_stiffness_optimization_V2 (30 trials, 20 Pareto solutions) - Dashboard running on localhost:3001 - All P1 and P2 features verified working 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 15:58:00 -05:00
"start_time": start_time,
"end_time": end_time
})
conn.close()
return {"trials": trials}
# Legacy: Read from JSON history
if not history_file.exists():
return {"trials": []}
with open(history_file) as f:
history = json.load(f)
# Apply limit if specified
if limit:
history = history[-limit:]
return {"trials": history}
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 get history: {str(e)}")
@router.get("/studies/{study_id}/pruning")
async def get_pruning_history(study_id: str):
"""Get pruning diagnostics"""
try:
study_dir = STUDIES_DIR / study_id
pruning_file = study_dir / "2_results" / "pruning_history.json"
if not pruning_file.exists():
return {"pruned_trials": []}
with open(pruning_file) as f:
pruning_history = json.load(f)
return {"pruned_trials": pruning_history}
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 get pruning history: {str(e)}")
def _infer_objective_unit(objective: Dict) -> str:
"""Infer unit from objective name and description"""
name = objective.get("name", "").lower()
desc = objective.get("description", "").lower()
# Common unit patterns
if "frequency" in name or "hz" in desc:
return "Hz"
elif "stiffness" in name or "n/mm" in desc:
return "N/mm"
elif "mass" in name or "kg" in desc:
return "kg"
elif "stress" in name or "mpa" in desc or "pa" in desc:
return "MPa"
elif "displacement" in name or "mm" in desc:
return "mm"
elif "force" in name or "newton" in desc:
return "N"
elif "%" in desc or "percent" in desc:
return "%"
# Check if unit is explicitly mentioned in description (e.g., "(N/mm)")
import re
unit_match = re.search(r'\(([^)]+)\)', desc)
if unit_match:
return unit_match.group(1)
return "" # No unit found
@router.get("/studies/{study_id}/metadata")
async def get_study_metadata(study_id: str):
"""Read optimization_config.json for objectives, design vars, units (Protocol 13)"""
try:
study_dir = STUDIES_DIR / study_id
if not study_dir.exists():
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
# Load config (check multiple locations)
config_file = study_dir / "optimization_config.json"
if not config_file.exists():
config_file = study_dir / "1_setup" / "optimization_config.json"
if not config_file.exists():
raise HTTPException(status_code=404, detail=f"Config file not found for study {study_id}")
with open(config_file) as f:
config = json.load(f)
# Enhance objectives with inferred units if not present
objectives = config.get("objectives", [])
for obj in objectives:
if "unit" not in obj or not obj["unit"]:
obj["unit"] = _infer_objective_unit(obj)
return {
"objectives": objectives,
"design_variables": config.get("design_variables", []),
"constraints": config.get("constraints", []),
"study_name": config.get("study_name", study_id),
"description": config.get("description", "")
}
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 get study metadata: {str(e)}")
@router.get("/studies/{study_id}/optimizer-state")
async def get_optimizer_state(study_id: str):
"""Read realtime optimizer state from intelligent_optimizer/ (Protocol 13)"""
try:
study_dir = STUDIES_DIR / study_id
results_dir = study_dir / "2_results"
state_file = results_dir / "intelligent_optimizer" / "optimizer_state.json"
if not state_file.exists():
return {"available": False}
with open(state_file) as f:
state = json.load(f)
return {"available": True, **state}
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 get optimizer state: {str(e)}")
@router.get("/studies/{study_id}/pareto-front")
async def get_pareto_front(study_id: str):
"""Get Pareto-optimal solutions for multi-objective studies (Protocol 13)"""
try:
study_dir = STUDIES_DIR / study_id
results_dir = study_dir / "2_results"
study_db = results_dir / "study.db"
if not study_db.exists():
return {"is_multi_objective": False, "pareto_front": []}
# Import optuna here to avoid loading it for all endpoints
import optuna
storage = optuna.storages.RDBStorage(f"sqlite:///{study_db}")
study = optuna.load_study(study_name=study_id, storage=storage)
# Check if multi-objective
if len(study.directions) == 1:
return {"is_multi_objective": False, "pareto_front": []}
# Get Pareto front
pareto_trials = study.best_trials
return {
"is_multi_objective": True,
"pareto_front": [
{
"trial_number": t.number,
"values": t.values,
"params": t.params,
"user_attrs": dict(t.user_attrs),
"constraint_satisfied": t.user_attrs.get("constraint_satisfied", True)
}
for t in pareto_trials
]
}
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 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.get("/studies/{study_id}/optuna-url")
async def get_optuna_dashboard_url(study_id: str):
"""
Get the Optuna dashboard URL for a specific study.
Returns the URL to access the study in Optuna dashboard.
The Optuna dashboard should be started with a relative path from the Atomizer root:
sqlite:///studies/{study_id}/2_results/study.db
"""
try:
study_dir = STUDIES_DIR / study_id
if not study_dir.exists():
raise HTTPException(status_code=404, detail=f"Study {study_id} not found")
results_dir = study_dir / "2_results"
study_db = results_dir / "study.db"
if not study_db.exists():
raise HTTPException(status_code=404, detail=f"No Optuna database found for study {study_id}")
# Get the study name from the database (may differ from folder name)
import optuna
storage = optuna.storages.RDBStorage(f"sqlite:///{study_db}")
studies = storage.get_all_studies()
if not studies:
raise HTTPException(status_code=404, detail=f"No Optuna study found in database for {study_id}")
# Use the actual study name from the database
optuna_study_name = studies[0].study_name
# Return URL info for the frontend
# The dashboard should be running on port 8081 with the correct database
return {
"study_id": study_id,
"optuna_study_name": optuna_study_name,
"database_path": f"studies/{study_id}/2_results/study.db",
"dashboard_url": f"http://localhost:8081/dashboard/studies/{studies[0]._study_id}",
"dashboard_base": "http://localhost:8081",
"note": "Optuna dashboard must be started with: sqlite:///studies/{study_id}/2_results/study.db"
}
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 get Optuna URL: {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)}")