Merge pull request 'fix: feat: Push3 evolution — elitism (top N survive unchanged) (#640)' (#643) from fix/issue-640 into master

This commit is contained in:
johba 2026-03-12 23:58:36 +01:00
commit f1ed0e4fdc

View file

@ -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[@]}))]}"