diff --git a/tools/push3-evolution/evolve.sh b/tools/push3-evolution/evolve.sh index 47d9f1a..20a23db 100755 --- a/tools/push3-evolution/evolve.sh +++ b/tools/push3-evolution/evolve.sh @@ -10,6 +10,7 @@ # --population 10 \ # --generations 5 \ # --mutation-rate 2 \ +# --elites 2 \ # --output evolved/ # # Algorithm: @@ -18,7 +19,8 @@ # a. Score all candidates via fitness.sh # b. Log generation stats (min/max/mean fitness, best candidate) # c. Select k survivors via tournament selection (k = population/2) -# d. Generate next population: mutate survivors + crossover pairs +# d. Elitism: copy top N candidates unchanged into next generation +# e. Generate next population: mutate survivors + crossover pairs # 3. Output best candidate as Push3 file. # 4. Show diff: original vs evolved (which constants changed, by how much). # @@ -61,6 +63,7 @@ SEED="" POPULATION=10 GENERATIONS=5 MUTATION_RATE=2 +ELITES=2 OUTPUT_DIR="" while [[ $# -gt 0 ]]; do @@ -69,6 +72,7 @@ while [[ $# -gt 0 ]]; do --population) POPULATION="$2"; shift 2 ;; --generations) GENERATIONS="$2"; shift 2 ;; --mutation-rate) MUTATION_RATE="$2"; shift 2 ;; + --elites) ELITES="$2"; shift 2 ;; --output) OUTPUT_DIR="$2"; shift 2 ;; *) echo "Unknown option: $1" >&2; exit 2 ;; esac @@ -88,6 +92,11 @@ for _name_val in "population:$POPULATION" "generations:$GENERATIONS" "mutation-r fi done +if ! [[ "$ELITES" =~ ^[0-9]+$ ]]; then + echo "Error: --elites must be a non-negative integer (got: $ELITES)" >&2 + exit 2 +fi + # Canonicalize paths SEED="$(cd "$(dirname "$SEED")" && pwd)/$(basename "$SEED")" mkdir -p "$OUTPUT_DIR" @@ -142,6 +151,25 @@ print(min(nums), max(nums), round(sum(nums) / len(nums))) PYEOF } +# Top-N selection: return filepaths of the N highest-scoring candidates (descending). +py_top_n() { + local n="$1" + local scores_file="$2" + python3 - "$n" "$scores_file" <<'PYEOF' +import sys +n = int(sys.argv[1]) +entries = [] +with open(sys.argv[2]) as f: + for line in f: + parts = line.rstrip('\n').split('\t') + if len(parts) >= 3: + entries.append((int(parts[1]), parts[2])) +entries.sort(key=lambda x: x[0], reverse=True) +for _, path in entries[:n]: + print(path) +PYEOF +} + # Tournament selection: given a scores file (one "idx score filepath" per line), # run k tournaments of size 2 and return winner filepaths (one per line). py_tournament() { @@ -208,6 +236,7 @@ log " Seed: $SEED" log " Population: $POPULATION" log " Generations: $GENERATIONS" log " Mutation rate: $MUTATION_RATE" +log " Elites: $ELITES" log " Output: $OUTPUT_DIR" log " TSX: $TSX_CMD" log " Eval mode: $EVAL_MODE" @@ -388,15 +417,37 @@ PYEOF log " Selected ${#SURVIVOR_FILES[@]} survivors via tournament" - # --- d. Generate next population --- + # --- d/e. Generate next population (elitism + offspring) --- NEXT_GEN_DIR="$WORK_DIR/gen_$((gen + 1))" mkdir -p "$NEXT_GEN_DIR" NEXT_IDX=0 - HALF=$((POPULATION / 2)) - # First half: mutate random survivors + # --- d. Elitism: copy top ELITES candidates unchanged --- + + if [ "$ELITES" -gt 0 ]; then + ELITE_FILES=() + while IFS= read -r ELITE_FILE; do + [ -f "$ELITE_FILE" ] && ELITE_FILES+=("$ELITE_FILE") + done < <(py_top_n "$ELITES" "$SCORES_FILE") + + for ELITE_FILE in "${ELITE_FILES[@]}"; do + DEST="$NEXT_GEN_DIR/candidate_$(printf '%03d' $NEXT_IDX).push3" + cp "$ELITE_FILE" "$DEST" + printf '0\n' > "${DEST%.push3}.ops" + NEXT_IDX=$((NEXT_IDX + 1)) + done + + log " Elitism: carried over ${#ELITE_FILES[@]} top candidate(s) unchanged" + fi + + # --- e. Fill remaining slots with mutation and crossover offspring --- + + NON_ELITE=$((POPULATION - NEXT_IDX)) + HALF=$((NON_ELITE / 2)) + + # First half of remaining: mutate random survivors for _i in $(seq 1 $HALF); do SUR="${SURVIVOR_FILES[$((RANDOM % ${#SURVIVOR_FILES[@]}))]}" DEST="$NEXT_GEN_DIR/candidate_$(printf '%03d' $NEXT_IDX).push3" @@ -411,8 +462,8 @@ PYEOF NEXT_IDX=$((NEXT_IDX + 1)) done - # Second half: crossover random survivor pairs - REMAINING=$((POPULATION - HALF)) + # Second half of remaining: crossover random survivor pairs + REMAINING=$((POPULATION - NEXT_IDX)) for _i in $(seq 1 $REMAINING); do SUR_A="${SURVIVOR_FILES[$((RANDOM % ${#SURVIVOR_FILES[@]}))]}" SUR_B="${SURVIVOR_FILES[$((RANDOM % ${#SURVIVOR_FILES[@]}))]}"