Files
Atomizer/dashboard/api/app.py
Anto01 91fb929f6a refactor: Centralize NX and environment configuration in config.py
MAJOR IMPROVEMENT: Single source of truth for all system paths

Now to change NX version or Python environment, edit ONE file (config.py):
  NX_VERSION = "2412"           # Change this for NX updates
  PYTHON_ENV_NAME = "atomizer"  # Change this for env updates

All code automatically uses new paths - no manual file hunting!

New Central Configuration (config.py):
- NX_VERSION: Automatically updates all NX paths
- NX_INSTALLATION_DIR: Derived from version
- NX_RUN_JOURNAL: Path to run_journal.exe
- NX_MATERIAL_LIBRARY: Path to physicalmateriallibrary.xml
- NX_PYTHON_STUBS: Path to Python stubs for intellisense
- PYTHON_ENV_NAME: Python environment name
- PROJECT_ROOT: Auto-detected project root
- Helper functions: get_nx_journal_command(), validate_config(), print_config()

Updated Files to Use Config:
- optimization_engine/nx_updater.py: Uses NX_RUN_JOURNAL from config
- dashboard/api/app.py: Uses NX_RUN_JOURNAL from config
- Both have fallbacks if config unavailable

Benefits:
1. Change NX version in 1 place, not 10+ files
2. Automatic validation of paths on import
3. Helper functions for common operations
4. Clear error messages if paths missing
5. Easy to add new Simcenter versions

Future NX Update Process:
1. Edit config.py: NX_VERSION = "2506"
2. Run: python config.py (verify paths)
3. Done! All code uses NX 2506

Migration Scripts Included:
- migrate_to_config.py: Full migration with documentation
- apply_config_migration.py: Applied to update dashboard

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 14:31:33 -05:00

743 lines
22 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
# ====================
# Study Management API
# ====================
@app.route('/api/study/create', methods=['POST'])
def create_study():
"""
Create a new study with folder structure.
Request body:
{
"study_name": "my_new_study",
"description": "Optional description"
}
"""
try:
data = request.get_json()
study_name = data.get('study_name')
description = data.get('description', '')
if not study_name:
return jsonify({
'success': False,
'error': 'study_name is required'
}), 400
# Create study folder structure
study_dir = project_root / 'optimization_results' / study_name
if study_dir.exists():
return jsonify({
'success': False,
'error': f'Study {study_name} already exists'
}), 400
# Create directories
study_dir.mkdir(parents=True, exist_ok=True)
(study_dir / 'sim').mkdir(exist_ok=True)
(study_dir / 'results').mkdir(exist_ok=True)
# Create initial metadata
metadata = {
'study_name': study_name,
'description': description,
'created_at': datetime.now().isoformat(),
'status': 'created',
'has_sim_files': False,
'is_configured': False
}
metadata_path = study_dir / 'metadata.json'
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=2)
return jsonify({
'success': True,
'message': f'Study {study_name} created successfully',
'study_path': str(study_dir),
'sim_folder': str(study_dir / 'sim')
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/study/<study_name>/sim/files', methods=['GET'])
def list_sim_files(study_name: str):
"""
List all files in the study's sim/ folder.
Args:
study_name: Name of the study
"""
try:
study_dir = project_root / 'optimization_results' / study_name
sim_dir = study_dir / 'sim'
if not sim_dir.exists():
return jsonify({
'success': False,
'error': f'Study {study_name} does not exist'
}), 404
# List all files
files = []
for file_path in sim_dir.iterdir():
if file_path.is_file():
files.append({
'name': file_path.name,
'size': file_path.stat().st_size,
'extension': file_path.suffix,
'modified': datetime.fromtimestamp(file_path.stat().st_mtime).isoformat()
})
# Check for .sim file
has_sim = any(f['extension'] == '.sim' for f in files)
return jsonify({
'success': True,
'files': files,
'has_sim_file': has_sim,
'sim_folder': str(sim_dir)
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/study/<study_name>/explore', methods=['POST'])
def explore_sim_file(study_name: str):
"""
Explore the .sim file in the study folder to extract expressions.
Args:
study_name: Name of the study
"""
try:
study_dir = project_root / 'optimization_results' / study_name
sim_dir = study_dir / 'sim'
# Find .sim file
sim_files = list(sim_dir.glob('*.sim'))
if not sim_files:
return jsonify({
'success': False,
'error': 'No .sim file found in sim/ folder'
}), 404
sim_file = sim_files[0]
# Run NX journal to extract expressions
import subprocess
journal_script = project_root / 'dashboard' / 'scripts' / 'extract_expressions.py'
output_file = study_dir / 'expressions.json'
# Execute journal
# Import centralized NX paths
try:
import sys
from pathlib import Path as P
sys.path.insert(0, str(P(__file__).parent.parent.parent))
from config import NX_RUN_JOURNAL
nx_executable = str(NX_RUN_JOURNAL)
except ImportError:
# Fallback if config not available
nx_executable = r"C:\Program Files\Siemens\NX2412\NXBIN\run_journal.exe"
result = subprocess.run(
[nx_executable, str(journal_script), str(sim_file), str(output_file)],
capture_output=True,
text=True,
timeout=120
)
if result.returncode != 0:
return jsonify({
'success': False,
'error': f'NX journal failed: {result.stderr}'
}), 500
# Load extracted expressions
if not output_file.exists():
return jsonify({
'success': False,
'error': 'Expression extraction failed - no output file'
}), 500
with open(output_file, 'r') as f:
expressions = json.load(f)
return jsonify({
'success': True,
'sim_file': str(sim_file),
'expressions': expressions
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/study/<study_name>/config', methods=['GET'])
def get_study_config(study_name: str):
"""
Get the configuration for a study.
Args:
study_name: Name of the study
"""
try:
study_dir = project_root / 'optimization_results' / study_name
config_path = study_dir / 'config.json'
if not config_path.exists():
return jsonify({
'success': True,
'config': None,
'message': 'No configuration found for this study'
})
with open(config_path, 'r') as f:
config = json.load(f)
return jsonify({
'success': True,
'config': config
})
except Exception as e:
return jsonify({
'success': False,
'error': str(e)
}), 500
@app.route('/api/study/<study_name>/config', methods=['POST'])
def save_study_config(study_name: str):
"""
Save configuration for a study.
Args:
study_name: Name of the study
Request body:
{
"design_variables": [...],
"objectives": [...],
"constraints": [...],
"optimization_settings": {...}
}
"""
try:
study_dir = project_root / 'optimization_results' / study_name
if not study_dir.exists():
return jsonify({
'success': False,
'error': f'Study {study_name} does not exist'
}), 404
config = request.get_json()
config_path = study_dir / 'config.json'
# Save configuration
with open(config_path, 'w') as f:
json.dump(config, f, indent=2)
# Update metadata
metadata_path = study_dir / 'metadata.json'
if metadata_path.exists():
with open(metadata_path, 'r') as f:
metadata = json.load(f)
metadata['is_configured'] = True
metadata['last_modified'] = datetime.now().isoformat()
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=2)
return jsonify({
'success': True,
'message': f'Configuration saved for study {study_name}'
})
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:8080")
print("Access the dashboard at: http://localhost:8080")
print("="*60)
app.run(debug=True, host='0.0.0.0', port=8080, threaded=True)