diff --git a/openevolve/database.py b/openevolve/database.py index ab1dad21b..e240c5775 100644 --- a/openevolve/database.py +++ b/openevolve/database.py @@ -2078,12 +2078,18 @@ def migrate_programs(self) -> None: self._validate_migration_results() return + # Choose every island's migrants from the population before this round. Choosing them + # while migrating lets an island's top slots go to copies it has just received (which + # _migrate_one skips), and lets those copies displace its own programs from their cells + # before they get the chance to migrate. + planned_moves = [] for i, island in enumerate(self.islands): - if len(island) == 0: - continue - - # Select top programs from this island for migration - island_programs = [self.programs[pid] for pid in island if pid in self.programs] + # Migrant copies are never migrated again, so they must not take migration slots + island_programs = [ + self.programs[pid] + for pid in island + if pid in self.programs and not self.programs[pid].metadata.get("migrant", False) + ] if not island_programs: continue @@ -2102,7 +2108,10 @@ def migrate_programs(self) -> None: for migrant in migrants: for target_island in target_islands: - self._migrate_one(migrant.id, target_island) + planned_moves.append((migrant.id, target_island)) + + for program_id, target_island in planned_moves: + self._migrate_one(program_id, target_island) # Update last migration generation self.last_migration_generation = max(self.island_generations) diff --git a/tests/test_island_migration.py b/tests/test_island_migration.py index 5765bd33d..00c649740 100644 --- a/tests/test_island_migration.py +++ b/tests/test_island_migration.py @@ -114,6 +114,21 @@ def test_migration_ring_topology(self): # This is a known limitation of the current implementation that processes islands # sequentially while modifying them, causing interference between migration rounds. + def test_every_island_migrates_its_own_best(self): + """Migrants are chosen before any are moved, so an island's own best still migrates + even when a better migrant has just arrived from the previous island in the ring.""" + # Different code lengths put the programs in different MAP-Elites cells + for pid, score, island, pad in [("a", 0.9, 0, 0), ("b", 0.5, 1, 400), ("c", 0.3, 2, 800)]: + program = self._create_test_program(pid, score, island) + program.code += "#" * (pad + 1) + self.db.add(program, target_island=island) + + self.db.migrate_programs() + + for island in self.db.islands: + scores = sorted(self.db.programs[pid].metrics["score"] for pid in island) + self.assertEqual(scores, [0.3, 0.5, 0.9]) + def test_migration_rate_respected(self): """Test that migration rate is properly applied""" # Add multiple programs to island 0