File size: 2,678 Bytes
01795ee
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# Block Optimization Fix Summary

## The Problem

NSGA-II optimizer was only producing **33-42 blocks** instead of the expected **106 blocks**.

## Root Causes

### 1. Reference vs Copy Issue
When storing best solutions from the Pareto front, we stored references instead of copies:

```python
# WRONG - stores references that get overwritten
best_solutions = [(population[i], objectives[i]) for i in fronts[0]]
best_block_solutions = [block_population[i] for i in fronts[0]]
```

Since `population` and `block_population` are replaced each generation with `offspring`, the stored references pointed to stale/corrupted data.

### 2. Block-Trainset Mismatch
Even with copies, the stored block assignments were created for a *different* trainset selection. When the best solution evolved to have different service trainsets, the old block assignment still mapped to old trainset indices.

Example:
- Generation 50: Best solution has trainsets [0, 2, 5] β†’ blocks assigned to indices 0, 2, 5
- Generation 150: Best solution evolves to trainsets [1, 3, 7] β†’ but block assignment still references 0, 2, 5
- Result: Many blocks map to non-service trainsets β†’ lost blocks

## The Fix

**Always create fresh block assignments for the final best solution:**

```python
# Select best solution from Pareto front
if best_solutions:
    best_idx = min(range(len(best_solutions)), 
                  key=lambda i: self.evaluator.fitness_function(best_solutions[i][0]))
    best_solution, best_objectives = best_solutions[best_idx]
    if self.optimize_blocks:
        # Always create fresh block assignment for the best solution
        # to ensure all 106 blocks are properly assigned
        best_block_sol = self._create_block_assignment(best_solution)
```

The `_create_block_assignment` distributes all blocks evenly across current service trainsets:

```python
def _create_block_assignment(self, trainset_sol: np.ndarray) -> np.ndarray:
    service_indices = np.where(trainset_sol == 0)[0]
    
    if len(service_indices) == 0:
        return np.full(self.n_blocks, -1, dtype=int)
    
    # Distribute blocks evenly across service trains
    block_sol = np.zeros(self.n_blocks, dtype=int)
    for i in range(self.n_blocks):
        block_sol[i] = service_indices[i % len(service_indices)]
    
    return block_sol
```

## Result

| Optimizer | Before Fix | After Fix |
|-----------|-----------|-----------|
| GA        | 106 βœ“     | 106 βœ“     |
| CMA-ES    | 106 βœ“     | 106 βœ“     |
| PSO       | 106 βœ“     | 106 βœ“     |
| SA        | 106 βœ“     | 106 βœ“     |
| NSGA-II   | 33-42 βœ—   | 106 βœ“     |

All optimizers now correctly assign all 106 service blocks.