Skip to content

Commit 38207bb

Browse files
fix: use island-aware inspiration sampling in sample_from_island (#471)
sample_from_island() used plain random.sample() for inspirations when the island was non-empty, while only the empty-island fallback used _sample_inspirations() (island best + elites + feature-cell diversity). Add an explicit island_id parameter to _sample_inspirations() and reuse it in sample_from_island(). This keeps parallel workers isolated even when archive fallback returns a parent whose metadata belongs to a different island, and avoids the shared current_island fallback. Closes #437
1 parent 6a56527 commit 38207bb

2 files changed

Lines changed: 153 additions & 15 deletions

File tree

openevolve/database.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -467,21 +467,13 @@ def sample_from_island(
467467
parent = self._sample_from_island_weighted(island_id)
468468
sampling_mode = "weighted"
469469

470-
# Select inspirations from the same island
470+
# Select inspirations using the same elite/diversity-aware strategy as sample().
471+
# Pass the requested island explicitly so parallel workers remain isolated even
472+
# if an archive fallback returns a parent whose metadata points elsewhere.
471473
if num_inspirations is None:
472474
num_inspirations = 5 # Default for backward compatibility
473475

474-
# Get other programs from the island for inspirations
475-
other_programs = [pid for pid in island_programs if pid != parent.id]
476-
477-
if len(other_programs) < num_inspirations:
478-
# Not enough programs in island, use what we have
479-
inspiration_ids = other_programs
480-
else:
481-
# Sample inspirations
482-
inspiration_ids = random.sample(other_programs, num_inspirations)
483-
484-
inspirations = [self.programs[pid] for pid in inspiration_ids if pid in self.programs]
476+
inspirations = self._sample_inspirations(parent, n=num_inspirations, island_id=island_id)
485477

486478
logger.debug(
487479
f"Sampled parent {parent.id} and {len(inspirations)} inspirations from island {island_id} "
@@ -1569,7 +1561,9 @@ def _sample_from_archive_for_island(self, island_id: int) -> Program:
15691561
parent_id = random.choice(valid_archive)
15701562
return self.programs[parent_id]
15711563

1572-
def _sample_inspirations(self, parent: Program, n: int = 5) -> List[Program]:
1564+
def _sample_inspirations(
1565+
self, parent: Program, n: int = 5, island_id: Optional[int] = None
1566+
) -> List[Program]:
15731567
"""
15741568
Sample inspiration programs for the next evolution step.
15751569
@@ -1579,14 +1573,23 @@ def _sample_inspirations(self, parent: Program, n: int = 5) -> List[Program]:
15791573
Args:
15801574
parent: Parent program
15811575
n: Number of inspirations to sample
1576+
island_id: Explicit island to sample from. If omitted, use the
1577+
parent program's island metadata.
15821578
15831579
Returns:
15841580
List of inspiration programs from the current island
15851581
"""
15861582
inspirations = []
15871583

1588-
# Get the parent's island (should be current_island)
1589-
parent_island = parent.metadata.get("island", self.current_island)
1584+
# Prefer an explicitly requested island. This matters for
1585+
# sample_from_island(), where archive fallback may return a parent whose
1586+
# metadata belongs to a different island.
1587+
if island_id is None:
1588+
parent_island = parent.metadata.get("island", self.current_island)
1589+
else:
1590+
parent_island = island_id
1591+
1592+
parent_island %= len(self.islands)
15901593

15911594
# Get all programs from the current island
15921595
island_program_ids = list(self.islands[parent_island])
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Regression tests for issue #437: island-aware inspiration sampling."""
2+
3+
import unittest
4+
from unittest.mock import patch
5+
6+
from openevolve.config import Config
7+
from openevolve.database import Program, ProgramDatabase
8+
9+
10+
class TestSampleFromIslandInspirations(unittest.TestCase):
11+
def setUp(self):
12+
config = Config()
13+
config.database.in_memory = True
14+
config.database.num_islands = 2
15+
config.database.exploration_ratio = 1.0
16+
config.database.exploitation_ratio = 0.0
17+
self.db = ProgramDatabase(config.database)
18+
19+
self.parent = Program(
20+
id="parent",
21+
code="def parent(): pass",
22+
metrics={"score": 0.5},
23+
metadata={"island": 1},
24+
)
25+
self.elite = Program(
26+
id="elite",
27+
code="def elite(): pass",
28+
metrics={"score": 1.0},
29+
metadata={"island": 1},
30+
)
31+
32+
self.db.programs = {
33+
self.parent.id: self.parent,
34+
self.elite.id: self.elite,
35+
}
36+
self.db.islands = [set(), {self.parent.id, self.elite.id}]
37+
self.db.island_best_programs = [None, self.elite.id]
38+
39+
def test_sample_from_island_reuses_strategy_aware_inspiration_sampler(self):
40+
with (
41+
patch.object(
42+
self.db,
43+
"_sample_from_island_random",
44+
return_value=self.parent,
45+
),
46+
patch.object(
47+
self.db,
48+
"_sample_inspirations",
49+
return_value=[self.elite],
50+
) as sample_inspirations,
51+
):
52+
parent, inspirations = self.db.sample_from_island(
53+
island_id=1,
54+
num_inspirations=2,
55+
)
56+
57+
self.assertIs(parent, self.parent)
58+
self.assertEqual(inspirations, [self.elite])
59+
sample_inspirations.assert_called_once_with(
60+
self.parent,
61+
n=2,
62+
island_id=1,
63+
)
64+
65+
def test_explicit_island_overrides_parent_metadata_for_inspirations(self):
66+
island_zero_best = Program(
67+
id="island-zero-best",
68+
code="def island_zero(): pass",
69+
metrics={"score": 2.0},
70+
metadata={"island": 0},
71+
)
72+
parent_from_zero = Program(
73+
id="parent-from-zero",
74+
code="def parent_zero(): pass",
75+
metrics={"score": 0.1},
76+
metadata={"island": 0},
77+
)
78+
island_one_best = Program(
79+
id="island-one-best",
80+
code="def island_one(): pass",
81+
metrics={"score": 1.0},
82+
metadata={"island": 1},
83+
)
84+
85+
self.db.programs = {
86+
parent_from_zero.id: parent_from_zero,
87+
island_zero_best.id: island_zero_best,
88+
island_one_best.id: island_one_best,
89+
}
90+
self.db.islands = [
91+
{parent_from_zero.id, island_zero_best.id},
92+
{island_one_best.id},
93+
]
94+
self.db.island_best_programs = [
95+
island_zero_best.id,
96+
island_one_best.id,
97+
]
98+
99+
inspirations = self.db._sample_inspirations(
100+
parent_from_zero,
101+
n=1,
102+
island_id=1,
103+
)
104+
105+
self.assertEqual([program.id for program in inspirations], [island_one_best.id])
106+
self.assertTrue(all(program.metadata.get("island") == 1 for program in inspirations))
107+
108+
def test_zero_inspirations_is_forwarded_without_random_sampling(self):
109+
with (
110+
patch.object(
111+
self.db,
112+
"_sample_from_island_random",
113+
return_value=self.parent,
114+
),
115+
patch.object(
116+
self.db,
117+
"_sample_inspirations",
118+
return_value=[],
119+
) as sample_inspirations,
120+
):
121+
_, inspirations = self.db.sample_from_island(
122+
island_id=1,
123+
num_inspirations=0,
124+
)
125+
126+
self.assertEqual(inspirations, [])
127+
sample_inspirations.assert_called_once_with(
128+
self.parent,
129+
n=0,
130+
island_id=1,
131+
)
132+
133+
134+
if __name__ == "__main__":
135+
unittest.main()

0 commit comments

Comments
 (0)