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:
2025-11-24 07:49:48 -05:00
parent f76bd52894
commit dd7f0c0f82
7 changed files with 1529 additions and 220 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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/

View 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

View 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()