GEPA
Reflective mutation over execution feedback, with Pareto-frontier candidate selection.
"""GEPA SelectionPolicy component — Pareto-frontier candidate sampling (Algorithm 2 in the paper).
Port of ``ParetoCandidateSelector`` (``gepa/strategies/candidate_selector.py``) and the three
utilities it stands on (``gepa/gepa_utils.py``: ``is_dominated`` → ``remove_dominated_programs`` →
``select_program_candidate_from_pareto_front``), with candidates keyed by Genome id instead of by
position in ``GEPAState.program_candidates``.
The mechanism, unchanged: take the set of candidates that hold at least one frontier key, prune the
ones that are *dominated* (every key they hold is also held by someone else — the "illumination" step
the paper credits to Mouret & Clune 2015), then sample proportionally to how many keys each surviving
candidate holds. A candidate that is best on more of the search's axes is proposed from more often,
and a candidate that is best on exactly one obscure axis is still never starved. This is what keeps
GEPA off the single-lineage hill-climb that greedy ``SelectBestCandidate`` collapses into (paper
Table 3: +12.44% over greedy, +7.33% over beam search).
**No inspirations.** GEPA's reflective mutation shows the LM one candidate and that candidate's own
execution feedback — never a gallery of rival programs. The selection is a parent and nothing else.
"""
from __future__ import annotations
import random
from ...components.selection import SelectionPolicy
from ...records import LoopContext, Selection
def _is_dominated(candidate, others: set, mapping: dict) -> bool:
"""``gepa_utils.is_dominated``: true when every frontier key ``candidate`` holds is also held by
one of ``others`` — i.e. dropping it removes nothing from the frontier."""
for key, front in mapping.items():
if candidate not in front:
continue
if not any(other in front for other in others):
return False
return True
def remove_dominated(mapping: dict, scores: dict) -> dict:
"""``gepa_utils.remove_dominated_programs``: repeatedly drop a dominated candidate until none is
left. Candidates are tried lowest-aggregate-score first, so when two candidates cover each other
the weaker one is the one that goes."""
frequency: dict = {}
for front in mapping.values():
for candidate in front:
frequency[candidate] = frequency.get(candidate, 0) + 1
dominated: set = set()
candidates = sorted(frequency, key=lambda c: scores.get(c, float("-inf")))
found_to_remove = True
while found_to_remove:
found_to_remove = False
for candidate in candidates:
if candidate in dominated:
continue
others = set(candidates) - {candidate} - dominated
if _is_dominated(candidate, others, mapping):
dominated.add(candidate)
found_to_remove = True
break
survivors = {c for c in candidates if c not in dominated}
return {key: {c for c in front if c in survivors} for key, front in mapping.items()}
def select_from_pareto_front(mapping: dict, scores: dict, rng: random.Random):
"""``gepa_utils.select_program_candidate_from_pareto_front``: prune the dominated candidates,
then draw one with probability proportional to the number of frontier keys it holds.
Returns ``None`` when no candidate holds a key. Upstream asserts a non-empty sampling list here
and leaves the fallback commented out with a "TODO: Determine if we need this fallback"; a
galapagos run must not die on it, so the caller falls back to the best aggregate score — the
exact branch upstream sketched.
"""
pruned = remove_dominated(mapping, scores)
frequency: dict = {}
for front in pruned.values():
for candidate in front:
frequency[candidate] = frequency.get(candidate, 0) + 1
sampling_list = [candidate for candidate, freq in frequency.items() for _ in range(freq)]
if not sampling_list:
return None
return rng.choice(sampling_list)
class ParetoCandidateSelector(SelectionPolicy):
"""Sample the parent from the pruned Pareto frontier of the candidate pool.
Reads the frontier the :class:`~galapagos.scaffolds.gepa.population.GepaCandidatePool` maintains
(``frontier_mapping``) and its aggregate ranking (``aggregate_score``), the same two reads the
upstream selector makes against ``GEPAState``. All randomness flows through ``self.rng``, which
the base class seeds and checkpoints, so a resumed run keeps sampling where it left off.
"""
def select(self, population, ctx: LoopContext | None = None) -> Selection:
members = population.all()
if not members:
raise RuntimeError("cannot select from an empty population")
scores = {genome.id: population.aggregate_score(genome) for genome in members}
mapping = population.frontier_mapping()
chosen = select_from_pareto_front(mapping, scores, self.rng)
mode = "pareto_frequency"
if chosen is None or chosen not in scores:
chosen = max(scores, key=lambda gid: scores[gid])
mode = "aggregate_best"
parent = next(genome for genome in members if genome.id == chosen)
frontier_candidates = {c for front in mapping.values() for c in front}
return Selection(
parent=parent,
inspirations=[], # reflective mutation reflects on ONE candidate's own feedback
pool=members,
details={
"selection_strategy": "pareto_frontier",
"selection_mode": mode,
"frontier_type": population.frontier_type,
"frontier_key_count": len(mapping),
"frontier_candidate_count": len(frontier_candidates),
"parent_frontier_keys": sum(1 for front in mapping.values() if parent.id in front),
},
)