Skip to content

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

object 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

class ColorRpnProgram internal constructor(val source: String, val tokens: List<String>, val unknown: List<String>, private val ops: IntArray, private val data: DoubleArray)

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

data class ColorRpnBreeding(val mutationRate: Double = 0.12, val targetLength: Int = 18, val seedSplice: Double = 0.42, val weights: Map<String, Double> = DEFAULT_WEIGHTS, val nudgeLength: Boolean = true)

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

class StackSLEvolution(private val targetAt: (dim: Int) -> StackSLTarget, settings: StackSLEvolveSettings = StackSLEvolveSettings(), val seed: Long = 1L, val parallel: Boolean = true, val time: Float = 1.75f)

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

data class StackSLStage(val generations: Int = Int.MAX_VALUE, val dim: Int = DEFAULT_FITNESS_DIM, val metric: StackSLMetric = StackSLMetric.LPIPS, val lpipsGrid: Int = 2)

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

data class StackSLEvolveSettings(val mu: Int = 12, val lambda: Int = 24, val islands: Int = 1, val migrateEvery: Int = 8, val migrants: Int = 2, val selection: Selection = Selection.LEXICASE, val tournament: Int = 3, val crossoverRate: Double = 0.25, val immigrantRate: Double = 0.05, val maxTokens: Int = 48, val stages: List<StackSLStage> = listOf(StackSLStage()), val fitEvery: Int = 0, val fitIterations: Int = 16, val fitLearningRate: Double = 0.05, val watchmakerEvery: Int = 0, val watchmakerOnStall: Int = 0, val watchmakerRevise: Boolean = true, val watchmakerWait: Boolean = true, val proposals: Int = 8, val parents: Int = 4)

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

data class StackSLGenerationRecord(val generation: Int = 0, val renders: Int = 0, val stage: String = "", val bestFitness: Double = 0.0, val bestSkill: Double = 0.0, val leaderOrigin: String? = null)

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

data class StackSLImprovement(val generation: Int = 0, val renders: Int = 0, val fitness: Double = 0.0, val skill: Double = 0.0, val origin: String = "", val program: String = "")

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

data class StackSLFitOutcome(val program: String, val fitted: String?, val lossBefore: Double?, val lossAfter: Double?, val skillBefore: Double?, val skillAfter: Double?, val problem: String?)

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.

Declaration Summary
var offered: Int Candidates this origin produced.
var survived: Int How many of them survived a trim into the next generation.
var leads: Int How many of them became the overall leader, which is the column a claim about an operator rests on.

StackSLSnapshotMember

data class StackSLSnapshotMember(val program: String = "", val origin: String = "", val fitness: Double = 0.0, val skill: Double = 0.0)

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

data class StackSLEvolutionSnapshot(val version: Int = 1, val label: String = "", val target: String = "", val seed: Long = 1L, val generation: Int = 0, val renders: Int = 0, val settings: StackSLEvolveSettings = StackSLEvolveSettings(), val islands: List<List<StackSLSnapshotMember>> = emptyList(), val history: List<StackSLGenerationRecord> = emptyList(), val improvements: List<StackSLImprovement> = emptyList(), val notes: List<String> = emptyList(), val credit: Map<String, StackSLOriginCredit> = emptyMap(), val usage: StackSLModelUsage = StackSLModelUsage(), val lastRound: List<StackSLProposalOutcome> = emptyList(), val savedAt: Long = 0L)

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. LSystemGenome in alife_evolve.kt already has point, insert, delete, duplicate, swap, single-point crossover and a signature, 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.MorphoGenome does exactly that for MorphoHDL, and already reports its invalid fraction rather than hiding it. It predates PopulationSearch and 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.mapElites and Selection.LEXICASE are 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

Declaration Summary
fun evolveStackSLTowards(target: StackSLTarget, budget: Int = 600, seed: Long = 1L, genome: StackSLGenome = StackSLGenome(), dim: Int = target.raster.width, mu: Int = 6, lambda: Int = 12, selection: Selection = Selection.TOURNAMENT, tournament: Int = 3, crossoverRate: Double = 0.25, immigrantRate: Double = 0.05, pyramidLevels: Int = StackSLImageObjective.DEFAULT_PYRAMID_LEVELS, parallel: Boolean = true, metric: StackSLMetric = StackSLMetric.LPIPS, cache: StackSLScoreCache? = StackSLScoreCache(), supersample: Int = 1, lpipsSide: Int = StackSLImageObjective.DEFAULT_LPIPS_SIDE, label: String = "stacksl ${target.name}"): StackSLEvolveRun Evolve a StackSL program toward target, with the random control arm run beside it.
fun danglingValuesOf(program: StackSLProgram): Int How many values program pushes and never consumes, read off the compiler's own analysis.
fun CpuRaster.toImg(): Img A CpuRaster as an RGB Img, for montage and savePng. Alpha is dropped, not premultiplied.

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

enum class 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

class StackSLPalette(val words: List<StackSLWord>, val numbers: List<Double> = DEFAULT_NUMBERS, val numberShare: Double = 0.45)

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

class StackSLGenome(val palette: StackSLPalette = StackSLPalette.DEFAULT, val minTokens: Int = 3, val maxTokens: Int = 24, val maxRandomTokens: Int = maxTokens) : GenomeOps<StackSLProgram>

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

class RasterStructure(val spread: Double, val distinctColours: Int, val spatialFrequency: Double, val colourfulness: Double)

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

object 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

object 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

enum class StackSLMetric

What StackSLImageObjective counts as "close to the target".

Constants: PIXEL, LPIPS, LPIPS_PIXEL.

StackSLScores

class StackSLScores(val valid: Boolean, val pixelFitness: Double, val pixelCases: DoubleArray, val lpipsFitness: Double, val lpipsCases: DoubleArray, val behaviour: DoubleArray)

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

class 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

class StackSLImageObjective(val target: StackSLTarget, val dim: Int = target.raster.width, val time: Float = 1.75f, val caseGrid: Int = 4, val pyramidLevels: Int = DEFAULT_PYRAMID_LEVELS, val parallel: Boolean = true, val metric: StackSLMetric = StackSLMetric.LPIPS, val cache: StackSLScoreCache? = null, val supersample: Int = 1, val lpipsSide: Int = DEFAULT_LPIPS_SIDE, val lpipsRegions: Boolean = false, val lpipsGrid: Int = 2) : Objective<StackSLProgram>

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

class StackSLEvolveRun(val target: StackSLTarget, val evolved: SearchRun<StackSLProgram>, val control: SearchRun<StackSLProgram>, val invalidRate: Double, val danglingValues: Int, val objective: StackSLImageObjective)

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

fun interface 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

data class StackSLProposalRequest(val targetName: String, val targetColours: String, val metric: String, val caseLabels: List<String>, val generation: Int, val stalled: Int, val parents: List<StackSLParentView>, val proposals: Int, val maxTokens: Int, val lastRound: List<StackSLProposalOutcome>, val sheetPng: ByteArray, val preview: ((List<String>) -> StackSLPreview)? = null)

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

data class StackSLParentView(val rank: Int, val program: String, val fitness: Double, val skill: Double, val cases: List<Double>, val origin: String)

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

data class StackSLProposalReply(val proposals: List<StackSLProposal>, val usage: StackSLModelUsage = StackSLModelUsage(), val problem: String? = null, val raw: String = "")

What a round returned, and what it cost.

Declaration Summary
val proposals: List<StackSLProposal> The programs the round produced, in the order they were written. Empty when nothing parsed.
val usage: StackSLModelUsage What the round cost: calls, tokens and time.
val problem: String? Lines that could not be read, or a failure, said plainly.
val raw: String The reply verbatim, so a round that parsed badly can still be read.

StackSLProposalOutcome

data class StackSLProposalOutcome(val program: String = "", val origin: String = "", val fitness: Double? = null, val skill: Double? = null, val rank: Int? = null, val problem: String? = null, val why: String = "")

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

data class StackSLModelUsage(val model: String = "", val calls: Int = 0, val inputTokens: Long = 0, val cachedTokens: Long = 0, val outputTokens: Long = 0, val millis: Long = 0)

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

class StackSLWatchmaker(val model: String = Ai4ModelDefaults.envOrDefault("AFIELD_AI_WATCHMAKER_MODEL", Ai4ModelDefaults.fastModel()), val reasoning: LlmReasoningEffort = LlmReasoningEffort.LOW, val maxOutputTokens: Long = 6000) : StackSLProposer

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

object 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

Declaration Summary
fun <G> Box.evolveByEye(parents: List<G>, breed: (List<G>, Random) -> Offspring<G>, picture: (G) -> ByteArray, text: (G) -> String = { it.toString() }, meta: (G) -> String = { "" }, fresh: ((Random) -> G)? = null, children: Int = 7, pick: Int = 2, columns: Int = 3, channel: String = "evolve", title: String = "evolve", settings: List<EyeSetting> = emptyList(), seed: Long? = null): EyeEvolution<G> Show an EyeEvolution grid for parents in this box and return it; see EyeEvolution.
fun <G> Box.evolveByEye(ops: GenomeOps<G>, parents: List<G>, picture: (G) -> ByteArray, text: (G) -> String = { it.toString() }, children: Int = 7, pick: Int = 2, columns: Int = 3, channel: String = "evolve", seed: Long? = null): EyeEvolution<G> evolveByEye for a genome with GenomeOps: a child is GenomeOps.crossover of two of the parents, or one of GenomeOps.mutations applied to one when crossover declines; Fresh Parents draws GenomeOps.random.
fun <G> breedFrom(ops: GenomeOps<G>, parents: List<G>, rng: Random): Offspring<G> One child of parents by ops: crossover of two (distinct when there are two), else a named mutation of one.

EyeEvolution

class EyeEvolution<G> internal constructor(val owner: Box, val start: List<G>, private val breed: (List<G>, Random) -> Offspring<G>, private val picture: (G) -> ByteArray, private val text: (G) -> String, private val meta: (G) -> String, private val freshParent: ((Random) -> G)?, val children: Int, val pick: Int, val columns: Int, val channel: String, val title: String, val settings: List<EyeSetting>, seed: Long?)

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

class EyeSetting(val name: String, var value: Double, val min: Double, val max: Double, val step: Double = 1.0, val choices: List<Double> = emptyList(), val label: String = name, val onChange: (Double) -> Unit)

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

interface GenomeOps<G>

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

class Evaluation(val fitness: Double, val cases: DoubleArray = DoubleArray(0), val behaviour: DoubleArray = DoubleArray(0))

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

fun interface Objective<G>

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

enum class Selection

How PopulationSearch.evolve picks a parent.

Constants: TOURNAMENT, LEXICASE.

SearchRun

class SearchRun<G>(val label: String, val best: Individual<G>?, val evaluations: Int, val trace: DoubleArray, val proposals: Int, val recombined: Int, val immigrants: Int, val generations: Int, val survivors: List<Individual<G>>, val millis: Double, val improvements: List<Improvement<G>> = emptyList())

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

object PopulationSearch

The drivers. All four take the same budget and return the same shape of result.

Declaration Summary
const val PROPOSAL_LIMIT: Int Proposals a driver may make per unit of budget before it is judged stuck and stopped.
fun <G> random(ops: GenomeOps<G>, objective: Objective<G>, budget: Int, seed: Long = 1L, label: String = "random"): SearchRun<G> Draw budget independent genomes and keep the best. The control.
fun <G> hillClimb(ops: GenomeOps<G>, objective: Objective<G>, budget: Int, seed: Long = 1L, patience: Int = 40, start: G? = null, label: String = "hill climb"): SearchRun<G> Accept any variation that is not worse, restart when stuck.
fun <G> evolve(ops: GenomeOps<G>, objective: Objective<G>, budget: Int, seed: Long = 1L, mu: Int = 8, lambda: Int = 16, tournament: Int = 2, selection: Selection = Selection.TOURNAMENT, crossoverRate: Double = 0.0, immigrantRate: Double = 0.0, elitistMutation: Boolean = true, seeds: List<G> = emptyList(), label: String = "evolve", onGeneration: ((generation: Int, evaluations: Int) -> Unit)? = null): SearchRun<G> A (µ+λ) generational search: elitist truncation, tournament or lexicase selection, optional recombination and optional random immigrants.
fun <G> novelty(ops: GenomeOps<G>, objective: Objective<G>, budget: Int, seed: Long = 1L, mu: Int = 8, lambda: Int = 16, neighbours: Int = 3, validityWeight: Double = 0.0, archiveThreshold: Double = 0.0, validity: (Double) -> Double = { if (it.isFinite()) 1.0 else 0.0 }, label: String = "novelty"): NoveltyRun<G> Novelty search: score a genome by how unlike everything already found it is, not by how good.
fun <G> mapElites(ops: GenomeOps<G>, objective: Objective<G>, budget: Int, axes: List<ClosedFloatingPointRange<Double>>, bins: List<Int>, seed: Long = 1L, initial: Int = 32, label: String = "map-elites"): MapElitesRun<G> MAP-Elites: keep the best genome found in each cell of a grid over the behaviour space.
class MapElitesRun<G>(val run: SearchRun<G>, val elites: Map<List<Int>, Individual<G>>, val bins: List<Int>, val axes: List<ClosedFloatingPointRange<Double>>, val offGridCandidates: Int) mapElites's result: the archive, and the two numbers a QD result is reported as.
class NoveltyRun<G>(val run: SearchRun<G>, val archive: List<Individual<G>>) novelty's result: the underlying run plus the behavioural archive it grew.

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

class SearchArm<G>(val name: String, val run: (ops: GenomeOps<G>, objective: Objective<G>, budget: Int, seed: Long) -> SearchRun<G>)

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

class SearchComparison<G>(val label: String, val budget: Int, val arms: List<String>, val cells: List<SearchComparisonCell<G>>)

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.