Phase 3.3: Multi-objective optimization fix, updated docs & Claude skill
- Fixed drone gimbal optimization to use proper semantic directions - Changed from ['minimize', 'minimize'] to ['minimize', 'maximize'] - Updated Claude skill (v2.0) with Phase 3.3 integration - Added centralized extractor library documentation - Added multi-objective optimization (Protocol 11) section - Added NX multi-solution protocol documentation - Added dashboard integration documentation - Fixed Pareto front degenerate issue with proper NSGA-II configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
# Analyze Optimization Workflow Skill
|
||||
|
||||
**Last Updated**: November 23, 2025
|
||||
**Version**: 2.0 - Phase 3.3 Integration
|
||||
|
||||
You are analyzing a structural optimization request for the Atomizer system.
|
||||
|
||||
When the user provides a request, break it down into atomic workflow steps and classify each step intelligently.
|
||||
@@ -7,10 +10,11 @@ When the user provides a request, break it down into atomic workflow steps and c
|
||||
## Step Types
|
||||
|
||||
**1. ENGINEERING FEATURES** - Complex FEA/CAE operations needing specialized knowledge:
|
||||
- Extract results from OP2 files (displacement, stress, strain, element forces, etc.)
|
||||
- Extract results from OP2 files using centralized extractors
|
||||
- Modify FEA properties (CBUSH/CBAR stiffness, PCOMP layup, material properties)
|
||||
- Run simulations (SOL101, SOL103, etc.)
|
||||
- Run simulations (SOL101 static, SOL103 modal, etc.)
|
||||
- Create/modify geometry in NX
|
||||
- Multi-solution workflows (static + modal, thermal + structural)
|
||||
|
||||
**2. INLINE CALCULATIONS** - Simple math operations (auto-generate Python):
|
||||
- Calculate average, min, max, sum
|
||||
@@ -23,18 +27,179 @@ When the user provides a request, break it down into atomic workflow steps and c
|
||||
- Filtering/aggregation logic
|
||||
|
||||
**4. OPTIMIZATION** - Algorithm and configuration:
|
||||
- Optuna, genetic algorithm, etc.
|
||||
- Single-objective: Optuna TPE, CMA-ES, Random Search
|
||||
- Multi-objective: NSGA-II (Protocol 11), NSGA-III, MOEA/D
|
||||
- Design variables and their ranges
|
||||
- Multi-objective vs single objective
|
||||
- Constraints and objectives
|
||||
|
||||
## Centralized Extractor Library
|
||||
|
||||
**Location**: `optimization_engine/extractors/`
|
||||
|
||||
Use these standardized extractors instead of custom OP2 code:
|
||||
|
||||
### Available Extractors:
|
||||
|
||||
1. **extract_displacement.py**
|
||||
- `extract_displacement(op2_file, subcase)` → max displacement in mm
|
||||
- Returns: `{'max_displacement': float, 'node_id': int}`
|
||||
|
||||
2. **extract_von_mises_stress.py**
|
||||
- `extract_solid_stress(op2_file, subcase, element_type)` → max von Mises stress in MPa
|
||||
- Element types: `'ctetra'`, `'chexa'`, `'cquad4'`, `'ctria3'`
|
||||
- Returns: `{'max_von_mises': float, 'element_id': int}`
|
||||
|
||||
3. **extract_frequency.py**
|
||||
- `extract_frequency(op2_file, subcase, mode_number)` → frequency in Hz
|
||||
- Returns: `{'frequency': float, 'mode': int, 'eigenvalue': float}`
|
||||
|
||||
4. **extract_mass_from_bdf.py**
|
||||
- `extract_mass_from_bdf(bdf_file)` → FEM mass in kg
|
||||
- Returns: `{'mass_kg': float, 'cg': [x, y, z], 'inertia': [[...]]}`
|
||||
|
||||
5. **extract_mass_from_expression.py**
|
||||
- `extract_mass_from_expression(prt_file, expression_name)` → CAD mass in kg
|
||||
- Returns: `float` (mass value from NX expression)
|
||||
|
||||
6. **op2_extractor.py** (Base Class)
|
||||
- `OP2Extractor` - Base class for custom extractors
|
||||
- Provides common OP2 file handling and error management
|
||||
|
||||
### Extractor Selection Guide:
|
||||
|
||||
- **Displacement** → Use `extract_displacement`
|
||||
- **Von Mises Stress** (solids/shells) → Use `extract_solid_stress`
|
||||
- **Natural Frequency** (modal analysis) → Use `extract_frequency`
|
||||
- **Mass** (from FEM) → Use `extract_mass_from_bdf`
|
||||
- **Mass** (from CAD geometry) → Use `extract_mass_from_expression`
|
||||
|
||||
## Multi-Objective Optimization (Protocol 11)
|
||||
|
||||
### NSGA-II Configuration:
|
||||
|
||||
```python
|
||||
from optuna.samplers import NSGAIISampler
|
||||
|
||||
study = optuna.create_study(
|
||||
study_name="study_name",
|
||||
storage="sqlite:///study.db",
|
||||
directions=['minimize', 'maximize'], # Semantic directions
|
||||
sampler=NSGAIISampler()
|
||||
)
|
||||
```
|
||||
|
||||
### Key Concepts:
|
||||
|
||||
- **Pareto Front**: Set of non-dominated solutions
|
||||
- **Semantic Directions**: `['minimize', 'maximize']` - NEVER use negative values
|
||||
- **Trade-offs**: Conflicting objectives (e.g., minimize mass vs maximize stiffness)
|
||||
- **Feasibility**: Solutions must satisfy all constraints
|
||||
- **Visualization**: Parallel Coordinates Plot + Pareto Front scatter plot
|
||||
|
||||
### Multi-Objective Return Format:
|
||||
|
||||
```python
|
||||
def objective(trial: optuna.Trial) -> tuple:
|
||||
"""
|
||||
Returns:
|
||||
(obj1, obj2): Tuple for NSGA-II
|
||||
"""
|
||||
# Extract objectives
|
||||
mass = extract_mass(...) # to minimize
|
||||
freq = extract_frequency(...) # to maximize
|
||||
|
||||
# Return POSITIVE values - directions handled by study config
|
||||
return (mass, freq)
|
||||
```
|
||||
|
||||
### Common Pitfall - AVOID:
|
||||
|
||||
```python
|
||||
# ❌ WRONG: Using negative values to simulate maximization
|
||||
return (mass, -frequency) # Creates degenerate Pareto front
|
||||
|
||||
# ✅ CORRECT: Use proper semantic directions
|
||||
return (mass, frequency) # Study configured with ['minimize', 'maximize']
|
||||
```
|
||||
|
||||
## NX Multi-Solution Protocol
|
||||
|
||||
**Critical Protocol**: When simulations have multiple solutions (e.g., Solution 1 = Static, Solution 2 = Modal):
|
||||
|
||||
### Required API:
|
||||
|
||||
```python
|
||||
# ✅ CORRECT: Use SolveAllSolutions() with Foreground mode
|
||||
result = nx_solver.run_simulation(
|
||||
sim_file=sim_file,
|
||||
working_dir=model_dir,
|
||||
expression_updates=design_vars,
|
||||
solution_name=None # Solve ALL solutions
|
||||
)
|
||||
```
|
||||
|
||||
### Why This Matters:
|
||||
|
||||
- `SolveChainOfSolutions()` with Background mode only solves the first solution
|
||||
- Causes stale OP2 files and identical results across trials
|
||||
- `SolveAllSolutions()` ensures all solutions complete before returning
|
||||
- See: `docs/NX_MULTI_SOLUTION_PROTOCOL.md`
|
||||
|
||||
## Dashboard Integration
|
||||
|
||||
### Visualization Features:
|
||||
|
||||
1. **Parallel Coordinates Plot**
|
||||
- Structure: Design Variables → Objectives → Constraints
|
||||
- Color-coded axes (blue/green/yellow)
|
||||
- Interactive trial selection
|
||||
- Feasibility coloring (green = feasible, red = infeasible)
|
||||
|
||||
2. **Pareto Front Plot**
|
||||
- 2D scatter for bi-objective problems
|
||||
- Shows trade-offs between objectives
|
||||
- Highlights non-dominated solutions
|
||||
|
||||
3. **Real-Time Updates**
|
||||
- WebSocket connection for live monitoring
|
||||
- Backend: FastAPI (port 8000)
|
||||
- Frontend: React + Vite (port 3003)
|
||||
|
||||
### Dashboard Access:
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
cd atomizer-dashboard/backend && python -m uvicorn api.main:app --reload
|
||||
|
||||
# Frontend
|
||||
cd atomizer-dashboard/frontend && npm run dev
|
||||
|
||||
# Access: http://localhost:3003
|
||||
```
|
||||
|
||||
## Important Distinctions
|
||||
|
||||
- "extract forces from 1D elements" → ENGINEERING FEATURE (needs pyNastran/OP2 knowledge)
|
||||
- "find average of forces" → INLINE CALCULATION (simple Python: sum/len)
|
||||
- "compare max to average and create metric" → POST-PROCESSING HOOK (custom logic)
|
||||
- Element forces vs Reaction forces are DIFFERENT (element internal forces vs nodal reactions)
|
||||
- CBUSH vs CBAR are different element types with different properties
|
||||
- Extract from OP2 vs Read from .prt expression are different domains
|
||||
### Element Types:
|
||||
- CBUSH vs CBAR vs CBEAM are different 1D elements
|
||||
- CQUAD4 vs CTRIA3 are shell elements
|
||||
- CTETRA vs CHEXA are solid elements
|
||||
|
||||
### Result Types:
|
||||
- Element forces vs Reaction forces are DIFFERENT
|
||||
- Von Mises stress vs Principal stress
|
||||
- Displacement magnitude vs Component displacement
|
||||
|
||||
### Data Sources:
|
||||
- OP2 file (FEA results) → Use extractors
|
||||
- .prt expression (CAD parameters) → Use `extract_mass_from_expression`
|
||||
- .fem/.bdf file (FEM model) → Use `extract_mass_from_bdf`
|
||||
- Multi-solution OP2 files: `solution_1.op2`, `solution_2.op2`
|
||||
|
||||
### Optimization Algorithms:
|
||||
- **TPE** (Tree-structured Parzen Estimator): Single-objective, adaptive
|
||||
- **NSGA-II**: Multi-objective, Pareto-based
|
||||
- **CMA-ES**: Single-objective, gradient-free
|
||||
- **Random Search**: Baseline comparison
|
||||
|
||||
## Output Format
|
||||
|
||||
@@ -44,80 +209,125 @@ Return a detailed JSON analysis with this structure:
|
||||
{
|
||||
"engineering_features": [
|
||||
{
|
||||
"action": "extract_1d_element_forces",
|
||||
"action": "extract_frequency",
|
||||
"domain": "result_extraction",
|
||||
"description": "Extract element forces from 1D elements (CBAR) in Z direction from OP2 file",
|
||||
"description": "Extract fundamental frequency from modal analysis using centralized extractor",
|
||||
"params": {
|
||||
"element_types": ["CBAR"],
|
||||
"result_type": "element_force",
|
||||
"direction": "Z"
|
||||
"extractor": "extract_frequency",
|
||||
"op2_file": "solution_2.op2",
|
||||
"subcase": 1,
|
||||
"mode_number": 1
|
||||
},
|
||||
"why_engineering": "Requires pyNastran library and OP2 file format knowledge"
|
||||
"why_engineering": "Uses centralized extractor library for standardized OP2 extraction"
|
||||
},
|
||||
{
|
||||
"action": "extract_mass",
|
||||
"domain": "result_extraction",
|
||||
"description": "Extract mass from CAD expression p173",
|
||||
"params": {
|
||||
"extractor": "extract_mass_from_expression",
|
||||
"prt_file": "Model.prt",
|
||||
"expression_name": "p173"
|
||||
},
|
||||
"why_engineering": "Reads NX expression value directly from part file"
|
||||
}
|
||||
],
|
||||
"inline_calculations": [
|
||||
{
|
||||
"action": "calculate_average",
|
||||
"description": "Calculate average of extracted forces",
|
||||
"action": "convert_units",
|
||||
"description": "Convert mass from kg to grams",
|
||||
"params": {
|
||||
"input": "forces_z",
|
||||
"operation": "mean"
|
||||
"input": "mass_kg",
|
||||
"operation": "multiply",
|
||||
"factor": 1000.0
|
||||
},
|
||||
"code_hint": "avg = sum(forces_z) / len(forces_z)"
|
||||
},
|
||||
{
|
||||
"action": "find_minimum",
|
||||
"description": "Find minimum force value",
|
||||
"params": {
|
||||
"input": "forces_z",
|
||||
"operation": "min"
|
||||
},
|
||||
"code_hint": "min_val = min(forces_z)"
|
||||
}
|
||||
],
|
||||
"post_processing_hooks": [
|
||||
{
|
||||
"action": "custom_objective_metric",
|
||||
"description": "Compare minimum to average and create objective metric to minimize",
|
||||
"params": {
|
||||
"inputs": ["min_force", "avg_force"],
|
||||
"formula": "min_force / avg_force",
|
||||
"objective": "minimize"
|
||||
},
|
||||
"why_hook": "Custom business logic that combines multiple calculations"
|
||||
"code_hint": "mass_g = mass_kg * 1000.0"
|
||||
}
|
||||
],
|
||||
"post_processing_hooks": [],
|
||||
"optimization": {
|
||||
"algorithm": "genetic_algorithm",
|
||||
"protocol": "protocol_11_multi_objective",
|
||||
"algorithm": "NSGAIISampler",
|
||||
"type": "multi_objective",
|
||||
"design_variables": [
|
||||
{
|
||||
"parameter": "cbar_stiffness_x",
|
||||
"type": "FEA_property",
|
||||
"element_type": "CBAR",
|
||||
"direction": "X"
|
||||
"parameter": "beam_thickness",
|
||||
"type": "NX_expression",
|
||||
"bounds": [5.0, 10.0],
|
||||
"unit": "mm"
|
||||
}
|
||||
],
|
||||
"objectives": [
|
||||
{
|
||||
"name": "mass",
|
||||
"type": "minimize",
|
||||
"target": "custom_objective_metric"
|
||||
"target": "mass_g",
|
||||
"unit": "g"
|
||||
},
|
||||
{
|
||||
"name": "frequency",
|
||||
"type": "maximize",
|
||||
"target": "fundamental_freq",
|
||||
"unit": "Hz"
|
||||
}
|
||||
]
|
||||
],
|
||||
"constraints": [
|
||||
{
|
||||
"name": "max_displacement",
|
||||
"type": "less_than",
|
||||
"threshold": 1.5,
|
||||
"unit": "mm"
|
||||
}
|
||||
],
|
||||
"multi_solution_workflow": {
|
||||
"required": true,
|
||||
"solutions": ["static", "modal"],
|
||||
"protocol": "SolveAllSolutions with Foreground mode"
|
||||
}
|
||||
},
|
||||
"dashboard": {
|
||||
"visualization": "parallel_coordinates + pareto_front",
|
||||
"backend_port": 8000,
|
||||
"frontend_port": 3003
|
||||
},
|
||||
"summary": {
|
||||
"total_steps": 5,
|
||||
"engineering_needed": 1,
|
||||
"auto_generate": 4,
|
||||
"research_needed": ["1D element force extraction", "Genetic algorithm implementation"]
|
||||
"total_steps": 3,
|
||||
"engineering_needed": 2,
|
||||
"auto_generate": 1,
|
||||
"uses_centralized_extractors": true,
|
||||
"multi_objective": true,
|
||||
"multi_solution_workflow": true,
|
||||
"research_needed": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Be intelligent about:
|
||||
- Distinguishing element types (CBUSH vs CBAR vs CBEAM)
|
||||
- Directions (X vs Y vs Z)
|
||||
- Metrics (min vs max vs average)
|
||||
- Algorithms (Optuna TPE vs genetic algorithm vs gradient-based)
|
||||
- Data sources (OP2 file vs .prt expression vs .fem file)
|
||||
## Analysis Guidelines
|
||||
|
||||
Return ONLY the JSON analysis, no other text.
|
||||
Be intelligent about:
|
||||
|
||||
1. **Extractor Selection**
|
||||
- Always prefer centralized extractors over custom OP2 code
|
||||
- Match extractor to result type (displacement, stress, frequency, mass)
|
||||
|
||||
2. **Multi-Objective Optimization**
|
||||
- Identify conflicting objectives requiring Pareto analysis
|
||||
- Use NSGA-II for 2-3 objectives
|
||||
- Use proper semantic directions (no negative values)
|
||||
|
||||
3. **Multi-Solution Workflows**
|
||||
- Detect when static + modal analysis is needed
|
||||
- Flag requirement for `SolveAllSolutions()` protocol
|
||||
- Identify separate OP2 files per solution
|
||||
|
||||
4. **Dashboard Integration**
|
||||
- Suggest parallel coordinates for high-dimensional problems
|
||||
- Recommend Pareto front visualization for multi-objective
|
||||
- Note real-time monitoring capability
|
||||
|
||||
5. **Protocol Selection**
|
||||
- Protocol 11: Multi-objective NSGA-II
|
||||
- Protocol 10: Single-objective with intelligent strategies
|
||||
- Legacy: Basic single-objective TPE
|
||||
|
||||
Return ONLY the JSON analysis, no other text.
|
||||
|
||||
40
CHANGELOG.md
40
CHANGELOG.md
@@ -6,6 +6,46 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Phase 3.3 - Dashboard & Multi-Solution Support (November 23, 2025)
|
||||
|
||||
#### Added
|
||||
- **React Dashboard** with comprehensive multi-objective visualization
|
||||
- Parallel Coordinates Plot with research-based design
|
||||
- Light theme with high-visibility colors (white background, dark text)
|
||||
- Interactive trial selection and highlighting
|
||||
- Color-coded axes: Design Variables (blue) → Objectives (green) → Constraints (yellow)
|
||||
- Automatic constraint extraction from trial user_attrs
|
||||
- Support for units display (mm, MPa, Hz, g, etc.)
|
||||
- Pareto Front scatter plot for multi-objective optimization
|
||||
- Real-time WebSocket updates for live monitoring
|
||||
- FastAPI backend (port 8000) with Vite frontend (port 3003)
|
||||
- Optimizer Strategy Panel showing algorithm info and metrics
|
||||
- Comprehensive dashboard documentation: [DASHBOARD.md](docs/DASHBOARD.md)
|
||||
|
||||
- **NX Multi-Solution Protocol** documentation
|
||||
- Critical fix for multi-solution workflows documented
|
||||
- Best practices for static + modal, thermal + structural simulations
|
||||
- [NX_MULTI_SOLUTION_PROTOCOL.md](docs/NX_MULTI_SOLUTION_PROTOCOL.md)
|
||||
|
||||
- **Centralized Extractor Library**
|
||||
- `optimization_engine/extractors/` with base classes
|
||||
- Eliminates code duplication across studies
|
||||
- Unified error handling and OP2 file processing
|
||||
|
||||
#### Changed
|
||||
- **CRITICAL FIX**: Multi-solution NX workflows now use `SolveAllSolutions()` API
|
||||
- Ensures all solutions (static + modal, etc.) complete before returning
|
||||
- Uses Foreground mode for reliable multi-solution solves
|
||||
- Prevents stale OP2 files and identical results across trials
|
||||
- Implementation in `optimization_engine/solve_simulation.py` lines 271-295
|
||||
|
||||
#### Fixed
|
||||
- Multi-solution NX simulations only solving first solution
|
||||
- Parallel coordinates plot crashes due to undefined axis labels
|
||||
- Dashboard page crashes from missing data validation
|
||||
- Identical frequency values across trials in multi-solution studies
|
||||
- NaN values in constraint visualization
|
||||
|
||||
### Phase 3.2 - Integration & NX Enhancements (In Progress)
|
||||
|
||||
#### Added
|
||||
|
||||
30
README.md
30
README.md
@@ -31,11 +31,27 @@ Atomizer enables engineers to:
|
||||
|
||||
---
|
||||
|
||||
📘 **For Developers**: See [DEVELOPMENT_GUIDANCE.md](DEVELOPMENT_GUIDANCE.md) for comprehensive status report, current priorities, and strategic direction.
|
||||
## Documentation
|
||||
|
||||
📘 **Vision & Roadmap**: See [DEVELOPMENT_ROADMAP.md](DEVELOPMENT_ROADMAP.md) for the long-term vision and phase-by-phase implementation plan.
|
||||
📚 **[Complete Documentation Index](docs/00_INDEX.md)** - Start here for all documentation
|
||||
|
||||
📘 **Development Status**: See [DEVELOPMENT.md](DEVELOPMENT.md) for detailed task tracking and completed work.
|
||||
### Quick Links
|
||||
|
||||
- **[Visual Architecture Diagrams](docs/09_DIAGRAMS/)** - 🆕 Comprehensive Mermaid diagrams showing system architecture and workflows
|
||||
- **[Protocol Specifications](docs/PROTOCOLS.md)** - All active protocols (10, 11, 13) consolidated
|
||||
- **[Development Guide](DEVELOPMENT.md)** - Development workflow, testing, contributing
|
||||
- **[Dashboard Guide](docs/DASHBOARD.md)** - 🆕 Comprehensive React dashboard with multi-objective visualization
|
||||
- **[NX Multi-Solution Protocol](docs/NX_MULTI_SOLUTION_PROTOCOL.md)** - 🆕 Critical fix for multi-solution workflows
|
||||
- **[Getting Started](docs/HOW_TO_EXTEND_OPTIMIZATION.md)** - Create your first optimization study
|
||||
|
||||
### By Topic
|
||||
|
||||
- **Protocols**: [PROTOCOLS.md](docs/PROTOCOLS.md) - Protocol 10 (Intelligent Optimization), 11 (Multi-Objective), 13 (Dashboard)
|
||||
- **Architecture**: [HOOK_ARCHITECTURE.md](docs/HOOK_ARCHITECTURE.md), [NX_SESSION_MANAGEMENT.md](docs/NX_SESSION_MANAGEMENT.md)
|
||||
- **Dashboard**: [DASHBOARD_MASTER_PLAN.md](docs/DASHBOARD_MASTER_PLAN.md), [DASHBOARD_REACT_IMPLEMENTATION.md](docs/DASHBOARD_REACT_IMPLEMENTATION.md)
|
||||
- **Advanced**: [HYBRID_MODE_GUIDE.md](docs/HYBRID_MODE_GUIDE.md) - LLM-assisted workflows
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -163,16 +179,18 @@ python run_5trial_test.py
|
||||
|
||||
## Features
|
||||
|
||||
- **Intelligent Optimization**: Optuna-powered TPE sampler with multi-objective support
|
||||
- **Intelligent Multi-Objective Optimization**: NSGA-II algorithm for Pareto-optimal solutions
|
||||
- **Advanced Dashboard**: React-based real-time monitoring with parallel coordinates visualization
|
||||
- **NX Integration**: Seamless journal-based control of Siemens NX Simcenter
|
||||
- **Multi-Solution Support**: Automatic handling of combined analysis types (static + modal, thermal + structural)
|
||||
- **Smart Logging**: Detailed per-trial logs + high-level optimization progress tracking
|
||||
- **Plugin System**: Extensible hooks at pre-solve, post-solve, and post-extraction points
|
||||
- **Study Management**: Isolated study folders with automatic result organization
|
||||
- **Substudy System**: NX-like hierarchical studies with shared models and independent configurations
|
||||
- **Live History Tracking**: Real-time incremental JSON updates for monitoring progress
|
||||
- **Resume Capability**: Interrupt and resume optimizations without data loss
|
||||
- **Web Dashboard**: Real-time monitoring and configuration UI
|
||||
- **Example Study**: Bracket displacement maximization with full substudy workflow
|
||||
- **Pareto Front Analysis**: Automatic extraction and visualization of non-dominated solutions
|
||||
- **Parallel Coordinates Plot**: Research-grade multi-dimensional visualization with interactive selection
|
||||
|
||||
## Current Status
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Parallel Coordinates Plot - Protocol 13
|
||||
* High-dimensional visualization for multi-objective Pareto fronts
|
||||
* Shows objectives and design variables as parallel axes
|
||||
* Parallel Coordinates Plot - Enhanced Multi-Objective Visualization
|
||||
* Shows design variables → objectives → constraints in proper research format
|
||||
* Light theme with high visibility
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
@@ -10,6 +10,7 @@ interface ParetoTrial {
|
||||
trial_number: number;
|
||||
values: number[];
|
||||
params: Record<string, number>;
|
||||
user_attrs?: Record<string, any>;
|
||||
constraint_satisfied?: boolean;
|
||||
}
|
||||
|
||||
@@ -26,74 +27,145 @@ interface DesignVariable {
|
||||
max: number;
|
||||
}
|
||||
|
||||
interface Constraint {
|
||||
name: string;
|
||||
threshold: number;
|
||||
type: 'less_than' | 'greater_than';
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
interface ParallelCoordinatesPlotProps {
|
||||
paretoData: ParetoTrial[];
|
||||
objectives: Objective[];
|
||||
designVariables: DesignVariable[];
|
||||
constraints?: Constraint[];
|
||||
}
|
||||
|
||||
export function ParallelCoordinatesPlot({
|
||||
paretoData,
|
||||
objectives,
|
||||
designVariables
|
||||
designVariables,
|
||||
constraints = []
|
||||
}: ParallelCoordinatesPlotProps) {
|
||||
const [hoveredTrial, setHoveredTrial] = useState<number | null>(null);
|
||||
const [selectedTrials, setSelectedTrials] = useState<Set<number>>(new Set());
|
||||
|
||||
if (paretoData.length === 0) {
|
||||
// Safety checks
|
||||
if (!paretoData || paretoData.length === 0) {
|
||||
return (
|
||||
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
|
||||
<h3 className="text-lg font-semibold mb-4 text-dark-100">Parallel Coordinates</h3>
|
||||
<div className="h-96 flex items-center justify-center text-dark-300">
|
||||
No Pareto front data yet
|
||||
<div className="bg-white rounded-lg p-6 border border-gray-300 shadow-sm">
|
||||
<h3 className="text-lg font-semibold mb-4 text-gray-900">Parallel Coordinates Plot</h3>
|
||||
<div className="h-96 flex items-center justify-center text-gray-500">
|
||||
No Pareto front data available
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Combine objectives and design variables into axes
|
||||
const axes: Array<{name: string, label: string, type: 'objective' | 'param'}> = [
|
||||
...objectives.map((obj, i) => ({
|
||||
name: `obj_${i}`,
|
||||
label: obj.unit ? `${obj.name} (${obj.unit})` : obj.name,
|
||||
type: 'objective' as const
|
||||
})),
|
||||
...designVariables.map(dv => ({
|
||||
if (!objectives || objectives.length === 0 || !designVariables || designVariables.length === 0) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg p-6 border border-gray-300 shadow-sm">
|
||||
<h3 className="text-lg font-semibold mb-4 text-gray-900">Parallel Coordinates Plot</h3>
|
||||
<div className="h-96 flex items-center justify-center text-gray-500">
|
||||
Missing objectives or design variables metadata
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Structure axes: Design Variables → Objectives → Constraints
|
||||
const axes: Array<{
|
||||
name: string;
|
||||
label: string;
|
||||
type: 'design_var' | 'objective' | 'constraint';
|
||||
unit?: string;
|
||||
}> = [];
|
||||
|
||||
// Add design variables
|
||||
designVariables.forEach(dv => {
|
||||
axes.push({
|
||||
name: dv.name,
|
||||
label: dv.unit ? `${dv.name} (${dv.unit})` : dv.name,
|
||||
type: 'param' as const
|
||||
}))
|
||||
];
|
||||
label: dv.unit ? `${dv.name}\n(${dv.unit})` : dv.name,
|
||||
type: 'design_var',
|
||||
unit: dv.unit
|
||||
});
|
||||
});
|
||||
|
||||
// Normalize data to [0, 1] for each axis
|
||||
const normalizedData = paretoData.map(trial => {
|
||||
const allValues: number[] = [];
|
||||
// Add objectives
|
||||
objectives.forEach((obj, i) => {
|
||||
axes.push({
|
||||
name: `objective_${i}`,
|
||||
label: obj.unit ? `${obj.name}\n(${obj.unit})` : obj.name,
|
||||
type: 'objective',
|
||||
unit: obj.unit
|
||||
});
|
||||
});
|
||||
|
||||
// Add objectives
|
||||
trial.values.forEach(val => allValues.push(val));
|
||||
// Add constraints (extract from user_attrs)
|
||||
const constraintNames = new Set<string>();
|
||||
paretoData.forEach(trial => {
|
||||
if (trial.user_attrs) {
|
||||
// Common constraint metrics
|
||||
if (trial.user_attrs.max_stress !== undefined) constraintNames.add('max_stress');
|
||||
if (trial.user_attrs.max_displacement !== undefined) constraintNames.add('max_displacement');
|
||||
if (trial.user_attrs.frequency !== undefined && objectives.findIndex(obj => obj.name.toLowerCase().includes('freq')) === -1) {
|
||||
constraintNames.add('frequency');
|
||||
}
|
||||
if (trial.user_attrs.mass !== undefined && objectives.findIndex(obj => obj.name.toLowerCase().includes('mass')) === -1) {
|
||||
constraintNames.add('mass');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add design variables
|
||||
constraintNames.forEach(name => {
|
||||
const unit = name.includes('stress') ? 'MPa' :
|
||||
name.includes('displacement') ? 'mm' :
|
||||
name.includes('frequency') ? 'Hz' :
|
||||
name.includes('mass') ? 'g' : '';
|
||||
axes.push({
|
||||
name: `constraint_${name}`,
|
||||
label: unit ? `${name}\n(${unit})` : name,
|
||||
type: 'constraint',
|
||||
unit
|
||||
});
|
||||
});
|
||||
|
||||
// Extract values for each axis
|
||||
const trialData = paretoData.map(trial => {
|
||||
const values: number[] = [];
|
||||
|
||||
// Design variables
|
||||
designVariables.forEach(dv => {
|
||||
allValues.push(trial.params[dv.name]);
|
||||
values.push(trial.params[dv.name] ?? 0);
|
||||
});
|
||||
|
||||
// Objectives
|
||||
trial.values.forEach(val => {
|
||||
values.push(val);
|
||||
});
|
||||
|
||||
// Constraints
|
||||
constraintNames.forEach(name => {
|
||||
values.push(trial.user_attrs?.[name] ?? 0);
|
||||
});
|
||||
|
||||
return {
|
||||
trial_number: trial.trial_number,
|
||||
values: allValues,
|
||||
values,
|
||||
feasible: trial.constraint_satisfied !== false
|
||||
};
|
||||
});
|
||||
|
||||
// Calculate min/max for each axis
|
||||
// Calculate min/max for normalization
|
||||
const ranges = axes.map((_, axisIdx) => {
|
||||
const values = normalizedData.map(d => d.values[axisIdx]);
|
||||
const values = trialData.map(d => d.values[axisIdx]);
|
||||
return {
|
||||
min: Math.min(...values),
|
||||
max: Math.max(...values)
|
||||
};
|
||||
});
|
||||
|
||||
// Normalize function
|
||||
// Normalize to [0, 1]
|
||||
const normalize = (value: number, axisIdx: number): number => {
|
||||
const range = ranges[axisIdx];
|
||||
if (range.max === range.min) return 0.5;
|
||||
@@ -101,9 +173,9 @@ export function ParallelCoordinatesPlot({
|
||||
};
|
||||
|
||||
// Chart dimensions
|
||||
const width = 800;
|
||||
const height = 400;
|
||||
const margin = { top: 80, right: 20, bottom: 40, left: 20 };
|
||||
const width = 1400;
|
||||
const height = 500;
|
||||
const margin = { top: 100, right: 50, bottom: 60, left: 50 };
|
||||
const plotWidth = width - margin.left - margin.right;
|
||||
const plotHeight = height - margin.top - margin.bottom;
|
||||
|
||||
@@ -120,149 +192,215 @@ export function ParallelCoordinatesPlot({
|
||||
setSelectedTrials(newSelected);
|
||||
};
|
||||
|
||||
// Color scheme - highly visible
|
||||
const getLineColor = (trial: typeof trialData[0], isHovered: boolean, isSelected: boolean) => {
|
||||
if (isSelected) return '#FF6B00'; // Bright orange for selected
|
||||
if (!trial.feasible) return '#DC2626'; // Red for infeasible
|
||||
if (isHovered) return '#2563EB'; // Blue for hover
|
||||
return '#10B981'; // Green for feasible
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-dark-700 rounded-lg p-6 border border-dark-600">
|
||||
<div className="bg-white rounded-lg p-6 border border-gray-300 shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-dark-100">
|
||||
Parallel Coordinates ({paretoData.length} solutions)
|
||||
</h3>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
Parallel Coordinates Plot ({paretoData.length} solutions)
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Design Variables → Objectives → Constraints
|
||||
</p>
|
||||
</div>
|
||||
{selectedTrials.size > 0 && (
|
||||
<button
|
||||
onClick={() => setSelectedTrials(new Set())}
|
||||
className="text-xs px-3 py-1 bg-dark-600 hover:bg-dark-500 rounded text-dark-200"
|
||||
className="text-xs px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded text-gray-800 font-medium transition-colors"
|
||||
>
|
||||
Clear Selection ({selectedTrials.size})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<svg width={width} height={height} className="overflow-visible">
|
||||
<g transform={`translate(${margin.left}, ${margin.top})`}>
|
||||
{/* Draw axes */}
|
||||
{axes.map((axis, i) => {
|
||||
const x = i * axisSpacing;
|
||||
return (
|
||||
<g key={axis.name} transform={`translate(${x}, 0)`}>
|
||||
{/* Axis line */}
|
||||
<line
|
||||
y1={0}
|
||||
y2={plotHeight}
|
||||
stroke="#475569"
|
||||
strokeWidth={2}
|
||||
<div className="overflow-x-auto">
|
||||
<svg width={width} height={height} className="overflow-visible">
|
||||
<g transform={`translate(${margin.left}, ${margin.top})`}>
|
||||
{/* Draw axes */}
|
||||
{axes.map((axis, i) => {
|
||||
const x = i * axisSpacing;
|
||||
const bgColor = axis.type === 'design_var' ? '#EFF6FF' :
|
||||
axis.type === 'objective' ? '#F0FDF4' : '#FEF3C7';
|
||||
|
||||
return (
|
||||
<g key={axis.name} transform={`translate(${x}, 0)`}>
|
||||
{/* Background highlight */}
|
||||
<rect
|
||||
x={-15}
|
||||
y={-20}
|
||||
width={30}
|
||||
height={plotHeight + 40}
|
||||
fill={bgColor}
|
||||
opacity={0.3}
|
||||
/>
|
||||
|
||||
{/* Axis line */}
|
||||
<line
|
||||
y1={0}
|
||||
y2={plotHeight}
|
||||
stroke="#374151"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
|
||||
{/* Axis label */}
|
||||
<text
|
||||
y={-30}
|
||||
textAnchor="middle"
|
||||
fill="#111827"
|
||||
fontSize={12}
|
||||
fontWeight="600"
|
||||
className="select-none"
|
||||
>
|
||||
{(axis.label || '').split('\n').map((line, idx) => (
|
||||
<tspan key={idx} x={0} dy={idx === 0 ? 0 : 14}>{line}</tspan>
|
||||
))}
|
||||
</text>
|
||||
|
||||
{/* Type badge */}
|
||||
<text
|
||||
y={-55}
|
||||
textAnchor="middle"
|
||||
fill="#6B7280"
|
||||
fontSize={9}
|
||||
fontWeight="500"
|
||||
className="select-none"
|
||||
>
|
||||
{axis.type === 'design_var' ? 'DESIGN VAR' :
|
||||
axis.type === 'objective' ? 'OBJECTIVE' : 'CONSTRAINT'}
|
||||
</text>
|
||||
|
||||
{/* Min/max labels */}
|
||||
<text
|
||||
y={plotHeight + 20}
|
||||
textAnchor="middle"
|
||||
fill="#374151"
|
||||
fontSize={10}
|
||||
fontWeight="500"
|
||||
>
|
||||
{ranges[i].min.toFixed(2)}
|
||||
</text>
|
||||
<text
|
||||
y={-10}
|
||||
textAnchor="middle"
|
||||
fill="#374151"
|
||||
fontSize={10}
|
||||
fontWeight="500"
|
||||
>
|
||||
{ranges[i].max.toFixed(2)}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Draw polylines for each trial */}
|
||||
{trialData.map(trial => {
|
||||
const isHovered = hoveredTrial === trial.trial_number;
|
||||
const isSelected = selectedTrials.has(trial.trial_number);
|
||||
const isHighlighted = isHovered || isSelected;
|
||||
|
||||
// Build path
|
||||
const pathData = axes.map((_, i) => {
|
||||
const x = i * axisSpacing;
|
||||
const normalizedY = normalize(trial.values[i], i);
|
||||
const y = plotHeight * (1 - normalizedY);
|
||||
return i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`;
|
||||
}).join(' ');
|
||||
|
||||
return (
|
||||
<path
|
||||
key={trial.trial_number}
|
||||
d={pathData}
|
||||
fill="none"
|
||||
stroke={getLineColor(trial, isHovered, isSelected)}
|
||||
strokeWidth={isHighlighted ? 3 : 1.5}
|
||||
opacity={
|
||||
selectedTrials.size > 0
|
||||
? (isSelected ? 0.95 : 0.1)
|
||||
: (isHighlighted ? 0.95 : 0.5)
|
||||
}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="transition-all duration-150 cursor-pointer"
|
||||
style={{
|
||||
filter: isHighlighted ? 'drop-shadow(0 2px 4px rgba(0,0,0,0.2))' : 'none'
|
||||
}}
|
||||
onMouseEnter={() => setHoveredTrial(trial.trial_number)}
|
||||
onMouseLeave={() => setHoveredTrial(null)}
|
||||
onClick={() => toggleTrial(trial.trial_number)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Axis label */}
|
||||
<text
|
||||
y={-10}
|
||||
textAnchor="middle"
|
||||
fill="#94a3b8"
|
||||
fontSize={12}
|
||||
className="select-none"
|
||||
transform={`rotate(-45, 0, -10)`}
|
||||
>
|
||||
{axis.label}
|
||||
{/* Hover tooltip */}
|
||||
{hoveredTrial !== null && (
|
||||
<g transform={`translate(${plotWidth + 20}, 20)`}>
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={140}
|
||||
height={70}
|
||||
fill="white"
|
||||
stroke="#D1D5DB"
|
||||
strokeWidth={2}
|
||||
rx={6}
|
||||
style={{ filter: 'drop-shadow(0 4px 6px rgba(0,0,0,0.1))' }}
|
||||
/>
|
||||
<text x={12} y={24} fill="#111827" fontSize={13} fontWeight="700">
|
||||
Trial #{hoveredTrial}
|
||||
</text>
|
||||
|
||||
{/* Min/max labels */}
|
||||
<text
|
||||
y={plotHeight + 15}
|
||||
textAnchor="middle"
|
||||
fill="#64748b"
|
||||
fontSize={10}
|
||||
>
|
||||
{ranges[i].min.toFixed(2)}
|
||||
<text x={12} y={44} fill="#6B7280" fontSize={11}>
|
||||
Click to select/deselect
|
||||
</text>
|
||||
<text
|
||||
y={-25}
|
||||
textAnchor="middle"
|
||||
fill="#64748b"
|
||||
fontSize={10}
|
||||
>
|
||||
{ranges[i].max.toFixed(2)}
|
||||
<text x={12} y={60} fill="#374151" fontSize={10} fontWeight="600">
|
||||
{selectedTrials.has(hoveredTrial) ? '✓ Selected' : '○ Not selected'}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Draw lines for each trial */}
|
||||
{normalizedData.map(trial => {
|
||||
const isHovered = hoveredTrial === trial.trial_number;
|
||||
const isSelected = selectedTrials.has(trial.trial_number);
|
||||
const isHighlighted = isHovered || isSelected;
|
||||
|
||||
// Build path
|
||||
const pathData = axes.map((_, i) => {
|
||||
const x = i * axisSpacing;
|
||||
const normalizedY = normalize(trial.values[i], i);
|
||||
const y = plotHeight * (1 - normalizedY);
|
||||
return i === 0 ? `M ${x} ${y}` : `L ${x} ${y}`;
|
||||
}).join(' ');
|
||||
|
||||
return (
|
||||
<path
|
||||
key={trial.trial_number}
|
||||
d={pathData}
|
||||
fill="none"
|
||||
stroke={
|
||||
isSelected ? '#fbbf24' :
|
||||
trial.feasible ? '#10b981' : '#ef4444'
|
||||
}
|
||||
strokeWidth={isHighlighted ? 2.5 : 1}
|
||||
opacity={
|
||||
selectedTrials.size > 0
|
||||
? (isSelected ? 1 : 0.1)
|
||||
: (isHighlighted ? 1 : 0.4)
|
||||
}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="transition-all duration-200 cursor-pointer"
|
||||
onMouseEnter={() => setHoveredTrial(trial.trial_number)}
|
||||
onMouseLeave={() => setHoveredTrial(null)}
|
||||
onClick={() => toggleTrial(trial.trial_number)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Hover tooltip */}
|
||||
{hoveredTrial !== null && (
|
||||
<g transform={`translate(${plotWidth + 10}, 20)`}>
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={120}
|
||||
height={60}
|
||||
fill="#1e293b"
|
||||
stroke="#334155"
|
||||
strokeWidth={1}
|
||||
rx={4}
|
||||
/>
|
||||
<text x={10} y={20} fill="#e2e8f0" fontSize={12} fontWeight="bold">
|
||||
Trial #{hoveredTrial}
|
||||
</text>
|
||||
<text x={10} y={38} fill="#94a3b8" fontSize={10}>
|
||||
Click to select
|
||||
</text>
|
||||
<text x={10} y={52} fill="#94a3b8" fontSize={10}>
|
||||
{selectedTrials.has(hoveredTrial) ? '✓ Selected' : '○ Not selected'}
|
||||
</text>
|
||||
</g>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="flex gap-6 justify-center mt-4 text-sm">
|
||||
<div className="flex gap-8 justify-center mt-6 text-sm border-t border-gray-200 pt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 bg-green-400" />
|
||||
<span className="text-dark-200">Feasible</span>
|
||||
<div className="w-10 h-1" style={{ backgroundColor: '#10B981' }} />
|
||||
<span className="text-gray-700 font-medium">Feasible</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 bg-red-400" />
|
||||
<span className="text-dark-200">Infeasible</span>
|
||||
<div className="w-10 h-1" style={{ backgroundColor: '#DC2626' }} />
|
||||
<span className="text-gray-700 font-medium">Infeasible</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-0.5 bg-yellow-400" style={{ height: '2px' }} />
|
||||
<span className="text-dark-200">Selected</span>
|
||||
<div className="w-10 h-1.5" style={{ backgroundColor: '#FF6B00' }} />
|
||||
<span className="text-gray-700 font-medium">Selected</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-1" style={{ backgroundColor: '#2563EB' }} />
|
||||
<span className="text-gray-700 font-medium">Hover</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Axis type legend */}
|
||||
<div className="flex gap-6 justify-center mt-3 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded" style={{ backgroundColor: '#EFF6FF' }} />
|
||||
<span className="text-gray-600">Design Variables</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded" style={{ backgroundColor: '#F0FDF4' }} />
|
||||
<span className="text-gray-600">Objectives</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded" style={{ backgroundColor: '#FEF3C7' }} />
|
||||
<span className="text-gray-600">Constraints</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
390
docs/DASHBOARD.md
Normal file
390
docs/DASHBOARD.md
Normal file
@@ -0,0 +1,390 @@
|
||||
# Atomizer Dashboard
|
||||
|
||||
**Last Updated**: November 23, 2025
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Atomizer Dashboard is a real-time web-based interface for monitoring and analyzing multi-objective optimization studies. Built with React, TypeScript, and Tailwind CSS, it provides comprehensive visualization and interaction capabilities for NSGA-II based structural optimization.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Frontend Stack
|
||||
- **Framework**: React 18 with TypeScript
|
||||
- **Build Tool**: Vite
|
||||
- **Styling**: Tailwind CSS with custom dark/light theme support
|
||||
- **Charts**: Recharts for data visualization
|
||||
- **State Management**: React hooks (useState, useEffect)
|
||||
- **WebSocket**: Real-time optimization updates
|
||||
|
||||
### Backend Stack
|
||||
- **Framework**: FastAPI (Python)
|
||||
- **Database**: Optuna SQLite studies
|
||||
- **API**: RESTful endpoints with WebSocket support
|
||||
- **CORS**: Configured for local development
|
||||
|
||||
### Ports
|
||||
- **Frontend**: `http://localhost:3003` (Vite dev server)
|
||||
- **Backend**: `http://localhost:8000` (FastAPI/Uvicorn)
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Multi-Objective Visualization
|
||||
|
||||
#### Pareto Front Plot
|
||||
- 2D scatter plot showing trade-offs between objectives
|
||||
- Color-coded by constraint satisfaction (green = feasible, red = infeasible)
|
||||
- Interactive hover tooltips with trial details
|
||||
- Automatically extracts Pareto-optimal solutions using NSGA-II
|
||||
|
||||
#### Parallel Coordinates Plot
|
||||
**Research-Based Multi-Dimensional Visualization**
|
||||
|
||||
Structure: **Design Variables → Objectives → Constraints**
|
||||
|
||||
Features:
|
||||
- **Light Theme**: White background with high-visibility dark text and colors
|
||||
- **Color-Coded Axes**:
|
||||
- Blue background: Design variables
|
||||
- Green background: Objectives
|
||||
- Yellow background: Constraints
|
||||
- **Interactive Selection**:
|
||||
- Hover over lines to highlight individual trials
|
||||
- Click to select/deselect trials
|
||||
- Multi-select with visual feedback (orange highlight)
|
||||
- **Type Badges**: Labels showing DESIGN VAR, OBJECTIVE, or CONSTRAINT
|
||||
- **Units Display**: Automatic unit labeling (mm, MPa, Hz, g, etc.)
|
||||
- **Min/Max Labels**: Range values displayed on each axis
|
||||
- **Feasibility Coloring**:
|
||||
- Green: Feasible solutions
|
||||
- Red: Infeasible solutions (constraint violations)
|
||||
- Blue: Hover highlight
|
||||
- Orange: Selected trials
|
||||
|
||||
**Implementation**: [ParallelCoordinatesPlot.tsx](atomizer-dashboard/frontend/src/components/ParallelCoordinatesPlot.tsx:1)
|
||||
|
||||
**Line colors**:
|
||||
```typescript
|
||||
if (isSelected) return '#FF6B00'; // Orange for selected
|
||||
if (!trial.feasible) return '#DC2626'; // Red for infeasible
|
||||
if (isHovered) return '#2563EB'; // Blue for hover
|
||||
return '#10B981'; // Green for feasible
|
||||
```
|
||||
|
||||
### 2. Optimizer Strategy Panel
|
||||
Displays algorithm information:
|
||||
- **Algorithm**: NSGA-II, TPE, or custom
|
||||
- **Type**: Single-objective or Multi-objective
|
||||
- **Objectives Count**: Number of optimization objectives
|
||||
- **Design Variables Count**: Number of design parameters
|
||||
|
||||
### 3. Convergence Monitoring
|
||||
- **Convergence Plot**: Best value vs. trial number
|
||||
- **Real-time Updates**: WebSocket-driven live updates
|
||||
- **Pruned Trials**: Visual indication of pruned trials
|
||||
|
||||
### 4. Parameter Space Exploration
|
||||
- **2D Scatter Plot**: Design variable relationships
|
||||
- **Color Mapping**: Objective values mapped to color intensity
|
||||
- **Interactive Tooltips**: Trial details on hover
|
||||
|
||||
### 5. Trial History Table
|
||||
- Comprehensive list of all trials
|
||||
- Sortable columns
|
||||
- Status indicators (COMPLETE, PRUNED, FAIL)
|
||||
- Parameter values and objective values
|
||||
- User attributes (constraints)
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Studies
|
||||
|
||||
#### GET `/api/optimization/studies`
|
||||
List all available optimization studies.
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "drone_gimbal_arm_optimization",
|
||||
"name": "drone_gimbal_arm_optimization",
|
||||
"direction": ["minimize", "maximize"],
|
||||
"n_trials": 100,
|
||||
"best_value": [3245.67, 165.3],
|
||||
"sampler": "NSGAIISampler"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### GET `/api/optimization/studies/{study_id}/trials`
|
||||
Get all trials for a study.
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"trials": [
|
||||
{
|
||||
"number": 0,
|
||||
"values": [3456.2, 145.6],
|
||||
"params": {
|
||||
"beam_half_core_thickness": 7.5,
|
||||
"beam_face_thickness": 2.1,
|
||||
"holes_diameter": 30.0,
|
||||
"hole_count": 11
|
||||
},
|
||||
"state": "COMPLETE",
|
||||
"user_attrs": {
|
||||
"max_stress": 95.3,
|
||||
"max_displacement": 1.2,
|
||||
"frequency": 145.6,
|
||||
"mass": 3456.2,
|
||||
"constraint_satisfied": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### GET `/api/optimization/studies/{study_id}/metadata`
|
||||
Get study metadata including objectives and design variables.
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"objectives": [
|
||||
{
|
||||
"name": "mass",
|
||||
"type": "minimize",
|
||||
"unit": "g"
|
||||
},
|
||||
{
|
||||
"name": "frequency",
|
||||
"type": "maximize",
|
||||
"unit": "Hz"
|
||||
}
|
||||
],
|
||||
"design_variables": [
|
||||
{
|
||||
"name": "beam_half_core_thickness",
|
||||
"unit": "mm",
|
||||
"min": 5.0,
|
||||
"max": 10.0
|
||||
}
|
||||
],
|
||||
"sampler": "NSGAIISampler"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET `/api/optimization/studies/{study_id}/pareto-front`
|
||||
Get Pareto-optimal solutions for multi-objective studies.
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"is_multi_objective": true,
|
||||
"pareto_front": [
|
||||
{
|
||||
"trial_number": 0,
|
||||
"values": [3245.67, 165.3],
|
||||
"params": {...},
|
||||
"user_attrs": {...},
|
||||
"constraint_satisfied": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### WebSocket
|
||||
|
||||
#### WS `/ws/optimization/{study_id}`
|
||||
Real-time trial updates during optimization.
|
||||
|
||||
**Message Format**:
|
||||
```json
|
||||
{
|
||||
"type": "trial_complete",
|
||||
"trial": {
|
||||
"number": 5,
|
||||
"values": [3456.2, 145.6],
|
||||
"params": {...}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running the Dashboard
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
cd atomizer-dashboard/backend
|
||||
python -m uvicorn api.main:app --reload --port 8000
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
cd atomizer-dashboard/frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Access at: `http://localhost:3003`
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Vite Proxy ([vite.config.ts](atomizer-dashboard/frontend/vite.config.ts:1))
|
||||
|
||||
```typescript
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
port: 3003,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8000',
|
||||
changeOrigin: true,
|
||||
secure: false,
|
||||
ws: true, // WebSocket support
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### CORS ([backend/api/main.py](atomizer-dashboard/backend/api/main.py:1))
|
||||
|
||||
```python
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["http://localhost:3003"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component Structure
|
||||
|
||||
```
|
||||
atomizer-dashboard/
|
||||
├── frontend/
|
||||
│ ├── src/
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── ParallelCoordinatesPlot.tsx # Multi-objective visualization
|
||||
│ │ │ ├── ParetoPlot.tsx # Pareto front scatter plot
|
||||
│ │ │ ├── OptimizerPanel.tsx # Strategy information
|
||||
│ │ │ ├── common/
|
||||
│ │ │ │ └── Card.tsx # Reusable card component
|
||||
│ │ │ └── dashboard/
|
||||
│ │ │ ├── MetricCard.tsx # KPI display
|
||||
│ │ │ └── StudyCard.tsx # Study selector
|
||||
│ │ ├── pages/
|
||||
│ │ │ └── Dashboard.tsx # Main dashboard page
|
||||
│ │ ├── hooks/
|
||||
│ │ │ └── useWebSocket.ts # WebSocket connection
|
||||
│ │ ├── api/
|
||||
│ │ │ └── client.ts # API client
|
||||
│ │ └── types/
|
||||
│ │ └── index.ts # TypeScript types
|
||||
│ └── vite.config.ts
|
||||
└── backend/
|
||||
└── api/
|
||||
├── main.py # FastAPI app
|
||||
└── routes/
|
||||
└── optimization.py # Optimization endpoints
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. **Optimization Engine** runs trials and stores results in Optuna SQLite database
|
||||
2. **Backend API** reads from database and exposes REST endpoints
|
||||
3. **Frontend** fetches data via `/api/optimization/*` endpoints
|
||||
4. **WebSocket** pushes real-time updates to connected clients
|
||||
5. **React Components** render visualizations based on fetched data
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Dashboard Page Crashes
|
||||
|
||||
**Issue**: `TypeError: Cannot read properties of undefined (reading 'split')`
|
||||
|
||||
**Fix**: Ensure all data is validated before rendering. ParallelCoordinatesPlot now includes:
|
||||
```typescript
|
||||
if (!paretoData || paretoData.length === 0) return <EmptyState />;
|
||||
if (!objectives || !designVariables) return <EmptyState />;
|
||||
```
|
||||
|
||||
### No Data Showing
|
||||
|
||||
1. Check backend is running: `curl http://localhost:8000/api/optimization/studies`
|
||||
2. Verify study exists in Optuna database
|
||||
3. Check browser console for API errors
|
||||
4. Ensure WebSocket connection is established
|
||||
|
||||
### CORS Errors
|
||||
|
||||
- Backend must allow origin `http://localhost:3003`
|
||||
- Frontend proxy must target `http://127.0.0.1:8000` (not `localhost`)
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Multi-Objective Studies
|
||||
|
||||
1. **Always use metadata endpoint** to get objective/variable definitions
|
||||
2. **Extract constraints from user_attrs** for parallel coordinates
|
||||
3. **Filter Pareto front** using `paretoData.pareto_front` array
|
||||
4. **Validate constraint_satisfied** field before coloring
|
||||
|
||||
### For Real-Time Updates
|
||||
|
||||
1. **Use WebSocket** for live trial updates
|
||||
2. **Debounce state updates** to avoid excessive re-renders
|
||||
3. **Close WebSocket** connection on component unmount
|
||||
|
||||
### For Performance
|
||||
|
||||
1. **Limit displayed trials** for large studies (e.g., show last 1000)
|
||||
2. **Use React.memo** for expensive components
|
||||
3. **Virtualize large lists** if showing >100 trials in tables
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] 3D Pareto front visualization for 3+ objectives
|
||||
- [ ] Advanced filtering and search in trial history
|
||||
- [ ] Export results to CSV/JSON
|
||||
- [ ] Custom parallel coordinates brushing/filtering
|
||||
- [ ] Multi-study comparison view
|
||||
- [ ] Hypervolume indicator tracking
|
||||
- [ ] Interactive design variable sliders
|
||||
- [ ] Constraint importance analysis
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **Optuna Documentation**: https://optuna.readthedocs.io/
|
||||
- **NSGA-II Algorithm**: Deb et al. (2002)
|
||||
- **Parallel Coordinates**: Inselberg & Dimsdale (1990)
|
||||
- **React Documentation**: https://react.dev/
|
||||
- **FastAPI Documentation**: https://fastapi.tiangolo.com/
|
||||
197
docs/NX_MULTI_SOLUTION_PROTOCOL.md
Normal file
197
docs/NX_MULTI_SOLUTION_PROTOCOL.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# NX Multi-Solution Solve Protocol
|
||||
|
||||
## Critical Finding: SolveAllSolutions API Required for Multi-Solution Models
|
||||
|
||||
**Date**: November 23, 2025
|
||||
**Last Updated**: November 23, 2025
|
||||
**Protocol**: Multi-Solution Nastran Solve
|
||||
**Affected Models**: Any NX simulation with multiple solutions (e.g., static + modal, thermal + structural)
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
When an NX simulation contains multiple solutions (e.g., Solution 1 = Static Analysis, Solution 2 = Modal Analysis), using `SolveChainOfSolutions()` with Background mode **does not wait for all solutions to complete** before returning control to Python. This causes:
|
||||
|
||||
1. **Missing OP2 Files**: Only the first solution's OP2 file is generated
|
||||
2. **Stale Data**: Subsequent trials read old OP2 files from previous runs
|
||||
3. **Identical Results**: All trials show the same values for results from missing solutions
|
||||
4. **Silent Failures**: No error is raised - the solve completes but files are not written
|
||||
|
||||
### Example Scenario
|
||||
|
||||
**Drone Gimbal Arm Optimization**:
|
||||
- Solution 1: Static analysis (stress, displacement)
|
||||
- Solution 2: Modal analysis (frequency)
|
||||
|
||||
**Symptoms**:
|
||||
- All 100 trials showed **identical frequency** (27.476 Hz)
|
||||
- Only `beam_sim1-solution_1.op2` was created
|
||||
- `beam_sim1-solution_2.op2` was never regenerated after Trial 0
|
||||
- Both `.dat` files were written correctly, but solve didn't wait for completion
|
||||
|
||||
---
|
||||
|
||||
## Root Cause
|
||||
|
||||
```python
|
||||
# WRONG APPROACH (doesn't wait for completion)
|
||||
psolutions1 = []
|
||||
solution_idx = 1
|
||||
while True:
|
||||
solution_obj_name = f"Solution[Solution {solution_idx}]"
|
||||
simSolution = simSimulation1.FindObject(solution_obj_name)
|
||||
if simSolution:
|
||||
psolutions1.append(simSolution)
|
||||
solution_idx += 1
|
||||
else:
|
||||
break
|
||||
|
||||
theCAESimSolveManager.SolveChainOfSolutions(
|
||||
psolutions1,
|
||||
NXOpen.CAE.SimSolution.SolveOption.Solve,
|
||||
NXOpen.CAE.SimSolution.SetupCheckOption.CompleteDeepCheckAndOutputErrors,
|
||||
NXOpen.CAE.SimSolution.SolveMode.Background # ❌ Returns immediately!
|
||||
)
|
||||
```
|
||||
|
||||
**Issue**: Background mode runs asynchronously and returns control to Python before all solutions finish solving.
|
||||
|
||||
---
|
||||
|
||||
## Correct Solution
|
||||
|
||||
### For Solving All Solutions
|
||||
|
||||
Use `SolveAllSolutions()` API with **Foreground mode**:
|
||||
|
||||
```python
|
||||
# CORRECT APPROACH (waits for completion)
|
||||
if solution_name:
|
||||
# Solve specific solution in background mode
|
||||
solution_obj_name = f"Solution[{solution_name}]"
|
||||
simSolution1 = simSimulation1.FindObject(solution_obj_name)
|
||||
psolutions1 = [simSolution1]
|
||||
|
||||
numsolutionssolved1, numsolutionsfailed1, numsolutionsskipped1 = theCAESimSolveManager.SolveChainOfSolutions(
|
||||
psolutions1,
|
||||
NXOpen.CAE.SimSolution.SolveOption.Solve,
|
||||
NXOpen.CAE.SimSolution.SetupCheckOption.CompleteDeepCheckAndOutputErrors,
|
||||
NXOpen.CAE.SimSolution.SolveMode.Background
|
||||
)
|
||||
else:
|
||||
# Solve ALL solutions using SolveAllSolutions API (Foreground mode)
|
||||
# This ensures all solutions (static + modal, etc.) complete before returning
|
||||
print(f"[JOURNAL] Solving all solutions using SolveAllSolutions API (Foreground mode)...")
|
||||
|
||||
numsolutionssolved1, numsolutionsfailed1, numsolutionsskipped1 = theCAESimSolveManager.SolveAllSolutions(
|
||||
NXOpen.CAE.SimSolution.SolveOption.Solve,
|
||||
NXOpen.CAE.SimSolution.SetupCheckOption.CompleteCheckAndOutputErrors,
|
||||
NXOpen.CAE.SimSolution.SolveMode.Foreground, # ✅ Blocks until complete
|
||||
False
|
||||
)
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
|
||||
| Aspect | SolveChainOfSolutions | SolveAllSolutions |
|
||||
|--------|----------------------|-------------------|
|
||||
| **Manual enumeration** | Required (loop through solutions) | Automatic (handles all solutions) |
|
||||
| **Background mode behavior** | Returns immediately, async | N/A (Foreground recommended) |
|
||||
| **Foreground mode behavior** | Blocks until complete | Blocks until complete ✅ |
|
||||
| **Use case** | Specific solution selection | Solve all solutions |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Location
|
||||
|
||||
**File**: `optimization_engine/solve_simulation.py`
|
||||
**Lines**: 271-295
|
||||
|
||||
**When to use this protocol**:
|
||||
- When `solution_name=None` is passed to `NXSolver.run_simulation()`
|
||||
- Any simulation with multiple solutions that must all complete
|
||||
- Multi-objective optimization requiring results from different analysis types
|
||||
|
||||
---
|
||||
|
||||
## Verification Steps
|
||||
|
||||
After implementing the fix, verify:
|
||||
|
||||
1. **Both .dat files are written** (one per solution)
|
||||
```
|
||||
beam_sim1-solution_1.dat # Static analysis
|
||||
beam_sim1-solution_2.dat # Modal analysis
|
||||
```
|
||||
|
||||
2. **Both .op2 files are created** with updated timestamps
|
||||
```
|
||||
beam_sim1-solution_1.op2 # Contains stress, displacement
|
||||
beam_sim1-solution_2.op2 # Contains eigenvalues, mode shapes
|
||||
```
|
||||
|
||||
3. **Results are unique per trial** - check that frequency values vary across trials
|
||||
|
||||
4. **Journal log shows**:
|
||||
```
|
||||
[JOURNAL] Solving all solutions using SolveAllSolutions API (Foreground mode)...
|
||||
[JOURNAL] Solve completed!
|
||||
[JOURNAL] Solutions solved: 2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Related Issues Fixed
|
||||
|
||||
1. **All trials showing identical frequency**: Fixed by ensuring modal solution runs
|
||||
2. **Only one data point in dashboard**: Fixed by all trials succeeding
|
||||
3. **Parallel coordinates with NaN**: Fixed by having complete data from all solutions
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- **User's Example**: `nx_journals/user_generated_journals/journal_solve_all_solution.py` (line 27)
|
||||
- **NX Open Documentation**: SimSolveManager.SolveAllSolutions() method
|
||||
- **Implementation**: `optimization_engine/solve_simulation.py`
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use Foreground mode** when solving all solutions
|
||||
2. **Verify OP2 timestamp changes** to ensure fresh solves
|
||||
3. **Check solve counts** in journal output to confirm both solutions ran
|
||||
4. **Test with 5 trials** before running large optimizations
|
||||
5. **Monitor unique frequency values** as a smoke test for multi-solution models
|
||||
|
||||
---
|
||||
|
||||
## Example Use Cases
|
||||
|
||||
### ✅ Correct Usage
|
||||
|
||||
```python
|
||||
# Multi-objective optimization with static + modal
|
||||
result = nx_solver.run_simulation(
|
||||
sim_file=sim_file,
|
||||
working_dir=model_dir,
|
||||
expression_updates=design_vars,
|
||||
solution_name=None # Solve ALL solutions
|
||||
)
|
||||
```
|
||||
|
||||
### ❌ Incorrect Usage (Don't Do This)
|
||||
|
||||
```python
|
||||
# Running modal separately - inefficient and error-prone
|
||||
result1 = nx_solver.run_simulation(..., solution_name="Solution 1") # Static
|
||||
result2 = nx_solver.run_simulation(..., solution_name="Solution 2") # Modal
|
||||
# This doubles the solve time and requires managing two result objects
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ Implemented and Verified
|
||||
**Impact**: Critical for all multi-solution optimization workflows
|
||||
316
studies/drone_gimbal_arm_optimization/run_optimization.py
Normal file
316
studies/drone_gimbal_arm_optimization/run_optimization.py
Normal file
@@ -0,0 +1,316 @@
|
||||
"""
|
||||
Drone Gimbal Arm Optimization - Protocol 11 (Multi-Objective NSGA-II)
|
||||
======================================================================
|
||||
|
||||
Multi-objective optimization using NSGA-II to find Pareto front:
|
||||
1. Minimize mass (target < 120g)
|
||||
2. Maximize fundamental frequency (target > 150 Hz)
|
||||
|
||||
Constraints:
|
||||
- Max displacement < 1.5mm (850g camera payload)
|
||||
- Max stress < 120 MPa (Al 6061-T6, SF=2.3)
|
||||
- Natural frequency > 150 Hz (avoid rotor resonance)
|
||||
|
||||
Usage:
|
||||
python run_optimization.py --trials 30
|
||||
python run_optimization.py --trials 5 # Quick test
|
||||
python run_optimization.py --resume # Continue existing study
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Add project root to path
|
||||
project_root = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
import optuna
|
||||
from optuna.samplers import NSGAIISampler
|
||||
from optimization_engine.nx_solver import NXSolver
|
||||
from optimization_engine.extractors.extract_displacement import extract_displacement
|
||||
from optimization_engine.extractors.extract_von_mises_stress import extract_solid_stress
|
||||
from optimization_engine.extractors.op2_extractor import OP2Extractor
|
||||
from optimization_engine.extractors.extract_frequency import extract_frequency
|
||||
from optimization_engine.extractors.extract_mass_from_bdf import extract_mass_from_bdf
|
||||
|
||||
# Import central configuration
|
||||
try:
|
||||
import config as atomizer_config
|
||||
except ImportError:
|
||||
atomizer_config = None
|
||||
|
||||
|
||||
def load_config(config_file: Path) -> dict:
|
||||
"""Load configuration from JSON file."""
|
||||
with open(config_file, 'r') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Run drone gimbal arm multi-objective optimization')
|
||||
parser.add_argument('--trials', type=int, default=30, help='Number of optimization trials')
|
||||
parser.add_argument('--resume', action='store_true', help='Resume existing study')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Get study directory
|
||||
study_dir = Path(__file__).parent
|
||||
|
||||
print("=" * 80)
|
||||
print("DRONE GIMBAL ARM OPTIMIZATION - PROTOCOL 11 (NSGA-II)")
|
||||
print("=" * 80)
|
||||
print()
|
||||
print("Engineering Scenario:")
|
||||
print(" Professional aerial cinematography drone camera gimbal support arm")
|
||||
print()
|
||||
print("Objectives:")
|
||||
print(" 1. MINIMIZE mass (target < 4000g, baseline = 4500g)")
|
||||
print(" 2. MAXIMIZE fundamental frequency (target > 150 Hz)")
|
||||
print()
|
||||
print("Constraints:")
|
||||
print(" - Max displacement < 1.5mm (850g camera payload)")
|
||||
print(" - Max von Mises stress < 120 MPa (Al 6061-T6, SF=2.3)")
|
||||
print(" - Natural frequency > 150 Hz (avoid rotor resonance 80-120 Hz)")
|
||||
print()
|
||||
print("Design Variables:")
|
||||
print(" - beam_half_core_thickness: 5-10 mm")
|
||||
print(" - beam_face_thickness: 1-3 mm")
|
||||
print(" - holes_diameter: 10-50 mm")
|
||||
print(" - hole_count: 8-14")
|
||||
print()
|
||||
print(f"Running {args.trials} trials with NSGA-II sampler...")
|
||||
print("=" * 80)
|
||||
print()
|
||||
|
||||
# Load configuration
|
||||
opt_config_file = study_dir / "1_setup" / "optimization_config.json"
|
||||
|
||||
if not opt_config_file.exists():
|
||||
print(f"[ERROR] Optimization config not found: {opt_config_file}")
|
||||
sys.exit(1)
|
||||
|
||||
opt_config = load_config(opt_config_file)
|
||||
|
||||
print(f"Loaded optimization config: {opt_config['study_name']}")
|
||||
print(f"Protocol: {opt_config['optimization_settings']['protocol']}")
|
||||
print()
|
||||
|
||||
# Setup paths
|
||||
model_dir = study_dir / "1_setup" / "model"
|
||||
model_file = model_dir / "Beam.prt"
|
||||
sim_file = model_dir / "Beam_sim1.sim"
|
||||
results_dir = study_dir / "2_results"
|
||||
results_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Initialize NX solver
|
||||
nx_solver = NXSolver(
|
||||
nastran_version=atomizer_config.NX_VERSION if atomizer_config else "2412",
|
||||
timeout=atomizer_config.NASTRAN_TIMEOUT if atomizer_config else 600,
|
||||
use_journal=True,
|
||||
enable_session_management=True,
|
||||
study_name="drone_gimbal_arm_optimization"
|
||||
)
|
||||
|
||||
def objective(trial: optuna.Trial) -> tuple:
|
||||
"""
|
||||
Multi-objective function for NSGA-II.
|
||||
|
||||
Returns:
|
||||
(mass, frequency): Tuple for NSGA-II (minimize mass, maximize frequency)
|
||||
"""
|
||||
# Sample design variables
|
||||
design_vars = {}
|
||||
for dv in opt_config['design_variables']:
|
||||
design_vars[dv['parameter']] = trial.suggest_float(
|
||||
dv['parameter'],
|
||||
dv['bounds'][0],
|
||||
dv['bounds'][1]
|
||||
)
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Trial #{trial.number}")
|
||||
print(f"{'='*60}")
|
||||
print(f"Design Variables:")
|
||||
for name, value in design_vars.items():
|
||||
print(f" {name}: {value:.3f}")
|
||||
|
||||
# Run simulation
|
||||
print(f"\nRunning simulation...")
|
||||
try:
|
||||
result = nx_solver.run_simulation(
|
||||
sim_file=sim_file,
|
||||
working_dir=model_dir,
|
||||
expression_updates=design_vars,
|
||||
solution_name=None # Solve all solutions (static + modal)
|
||||
)
|
||||
|
||||
if not result['success']:
|
||||
print(f"[ERROR] Simulation failed: {result.get('error', 'Unknown error')}")
|
||||
trial.set_user_attr("feasible", False)
|
||||
trial.set_user_attr("error", result.get('error', 'Unknown'))
|
||||
# Prune failed simulations instead of returning penalty values
|
||||
raise optuna.TrialPruned(f"Simulation failed: {result.get('error', 'Unknown error')}")
|
||||
|
||||
op2_file = result['op2_file']
|
||||
print(f"Simulation successful: {op2_file}")
|
||||
|
||||
# Extract all objectives and constraints
|
||||
print(f"\nExtracting results...")
|
||||
|
||||
# Extract mass (grams) from CAD expression p173
|
||||
# This expression measures the CAD mass directly
|
||||
from optimization_engine.extractors.extract_mass_from_expression import extract_mass_from_expression
|
||||
|
||||
prt_file = model_file # Beam.prt
|
||||
mass_kg = extract_mass_from_expression(prt_file, expression_name="p173")
|
||||
mass = mass_kg * 1000.0 # Convert to grams
|
||||
print(f" mass: {mass:.3f} g (from CAD expression p173)")
|
||||
|
||||
# Extract frequency (Hz) - from modal analysis (solution 2)
|
||||
# The drone gimbal has TWO solutions: solution_1 (static) and solution_2 (modal)
|
||||
op2_modal = str(op2_file).replace("solution_1", "solution_2")
|
||||
freq_result = extract_frequency(op2_modal, subcase=1, mode_number=1)
|
||||
frequency = freq_result['frequency']
|
||||
print(f" fundamental_frequency: {frequency:.3f} Hz")
|
||||
|
||||
# Extract displacement (mm) - from static analysis (subcase 1)
|
||||
disp_result = extract_displacement(op2_file, subcase=1)
|
||||
max_disp = disp_result['max_displacement']
|
||||
print(f" max_displacement_limit: {max_disp:.3f} mm")
|
||||
|
||||
# Extract stress (MPa) - from static analysis (subcase 1)
|
||||
stress_result = extract_solid_stress(op2_file, subcase=1, element_type='cquad4')
|
||||
max_stress = stress_result['max_von_mises']
|
||||
print(f" max_stress_limit: {max_stress:.3f} MPa")
|
||||
|
||||
# Frequency constraint uses same value as objective
|
||||
min_freq = frequency
|
||||
print(f" min_frequency_limit: {min_freq:.3f} Hz")
|
||||
|
||||
# Check constraints
|
||||
constraint_values = {
|
||||
'max_displacement_limit': max_disp,
|
||||
'max_stress_limit': max_stress,
|
||||
'min_frequency_limit': min_freq
|
||||
}
|
||||
|
||||
constraint_violations = []
|
||||
for constraint in opt_config['constraints']:
|
||||
name = constraint['name']
|
||||
value = constraint_values[name]
|
||||
threshold = constraint['threshold']
|
||||
c_type = constraint['type']
|
||||
|
||||
if c_type == 'less_than' and value > threshold:
|
||||
violation = (value - threshold) / threshold
|
||||
constraint_violations.append(f"{name}: {value:.2f} > {threshold} (violation: {violation:.1%})")
|
||||
elif c_type == 'greater_than' and value < threshold:
|
||||
violation = (threshold - value) / threshold
|
||||
constraint_violations.append(f"{name}: {value:.2f} < {threshold} (violation: {violation:.1%})")
|
||||
|
||||
if constraint_violations:
|
||||
print(f"\n[WARNING] Constraint violations:")
|
||||
for v in constraint_violations:
|
||||
print(f" - {v}")
|
||||
trial.set_user_attr("constraint_violations", constraint_violations)
|
||||
trial.set_user_attr("feasible", False)
|
||||
# NSGA-II handles constraints through constraint_satisfied flag - no penalty needed
|
||||
else:
|
||||
print(f"\n[OK] All constraints satisfied")
|
||||
trial.set_user_attr("feasible", True)
|
||||
|
||||
# Store all results as trial attributes for dashboard
|
||||
trial.set_user_attr("mass", mass)
|
||||
trial.set_user_attr("frequency", frequency)
|
||||
trial.set_user_attr("max_displacement", max_disp)
|
||||
trial.set_user_attr("max_stress", max_stress)
|
||||
trial.set_user_attr("design_vars", design_vars)
|
||||
|
||||
print(f"\nObjectives:")
|
||||
print(f" mass: {mass:.2f} g (minimize)")
|
||||
print(f" frequency: {frequency:.2f} Hz (maximize)")
|
||||
|
||||
# Return tuple for NSGA-II: (minimize mass, maximize frequency)
|
||||
# Using proper semantic directions in study creation
|
||||
return (mass, frequency)
|
||||
|
||||
except optuna.TrialPruned:
|
||||
# Re-raise pruned exceptions (don't catch them)
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"\n[ERROR] Trial failed with exception: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
trial.set_user_attr("error", str(e))
|
||||
trial.set_user_attr("feasible", False)
|
||||
# Prune corrupted trials instead of returning penalty values
|
||||
raise optuna.TrialPruned(f"Trial failed with exception: {str(e)}")
|
||||
|
||||
# Create Optuna study with NSGA-II sampler
|
||||
study_name = opt_config['study_name']
|
||||
storage = f"sqlite:///{results_dir / 'study.db'}"
|
||||
|
||||
if args.resume:
|
||||
print(f"[INFO] Resuming existing study: {study_name}")
|
||||
study = optuna.load_study(
|
||||
study_name=study_name,
|
||||
storage=storage,
|
||||
sampler=NSGAIISampler()
|
||||
)
|
||||
print(f"[INFO] Loaded study with {len(study.trials)} existing trials")
|
||||
else:
|
||||
print(f"[INFO] Creating new study: {study_name}")
|
||||
study = optuna.create_study(
|
||||
study_name=study_name,
|
||||
storage=storage,
|
||||
directions=['minimize', 'maximize'], # Minimize mass, maximize frequency
|
||||
sampler=NSGAIISampler(),
|
||||
load_if_exists=True # Always allow resuming existing study
|
||||
)
|
||||
|
||||
# Run optimization
|
||||
print(f"\nStarting optimization with {args.trials} trials...")
|
||||
print()
|
||||
|
||||
study.optimize(
|
||||
objective,
|
||||
n_trials=args.trials,
|
||||
show_progress_bar=True
|
||||
)
|
||||
|
||||
# Save final results
|
||||
print()
|
||||
print("=" * 80)
|
||||
print("Optimization Complete!")
|
||||
print("=" * 80)
|
||||
print()
|
||||
print(f"Total trials: {len(study.trials)}")
|
||||
print(f"Pareto front solutions: {len(study.best_trials)}")
|
||||
print()
|
||||
|
||||
# Show Pareto front
|
||||
print("Pareto Front (non-dominated solutions):")
|
||||
print()
|
||||
for i, trial in enumerate(study.best_trials):
|
||||
mass = trial.values[0]
|
||||
freq = trial.values[1] # Frequency is stored as positive now
|
||||
feasible = trial.user_attrs.get('feasible', False)
|
||||
print(f" Solution #{i+1} (Trial {trial.number}):")
|
||||
print(f" Mass: {mass:.2f} g")
|
||||
print(f" Frequency: {freq:.2f} Hz")
|
||||
print(f" Feasible: {feasible}")
|
||||
print()
|
||||
|
||||
print("Results available in: studies/drone_gimbal_arm_optimization/2_results/")
|
||||
print()
|
||||
print("View in Dashboard:")
|
||||
print(" 1. Ensure backend is running: cd atomizer-dashboard/backend && python -m uvicorn api.main:app --reload")
|
||||
print(" 2. Open dashboard: http://localhost:3003")
|
||||
print(" 3. Select study: drone_gimbal_arm_optimization")
|
||||
print()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user