Source code for curate_ipsum.synthesis.population

"""
Population management for the genetic algorithm.

Handles individual selection, replacement, and population-level operations.
The population is the mutable state of the GA loop — it evolves across iterations.
"""

from __future__ import annotations

import logging
import random

from curate_ipsum.synthesis.models import Individual, PatchSource

LOG = logging.getLogger("synthesis.population")


[docs] class Population: """Manages a population of candidate patches for genetic evolution.""" def __init__(self, individuals: list[Individual] | None = None) -> None: self._individuals: list[Individual] = list(individuals or [])
[docs] @classmethod def from_candidates( cls, candidates: list[str], generation: int = 0, source: PatchSource = PatchSource.LLM, ) -> "Population": """Initialize population from raw code strings (e.g., LLM outputs).""" individuals = [] for code in candidates: ind = Individual( code=code, generation=generation, source=source, ) if ind.is_valid(): individuals.append(ind) else: LOG.debug("Discarded syntactically invalid candidate: %.60s...", code) LOG.info( "Initialized population: %d valid / %d total candidates", len(individuals), len(candidates), ) return cls(individuals)
def __len__(self) -> int: return len(self._individuals) def __iter__(self): return iter(self._individuals) @property def individuals(self) -> list[Individual]: return list(self._individuals) @property def best(self) -> Individual | None: """Return the individual with highest fitness, or None if empty.""" if not self._individuals: return None return max(self._individuals, key=lambda ind: ind.fitness) @property def average_fitness(self) -> float: if not self._individuals: return 0.0 return sum(ind.fitness for ind in self._individuals) / len(self._individuals)
[docs] def select_elite(self, n: int) -> list[Individual]: """Select top-n individuals by fitness.""" n = min(n, len(self._individuals)) sorted_pop = sorted(self._individuals, key=lambda ind: ind.fitness, reverse=True) return sorted_pop[:n]
[docs] def tournament_select(self, n: int, k: int = 3) -> list[Individual]: """ Select n individuals via k-tournament selection. For each selection: pick k random individuals, keep the fittest. """ if len(self._individuals) < k: return list(self._individuals) selected: list[Individual] = [] for _ in range(n): competitors = random.sample(self._individuals, min(k, len(self._individuals))) winner = max(competitors, key=lambda ind: ind.fitness) selected.append(winner) return selected
[docs] def add_individual(self, individual: Individual) -> None: self._individuals.append(individual)
[docs] def add_individuals(self, individuals: list[Individual]) -> None: self._individuals.extend(individuals)
[docs] def remove_weakest(self, n: int) -> list[Individual]: """Remove and return the n weakest individuals.""" n = min(n, len(self._individuals)) sorted_pop = sorted(self._individuals, key=lambda ind: ind.fitness) removed = sorted_pop[:n] remaining = sorted_pop[n:] self._individuals = remaining return removed
[docs] def replace_with(self, new_generation: list[Individual]) -> None: """Replace entire population with a new generation.""" self._individuals = list(new_generation)
[docs] def size(self) -> int: return len(self._individuals)