Skip to content
Merged

fixes #224

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion openevolve/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version information for openevolve package."""

__version__ = "0.2.1"
__version__ = "0.2.2"
32 changes: 29 additions & 3 deletions openevolve/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def add(
Args:
program: Program to add
iteration: Current iteration (defaults to last_iteration)
target_island: Specific island to add to (uses current_island if None)
target_island: Specific island to add to (auto-detects parent's island if None)

Returns:
Program ID
Expand Down Expand Up @@ -263,8 +263,34 @@ def add(

self.feature_map[feature_key] = program.id

# Add to specific island (not random!)
island_idx = target_island if target_island is not None else self.current_island
# Determine target island
# If target_island is not specified and program has a parent, inherit parent's island
if target_island is None and program.parent_id:
parent = self.programs.get(program.parent_id)
if parent and "island" in parent.metadata:
# Child inherits parent's island to maintain island isolation
island_idx = parent.metadata["island"]
logger.debug(
f"Program {program.id} inheriting island {island_idx} from parent {program.parent_id}"
)
else:
# Parent not found or has no island, use current_island
island_idx = self.current_island
if parent:
logger.warning(
f"Parent {program.parent_id} has no island metadata, using current_island {island_idx}"
)
else:
logger.warning(
f"Parent {program.parent_id} not found, using current_island {island_idx}"
)
elif target_island is not None:
# Explicit target island specified (e.g., for migrants)
island_idx = target_island
else:
# No parent and no target specified, use current island
island_idx = self.current_island

island_idx = island_idx % len(self.islands) # Ensure valid island
self.islands[island_idx].add(program.id)

Expand Down
3 changes: 2 additions & 1 deletion openevolve/process_parallel.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,8 @@ async def run_evolution(
# Reconstruct program from dict
child_program = Program(**result.child_program_dict)

# Add to database
# Add to database (will auto-inherit parent's island)
# No need to specify target_island - database will handle parent island inheritance
self.database.add(child_program, iteration=completed_iteration)

# Store artifacts
Expand Down
4 changes: 2 additions & 2 deletions openevolve/prompt/sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ def _format_evolution_history(
combined_programs_str = top_programs_str + diverse_programs_str

# Format inspirations section
inspirations_section_str = self._format_inspirations_section(inspirations, language)
inspirations_section_str = self._format_inspirations_section(inspirations, language, feature_dimensions)

# Combine into full history
return history_template.format(
Expand All @@ -412,7 +412,7 @@ def _format_evolution_history(
)

def _format_inspirations_section(
self, inspirations: List[Dict[str, Any]], language: str
self, inspirations: List[Dict[str, Any]], language: str, feature_dimensions: Optional[List[str]] = None
) -> str:
"""
Format the inspirations section for the prompt
Expand Down
174 changes: 174 additions & 0 deletions tests/test_island_parent_consistency.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
"""
Test for island parent-child consistency - Programs' parents should be in the corresponding islands
"""

import unittest
from openevolve.config import Config
from openevolve.database import ProgramDatabase, Program


class TestIslandParentConsistency(unittest.TestCase):
"""Test that parent-child relationships respect island boundaries"""

def test_parent_child_island_consistency(self):
"""Test that children are added to the same island as their parents"""
config = Config()
config.database.num_islands = 3
database = ProgramDatabase(config.database)

# Create initial program on island 0
initial_program = Program(
id="initial",
code="def initial(): pass",
metrics={"score": 0.5},
iteration_found=0
)
database.add(initial_program) # Should go to island 0 (current_island)

# Verify initial program is on island 0
self.assertIn("initial", database.islands[0])
self.assertEqual(initial_program.metadata.get("island"), 0)

# Now switch to island 1
database.next_island()
self.assertEqual(database.current_island, 1)

# Create a child of the initial program
child_program = Program(
id="child1",
code="def child1(): pass",
parent_id="initial", # Parent is on island 0
metrics={"score": 0.6},
iteration_found=1
)

# Add child without specifying target_island
# This is what happens in process_parallel.py line 445
database.add(child_program)

# With the fix: child should go to parent's island (0), not current_island (1)
parent_island = database.programs["initial"].metadata.get("island", 0)
child_island = database.programs["child1"].metadata.get("island")

# Check if parent is in child's island (this is what the user's assertion checks)
if child_program.parent_id:
# This is the exact check from the issue report - should now pass
self.assertIn(
child_program.parent_id,
database.islands[child_island],
"Parent should be in child's island"
)

# Verify child is on same island as parent
self.assertEqual(
parent_island,
child_island,
f"Child should be on same island as parent. Parent: island {parent_island}, Child: island {child_island}"
)

def test_multiple_generations_island_drift(self):
"""Test that island drift happens across multiple generations"""
config = Config()
config.database.num_islands = 4
database = ProgramDatabase(config.database)

# Create a lineage
programs = []
for i in range(10):
if i == 0:
# Initial program
prog = Program(
id=f"prog_{i}",
code=f"def prog_{i}(): pass",
metrics={"score": 0.1 * i},
iteration_found=i
)
else:
# Child of previous
prog = Program(
id=f"prog_{i}",
code=f"def prog_{i}(): pass",
parent_id=f"prog_{i-1}",
metrics={"score": 0.1 * i},
iteration_found=i
)

database.add(prog)
programs.append(prog)

# Switch islands periodically (simulating what happens in evolution)
if i % 3 == 0:
database.next_island()

# Check island consistency
inconsistent_pairs = []
for prog in programs:
if prog.parent_id:
parent = database.programs.get(prog.parent_id)
if parent:
parent_island = parent.metadata.get("island")
child_island = prog.metadata.get("island")

# Check if parent is in child's island
if prog.parent_id not in database.islands[child_island]:
inconsistent_pairs.append((prog.parent_id, prog.id))

# With the fix, we should find NO inconsistent parent-child island assignments
self.assertEqual(
len(inconsistent_pairs),
0,
f"Found {len(inconsistent_pairs)} inconsistent parent-child pairs: {inconsistent_pairs}"
)

# Verify all parent-child pairs are on the same island
for prog in programs:
if prog.parent_id:
parent = database.programs.get(prog.parent_id)
if parent:
parent_island = parent.metadata.get("island")
child_island = prog.metadata.get("island")
self.assertEqual(
parent_island,
child_island,
f"Parent {prog.parent_id} (island {parent_island}) and "
f"child {prog.id} (island {child_island}) should be on same island"
)


def test_explicit_migration_override(self):
"""Test that explicit target_island overrides parent island inheritance"""
config = Config()
config.database.num_islands = 3
database = ProgramDatabase(config.database)

# Create parent on island 0
parent = Program(
id="parent",
code="def parent(): pass",
metrics={"score": 0.5},
iteration_found=0
)
database.add(parent) # Goes to island 0
self.assertIn("parent", database.islands[0])

# Create child but explicitly send to island 2 (migration)
migrant_child = Program(
id="migrant",
code="def migrant(): pass",
parent_id="parent",
metrics={"score": 0.7},
iteration_found=1
)
database.add(migrant_child, target_island=2) # Explicit migration

# Verify migrant went to island 2, not parent's island 0
self.assertIn("migrant", database.islands[2])
self.assertNotIn("migrant", database.islands[0])
self.assertEqual(migrant_child.metadata.get("island"), 2)

# Parent should still be on island 0
self.assertEqual(database.programs["parent"].metadata.get("island"), 0)


if __name__ == "__main__":
unittest.main()
Loading