| (in-package #:nekod.selfmod)
|
|
|
|
|
|
|
|
|
|
|
|
|
| (defun crossover-specs (spec-a spec-b)
|
| "Single-point crossover between two container specs.
|
| Returns two children specs."
|
| (let ((child-a (copy-structure spec-a))
|
| (child-b (copy-structure spec-b)))
|
|
|
| (rotatef (container-spec-restart-policy child-a)
|
| (container-spec-restart-policy child-b))
|
|
|
| (let* ((env-a (container-spec-env spec-a))
|
| (env-b (container-spec-env spec-b))
|
| (mid-a (ceiling (length env-a) 2))
|
| (mid-b (ceiling (length env-b) 2)))
|
| (setf (container-spec-env child-a)
|
| (append (subseq env-a 0 mid-a)
|
| (subseq env-b mid-b)))
|
| (setf (container-spec-env child-b)
|
| (append (subseq env-b 0 mid-b)
|
| (subseq env-a mid-a))))
|
| (values child-a child-b)))
|
|
|
|
|
|
|
| (defun tournament-select (population scores &optional (k 3))
|
| "Tournament selection: pick K random candidates, return the fittest."
|
| (let* ((n (length population))
|
| (candidates (loop repeat k collect (random n)))
|
| (best-idx (first (sort candidates #'>
|
| :key (lambda (i) (nth i scores))))))
|
| (nth best-idx population)))
|
|
|
| (defun roulette-select (population scores)
|
| "Fitness-proportionate (roulette wheel) selection."
|
| (let* ((total (reduce #'+ scores))
|
| (pick (random (max total 0.001)))
|
| (running 0.0))
|
| (loop for spec in population
|
| for score in scores
|
| do (incf running score)
|
| when (>= running pick)
|
| return spec
|
| finally (return (car (last population))))))
|
|
|
|
|
|
|
| (defun population-diversity (population)
|
| "Measure diversity as fraction of specs with unique restart policies."
|
| (let ((policies (mapcar #'container-spec-restart-policy population)))
|
| (/ (length (remove-duplicates policies)) (max (length population) 1))))
|
|
|
|
|
|
|
| (defun evolutionary-step (population telemetry
|
| &key (mutation-rate 0.15)
|
| (crossover-rate 0.6)
|
| (elitism 2))
|
| "Full evolutionary step: selection, crossover, mutation, elitism.
|
| Returns next-generation population of same size."
|
| (let* ((n (length population))
|
| (scores (mapcar (lambda (s) (fitness s telemetry)) population))
|
|
|
| (sorted-pairs (sort (mapcar #'cons population scores) #'> :key #'cdr))
|
| (elites (mapcar #'car (subseq sorted-pairs 0 (min elitism n))))
|
| (next (copy-list elites)))
|
|
|
| (loop while (< (length next) n)
|
| do (let ((parent-a (tournament-select population scores))
|
| (parent-b (tournament-select population scores)))
|
| (if (< (random 1.0) crossover-rate)
|
| (multiple-value-bind (child-a child-b)
|
| (crossover-specs parent-a parent-b)
|
| (push (or (mutate-spec child-a mutation-rate) child-a) next)
|
| (when (< (length next) n)
|
| (push (or (mutate-spec child-b mutation-rate) child-b) next)))
|
| (push (or (mutate-spec parent-a mutation-rate) parent-a) next))))
|
| (subseq next 0 n)))
|
|
|
|
|
|
|
| (defun converged-p (population telemetry &optional (threshold 0.01))
|
| "Check if population has converged (fitness variance < threshold)."
|
| (let* ((scores (mapcar (lambda (s) (fitness s telemetry)) population))
|
| (mean (/ (reduce #'+ scores) (max (length scores) 1)))
|
| (var (/ (reduce #'+ (mapcar (lambda (s) (expt (- s mean) 2)) scores))
|
| (max (length scores) 1))))
|
| (< var threshold)))
|
|
|