# SYS_11: Multi-Objective Support ## Overview **ALL** optimization engines in Atomizer **MUST** support both single-objective and multi-objective optimization without requiring code changes. This protocol ensures system robustness and prevents runtime failures when handling Pareto optimization. **Key Requirement**: Code must work with both `study.best_trial` (single) and `study.best_trials` (multi) APIs. --- ## When to Use | Trigger | Action | |---------|--------| | 2+ objectives defined in config | Use NSGA-II sampler | | "pareto", "multi-objective" mentioned | Load this protocol | | "tradeoff", "competing goals" | Suggest multi-objective approach | | "minimize X AND maximize Y" | Configure as multi-objective | --- ## Quick Reference **Single vs. Multi-Objective API**: | Operation | Single-Objective | Multi-Objective | |-----------|-----------------|-----------------| | Best trial | `study.best_trial` | `study.best_trials[0]` | | Best params | `study.best_params` | `trial.params` | | Best value | `study.best_value` | `trial.values` (tuple) | | Direction | `direction='minimize'` | `directions=['minimize', 'maximize']` | | Sampler | TPE, CMA-ES, GP | NSGA-II (mandatory) | --- ## The Problem This Solves Previously, optimization components only supported single-objective. When used with multi-objective studies: 1. Trials run successfully 2. Trials saved to database 3. **CRASH** when compiling results - `study.best_trial` raises RuntimeError - No tracking files generated - Silent failures **Root Cause**: Optuna has different APIs: ```python # Single-Objective (works) study.best_trial # Returns Trial object study.best_params # Returns dict study.best_value # Returns float # Multi-Objective (RAISES RuntimeError) study.best_trial # ❌ RuntimeError study.best_params # ❌ RuntimeError study.best_value # ❌ RuntimeError study.best_trials # ✓ Returns LIST of Pareto-optimal trials ``` --- ## Solution Pattern ### 1. Always Check Study Type ```python is_multi_objective = len(study.directions) > 1 ``` ### 2. Use Conditional Access ```python if is_multi_objective: best_trials = study.best_trials if best_trials: # Select representative trial (e.g., first Pareto solution) representative_trial = best_trials[0] best_params = representative_trial.params best_value = representative_trial.values # Tuple best_trial_num = representative_trial.number else: best_params = {} best_value = None best_trial_num = None else: # Single-objective: safe to use standard API best_params = study.best_params best_value = study.best_value best_trial_num = study.best_trial.number ``` ### 3. Return Rich Metadata Always include in results: ```python { 'best_params': best_params, 'best_value': best_value, # float or tuple 'best_trial': best_trial_num, 'is_multi_objective': is_multi_objective, 'pareto_front_size': len(study.best_trials) if is_multi_objective else 1, } ``` --- ## Implementation Checklist When creating or modifying any optimization component: - [ ] **Study Creation**: Support `directions` parameter ```python if len(objectives) > 1: directions = [obj['type'] for obj in objectives] # ['minimize', 'maximize'] study = optuna.create_study(directions=directions, ...) else: study = optuna.create_study(direction='minimize', ...) ``` - [ ] **Result Compilation**: Check `len(study.directions) > 1` - [ ] **Best Trial Access**: Use conditional logic - [ ] **Logging**: Print Pareto front size for multi-objective - [ ] **Reports**: Handle tuple objectives in visualization - [ ] **Testing**: Test with BOTH single and multi-objective cases --- ## Configuration **Multi-Objective Config Example**: ```json { "objectives": [ { "name": "stiffness", "type": "maximize", "description": "Structural stiffness (N/mm)", "unit": "N/mm" }, { "name": "mass", "type": "minimize", "description": "Total mass (kg)", "unit": "kg" } ], "optimization_settings": { "sampler": "NSGAIISampler", "n_trials": 50 } } ``` **Objective Function Return Format**: ```python # Single-objective: return float def objective_single(trial): # ... compute ... return objective_value # float # Multi-objective: return tuple def objective_multi(trial): # ... compute ... return (stiffness, mass) # tuple of floats ``` --- ## Semantic Directions Use semantic direction values - no negative tricks: ```python # ✅ CORRECT: Semantic directions objectives = [ {"name": "stiffness", "type": "maximize"}, {"name": "mass", "type": "minimize"} ] # Return: (stiffness, mass) - both positive values # ❌ WRONG: Negative trick def objective(trial): return (-stiffness, mass) # Don't negate to fake maximize ``` Optuna handles directions correctly when you specify `directions=['maximize', 'minimize']`. --- ## Testing Protocol Before marking any optimization component complete: ### Test 1: Single-Objective ```python # Config with 1 objective directions = None # or ['minimize'] # Run optimization # Verify: completes without errors ``` ### Test 2: Multi-Objective ```python # Config with 2+ objectives directions = ['minimize', 'minimize'] # Run optimization # Verify: completes without errors # Verify: ALL tracking files generated ``` ### Test 3: Verify Outputs - `2_results/study.db` exists - `2_results/intelligent_optimizer/` has tracking files - `2_results/optimization_summary.json` exists - No RuntimeError in logs --- ## NSGA-II Configuration For multi-objective optimization, use NSGA-II: ```python import optuna from optuna.samplers import NSGAIISampler sampler = NSGAIISampler( population_size=50, # Pareto front population mutation_prob=None, # Auto-computed crossover_prob=0.9, # Recombination rate swapping_prob=0.5, # Gene swapping probability seed=42 # Reproducibility ) study = optuna.create_study( directions=['maximize', 'minimize'], sampler=sampler, study_name="multi_objective_study", storage="sqlite:///study.db" ) ``` --- ## Pareto Front Handling ### Accessing Pareto Solutions ```python if is_multi_objective: pareto_trials = study.best_trials print(f"Found {len(pareto_trials)} Pareto-optimal solutions") for trial in pareto_trials: print(f"Trial {trial.number}: {trial.values}") print(f" Params: {trial.params}") ``` ### Selecting Representative Solution ```python # Option 1: First Pareto solution representative = study.best_trials[0] # Option 2: Weighted selection def weighted_selection(trials, weights): best_score = float('inf') best_trial = None for trial in trials: score = sum(w * v for w, v in zip(weights, trial.values)) if score < best_score: best_score = score best_trial = trial return best_trial # Option 3: Knee point (maximum distance from ideal line) # Requires more complex computation ``` --- ## Troubleshooting | Symptom | Cause | Solution | |---------|-------|----------| | RuntimeError on `best_trial` | Multi-objective study using single API | Use conditional check pattern | | Empty Pareto front | No feasible solutions | Check constraints, relax if needed | | Only 1 Pareto solution | Objectives not conflicting | Verify objectives are truly competing | | NSGA-II with single objective | Wrong config | Use TPE/CMA-ES for single-objective | --- ## Cross-References - **Depends On**: None (mandatory for all) - **Used By**: All optimization components - **Integrates With**: - [SYS_10_IMSO](./SYS_10_IMSO.md) (selects NSGA-II for multi-objective) - [SYS_13_DASHBOARD_TRACKING](./SYS_13_DASHBOARD_TRACKING.md) (Pareto visualization) - **See Also**: [OP_04_ANALYZE_RESULTS](../operations/OP_04_ANALYZE_RESULTS.md) for Pareto analysis --- ## Implementation Files Files that implement this protocol: - `optimization_engine/intelligent_optimizer.py` - `_compile_results()` method - `optimization_engine/study_continuation.py` - Result handling - `optimization_engine/hybrid_study_creator.py` - Study creation Files requiring this protocol: - [ ] `optimization_engine/study_continuation.py` - [ ] `optimization_engine/hybrid_study_creator.py` - [ ] `optimization_engine/intelligent_setup.py` - [ ] `optimization_engine/llm_optimization_runner.py` --- ## Version History | Version | Date | Changes | |---------|------|---------| | 1.0 | 2025-11-20 | Initial release, mandatory for all engines |