Skip to content

Commit 1ee7110

Browse files
committed
Implement island-specific best program tracking
Added tracking and updating of best programs per island to support proper island-based evolution. Updated inspiration sampling and top program queries to maintain genetic isolation between islands. Adjusted prompt context in iteration to use island-specific top programs.
1 parent d90d47f commit 1ee7110

File tree

2 files changed

+163
-47
lines changed

2 files changed

+163
-47
lines changed

openevolve/database.py

Lines changed: 157 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ def __init__(self, config: DatabaseConfig):
122122

123123
# Track the absolute best program separately
124124
self.best_program_id: Optional[str] = None
125+
126+
# Track best program per island for proper island-based evolution
127+
self.island_best_programs: List[Optional[str]] = [None] * config.num_islands
125128

126129
# Track the last iteration number (for resuming)
127130
self.last_iteration: int = 0
@@ -205,6 +208,9 @@ def add(
205208

206209
# Update the absolute best program tracking (after population enforcement)
207210
self._update_best_program(program)
211+
212+
# Update island-specific best program tracking
213+
self._update_island_best_program(program, island_idx)
208214

209215
# Save to disk if configured
210216
if self.config.db_path:
@@ -315,31 +321,47 @@ def get_best_program(self, metric: Optional[str] = None) -> Optional[Program]:
315321

316322
return sorted_programs[0] if sorted_programs else None
317323

318-
def get_top_programs(self, n: int = 10, metric: Optional[str] = None) -> List[Program]:
324+
def get_top_programs(self, n: int = 10, metric: Optional[str] = None, island_idx: Optional[int] = None) -> List[Program]:
319325
"""
320326
Get the top N programs based on a metric
321327
322328
Args:
323329
n: Number of programs to return
324330
metric: Metric to use for ranking (uses average if None)
331+
island_idx: If specified, only return programs from this island
325332
326333
Returns:
327334
List of top programs
328335
"""
329336
if not self.programs:
330337
return []
331338

339+
# Get candidate programs
340+
if island_idx is not None:
341+
# Island-specific query
342+
island_programs = [
343+
self.programs[pid] for pid in self.islands[island_idx]
344+
if pid in self.programs
345+
]
346+
candidates = island_programs
347+
else:
348+
# Global query
349+
candidates = list(self.programs.values())
350+
351+
if not candidates:
352+
return []
353+
332354
if metric:
333355
# Sort by specific metric
334356
sorted_programs = sorted(
335-
[p for p in self.programs.values() if metric in p.metrics],
357+
[p for p in candidates if metric in p.metrics],
336358
key=lambda p: p.metrics[metric],
337359
reverse=True,
338360
)
339361
else:
340362
# Sort by average of all numeric metrics
341363
sorted_programs = sorted(
342-
self.programs.values(),
364+
candidates,
343365
key=lambda p: safe_numeric_average(p.metrics),
344366
reverse=True,
345367
)
@@ -379,6 +401,7 @@ def save(self, path: Optional[str] = None, iteration: int = 0) -> None:
379401
"islands": [list(island) for island in self.islands],
380402
"archive": list(self.archive),
381403
"best_program_id": self.best_program_id,
404+
"island_best_programs": self.island_best_programs,
382405
"last_iteration": iteration or self.last_iteration,
383406
"current_island": self.current_island,
384407
"island_generations": self.island_generations,
@@ -412,6 +435,7 @@ def load(self, path: str) -> None:
412435
saved_islands = metadata.get("islands", [])
413436
self.archive = set(metadata.get("archive", []))
414437
self.best_program_id = metadata.get("best_program_id")
438+
self.island_best_programs = metadata.get("island_best_programs", [None] * len(saved_islands))
415439
self.last_iteration = metadata.get("last_iteration", 0)
416440
self.current_island = metadata.get("current_island", 0)
417441
self.island_generations = metadata.get("island_generations", [0] * len(saved_islands))
@@ -440,6 +464,10 @@ def load(self, path: str) -> None:
440464
# Ensure island_generations list has correct length
441465
if len(self.island_generations) != len(self.islands):
442466
self.island_generations = [0] * len(self.islands)
467+
468+
# Ensure island_best_programs list has correct length
469+
if len(self.island_best_programs) != len(self.islands):
470+
self.island_best_programs = [None] * len(self.islands)
443471

444472
logger.info(f"Loaded database with {len(self.programs)} programs from {path}")
445473

@@ -748,6 +776,53 @@ def _update_best_program(self, program: Program) -> None:
748776
else:
749777
logger.info(f"New best program {program.id} replaces {old_id}")
750778

779+
def _update_island_best_program(self, program: Program, island_idx: int) -> None:
780+
"""
781+
Update the best program tracking for a specific island
782+
783+
Args:
784+
program: Program to consider as the new best for the island
785+
island_idx: Island index
786+
"""
787+
# Ensure island_idx is valid
788+
if island_idx >= len(self.island_best_programs):
789+
logger.warning(f"Invalid island index {island_idx}, skipping island best update")
790+
return
791+
792+
# If island doesn't have a best program yet, this becomes the best
793+
current_island_best_id = self.island_best_programs[island_idx]
794+
if current_island_best_id is None:
795+
self.island_best_programs[island_idx] = program.id
796+
logger.debug(f"Set initial best program for island {island_idx} to {program.id}")
797+
return
798+
799+
# Check if current best still exists
800+
if current_island_best_id not in self.programs:
801+
logger.warning(
802+
f"Island {island_idx} best program {current_island_best_id} no longer exists, updating to {program.id}"
803+
)
804+
self.island_best_programs[island_idx] = program.id
805+
return
806+
807+
current_island_best = self.programs[current_island_best_id]
808+
809+
# Update if the new program is better
810+
if self._is_better(program, current_island_best):
811+
old_id = current_island_best_id
812+
self.island_best_programs[island_idx] = program.id
813+
814+
# Log the change
815+
if "combined_score" in program.metrics and "combined_score" in current_island_best.metrics:
816+
old_score = current_island_best.metrics["combined_score"]
817+
new_score = program.metrics["combined_score"]
818+
score_diff = new_score - old_score
819+
logger.debug(
820+
f"Island {island_idx}: New best program {program.id} replaces {old_id} "
821+
f"(combined_score: {old_score:.4f}{new_score:.4f}, +{score_diff:.4f})"
822+
)
823+
else:
824+
logger.debug(f"Island {island_idx}: New best program {program.id} replaces {old_id}")
825+
751826
def _sample_parent(self) -> Program:
752827
"""
753828
Sample a parent program from the current island for the next evolution step
@@ -869,91 +944,124 @@ def _sample_random_parent(self) -> Program:
869944

870945
def _sample_inspirations(self, parent: Program, n: int = 5) -> List[Program]:
871946
"""
872-
Sample inspiration programs for the next evolution step
947+
Sample inspiration programs for the next evolution step.
948+
949+
For proper island-based evolution, inspirations are sampled ONLY from the
950+
current island, maintaining genetic isolation between islands.
873951
874952
Args:
875953
parent: Parent program
876954
n: Number of inspirations to sample
877955
878956
Returns:
879-
List of inspiration programs
957+
List of inspiration programs from the current island
880958
"""
881959
inspirations = []
960+
961+
# Get the parent's island (should be current_island)
962+
parent_island = parent.metadata.get("island", self.current_island)
963+
964+
# Get all programs from the current island
965+
island_program_ids = list(self.islands[parent_island])
966+
island_programs = [self.programs[pid] for pid in island_program_ids if pid in self.programs]
967+
968+
if not island_programs:
969+
logger.warning(f"Island {parent_island} has no programs for inspiration sampling")
970+
return []
882971

883-
# Always include the absolute best program if available and different from parent
972+
# Include the island's best program if available and different from parent
973+
island_best_id = self.island_best_programs[parent_island]
884974
if (
885-
self.best_program_id is not None
886-
and self.best_program_id != parent.id
887-
and self.best_program_id in self.programs
975+
island_best_id is not None
976+
and island_best_id != parent.id
977+
and island_best_id in self.programs
888978
):
889-
best_program = self.programs[self.best_program_id]
890-
inspirations.append(best_program)
891-
logger.debug(f"Including best program {self.best_program_id} in inspirations")
892-
elif self.best_program_id is not None and self.best_program_id not in self.programs:
893-
# Clean up stale best program reference
979+
island_best = self.programs[island_best_id]
980+
inspirations.append(island_best)
981+
logger.debug(f"Including island {parent_island} best program {island_best_id} in inspirations")
982+
elif island_best_id is not None and island_best_id not in self.programs:
983+
# Clean up stale island best reference
894984
logger.warning(
895-
f"Best program {self.best_program_id} no longer exists, clearing reference"
985+
f"Island {parent_island} best program {island_best_id} no longer exists, clearing reference"
896986
)
897-
self.best_program_id = None
987+
self.island_best_programs[parent_island] = None
898988

899-
# Add top programs as inspirations
989+
# Add top programs from the island as inspirations
900990
top_n = max(1, int(n * self.config.elite_selection_ratio))
901-
top_programs = self.get_top_programs(n=top_n)
902-
for program in top_programs:
991+
top_island_programs = self.get_top_programs(n=top_n, island_idx=parent_island)
992+
for program in top_island_programs:
903993
if program.id not in [p.id for p in inspirations] and program.id != parent.id:
904994
inspirations.append(program)
905995

906-
# Add diverse programs using config.num_diverse_programs
907-
if len(self.programs) > n and len(inspirations) < n:
908-
# Calculate how many diverse programs to add (up to remaining slots)
996+
# Add diverse programs from within the island
997+
if len(island_programs) > n and len(inspirations) < n:
909998
remaining_slots = n - len(inspirations)
910999

911-
# Sample from different feature cells for diversity
1000+
# Try to sample from different feature cells within the island
9121001
feature_coords = self._calculate_feature_coords(parent)
913-
914-
# Get programs from nearby feature cells
9151002
nearby_programs = []
916-
for _ in range(remaining_slots):
1003+
1004+
# Create a mapping of feature cells to island programs for efficient lookup
1005+
island_feature_map = {}
1006+
for prog_id in island_program_ids:
1007+
if prog_id in self.programs:
1008+
prog = self.programs[prog_id]
1009+
prog_coords = self._calculate_feature_coords(prog)
1010+
cell_key = self._feature_coords_to_key(prog_coords)
1011+
island_feature_map[cell_key] = prog_id
1012+
1013+
# Try to find programs from nearby feature cells within the island
1014+
for _ in range(remaining_slots * 3): # Try more times to find nearby programs
9171015
# Perturb coordinates
9181016
perturbed_coords = [
919-
max(0, min(self.feature_bins - 1, c + random.randint(-1, 1)))
1017+
max(0, min(self.feature_bins - 1, c + random.randint(-2, 2)))
9201018
for c in feature_coords
9211019
]
922-
923-
# Try to get program from this cell
1020+
9241021
cell_key = self._feature_coords_to_key(perturbed_coords)
925-
if cell_key in self.feature_map:
926-
program_id = self.feature_map[cell_key]
927-
# Check if program still exists before adding
1022+
if cell_key in island_feature_map:
1023+
program_id = island_feature_map[cell_key]
9281024
if (
9291025
program_id != parent.id
9301026
and program_id not in [p.id for p in inspirations]
1027+
and program_id not in [p.id for p in nearby_programs]
9311028
and program_id in self.programs
9321029
):
9331030
nearby_programs.append(self.programs[program_id])
934-
elif program_id not in self.programs:
935-
# Clean up stale reference in feature_map
936-
logger.debug(f"Removing stale program {program_id} from feature_map")
937-
del self.feature_map[cell_key]
1031+
if len(nearby_programs) >= remaining_slots:
1032+
break
9381033

939-
# If we need more, add random programs
1034+
# If we still need more, add random programs from the island
9401035
if len(inspirations) + len(nearby_programs) < n:
9411036
remaining = n - len(inspirations) - len(nearby_programs)
942-
all_ids = set(self.programs.keys())
1037+
1038+
# Get available programs from the island
9431039
excluded_ids = (
9441040
{parent.id}
9451041
.union(p.id for p in inspirations)
9461042
.union(p.id for p in nearby_programs)
9471043
)
948-
available_ids = list(all_ids - excluded_ids)
949-
950-
if available_ids:
951-
random_ids = random.sample(available_ids, min(remaining, len(available_ids)))
1044+
available_island_ids = [
1045+
pid for pid in island_program_ids
1046+
if pid not in excluded_ids and pid in self.programs
1047+
]
1048+
1049+
if available_island_ids:
1050+
random_ids = random.sample(
1051+
available_island_ids,
1052+
min(remaining, len(available_island_ids))
1053+
)
9521054
random_programs = [self.programs[pid] for pid in random_ids]
9531055
nearby_programs.extend(random_programs)
9541056

9551057
inspirations.extend(nearby_programs)
9561058

1059+
# Log island isolation info
1060+
logger.debug(
1061+
f"Sampled {len(inspirations)} inspirations from island {parent_island} "
1062+
f"(island has {len(island_programs)} programs total)"
1063+
)
1064+
9571065
return inspirations[:n]
9581066

9591067
def _enforce_population_limit(self, exclude_program_id: Optional[str] = None) -> None:
@@ -1103,6 +1211,9 @@ def migrate_programs(self) -> None:
11031211
# Add to target island
11041212
self.islands[target_island].add(migrant_copy.id)
11051213
self.programs[migrant_copy.id] = migrant_copy
1214+
1215+
# Update island-specific best program if migrant is better
1216+
self._update_island_best_program(migrant_copy, target_island)
11061217

11071218
logger.debug(
11081219
f"Migrated program {migrant.id} from island {i} to island {target_island}"
@@ -1214,10 +1325,13 @@ def log_island_status(self) -> None:
12141325
logger.info("Island Status:")
12151326
for stat in stats:
12161327
current_marker = " *" if stat["is_current"] else " "
1328+
island_idx = stat['island']
1329+
island_best_id = self.island_best_programs[island_idx] if island_idx < len(self.island_best_programs) else None
1330+
best_indicator = f" (best: {island_best_id})" if island_best_id else ""
12171331
logger.info(
12181332
f"{current_marker} Island {stat['island']}: {stat['population_size']} programs, "
12191333
f"best={stat['best_score']:.4f}, avg={stat['average_score']:.4f}, "
1220-
f"diversity={stat['diversity']:.2f}, gen={stat['generation']}"
1334+
f"diversity={stat['diversity']:.2f}, gen={stat['generation']}{best_indicator}"
12211335
)
12221336

12231337
# Artifact storage and retrieval methods

openevolve/iteration.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,18 @@ async def run_iteration_with_shared_db(
5353
# Get artifacts for the parent program if available
5454
parent_artifacts = database.get_artifacts(parent.id)
5555

56-
# Get actual top programs for prompt context (separate from inspirations)
57-
actual_top_programs = database.get_top_programs(5)
56+
# Get island-specific top programs for prompt context (maintain island isolation)
57+
parent_island = parent.metadata.get("island", database.current_island)
58+
island_top_programs = database.get_top_programs(5, island_idx=parent_island)
59+
island_previous_programs = database.get_top_programs(3, island_idx=parent_island)
5860

5961
# Build prompt
6062
prompt = prompt_sampler.build_prompt(
6163
current_program=parent.code,
6264
parent_program=parent.code,
6365
program_metrics=parent.metrics,
64-
previous_programs=[p.to_dict() for p in database.get_top_programs(3)],
65-
top_programs=[p.to_dict() for p in actual_top_programs],
66+
previous_programs=[p.to_dict() for p in island_previous_programs],
67+
top_programs=[p.to_dict() for p in island_top_programs],
6668
inspirations=[p.to_dict() for p in inspirations],
6769
language=config.language,
6870
evolution_round=iteration,

0 commit comments

Comments
 (0)