From ab15eb4fdbe71312b65bdd070451fec2455f1910 Mon Sep 17 00:00:00 2001 From: Asankhaya Sharma Date: Wed, 4 Feb 2026 11:44:05 +0800 Subject: [PATCH 1/2] Create test_island_child_placement.py --- tests/test_island_child_placement.py | 242 +++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 tests/test_island_child_placement.py diff --git a/tests/test_island_child_placement.py b/tests/test_island_child_placement.py new file mode 100644 index 000000000..170ccc7e5 --- /dev/null +++ b/tests/test_island_child_placement.py @@ -0,0 +1,242 @@ +""" +Tests for verifying child programs are placed in the correct target island. + +This test specifically catches the bug where children inherit their parent's island +instead of being placed in the target island that was requested for the iteration. +""" + +import unittest + +from openevolve.config import Config, DatabaseConfig +from openevolve.database import ProgramDatabase, Program + + +class TestIslandChildPlacement(unittest.TestCase): + """Test that child programs are placed in the correct island""" + + def setUp(self): + """Set up test database with multiple islands""" + config = Config() + config.database.num_islands = 3 + config.database.population_size = 100 + self.db = ProgramDatabase(config.database) + + def test_child_inherits_parent_island_when_no_target_specified(self): + """Test that child inherits parent's island when no target_island is given""" + # Add parent to island 0 + parent = Program( + id="parent_0", + code="def parent(): pass", + generation=0, + metrics={"combined_score": 0.5}, + ) + self.db.add(parent, target_island=0) + + # Add child without specifying target_island + child = Program( + id="child_0", + code="def child(): pass", + generation=1, + parent_id="parent_0", + metrics={"combined_score": 0.6}, + ) + self.db.add(child) # No target_island specified + + # Child should inherit parent's island (island 0) + self.assertEqual(child.metadata.get("island"), 0) + self.assertIn("child_0", self.db.islands[0]) + + def test_child_placed_in_target_island_when_specified(self): + """Test that child is placed in target_island when explicitly specified""" + # Add parent to island 0 + parent = Program( + id="parent_1", + code="def parent(): pass", + generation=0, + metrics={"combined_score": 0.5}, + ) + self.db.add(parent, target_island=0) + + # Add child with explicit target_island=2 + child = Program( + id="child_1", + code="def child(): pass", + generation=1, + parent_id="parent_1", + metrics={"combined_score": 0.6}, + ) + self.db.add(child, target_island=2) + + # Child should be in island 2, NOT island 0 + self.assertEqual(child.metadata.get("island"), 2) + self.assertIn("child_1", self.db.islands[2]) + self.assertNotIn("child_1", self.db.islands[0]) + + +class TestEmptyIslandChildPlacement(unittest.TestCase): + """ + Test the critical bug: when sampling from an empty island falls back to + another island's parent, the child should still go to the TARGET island. + """ + + def setUp(self): + """Set up test database with programs only in island 0""" + config = Config() + config.database.num_islands = 3 + config.database.population_size = 100 + self.db = ProgramDatabase(config.database) + + # Add programs ONLY to island 0 + for i in range(5): + program = Program( + id=f"island0_prog_{i}", + code=f"def func_{i}(): pass", + generation=0, + metrics={"combined_score": 0.5 + i * 0.1}, + ) + self.db.add(program, target_island=0) + + # Verify setup: island 0 has programs, islands 1 and 2 are empty + self.assertGreater(len(self.db.islands[0]), 0) + self.assertEqual(len(self.db.islands[1]), 0) + self.assertEqual(len(self.db.islands[2]), 0) + + def test_sample_from_empty_island_returns_fallback_parent(self): + """Test that sampling from empty island falls back to available programs""" + # Sample from empty island 1 + parent, inspirations = self.db.sample_from_island(island_id=1) + + # Should return a parent (from island 0 via fallback) + self.assertIsNotNone(parent) + # Parent is from island 0 + self.assertEqual(parent.metadata.get("island"), 0) + + def test_child_should_go_to_target_island_not_parent_island(self): + """ + CRITICAL TEST: This tests the bug reported in issue #391. + + When we want to add a child to island 1 (empty), but the parent came + from island 0 (via fallback sampling), the child should still be + placed in island 1 (the TARGET), not island 0 (the parent's island). + """ + # Sample from empty island 1 - will fall back to island 0 + parent, inspirations = self.db.sample_from_island(island_id=1) + + # Parent is from island 0 (the only island with programs) + self.assertEqual(parent.metadata.get("island"), 0) + + # Create a child program + child = Program( + id="child_for_island_1", + code="def evolved(): pass", + generation=1, + parent_id=parent.id, + metrics={"combined_score": 0.8}, + ) + + # THIS IS THE BUG: If we don't pass target_island, child goes to parent's island + # Simulating current behavior (without fix): + self.db.add(child) # No target_island - inherits parent's island + + # BUG: Child ends up in island 0 (parent's island) instead of island 1 (target) + # This assertion will FAIL with current code, demonstrating the bug + self.assertEqual( + child.metadata.get("island"), 1, + "Child should be in target island 1, not parent's island 0. " + "This is the bug from issue #391!" + ) + + def test_explicit_target_island_overrides_parent_inheritance(self): + """Test that explicit target_island works even with fallback parent""" + # Sample from empty island 2 - will fall back to island 0 + parent, inspirations = self.db.sample_from_island(island_id=2) + + # Parent is from island 0 + self.assertEqual(parent.metadata.get("island"), 0) + + # Create child and explicitly specify target island + child = Program( + id="child_for_island_2", + code="def evolved(): pass", + generation=1, + parent_id=parent.id, + metrics={"combined_score": 0.8}, + ) + + # With explicit target_island, child should go to island 2 + self.db.add(child, target_island=2) + + # This should work - explicit target_island is respected + self.assertEqual(child.metadata.get("island"), 2) + self.assertIn("child_for_island_2", self.db.islands[2]) + + +class TestIslandPopulationGrowth(unittest.TestCase): + """ + Test that simulates multiple evolution iterations and checks + that all islands eventually get populated. + """ + + def setUp(self): + """Set up test database""" + config = Config() + config.database.num_islands = 3 + config.database.population_size = 100 + self.db = ProgramDatabase(config.database) + + def test_islands_should_all_get_populated(self): + """ + Simulate evolution and verify all islands get programs. + + This test demonstrates the feedback loop bug: if children always + inherit parent's island, and we start with only island 0 populated, + all new programs will go to island 0. + """ + # Start with initial program in island 0 only + initial = Program( + id="initial", + code="def initial(): pass", + generation=0, + metrics={"combined_score": 0.5}, + ) + self.db.add(initial, target_island=0) + + # Simulate 9 iterations, targeting islands in round-robin fashion + for i in range(9): + target_island = i % 3 # 0, 1, 2, 0, 1, 2, 0, 1, 2 + + # Sample from target island (may fall back if empty) + parent, _ = self.db.sample_from_island(island_id=target_island) + + # Create child + child = Program( + id=f"child_{i}", + code=f"def child_{i}(): pass", + generation=1, + parent_id=parent.id, + metrics={"combined_score": 0.5 + i * 0.05}, + ) + + # BUG: Current code doesn't pass target_island, so child inherits parent's island + # This simulates the current buggy behavior: + self.db.add(child) # No target_island + + # Check island populations + island_sizes = [len(self.db.islands[i]) for i in range(3)] + + # With the bug, ALL programs end up in island 0 + # This assertion will FAIL, demonstrating the bug + self.assertGreater( + island_sizes[1], 0, + f"Island 1 should have programs but has {island_sizes[1]}. " + f"All islands: {island_sizes}. Bug: all programs went to island 0!" + ) + self.assertGreater( + island_sizes[2], 0, + f"Island 2 should have programs but has {island_sizes[2]}. " + f"All islands: {island_sizes}. Bug: all programs went to island 0!" + ) + + +if __name__ == "__main__": + unittest.main() From 17b0c07a578d886656e8ab0a97c9a9eec6d1e628 Mon Sep 17 00:00:00 2001 From: Asankhaya Sharma Date: Wed, 4 Feb 2026 11:51:20 +0800 Subject: [PATCH 2/2] Pass target_island to database.add Fix issue #391 by ensuring children are placed in the intended target island instead of inheriting the parent's island. Add target_island to SerializableResult, capture sampling_island in the worker, and pass result.target_island into database.add when inserting child programs. Update tests to reflect the fixed behavior (children go to the target island) and add regression tests that demonstrate the old buggy behavior when target_island is not provided and the correct behavior when it is. --- openevolve/process_parallel.py | 16 +++- tests/test_island_child_placement.py | 125 +++++++++++++++++++++++---- 2 files changed, 119 insertions(+), 22 deletions(-) diff --git a/openevolve/process_parallel.py b/openevolve/process_parallel.py index 2d65b6ce4..9dfeda7a3 100644 --- a/openevolve/process_parallel.py +++ b/openevolve/process_parallel.py @@ -33,6 +33,7 @@ class SerializableResult: artifacts: Optional[Dict[str, Any]] = None iteration: int = 0 error: Optional[str] = None + target_island: Optional[int] = None # Island where child should be placed def _worker_init(config_dict: dict, evaluation_file: str, parent_env: dict = None) -> None: @@ -297,6 +298,9 @@ def _run_iteration_worker( iteration_time = time.time() - iteration_start + # Get target island from snapshot (where child should be placed) + target_island = db_snapshot.get("sampling_island") + return SerializableResult( child_program_dict=child_program.to_dict(), parent_id=parent.id, @@ -305,6 +309,7 @@ def _run_iteration_worker( llm_response=llm_response, artifacts=artifacts, iteration=iteration, + target_island=target_island, ) except Exception as e: @@ -539,9 +544,14 @@ async def run_evolution( # Reconstruct program from dict child_program = Program(**result.child_program_dict) - # 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) + # Add to database with explicit target_island to ensure proper island placement + # This fixes issue #391: children should go to the target island, not inherit + # from the parent (which may be from a different island due to fallback sampling) + self.database.add( + child_program, + iteration=completed_iteration, + target_island=result.target_island, + ) # Store artifacts if result.artifacts: diff --git a/tests/test_island_child_placement.py b/tests/test_island_child_placement.py index 170ccc7e5..f911b4ec7 100644 --- a/tests/test_island_child_placement.py +++ b/tests/test_island_child_placement.py @@ -113,14 +113,18 @@ def test_sample_from_empty_island_returns_fallback_parent(self): def test_child_should_go_to_target_island_not_parent_island(self): """ - CRITICAL TEST: This tests the bug reported in issue #391. + CRITICAL TEST: This tests the fix for issue #391. When we want to add a child to island 1 (empty), but the parent came from island 0 (via fallback sampling), the child should still be placed in island 1 (the TARGET), not island 0 (the parent's island). + + The fix: process_parallel.py now passes target_island to database.add() """ + target_island = 1 # We want to add child to island 1 + # Sample from empty island 1 - will fall back to island 0 - parent, inspirations = self.db.sample_from_island(island_id=1) + parent, inspirations = self.db.sample_from_island(island_id=target_island) # Parent is from island 0 (the only island with programs) self.assertEqual(parent.metadata.get("island"), 0) @@ -134,17 +138,15 @@ def test_child_should_go_to_target_island_not_parent_island(self): metrics={"combined_score": 0.8}, ) - # THIS IS THE BUG: If we don't pass target_island, child goes to parent's island - # Simulating current behavior (without fix): - self.db.add(child) # No target_island - inherits parent's island + # FIX: Pass target_island explicitly (this is what process_parallel.py now does) + self.db.add(child, target_island=target_island) - # BUG: Child ends up in island 0 (parent's island) instead of island 1 (target) - # This assertion will FAIL with current code, demonstrating the bug + # Child should be in island 1 (target), not island 0 (parent's island) self.assertEqual( child.metadata.get("island"), 1, - "Child should be in target island 1, not parent's island 0. " - "This is the bug from issue #391!" + "Child should be in target island 1, not parent's island 0." ) + self.assertIn("child_for_island_1", self.db.islands[1]) def test_explicit_target_island_overrides_parent_inheritance(self): """Test that explicit target_island works even with fallback parent""" @@ -188,9 +190,8 @@ def test_islands_should_all_get_populated(self): """ Simulate evolution and verify all islands get programs. - This test demonstrates the feedback loop bug: if children always - inherit parent's island, and we start with only island 0 populated, - all new programs will go to island 0. + With the fix for issue #391, children are placed in the target island + even when the parent came from a different island (via fallback sampling). """ # Start with initial program in island 0 only initial = Program( @@ -217,24 +218,110 @@ def test_islands_should_all_get_populated(self): metrics={"combined_score": 0.5 + i * 0.05}, ) - # BUG: Current code doesn't pass target_island, so child inherits parent's island - # This simulates the current buggy behavior: - self.db.add(child) # No target_island + # FIX: Pass target_island explicitly (this is what process_parallel.py now does) + self.db.add(child, target_island=target_island) # Check island populations island_sizes = [len(self.db.islands[i]) for i in range(3)] - # With the bug, ALL programs end up in island 0 - # This assertion will FAIL, demonstrating the bug + # With the fix, programs should be distributed across all islands self.assertGreater( island_sizes[1], 0, f"Island 1 should have programs but has {island_sizes[1]}. " - f"All islands: {island_sizes}. Bug: all programs went to island 0!" + f"All islands: {island_sizes}." ) self.assertGreater( island_sizes[2], 0, f"Island 2 should have programs but has {island_sizes[2]}. " - f"All islands: {island_sizes}. Bug: all programs went to island 0!" + f"All islands: {island_sizes}." + ) + + # With the fix, all islands should have at least 1 program + # (some programs may be deduplicated, but distribution should happen) + for i, size in enumerate(island_sizes): + self.assertGreaterEqual(size, 1, f"Island {i} should have at least 1 program") + + +class TestRegressionOldBehavior(unittest.TestCase): + """ + Regression tests to ensure we don't revert to the old buggy behavior. + These tests verify that NOT passing target_island causes the bug. + """ + + def setUp(self): + """Set up test database""" + config = Config() + config.database.num_islands = 3 + config.database.population_size = 100 + self.db = ProgramDatabase(config.database) + + def test_without_target_island_child_inherits_parent(self): + """ + Verify that without explicit target_island, child inherits parent's island. + This is the OLD buggy behavior that we need to avoid in process_parallel.py. + """ + # Add parent to island 0 + parent = Program( + id="parent", + code="def parent(): pass", + generation=0, + metrics={"combined_score": 0.5}, + ) + self.db.add(parent, target_island=0) + + # Sample from empty island 2 (will fall back to island 0) + sampled_parent, _ = self.db.sample_from_island(island_id=2) + self.assertEqual(sampled_parent.metadata.get("island"), 0) + + # Create child WITHOUT passing target_island (old buggy behavior) + child = Program( + id="child", + code="def child(): pass", + generation=1, + parent_id=sampled_parent.id, + metrics={"combined_score": 0.6}, + ) + self.db.add(child) # No target_island! + + # Without target_island, child inherits parent's island (0), not target (2) + # This is the BUG - child should be in island 2 but ends up in island 0 + self.assertEqual( + child.metadata.get("island"), 0, + "Without target_island, child incorrectly inherits parent's island" + ) + + def test_with_target_island_child_goes_to_target(self): + """ + Verify that WITH explicit target_island, child goes to target island. + This is the FIXED behavior implemented in process_parallel.py. + """ + # Add parent to island 0 + parent = Program( + id="parent2", + code="def parent(): pass", + generation=0, + metrics={"combined_score": 0.5}, + ) + self.db.add(parent, target_island=0) + + # Sample from empty island 2 (will fall back to island 0) + sampled_parent, _ = self.db.sample_from_island(island_id=2) + self.assertEqual(sampled_parent.metadata.get("island"), 0) + + # Create child WITH target_island (fixed behavior) + child = Program( + id="child2", + code="def child(): pass", + generation=1, + parent_id=sampled_parent.id, + metrics={"combined_score": 0.6}, + ) + self.db.add(child, target_island=2) # Explicit target! + + # With target_island, child goes to island 2 (correct) + self.assertEqual( + child.metadata.get("island"), 2, + "With target_island, child should go to target island" )