From dd7f0c0f8235a4163d94f1242b6404f0d99bc682 Mon Sep 17 00:00:00 2001 From: Anto01 Date: Mon, 24 Nov 2025 07:49:48 -0500 Subject: [PATCH] Phase 3.3: Multi-objective optimization fix, updated docs & Claude skill MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .claude/skills/analyze-workflow.md | 330 ++++++++++--- CHANGELOG.md | 40 ++ README.md | 30 +- .../components/ParallelCoordinatesPlot.tsx | 446 ++++++++++++------ docs/DASHBOARD.md | 390 +++++++++++++++ docs/NX_MULTI_SOLUTION_PROTOCOL.md | 197 ++++++++ .../run_optimization.py | 316 +++++++++++++ 7 files changed, 1529 insertions(+), 220 deletions(-) create mode 100644 docs/DASHBOARD.md create mode 100644 docs/NX_MULTI_SOLUTION_PROTOCOL.md create mode 100644 studies/drone_gimbal_arm_optimization/run_optimization.py diff --git a/.claude/skills/analyze-workflow.md b/.claude/skills/analyze-workflow.md index 5ad351fc..029dc440 100644 --- a/.claude/skills/analyze-workflow.md +++ b/.claude/skills/analyze-workflow.md @@ -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. \ No newline at end of file +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. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d8272e3..ffc9e355 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/README.md b/README.md index 50935a02..00a41043 100644 --- a/README.md +++ b/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 diff --git a/atomizer-dashboard/frontend/src/components/ParallelCoordinatesPlot.tsx b/atomizer-dashboard/frontend/src/components/ParallelCoordinatesPlot.tsx index c6217afe..60ebfa72 100644 --- a/atomizer-dashboard/frontend/src/components/ParallelCoordinatesPlot.tsx +++ b/atomizer-dashboard/frontend/src/components/ParallelCoordinatesPlot.tsx @@ -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; + user_attrs?: Record; 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(null); const [selectedTrials, setSelectedTrials] = useState>(new Set()); - if (paretoData.length === 0) { + // Safety checks + if (!paretoData || paretoData.length === 0) { return ( -
-

Parallel Coordinates

-
- No Pareto front data yet +
+

Parallel Coordinates Plot

+
+ No Pareto front data available
); } - // 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 ( +
+

Parallel Coordinates Plot

+
+ Missing objectives or design variables metadata +
+
+ ); + } + + // 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(); + 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 ( -
+
-

- Parallel Coordinates ({paretoData.length} solutions) -

+
+

+ Parallel Coordinates Plot ({paretoData.length} solutions) +

+

+ Design Variables → Objectives → Constraints +

+
{selectedTrials.size > 0 && ( )}
- - - {/* Draw axes */} - {axes.map((axis, i) => { - const x = i * axisSpacing; - return ( - - {/* Axis line */} - + + + {/* Draw axes */} + {axes.map((axis, i) => { + const x = i * axisSpacing; + const bgColor = axis.type === 'design_var' ? '#EFF6FF' : + axis.type === 'objective' ? '#F0FDF4' : '#FEF3C7'; + + return ( + + {/* Background highlight */} + + + {/* Axis line */} + + + {/* Axis label */} + + {(axis.label || '').split('\n').map((line, idx) => ( + {line} + ))} + + + {/* Type badge */} + + {axis.type === 'design_var' ? 'DESIGN VAR' : + axis.type === 'objective' ? 'OBJECTIVE' : 'CONSTRAINT'} + + + {/* Min/max labels */} + + {ranges[i].min.toFixed(2)} + + + {ranges[i].max.toFixed(2)} + + + ); + })} + + {/* 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 ( + 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 */} - - {axis.label} + {/* Hover tooltip */} + {hoveredTrial !== null && ( + + + + Trial #{hoveredTrial} - - {/* Min/max labels */} - - {ranges[i].min.toFixed(2)} + + Click to select/deselect - - {ranges[i].max.toFixed(2)} + + {selectedTrials.has(hoveredTrial) ? '✓ Selected' : '○ Not selected'} - ); - })} - - {/* 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 ( - 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 && ( - - - - Trial #{hoveredTrial} - - - Click to select - - - {selectedTrials.has(hoveredTrial) ? '✓ Selected' : '○ Not selected'} - - - )} - - + )} + + +
{/* Legend */} -
+
-
- Feasible +
+ Feasible
-
- Infeasible +
+ Infeasible
-
- Selected +
+ Selected +
+
+
+ Hover +
+
+ + {/* Axis type legend */} +
+
+
+ Design Variables +
+
+
+ Objectives +
+
+
+ Constraints
diff --git a/docs/DASHBOARD.md b/docs/DASHBOARD.md new file mode 100644 index 00000000..6f40d86a --- /dev/null +++ b/docs/DASHBOARD.md @@ -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 ; +if (!objectives || !designVariables) return ; +``` + +### 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/ diff --git a/docs/NX_MULTI_SOLUTION_PROTOCOL.md b/docs/NX_MULTI_SOLUTION_PROTOCOL.md new file mode 100644 index 00000000..84c6f0d0 --- /dev/null +++ b/docs/NX_MULTI_SOLUTION_PROTOCOL.md @@ -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 diff --git a/studies/drone_gimbal_arm_optimization/run_optimization.py b/studies/drone_gimbal_arm_optimization/run_optimization.py new file mode 100644 index 00000000..1c7a1444 --- /dev/null +++ b/studies/drone_gimbal_arm_optimization/run_optimization.py @@ -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()