|
3 | 3 | """ |
4 | 4 |
|
5 | 5 | import unittest |
| 6 | +import uuid |
6 | 7 | from openevolve.config import Config |
7 | 8 | from openevolve.database import Program, ProgramDatabase |
8 | 9 |
|
@@ -457,6 +458,183 @@ def test_diversity_feature_integration(self): |
457 | 458 | self.assertGreaterEqual(coord, 0) |
458 | 459 | self.assertLess(coord, self.db.feature_bins) |
459 | 460 |
|
| 461 | + def test_migration_prevents_re_migration(self): |
| 462 | + """Test that programs marked as migrants don't migrate again""" |
| 463 | + # Create database with multiple islands |
| 464 | + config = Config() |
| 465 | + config.database.in_memory = True |
| 466 | + config.database.num_islands = 3 |
| 467 | + config.database.migration_interval = 1 # Migrate every generation |
| 468 | + multi_db = ProgramDatabase(config.database) |
| 469 | + |
| 470 | + # Add programs to each island (avoid "migrant" in original IDs) |
| 471 | + for i in range(3): |
| 472 | + program = Program( |
| 473 | + id=f"test_prog_{i}", |
| 474 | + code=f"def test_{i}(): return {i}", |
| 475 | + language="python", |
| 476 | + metrics={"score": 0.5 + i * 0.1}, |
| 477 | + ) |
| 478 | + multi_db.add(program, target_island=i) |
| 479 | + |
| 480 | + # Manually mark one as a migrant |
| 481 | + migrant_program = multi_db.get("test_prog_0") |
| 482 | + migrant_program.metadata["migrant"] = True |
| 483 | + |
| 484 | + # Store original ID |
| 485 | + original_id = migrant_program.id |
| 486 | + |
| 487 | + # Count initial programs with "_migrant_" pattern (created by migration) |
| 488 | + initial_migrant_count = sum(1 for pid in multi_db.programs if "_migrant_" in pid) |
| 489 | + self.assertEqual(initial_migrant_count, 0) # Should be none initially |
| 490 | + |
| 491 | + # Run migration |
| 492 | + multi_db.island_generations[0] = config.database.migration_interval |
| 493 | + multi_db.island_generations[1] = config.database.migration_interval |
| 494 | + multi_db.island_generations[2] = config.database.migration_interval |
| 495 | + multi_db.migrate_programs() |
| 496 | + |
| 497 | + # Check that the migrant program wasn't re-migrated |
| 498 | + # It should still exist with the same ID (not a new migrant ID) |
| 499 | + still_exists = multi_db.get(original_id) |
| 500 | + self.assertIsNotNone(still_exists) |
| 501 | + |
| 502 | + # Count new programs created by migration (identified by "_migrant_" pattern) |
| 503 | + new_migrant_ids = [pid for pid in multi_db.programs if "_migrant_" in pid] |
| 504 | + |
| 505 | + # Each non-migrant program (2 of them) migrates to 2 adjacent islands |
| 506 | + # So we expect 2 * 2 = 4 new migrant programs |
| 507 | + # The already-marked migrant (test_prog_0) should NOT create any new copies |
| 508 | + self.assertEqual(len(new_migrant_ids), 4) |
| 509 | + |
| 510 | + # Verify the already-migrant program didn't create new copies |
| 511 | + migrant_descendants = [pid for pid in new_migrant_ids if original_id in pid] |
| 512 | + self.assertEqual(len(migrant_descendants), 0, |
| 513 | + f"Program {original_id} should not have created migrant copies") |
| 514 | + |
| 515 | + def test_empty_island_initialization_creates_copies(self): |
| 516 | + """Test that empty islands are initialized with copies, not shared references""" |
| 517 | + # Create database with multiple islands |
| 518 | + config = Config() |
| 519 | + config.database.in_memory = True |
| 520 | + config.database.num_islands = 3 |
| 521 | + # Force exploration mode to test empty island handling |
| 522 | + config.database.exploration_ratio = 1.0 |
| 523 | + config.database.exploitation_ratio = 0.0 |
| 524 | + multi_db = ProgramDatabase(config.database) |
| 525 | + |
| 526 | + # Add a single program to island 1 |
| 527 | + program = Program( |
| 528 | + id="original_program", |
| 529 | + code="def original(): return 42", |
| 530 | + language="python", |
| 531 | + metrics={"score": 0.9, "combined_score": 0.9}, |
| 532 | + ) |
| 533 | + multi_db.add(program, target_island=1) |
| 534 | + |
| 535 | + # Make it the best program |
| 536 | + multi_db.best_program_id = "original_program" |
| 537 | + |
| 538 | + # Switch to empty island 0 and sample |
| 539 | + multi_db.set_current_island(0) |
| 540 | + sampled_parent, _ = multi_db.sample() |
| 541 | + |
| 542 | + # The sampled program should be a copy, not the original |
| 543 | + self.assertNotEqual(sampled_parent.id, "original_program") |
| 544 | + self.assertEqual(sampled_parent.code, program.code) # Same code |
| 545 | + self.assertEqual(sampled_parent.parent_id, "original_program") # Parent is the original |
| 546 | + |
| 547 | + # Check island membership |
| 548 | + self.assertIn("original_program", multi_db.islands[1]) |
| 549 | + self.assertNotIn("original_program", multi_db.islands[0]) |
| 550 | + self.assertIn(sampled_parent.id, multi_db.islands[0]) |
| 551 | + |
| 552 | + # Run validation - should not raise any errors |
| 553 | + multi_db._validate_migration_results() |
| 554 | + |
| 555 | + def test_no_program_assigned_to_multiple_islands(self): |
| 556 | + """Test that programs are never assigned to multiple islands""" |
| 557 | + # Create database with multiple islands |
| 558 | + config = Config() |
| 559 | + config.database.in_memory = True |
| 560 | + config.database.num_islands = 4 |
| 561 | + multi_db = ProgramDatabase(config.database) |
| 562 | + |
| 563 | + # Add programs to different islands |
| 564 | + program_ids = [] |
| 565 | + for i in range(4): |
| 566 | + program = Program( |
| 567 | + id=f"island_test_{i}", |
| 568 | + code=f"def test_{i}(): return {i}", |
| 569 | + language="python", |
| 570 | + metrics={"score": 0.5 + i * 0.1, "combined_score": 0.5 + i * 0.1}, |
| 571 | + ) |
| 572 | + multi_db.add(program, target_island=i) |
| 573 | + program_ids.append(program.id) |
| 574 | + |
| 575 | + # Make the best program from island 3 |
| 576 | + multi_db.best_program_id = "island_test_3" |
| 577 | + |
| 578 | + # Sample from empty islands - this should create copies |
| 579 | + for empty_island in range(4): |
| 580 | + if len(multi_db.islands[empty_island]) == 0: |
| 581 | + multi_db.set_current_island(empty_island) |
| 582 | + parent, _ = multi_db.sample() |
| 583 | + |
| 584 | + # Check that no program ID appears in multiple islands |
| 585 | + all_island_programs = {} |
| 586 | + for island_idx, island_programs in enumerate(multi_db.islands): |
| 587 | + for program_id in island_programs: |
| 588 | + if program_id in all_island_programs: |
| 589 | + self.fail( |
| 590 | + f"Program {program_id} found in both island {all_island_programs[program_id]} " |
| 591 | + f"and island {island_idx}" |
| 592 | + ) |
| 593 | + all_island_programs[program_id] = island_idx |
| 594 | + |
| 595 | + # Run validation - should not raise any errors |
| 596 | + multi_db._validate_migration_results() |
| 597 | + |
| 598 | + def test_migration_validation_passes(self): |
| 599 | + """Test that migration validation passes after our fixes""" |
| 600 | + # Create database with multiple islands |
| 601 | + config = Config() |
| 602 | + config.database.in_memory = True |
| 603 | + config.database.num_islands = 3 |
| 604 | + config.database.migration_interval = 1 |
| 605 | + multi_db = ProgramDatabase(config.database) |
| 606 | + |
| 607 | + # Add programs and run several migration cycles |
| 608 | + for i in range(6): |
| 609 | + program = Program( |
| 610 | + id=f"test_program_{i}", |
| 611 | + code=f"def test_{i}(): return {i * 2}", |
| 612 | + language="python", |
| 613 | + metrics={"score": 0.4 + i * 0.1, "combined_score": 0.4 + i * 0.1}, |
| 614 | + ) |
| 615 | + multi_db.add(program, target_island=i % 3) |
| 616 | + |
| 617 | + # Run multiple migration cycles |
| 618 | + for cycle in range(3): |
| 619 | + # Increment generations to trigger migration |
| 620 | + for island in range(3): |
| 621 | + multi_db.island_generations[island] += 1 |
| 622 | + |
| 623 | + # Migrate programs |
| 624 | + multi_db.migrate_programs() |
| 625 | + |
| 626 | + # Validation should pass without warnings |
| 627 | + multi_db._validate_migration_results() |
| 628 | + |
| 629 | + # Verify no program has exponential ID growth |
| 630 | + for program_id in multi_db.programs: |
| 631 | + # Count occurrences of "migrant" in ID |
| 632 | + migrant_count = program_id.count("migrant") |
| 633 | + self.assertLessEqual( |
| 634 | + migrant_count, 1, |
| 635 | + f"Program ID {program_id} has been migrated multiple times" |
| 636 | + ) |
| 637 | + |
460 | 638 |
|
461 | 639 | if __name__ == "__main__": |
462 | 640 | unittest.main() |
0 commit comments