@@ -147,6 +147,12 @@ def _step(self) -> List[Prompt]:
147147 return self .prompts
148148
149149 def _do_intensification (self , challenger : Prompt ) -> None :
150+ if challenger in self .incumbents :
151+ return
152+ if challenger in self .non_incumbents :
153+ # remove from non-incumbents to re-evaluate
154+ self .non_incumbents .remove (challenger )
155+
150156 common_blocks = self ._get_common_blocks (self .incumbents )
151157
152158 # bootstrap if no common blocks yet
@@ -330,8 +336,9 @@ def _select_survivors(self) -> None:
330336 dists = self ._calculate_crowding_distance (worst_front_vecs )
331337
332338 # Find index relative to the worst front list
333- local_worst_idx = int (np .argmin (dists ))
334- # Map back to the main challenger list index
339+ min_dist = np .min (dists )
340+ tied_indices = np .where (dists == min_dist )[0 ]
341+ local_worst_idx = np .random .choice (tied_indices )
335342 victim_idx = worst_front_indices [local_worst_idx ]
336343
337344 self .non_incumbents .pop (victim_idx )
@@ -369,12 +376,52 @@ def _select_parent_from_pool(self, selection_pool: List[Prompt]) -> Prompt:
369376 if p2 in self .incumbents :
370377 return p2
371378
379+ # both are non-incumbents
380+ blocks_map = self .task .get_evaluated_blocks ([p1 , p2 ])
381+ blocks1 = blocks_map .get (str (p1 ), set ())
382+ blocks2 = blocks_map .get (str (p2 ), set ())
383+
384+ if blocks1 == blocks2 : # both evaluated on same blocks
385+ # use NDS + Crowding Distance
386+ self .task .set_block_idx (list (sorted (blocks1 )))
387+ res = self .task .evaluate ([p1 , p2 ], self .predictor )
388+ # check if dominated
389+ vecs = self ._get_objective_vectors (res )
390+ if self ._is_dominated (vecs [0 ], vecs [1 ]):
391+ return p2
392+ if self ._is_dominated (vecs [1 ], vecs [0 ]):
393+ return p1
394+ # tie-breaker: crowding distance
395+ distances = self ._calculate_crowding_distance (vecs )
396+ if distances [0 ] > distances [1 ]:
397+ return p1
398+ if distances [1 ] > distances [0 ]:
399+ return p2
400+
401+ # same crowding distance: random
402+
403+ # use weaker dominance definition
404+ # eval on common blocks only
405+ common_blocks = blocks1 & blocks2
406+ if common_blocks :
407+ self .task .set_block_idx (list (sorted (common_blocks )))
408+ res = self .task .evaluate ([p1 , p2 ], self .predictor )
409+ vecs = self ._get_objective_vectors (res )
410+
411+ if self ._is_weakly_dominated (vecs [0 ], vecs [1 ]):
412+ return p2
413+ if self ._is_weakly_dominated (vecs [1 ], vecs [0 ]):
414+ return p1
415+
372416 return random .choice ((p1 , p2 ))
373417
374418
375419 def _pick_incumbent_by_crowding (self , p1 : Prompt , p2 : Prompt ) -> Prompt :
376420 """Break incumbent ties using crowding distance over common evaluated blocks."""
377- res = self .task .evaluate (self .incumbents , self .predictor , eval_strategy = "evaluated" )
421+ common_blocks = self ._get_common_blocks ([p1 , p2 ])
422+ if common_blocks :
423+ self .task .set_block_idx (common_blocks )
424+ res = self .task .evaluate (self .incumbents , self .predictor )
378425 inc_vectors = self ._get_objective_vectors (res )
379426 inc_distances = self ._calculate_crowding_distance (inc_vectors )
380427
@@ -430,6 +477,11 @@ def _non_dominated_sort(obj_vectors: np.ndarray) -> List[List[int]]:
430477 def _is_dominated (vec1 , vec2 ):
431478 """Returns True if vec2 dominates vec1 in a maximize-all setting."""
432479 return np .all (vec2 >= vec1 ) and np .any (vec2 > vec1 )
480+
481+ @staticmethod
482+ def _is_weakly_dominated (vec1 , vec2 ):
483+ """Returns True if vec2 weakly dominates vec1 in a maximize-all setting."""
484+ return np .all (vec2 >= vec1 )
433485
434486 @staticmethod
435487 def _calculate_crowding_distance (obj_vectors : np .ndarray ) -> np .ndarray :
0 commit comments