@@ -368,87 +368,70 @@ def sample_from_island(
368368 ) -> Tuple [Program , List [Program ]]:
369369 """
370370 Sample a program and inspirations from a specific island without modifying current_island
371-
371+
372372 This method is thread-safe and doesn't modify shared state, avoiding race conditions
373373 when multiple workers sample from different islands concurrently.
374-
374+
375+ Uses the same exploration/exploitation/random strategy as sample() to ensure
376+ consistent behavior between single-process and parallel execution modes.
377+
375378 Args:
376379 island_id: The island to sample from
377380 num_inspirations: Number of inspiration programs to sample (defaults to 5)
378-
381+
379382 Returns:
380383 Tuple of (parent_program, inspiration_programs)
381384 """
382385 # Ensure valid island ID
383386 island_id = island_id % len (self .islands )
384-
387+
385388 # Get programs from the specific island
386389 island_programs = list (self .islands [island_id ])
387-
390+
388391 if not island_programs :
389392 # Island is empty, fall back to sampling from all programs
390393 logger .debug (f"Island { island_id } is empty, sampling from all programs" )
391394 return self .sample (num_inspirations )
392-
393- # Select parent from island programs
394- if len (island_programs ) == 1 :
395- parent_id = island_programs [0 ]
395+
396+ # Use exploration_ratio and exploitation_ratio to decide sampling strategy
397+ # This matches the logic in _sample_parent() for consistent behavior
398+ rand_val = random .random ()
399+
400+ if rand_val < self .config .exploration_ratio :
401+ # EXPLORATION: Sample randomly from island (diverse sampling)
402+ parent = self ._sample_from_island_random (island_id )
403+ sampling_mode = "exploration"
404+ elif rand_val < self .config .exploration_ratio + self .config .exploitation_ratio :
405+ # EXPLOITATION: Sample from archive (elite programs)
406+ parent = self ._sample_from_archive_for_island (island_id )
407+ sampling_mode = "exploitation"
396408 else :
397- # Use weighted sampling based on program scores
398- island_program_objects = [
399- self .programs [pid ] for pid in island_programs
400- if pid in self .programs
401- ]
402-
403- if not island_program_objects :
404- # Fallback if programs not found
405- parent_id = random .choice (island_programs )
406- else :
407- # Calculate weights based on fitness scores
408- weights = []
409- for prog in island_program_objects :
410- fitness = get_fitness_score (prog .metrics , self .config .feature_dimensions )
411- # Add small epsilon to avoid zero weights
412- weights .append (max (fitness , 0.001 ))
413-
414- # Normalize weights
415- total_weight = sum (weights )
416- if total_weight > 0 :
417- weights = [w / total_weight for w in weights ]
418- else :
419- weights = [1.0 / len (island_program_objects )] * len (island_program_objects )
420-
421- # Sample parent based on weights
422- parent = random .choices (island_program_objects , weights = weights , k = 1 )[0 ]
423- parent_id = parent .id
424-
425- parent = self .programs .get (parent_id )
426- if not parent :
427- # Should not happen, but handle gracefully
428- logger .error (f"Parent program { parent_id } not found in database" )
429- return self .sample (num_inspirations )
430-
409+ # WEIGHTED: Use fitness-weighted sampling (remaining probability)
410+ parent = self ._sample_from_island_weighted (island_id )
411+ sampling_mode = "weighted"
412+
431413 # Select inspirations from the same island
432414 if num_inspirations is None :
433415 num_inspirations = 5 # Default for backward compatibility
434-
416+
435417 # Get other programs from the island for inspirations
436- other_programs = [pid for pid in island_programs if pid != parent_id ]
437-
418+ other_programs = [pid for pid in island_programs if pid != parent . id ]
419+
438420 if len (other_programs ) < num_inspirations :
439421 # Not enough programs in island, use what we have
440422 inspiration_ids = other_programs
441423 else :
442424 # Sample inspirations
443425 inspiration_ids = random .sample (other_programs , num_inspirations )
444-
426+
445427 inspirations = [
446- self .programs [pid ] for pid in inspiration_ids
428+ self .programs [pid ] for pid in inspiration_ids
447429 if pid in self .programs
448430 ]
449-
431+
450432 logger .debug (
451- f"Sampled parent { parent .id } and { len (inspirations )} inspirations from island { island_id } "
433+ f"Sampled parent { parent .id } and { len (inspirations )} inspirations from island { island_id } "
434+ f"(mode: { sampling_mode } , rand_val: { rand_val :.3f} )"
452435 )
453436 return parent , inspirations
454437
@@ -1264,6 +1247,132 @@ def _sample_random_parent(self) -> Program:
12641247 program_id = random .choice (list (self .programs .keys ()))
12651248 return self .programs [program_id ]
12661249
1250+ def _sample_from_island_weighted (self , island_id : int ) -> Program :
1251+ """
1252+ Sample a parent from a specific island using fitness-weighted selection
1253+
1254+ Args:
1255+ island_id: The island to sample from
1256+
1257+ Returns:
1258+ Parent program selected using fitness-weighted sampling
1259+ """
1260+ island_id = island_id % len (self .islands )
1261+ island_programs = list (self .islands [island_id ])
1262+
1263+ if not island_programs :
1264+ # Island is empty, fall back to any available program
1265+ logger .debug (f"Island { island_id } is empty, sampling from all programs" )
1266+ return self ._sample_random_parent ()
1267+
1268+ # Select parent from island programs
1269+ if len (island_programs ) == 1 :
1270+ parent_id = island_programs [0 ]
1271+ else :
1272+ # Use weighted sampling based on program scores
1273+ island_program_objects = [
1274+ self .programs [pid ] for pid in island_programs
1275+ if pid in self .programs
1276+ ]
1277+
1278+ if not island_program_objects :
1279+ # Fallback if programs not found
1280+ parent_id = random .choice (island_programs )
1281+ else :
1282+ # Calculate weights based on fitness scores
1283+ weights = []
1284+ for prog in island_program_objects :
1285+ fitness = get_fitness_score (prog .metrics , self .config .feature_dimensions )
1286+ # Add small epsilon to avoid zero weights
1287+ weights .append (max (fitness , 0.001 ))
1288+
1289+ # Normalize weights
1290+ total_weight = sum (weights )
1291+ if total_weight > 0 :
1292+ weights = [w / total_weight for w in weights ]
1293+ else :
1294+ weights = [1.0 / len (island_program_objects )] * len (island_program_objects )
1295+
1296+ # Sample parent based on weights
1297+ parent = random .choices (island_program_objects , weights = weights , k = 1 )[0 ]
1298+ parent_id = parent .id
1299+
1300+ parent = self .programs .get (parent_id )
1301+ if not parent :
1302+ # Should not happen, but handle gracefully
1303+ logger .error (f"Parent program { parent_id } not found in database" )
1304+ return self ._sample_random_parent ()
1305+
1306+ return parent
1307+
1308+ def _sample_from_island_random (self , island_id : int ) -> Program :
1309+ """
1310+ Sample a completely random parent from a specific island (uniform distribution)
1311+
1312+ Args:
1313+ island_id: The island to sample from
1314+
1315+ Returns:
1316+ Parent program selected uniformly at random
1317+ """
1318+ island_id = island_id % len (self .islands )
1319+ island_programs = list (self .islands [island_id ])
1320+
1321+ if not island_programs :
1322+ # Island is empty, fall back to any available program
1323+ logger .debug (f"Island { island_id } is empty, sampling from all programs" )
1324+ return self ._sample_random_parent ()
1325+
1326+ # Clean up stale references
1327+ valid_programs = [pid for pid in island_programs if pid in self .programs ]
1328+
1329+ if not valid_programs :
1330+ logger .warning (f"Island { island_id } has no valid programs, falling back to random sampling" )
1331+ return self ._sample_random_parent ()
1332+
1333+ # Uniform random selection
1334+ parent_id = random .choice (valid_programs )
1335+ return self .programs [parent_id ]
1336+
1337+ def _sample_from_archive_for_island (self , island_id : int ) -> Program :
1338+ """
1339+ Sample a parent from the archive, preferring programs from the specified island
1340+
1341+ Args:
1342+ island_id: The island to prefer programs from
1343+
1344+ Returns:
1345+ Parent program from archive (preferably from the specified island)
1346+ """
1347+ if not self .archive :
1348+ # Fallback to weighted sampling from island
1349+ logger .debug (f"Archive is empty, falling back to weighted island sampling" )
1350+ return self ._sample_from_island_weighted (island_id )
1351+
1352+ # Clean up stale references in archive
1353+ valid_archive = [pid for pid in self .archive if pid in self .programs ]
1354+
1355+ if not valid_archive :
1356+ logger .warning ("Archive has no valid programs, falling back to weighted island sampling" )
1357+ return self ._sample_from_island_weighted (island_id )
1358+
1359+ island_id = island_id % len (self .islands )
1360+
1361+ # Prefer programs from the specified island in archive
1362+ archive_programs_in_island = [
1363+ pid
1364+ for pid in valid_archive
1365+ if self .programs [pid ].metadata .get ("island" ) == island_id
1366+ ]
1367+
1368+ if archive_programs_in_island :
1369+ parent_id = random .choice (archive_programs_in_island )
1370+ return self .programs [parent_id ]
1371+ else :
1372+ # Fall back to any valid archive program if island has none
1373+ parent_id = random .choice (valid_archive )
1374+ return self .programs [parent_id ]
1375+
12671376 def _sample_inspirations (self , parent : Program , n : int = 5 ) -> List [Program ]:
12681377 """
12691378 Sample inspiration programs for the next evolution step.
0 commit comments