339 lines
8.6 KiB
Markdown
339 lines
8.6 KiB
Markdown
|
|
# SYS_11: Multi-Objective Support
|
||
|
|
|
||
|
|
<!--
|
||
|
|
PROTOCOL: Multi-Objective Optimization Support
|
||
|
|
LAYER: System
|
||
|
|
VERSION: 1.0
|
||
|
|
STATUS: Active (MANDATORY)
|
||
|
|
LAST_UPDATED: 2025-12-05
|
||
|
|
PRIVILEGE: user
|
||
|
|
LOAD_WITH: []
|
||
|
|
-->
|
||
|
|
|
||
|
|
## 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 |
|