Files
Atomizer/dashboard/api/app.py
Anto01 1dab9d638d feat: Add professional web-based optimization dashboard
Complete dashboard UI for controlling and monitoring optimization runs.

Backend API (Flask):
- RESTful endpoints for study management
- Start/stop/resume optimization runs
- Real-time status monitoring
- Configuration management
- Visualization data endpoints

Frontend (HTML/CSS/JS + Chart.js):
- Modern gradient design with cards and charts
- Study list sidebar with metadata
- Active optimizations monitoring (5s polling)
- Interactive charts (progress, design vars, constraints)
- Trial history table
- New optimization modal
- Resume/delete study actions

Features:
- List all studies with trial counts
- View detailed study results
- Start new optimizations from UI
- Resume existing studies with additional trials
- Real-time progress monitoring
- Delete unwanted studies
- Chart.js visualizations (progress, DVs, constraints)
- Configuration file selection
- Study metadata tracking

Usage:
  python dashboard/start_dashboard.py
  # Opens browser to http://localhost:5000

Dependencies:
  flask, flask-cors (auto-installed)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-15 13:37:33 -05:00

464 lines
14 KiB
Python

"""
Atomizer Dashboard API
RESTful API for controlling and monitoring optimization runs.
Provides endpoints for:
- Starting/stopping optimizations
- Managing studies (list, resume, delete)
- Real-time monitoring of progress
- Retrieving results and visualizations
"""
from flask import Flask, jsonify, request, send_from_directory
from flask_cors import CORS
import json
import sys
from pathlib import Path
from typing import Dict, List, Any
import threading
import time
from datetime import datetime
# Add project root to path
project_root = Path(__file__).parent.parent.parent
sys.path.insert(0, str(project_root))
from optimization_engine.runner import OptimizationRunner
app = Flask(__name__, static_folder='../frontend', static_url_path='')
CORS(app)
# Global state for running optimizations
active_optimizations = {}
optimization_lock = threading.Lock()
@app.route('/')
def index():
"""Serve the dashboard frontend."""
return send_from_directory(app.static_folder, 'index.html')
@app.route('/api/studies', methods=['GET'])
def list_studies():
"""
List all available optimization studies.
Returns:
JSON array of study metadata
"""
try:
# Use a dummy runner to access list_studies
config_path = project_root / 'examples/bracket/optimization_config_stress_displacement.json'
runner = OptimizationRunner(
config_path=config_path,
model_updater=lambda x: None,
simulation_runner=lambda: Path('dummy.op2'),
result_extractors={}
)
studies = runner.list_studies()
return jsonify({
'success': True,
'studies': studies
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/studies/<study_name>', methods=['GET'])
def get_study_details(study_name: str):
"""
Get detailed information about a specific study.
Args:
study_name: Name of the study
Returns:
JSON with study metadata, history, and summary
"""
try:
config_path = project_root / 'examples/bracket/optimization_config_stress_displacement.json'
runner = OptimizationRunner(
config_path=config_path,
model_updater=lambda x: None,
simulation_runner=lambda: Path('dummy.op2'),
result_extractors={}
)
output_dir = runner.output_dir
# Load history
history_path = output_dir / 'history.json'
history = []
if history_path.exists():
with open(history_path, 'r') as f:
history = json.load(f)
# Load summary
summary_path = output_dir / 'optimization_summary.json'
summary = {}
if summary_path.exists():
with open(summary_path, 'r') as f:
summary = json.load(f)
# Load metadata
metadata_path = runner._get_study_metadata_path(study_name)
metadata = {}
if metadata_path.exists():
with open(metadata_path, 'r') as f:
metadata = json.load(f)
return jsonify({
'success': True,
'study_name': study_name,
'metadata': metadata,
'history': history,
'summary': summary
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/studies/<study_name>/delete', methods=['DELETE'])
def delete_study(study_name: str):
"""
Delete a study and all its data.
Args:
study_name: Name of the study to delete
"""
try:
config_path = project_root / 'examples/bracket/optimization_config_stress_displacement.json'
runner = OptimizationRunner(
config_path=config_path,
model_updater=lambda x: None,
simulation_runner=lambda: Path('dummy.op2'),
result_extractors={}
)
# Delete database and metadata
db_path = runner._get_study_db_path(study_name)
metadata_path = runner._get_study_metadata_path(study_name)
if db_path.exists():
db_path.unlink()
if metadata_path.exists():
metadata_path.unlink()
return jsonify({
'success': True,
'message': f'Study {study_name} deleted successfully'
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/optimization/start', methods=['POST'])
def start_optimization():
"""
Start a new optimization run or resume an existing one.
Request body:
{
"study_name": "my_study",
"n_trials": 50,
"resume": false,
"config_path": "path/to/config.json"
}
"""
try:
data = request.get_json()
study_name = data.get('study_name', f'study_{datetime.now().strftime("%Y%m%d_%H%M%S")}')
n_trials = data.get('n_trials', 50)
resume = data.get('resume', False)
config_path = data.get('config_path', 'examples/bracket/optimization_config_stress_displacement.json')
with optimization_lock:
if study_name in active_optimizations:
return jsonify({
'success': False,
'error': f'Study {study_name} is already running'
}), 400
# Mark as active
active_optimizations[study_name] = {
'status': 'starting',
'start_time': datetime.now().isoformat(),
'n_trials': n_trials,
'current_trial': 0
}
# Start optimization in background thread
def run_optimization():
try:
# Import necessary functions
from examples.test_journal_optimization import (
bracket_model_updater,
bracket_simulation_runner,
stress_extractor,
displacement_extractor
)
runner = OptimizationRunner(
config_path=project_root / config_path,
model_updater=bracket_model_updater,
simulation_runner=bracket_simulation_runner,
result_extractors={
'stress_extractor': stress_extractor,
'displacement_extractor': displacement_extractor
}
)
with optimization_lock:
active_optimizations[study_name]['status'] = 'running'
study = runner.run(
study_name=study_name,
n_trials=n_trials,
resume=resume
)
with optimization_lock:
active_optimizations[study_name]['status'] = 'completed'
active_optimizations[study_name]['end_time'] = datetime.now().isoformat()
active_optimizations[study_name]['best_value'] = study.best_value
active_optimizations[study_name]['best_params'] = study.best_params
except Exception as e:
with optimization_lock:
active_optimizations[study_name]['status'] = 'failed'
active_optimizations[study_name]['error'] = str(e)
thread = threading.Thread(target=run_optimization, daemon=True)
thread.start()
return jsonify({
'success': True,
'message': f'Optimization {study_name} started',
'study_name': study_name
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/optimization/status', methods=['GET'])
def get_optimization_status():
"""
Get status of all active optimizations.
Returns:
JSON with status of all running/recent optimizations
"""
with optimization_lock:
return jsonify({
'success': True,
'active_optimizations': active_optimizations
})
@app.route('/api/optimization/<study_name>/status', methods=['GET'])
def get_study_status(study_name: str):
"""
Get real-time status of a specific optimization.
Args:
study_name: Name of the study
"""
with optimization_lock:
if study_name not in active_optimizations:
# Try to get from history
try:
config_path = project_root / 'examples/bracket/optimization_config_stress_displacement.json'
runner = OptimizationRunner(
config_path=config_path,
model_updater=lambda x: None,
simulation_runner=lambda: Path('dummy.op2'),
result_extractors={}
)
history_path = runner.output_dir / 'history.json'
if history_path.exists():
with open(history_path, 'r') as f:
history = json.load(f)
return jsonify({
'success': True,
'status': 'completed',
'n_trials': len(history)
})
except:
pass
return jsonify({
'success': False,
'error': 'Study not found'
}), 404
return jsonify({
'success': True,
**active_optimizations[study_name]
})
@app.route('/api/config/load', methods=['GET'])
def load_config():
"""
Load optimization configuration from file.
Query params:
path: Path to config file (relative to project root)
"""
try:
config_path = request.args.get('path', 'examples/bracket/optimization_config_stress_displacement.json')
full_path = project_root / config_path
with open(full_path, 'r') as f:
config = json.load(f)
return jsonify({
'success': True,
'config': config,
'path': config_path
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/config/save', methods=['POST'])
def save_config():
"""
Save optimization configuration to file.
Request body:
{
"path": "path/to/config.json",
"config": {...}
}
"""
try:
data = request.get_json()
config_path = data.get('path')
config = data.get('config')
if not config_path or not config:
return jsonify({
'success': False,
'error': 'Missing path or config'
}), 400
full_path = project_root / config_path
full_path.parent.mkdir(parents=True, exist_ok=True)
with open(full_path, 'w') as f:
json.dump(config, f, indent=2)
return jsonify({
'success': True,
'message': f'Configuration saved to {config_path}'
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/results/visualization/<study_name>', methods=['GET'])
def get_visualization_data(study_name: str):
"""
Get data formatted for visualization (charts).
Args:
study_name: Name of the study
"""
try:
config_path = project_root / 'examples/bracket/optimization_config_stress_displacement.json'
runner = OptimizationRunner(
config_path=config_path,
model_updater=lambda x: None,
simulation_runner=lambda: Path('dummy.op2'),
result_extractors={}
)
history_path = runner.output_dir / 'history.json'
if not history_path.exists():
return jsonify({
'success': False,
'error': 'No history found for this study'
}), 404
with open(history_path, 'r') as f:
history = json.load(f)
# Format data for charts
trials = [entry['trial_number'] for entry in history]
objectives = {}
design_vars = {}
constraints = {}
for entry in history:
for obj_name, obj_value in entry['objectives'].items():
if obj_name not in objectives:
objectives[obj_name] = []
objectives[obj_name].append(obj_value)
for dv_name, dv_value in entry['design_variables'].items():
if dv_name not in design_vars:
design_vars[dv_name] = []
design_vars[dv_name].append(dv_value)
for const_name, const_value in entry['constraints'].items():
if const_name not in constraints:
constraints[const_name] = []
constraints[const_name].append(const_value)
# Calculate running best
total_objectives = [entry['total_objective'] for entry in history]
running_best = []
current_best = float('inf')
for val in total_objectives:
current_best = min(current_best, val)
running_best.append(current_best)
return jsonify({
'success': True,
'trials': trials,
'objectives': objectives,
'design_variables': design_vars,
'constraints': constraints,
'total_objectives': total_objectives,
'running_best': running_best
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
if __name__ == '__main__':
print("="*60)
print("ATOMIZER DASHBOARD API")
print("="*60)
print("Starting Flask server on http://localhost:5000")
print("Access the dashboard at: http://localhost:5000")
print("="*60)
app.run(debug=True, host='0.0.0.0', port=5000, threaded=True)