Search
Optimisation over a space you can only sample. You say how to make a genome and how to vary one, you say what a genome scored, and a driver spends a budget of evaluations against that pair. Nothing in the drivers knows what a genome is, so it can be a cellular-automaton rule, an L-system, an Fn graph or a design vector. One file holds all five: random sampling, hill climbing, a (µ+λ) evolutionary loop, novelty search and MAP-Elites. The callers are on three other pages: ALife evolves rules and L-systems, Function evolves function graphs, and Simulation's KratosStudy searches a design space. afield.lang.morpho keeps a search of its own. Read PopulationSearch.kt for why random is reported as a control arm, and why the budget is evaluations rather than generations: both decide whether a result means anything.
10 source file(s), 464 public symbol(s), 464 of them carrying KDoc. Everything below is the documentation that lives beside the code — the same text the completion documentation panel shows in the editor. A blank summary means the declaration has no KDoc yet, not that this page dropped it.
ColorRpn.kt
A reverse-Polish language whose programs are pictures: tokens push numbers and colours onto a stack, the program runs once per pixel of a square, and what is left on the stack is that pixel's colour.
ColorRpn is the language, and it is small enough to read in one sitting: four variables for
where the pixel is, twenty-five words, and no way to fail. Compiling a program gives a
ColorRpnProgram, which reports the colour at one point, renders a square at any size on all
cores, or hands back PNG bytes that display shows as an image.
display("main", ColorRpn.compile("x 9 * sin 0.18 + y 0.55 + 0.62 hsl").png(256))
The rest of the family builds a search on top: ColorRpnGenome.kt breeds one program from two,
and ColorRpnEvolution.kt puts a grid of the results in a box for you to pick from. The
language is described word by word on ColorRpn below.
src/jvmMain/kotlin/afield/lang/rpn/ColorRpn.kt — read the source
ColorRpn
The colour expression language of the madp GA demo (ga.html): a reverse-Polish program run once per pixel, whose result is a colour. It is the genome of interactive evolution in the manner of Karl Sims' Genetic Images (see ColorRpnGenome and afield.study.search.EyeEvolution), and this file follows the demo's interpreter exactly, so a program draws the same picture here as in the browser.
| Declaration | Summary |
|---|---|
val operators: List<String> |
The words of the language, lower case, in the demo's order: stack, constructors, setters, binary, unary, special. |
val variables: List<String> |
The four variables. |
fun tokens(source: String?): List<String> |
The tokens of source: whitespace separated, with // and # comments removed. |
fun isWord(token: String): Boolean |
True when token is one of operators or variables (case-insensitively). |
fun number(token: String): Double? |
token as a number, the way JavaScript's Number(token) reads it, or null if it is not a finite one. |
fun format(value: Double): String |
A number as the demo writes it into a program: three decimals below 10, two below 100, one above, trailing zeros dropped. |
fun pretty(tokens: List<String>): String |
tokens laid out as the demo lays out a program: a new line after eight tokens, after 42 characters, or after rgb, hsl, blend or if once a line has five tokens. |
fun compile(source: String?): ColorRpnProgram |
Compile source into a program that can be evaluated per pixel. Never fails. |
fun wrap01(v: Double): Double |
Wrap v into 0..1 the way the demo does (v % 1, lifted when negative); non-finite is 0.5. |
fun clamp01(v: Double): Double |
Clamp v into 0..1; non-finite is 0.5. |
fun safe(v: Double): Double |
v held within ±1e6; non-finite is 0.5. |
fun safeDiv(a: Double, b: Double): Double |
a / b, with b kept at least 1e-6 away from zero (keeping its sign), then safe. |
fun perlin(phase: Double, seed: Int): Double |
1D gradient noise in 0..1 at phase, for one of the per-use seeds. |
fun fbm(phase: Double, lacunarity: Double, seed: Int): Double |
Four octaves of perlin; lacunarity at or below 1 is read as 1.5..4, above it is held to 1.05..6. |
fun luma(r: Double, g: Double, b: Double): Double |
Rec. 709 luma: what a colour counts as where a word wants a number. |
fun hslToRgb(h0: Double, s0: Double, l0: Double, out: DoubleArray): DoubleArray |
HSL to RGB into out: hue wrapped, saturation and lightness clamped. |
fun rgbToHsl(r0: Double, g0: Double, b0: Double, out: DoubleArray): DoubleArray |
RGB (each wrapped into 0..1) to HSL into out. |
ColorRpnProgram
A compiled ColorRpn program.
| Declaration | Summary |
|---|---|
val source: String |
The text it was compiled from. |
val tokens: List<String> |
The tokens it kept, normalised: lower case, numbers as ColorRpn.format writes them. Two programs with the same normalized draw the same picture. |
val unknown: List<String> |
The tokens it skipped, in the order they appeared. An unknown word is dropped, not an error. |
val normalized: String |
tokens joined by single spaces. |
fun color(x: Double, y: Double, r: Double = radius(x, y), a: Double = angle(x, y)): DoubleArray |
The colour at one point, as [r, g, b] in 0..1; r and a as ColorRpn defines them for the point (x, y). |
fun render(size: Int): IntArray |
The picture at size×size, ARGB, row 0 at the top, rendered on all cores. Pixel centres sit at (i + 0.5) / size, held to single precision as the demo's coordinate arrays hold them. |
fun png(size: Int): ByteArray |
render encoded as PNG bytes, which display(...) shows as an image. |
override fun toString(): String |
The program as normalized, which is what the demo's captions print. |
ColorRpnEvolution.kt
The box-facing front of the ColorRpn family: one call puts a breeding grid of generated
pictures in a box's output panel, and clicking two of them breeds the next generation.
colorRpnEvolution is the three other files joined up, with the madp GA demo's choices filled
in: ColorRpn as the genome, ColorRpnGenome as the breeding, EyeEvolution.kt's grid as the
interface, and a toolbar over the breeding knobs that are worth turning while you watch. Call
afield.study.search.evolveByEye directly instead when you want to change any of that.
src/jvmMain/kotlin/afield/lang/rpn/ColorRpnEvolution.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun Box.colorRpnEvolution(parentA: String, parentB: String, breeding: ColorRpnBreeding = ColorRpnBreeding(), resolution: Int = 256, seed: Long? = null, channel: String = "ga"): EyeEvolution<List<String>> |
The madp GA demo in a box: two ColorRpn parents and seven children in a 3×3 grid, bred by ColorRpnGenome; click two tiles and they become the parents of the next generation. |
ColorRpnGenome.kt
The breeding rules for ColorRpn programs: what a child of two programs is, and the eight
mutations that make one.
ColorRpnBreeding is the knobs, with the madp GA demo's values as the defaults: how many
mutations a child gets, the length it is nudged toward, the chance of splicing a slice out of
the other parent, and how often each of the eight mutations is chosen. ColorRpnGenome applies
them, and because it implements GenomeOps the same genome drives both halves of
afield.study.search: the picture grid of EyeEvolution.kt, where you pick the parents, and
the scored drivers of PopulationSearch.kt, where an Objective picks them.
A mutation can produce a program that makes no sense, and nothing rejects it. ColorRpn reads
any token sequence as some picture, which is what lets a search of this space run without a
validity test.
src/jvmMain/kotlin/afield/lang/rpn/ColorRpnGenome.kt — read the source
ColorRpnBreeding
How the GA demo breeds ColorRpn programs. The defaults are the demo's.
| Declaration | Summary |
|---|---|
val mutationRate: Double |
Scales how many mutations a child gets: about mutationRate × targetLength × (0.45..0.85), and at least one with probability mutationRate × 2.5 when that rounds to none. |
val targetLength: Int |
The length a child is nudged toward, give or take two tokens (4..96). |
val seedSplice: Double |
The chance that a child, before mutating, takes a slice of one to three tokens from the other parent. |
val weights: Map<String, Double> |
The relative frequency of each mutation, by the names ColorRpnGenome.mutations uses. |
val nudgeLength: Boolean |
Whether a child is grown or trimmed toward targetLength after mutating; false leaves it as long as its mutations made it. |
ColorRpnGenome
class ColorRpnGenome(val breeding: ColorRpnBreeding = ColorRpnBreeding()) : GenomeOps<List<String>>
ColorRpn programs as a genome: a list of tokens, bred the way the madp GA demo breeds them.
| Declaration | Summary |
|---|---|
override fun random(rng: Random): List<String> |
A randomProgram of ColorRpnBreeding.targetLength tokens. |
override val mutations: List<Variation<List<String>>> |
One Variation per mutation named in ColorRpnBreeding.weights, each applying only that mutation. The weights are not consulted here — evolve picks uniformly among the operators — so they shape crossover's children and not a mutation-only search. |
override fun crossover(a: List<String>, b: List<String>, rng: Random): List<String> |
The tokens of child, dropping the mutation names it also reports. |
override fun signature(g: List<String>): String |
The genome compiled and renormalised, so two token lists that draw the same picture share one evaluation. Tokens the language does not know are dropped by compiling, so they never distinguish two genomes. |
class Child(val tokens: List<String>, val notes: List<String>) |
A child and the names of the mutations that made it (the demo's tile caption, e.g. seed+num+op). |
fun child(parentA: List<String>, parentB: List<String>, rng: Random): Child |
One child of parentA and parentB; see the class description. |
fun children(parentA: List<String>, parentB: List<String>, count: Int, rng: Random): List<Child> |
count children of the two parents, as the demo's grid has seven. |
fun randomProgram(length: Int, rng: Random): List<String> |
A random program of length tokens: a third numbers, a third variables, a third operators. |
fun mutate(tokens: MutableList<String>, donor: List<String>, rng: Random, notes: MutableList<String>) |
Apply one mutation, chosen by ColorRpnBreeding.weights, to tokens in place, borrowing slices from donor. |
StackSLEvolution.kt
The resumable StackSL search: StackSLEvolution holds the island populations, the stage
schedule and the per-origin credit table between generations, and saves them as a
StackSLEvolutionSnapshot.
The settings (StackSLEvolveSettings, StackSLStage), the history records and the
StackSLPopulationArchive that files snapshots on disk live here too.
src/jvmMain/kotlin/afield/lang/stacksl/evolution/StackSLEvolution.kt — read the source
Used in: Evolving a Shader Toward a Picture
Top-level functions
| Declaration | Summary |
|---|---|
fun StackSLProgram.render(side: Int = 160, time: Float = 1.75f): Img |
What this program draws, at side pixels square and render time time: for showImg and montage. |
fun Img.toPngDataUrl(): String |
An Img as a PNG data URL, for a model or an <img>. |
StackSLEvolution
A StackSL search you can stop, look at, steer and resume: evolveStackSLTowards taken apart.
| Declaration | Summary |
|---|---|
val seed: Long |
The one seed every draw, selection and mutation is threaded from; a snapshot carries it. |
val parallel: Boolean |
Split each render's rows across cores. |
val time: Float |
The render time every candidate is drawn at; StackSLCpu.stillTemplate's default. |
var settings: StackSLEvolveSettings |
The current settings. Change them with configure, which validates them first. |
val targetName: String |
The target's name, the same at every resolution. |
var generation: Int |
Generations run so far, over every island. |
var renders: Int |
Programs rendered for fitness so far: the budget. Cache hits, thumbnails and fits are not in it. |
val millis: MutableMap<String, Double> |
Wall time spent in step, fit and the watchmaker, by what spent it. |
var watchmaker: StackSLProposer? |
The language model, when one takes part. null is the blind search. |
val islands: List<List<Individual<StackSLProgram>>> |
Each island's population, best first. |
val ranked: List<Individual<StackSLProgram>> |
Every island's members, best first, programs distinct. |
val started: Boolean |
Whether there is a population yet. Looking at a search that has not started does not start it. |
val best: Individual<StackSLProgram>? |
The best program in any island. |
val history: MutableList<StackSLGenerationRecord> |
One line per generation: the leader's score and where it came from. |
val improvements: MutableList<StackSLImprovement> |
Every time the overall leader improved, with the program, so the lineage can be drawn. |
val notes: MutableList<String> |
What happened besides ordinary generations: stage changes, watchmaker rounds, fits, crossbreeds. |
val credit: MutableMap<String, StackSLOriginCredit> |
Per origin: offered, survived into a population, took the overall lead. |
var usage: StackSLModelUsage |
What the watchmaker has cost so far. |
val genome: StackSLGenome |
The genome the blind operators use: the default vocabulary at StackSLEvolveSettings.maxTokens. |
val stage: StackSLStage |
The stage the next generation runs under. |
val objective: StackSLImageObjective |
The objective of the current StackSLEvolution.stage. |
val target: StackSLTarget |
The current stage's target. |
fun skill(fitness: Double): Double |
fitness rescaled so 0 is a flat fill of the target's mean colour and 1 is exact. |
fun step(generations: Int = 1, maxRenders: Int = Int.MAX_VALUE): StackSLEvolution |
Run generations generations, or fewer if maxRenders is reached first. |
fun runTo(budget: Int): StackSLEvolution |
Run until budget programs have been rendered for fitness. |
fun inject(programs: List<String>, origin: String = "proposed", island: Int = 0): List<StackSLProposalOutcome> |
Offer programs to the population, as origin. Each is checked (StackSLProposals.check), rendered, scored and put into island (0 by default; -1 deals them round the islands), where truncation decides whether it stays. Costs one render each, like any candidate. |
fun fit(program: StackSLProgram, literals: List<Int>? = null, iterations: Int = settings.fitIterations, learningRate: Double = settings.fitLearningRate, origin: String = "fit", loss: StackSLFitLoss? = null): StackSLFitOutcome |
A round of Adam on program's numeric literals, toward the current stage's target, then scored under the search's own metric and offered to the population as origin. |
fun crossbreed(other: StackSLEvolutionSnapshot, count: Int = max(2, settings.mu / 2)): List<StackSLProposalOutcome> |
Bring in other's survivors: its top count programs as they are, and count one-point recombinants of its leaders with this population's. A rewound conversation can breed with the branch it left this way. |
fun askWatchmaker(proposals: Int = settings.proposals, parents: Int = settings.parents): List<StackSLProposalOutcome> |
One watchmaker round now: show the model the target and the top parents, take proposals programs back, and offer them as watchmaker. Waits for the reply. |
fun distinctLeaders(n: Int): List<Individual<StackSLProgram>> |
The best n programs that score differently, for the watchmaker to see. |
fun request(leaders: List<Individual<StackSLProgram>> = distinctLeaders(settings.parents), proposals: Int = settings.proposals): StackSLProposalRequest |
What the watchmaker is shown. Public so a person can read exactly what a model would see. |
fun preview(programs: List<String>): StackSLPreview |
What programs draw, for the watchmaker's second pass: each at 64 px beside the target, with whether the search would refuse it and why. Thumbnails, not fitness renders: not in renders. |
fun contactSheet(top: Int = 6, side: Int = 128): Img |
The target and the top programs that score differently (distinctLeaders), rendered at side, labelled by rank and skill. Only the target before started. |
fun thumbnailSheet(programs: List<StackSLProgram>, side: Int = THUMBNAIL): Img |
What the watchmaker sees: the target and programs at 64 px, labelled T, #1, #2... |
fun layoutOf(program: StackSLProgram, dim: Int = stage.dim): Double |
The held-out check: StackSLLayout.layoutAgreement of program against the target at dim. For reports only; nothing in the search reads it. |
fun summary(): String |
A few lines: where the search is, its leader, and which origins have earned their keep. |
override fun toString(): String |
The search's summary block. |
fun configure(change: (StackSLEvolveSettings) -> StackSLEvolveSettings): StackSLEvolution |
Change the settings between generations. A changed stage re-scores the population. |
fun snapshot(label: String = ""): StackSLEvolutionSnapshot |
Everything needed to put this search back as it is now, as plain data. |
fun restore(snapshot: StackSLEvolutionSnapshot): StackSLEvolution |
Put the search back to snapshot. Programs are re-scored, which costs nothing for one already seen. |
fun reset(): StackSLEvolution |
The search as it would be before its first generation: nothing in it, the same settings. |
StackSLStage
One stage of a schedule: how many generations it lasts and what fitness means during it.
| Declaration | Summary |
|---|---|
val generations: Int |
How many generations it lasts. The schedule's last stage runs on whatever this says. |
val dim: Int |
The square side candidates are rendered at while it runs. A multiple of 4 in 8..256. |
val metric: StackSLMetric |
What "close to the target" means during it. |
val lpipsGrid: Int |
Regions per side scored as lexicase cases under LPIPS: 2 is the quadrants, 3 the ninths, 0 none. Ignored under StackSLMetric.PIXEL, whose cases are its own 4 by 4 grid. |
val key: String |
What identifies the stage's objective and score cache. |
fun lpipsSide(): Int |
The side LPIPS sees: the smallest multiple of dim that fits the network and the grid. |
fun describe(): String |
The stage in words: the metric, the resolution, and the region grid when it has one. |
fun validate() |
Throw if any of the stage's numbers is out of range. |
StackSLEvolveSettings
Everything about how a StackSLEvolution searches. Plain data, so a snapshot carries it.
| Declaration | Summary |
|---|---|
val mu: Int |
Survivors kept per island each generation. |
val lambda: Int |
Children produced per island each generation. |
val islands: Int |
How many populations evolve side by side, exchanging programs through migrateEvery. |
val migrateEvery: Int |
Every this many generations, each island sends its best to the next one; 0 never. |
val migrants: Int |
How many of an island's best travel on a migration. A program the destination already has is not copied. |
val selection: Selection |
How a parent is chosen. Selection.LEXICASE falls back to Selection.TOURNAMENT for any stage whose objective offers no cases, rather than degenerating into a random draw. |
val tournament: Int |
How many candidates a tournament compares. Unused under lexicase selection. |
val crossoverRate: Double |
The chance a child is recombined from two parents rather than mutated from one. |
val immigrantRate: Double |
The chance a child is a fresh random draw instead, which is how an island keeps new words arriving. |
val maxTokens: Int |
Longest program, in words, definitions included; proposals over it are refused, never cut. |
val stages: List<StackSLStage> |
The schedule. The last stage runs for ever, whatever its StackSLStage.generations says. |
val fitEvery: Int |
Every this many generations, a round of Adam on the leader's literals; 0 never. |
val fitIterations: Int |
Adam steps per fitEvery round. |
val fitLearningRate: Double |
Adam's step size over the literals, which are in the program's own units. |
val watchmakerEvery: Int |
Every this many generations, a watchmaker round; 0 never (unless watchmakerOnStall). |
val watchmakerOnStall: Int |
A watchmaker round once the leader has not improved for this many generations; 0 never. |
val watchmakerRevise: Boolean |
Show the watchmaker what its programs draw and take its corrections (a second call per round). |
val watchmakerWait: Boolean |
Wait for the watchmaker's reply before the next generation (else it lands when it lands). |
val proposals: Int |
Programs asked of the watchmaker per round. |
val parents: Int |
Leaders it is shown per round. |
fun stageAt(g: Int): StackSLStage |
The stage generation g runs under. |
fun validate() |
Throw if any of the settings is out of range, each stage included. |
StackSLGenerationRecord
One generation's leader. leaderOrigin is set only in a generation where the leader improved.
| Declaration | Summary |
|---|---|
val generation: Int |
Which generation this is, from 1. |
val renders: Int |
Renders spent by the end of it — the budget axis every trace is read against. |
val stage: String |
The stage it ran under, as StackSLStage.key writes it. |
val bestFitness: Double |
The leader's fitness at the end of it. |
val bestSkill: Double |
The same score in skill units: 0 is a flat fill of the target's mean colour, 1 is exact. |
val leaderOrigin: String? |
What produced the new leader, or null in a generation where the leader did not change. |
StackSLImprovement
A new overall leader.
| Declaration | Summary |
|---|---|
val generation: Int |
The generation it arrived in. |
val renders: Int |
Renders spent when it did, so improvements can be plotted against budget rather than time. |
val fitness: Double |
What it scored. |
val skill: Double |
The same score in skill units; see StackSLGenerationRecord.bestSkill. |
val origin: String |
What produced it: a named mutation, a crossover, a migrant, or a watchmaker round. |
val program: String |
The program itself, so the list of improvements is a readable lineage. |
StackSLFitOutcome
What a StackSLEvolution.fit did: the loss the descent ran on before and after, and the skill under the search's own metric before and after. problem says why there is no fitted program.
| Declaration | Summary |
|---|---|
val program: String |
The program the descent started from. |
val fitted: String? |
The program with its literals moved, or null when there is a problem. |
val lossBefore: Double? |
The descent's own loss before it ran, which is not the search's metric. |
val lossAfter: Double? |
The same loss after. A descent can lower this and still lose skill; both are reported. |
val skillBefore: Double? |
Skill under the search's metric before the descent. |
val skillAfter: Double? |
Skill under the search's metric after it, or null when the fitted program was refused. The fitted program is offered to the population either way: this says whether it was worth it. |
val problem: String? |
Why there is no fitted program, or null when there is one. |
StackSLOriginCredit
data class StackSLOriginCredit(var offered: Int = 0, var survived: Int = 0, var leads: Int = 0)
How one origin of candidates has done.
StackSLSnapshotMember
A member of a saved island.
| Declaration | Summary |
|---|---|
val program: String |
The program as source text. |
val origin: String |
What produced it. |
val fitness: Double |
What it scored under the stage it was saved from. |
val skill: Double |
The same score in skill units; see StackSLGenerationRecord.bestSkill. |
StackSLEvolutionSnapshot
A StackSLEvolution as plain data. StackSLEvolutionSnapshot.write and read are its JSON.
| Declaration | Summary |
|---|---|
val version: Int |
The format version. Reading ignores fields it does not know, so a newer file still loads. |
val label: String |
A name for this saved population, which the archive files it under. |
val target: String |
The target's name, which is how a restored search finds the picture again. |
val seed: Long |
The search's seed, so a restored run continues the same stream rather than a fresh one. |
val generation: Int |
The generation it was saved at. |
val renders: Int |
Renders spent by then. |
val settings: StackSLEvolveSettings |
The settings it was running under, saved whole because they are plain data. |
val islands: List<List<StackSLSnapshotMember>> |
Each island's members, best first. |
val history: List<StackSLGenerationRecord> |
One record per generation. |
val improvements: List<StackSLImprovement> |
Every generation in which the overall leader improved. |
val notes: List<String> |
The run's own log lines, in order. |
val credit: Map<String, StackSLOriginCredit> |
What each origin has produced, survived and led with, keyed by origin name. |
val usage: StackSLModelUsage |
What the watchmaker has cost the run so far. |
val lastRound: List<StackSLProposalOutcome> |
The last watchmaker round's outcomes, which the next round's prompt reports back. |
val savedAt: Long |
When it was written, as epoch milliseconds. |
val best: StackSLSnapshotMember? |
The best saved program and its skill, or null for an empty snapshot. |
fun write(file: File) |
Write the snapshot as JSON, through a temporary file so a reader never sees a half-written one. |
StackSLPopulationArchive
class StackSLPopulationArchive(val directory: File)
Saved populations by id, append-only, in a directory: what StackSLEvolution.crossbreed reads.
| Declaration | Summary |
|---|---|
fun add(snapshot: StackSLEvolutionSnapshot): String |
File snapshot under a new id and return the id. |
fun get(id: String): StackSLEvolutionSnapshot? |
The snapshot filed as id, or null. |
fun list(): List<Pair<String, StackSLEvolutionSnapshot>> |
Every id, oldest first, with its label, generation and best skill. |
StackSLEvolve.kt
Genetic programming over StackSL token sequences, scored by rendering the program and comparing it to a target image.
This is a third shape of consumer for PopulationSearch.kt. The two that were there before it
evolve numbers or rule tables — FunctionEvolve.kt mutates an Fn expression graph and
alife_evolve.kt mutates rule tables, growth windows and grammar bodies — and neither has a
phenotype you can look at. Here the genome is a program in a real language, the phenotype is a
picture, and the thing in between is the production emitter plus afield.lang.stacksl.cpu's
interpreter. Nothing about what a StackSL word means is restated in this file.
What is new here and what is not
Being precise about this, because three of the four ingredients already existed:
- Not new: linear-genome GP with this operator set.
LSystemGenomeinalife_evolve.ktalready haspoint,insert,delete,duplicate,swap, single-point crossover and asignature, over a string of turtle symbols.StackSLGenome's operators are that set, lifted to a token list, and the resemblance is deliberate. - Not new: synthesising a program as text and letting a real compiler judge it.
afield.lang.morpho.MorphoGenomedoes exactly that for MorphoHDL, and already reports its invalid fraction rather than hiding it. It predatesPopulationSearchand runs on a private search core. - New: the phenotype is an image, produced by the same emitter a GPU reads, so fitness is a measurement of a rendered picture rather than of a scalar or a netlist.
- New: a program-text genome on the generic core, so
PopulationSearch.random,PopulationSearch.evolve,PopulationSearch.novelty,PopulationSearch.mapElitesandSelection.LEXICASEare all available to it without any of them being written again.
Arity: there was nothing to repair and nothing to reject
The plan this file was written against asked for a deliberate choice between repairing a token
sequence into a legal stack effect and rejecting it as invalid. Measured against the real
compiler, that is a false dichotomy for a straight-line program: StackSL has no invalid
straight-line programs. StackSLCompileOptions.defaultValues fills any operand the stack does
not have, and it does so without an ERROR diagnostic even at tolerant = false — + on its
own compiles, ok, and leaves the stack at depth 1. The silent-pruning hazard that
finalStackDepth exists to expose belongs to definitions (: name … ;), whose arity has to
work out for the emitted function to be called at all; this generator emits no definitions, so
it cannot hit it.
So StackSLGenome does not repair for validity and does not reject. It carries a model of
each word's stack effect (StackSLWord.consumes) and uses it only to steer generation — the
same relationship MorphoWidthModel has to MorphoHDL's real width inference, where "the compiler
remains the judge; the model's accuracy shows up as failureRate and nowhere else". Here the
model's accuracy shows up as StackSLEvolveRun.danglingValues, which counts values a program
pushed and never consumed, read off the compiler's own analysis.
The gate that actually matters is on the image, not on the program
A program that compiles perfectly and renders a flat grey rectangle is the single most likely way
a search like this produces a fake result: two blanks agree with each other everywhere, and a
plain error metric will happily rank one of them first. StackSLImageObjective therefore scores
a constant-colour render as invalid, inside the fitness function, and refuses a target that is
itself blank.
Beating chance is not resembling the target
The first version of this file scored 1 - mean absolute RGB error, now StackSLMetric.PIXEL.
Evolution beat the random control on four of five targets under it, and the pictures mostly did
not look like their targets: a yellow disc evolved into dark diagonal streaks, four quadrants
into a vertical gradient. An absolute error is satisfied first by matching the target's overall
colour balance, and the search found that rather than the layout. StackSLMetric.LPIPS, the
default, scores with a learned perceptual distance instead (afield.ai.inference.onnx.Lpips).
StackSLLayout.layoutAgreement is the independent check on whether a result resembles its
target, and StackSLEvolveGallery.figureTargets runs both metrics against each other and asserts
on it.
Cost, so the budget is chosen rather than discovered
One 32x32 candidate is about 1,000 pixel-shader invocations through afield.lang.stacksl.cpu's
interpreter, which runs 1.3–1.6 x 10^4 a second per core (dev_docs/STACKSL_CPU_BACKEND.md):
30 ms with parallel = true on ten cores, 70 ms to 130 ms on one, depending on how many fbm
octaves the program has and what else the machine is doing. Compiling and expanding the
template costs 0.35 ms and is noise by comparison. A 600-evaluation search is therefore about
twenty seconds, and the two-arm comparison evolveStackSLTowards runs about forty.
evolveStackSLTowards is the one-call entry point a box wants; see public/docs/stacksl_evolve.md.
src/jvmMain/kotlin/afield/lang/stacksl/evolution/StackSLEvolve.kt — read the source
Used in: Evolving a Shader Toward a Picture
Top-level functions
Top-level values
| Declaration | Summary |
|---|---|
const val DEFAULT_FITNESS_DIM: Int |
Fitness resolution that costs about 30 ms a candidate on ten cores. See the file KDoc. |
StackSLWordKind
What a word does to the stack's depth, in the model this file steers generation with.
Constants: PUSH, UNARY, BINARY, TERNARY.
StackSLWord
class StackSLWord(val token: String, val kind: StackSLWordKind)
One word in the generator's vocabulary, with the stack effect this file believes it has.
| Declaration | Summary |
|---|---|
val consumes: Int |
Values the word takes off the stack, in the model. |
override fun toString(): String |
The word's token, so a palette prints as its vocabulary. |
StackSLProgram
class StackSLProgram(val tokens: List<String>)
A StackSL program as a token list — the genome.
| Declaration | Summary |
|---|---|
val text: String |
The program as source text, one space between tokens. |
val size: Int |
How many tokens; the parsimony axis and one of the MAP-Elites axes. |
override fun toString(): String |
The program as text, which is also what the search memoises on. |
StackSLPalette
The vocabulary a search draws from, and the numeric-literal distribution beside it.
| Declaration | Summary |
|---|---|
val words: List<StackSLWord> |
The words a draw may choose from, each with the arity it is modelled at. One of every kind is required. |
val numbers: List<Double> |
Literal values the number draw picks from before jitter widens them. |
val numberShare: Double |
Probability that a PUSH draw yields a fresh numeric literal rather than a named word. |
fun kindOf(token: String): StackSLWordKind |
The modelled kind of token; a token that parses as a number, or a vector literal such as [0.9,0.1,0.1] (which a proposed program may carry), is a StackSLWordKind.PUSH. |
fun consumes(token: String): Int |
How many values token is modelled to consume. |
fun draw(kind: StackSLWordKind, rng: Random): String |
A fresh token of kind; a PUSH is a numeric literal with probability numberShare. |
fun substitute(token: String, rng: Random): String |
A token of the same modelled kind as token. |
fun drawNumber(rng: Random): String |
A numeric literal, occasionally with a StackSL suffix that turns it into a modulator. |
StackSLGenome
A StackSL program presented to PopulationSearch as a genome.
| Declaration | Summary |
|---|---|
val palette: StackSLPalette |
The words and literals every draw and substitution comes from. |
val minTokens: Int |
Shortest program the generator or delete will produce. |
val maxTokens: Int |
Longest; every operator clamps to it, so bloat has a hard ceiling rather than a parsimony penalty that would have to be weighed against image error in units nobody has. |
val maxRandomTokens: Int |
Longest program random draws; maxTokens still bounds what mutation may grow to. |
override fun random(rng: Random): StackSLProgram |
An independent draw — and therefore exactly what PopulationSearch.random samples. |
override val mutations: List<Variation<StackSLProgram>> |
The six positional operators, by the names a run reports against each improvement: point substitutes one token, insert and delete change the length, duplicate copies a slice, jitter moves a numeric literal, and swap exchanges two positions. Each clamps to minTokens..maxTokens, so none of them can produce a program the search would refuse. |
override fun crossover(a: StackSLProgram, b: StackSLProgram, rng: Random): StackSLProgram |
One-point crossover: a prefix of a and a suffix of b. |
override fun signature(g: StackSLProgram): String |
The program text. See the class KDoc for why this is worth having. |
fun canonical(tokens: List<String>): StackSLProgram |
Clamp to minTokens..maxTokens. This is not a validity repair — see the file KDoc: a straight-line StackSL program cannot be arity-invalid, because the compiler fills missing operands from defaultValues and does not complain. It is a bloat ceiling and nothing else. |
fun modelledDepthAt(tokens: List<String>, index: Int): Int |
The modelled stack depth just before token index, starting from StackSL's own depth of 1. |
RasterStructure
The three numbers that say whether a raster is a picture or a rectangle of one colour.
| Declaration | Summary |
|---|---|
val spread: Double |
The largest per-channel standard deviation over the image, in 0..1 units. A flat fill is exactly 0; the StackSL gallery's stills sit between 0.08 and 0.35. |
val distinctColours: Int |
How many distinct 8-bit RGB triples appear, capped at DISTINCT_CAP so a busy image does not pay for counting them all. |
val spatialFrequency: Double |
Mean absolute luma gradient, scaled into 0..1. Independent of spread in the way that matters: a smooth left-to-right ramp has a large spread and a tiny spatial frequency, and a checkerboard has both. |
val colourfulness: Double |
Mean per-pixel max(rgb) - min(rgb) — how far the image is from grey. |
override fun toString(): String |
All four numbers on one line, which is how a refusal reports what it measured. |
StackSLLayout
Does a picture have its target's layout? The check this file uses on a search's results, kept apart from anything a search optimises.
| Declaration | Summary |
|---|---|
fun layoutAgreement(candidate: CpuRaster, target: CpuRaster): Double |
Would someone looking at candidate see target's layout? Cohen's kappa: 1 exact, 0 no better than chance, negative for the target's negative. |
const val PALETTE_LIMIT: Int |
Most colours a target may have for layoutAgreement to label by nearest colour. |
fun principalAxis(raster: CpuRaster): DoubleArray |
The unit RGB direction of greatest variance over raster, by power iteration. |
StackSLTarget
class StackSLTarget(val name: String, val raster: CpuRaster)
A named target image a search is aimed at.
| Declaration | Summary |
|---|---|
val structure: RasterStructure |
The target's own structure — measured, so a blank target can be refused. |
val flatColour: FloatArray |
The target's mean RGB — the colour of the best possible flat answer. |
fun flatRaster(): CpuRaster |
A raster of nothing but flatColour — the trivial answer, at the same size. |
override fun toString(): String |
The target's name, the raster's size and its RasterStructure. |
StackSLTargets
Five synthetic targets, generated in plain Kotlin.
| Declaration | Summary |
|---|---|
fun spot(dim: Int): StackSLTarget |
A bright warm disc on near-black: radial, one hard edge. |
fun rings(dim: Int): StackSLTarget |
Concentric black-and-white rings: radial and periodic. |
fun bands(dim: Int): StackSLTarget |
Six hard vertical stripes, yellow against deep blue: high frequency in x, none in y. |
fun checker(dim: Int): StackSLTarget |
A 4x4 magenta-and-teal checkerboard: periodic in both axes. |
fun quadrants(dim: Int): StackSLTarget |
Four flat quadrants: piecewise constant, hard edges, four saturated colours. |
fun byName(name: String, dim: Int): StackSLTarget? |
The built-in target called name at dim, or null for a name that is not one of the five. |
fun all(dim: Int): List<StackSLTarget> |
All five at dim square, in the order the contact sheet shows them. |
fun generate(name: String, dim: Int, rgb: (u: Double, v: Double) -> FloatArray): StackSLTarget |
Build a target from a (u, v) -> rgb function. |
StackSLMetric
What StackSLImageObjective counts as "close to the target".
Constants: PIXEL, LPIPS, LPIPS_PIXEL.
StackSLScores
One render's scores under both metrics, so one render can serve several objectives.
| Declaration | Summary |
|---|---|
val valid: Boolean |
false for a render the blank guard refused or a program that threw; every score is then StackSLImageObjective.INVALID_SCORE. |
val pixelFitness: Double |
The whole-picture pixel-error score, higher better. |
val pixelCases: DoubleArray |
One pixel-error case per cell of the objective's grid, for lexicase selection. |
val lpipsFitness: Double |
The whole-picture LPIPS score, higher better. |
val lpipsCases: DoubleArray |
One LPIPS case per region, or empty when the objective was not asked for them (StackSLImageObjective.lpipsRegions). |
val behaviour: DoubleArray |
The behavioural descriptor, which MAP-Elites bins on and novelty search measures against. |
fun fitness(metric: StackSLMetric): Double |
The scalar fitness under metric. |
fun evaluation(metric: StackSLMetric): Evaluation |
The search-facing Evaluation under metric. |
StackSLScoreCache
Program text to StackSLScores, for objectives that share one target.
| Declaration | Summary |
|---|---|
val hits: Int |
Evaluations served from the cache rather than the interpreter. |
val size: Int |
Distinct programs scored. |
StackSLImageObjective
Score a StackSL program by rendering it and comparing it to target.
| Declaration | Summary |
|---|---|
val target: StackSLTarget |
The picture to get close to. A target that is itself blank is refused in init. |
val dim: Int |
The square side each candidate is rendered at; the target's own width by default. |
val time: Float |
The time value every still is rendered at, so an animated program is judged on one frame. |
val caseGrid: Int |
The StackSLMetric.PIXEL lexicase cases: one per cell of this grid. |
val pyramidLevels: Int |
Scales the StackSLMetric.PIXEL error is averaged over; 1 is a plain full-resolution mean. |
val parallel: Boolean |
false renders each candidate on one thread — see evolveStackSLTowards for when to. |
val metric: StackSLMetric |
What "close to the target" means. See StackSLMetric. |
val cache: StackSLScoreCache? |
Scores already computed for this target, keyed by program text, shared between objectives. |
val supersample: Int |
Render each candidate at this multiple of dim and average down: 1 (no supersampling) or a power of two. Without it a program can score by aliasing: stripes finer than a fitness pixel, sampled at pixel centres, can land on a pattern that resembles the target at 32 pixels and nothing at 128. Costs supersample^2 times the render. |
val lpipsSide: Int |
The side LPIPS sees. A render smaller than this is enlarged pixel by pixel (and the target with it) before comparison, because AlexNet's deeper stages are 1x1 at 32 pixels. Enlarging adds no detail, it gives the network room to look at the detail the render has. |
val lpipsRegions: Boolean |
Also score LPIPS on each region of an lpipsGrid by lpipsGrid tiling of the lpipsSide image, as lexicase cases. That is lpipsGrid^2 more (smaller) LPIPS evaluations per candidate, so off unless you select by lexicase. |
val lpipsGrid: Int |
Regions per side when lpipsRegions is on: 2 is the four quadrants. Each region must be at least Lpips.MIN_SIDE square, so 3 needs an lpipsSide of 96. More regions pin where and how many more tightly; bands evolving the right stripes out of phase is what they are for. |
val flatFloor: Double |
What a flat fill of the target's own mean colour would score — the trivial answer's fitness. |
fun skill(fitness: Double): Double |
A fitness rescaled so 0 is the flat answer and 1 is exact. |
var considered: Int |
Candidates offered so far, including ones this objective rejected. |
var invalid: Int |
Candidates rejected as blank or as a crash. The failureRate of MorphoSearchRun, here. |
val invalidRate: Double |
Fraction of candidates that were not pictures. Reported, never hidden. |
override fun evaluate(genome: StackSLProgram): Evaluation |
Render genome once, score it under both metrics and return the one metric asks for. A render that throws, or that the blank guard refuses, scores INVALID_SCORE on every case rather than propagating, and is counted in invalid. A cache hit skips the render. |
fun render(genome: StackSLProgram, dim: Int = this.dim): CpuRaster |
Render genome at dim — the same call evaluate makes, for a caller that wants the pixels. |
fun renderForFitness(genome: StackSLProgram): CpuRaster |
What evaluate scores: genome at dim, rendered at dim * supersample and box-filtered down, so detail finer than a fitness pixel averages out instead of aliasing. |
fun scoreAt(genome: StackSLProgram, other: StackSLTarget): Double |
Fitness of a program against a target at that target's resolution, under the same metric. |
fun scores(raster: CpuRaster): StackSLScores |
Both metrics' scores for a raster already rendered at dim. |
StackSLEvolveRun
One search, its control arm, and everything needed to say whether the result is real.
| Declaration | Summary |
|---|---|
val target: StackSLTarget |
The picture both arms were aimed at. |
val evolved: SearchRun<StackSLProgram> |
The search arm. |
val control: SearchRun<StackSLProgram> |
The control arm: the same genome and budget, drawn at random rather than selected. |
val invalidRate: Double |
The evolved arm's StackSLImageObjective.invalidRate: candidates that were not pictures. |
val danglingValues: Int |
Values the best program pushed and never consumed, off the compiler's own StackSLAnalysis. The arity model's error bar, in the only units it has. |
val objective: StackSLImageObjective |
The objective the evolved arm ran against — kept so skill and re-rendering are available. |
val best: StackSLProgram? |
The evolved program, or null if every candidate was rejected. |
val margin: Double |
Evolved fitness minus control fitness at equal budget. The number a claim is made of. |
val evolvedSkill: Double |
Evolved fitness in StackSLImageObjective.skill units: 0 is a flat fill, 1 is exact. |
val controlSkill: Double |
The control's fitness in the same units. |
val skillRatio: Double |
How many times further above the trivial flat answer evolve got than random did. |
val evolvedLayout: Double by lazy |
StackSLLayout.layoutAgreement of the evolved program against the target, at the search's own resolution: does it put light and dark where the target does? Not what either arm optimised, which is why it is worth reporting beside their fitness. |
val controlLayout: Double by lazy |
The control's evolvedLayout. |
fun contactSheet(side: Int = 160, also: List<Pair<String, StackSLProgram>> = emptyList()): Img |
Target, evolved best and the control's best side by side at side pixels, for showImg. |
fun skillOf(program: StackSLProgram): Double |
program's skill under this run's own objective, the units evolvedSkill is in. |
fun summary(): String |
One aligned line comparing the two arms: both fitnesses and skills, the margin between them, the layout check, the invalid rate, the dangling values and the time. |
override fun toString(): String |
The run's summary line. |
StackSLEvolveStudio.kt
The AI4 chat surface for a StackSLEvolution: stackSLEvolveStudio registers the evolve_*
tools on a box and ties the population to the conversation's branches.
stackSLEvolution keeps one search on the box, so re-running the box keeps its population.
src/jvmMain/kotlin/afield/lang/stacksl/evolution/StackSLEvolveStudio.kt — read the source
Used in: Evolving a Shader Toward a Picture
Top-level functions
| Declaration | Summary |
|---|---|
fun Box.stackSLEvolveStudio(evolution: StackSLEvolution, id: String = "evolve") |
The StackSL search as a world the AI4 chat works in, the way ai4_paint_studio makes a canvas one: the chat runs generations, looks at the leaders, proposes programs of its own, asks the cheap model for a round, takes a gradient step on a literal, and breeds with populations it left behind. |
fun Box.stackSLEvolution(target: StackSLTarget, settings: StackSLEvolveSettings = StackSLEvolveSettings.wide(), seed: Long = 1L): StackSLEvolution |
The search kept on this box, so re-running the box keeps the population: made from target and settings the first time, and the same object after that while the target's name is the same. |
StackSLWatchmaker.kt
A language model as a StackSL mutation operator: StackSLWatchmaker shows a model the target
and the leaders' renders and turns its reply into candidate programs.
The prompt is built by StackSLWatchmakerPrompt, replies are parsed and checked by
StackSLProposals, and StackSLModelUsage sums model calls, tokens and prompt-cache reads.
src/jvmMain/kotlin/afield/lang/stacksl/evolution/StackSLWatchmaker.kt — read the source
StackSLProposer
The watchmaker: a language model inside a StackSL search, proposing edits to the leaders.
| Declaration | Summary |
|---|---|
fun propose(request: StackSLProposalRequest): StackSLProposalReply |
Answer one round with candidate programs, and what the round cost. |
StackSLProposalRequest
What one watchmaker round is shown.
| Declaration | Summary |
|---|---|
val targetName: String |
The target's own name, which the prompt never includes: spot or rings would say what to draw. It is here so that a person reading a request knows which target it is for. |
val targetColours: String |
The target's colours written as numbers, which the prompt gives in place of its name. |
val metric: String |
The stage's metric in words — pixel, LPIPS, or both, and over what grid. |
val caseLabels: List<String> |
A name for each entry of StackSLParentView.cases, in the same order: one per region of the LPIPS and pixel grids. Empty when the stage scores whole pictures. |
val generation: Int |
The generation this round belongs to. |
val stalled: Int |
Generations since the best program improved, so a model can tell a stalled search from a moving one. |
val parents: List<StackSLParentView> |
The leaders, best first, numbered as they are labelled on the sheet. |
val proposals: Int |
How many programs this round asks for. |
val maxTokens: Int |
The longest a proposed program may be, in words; a longer one is refused unrendered. |
val lastRound: List<StackSLProposalOutcome> |
What became of the previous round's proposals, which the prompt reports back. |
val sheetPng: ByteArray |
The target and the parents, numbered as in parents: StackSLEvolution.thumbnailSheet. |
val preview: ((List<String>) -> StackSLPreview)? |
What a list of programs draws, for the model to check its own proposals against before they are scored: StackSLProposers that do a second pass call it. null for no second pass. |
val sheetDataUrl: String |
sheetPng as a data: URL, which is the form the model is sent it in. |
StackSLPreview
class StackSLPreview(val sheetPng: ByteArray, val status: List<String>)
What some programs draw: the target and each program at 64 px, P1, P2..., and one status line per program.
| Declaration | Summary |
|---|---|
val sheetDataUrl: String |
sheetPng as a data: URL, which is the form the model is sent it in. |
StackSLParentView
One leader as the watchmaker sees it.
| Declaration | Summary |
|---|---|
val rank: Int |
Its place among the leaders, from 1, matching its label on the sheet. |
val program: String |
The program itself, as the model is asked to edit it. |
val fitness: Double |
The objective's own score, on whatever scale the stage's metric uses. |
val skill: Double |
fitness rescaled so that 0 is a flat fill of the target's mean colour and 1 is exact. This is the number the prompt leads with, because a raw fitness means nothing across stages. |
val cases: List<Double> |
Per-region scores, named in order by StackSLProposalRequest.caseLabels. |
val origin: String |
The operator that produced it — a blind mutation, a crossover, or an earlier watchmaker round. |
StackSLProposal
data class StackSLProposal(val parent: Int?, val program: String, val why: String = "")
One proposed program. parent is a 1-based rank, null for one written from scratch.
StackSLProposalReply
What a round returned, and what it cost.
StackSLProposalOutcome
What happened to one offered program. rank is its place overall when it stayed, else null.
| Declaration | Summary |
|---|---|
val program: String |
The program as it was offered, normalised. |
val origin: String |
The label it was offered under: watchmaker:new for one written from scratch, watchmaker:edit for an edit. |
val fitness: Double? |
What it scored, or null if it never got a score — refused, or already in the cache. |
val skill: Double? |
Its fitness rescaled as StackSLParentView.skill is, or null when there is no fitness. |
val rank: Int? |
Its place among all programs when it was kept, counting from 1; null when it was not. |
val problem: String? |
Why it was refused, or null if it entered the population. |
val why: String |
The model's own one-line reason for proposing it, carried through unchanged. |
StackSLModelUsage
Model calls and tokens, summed.
| Declaration | Summary |
|---|---|
val model: String |
Which model answered. plus keeps the newer name when it is not blank. |
val calls: Int |
How many times the model was called, a failed call included. |
val inputTokens: Long |
Input tokens across those calls, cachedTokens among them. |
val cachedTokens: Long |
Input tokens the provider served from its prompt cache; see uncachedTokens. |
val outputTokens: Long |
Tokens the model wrote. |
val millis: Long |
Milliseconds spent waiting on the model. |
operator fun plus(o: StackSLModelUsage) |
The two tallies added, field by field; see model for the one field that is not summed. |
val uncachedTokens: Long |
Input tokens not served from the cache: the part that is paid for in full. |
override fun toString(): String |
The model, the calls, the token split and the time, as the run's notes print it. |
StackSLWatchmaker
The watchmaker on Field's configured AI provider, at the fast model unless told otherwise (AFIELD_AI_WATCHMAKER_MODEL).
| Declaration | Summary |
|---|---|
val model: String |
Which model to ask; AFIELD_AI_WATCHMAKER_MODEL overrides the configured fast model. |
val reasoning: LlmReasoningEffort |
How much the model is asked to think per round. Low, because a round is one small picture and a few hundred words. |
val maxOutputTokens: Long |
The ceiling on one reply, which is a few dozen one-line programs. |
override fun propose(request: StackSLProposalRequest): StackSLProposalReply |
Ask the model for one round, and the second pass when StackSLProposalRequest.preview is given. A call that throws comes back as a reply with no proposals and the failure in StackSLProposalReply.problem, so a round the provider refused costs the search a generation rather than ending it. |
StackSLWatchmakerPrompt
object StackSLWatchmakerPrompt
The watchmaker's prompt: stable once and cached, round per call, and parse for the reply.
| Declaration | Summary |
|---|---|
fun lookup(word: String): String |
The guide's section(s) mentioning word in backticks, or the section titles when word is blank. |
val stable: String by lazy |
Everything that is the same on every call. |
fun round(r: StackSLProposalRequest): String |
One round's request. |
fun revision(r: StackSLProposalRequest, drafts: List<StackSLProposal>, shown: StackSLPreview): String |
The second pass: what each draft draws, and a request for the corrected list. |
fun parse(text: String, parents: Int): Pair<List<StackSLProposal>, List<String>> |
The proposals in a reply, and the non-blank lines that were not proposals. |
StackSLProposals
Checking, normalising and describing programs that come from outside the blind operators.
| Declaration | Summary |
|---|---|
fun program(text: String): StackSLProgram |
text as a program: comments dropped, vector literals closed up, whitespace normalised. |
fun check(text: String, maxTokens: Int): String? |
Why text cannot be offered, or null when it can. |
fun tokenColumns(text: String): List<Int> |
Where each token of text starts, as a 1-based column, in token order. |
fun describeColours(target: StackSLTarget): String |
The target's colours, as a model can use them: each colour and its share of the picture for a target of at most StackSLLayout.PALETTE_LIMIT colours, otherwise the mean and the darkest and brightest tenths. |
EyeEvolution.kt
Interactive evolution: a grid of pictures in a box's output panel, where clicking the ones you like breeds the next generation. The fitness function is the person looking at it.
This is the half of afield.study.search that scores nothing itself. PopulationSearch.kt
spends a budget of evaluations against an Objective; this file spends your attention instead.
Both describe a genome with the same GenomeOps, so a genome written for one drives the other
unchanged, and breedFrom is the shared rule for turning a pair of parents into a child.
EyeEvolution holds the grid, the current parents and every earlier set; evolveByEye puts
one in a box and hands it back; EyeSetting is a number the grid's toolbar shows and lets you
turn between generations. Nothing here knows what a genome is beyond the four functions you
supply for breeding it, drawing it, naming it and labelling its tile. In this tree the genome it
is used with is afield.lang.rpn.ColorRpn, and ColorRpnEvolution.kt is the worked example of
wiring one up.
src/jvmMain/kotlin/afield/study/search/EyeEvolution.kt — read the source
Top-level functions
EyeEvolution
Interactive evolution, where the eye is the fitness function: a grid of tiles in a box's output panel (the parents first, then their children), and clicking EyeEvolution.pick tiles makes them the next parents and breeds again. This is the loop of Karl Sims' Genetic Images and of Dawkins' biomorphs; afield.lang.rpn.ColorRpn programs (the madp GA demo) are one genome for it, and any genome with a picture works — StackSL programs, FLines, synth patches drawn as spectra.
| Declaration | Summary |
|---|---|
val owner: Box |
The box whose output panel holds the grid. |
val start: List<G> |
The parents a run starts from, and restart returns to. |
val children: Int |
How many children each generation has, after the parents. |
val pick: Int |
How many tiles a click selection takes. |
val columns: Int |
How many tiles a grid row holds. |
val channel: String |
The output channel that holds the grid. |
val title: String |
The word in the grid's title bar. |
val settings: List<EyeSetting> |
The settings the toolbar shows, editable there. |
var onPick: (List<G>) -> Unit |
Called with the new parents after every selection, on the grid's thread. |
var parents: List<G> |
The current parents. |
var tiles: List<EyeTile<G>> |
The current tiles, parents first. |
val history: MutableList<List<G>> |
Every set of parents so far, oldest first; the current one last. |
var generation: Int |
How many generations have been bred, counting the first. |
fun pick(vararg indices: Int) |
Make tiles indices (0 is the first parent) the parents, breed, and show the new grid. |
fun reroll() |
New children of the same parents. |
fun fresh() |
Random parents, when the genome can draw them; otherwise a reroll. |
fun restart() |
Back to the parents the code started from. |
fun breedAndShow(reason: String = "start") |
Breed a generation from parents and show it. Runs on the calling thread. |
EyeTile
class EyeTile<G>(val genome: G, val label: String, val note: String, val isParent: Boolean)
One tile of an EyeEvolution grid: a genome, its label (parent a, child 3), and how it was made.
Offspring
class Offspring<G>(val genome: G, val note: String = "")
A child and a short note on how it was bred (shown in its tile's header, e.g. seed+num+op).
EyeSetting
A number the EyeEvolution toolbar shows and lets you change; changing it calls onChange and breeds a new generation from the same parents. choices, when given, makes it a menu.
| Declaration | Summary |
|---|---|
val name: String |
Identifies the control in the toolbar; the browser sends it back to say which one moved. |
var value: Double |
The current value, which the toolbar shows and a change replaces. |
val min: Double |
The smallest value this accepts. A value arriving from the toolbar is clamped into min..max, including when choices is a menu, so a menu entry outside the range is silently pulled back into it. |
val max: Double |
The largest value this accepts; see min for the clamp. |
val step: Double |
The number field's increment. Ignored when choices makes this a menu. |
val choices: List<Double> |
When non-empty, the control is a menu of exactly these values rather than a number field. |
val label: String |
The text shown beside the control; name by default. |
val onChange: (Double) -> Unit |
Called with the new value once it is clamped, before the next generation is bred. |
PopulationSearch.kt
Population search over any genome type — the generic drivers that alife_evolve.kt,
FunctionEvolve.kt and afield.simulation.kratos.KratosStudy all run on. (afield.lang.morpho.MorphoSearch
keeps its own.)
A caller supplies two things and nothing else. GenomeOps says how to make a genome, how to vary
one, and (optionally) how to recombine two; Objective says what a genome scored. The five
drivers in PopulationSearch then spend a budget of evaluations against that pair:
PopulationSearch.random draws independently and keeps the best, PopulationSearch.hillClimb
walks one point, PopulationSearch.evolve runs a (µ+λ) generational loop with tournament or
lexicase selection, PopulationSearch.novelty replaces the objective's own fitness with
distance from an archive of behaviours, and PopulationSearch.mapElites keeps the best genome
per cell of a grid over the behaviour space rather than one winner.
PopulationSearch.random is the control arm. It draws from exactly the distribution the
other four mutate within, so a difference between their results is a difference in selection
rather than in the generator. Reporting an evolutionary result without it says nothing: a
generator good enough to stumble onto a decent genome occasionally looks like a successful
search. MorphoSearch reports the same pair for the same reason.
Budget is evaluations, not generations. A population of 24 and a hill climb of 1 do very
different amounts of work per generation, so a per-generation budget would compare budgets rather
than methods. SearchRun.trace is likewise best-so-far per evaluation.
What determinism means here. Given the same GenomeOps, the same Objective and the same
seed, a driver returns the same SearchRun: the same best genome, the same trace, the same
evaluation count, every time. It is one seeded Random threaded through every draw, and
evaluation is sequential, because parallel evaluation would reorder those draws and throw
the property away. It says nothing about thread safety, and it holds only if the caller's
mutate and evaluate are themselves deterministic functions of their arguments.
src/jvmMain/kotlin/afield/study/search/PopulationSearch.kt — read the source
Used in: Spatial artificial life, Braitenberg vehicles with memory
Top-level functions
| Declaration | Summary |
|---|---|
fun <G> objectiveOf(score: (G) -> Double): Objective<G> |
Build an Objective from a plain scalar score. |
Variation
class Variation<G>(val name: String, val apply: (G, Random) -> G)
A named variation operator, so a run can report which mutation produced each improvement.
| Declaration | Summary |
|---|---|
override fun toString(): String |
The operator's name, which is what a run records against each improvement. |
GenomeOps
Everything a population search needs to know about a genome type.
| Declaration | Summary |
|---|---|
fun random(rng: Random): G |
An independent draw from the genome distribution. The control arm samples exactly this. |
val mutations: List<Variation<G>> |
The variation operators, at least one. evolve picks uniformly among them and records the name. |
fun crossover(a: G, b: G, rng: Random): G? |
Recombine two parents, or null — the default — to declare that this genome has no recombination. |
fun signature(g: G): String? |
A stable string identifying a genome's phenotype, or null — the default — for no memoisation. |
Evaluation
What one candidate scored.
| Declaration | Summary |
|---|---|
val fitness: Double |
The scalar, higher is better. Drives tournament selection and the trace. |
val cases: DoubleArray |
Per-test-case scores, higher better, for Selection.LEXICASE. Empty means the objective has no case structure, and lexicase then degenerates to a uniform random draw — which is why evolve rejects that combination instead of selecting at random. |
val behaviour: DoubleArray |
A fixed-length behavioural descriptor for PopulationSearch.novelty. Empty means the objective offers none. |
override fun toString(): String |
The fitness and the sizes of cases and behaviour, for a log line. |
Objective
Scores a genome. Higher is better, always — a cost function is negated by its caller, not here.
| Declaration | Summary |
|---|---|
fun evaluate(genome: G): Evaluation |
Score one genome. A throw is caught and read as negative infinity, so a candidate the objective cannot evaluate costs its budget and loses rather than ending the run. |
Individual
class Individual<G>(val genome: G, val evaluation: Evaluation, val origin: String)
A genome and what it scored.
| Declaration | Summary |
|---|---|
val fitness: Double |
Shorthand for Evaluation.fitness, which is what selection and the trace compare. |
override fun toString(): String |
The fitness and the origin that produced it, as the drivers' logs print it. |
Improvement
class Improvement<G>(val generation: Int, val fitness: Double, val origin: String, val genome: G)
One generation in which the population's leader improved.
| Declaration | Summary |
|---|---|
val generation: Int |
1-based generation index. |
val fitness: Double |
What the new leader scored. |
val origin: String |
The variation operator that produced it, or "seed"/"random"/"immigrant". |
val genome: G |
The new leader itself, so the list of improvements is a lineage and a figure can show what the search looked like as it went rather than only where it ended up. |
override fun toString(): String |
The generation, the fitness and the origin, as a run's improvement log prints it. |
Selection
How PopulationSearch.evolve picks a parent.
Constants: TOURNAMENT, LEXICASE.
SearchRun
What one search run did.
| Declaration | Summary |
|---|---|
val label: String |
Which driver this was: random, hill climb, evolve, novelty or map-elites. |
val best: Individual<G>? |
The best genome the run evaluated, or null if it never evaluated one. |
val evaluations: Int |
Times the objective was actually called. A cache hit costs none of this, so it is the budget the run spent rather than the number of genomes it looked at. |
val trace: DoubleArray |
Best-so-far fitness after each evaluation, so two drivers at the same budget are directly comparable however differently they are shaped internally. |
val proposals: Int |
Genomes offered to the evaluator, including cache hits. Well above evaluations means the driver is re-treading; see GenomeOps.signature. |
val recombined: Int |
How many children came from GenomeOps.crossover. Zero for a genome that declares no recombination, and the number to look at before calling a run "a GA". |
val immigrants: Int |
How many children were fresh draws rather than offspring, from PopulationSearch.evolve's immigrantRate. Zero for every other driver. |
val generations: Int |
Generations the loop completed. Zero for the drivers that have no generation. |
val survivors: List<Individual<G>> |
The population the run ended with — the last generation from PopulationSearch.evolve, the filled cells from PopulationSearch.mapElites, the point PopulationSearch.hillClimb stopped on. Always empty for PopulationSearch.random, which keeps no population. |
val millis: Double |
Wall-clock milliseconds for the whole run, the caller's objective included. |
val improvements: List<Improvement<G>> |
Every generation in which the leader improved, in order. Empty for a non-generational driver. |
val bestFitness: Double |
The scalar the run reached, or negative infinity if it never evaluated anything. |
val initialFitness: Double |
Best-so-far at the first evaluation — the one-shot baseline every later point improves on. |
fun summary(): String |
One aligned line for a comparison table: the label, the budget spent, the best fitness, the generations and the proposals, with crossover and immigrant counts only when non-zero. |
override fun toString(): String |
The run's summary line. |
PopulationSearch
The drivers. All four take the same budget and return the same shape of result.
SearchComparison.kt
Equal-budget comparison of search drivers for any GenomeOps and Objective: "does search beat
random sampling here?"
Every arm gets exactly the same number of objective calls at every seed, and the result keeps each
run's per-evaluation SearchRun.trace, so the arms can be compared at any budget up to the one they
ran at. compareSearch runs it; braitenberg_vehicles.md §10 and alife_examples.md ("Does search
beat random?") read it.
src/jvmMain/kotlin/afield/study/search/SearchComparison.kt — read the source
Used in: Spatial artificial life
Top-level functions
| Declaration | Summary |
|---|---|
fun <G> compareSearch(ops: GenomeOps<G>, objective: Objective<G>, budget: Int = 300, seeds: List<Long> = (1L..12L).toList(), arms: List<SearchArm<G>> = SearchArm.standard(), label: String = "search", parallel: Boolean = true): SearchComparison<G> |
Run every arm in arms on ops and objective for exactly budget objective calls at every seed in seeds. The first arm is the control the others' wins are counted against. |
SearchArm
One search driver as an arm of a SearchComparison: a name and how to run it for a budget at a seed.
| Declaration | Summary |
|---|---|
val name: String |
Short name for tables, plots and SearchComparison.of. |
val run: (ops: GenomeOps<G>, objective: Objective<G>, budget: Int, seed: Long) -> SearchRun<G> |
Run the driver on ops and objective for exactly budget evaluations from seed. |
override fun toString(): String |
The name. |
SearchComparisonCell
class SearchComparisonCell<G>(val arm: String, val seed: Long, val run: SearchRun<G>)
One arm's run at one seed.
| Declaration | Summary |
|---|---|
val arm: String |
The arm's name. |
val seed: Long |
The search seed. |
val run: SearchRun<G> |
The run, with its per-evaluation SearchRun.trace. |
SearchComparison
compareSearch's result: every arm at every seed, all at budget evaluations.
| Declaration | Summary |
|---|---|
val label: String |
What was searched, for the table header. |
val budget: Int |
Evaluations each run was allowed. |
val arms: List<String> |
Arm names, in run order; the first is the control the others are scored against. |
val cells: List<SearchComparisonCell<G>> |
Every run, seed-major. |
fun of(arm: String): List<SearchComparisonCell<G>> |
The runs of arm, in seed order. |
val seeds: Int |
Seeds per arm. |
fun best(arm: String): DoubleArray |
Best fitness of each seed's run of arm. |
fun bestAt(arm: String, evaluations: Int): DoubleArray |
Best-so-far of each seed's run of arm after evaluations evaluations (≤ budget). |
fun wins(a: String, b: String, evaluations: Int = budget): Int |
Seeds at which a ended strictly above b, optionally after only evaluations. |
fun median(arm: String, evaluations: Int = budget): Double |
Median per-seed best of arm after evaluations. |
fun leader(evaluations: Int = budget): String |
The arm with the highest median after evaluations. |
fun champion(arm: String): G |
The best genome any run of arm found. |
fun rows(evaluations: Int = budget): String |
One line per arm: median, IQR, min, max of the per-seed bests at evaluations, and wins against the control. |
fun table(evaluations: Int = budget): String |
rows under a header naming the label, budget and seed count. |
fun budgetTable(budgets: List<Int>): String |
The same runs read at several budgets (each ≤ budget): one row per budget, each arm's median and wins against the control. Equal to running each budget separately (pinned by SearchComparisonTest), because none of the drivers schedules around its budget. |
fun perSeed(): String |
Per-seed bests, one row per seed. |
fun curve(arm: String): Array<DoubleArray> |
Best-so-far of arm after each evaluation across seeds: [q25, median, q75] rows. |
fun medianRun(arm: String): SearchRun<G> |
The run of arm whose best fitness is the median over seeds (the lower middle for an even count). |
fun fitnessRange(): ClosedFloatingPointRange<Double> |
The fitness range plot uses when none is given: every arm's quartile band. |
fun plot(width: Int = 300, height: Int = 300, yRange: ClosedFloatingPointRange<Double> = fitnessRange()): Img |
Best-so-far fitness against evaluations: each arm's median as a line, its interquartile range as a band, its final median written at the right. Arms take palette in order (grey for the control, then blue, orange, green). Pass the same yRange to two plots to put them on one scale. |
override fun toString(): String |
table. |