ALife
Spatial artificial life: grid cellular automata (life-like, cyclic and Wolfram's elementary rules), continuous systems (reaction-diffusion, Turing patterns, Lenia), agent populations (boids, Physarum, diffusion-limited aggregation) and structural growth (L-systems, space colonization), plus the measurement layer every one of them makes its claims through.
17 source file(s), 938 public symbol(s), 851 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.
alife.kt
Spatial artificial life for the Field stdlib — the continuous / grid / agent half of ALife, the
counterpart to the graph-rewriting half already here (growing.kt, dynamic_graph_systems.kt,
interactionnets.kt, booleannetworks.kt) and to the constraint-based generator in wfc.kt.
Six files:
alife.kt(this one) — the substrate:IntGrid, the grid↔Imgbridges, and the measurement layer. Everything else in the family makes its claims through the functions at the bottom of this file.alife_ca.kt— discrete cellular automata:LifeRuleparsed fromB3/S23, outer-totalistic and cyclic multi-state CA, Wolfram's elementary 1-D rules rendered as a space-timeImg, and a 3-D life-like CA that marches out to aTriMesh.alife_continuous.kt— Gray–Scott reaction–diffusion, Turing patterns, and Lenia (continuous CA: ring-kernel convolution plus a growth mapping).alife_field.kt— the inlineToHTMLviews forIntGrid,GrayScottStateandLeniaRun, kept separate so the five system files name nothing from Field's visualizer.alife_agents.kt— boids flocking, a Physarum/slime-mould deposit–sense–move population over a trailImg, and diffusion-limited aggregation.alife_lsystem.kt— a bracketed/parametric L-system with a turtle that emitsafield.geometry.path.FLines, and space-colonization branching.
Design choices, all deliberate:
- Built on
Img, not on a private raster. Every continuous system is anImg -> Imgstep, so a reaction–diffusion field pipelines straight intogaussianBlur,colormap,connectedComponents,isoContourFLinesandasHeightSdfwith no glue. Nothing here reimplements convolution. - A step is a value, not a side effect.
step()returns the next state; you keep the frames you want. The one exception is the mutable agent populations, where copying 50 000 agents per frame would be the whole cost. - Deterministic from a seed. Every stochastic system takes a
seed: Longand uses its ownkotlin.random.Random, so a figure in the docs is reproducible and a regression is a diff. - No external dependency. Pure Kotlin over
FloatArray/IntArray. - Measured, not asserted. The functions under "Measurement" exist because "it produced pixels"
is not evidence. A flock is a population whose
polarizationis high and whosenearestNeighborDistanceshave a floor; a DLA cluster is one whosemassRadiusDimensionis ~1.7; a Gray–Scott spot regime is one whoseblobShapesare round. Each of those is a number a control arm gets wrong.
Out of scope: GPU implementations (that is Field 4 / StackSL / RectSL — these are CPU reference implementations you can read), and any system needing a trained model.
src/jvmMain/kotlin/afield/simulation/agents/alife.kt — read the source
Used in: Spatial artificial life
Top-level functions
| Declaration | Summary |
|---|---|
fun IntGrid.toImg(states: Int? = null): Img |
Render this grid as a 1-channel Img with state s mapped to s / max(1, states - 1), so a two-state grid becomes a clean 0/1 mask and a 12-state cyclic CA becomes a ramp ready for Img.colormap. |
fun IntGrid.toImg(palette: List<Triple<Float, Float, Float>>): Img |
Render this grid as RGB through an explicit palette, one Triple(r, g, b) per state — the exact way to show a multi-state automaton, where a grayscale ramp implies an ordering between states that may not exist. States beyond the palette wrap around it. |
fun Img.toIntGrid(threshold: Float = 0.5f, wrap: Boolean = true): IntGrid |
Threshold a 1-channel Img into a two-state IntGrid — the way a reaction–diffusion field, a Physarum trail or an imported picture becomes CA seed material. |
fun cyclicPalette(states: Int): List<Triple<Float, Float, Float>> |
A dark-background categorical palette sized for multi-state automata: state 0 is near-black background, and the rest sweep the hue circle at fixed saturation/value so no two adjacent states look alike. Built from hsvToRgb's maths directly so it needs no Img round trip. |
fun translationBetween(a: IntGrid, b: IntGrid): Pair<Int, Int>? |
The rigid translation from a to b as (dx, dy), or null if b is not a translate of a. |
fun IntGrid.liveCentroid(): Vec2? |
Area-weighted centroid of the non-zero cells, or null when the grid is empty. Fine for reporting; prefer translationBetween when the claim is that a pattern moved rigidly. |
fun polarization(vx: FloatArray, vy: FloatArray): Double |
The polarization (a.k.a. alignment) order parameter of a set of headings: \|Σ v̂\| / N, the length of the mean unit velocity. |
fun randomPolarizationBaseline(n: Int): Double |
The expected polarization of n independent uniformly-random headings — the null hypothesis to compare a measured value against. |
fun nearestNeighborDistances(xs: FloatArray, ys: FloatArray, wrapWidth: Double? = null, wrapHeight: Double? = null): DoubleArray |
Distance from every point to its nearest other point, in index order — the second half of "this is a flock": a flock has a floor under this distribution, because separation keeps agents apart, while a cohesion-only population collapses to a point and a random cloud has no floor at all. |
fun quantileOf(values: DoubleArray, p: Double): Double |
The p-quantile of values (0 = min, 0.5 = median, 1 = max), by nearest-rank on a sorted copy. |
fun Img.blobShapes(minArea: Int = 4, connectivity: Int = 8): List<BlobShape> |
Per-blob BlobShapes of a binary mask — the statistic that tells spots from stripes. |
fun Img.radialAutocorrelation(maxLag: Int = 32, directions: Int = 24, stride: Int = 2, bilinear: Boolean = false): DoubleArray |
Direction-averaged spatial autocorrelation of a 1-channel image at integer lags 0..maxLag, normalised so index 0 is 1. |
fun Img.correlationLength(maxLag: Int = 32, directions: Int = 24, stride: Int = 2, bilinear: Boolean = false, level: Double = 1.0 / Math.E): Double |
The correlation length of a pattern: the lag at which radialAutocorrelation first falls to level, linearly interpolated, or maxLag if it never does. |
fun Img.topShare(fraction: Double = 0.05): Double |
The share of an image's total mass carried by its brightest fraction of pixels — a concentration measure (the Lorenz-curve reading behind a Gini coefficient). |
fun massRadiusDimension(points: List<Vec2>, center: Vec2, rMin: Double = 4.0, rMax: Double = 64.0, samples: Int = 16, minCount: Int = 0): MassRadiusFit |
Fractal dimension by mass–radius scaling: fit N(r) ∝ r^D for the number of occupied points within radius r of center, over rMin..rMax, by least squares in log–log space. |
fun Img.totalMass(): Double |
Total of every sample, as a Double — the conserved quantity a pure-diffusion step must preserve. |
fun IntGrid.stateEntropyBits(states: Int? = null): Double |
Shannon entropy of the state histogram of a grid, in bits — 0 when every cell agrees and log2(states) when the states are equally common. Used to say "the initial soup was near-maximal and the settled pattern is not", quantitatively. |
fun smoothNoiseImg(width: Int, height: Int, cells: Int = 8, seed: Long = 1L): Img |
A smooth pseudo-random scalar field in 0..1 — value noise at cells cells across, smoothstep-faded (t*t*(3-2t)) between lattice values rather than linearly interpolated. |
fun discMaskImg(width: Int, height: Int, radius: Double): Img |
A filled disc mask of radius centred in a width x height image — the calibration object the gallery measures blobShapes and massRadiusDimension against, so both statistics are shown to read the value theory says rather than assumed to. |
IntGrid
A rectangular lattice of integer cell states — the substrate every discrete automaton in the family steps. Row-major, cells[y * width + x], origin top-left (matching Img).
| Declaration | Summary |
|---|---|
val width: Int |
Cells across, >= 1. |
val height: Int |
Cells down, >= 1. |
val cells: IntArray |
State per cell, cells[y * width + x]. |
val wrap: Boolean |
true for a torus (out-of-range reads wrap), false for a bounded board whose outside counts as state 0. A glider crossing a torus seam reappears; the R-pentomino's 1103-generation history needs room rather than a wrap. |
val size: Int |
Number of cells. |
fun at(x: Int, y: Int): Int |
State at (x, y); throws if out of bounds. See get for the border-resolving read. |
operator fun get(x: Int, y: Int): Int |
State at (x, y), resolving out-of-range coordinates through wrap: wrapped on a torus, otherwise 0. This is the read every neighbourhood count goes through. |
operator fun set(x: Int, y: Int, v: Int) |
Write v at (x, y), mutating in place; out-of-range writes are ignored on a bounded grid. |
fun copy(): IntGrid |
An independent copy. |
fun blank(): IntGrid |
A same-shape grid of zeros. |
fun count(state: Int): Int |
How many cells hold state. |
fun population(): Int |
How many cells are non-zero — "population", for a two-state automaton. |
fun maxState(): Int |
The largest state present, so a palette can be sized without being told. |
fun sameCellsAs(other: IntGrid): Boolean |
true when other has the same shape and exactly the same states. |
fun mooreCount(x: Int, y: Int, state: Int = 1): Int |
How many of the eight surrounding cells hold state (the Moore neighbourhood, excluding the centre) — the count Conway's Life and every other life-like rule is a function of. |
fun vonNeumannCount(x: Int, y: Int, state: Int = 1): Int |
How many of the four edge-adjacent cells hold state (the von Neumann neighbourhood). |
fun neighborhoodCount(x: Int, y: Int, state: Int, radius: Int, vonNeumann: Boolean = false): Int |
How many cells within Chebyshev distance radius (excluding the centre) hold state — the generalised Moore neighbourhood that "Larger than Life" rules and cyclicStep read. |
fun translated(dx: Int, dy: Int): IntGrid |
Copy this grid shifted by (dx, dy). On a torus nothing is lost; on a bounded grid whatever leaves is dropped. Used to state "the pattern moved by exactly (1, 1)" as an equality rather than as a claim about centroids — see translationBetween. |
fun livePoints(): List<Vec2> |
Coordinates of every non-zero cell, row-major. |
fun liveBounds(): IntArray? |
Inclusive bounding box of the non-zero cells as (minX, minY, maxX, maxY), or null when the grid is empty. Bounding-box displacement is how a translating pattern is measured without centroid rounding coming into it. |
override fun toString(): String |
|
override fun toHTML(): String |
Inline view for the value visualizer: the lattice at one pixel per cell, plus population, border mode and state entropy. The body lives in alife_field.kt (alifeGridHtml) so that this file keeps its "integer lattices and nothing else" footprint. |
BlobShape
Shape statistics of one connected blob: pixel area, the crack perimeter (the number of unit cell edges separating the blob from anything else, including the image border), and the dimensionless isoperimetricRatio.
| Declaration | Summary |
|---|---|
val label: Int |
The blob's label in the ComponentLabelling it came from. |
val area: Int |
Foreground pixel count. |
val crackPerimeter: Int |
Unit edges on the blob's boundary. |
val isoperimetricRatio: Double |
P² / (4πA) with the crack perimeter corrected by π/4; see blobShapes for why that factor is there and what the resulting scale means. |
MassRadiusFit
Result of a mass–radius fractal-dimension fit.
alife_agents.kt
Agent-based spatial ALife: boids flocking, a Physarum (slime-mould) deposit–sense–move population
over a trail Img, and diffusion-limited aggregation.
See alife.kt for the family overview.
These are the three canonical demonstrations that global order can have no global rule: a flock's
alignment, a slime mould's transport network and a dendrite's fractal dimension are all properties
nothing in the code mentions. Which is exactly why each one here ships with a control arm — a run of
the same code with one interaction switched off — because "emergence" is otherwise the easiest thing
in computing to claim and the hardest to check. See polarization, Img.topShare and
massRadiusDimension for the three numbers those controls fail.
Unlike the rest of the family these types are mutable: step() advances the population in place.
Copying 50 000 agents per frame would be the entire cost of a Physarum run, and the systems are
simulations rather than values.
src/jvmMain/kotlin/afield/simulation/agents/alife_agents.kt — read the source
Used in: Spatial artificial life
Top-level functions
| Declaration | Summary |
|---|---|
fun BoidFlock.step(p: BoidParams = BoidParams()) |
One boid step, mutating the flock in place: accumulate the three steering terms from every neighbour within BoidParams.perceptionRadius (toroidal metric), cap the total force, integrate, wrap. |
fun BoidFlock.stepTimes(p: BoidParams = BoidParams(), steps: Int) |
step applied steps times. |
fun BoidFlock.toFLines(length: Double = 4.0, z: Number = 0): List<afield.geometry.path.FLine> |
Draw the flock as one short arrow afield.geometry.path.FLine per agent — a heading is the thing worth seeing, and a dot per boid shows position while hiding the very thing being claimed. |
fun BoidFlock.toImg(scale: Double = 1.0, tail: Double = 3.0): Img |
Render the flock into an Img of the given size: each agent splats a small anti-aliased dot, and a short tail behind it shows its heading. |
fun PhysarumSwarm.step(p: PhysarumParams, rng: kotlin.random.Random): PhysarumSwarm |
One Physarum step: every agent deposits, samples its three sensors, turns toward the strongest, and moves; then the trail diffuses and decays. |
fun PhysarumSwarm.stepTimes(p: PhysarumParams, steps: Int, seed: Long = 1L): PhysarumSwarm |
step applied steps times with one kotlin.random.Random seeded from seed. |
fun dlaCluster(width: Int, height: Int, particles: Int = 4000, seed: Long = 1L, stickiness: Double = 1.0, maxWalkSteps: Long = 200_000_000L): Aggregate |
Grow a diffusion-limited aggregation cluster: a seed at the centre, then particles released far away that random-walk until they touch the cluster and stick. |
fun edenCluster(width: Int, height: Int, particles: Int = 4000, seed: Long = 1L): Aggregate |
Eden-model (ballistic) growth: repeatedly pick a random empty cell adjacent to the cluster and fill it. Same seed, same neighbourhood, same output type as dlaCluster — the only difference is that a growth site is chosen uniformly instead of by a diffusing walker. |
fun sprinkleCluster(width: Int, height: Int, particles: Int, radius: Double, seed: Long = 1L): Aggregate |
A uniform random sprinkle of particles cells inside a disc of the same radius as a comparable cluster — the second control for Aggregate.fractalDimension, and the one that catches a dimension estimator fooled by a bounded support. |
fun Aggregate.arrivalOrderImg(): Img |
Colour an aggregate by arrival order: the seed at one end of the ramp, the last particle to stick at the other. |
BoidParams
Reynolds' three boid steering weights plus the numbers that bound the motion.
| Declaration | Summary |
|---|---|
val perceptionRadius: Double |
How far an agent sees for alignment and cohesion. |
val separationRadius: Double |
The (smaller) radius inside which it actively pushes away. |
val separationWeight: Double |
Strength of the push away from close neighbours. |
val alignmentWeight: Double |
Strength of the pull toward the mean neighbour heading — set this to 0 and the flock stops being a flock, which is the control arm the polarization claim is made against. |
val cohesionWeight: Double |
Strength of the pull toward the neighbour centroid. |
val maxSpeed: Double |
Speed cap (agents fly at close to this). |
val minSpeedFraction: Double |
Floor on speed as a fraction of maxSpeed. |
val maxForce: Double |
Per-step cap on the steering acceleration; without it a boid in a dense cluster receives an unbounded separation impulse and leaves at implausible speed. |
BoidFlock
A population of boids on a torus of width x height, stored as parallel arrays.
| Declaration | Summary |
|---|---|
val width: Double |
Domain width. |
val height: Double |
Domain height. |
val x: FloatArray |
Agent x positions. |
val y: FloatArray |
Agent y positions. |
val vx: FloatArray |
Agent x velocities. |
val vy: FloatArray |
Agent y velocities. |
val count: Int |
How many agents. |
fun order(): Double |
The flock's current polarization — the alignment order parameter. |
fun neighborDistances(): DoubleArray |
Nearest-neighbour distances under the toroidal metric. See nearestNeighborDistances. |
fun meanSpeed(): Double |
Mean speed — a sanity readout, since a flock whose agents have stalled is not flocking. |
fun copy(): BoidFlock |
An independent copy, so a run can be forked to compare two parameter sets from one initial state. |
override fun toString(): String |
PhysarumParams
Jones' Physarum transport-network model parameters — the "deposit, sense, move" agent whose population reproduces the transport networks Physarum polycephalum grows.
| Declaration | Summary |
|---|---|
val sensorAngle: Double |
Angle between the centre sensor and each side sensor, in radians. |
val sensorDistance: Double |
How far ahead the sensors look, in cells. |
val turnAngle: Double |
How far an agent rotates when a side sensor wins, in radians. |
val stepSize: Double |
How far it moves per step, in cells. |
val deposit: Double |
How much trail it leaves per step. |
val decay: Double |
Fraction of the trail that survives each step (0.9 = 10 % evaporates). |
val blurRadius: Int |
Box-blur radius applied to the trail each step, which is the diffusion term. |
val exclusive: Boolean |
Jones' one-agent-per-cell rule: an agent may only move into a cell no other agent occupies, and otherwise stays put and picks a new random heading. |
val sensing: Boolean |
When false the agent turns at random instead of following the trail — the control arm. Everything else is identical, so a difference in Img.topShare between the two runs is attributable to the sensing and to nothing else. |
PhysarumSwarm
class PhysarumSwarm(val trail: Img, val x: FloatArray, val y: FloatArray, val heading: FloatArray)
A Physarum population over a trail field.
| Declaration | Summary |
|---|---|
val trail: Img |
The deposit field, mutated each step; 1-channel, unbounded above. |
val x: FloatArray |
Agent x positions in cells. |
val y: FloatArray |
Agent y positions in cells. |
val heading: FloatArray |
Agent headings in radians. |
val count: Int |
How many agents. |
val width: Int |
Trail field width in cells. |
val height: Int |
Trail field height in cells. |
override fun toString(): String |
Aggregate
A grown aggregate.
| Declaration | Summary |
|---|---|
val mask: Img |
1-channel binary occupancy of the cluster. |
val points: List<Vec2> |
The occupied cells, in the order they stuck — so growth order is available, not only the final shape. |
val center: Vec2 |
The seed location, which is the origin every radial measurement is taken from. |
val maxRadius: Double |
The furthest occupied cell from center, in cells. |
val walkSteps: Long |
How many random-walk steps were taken in total (the cost, and the thing a naive implementation makes quadratic). |
fun fractalDimension(rMin: Double = 4.0, rMaxFraction: Double = 0.7): MassRadiusFit |
massRadiusDimension of this aggregate, fitted over the range its own size supports. |
override fun toString(): String |
alife_animate.kt
One shared way to animate the alife*.kt family — because every system in it is a time
evolution, and a montage of frozen generations is the one view that hides the thing they are about.
The whole file is Box.animateEvolution plus four small "show a prefix of something" helpers. It sits
apart from the five simulation files for the same reason alife_field.kt does: those files name nothing
from Field's box runtime, and this one is entirely about it (Box, Frames, display).
Pacing is the craft, and it is the reason this is a shared helper rather than a copied loop. The naive animation — one simulation step per displayed frame — is wrong for almost every system here, and wrong in two opposite directions:
- Gray–Scott needs about 6000 explicit-Euler steps to reach its named regime. One step per frame at 20 fps is five minutes of a clip in which nothing happens.
- Conway's glider moves one cell per four generations. Forty steps per frame is a strobe.
So Box.animateEvolution takes the whole evolution — totalSteps — and the seconds it should occupy,
and derives the steps per displayed frame. Live playback uses monotonic time. Frame-synchronous recording
installs an AnimationClock whose time is exactly frameIndex / fps, so the same seed produces the same
ordered simulation states even when browser painting or encoding is slow. Capture performance changes how
long the build takes, never which states enter the movie.
A linear step schedule is not a neutral choice, and for some of these systems it is the wrong one.
Reaction–diffusion and Lenia are log-time processes: the visible change from step 60 to step 600 is
about the same as from step 600 to step 6000. Spend equal steps on equal wall-clock and the clip
front-loads — measured as the mean absolute luma difference between the extracted frames of the
Gray–Scott clip on alife_examples.md, a linear schedule gave 42.09 from the first frame to the
middle and 3.98 from the middle to the last: the reader watches it finish and then waits. So
Box.animateEvolution takes a ramp, and the schedule it drives is geometric rather than linear. See
that parameter for the arithmetic.
The step counter belongs in the frame. Every call site here labels its panels with the step number,
which is not decoration: it is how "did this clip actually reach the regime?" is answered by looking at
the last frame of the recorded mp4 rather than by trusting the schedule. A clip whose final label reads
step 2100 / 6000 is a failed clip, and it says so on its face.
The range policy from alife_field.kt applies with more force here, not less. A colour map whose
range is taken from the frame makes an animation of a growing structure appear to stand still, and makes
a saturating field indistinguishable from a dying one. So every call site passes explicit lo/hi to
Img.colormap. Auto-ranging is a per-figure mistake and a per-frame lie.
src/jvmMain/kotlin/afield/simulation/agents/alife_animate.kt — read the source
Used in: Spatial artificial life
Top-level functions
| Declaration | Summary |
|---|---|
fun <S> Box.animateEvolution(start: S, totalSteps: Int, seconds: Double = 7.0, columns: Int = 1, cellSide: Int = 420, channel: String = "main", framesTarget: Int = 140, ramp: Double = 1.0, step: (S) -> S, panels: (S, Int) -> List<Pair<String, Img>>): Frames |
Animate a stateful system in a box: step it toward a schedule, draw it, repeat, forever. |
fun Img.rowsRevealed(rows: Int): Img |
A copy of this image with every row after rows set to zero — the space-time diagram of a 1-D automaton as it would look at generation rows. |
fun rasterizeFLinesFramed(lines: List<FLine>, reference: List<FLine>, width: Int, height: Int, margin: Int = 4): Img |
rasterizeFLines, but with the framing taken from reference rather than from lines. |
fun flinePrefix(lines: List<FLine>, fraction: Double): List<FLine> |
The first fraction of a turtle's path, in the order the turtle walked it — so an L-system can be animated by drawing, which is what a turtle does, rather than by cutting between derivation depths. |
fun Aggregate.arrivalPrefixImg(upTo: Int): Img |
The first upTo particles of this aggregate, coloured by arrival order on the full cluster's scale. |
alife_bml.kt
The Biham–Middleton–Levine traffic model on the GPU: a torus of cells, each empty or holding one car, advanced by a single full-screen fragment pass into a ping-pong texture.
The rule
There are two kinds of car. Eastbound cars (red) only ever move one cell to the right, southbound cars (blue) only ever move one cell down, and the two take turns: on even steps every eastbound car whose right-hand cell is empty moves into it, all at once; on odd steps the southbound cars do the same downwards. The grid wraps in both directions. Nothing else happens, and no car is ever created or destroyed.
What makes the model famous is that its only parameter, the fraction of cells holding a car, has a sharp transition: at low density the cars sort themselves into diagonal bands that flow freely, at high density a single jam spanning the grid forms and nothing moves. On a 512 × 512 torus, 0.20 reaches a moving fraction of 0.998 and 0.45 reaches 0 by step 1750, while 0.32 is near the transition and decays slowly from flow toward a jam.
One texel is one cell
ShaderBmlSystem holds the state in an RGBA8 ping-pong target: red at 1 is an eastbound car, blue
at 1 a southbound one, black an empty cell. Each frame the pass reads the previous frame's texture
and writes this one, so one frame is one half-step, and a full step (both colours) takes two
frames. An 8-bit target is enough because the values are only ever 0 or 1.
The update is a pure gather. A cell becomes empty if it holds a car of the moving colour and the cell ahead is empty, becomes that car if it is empty and the cell behind holds one, and otherwise keeps what it had. Each cell decides from three texels of the previous frame, so the whole grid advances in parallel with no conflicts and conserves the car count exactly.
The first frame after construction or ShaderBmlSystem.reset ignores the previous texture and
scatters cars instead, each cell independently holding a car with probability
ShaderBmlSettings.density, eastbound with probability ShaderBmlSettings.eastboundFraction.
Getting one on screen
installInto registers the simulation as a pre-target of the render list and blits it to the
window. As with ShaderDlaSystem, constructing it inside a box seeds that box's fragment tab with
the update rule, so the rule arrives as source you can edit. The CPU-side alife family does not
have this model; nothing here reads the grid back.
src/jvmMain/kotlin/afield/simulation/agents/alife_bml.kt — read the source
ShaderBmlSettings
Everything ShaderBmlSystem reads. Most are bound as lambdas, so changing one while it runs takes effect on the next frame; density, eastboundFraction and randomSeed are only read by the scatter, so they take effect at the next ShaderBmlSystem.reset.
| Declaration | Summary |
|---|---|
var name: String |
Prefix for the module, mesh and pre-target names. Two systems in one renderer need two names. |
var width: Int |
Width of the torus in cells, which is also the texture width in pixels. |
var height: Int |
Height of the torus in cells. Must be positive, as must width. |
var density: Float |
The fraction of cells that hold a car. On a 512 × 512 torus 0.20 flows freely, 0.45 locks, and the default sits near the transition between them. |
var eastboundFraction: Float |
The fraction of cars that are eastbound (red); the rest are southbound (blue). |
var running: Boolean |
false holds the grid as it is; true advances it one half-step per frame. |
var randomSeed: Int |
Seed for the scatter; ShaderBmlSystem.reset can override it. |
var probe: String? |
A GLSL expression to hand to fieldProbe(...) once per moving car per half-step, for example "float(p.x + p.y)"; p is the cell the car is leaving. null (the default) leaves the rule uninstrumented. Setting it adds the include and the call to the rule and writes the result into the box's fragment tab, replacing what was there; turn the probe on with fieldProbeHere(). Read once, in the constructor. |
ShaderBmlSystem
A shader-driven BML traffic model: one fragment pass into a ping-pong target, half a step per frame. Build one, call installInto, and it runs. The file overview above describes the rule and the encoding.
| Declaration | Summary |
|---|---|
val renderer: Renderer |
The renderer to build the target and module on; by default the one this box is running in. |
val settings |
The settings this system was built with, after configure ran, and the handle for changing it live. |
var step |
How many half-steps have been recorded since the last reset. Even values move the eastbound cars, odd values the southbound ones, so step / 2 is the model's time. |
val simulation |
The ping-pong pair holding the grid. |
val stepModule: ModulePointer |
The full-screen pass that applies the rule. This is the module a surrounding box adopts. |
override val imageView: ImageView |
The grid as a texture: red is an eastbound car, blue a southbound one. |
override val imageViewArray: List<HasImageView> |
The same single colour attachment, in the list form the array-sampling path asks for. |
override fun render(command: CommandBuffer, index: Int) |
Runs one half-step. Called by whatever the system is registered with; see installInto. |
fun reset(seed: Int = settings.randomSeed) |
Scatter a fresh grid on the next frame from ShaderBmlSettings.density and ShaderBmlSettings.eastboundFraction, and restart step from 0. Passing no seed reuses ShaderBmlSettings.randomSeed. |
fun installInto(renderTarget: RenderTarget = renderer.renderList, withPreview: Boolean = true, previewName: String = "${settings.name}-preview", previewViewport: Rect? = null): ModulePointer? |
Wire the simulation into a render target as a pre-target and, by default, blit it to the window. Returns the preview module, or null with withPreview = false. |
fun blitToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-preview", viewport: Rect? = null, sourceRect: Rect? = null): ModulePointer |
Draw the grid to a target without registering the simulation with it. viewport is where it lands and sourceRect which part of the grid is taken; a small sourceRect over a large viewport is how to see individual cars. |
alife_continuous.kt
Continuous spatial ALife: reaction–diffusion (Gray–Scott / Turing) and Lenia, all shaped as
Img -> Img steps so they compose with the imaging family.
See alife.kt for the family overview.
Every system here is a field, not a lattice of labels, so the measurements that matter are
different from the discrete half: not "is this pattern periodic" but "what shape are its features"
(Img.blobShapes) and "what length scale did it choose" (Img.correlationLength). The one exception,
and the reason the two halves live in the same family, is leniaFromLifeRule: Lenia in its discrete
limit is a life-like CA, and that gives this file's float convolution and alife_ca.kt's integer
neighbour counting a claim they must agree on exactly.
src/jvmMain/kotlin/afield/simulation/agents/alife_continuous.kt — read the source
Used in: Spatial artificial life
Top-level functions
| Declaration | Summary |
|---|---|
fun Img.diffuse(rate: Double = 0.2, border: Border = Border.WRAP): Img |
One explicit-Euler diffusion step: u + rate * Lap(u), on a torus by default. |
fun Img.diffuseTimes(steps: Int, rate: Double = 0.2, border: Border = Border.WRAP): Img |
diffuse applied steps times. |
fun grayScottSeed(width: Int, height: Int, patches: Int = 5, patchSize: Int = 12, seed: Long = 1L): GrayScottState |
Start a Gray–Scott run: u = 1 everywhere, v = 0 except inside a few square seed patches of random noise. |
fun GrayScottState.step(p: GrayScottParams): GrayScottState |
One Gray–Scott timestep, returning a new GrayScottState. |
fun GrayScottState.stepTimes(p: GrayScottParams, steps: Int): GrayScottState |
step applied steps times, returning only the final state. |
fun GrayScottState.filmstrip(p: GrayScottParams, steps: Int, every: Int): List<GrayScottState> |
Every every-th state from this one out to steps, inclusive of both ends — a filmstrip of the run without stepping it twice. |
fun Img.morphology(level: Float? = null, minArea: Int = 4): MorphologyReport |
Measure a 1-channel field's morphology at threshold — the single call every regime claim in this family is made through. |
fun turingPattern(width: Int, height: Int, scale: Double = 1.0, steps: Int = 4000, rho: Double = 0.010, seed: Long = 1L): Img |
A Turing-instability field in the simplest form that shows the essential fact: two species, one diffusing fast and inhibiting, one diffusing slowly and activating, produce a pattern whose wavelength is set by the diffusion ratio and not by the initial condition. |
fun Img.leniaStep(rule: LeniaRule): Img |
One Lenia step over a 1-channel field, toroidal. Returns a new Img. |
fun Img.leniaTimes(rule: LeniaRule, steps: Int): Img |
leniaStep applied steps times. |
fun leniaFromLifeRule(rule: LifeRule): LeniaRule |
A LeniaRule that reproduces the two-state life-like rule rule exactly — Lenia's discrete limit, and the cross-check that ties this file to alife_ca.kt. |
fun leniaCreatures(json: String): List<LeniaCreature> |
Every 2-D creature in a Lenia catalogue in the format of animals.json from github.com/Chakazul/Lenia: a JSON array of entries with code, name, params and cells. Entries without params (the catalogue's section headings) are skipped. |
fun leniaRuleFromCatalogue(radius: Int, t: Double, mu: Double, sigma: Double, shellPeaks: DoubleArray = doubleArrayOf(1.0), kernelCore: Int = 1, growthKind: Int = 1): LeniaRule |
A LeniaRule with the kernel and growth mapping of Chan's reference implementation, as named by a catalogue entry's kn and gn. |
fun leniaCellsFromRle(rle: String): Img |
Decode a 2-D Lenia catalogue cell string into an Img: Chan's run-length encoding, in which . is 0, A..X are 1..24, two-letter pA..yO are 25..255, both scaled by 1/255, a leading count repeats the next value, $ ends a row (a count before it adds empty rows) and ! ends the pattern. Short rows are padded with zeros. |
fun leniaRun(initial: Img, rule: LeniaRule, steps: Int = 200, deathMass: Double = 0.005, saturationMass: Double = 0.80, sampleEvery: Int = 10): LeniaRun |
Run a Lenia rule from initial for steps and classify the outcome. |
fun fieldCentroid(f: Img): Pair<Double, Double> |
Mass-weighted centroid of a field, as (x, y) in cells. (0, 0) for an empty field. |
fun leniaSeed(width: Int, height: Int, rule: LeniaRule, blobRadii: Double = 1.4, seed: Long = 1L): Img |
A Lenia seed: a smooth radial blob of noise of radius rule.radius * blobRadii, centred, on an otherwise empty field. |
fun leniaMuSweep(width: Int, height: Int, mus: DoubleArray, steps: Int = 200, seed: Long = 1L, makeRule: (Double) -> LeniaRule = { LeniaPresets.ring(it) }): List<LeniaRun> |
Sweep mu and report the LeniaPhase and mass at each value — the measured phase diagram of a Lenia rule along its most consequential axis. |
fun fieldMaxAbsDiff(a: Img, b: Img): Double |
Largest absolute per-cell difference between two same-shape fields: an L-infinity norm, and not a sum. This is the residual a claim of "these two implementations agree exactly" asserts to be zero, and a per-cell tolerance compares against it directly. A total would be larger by up to the pixel count, so read the name carefully before choosing a tolerance. |
Top-level values
| Declaration | Summary |
|---|---|
val REACTION_DIFFUSION_LAPLACIAN: Kernel2D |
The 9-point Laplacian with the weights the reaction–diffusion literature uses: 0.05 on the diagonals, 0.2 on the edges, -1 at the centre. |
val GRAY_SCOTT_SPOTS: GrayScottParams |
F = 0.030, k = 0.062 — isolated round spots that neither grow nor merge. |
val GRAY_SCOTT_WORMS: GrayScottParams |
F = 0.037, k = 0.060 — elongated worms that wander and branch into a labyrinth. |
val GRAY_SCOTT_MAZE: GrayScottParams |
F = 0.029, k = 0.057 — a space-filling maze of connected corridors. |
val GRAY_SCOTT_MITOSIS: GrayScottParams |
F = 0.0367, k = 0.0649 — the dividing/sparse-spot regime. |
val GRAY_SCOTT_HOLES: GrayScottParams |
F = 0.039, k = 0.058 — the inverse morphology: round holes in a filled field. |
GrayScottState
class GrayScottState(val u: Img, val v: Img) : afield.ui.visualization.ToHTML
A Gray–Scott reaction–diffusion state: two coupled fields u (the substrate) and v (the autocatalyst), both Imgs of the same shape.
| Declaration | Summary |
|---|---|
val u: Img |
Substrate field, 1-channel; starts at 1 everywhere. |
val v: Img |
Autocatalyst field, 1-channel; starts at 0 except where you seed it. |
val width: Int |
Field width in cells. |
val height: Int |
Field height in cells. |
override fun toString(): String |
|
override fun toHTML(): String |
Inline view: both reagent fields on fixed 0..1 scales plus the morphology reading. Body in alife_field.kt (alifeGrayScottHtml). The scales are fixed because Img's own auto-normalising thumbnail turns an unseeded v = 0 control arm into a rich texture. |
GrayScottParams
Gray–Scott parameters.
| Declaration | Summary |
|---|---|
val feed: Double |
F, the rate fresh substrate is supplied at. |
val kill: Double |
k, the extra rate the autocatalyst is removed at. |
val du: Double |
Substrate diffusion coefficient. |
val dv: Double |
Autocatalyst diffusion coefficient (must be smaller than du for a Turing instability). |
val dt: Double |
Timestep. |
MorphologyReport
What the morphology of a reaction–diffusion field measures as — the numbers a claim about a regime has to be made out of, rather than a look at a thumbnail.
| Declaration | Summary |
|---|---|
val coverage: Double |
Fraction of the field above the threshold. |
val blobCount: Int |
How many connected features there are. |
val meanArea: Double |
Mean feature area in cells. |
val medianIsoperimetric: Double |
Median P²/4πA over the features: ~1 for round spots, large for worms. |
val correlationLength: Double |
The length scale from Img.correlationLength. |
val largestShare: Double |
Share of the covered area held by the single largest feature — the number that separates "many separate spots" from "one connected labyrinth". |
override fun toString(): String |
LeniaRule
A Lenia rule: a continuous cellular automaton. One field A in 0..1, one convolution kernel K, one growth mapping G, and the update
| Declaration | Summary |
|---|---|
val radius: Int |
R, kernel radius in cells. |
val dt: Double |
1/T in the usual notation — the integration step. Smaller is smoother and slower. |
val mu: Double |
Growth mapping centre: the neighbourhood mean that produces maximum growth. |
val sigma: Double |
Growth mapping width. |
val shellPeaks: DoubleArray |
Relative weights of the concentric kernel shells. |
val shellWidth: Double |
Gaussian cross-section width of each shell, as a fraction of the shell spacing. |
val growth: (Float) -> Float |
The growth mapping G, defaulting to the standard bell 2*exp(-(u-mu)²/(2σ²)) - 1; overridden by leniaFromLifeRule to recover a discrete rule. |
val kernelTaps: LeniaTaps? |
|
val taps: LeniaTaps |
The kernel as a sparse tap list: (dx, dy, weight) for every tap whose weight is worth evaluating, weights summing to 1 — kernelTaps when one was supplied, otherwise built from the shell parameters. |
fun kernelImg(): Img |
The kernel rendered as an Img for inspection — the picture that explains the rule. |
fun growthCurve(samples: Int = 256): DoubleArray |
The growth mapping sampled over 0..1, for plotting it beside the kernel. |
override fun toString(): String |
LeniaTaps
class LeniaTaps(val dx: IntArray, val dy: IntArray, val weight: FloatArray)
A Lenia kernel as parallel arrays of offsets and weights.
| Declaration | Summary |
|---|---|
val dx: IntArray |
X offsets. |
val dy: IntArray |
Y offsets. |
val weight: FloatArray |
Normalised weights, summing to 1. |
val size: Int |
Number of taps. |
LeniaPresets
Named continuous-Lenia parameter sets, and the one published lifeform this family ships.
| Declaration | Summary |
|---|---|
fun orbium(): LeniaCreature |
Orbium unicaudatus, the best-known Lenia lifeform: a single crescent-shaped creature, about twenty cells across, that glides in a straight line and keeps its shape. Rule R = 13, T = 10, mu = 0.15, sigma = 0.015, with the polynomial kernel and growth mapping its catalogue entry names. |
fun ring(mu: Double, sigma: Double = 0.017, radius: Int = 12, dt: Double = 0.1): LeniaRule |
The reference rule shape — one ring, R = 12, T = 10 — with mu left for a sweep. |
fun threeShell(mu: Double, sigma: Double = 0.025, radius: Int = 12, dt: Double = 0.1): LeniaRule |
A three-shell kernel, the shape Lenia's richer lifeforms use. |
LeniaCreature
class LeniaCreature(val code: String, val name: String, val rule: LeniaRule, val cells: Img)
A named Lenia lifeform: its rule and the initial cells that make it, as published.
| Declaration | Summary |
|---|---|
val code: String |
The catalogue's short code (O2u for Orbium). |
val name: String |
The catalogue's name. |
val rule: LeniaRule |
|
val cells: Img |
The creature's own array, values in 0..1, at the size the rule's radius expects. |
fun seed(width: Int, height: Int, cx: Int = width / 2, cy: Int = height / 2): Img |
cells placed with its centre at (cx, cy) on an otherwise empty width × height field. |
fun shuffled(seed: Long = 1L): LeniaCreature |
The same cell values in a random arrangement, under the same rule: the control for "the shape is the creature". It has the creature's mass and its histogram of values, and nothing else. |
override fun toString(): String |
LeniaPhase
Which of the three things a Lenia run can do.
Constants: DEATH, BOUNDED, SATURATION.
LeniaRun
The outcome of a Lenia run, as measured rather than as looked at.
| Declaration | Summary |
|---|---|
val phase: LeniaPhase |
Which of LeniaPhase the run ended in. |
val finalMass: Double |
Mean field value at the end (0 = empty, 1 = saturated). |
val massHistory: DoubleArray |
Mean field value at each recorded step, so the shape of the approach is inspectable and not only its endpoint. |
val blobCount: Int |
Connected features at the end. |
val centroidDrift: Double |
Distance the field's centre of mass moved over the last half of the run, in cells; a stationary pattern is ~0 and a travelling soliton is proportional to the run length. |
val field: Img |
The final field. |
override fun toString(): String |
|
override fun toHTML(): String |
Inline view: the final field on a fixed 0..1 scale, the measured phase, and the mass history as a sparkline. Body in alife_field.kt (alifeLeniaRunHtml). A saturated field is constant at 1 and an auto-ranged colour map draws it black, i.e. identical to death — so this view is the one place in the family where the range policy changes the result rather than tidying it. |
alife_dla.kt
Diffusion-limited aggregation on the GPU: a buffer of random walkers stepped, stuck and drawn by a single vertex shader, with the growing cluster held in a texture rather than in any Kotlin data structure.
The CPU version of the same process is dlaCluster in alife_agents.kt, and the two answer
different questions. dlaCluster returns points you can measure, with massRadiusDimension and a
control arm to measure them against. What is here returns an image, at a walker count the CPU
version cannot reach, and nothing reads it back. Measure DLA with alife_agents.kt; watch one grow
at window resolution with this.
The state is three channels of one texture
ShaderDlaSystem renders into a single RGBA16F ping-pong target, and every part of the simulation
except the walkers themselves lives in its channels:
- red is solidity, and a pixel counts as part of the cluster once red reaches
solidThreshold; - green marks where the walkers are this frame, and nothing carries it forward;
- blue is age, rising while a pixel is solid and falling back when it stops being, which is what a colour ramp reads if you want tips to look younger than the interior.
Two draws per frame, and no compute pass
The target is cleared at the start of every frame, so a full-screen preserve pass runs first and
rebuilds the carried state by sampling the previous frame's image: red decays, blue grows or
decays, and the seed disc is held solid. The particle pass then draws one additively blended point
per walker. Both read the previous frame rather than the one being built, and they run in the order
they were added to the target.
All of the simulation is in that second pass's vertex shader. It reads its walker from an addressable buffer through a push-constant pointer, probes the four neighbours of its pixel in the previous frame's red channel, and either sticks where it stands or takes one axis-aligned random step; then it writes the walker back. Drawing the point and advancing the simulation are the same draw call.
A stuck walker lasts one frame
A walker that sticks emits one red splat and is respawned at a uniformly random position on the
next frame, so particleCount bounds how many walkers are in flight rather than how large the
cluster can get. The cluster is only ever the red channel, which is also why it is mortal: red
loses solidDecayPerFrame every frame whether or not anything is still attaching to it, so at the
defaults a pixel that took one splat and nothing since falls back under the threshold about 500
frames later.
Getting one on screen
installInto registers the simulation as a pre-target of the render list and blits the result to
the window. Constructing the system inside a box also seeds that box's shader properties from the
particle pass, so the walk arrives as source you can edit rather than as a closed component; the
diffusion-limited aggregation section of
afield_four_graphics.md is the worked version of that, and says why
it carries no figure.
src/jvmMain/kotlin/afield/simulation/agents/alife_dla.kt — read the source
ShaderDlaSettings
Everything ShaderDlaSystem reads, from texture size down to the two colours a walker writes.
| Declaration | Summary |
|---|---|
var name: String |
Prefix for everything this system registers by name: its two modules, their meshes, the particle buffer, the barrier, and the entries it makes in a render target's pre-target and module maps. Two systems in one renderer need two names. |
var width: Int |
Width of the simulation texture in pixels, which is also the coordinate system the walkers step in: positions are pixels, not normalised. |
var height: Int |
Height of the simulation texture in pixels. Must be positive, as must width. |
var particleCount: Int |
How many walkers are in flight at once, one 16-byte record each. Because a stuck walker is respawned immediately this is a growth rate rather than a bound on the cluster, and it trades off against width and height: too few walkers for the area and the cluster takes thousands of frames to become visible, too many and the whole field sticks in the first few. |
var pointSize: Float |
gl_PointSize for the drawn walkers and splats. 1 draws a single pixel each. |
var walkStepPx: Float |
How far a walker moves each frame, along one of the four axis directions. |
var sampleStepPx: Float |
How far from a walker the four neighbour probes are taken, rounded and floored to one pixel. Raising it lets walkers stick before they touch, which thickens the branches. |
var solidThreshold: Float |
The red value at which a pixel counts as solid. Both the neighbour probe and the age channel ask this question, so it is one threshold for sticking and for colouring. |
var walkerGreen: Float |
The green a walker adds to the pixel it is standing on. Nothing reads green back, and the preserve pass does not carry it forward, so this is a live view of the diffusing cloud. |
var stuckRed: Float |
The red a walker adds at the moment it sticks. This is the only thing that grows the cluster, and it is added to what is already there, so a pixel several walkers have found reads above 1 and takes proportionally longer to decay away. |
var stuckBlue: Float |
The blue added alongside stuckRed, so a new branch tip starts with some age rather than none. |
var seedPositionPx: Vector2f? |
Centre of the disc the preserve pass holds solid, in pixels; null puts it at the middle of the texture. Something has to be solid before anything can stick to it, so this is where the cluster starts. Resolved once in the constructor, unlike the rest of these. |
var seedRadiusPx: Float |
Radius of that disc in pixels. |
var solidDecayPerFrame: Float |
How much red every pixel outside the seed disc loses per frame. At 0 the cluster is permanent; at the default of 0.001 a branch fades once walkers stop reinforcing it, which is what keeps a long-running field from filling in. |
var ageDecayPerFrame: Float |
How much blue a pixel below solidThreshold loses per frame. |
var ageGrowPerFrame: Float |
How much blue a solid pixel gains per frame, up to 1. At the default that is 100 frames to full age. |
var seedBlue: Float |
The blue the seed disc is held at, so the oldest part of the cluster reads as old from frame one. |
var format: Int |
Colour format of the ping-pong target. A float format is doing work here: solidDecayPerFrame at its default is smaller than one step of an 8-bit channel, so an 8-bit target would round each frame's decay away and nothing would ever fade. |
var clearColor: Vec4 |
Clear value for the target's colour attachment. The preserve pass covers every pixel, so this is what the first frame starts from rather than a background that stays visible behind the cluster. |
var randomSeed: Long |
Seed for the initial scatter of walkers, handed to ShaderDlaSystem.resetParticles at construction. |
ShaderDlaSystem
A shader-driven DLA system: two draws into a ping-pong target, with the walkers in an addressable buffer and the cluster in the target's red channel.
| Declaration | Summary |
|---|---|
val renderer: Renderer |
The renderer to build the target, modules and buffer on; by default the one this box is running in. |
val settings |
The settings this system was built with, after configure ran. Most of them are read fresh from here every frame, so this is the handle for changing the simulation while it runs. |
val particleBuffer: FrameBufferGroup |
The walkers: position, RNG state and a stuck flag, 16 bytes each, host-visible and addressable so the vertex shader can reach them through a push-constant pointer. It is one allocation shared by all frames in flight rather than one per frame, which is what makes the walk persist and what the buffer barrier in front of the particle pass is for. |
val simulation |
The ping-pong target holding the cluster. Both passes render into it and both sample the frame before. |
val preserveModule: ModulePointer |
The full-screen pass that carries the cluster forward: it samples the previous frame, decays red and blue, and holds the seed disc solid. Without it the per-frame clear would erase everything the walkers have built. |
val particleModule: ModulePointer |
The point-list pass that both advances and draws the walkers. This is the module a surrounding box adopts, so its GLSL is what appears in the box's shader tabs. |
val particleMesh: Mesh |
One point per walker, and where the settings reach the shader: every push. constant the walk reads is bound here as a lambda over settings. |
override val imageView: ImageView |
The cluster as a texture, so a system can be sampled by any other module the way a loaded image would be. Red is solidity, blue is age, and green is whatever the walkers did this frame. |
override val imageViewArray: List<HasImageView> |
The same single colour attachment, in the list form the array-sampling path asks for. |
override fun render(command: CommandBuffer, index: Int) |
Runs both passes for one frame. Called by whatever the system was registered with rather than by hand; installInto is the usual way to arrange that. |
fun resetParticles(seed: Long = lastRandomSeed) |
Scatter the walkers uniformly again, all of them unstuck. The cluster is in the texture and is untouched by this, so it restarts the diffusing half of the process against the structure already grown. Called once at construction with ShaderDlaSettings.randomSeed; passing no seed reuses the last one. |
fun installInto(renderTarget: RenderTarget = renderer.renderList, withPreview: Boolean = true, previewName: String = "${settings.name}-preview", previewViewport: Rect? = null): ModulePointer? |
Wire the simulation into a render target and, by default, show it. The system is added as a pre-target, so both of its passes run before that target draws its own contents, and the result is then blitted to the window. |
fun blitToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-preview", viewport: Rect? = null, sourceRect: Rect? = null, blend: Boolean = false, additive: Boolean = false): ModulePointer |
Draw the cluster to a target without registering the simulation with it, for a second view of a system installInto is already running, or for placing the picture by hand. |
alife_evolve.kt
The search half of this family: rule sets as genomes, the measurements in alife.kt as
fitness, and PopulationSearch as the engine that connects them.
Until this file existed every figure on the ALife page was "run a fixed rule and measure it". The
measuring vocabulary was already here and was already good — stateEntropyBits,
massRadiusDimension, blobShapes, translationBetween, LeniaRun.centroidDrift — and each of
those is a fitness function with nobody calling it. This file supplies the missing halves: a
GenomeOps for three families (LifeRuleGenome, LeniaGenome, LSystemGenome) and an
Objective for each, assembled out of statistics that were already in the file next door.
There is no search code here. Every driver comes from PopulationSearch, which is also what
FunctionEvolve.evolve runs on. That is the point: one engine, several genomes, rather than a
third private optimizer.
Every objective in this file evaluates every candidate from the same fixed initial condition.
A Lenia rule is seeded from leniaSeed(..., seed = initialSeed), a CA rule from the same
randomSoup, an L-system from the same axiom. A search that re-randomised the initial condition
per candidate would be selecting on the luck of the draw, and the resulting "improvement" would be
mostly the search rediscovering that some soups are easier than others. Where robustness across
initial conditions is the question, caEdgeOfChaosObjective takes several soups and reports one
Evaluation.cases entry each, which is what makes Selection.LEXICASE meaningful here.
src/jvmMain/kotlin/afield/simulation/agents/alife_evolve.kt — read the source
Used in: Spatial artificial life, Agent transcript gallery, Braitenberg vehicles with memory
Top-level functions
Top-level values
| Declaration | Summary |
|---|---|
const val EXTINCT_SCORE: Double |
What an extinct run scores: strictly below any survivor, and the same number everywhere. |
LifeRuleGenome
A life-like LifeRule as a genome: the birth and survival sets, over a fixed neighbourhood.
| Declaration | Summary |
|---|---|
val radius: Int |
|
val vonNeumann: Boolean |
|
val birthDensity: Double |
Probability a given count is in the birth set of a freshly drawn genome. |
val surviveDensity: Double |
Probability a given count is in the survival set of a freshly drawn genome. |
val maxCount: Int |
Highest neighbour count the fixed neighbourhood can produce. |
val birthCounts: IntRange |
Counts a birth bit may be set at: 1..maxCount, excluding 0. See the class KDoc. |
val surviveCounts: IntRange |
Counts a survival bit may be set at: 0..maxCount. |
override fun random(rng: Random): LifeRule |
|
override val mutations: List<Variation<LifeRule>> |
|
override fun crossover(a: LifeRule, b: LifeRule, rng: Random): LifeRule |
Uniform crossover: every bit of the table comes from one parent or the other, independently. |
override fun signature(g: LifeRule): String |
CaRunSummary
What one CA rule did to one soup: where its density settled and how much it was still changing.
| Declaration | Summary |
|---|---|
val density: Double |
Fraction of live cells at the end. |
val activity: Double |
Mean fraction of cells that changed state per step, over the scored window. |
val entropyBits: Double |
IntGrid.stateEntropyBits of the final grid. |
val extinct: Boolean |
true if the grid emptied. |
val finalGrid: IntGrid |
The grid the run ended on, so a figure can show what was selected. |
override fun toString(): String |
LeniaGenome
A LeniaRule as a genome: mu, sigma and the kernel's shell weights, over a fixed radius and time step.
| Declaration | Summary |
|---|---|
val radius: Int |
|
val dt: Double |
|
val shells: Int |
|
val muRange: ClosedFloatingPointRange<Double> |
|
val sigmaRange: ClosedFloatingPointRange<Double> |
|
override fun random(rng: Random): LeniaRule |
|
override val mutations: List<Variation<LeniaRule>> |
|
override fun crossover(a: LeniaRule, b: LeniaRule, rng: Random): LeniaRule |
|
override fun signature(g: LeniaRule): String |
Rounded to five decimals. Two Lenia rules whose parameters agree to that precision produce the same kernel taps to well inside float precision, so calling them the same phenotype is a statement about the simulation and not a convenience. |
LSystemGenome
An LSystem as a genome: one production per rewritten symbol, over a fixed turtle alphabet.
| Declaration | Summary |
|---|---|
val axiom: String |
|
val symbols: String |
|
val alphabet: String |
The characters a replacement may be built from. |
val minLength: Int |
|
val maxLength: Int |
|
override fun random(rng: Random): LSystem |
|
override val mutations: List<Variation<LSystem>> |
|
override fun crossover(a: LSystem, b: LSystem, rng: Random): LSystem |
Single-point crossover per production: a prefix from one parent, a suffix from the other. |
override fun signature(g: LSystem): String |
LSystemDrawing
What one L-system grammar drew: the branching structure and the two numbers it is scored on.
| Declaration | Summary |
|---|---|
val dimension: Double |
Mass–radius dimension of the drawn points, from massRadiusDimension. |
val rSquared: Double |
How straight the log–log fit was. Below ~0.95 the object has no scaling regime over this range and dimension is a slope through a curve, which is not a dimension. |
val totalLength: Double |
flineTotalLength of the drawing. |
val branches: Int |
How many separate drawn runs the turtle produced. |
val segments: Int |
|
val raster: Img |
The drawing, rasterised, ready for a contact sheet. |
val drawable: Boolean |
false when the grammar drew too little to measure; every number above is then meaningless and the objective says so instead of fitting three points. |
override fun toString(): String |
VehicleWiringGenome
class VehicleWiringGenome(val maxWeight: Double = 3.0) : GenomeOps<DoubleArray>
A Braitenberg vehicle's four sensor→motor weights as a genome, laid out as VehicleWorld.w lays out one vehicle (w[2m + s], sensor s to motor m), each within ±maxWeight.
| Declaration | Summary |
|---|---|
override fun random(rng: Random): DoubleArray |
|
override val mutations: List<Variation<DoubleArray>> |
|
override fun crossover(a: DoubleArray, b: DoubleArray, rng: Random): DoubleArray |
|
override fun signature(g: DoubleArray): String |
Rounded to two decimals: weights closer than that drive indistinguishably over a fitness run. |
alife_field.kt
Inline ToHTML views for the alife*.kt family — what a box draws when you evaluate an IntGrid
or a GrayScottState, with nothing to set up.
alife.kt and alife_continuous.kt name only the three entry points here (alifeGridHtml,
alifeGrayScottHtml, alifeLeniaRunHtml), each from a one-line toHTML override, and inherit
every pixel of their view from them.
The range policy:
- Every scale is named in the caption, with both endpoints spelled out in the units of the thing being shown.
- Domains are fixed by meaning, not by the frame. An
IntGrid's states run0 .. maxState; a Gray–Scottvruns0 .. 1, the range the reaction is defined on, notmin .. maxof this particular field. Auto-range a saturated Lenia field — constant at 1 — and it maps to the bottom of the ramp: solid black, identical to a field that has died. - One pixel per cell, never resampled. The
<img>is scaled by an integer factor withimage-rendering:pixelated, so cell coordinates and pixel coordinates stay the same thing and a reader can count. A filtered upscale destroys exactly the local structure a CA is about. - A flat field says so. If every cell holds the same value the caption reports it, because a uniform panel is otherwise indistinguishable from a broken one.
AlifeGallery's inline-view figure decodes the base64 PNG back out of the emitted HTML and
asserts the colour of individual pixels against the value the caption claims for them.
src/jvmMain/kotlin/afield/simulation/agents/alife_field.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun alifeGridHtml(grid: IntGrid): String |
The body of IntGrid.toHTML: the lattice itself, one pixel per cell, plus the two readings that say whether it is doing anything — population and state entropy. |
fun alifeGrayScottHtml(state: GrayScottState): String |
The body of GrayScottState.toHTML: both reagent fields side by side on fixed 0 .. 1 scales, plus the morphology reading that the docs page's regime claims are actually made through. |
fun alifeLeniaRunHtml(run: LeniaRun): String |
The body of LeniaRun.toHTML: the final field on a fixed 0 .. 1 scale, the measured phase, and the mass history as a sparkline. |
alife_fire.kt
Fire spreading through a forest on the GPU: a fixed scatter of trees, fire that you light by drawing into a texture, and four kinds of fire, one per colour channel, that compete for the same trees.
The rule
Each texel is a tree or bare ground, decided once by a hash of its position and
ShaderForestFireSettings.randomSeed, so the forest is the same every run with the same seed. A
tree is ready, burning or recovering, and which it is lives in one number per kind of fire: 1
the frame it catches, then falling by that kind's ShaderForestFireSettings.refractoryDecayPerFrame
each frame. Above ShaderForestFireSettings.activeFireThreshold it is burning; below
ShaderForestFireSettings.readyThreshold it can catch again; in between it is recovering and
cannot. A ready tree catches when a burning neighbour's fire, times
ShaderForestFireSettings.spreadGain, reaches ShaderForestFireSettings.ignitionThreshold and a
random draw against ShaderForestFireSettings.spreadProbability lets it across, or when the
ignition texture at that texel reaches the threshold. If several kinds could take it, the strongest wins, and
catching one kind sets the other three to zero.
Because trees recover, this is an excitable medium rather than a fire that leaves ash: a front passes, the forest behind it recovers, and a second front can follow the first. A front broken by bare ground curls at the break, and steady ignition makes a train of rings.
What is in each texture
ShaderForestFireSystem.state holds the four fire levels in RGBA; it is what imageView returns.
ShaderForestFireSystem.fireInput is where ignition is drawn, RGBA one kind per channel, carried
forward and faded by ShaderForestFireSettings.ignitionDecayPerFrame.
ShaderForestFireSystem.ink is a colour that burning trees pass on to their neighbours, so that what
you draw into it travels with the fire. The preview colours each kind red, green, blue or magenta,
tinted by the ink.
Lighting it
Nothing burns until something is lit. Draw into ShaderForestFireSystem.fireInput with a module
built on ShaderForestFireSystem.fireInputParameters, which blends additively: a fragment writing
vec4(1, 0, 0, 0) lights the red kind there. As with ShaderDlaSystem, constructing the system
inside a box seeds that box's fragment tab with the spreading rule, so the rule arrives as source
you can edit.
src/jvmMain/kotlin/afield/simulation/agents/alife_fire.kt — read the source
FireChannels
data class FireChannels(val r: Float = 0f, val g: Float = 0f, val b: Float = 0f, val a: Float = 0f)
Four numbers, one per kind of fire, red to alpha.
| Declaration | Summary |
|---|---|
val r: Float |
Kind 0, drawn red. |
val g: Float |
Kind 1, drawn green. |
val b: Float |
Kind 2, drawn blue. |
val a: Float |
Kind 3, drawn magenta. |
operator fun times(scale: Float) |
All four scaled. |
fun toVec4(): Vec4 |
As a vec4, for a push constant. |
ShaderForestFireSettings
Everything ShaderForestFireSystem reads. All but the sizes and format are bound as lambdas, so changing one while it runs takes effect on the next frame; changing the forest itself (treeDensity, treeScalePx, treeJitter, randomSeed) regrows it in place.
| Declaration | Summary |
|---|---|
var name: String |
Prefix for module, mesh and pre-target names. Two systems in one renderer need two names. |
var width: Int |
Forest width in texels. |
var height: Int |
Forest height in texels. |
var format: Int |
Format of all three textures. |
var clearColor: Vec4 |
Clear colour of every texture. Transparent black, because alpha is not opacity here: it is the fourth kind of fire, and an opaque clear would light all of it at once. |
var treeDensity: Float |
The fraction of texels that are trees. Below about 0.4 fire stops crossing the gaps. |
var treeScalePx: Float |
Size in texels of the coarse cells the forest is hashed on, so trees grow in clumps. |
var treeJitter: Float |
How much per-texel noise is mixed into the clumps, 0 to 1. |
var spreadStepPx: Float |
How far, in texels, a tree looks for burning neighbours. |
var spreadGain: Float |
Gain on a burning neighbour's fire before it is compared with ignitionThreshold. |
var spreadProbability: Float |
The chance, each frame, that one burning neighbour lights a ready tree. At 1 every front moves one texel a frame in all eight directions and comes out square; below 1 it moves by chance, slower on average, and rounds itself into a ragged circle. |
var paintGain: Float |
Gain on the ignition texture before it is compared with ignitionThreshold. |
var ignitionThreshold: Float |
The signal at or above which a ready tree catches. |
var activeFireThreshold: Float |
The fire level at or above which a tree is burning. |
var readyThreshold: Float |
The fire level at or below which a tree can catch again. |
var refractoryDecayPerFrame: FireChannels |
How fast each kind's fire level falls per frame. With the default thresholds red burns for 18 frames and is ready again after 97; alpha takes two and a half times as long. |
var ignitionDecayPerFrame: FireChannels |
How fast the ignition texture fades per frame, per kind. |
var inkDecayPerFrame: FireChannels |
How fast the ink fades per frame, per channel. |
var inkSpreadGain: Float |
The fraction of a burning neighbour's ink a burning tree takes on. |
var randomSeed: Long |
Seed for the forest. |
ShaderForestFireSystem
A forest-fire excitable medium: ignition, fire state and ink, each a full-screen pass into its own ping-pong target. Build one, call installInto, then light it with a module from fireInputParameters. The file overview above describes the rule and the encoding.
| Declaration | Summary |
|---|---|
val renderer: Renderer |
The renderer to build the targets and modules on; by default the one this box is running in. |
val settings |
The settings this system was built with, after configure ran, and the handle for changing it live. |
val fireInput |
Where ignition is drawn, one kind of fire per channel. Add modules to it with addFireInput. |
val state |
The four fire levels per tree. This is what imageView returns. |
val ink |
Colour carried from tree to tree by the fire. Draw into it with addInk. |
val fireInputPreserveModule: ModulePointer |
The pass that carries fireInput forward a frame and fades it. |
val stateUpdateModule: ModulePointer |
The spreading rule. The box adopts this one. |
val inkPreserveModule: ModulePointer |
The pass that carries ink forward a frame and fades it. |
val inkPropagateModule: ModulePointer |
The pass that hands ink from burning trees to their burning neighbours. Always runs last on ink. |
override val imageView: ImageView |
The fire levels, so a system can be sampled by another module the way a loaded image would be. |
override val imageViewArray: List<HasImageView> |
The same texture, in the list form the array-sampling path asks for. |
override fun render(command: CommandBuffer, index: Int) |
Runs ignition, the spreading rule and the ink, in that order. Called by whatever the system is registered with; see installInto. |
fun clearFireInput() |
Erase all drawn ignition on the next frame. |
fun clearState() |
Put every tree out and make it ready on the next frame. |
fun clearInk() |
Erase the ink on the next frame. |
fun reset() |
clearFireInput, clearState and clearInk together. |
fun addFireInput(name: String, module: ModulePointer) |
Add a module that draws into fireInput; it runs after the carry-forward pass, every frame. |
fun addFireInput(name: String, module: Module) |
addFireInput for a module that has not been given a pointer yet. |
fun addInk(name: String, module: ModulePointer) |
Add a module that draws into ink; it runs before the propagate pass, so the fire carries what it draws. |
fun addInk(name: String, module: Module) |
addInk for a module that has not been given a pointer yet. |
fun fireInputParameters(name: String, configure: Parameters.() -> Unit = {}): Parameters |
Parameters for a module that draws into fireInput: additive blending, so writing vec4(1, 0, 0, 0) adds red fire and a negative value erases it (the target is floating point), and the previous ignition available as sourceTex. Hand the result to moduleHere(...) in a box, then to addFireInput. |
fun inkParameters(name: String, configure: Parameters.() -> Unit = {}): Parameters |
Parameters for a module that draws into ink, additive, with the previous ink as sourceTex. |
fun installInto(renderTarget: RenderTarget = renderer.renderList, withPreview: Boolean = true, previewName: String = "${settings.name}-preview", previewViewport: Rect? = null): ModulePointer? |
Wire the simulation into a render target as a pre-target and, by default, show it with blitToScreen. Returns the preview module, or null with withPreview = false. |
fun blitToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-preview", viewport: Rect? = null): ModulePointer |
Draw the forest to a target: each kind of fire in its colour, bright while burning and dim while recovering, tinted by the ink. viewport is where it lands. |
fun blitFireInputToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-fire-input-preview", viewport: Rect? = null, sourceRect: Rect? = null): ModulePointer |
Draw the raw ignition texture. |
fun blitInkToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-ink-preview", viewport: Rect? = null, sourceRect: Rect? = null): ModulePointer |
Draw the raw ink texture. |
alife_lbm.kt
A two-dimensional fluid on the GPU by the lattice Boltzmann method, with a control texture you draw obstacles, forces and dye sources into, and a dye field that the flow carries.
The method
Every texel is a lattice site holding nine numbers, the D2Q9 populations: how much fluid at that
site is moving with each of the nine velocities (0,0), the four axis neighbours and the four
diagonals. A step is two moves. Collision relaxes each site's populations a fraction
ShaderLatticeBoltzmannSettings.relaxation of the way toward the equilibrium for that site's own
density and velocity (the BGK operator); streaming then moves each population one site along its
direction. A population that would stream out of a solid site is reflected back where it came from
instead, which is the no-slip wall. Density and velocity are never stored as such; they are sums
over the nine populations. The viscosity follows from the relaxation, (1/relaxation - 0.5) / 3 in
lattice units, so values near 2 are runny and turbulent and values near 1 are syrupy.
The method is only accurate while the fluid is slow against the lattice, below about 0.1 sites per
step; ShaderLatticeBoltzmannSettings.maxVelocity clamps it so that a strong push degrades the
physics rather than the numbers.
What is in each texture
ShaderLatticeBoltzmannSystem.fluid holds the populations in three RGBA32F attachments, written by
one pass: attachment 0 holds directions 0–3, attachment 1 directions 4–7, and attachment 2 holds
direction 8 in red, then density, velocity x and velocity y, so attachment 2 is also the readable
velocity field. ShaderLatticeBoltzmannSystem.controls holds, per texel, red obstacle (solid at or
above ShaderLatticeBoltzmannSettings.solidThreshold), green and blue a velocity to steer toward,
and alpha a dye source. ShaderLatticeBoltzmannSystem.dye is colour advected by the velocity and
fed by the dye source, by speed and by vorticity.
Every frame runs the controls (carried forward, then whatever you have added), then
ShaderLatticeBoltzmannSettings.stepsPerFrame fluid steps, then the dye.
Driving it
A fresh system is still water: nothing moves until something pushes. There are two ways. Set
ShaderLatticeBoltzmannSettings.baseVelocity for a wind everywhere, which with
ShaderLatticeBoltzmannSettings.wrapX makes a wind tunnel; or draw into the controls texture with
a module built on ShaderLatticeBoltzmannSystem.controlsParameters, which blends additively, so a
fragment that writes vec4(1, 0, 0, 0) marks a texel solid and vec4(0, 0.2, 0, 1) pushes it right
and releases dye there. The controls texture is carried from frame to frame, so a module that draws
every frame accumulates; ShaderLatticeBoltzmannSettings.obstacleDecayPerFrame and its siblings let
old marks fade.
As with ShaderDlaSystem, constructing it inside a box seeds that box's fragment tab with the
collision-and-streaming rule, so the physics arrives as source you can edit.
src/jvmMain/kotlin/afield/simulation/agents/alife_lbm.kt — read the source
Used in: Field 4 Graphics
FluidVisualizationMode
enum class FluidVisualizationMode
What ShaderLatticeBoltzmannSystem.blitToScreen draws.
Constants: COMPOSITE, DYE, VELOCITY, SPEED, DENSITY, CURL, CONTROLS.
ShaderLatticeBoltzmannSettings
Everything ShaderLatticeBoltzmannSystem reads. All but the sizes and formats are bound as lambdas, so changing one while it runs takes effect on the next frame.
| Declaration | Summary |
|---|---|
var name: String |
Prefix for module, mesh and pre-target names. Two systems in one renderer need two names. |
var width: Int |
Lattice width in sites, which is also the texture width in pixels. |
var height: Int |
Lattice height in sites. |
var format: Int |
Format of the controls and dye textures. |
var populationFormat: Int |
Format of the three population attachments. 32-bit, because the populations are small numbers whose differences carry the velocity, and half precision rounds slow flows away. |
var clearColor: Vec4 |
Clear colour of every texture on the first frame. |
var stepsPerFrame: Int |
Lattice steps per frame. The fluid moves at most a fraction of a site per step, so at one step a frame a flow takes thousands of frames to cross the lattice; this is the speed of the film, not of the physics. The dye is advected once per frame, the whole distance. |
var relaxation: Float |
The BGK relaxation rate ω, in (0, 2): how far each collision moves the populations toward equilibrium. Viscosity is (1/ω - 0.5) / 3, so larger is thinner and less stable. |
var baseDensity: Float |
The density the fluid starts at and that solid sites are held at. |
var baseVelocity: Vector2f |
A wind applied everywhere, in sites per step; the fluid starts moving at this velocity. |
var initialNoise: Float |
Random velocity, in sites per step, added to every site when the fluid starts or is cleared. A symmetric setup computed on a GPU stays exactly symmetric, so without it a wake that ought to shed vortices can sit still for ever; the default is small enough not to show. |
var driveBlend: Float |
How far each site's velocity is pulled toward baseVelocity each step, 0 to 1. This is what keeps the wind blowing against the drag of obstacles, and also what erases a wake: a disturbance fades over about 1 / driveBlend steps, so small values let wakes travel far. |
var controlVelocityScale: Float |
Scale from the controls texture's green and blue to a velocity added each step. A push held in place keeps accelerating the fluid there until maxVelocity stops it. |
var maxVelocity: Float |
The speed no site may exceed, in sites per step. Above about 0.3 the method becomes unstable. |
var wrapX: Boolean |
Whether the lattice wraps left to right; otherwise the edges clamp. |
var wrapY: Boolean |
Whether the lattice wraps top to bottom. |
var borderSolidPx: Float |
A solid frame this many pixels thick around the edge; 0 for none. |
var solidThreshold: Float |
The controls' red value at or above which a site is solid. |
var obstacleDecayPerFrame: Float |
How much each obstacle mark fades per frame. |
var directionDecayPerFrame: Float |
How much each steering mark fades per frame, toward zero from either side. |
var dyeSourceDecayPerFrame: Float |
How much each dye-source mark fades per frame. |
var controlDirectionLimit: Float |
The largest steering value the controls texture keeps, either sign, so accumulated pushes saturate. |
var dyeDecayPerFrame: Float |
The fraction of the dye that fades each frame. |
var dyeAdvectionScale: Float |
How far, in multiples of the velocity times stepsPerFrame, the dye is carried each frame. |
var controlDyeGain: Float |
Dye released per frame by a full dye-source mark. |
var speedDyeGain: Float |
Dye released by speed above the wind. |
var curlDyeGain: Float |
Dye released by vorticity, which is what makes vortices draw themselves. |
var dyeLimit: Float |
The brightest the dye may get. |
var speedVisualizationGain: Float |
Gain from speed to brightness in the speed and velocity views. |
var curlVisualizationGain: Float |
Gain from vorticity to saturation in the curl view. |
var densityVisualizationGain: Float |
Gain from density deviation to brightness in the density view. |
var compositeDyeExposure: Float |
Exposure of the dye in the composite view. |
var compositeVelocityGain: Float |
Weight of the velocity colour in the composite view. |
var compositeCurlGain: Float |
Weight of the vorticity colour in the composite view. |
var previewMode: FluidVisualizationMode |
What ShaderLatticeBoltzmannSystem.blitToScreen shows when it is not given a mode. |
ShaderLatticeBoltzmannSystem
A D2Q9 lattice Boltzmann fluid: controls, one fluid step and dye advection, each a full-screen pass into its own ping-pong target. Build one, call installInto, then push it, with ShaderLatticeBoltzmannSettings.baseVelocity or a module from controlsParameters. The file overview above describes the method and what each texture holds.
| Declaration | Summary |
|---|---|
val renderer: Renderer |
The renderer to build the targets and modules on; by default the one this box is running in. |
val settings |
The settings this system was built with, after configure ran, and the handle for changing it live. |
val controls |
The control field: red obstacle, green and blue steering velocity, alpha dye source. Draw into it with modules from controlsParameters, registered with addControls. |
val fluid |
The populations, as three attachments of one ping-pong target; attachment 2 is (f8, density, velocity.x, velocity.y), so fluid.latestImageViewFor(2) is the velocity field. |
val dye |
Colour carried by the flow. This is what imageView returns. |
val controlsPreserveModule: ModulePointer |
The pass that carries controls forward a frame and fades it. |
val fluidModule: ModulePointer |
One collision-and-streaming step, written to all three fluid attachments. The box adopts this one. |
val dyeUpdateModule: ModulePointer |
The pass that carries dye along the velocity, fades it and adds new dye. |
override val imageView: ImageView |
The dye, so a system can be sampled by another module the way a loaded image would be. |
override val imageViewArray: List<HasImageView> |
The same dye texture, in the list form the array-sampling path asks for. |
override fun render(command: CommandBuffer, index: Int) |
Runs controls, ShaderLatticeBoltzmannSettings.stepsPerFrame fluid steps and the dye, in that order. Called by whatever the system is registered with; see installInto. |
fun clearFluid() |
Put the fluid back to ShaderLatticeBoltzmannSettings.baseVelocity, plus ShaderLatticeBoltzmannSettings.initialNoise, on the next frame. |
fun clearControls() |
Erase every obstacle, push and dye source on the next frame. |
fun clearDye() |
Erase the dye on the next frame. |
fun reset() |
clearFluid, clearControls and clearDye together. |
fun addControls(name: String, module: ModulePointer) |
Add a module that draws into controls; it runs after the carry-forward pass, every frame. |
fun addControls(name: String, module: Module) |
addControls for a module that has not been given a pointer yet. |
fun addDye(name: String, module: ModulePointer) |
Add a module that draws into dye; it runs before the built-in update so the flow carries what it draws. |
fun addDye(name: String, module: Module) |
addDye for a module that has not been given a pointer yet. |
fun controlsParameters(name: String, configure: Parameters.() -> Unit = {}): Parameters |
Parameters for a module that draws into controls: additive blending, so what a fragment writes is added to what is there, and the previous controls available as sourceTex. Hand the result to moduleHere(...) in a box, then to addControls. |
fun dyeParameters(name: String, configure: Parameters.() -> Unit = {}): Parameters |
Parameters for a module that draws into dye, additive, with the previous dye as sourceTex. |
fun installInto(renderTarget: RenderTarget = renderer.renderList, withPreview: Boolean = true, previewName: String = "${settings.name}-preview", previewViewport: Rect? = null): ModulePointer? |
Wire the simulation into a render target as a pre-target and, by default, show it with blitToScreen. Returns the preview module, or null with withPreview = false. |
fun blitToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-preview", viewport: Rect? = null, mode: FluidVisualizationMode? = null): ModulePointer |
Draw the fluid to a target, as mode or, by default, as ShaderLatticeBoltzmannSettings.previewMode read every frame, so changing that setting switches the view live. viewport is where it lands; call this again with another name and viewport for a second view side by side. |
fun blitDyeToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-dye-preview", viewport: Rect? = null, sourceRect: Rect? = null): ModulePointer |
Draw the raw dye texture, without the composite's tone mapping. |
fun blitControlsToScreen(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-controls-preview", viewport: Rect? = null, sourceRect: Rect? = null): ModulePointer |
Draw the raw controls texture: red obstacle, green and blue steering, alpha not shown. |
alife_lsystem.kt
Structural spatial ALife: a bracketed, parametric, optionally stochastic L-system with a turtle that
emits FLines, plus space-colonization branching.
See alife.kt for the family overview.
L-systems are the odd one out in this family and belong in it for a precise reason: they are the one
generative system here whose output can be checked exactly and combinatorially rather than
statistically. Lindenmayer's original algae system has Fibonacci-length derivations; the Koch
snowflake has exactly 3 * 4^n forward symbols after n rewrites (the quadratic Koch island this
file also ships, LSYSTEM_KOCH_QUADRATIC, has 5^n — check which one you are asserting about); the
Koch snowflake's turtle path closes to a
measurable area. Those are equalities, not distributions, so they are the sharpest evidence in the
whole family — and they exercise the same turtle that draws the pretty trees.
src/jvmMain/kotlin/afield/simulation/agents/alife_lsystem.kt — read the source
Used in: Spatial artificial life
Top-level functions
| Declaration | Summary |
|---|---|
fun lSystem(axiom: String, vararg rules: Pair<Char, String>): LSystem |
Build an LSystem from an axiom and symbol to replacement pairs — the terse form. |
fun turtleDraw(s: String, p: TurtleParams = TurtleParams(), startX: Double = 0.0, startY: Double = 0.0, startHeading: Double = -PI / 2.0, rng: kotlin.random.Random = kotlin.random.Random(0)): TurtleResult |
Walk an L-system string with a turtle and return the FLines it drew. |
fun drawLSystem(system: LSystem, generations: Int, p: TurtleParams = TurtleParams(), seed: Long = 1L, startX: Double = 0.0, startY: Double = 0.0, startHeading: Double = -PI / 2.0): TurtleResult |
Derive system for generations and draw it in one call — the ordinary entry point. |
fun turtleClosureError(r: TurtleResult, step: Double): Double |
How far a turtle path fails to close, as a fraction of one step — a scale-free version of TurtleResult.closureError. |
fun flineBounds(lines: List<FLine>): DoubleArray? |
Axis-aligned bounds of every point in lines as (minX, minY, maxX, maxY), or null when there is nothing drawn. |
fun flineTotalLength(lines: List<FLine>): Double |
Total drawn length of lines — the sum of the straight-line distances between consecutive nodes within each line. |
fun flineSignedArea(lines: List<FLine>): Double |
Signed area enclosed by the polygon through every node of lines, by the shoelace formula. Meaningful only for a single closed curve, which is exactly the case it is used for: a Koch snowflake's area is a known multiple of its generating triangle's, so it is a second independent check on the turtle. |
fun rasterizeFLines(lines: List<FLine>, width: Int, height: Int, margin: Int = 4): Img |
Rasterise lines into a 1-channel Img, scaled and centred to fit with a margin-pixel border — the bridge from turtle geometry back into the raster half of the family, so an L-system can be measured with Img.blobShapes, skeletonised, or fed to a CA as a seed. |
fun spaceColonization(attractors: List<Vec2>, rootX: Double, rootY: Double, rootHeading: Double = -PI / 2.0, influenceRadius: Double = 24.0, killRadius: Double = 6.0, stepSize: Double = 4.0, maxIterations: Int = 400): ColonizationTree |
Space colonization (Runions et al.): grow a tree toward a cloud of attractors. |
fun discAttractors(count: Int, centerX: Double, centerY: Double, radius: Double, seed: Long = 1L): List<Vec2> |
A cloud of attractors filling a disc — the simplest colonisation target, and the one that makes the "the form comes from the space" claim easiest to see against a leaf-shaped or ring-shaped cloud. |
fun annulusAttractors(count: Int, centerX: Double, centerY: Double, innerRadius: Double, outerRadius: Double, seed: Long = 1L): List<Vec2> |
A cloud of attractors filling an annulus — a shape a tree cannot fill by growing straight, so the branching really has to follow the space. The clearest demonstration that space colonization is not simply "grow upward with noise". |
Top-level values
| Declaration | Summary |
|---|---|
val LSYSTEM_ALGAE: LSystem |
Lindenmayer's original algae system: A -> AB, B -> A, from the axiom A. |
val LSYSTEM_KOCH_QUADRATIC: LSystem |
The Koch curve (quadratic island form): F -> F+F-F-F+F, drawn with 90° turns. |
val LSYSTEM_KOCH_SNOWFLAKE: LSystem |
The Koch snowflake: axiom F--F--F with F -> F+F--F+F, drawn with 60° turns. |
val LSYSTEM_SIERPINSKI_ARROWHEAD: LSystem |
The Sierpiński arrowhead curve: A -> B-A-B, B -> A+B+A, 60° turns, drawn from A. |
val LSYSTEM_DRAGON: LSystem |
The dragon curve: X -> X+YF+, Y -> -FX-Y, 90° turns, axiom FX. |
val LSYSTEM_BINARY_TREE: LSystem |
A bracketed binary tree: F -> F[+F]F[-F]F, axiom F, drawn with a ~25° branch angle. |
val LSYSTEM_BUSH: LSystem |
Prusinkiewicz's bushy plant: X -> F[+X][-X]FX, F -> FF, axiom X, ~25° turns. |
val LSYSTEM_STOCHASTIC_PLANT: LSystem |
A stochastic plant: the same symbol X has three competing productions, so two derivations with different seeds are different plants of the same species. |
LProduction
data class LProduction(val symbol: Char, val replacement: String, val weight: Double = 1.0)
One production of an LSystem: replace symbol with replacement, with relative weight when several productions compete for the same symbol.
| Declaration | Summary |
|---|---|
val symbol: Char |
The single character this production rewrites. |
val replacement: String |
What it becomes. |
val weight: Double |
Relative probability among the productions sharing symbol; equal weights make a uniform choice, and a single production per symbol makes the system deterministic. |
LSystem
class LSystem(val axiom: String, val productions: List<LProduction>)
A Lindenmayer system: an axiom plus productions, rewritten in parallel.
| Declaration | Summary |
|---|---|
val axiom: String |
The generation-0 string. |
val productions: List<LProduction> |
The rewrite rules. |
val deterministic: Boolean |
true when no symbol has more than one production, so derivation needs no randomness. |
fun rewrite(s: String, rng: kotlin.random.Random = kotlin.random.Random(0)): String |
One parallel rewrite of s. |
fun derive(generations: Int, rng: kotlin.random.Random = kotlin.random.Random(0), maxLength: Int = 4_000_000): String |
Derive generations rewrites from the axiom, returning the final string. |
fun derivations(generations: Int, rng: kotlin.random.Random = kotlin.random.Random(0)): List<String> |
Every generation from the axiom to generations inclusive, so the growth itself is inspectable. |
override fun toString(): String |
TurtleParams
Turtle-graphics parameters for interpreting an L-system string.
| Declaration | Summary |
|---|---|
val angle: Double |
Turn per +/-, in radians. |
val step: Double |
Initial forward distance. |
val stepScale: Double |
Factor applied by " and, when scaleOnBranch, on every [. |
val widthScale: Double |
Factor applied by ! and, when scaleOnBranch, on every [. |
val scaleOnBranch: Boolean |
When true, entering a branch shortens and thins it automatically, which is what makes an unadorned F -> F[+F]F[-F]F look like a plant instead of a wire diagram. |
val angleJitter: Double |
Random fraction of angle added to each turn; 0 is exactly reproducible. |
val drawSymbols: String |
Which characters mean "move forward drawing". |
TurtleResult
The result of interpreting an L-system string with a turtle.
| Declaration | Summary |
|---|---|
val lines: List<FLine> |
One FLine per unbroken drawn run — a branch, in plant terms. Bracketed side branches and pen-up jumps each start a new line, so the list is the branching structure. |
val widths: List<Double> |
The line width in force for each entry of lines, parallel to it, so a renderer can taper a tree without re-walking the string. |
val startX: Double |
Where the pen started, x. |
val startY: Double |
Where the pen started, y. |
val finalX: Double |
Where the pen ended up, x. |
val finalY: Double |
Where the pen ended up, y. |
val drawnSegments: Int |
How many forward-drawing moves were made. |
val maxDepth: Int |
Deepest bracket nesting reached. |
val closureError: Double |
Distance from the pen's start to its end — 0 for a closed curve. This is the geometric equality a closed L-system (the Koch snowflake) is checked against; see turtleClosureError. |
override fun toString(): String |
ColonizationTree
A branching structure grown by space colonization.
| Declaration | Summary |
|---|---|
val x: DoubleArray |
Node x coordinates. |
val y: DoubleArray |
Node y coordinates. |
val parent: IntArray |
Parent index per node, -1 for the root. |
val consumedAttractors: Int |
How many attractors were reached and removed. |
val remainingAttractors: List<Vec2> |
Attractors the structure never got close enough to. |
val nodeCount: Int |
How many nodes. |
val edgeCount: Int |
How many parent→child edges — exactly nodeCount - 1 for a tree, which is worth checking. |
fun isTree(): Boolean |
true when the structure really is a tree: exactly one root, nodeCount - 1 edges, every non-root parent index valid and smaller than its child (so there can be no cycle). |
fun toFLines(z: Number = 0): List<FLine> |
One FLine per parent→child edge. |
fun depths(): IntArray |
Depth of every node from the root, in edges — the "order" of a branch, which is what a renderer needs to taper a tree. |
override fun toString(): String |
alife_physarum_gpu.kt
Jones' Physarum on the GPU, built on the TrailField stigmergy field in alife_trail.kt.
ShaderPhysarumSystem keeps a million agents (by default) in an addressable buffer and steps
them in one vertex-shader draw per frame: each agent samples the previous frame's trail at three
sensors, turns toward the strongest, steps, writes itself back and deposits one additive point.
Up to four species share the field, one per channel, each attracted by its own trail and repelled
by the others. sensing = false is the random-turn control arm, so Img.topShare of the trail
read back with TrailField.read separates a network from a random walk, as it does for the CPU
PhysarumSwarm.
Unlike the CPU model there is no one-agent-per-cell exclusion; at about one agent per pixel the density does that job.
src/jvmMain/kotlin/afield/simulation/agents/alife_physarum_gpu.kt — read the source
ShaderPhysarumSettings
Everything ShaderPhysarumSystem reads. The defaults are the CPU PhysarumParams defaults, so the two can be compared: 45° sensors 9 pixels ahead, 45° turns, one pixel a step, a radius-one blur and decay = 0.92 (the trail's half-life of 8.3 frames).
| Declaration | Summary |
|---|---|
var name: String |
Prefix for the modules and buffer this system registers; its trail field is named "<name>-trail". |
var width: Int |
Field width in pixels. |
var height: Int |
Field height in pixels. |
var agentCount: Int |
How many agents, 16 bytes each. A million at 1024x1024 is about one per pixel. |
var speciesCount: Int |
How many species, 1 to 4. Agent i is species i % speciesCount and deposits into channel species of the trail; it is attracted by its own channel and repelled by the others by repulsion. |
var sensorAngle: Float |
Angle between the centre sensor and each side sensor, radians. |
var sensorDistance: Float |
How far ahead the sensors look, pixels. |
var turnAngle: Float |
How far an agent turns when a side sensor wins, radians. |
var stepPx: Float |
Step length per frame, pixels. |
var deposit: Float |
Trail added per agent per frame. Only ratios matter to the agents; this sets the brightness. |
var repulsion: Float |
How strongly another species' trail counts against a sensor, relative to an agent's own. |
var jitter: Float |
Random extra turn per frame, radians, uniformly in ±jitter. |
var sensing: Boolean |
false turns at random instead of following the trail: the control arm, as in PhysarumParams.sensing. |
var pointSize: Float |
Point size for the deposit splat. |
var spawnRadiusFraction: Float |
Agents start in a disc of this fraction of the smaller side, headings random; 0 or less scatters them uniformly over the field instead. |
var exposure: Float |
Exposure of the default preview tone map; see TrailField.previewInto. |
var randomSeed: Long |
Seed for the initial placement, handed to ShaderPhysarumSystem.resetAgents at construction. |
ShaderPhysarumSystem
Jones' Physarum on the GPU: up to millions of agents in an addressable buffer that sense, turn, move and deposit into a TrailField, one vertex-shader invocation per agent per frame.
| Declaration | Summary |
|---|---|
val renderer: Renderer |
The renderer to build on; by default the one this box is running in. |
val settings |
The settings after configure; change them while it runs. |
val trail: TrailField |
The trail the agents write and read. Its own settings (half-life, diffusion, wind) are live too. |
val agentBuffer: FrameBufferGroup |
Agents: position (px), heading (radians), RNG state; 16 bytes each. |
val agentModule: ModulePointer |
The point-list pass that steps and deposits every agent. |
val agentMesh: Mesh |
One point per agent; the push. constants of the agent shader are bound here. |
override val imageView: ImageView |
|
override val imageViewArray: List<HasImageView> |
|
override fun render(command: CommandBuffer, index: Int) |
|
fun resetAgents(seed: Long = lastSeed) |
Place every agent again (see ShaderPhysarumSettings.spawnRadiusFraction); the trail is untouched. Call TrailField.clear too for a fresh start. |
fun installInto(renderTarget: RenderTarget = renderer.renderList, withPreview: Boolean = true, previewViewport: Rect? = null, palette: List<Vec4> = TrailField.DEFAULT_PALETTE): ModulePointer? |
Register the system so it steps once a frame and, by default, draw it tone-mapped (species coloured by TrailField.DEFAULT_PALETTE unless palette says otherwise). Returns the preview module, or null with withPreview = false. |
alife_script.kt
Per-agent scripted behaviour for ALife: a coroutine each agent runs as its own little program, on a fixed-step virtual clock, composing into weighted steering drives.
See alife.kt for the family overview, and alife_agents.kt for the family this completes.
Why this exists
Everything in alife_agents.kt is a global rule. A flock is BoidFlock.step(p: BoidParams) —
one function, one parameter block, applied identically to every agent on every tick. That is the
right shape for the three canonical emergence demonstrations and it is the wrong shape for the
moment you want this agent to spend two seconds approaching, half a second feeding and then go
somewhere else. A global rule has no place to keep "where am I up to". You end up encoding the
agent's position in its own program as an integer state field and a when, which is the standard
way this is done and is also why it is never done twice the same way.
A coroutine has a place to keep it: the continuation. So a behaviour here reads as the sequence it is —
forever {
phase("travel") { moveUntil({ near(site, 6.0) }, maxSeconds = 8.0) { seek(site); separate() } }
phase("feed") { rest(0.4) }
}
— and the scheduler, not the author, remembers that this agent is 1.3 seconds into its approach.
Why the scheduler counts ticks
The obvious way to build this is on kotlinx.coroutines with delay and a real dispatcher. That
gives you something you can watch and nothing you can assert, because the interleaving depends on
the machine. This scheduler is mine and it counts ticks:
wait(seconds)rounds up to a whole number of steps,ceil(seconds / fixedStep), minimum one. Wake-ups are compared asLongtick numbers, so there is no floating-point boundary to be on the wrong side of, andwait(0.5)at 60 Hz is exactly 30 steps, every time.- Resumptions run from a FIFO queue I own, ordered by
(wakeTick, sequence)with a unique monotonic sequence number, which is a total order. Two behaviours that wake on the same tick resume in the order they went to sleep. - Drives are held in a
LinkedHashMapkeyed by a monotonicLong, so they are evaluated in the order they were registered. (Keying a motor map byAny()— the natural thing, since the key is only a handle — makes the evaluation order depend on identity hash codes, i.e. on the run.) - Every drive reads a pre-step snapshot of every agent, so the result does not depend on which agent is integrated first.
- Randomness is per agent, from
Random(seed, id), never from a shared stream, so adding an agent does not reshuffle the others.
The consequence is ScriptWorld.state: run a population twice from one seed and the DoubleArray
is equal bit for bit. That is asserted, not asserted-about — see AlifeScriptGallery.
Composing, not duplicating
The steering verbs in BehaviourSteering are the Reynolds vocabulary already in this family:
BehaviourSteering.separate, BehaviourSteering.align and BehaviourSteering.cohere are the
three boid terms, so a scripted population can be a flock when its script says to be one, and
ScriptWorld.asBoidFlock hands the state to polarization, nearestNeighborDistances and the
rest of alife.kt's measures rather than growing a second set. Trails come out as FLines
(ScriptWorld.trailFLines) because geometry is FLine's job; nothing here rasterises anything
except where a contact sheet needs pixels.
What it deliberately is not
Not a behaviour tree and not a blackboard. Both are ways of giving a stateless evaluator somewhere
to put the state, and the whole argument above is that a coroutine already has somewhere. There is
no reactive re-evaluation either: a behaviour that wants to abandon what it is doing says so, with
BehaviourScope.moveUntil or BehaviourScope.timeout or by cancelling a BehaviourJob.
src/jvmMain/kotlin/afield/simulation/agents/alife_script.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun ScriptWorld.toImg(scale: Double = 2.0, trail: Float = 0.45f, minDot: Double = 2.5, marks: List<Pair<Double, Double>> = emptyList(), markRadius: Double = 6.0): Img |
The world as an RGB raster: walls, every agent's trail so far, the agents themselves, and any marks (goals, waypoints) as rings — the frame a Box.animateEvolution clip of a scripted population draws, in the way BoidFlock.toImg is the frame of a flock. |
BehaviourCancelled
class BehaviourCancelled internal constructor(val reason: String) : RuntimeException(reason)
Thrown into a behaviour at its next suspension point when its BehaviourJob is cancelled — by BehaviourJob.cancel, by its parent being cancelled, or by the agent dying.
ScriptEvent
data class ScriptEvent(val tick: Long, val actorId: Int, val kind: String, val detail: String)
Something a behaviour did, stamped with the tick it happened on — the run's audit trail.
| Declaration | Summary |
|---|---|
val tick: Long |
The step it was recorded on; 0 means "before the first step". |
val actorId: Int |
The agent it belongs to, or -1 for a world-level event. |
val kind: String |
A short stable tag — phase, phase-end, log, died, failed. |
val detail: String |
The phase name, message or exception text. |
override fun toString(): String |
ScriptDrive
A steering ingredient: given an agent and what it can sense, the velocity it would like to have.
| Declaration | Summary |
|---|---|
fun steer(self: ScriptActor, sense: ScriptSense): Vector2d |
The desired velocity for self, in world units per second. |
WeightedDrive
data class WeightedDrive(val drive: ScriptDrive, val weight: Double)
One ScriptDrive and the weight it carries in the blend.
ScriptSense
What an agent can see this step — always the pre-step snapshot, never the half-integrated present.
| Declaration | Summary |
|---|---|
val world: ScriptWorld |
|
val self: ScriptActor |
|
val speed: Double |
The speed the currently-evaluating behaviour asked for — scale desired velocities by this. |
val position: Vector2d |
self's position as at the start of this step. |
val velocity: Vector2d |
self's velocity as at the start of this step. |
fun neighbours(range: Double): List<ScriptActor> |
Live agents other than self within range of it, under the world's metric, in agent-id order. |
fun delta(x: Double, y: Double): Vector2d |
Offset from self to (x, y), taking the shorter way round if the world wraps. |
fun canSee(other: ScriptActor): Boolean |
Whether nothing blocks the straight line from self to other — both read off the pre-step snapshot, so it agrees with the positions every other query in this class reports. |
fun canSee(x: Double, y: Double): Boolean |
Whether nothing blocks the straight line from self to (x, y). |
fun visibleNeighbours(range: Double): List<ScriptActor> |
Live agents other than self within range that self can also see — neighbours with the wall between them removed. |
ScriptTarget
Somewhere an agent might steer toward — resolved fresh every step, so a target that moves is followed and a target that has gone (a dead agent, a cleared goal) simply stops pulling.
| Declaration | Summary |
|---|---|
fun resolve(world: ScriptWorld, self: ScriptActor): Vector2d? |
The position to aim at, or null if there is none this step. |
ScriptActor
One scripted agent: a position, a velocity, a motor set its behaviours write into, and a name for what it is currently doing.
| Declaration | Summary |
|---|---|
val world: ScriptWorld |
|
val id: Int |
|
val maxSpeed: Double |
Hard cap on speed, whatever a behaviour asks for. |
val radius: Double |
|
val steerResponse: Double |
How much of the gap between current and desired velocity is closed per step, in 0..1. 1 is instant turning (correct for a projectile, wrong for anything with inertia); the default 0.25 gives a visible arc without the overshoot a spring would. |
val tag: String |
|
val wallResponse: WallResponse |
What happens when this agent's swept path meets one of the world's obstacles: one of WallResponse's three, SLIDE by default. Irrelevant in a world with no obstacles, where the integrator does not even look. |
val restitution: Double |
How much of the normal velocity survives a WallResponse BOUNCE, in 0..1. Ignored by the other two responses. |
var alive: Boolean |
False once ScriptWorld has killed it; a dead agent is no longer integrated or sensed. |
var phase: String? |
The innermost BehaviourScope.phase currently running, or null between phases. |
val rng: Random |
This agent's own random stream, seeded from the world seed and the agent id. Per-agent rather than shared, so inserting an agent does not reshuffle every other agent's future. |
var wallContacts: Int |
How many times this agent's swept path has been stopped, deflected or reflected by a wall. |
val position: Vector2d |
A fresh copy of the current position. |
val velocity: Vector2d |
A fresh copy of the current velocity, in world units per second. |
val speed: Double |
Current speed. |
val driveCount: Int |
How many drives are currently registered across all active behaviours — a load readout. |
fun distanceTo(px: Double, py: Double): Double |
Distance to (px, py) under the world's metric. |
fun distanceTo(p: Vector2d): Double |
Distance to p under the world's metric. |
fun placeAt(px: Double, py: Double) |
Move this agent immediately, ignoring its velocity — teleporting, so use it for placement and respawns, not for motion. The trail records the jump as a jump. |
override fun toString(): String |
BehaviourJob
A running behaviour, and the handle that cancels it.
| Declaration | Summary |
|---|---|
val actor: ScriptActor |
|
var cancelled: Boolean |
True once cancel has been called, or a parent was cancelled. |
var done: Boolean |
True once the behaviour has returned, thrown, or finished unwinding a cancellation. |
val isActive: Boolean |
Still worth waiting for. |
fun cancel(reason: String = "cancelled") |
Cancel this behaviour and everything it started. Idempotent. |
override fun toString(): String |
ScriptWorld
A fixed-step arena of scripted agents: the clock, the scheduler, the population and the physics.
| Declaration | Summary |
|---|---|
val width: Double |
|
val height: Double |
|
val fixedStep: Double |
Seconds per tick. Also the quantum every BehaviourScope.wait is rounded up to, so it sets the time resolution of every script in the world. |
val seed: Long |
|
val wrap: Boolean |
True for a torus (the metric BoidFlock uses, and what asBoidFlock assumes); false to clamp at the walls, which stops agents but does not make them avoid them — that is BehaviourSteering.avoidEdges' job. |
val maxParallelPerActor: Int |
The cap BehaviourScope.parallel checks. A behaviour that launches a sub-behaviour inside a forever and never cancels it is the standard way to fill a scheduler with garbage, and it presents as a gradual slowdown rather than an error. This turns it into an error. |
val maxResumptionsPerStep: Int |
How many behaviour resumptions one step may take before the world declares a livelock. Catches the two shapes that otherwise hang: a while (true) with no wait, and a wait short enough to round to zero (it cannot — wait has a one-tick floor — but a hand-rolled forever over an empty body would still spin). |
val recordTrails: Boolean |
Whether to accumulate per-agent trails for trailFLines. On by default because the trail is the picture; turn it off for long runs where only the final state matters. |
val maxRouteSearchesPerStep: Int |
How many BehaviourSteering.route drives may run a fresh A* in one step. A grid search is the one thing in this file that is not O(1) per agent, and a population that all repaths on the same tick is the frame that drops. Agents over the budget keep steering to the route they already have and try again next step; the budget is spent in agent-id order, so which agents get it is reproducible. |
val maxRouteExpansions: Int |
Cells one A* may expand before giving up. A search that hits this returns no route rather than a bad one, and the drive falls back to a direct seek. |
val maxNavCells: Int |
The cap on a NavGrid's cell count. Exceeding it enlarges the cells and sets NavGrid.coarse rather than allocating a grid proportional to the arena. |
var tick: Long |
Steps taken. 0 before the first step. |
val time: Double |
Simulation seconds elapsed: exactly tick * fixedStep, never a wall-clock reading. |
val actors: List<ScriptActor> |
The population, in spawn order, alive and dead. |
val living: List<ScriptActor> |
Live agents, in spawn order. |
val events: List<ScriptEvent> |
Everything the behaviours recorded, in order. See ScriptEvent. |
val failures: List<String> |
Drives that threw or returned non-finite vectors, and behaviours that threw. |
var lastStepResumptions: Int |
How many behaviour resumptions the last step ran — a load readout, and a livelock warning. |
val wallField: ObstacleField |
The world's walls. See alife_walls.kt for what they are and why they are segments. |
var routeSearches: Long |
Total A* searches run since the world began — the cost readout for a routed population. |
var routeSearchesDeferred: Long |
Searches refused this run because maxRouteSearchesPerStep was already spent. |
val obstacles: List<ScriptObstacle> |
The walls, in the order they were added. |
fun addObstacle(o: ScriptObstacle): ScriptObstacle |
Add o to the world. |
fun addWall(x0: Double, y0: Double, x1: Double, y1: Double, name: String = "wall"): ScriptObstacle |
A single straight wall from (x0, y0) to (x1, y1). |
fun addPolyline(vararg points: Double, name: String = "polyline"): ScriptObstacle |
An open run of points, x, y, x, y, … — a corridor side, a curved barrier. |
fun addPolygon(vararg points: Double, name: String = "polygon"): ScriptObstacle |
A closed outline, x, y, x, y, …. Its interior is unreachable, not solid — see the file KDoc. |
fun addRect(x: Double, y: Double, w: Double, h: Double, name: String = "rect"): ScriptObstacle |
An axis-aligned box with its lower-left corner at (x, y). |
fun addDisc(cx: Double, cy: Double, radius: Double, segments: Int = 24, name: String = "disc"): ScriptObstacle |
A round pillar, approximated by segments sides. |
fun obstacleFLines(z: Number = 0): List<FLine> |
Every wall's outline, for drawing the world the agents are in. |
fun navGrid(inflate: Double): NavGrid |
The navigation grid for a circle of inflate, built on first ask and cached. |
fun findRoute(sx: Double, sy: Double, gx: Double, gy: Double, inflate: Double): DoubleArray? |
A route for a circle of inflate from (sx, sy) to (gx, gy) as x, y, x, y, …, or null if there is no way through. |
fun lineOfSight(x0: Double, y0: Double, x1: Double, y1: Double): Boolean |
Whether a straight sight line from (x0, y0) to (x1, y1) is unobstructed. |
fun canSee(a: ScriptActor, b: ScriptActor): Boolean |
Whether a can see b — both on snapshot positions, so it agrees with what drives read. |
fun spawn(x: Double, y: Double, maxSpeed: Double = 20.0, radius: Double = 1.0, steerResponse: Double = 0.25, vx: Double = 0.0, vy: Double = 0.0, tag: String = "", wallResponse: WallResponse = WallResponse.SLIDE, restitution: Double = 0.8, program: suspend BehaviourScope.() -> Unit): ScriptActor |
Add an agent at (x, y) running program. |
fun kill(a: ScriptActor) |
Kill a: cancel its behaviours (so their finally blocks empty its motor set), stop integrating it, and take it out of everyone's neighbour queries. |
fun neighbours(self: ScriptActor, range: Double): List<ScriptActor> |
Live agents other than self within range, in agent-id order, on snapshot positions. |
fun deltaX(from: Double, to: Double): Double |
Signed x offset from from to to, the shorter way round if the world wraps. |
fun deltaY(from: Double, to: Double): Double |
Signed y offset from from to to, the shorter way round if the world wraps. |
fun start() |
Run every spawned program up to its first suspension point, at tick 0. Idempotent. |
fun step() |
Advance one tick: wake what is due, run every behaviour to its next suspension point, then integrate. |
fun run(steps: Int) |
step applied steps times. |
fun state(): DoubleArray |
The whole population's state as one array — x, y, vx, vy per agent, in agent-id order. |
fun stateSignature(): String |
A short hex digest of state over the raw bits of every coordinate — for printing, where a 4N-element array will not fit. |
fun eventSignature(): String |
A digest of the event log — the symbolic witness beside the numeric one. |
fun asBoidFlock(): BoidFlock |
The live population as a BoidFlock, so alife.kt's measures — polarization, nearestNeighborDistances, BoidFlock.meanSpeed — apply to a scripted run unchanged. |
fun trailFLines(stride: Int = 1, z: Number = 0): List<FLine> |
One FLine per agent, its trail so far, split wherever it crossed a wrap seam. |
fun totalPathLength(): Double |
Total distance every agent travelled, summed — the "did anything happen at all" readout. |
override fun toString(): String |
ALifeBehaviour
annotation class ALifeBehaviour
Marks the two scripting receivers, so a steering block cannot accidentally call a suspending behaviour verb on the enclosing scope.
BehaviourScope
The verbs an agent's program is written in: sequencing, waiting on simulation time, repetition, bounded sub-behaviours, named phases, and motion.
| Declaration | Summary |
|---|---|
val world: ScriptWorld |
|
val self: ScriptActor |
|
val time: Double |
Simulation seconds since the run began. |
val tick: Long |
Steps taken since the run began. |
val position: Vector2d |
A fresh copy of this agent's current position. |
val velocity: Vector2d |
A fresh copy of this agent's current velocity. |
val phase: String? |
The innermost phase currently running, or null. |
val rng: Random |
This agent's own random stream. Per agent, so drawing from it cannot disturb anyone else. |
suspend fun wait(seconds: Double) |
Spend seconds of simulation time, rounded up to a whole number of steps. |
suspend fun waitTicks(ticks: Int) |
Spend exactly ticks steps. The primitive wait is expressed in. |
suspend fun waitUntil(maxSeconds: Double = 30.0, predicate: () -> Boolean): Boolean |
Wait until predicate holds, testing it once per step, giving up after maxSeconds. |
suspend fun forever(block: suspend BehaviourScope.() -> Unit) |
Repeat block for as long as this behaviour is active and the agent is alive. |
suspend fun every(seconds: Double, block: suspend BehaviourScope.() -> Unit) |
Run block, then wait seconds, forever. The period is the gap, not the cycle length. |
suspend fun repeatTimes(times: Int, block: suspend BehaviourScope.(Int) -> Unit) |
Run block times times, or until cancelled. |
suspend fun parallel(block: suspend BehaviourScope.() -> Unit): BehaviourJob |
Start block as a sub-behaviour of this one and carry on immediately. |
suspend fun timeout(seconds: Double, block: suspend BehaviourScope.() -> Unit): Boolean |
Run block with at most seconds to finish in. Returns true if it finished, false if it was cut off. |
suspend fun phase(name: String, block: suspend BehaviourScope.() -> Unit) |
Name what the agent is doing for the duration of block, and log it. |
fun log(message: String) |
Append a note to ScriptWorld.events on this tick. |
suspend fun moveFor(seconds: Double, speed: Double = self.maxSpeed, block: BehaviourSteering.() -> Unit) |
Steer by block's drives for seconds, at speed, then stop steering. |
suspend fun moveUntil(maxSeconds: Double = 10.0, speed: Double = self.maxSpeed, until: () -> Boolean, block: BehaviourSteering.() -> Unit): Boolean |
Steer by block's drives until until holds or maxSeconds runs out. Returns true if until became true. |
suspend fun coast(seconds: Double) |
Keep the velocity the agent already has, for seconds — ballistic motion. |
suspend fun rest(seconds: Double) |
Stop dead and stay stopped for seconds. |
fun die() |
Kill this agent: it stops being integrated and stops being sensed, and its behaviours are cancelled. |
fun point(p: Vector2d): ScriptTarget |
A fixed point. Copied on the way in, so a later mutation of p cannot move the target. |
fun point(x: Double, y: Double): ScriptTarget |
A fixed point. |
fun actor(other: ScriptActor): ScriptTarget |
Another agent — resolving to null once it is dead, so the pull simply stops. |
fun nearest(range: Double = max(world.width, world.height), accept: (ScriptActor) -> Boolean = { true }): ScriptTarget |
The nearest live agent within range satisfying accept, re-chosen every step. |
fun centroid(accept: (ScriptActor) -> Boolean = { true }): ScriptTarget |
The centroid of the live agents satisfying accept, on snapshot positions. |
fun near(t: ScriptTarget, distance: Double): Boolean |
True when this agent is within distance of t's current position. |
fun near(x: Double, y: Double, distance: Double): Boolean |
True when this agent is within distance of (x, y). |
BehaviourSteering
class BehaviourSteering internal constructor(private val scope: BehaviourScope)
The ingredients a BehaviourScope.moveFor blends: each verb adds one weighted ScriptDrive.
| Declaration | Summary |
|---|---|
val world: ScriptWorld |
The world being steered in. |
val self: ScriptActor |
The agent being steered. |
val rng: Random |
This agent's own random stream — for a choice made once, when the behaviour starts. |
val time: Double |
Simulation seconds at the moment the behaviour started, i.e. when this block ran. |
fun drive(weight: Double = 1.0, drive: ScriptDrive) |
Add a custom drive. The escape hatch every one of the verbs below is built on. |
fun keepVelocity(weight: Double = 1.0) |
Keep the velocity the agent already has. See BehaviourScope.coast. |
fun stop(weight: Double = 1.0) |
Hold still — a desired velocity of zero, which the agent decays toward. |
fun direction(heading: Vector2d, weight: Double = 1.0) |
Travel along a fixed heading, normalised here so its length does not become a second gain. |
fun heading(radians: Double, weight: Double = 1.0) |
Travel along the heading radians measures anticlockwise from +x. |
fun seek(t: ScriptTarget, weight: Double = 1.0) |
Head straight for t at full speed. |
fun arrive(t: ScriptTarget, slowRadius: Double = 12.0, stopRadius: Double = 0.5, weight: Double = 1.0) |
Head for t, easing off inside slowRadius and stopping at stopRadius. |
fun flee(t: ScriptTarget, weight: Double = 1.0) |
Head directly away from t. |
fun orbit(t: ScriptTarget, distance: Double = 20.0, clockwise: Boolean = true, weight: Double = 1.0) |
Circle t at distance, blending a tangential component with a radial correction. |
fun wander(turn: Double = 2.0, weight: Double = 1.0) |
Drift, turning by at most turn radians per second, from this agent's random stream. |
fun separate(range: Double = 6.0, weight: Double = 0.8) |
Push away from live agents within range, weighted by how close they are — Reynolds' separation, the same term BoidParams.separationWeight scales inside BoidFlock.step. |
fun align(range: Double = 15.0, weight: Double = 1.0) |
Match the mean heading of live agents within range — Reynolds' alignment term. |
fun cohere(range: Double = 15.0, weight: Double = 0.7) |
Steer toward the centroid of live agents within range — Reynolds' cohesion term. |
fun avoidEdges(margin: Double = 15.0, weight: Double = 1.0) |
Turn away from the arena edges within margin — for a world with wrap = false, where ScriptWorld.confine otherwise pins an agent against a wall with its velocity zeroed. |
fun avoidWalls(clearance: Double = 6.0, weight: Double = 1.2) |
Push away from any wall within clearance of the agent's surface, weighted by how close it is — the wall counterpart of separate, and written in the same shape for the same reason. |
fun cling(distance: Double = 1.0, clockwise: Boolean = true, range: Double = 25.0, weight: Double = 1.0) |
Follow the nearest wall, holding distance between the agent's surface and it — wall following, the same blend of a tangential term and a radial correction that orbit is. |
fun route(t: ScriptTarget, clearance: Double = 0.5, repathDistance: Double = 8.0, stallSeconds: Double = 0.75, waypointRadius: Double = -1.0, weight: Double = 1.0) |
Head for t along a route: a radius-aware A* through the world's walls, smoothed, and followed waypoint by waypoint. |
fun gradient(field: ScalarField, towards: Boolean = true, probe: Double = 2.0, weight: Double = 1.0) |
Climb (or descend) a ScalarField — chemotaxis, phototaxis, thermotaxis, whichever the field means. |
fun point(p: Vector2d): ScriptTarget |
A fixed point. See BehaviourScope.point. |
fun point(x: Double, y: Double): ScriptTarget |
A fixed point. See BehaviourScope.point. |
fun actor(other: ScriptActor): ScriptTarget |
Another agent, resolving to nothing once it is dead. See BehaviourScope.actor. |
fun nearest(range: Double = max(world.width, world.height), accept: (ScriptActor) -> Boolean = { true }): ScriptTarget |
The nearest live agent within range satisfying accept. See BehaviourScope.nearest. |
fun centroid(accept: (ScriptActor) -> Boolean = { true }): ScriptTarget |
The centroid of the live agents satisfying accept. See BehaviourScope.centroid. |
alife_trail.kt
A stigmergy field on the GPU: a four-channel trail texture that agents write into and read from, and that diffuses, decays and drifts on its own between their visits.
This is the medium of every "leave a mark, follow the marks" system: Physarum's chemoattractant,
ant pheromone, the dust in a particle system that remembers where the crowd went. TrailField
owns only the medium. Whatever writes into it is a deposit module you add with addDeposit,
built on depositParameters; ShaderPhysarumSystem is one such writer, and its agent shader is
the worked example of reading the field back.
One frame
The field is an RGBA16F PingPongRenderToTexture. Each frame, first, a full-screen evolve pass
rebuilds the field from the previous frame: it samples the old trail at p - wind (bilinear, so
sub-pixel wind drifts smoothly), mixes in the 3x3 mean by TrailFieldSettings.diffusion, and
multiplies each channel by its decay. Then every deposit module draws, additively, into the same
target, reading the previous frame through trailTex. The four channels are independent trails
(four species, or one trail plus three other kinds of mark); each has its own half-life.
The field is toroidal: sampling and diffusion wrap at the edges.
Reading it from a shader
#include "trail/sample" gives trailAt(trailTex, px) (bilinear, wrapping, pixel coordinates
with pixel centres at +0.5), trailGradient(trailTex, px, weights, h), and trailClip(px, dims)
for placing a deposit point at a pixel. The same source is TrailShaders.SAMPLE for code that
builds shader strings itself.
Reading it from Kotlin
read queues a copy of the raw floats and completes with an Img (four channels, unclamped);
channel(c) of that is what Img.topShare takes.
src/jvmMain/kotlin/afield/simulation/agents/alife_trail.kt — read the source
TrailFieldSettings
Everything TrailField reads: size, per-channel half-lives, diffusion, wind and format. All but the size and format are read every frame.
TrailShaders
The packaged GLSL behind TrailField, reachable from any shader as #include "trail/<name>".
| Declaration | Summary |
|---|---|
val SAMPLE: String |
trail/sample: wrapping bilinear sampling, a gradient, and pixel-to-clip placement. Expects nothing to be declared beforehand; every function takes the sampler as an argument. |
fun has(name: String): Boolean |
Whether trail/<name> names one of these sources, with or without .glsl. |
fun source(name: String): String |
The source of trail/<name>. |
TrailField
A GPU stigmergy field: a toroidal RGBA16F trail that diffuses, decays per channel and drifts, plus the plumbing for modules that deposit into it and shaders that sense it. See TrailFieldSettings for the overview.
| Declaration | Summary |
|---|---|
val renderer: Renderer |
The renderer to build on; by default the one this box is running in. |
val settings |
The settings, read every frame, so changing one takes effect on the next frame. |
val target |
The ping-pong target holding the trail. The evolve pass and every deposit module render into it. |
val sampler |
One sampler for every read of the trail; filtering is done in GLSL, so nothing depends on its mode. |
val evolveModule: ModulePointer |
The full-screen pass that carries the trail from one frame to the next: wind, diffusion, decay. |
fun decayFactors(): Vec4 |
Per-channel multiplicative decay per frame, from TrailFieldSettings.halfLifeFrames. |
fun depositParameters(name: String, configure: Parameters.() -> Unit = {}): Parameters |
Parameters for a module that deposits into this field: rendering into target, additively, with the previous frame of the trail bound to any sampler named trailTex. Build the module on these, then hand it to addDeposit. |
fun addDeposit(name: String, module: ModulePointer) |
Add a deposit module; deposits draw after the evolve pass, in the order they were added. |
fun removeDeposit(name: String) |
Remove a deposit module added with addDeposit. |
fun clear() |
Empty every channel. Takes effect over the next PingPongRenderToTexture.frameCount frames, so no history slot keeps old trail. |
fun previousTrailResolver(): (Introspected.Texture) -> Pair<HasImageViewArray, TextureSampler>? |
Resolver binding the trail's previous frame to a sampler named trailTex (and its latest to trailLatestTex), for modules built outside depositParameters that read the field. |
override val imageView: ImageView |
|
override val imageViewArray: List<HasImageView> |
|
override fun render(command: CommandBuffer, index: Int) |
|
fun installInto(renderTarget: RenderTarget = renderer.renderList) |
Register this field as a pre-target of renderTarget, so it steps once per frame. |
fun previewInto(target: RenderTarget = renderer.renderList, name: String = "${settings.name}-preview", viewport: Rect? = null, exposure: () -> Float = { 0.15f }, palette: List<Vec4> = DEFAULT_PALETTE): ModulePointer |
Draw the trail to target with an exposure tone map: channel c contributes palette[c](solvers.md#ceresexpression-kt--c) * (1 - exp(-exposure * trail.c)). Registers nothing else, so it can be called for a second view. |
fun read(): CompletableFuture<Img> |
Read the trail's raw floats back after its next frame: an Img of width x height x 4, top row first as the field stores it, unclamped. Completes once that frame's GPU work finishes. |
alife_vehicle_brains.kt
A larger Braitenberg vehicle genome, and an equal-budget comparison of the searches that look for one.
VehicleWiringGenome is four numbers. A VehicleBrainGenome adds what the vehicle model can
express besides its wires: where the sensors point, how sensitive they are, a bias on each motor,
and VehicleBrainGenome.hidden recurrent hidden neurons between the sensors and the motors. With no
hidden neurons it is 8 numbers; with 8 it is 112, with 16 it is 344. compareVehicleSearch runs random sampling, a
hill climber and the (µ+λ) evolver of PopulationSearch at the same evaluation budget over many
seeds and reports distributions, which is how braitenberg_vehicles.md §10 asks whether evolution
beats random sampling and at what genome size the answer changes.
src/jvmMain/kotlin/afield/simulation/agents/alife_vehicle_brains.kt — read the source
Used in: Braitenberg vehicles with memory
Top-level functions
| Declaration | Summary |
|---|---|
fun compareVehicleSearch(task: VehicleBrainTask, hidden: Int, budget: Int = 300, seeds: List<Long> = (1L..12L).toList(), mu: Int = 6, lambda: Int = 12, crossoverRate: Double = 0.3, immigrantRate: Double = 0.0, patience: Int = 30): VehicleSearchComparison |
Random sampling, hill climbing and evolution on task with a hidden-neuron VehicleBrainGenome, each for exactly budget objective calls at every seed in seeds, through compareSearch. Deterministic given its arguments. Evolution is (µ=mu + λ=lambda) with tournament 2, crossoverRate and immigrantRate (the fraction of children that are fresh random draws); the hill climber restarts after patience non-improving steps. For a fourth arm, or another genome, call compareSearch directly. |
VehicleBrain
A decoded vehicle controller. Every gene is in -1..1; decode scales them.
VehicleBrainWorld
A population of VehicleBrain vehicles on the same torus, kinematics and lights as VehicleWorld.
| Declaration | Summary |
|---|---|
val brain: VehicleBrain |
The controller every vehicle runs. |
val count: Int |
How many vehicles. |
val width: Double |
Domain width. |
val height: Double |
Domain height. |
val params: VehicleParams |
Kinematics; only the body fields (sensorDistance, baseSpeed, motorSpeed, axle) are read. |
var lights: List<Light> |
Lights at their current positions. |
val x: DoubleArray |
Body x. |
val y: DoubleArray |
Body y. |
val heading: DoubleArray |
Headings. |
val speed: DoubleArray |
Forward speed on the last step. |
fun light(px: Double, py: Double): Double |
Brightness at a point, summed over lights on the torus. |
fun nearestLightDistance(i: Int): Double |
Distance from vehicle i to the nearest light. |
fun meanLight(): Double |
Mean light at the bodies. |
fun meanSpeed(): Double |
Mean forward speed. |
fun step() |
Advance every vehicle one step. |
fun stepTimes(n: Int) |
step n times. |
fun trails(steps: Int, scale: Double = 1.2): Img |
Trails of steps steps from here, drawn over the light field as it stands at the end, at scale pixels per unit. Moving lights leave a faint track of their own. |
VehicleBrainGenome
class VehicleBrainGenome(val hidden: Int = 0) : GenomeOps<DoubleArray>
A VehicleBrain as a GenomeOps: VehicleBrain.genes``(hidden) numbers in -1..1, drawn uniformly.
VehicleBrainTask
A scoring task for a VehicleBrain: count vehicles from initialSeed among lights for steps steps, scored over the last scored by score, which sees the world after each step and returns that step's mean per-vehicle score.
| Declaration | Summary |
|---|---|
val name: String |
Short name for tables. |
val lights: List<Light> |
Lights at the start of every run. |
val score: (VehicleBrainWorld) -> Double |
Per-step score, averaged over the scored window. |
val count: Int |
Vehicles per evaluation. |
val steps: Int |
Steps per evaluation. |
val scored: Int |
Steps at the end that are scored. |
val initialSeed: Long |
Start positions. |
fun world(genes: DoubleArray, hidden: Int, seed: Long = initialSeed, vehicles: Int = count): VehicleBrainWorld |
A fresh world for genes decoded with hidden hidden neurons. |
fun objective(hidden: Int): Objective<DoubleArray> |
The task as an Objective for a hidden-neuron genome. Behaviour is [meanLight, meanSpeed]. |
VehicleSearchDriver
enum class VehicleSearchDriver(val arm: String)
The three searches compareVehicleSearch runs, by their SearchArm names.
Constants: RANDOM, HILL_CLIMB, EVOLVE.
| Declaration | Summary |
|---|---|
val arm: String |
The SearchArm.name this driver runs as. |
VehicleSearchCell
One driver's result for one seed.
| Declaration | Summary |
|---|---|
val driver: VehicleSearchDriver |
Which driver. |
val seed: Long |
The search seed. |
val run: SearchRun<DoubleArray> |
The run, with its per-evaluation SearchRun.trace. |
VehicleSearchComparison
compareVehicleSearch's result: every driver at every seed, all at budget evaluations. The statistics and the plot are general's, the SearchComparison any genome gets.
| Declaration | Summary |
|---|---|
val task: String |
The task. |
val hidden: Int |
Hidden neurons. |
val genes: Int |
Genes. |
val general: SearchComparison<DoubleArray> |
The generic comparison this wraps. |
val budget: Int |
Evaluations each run was allowed. |
val cells: List<VehicleSearchCell> |
Every run. |
fun of(d: VehicleSearchDriver): List<VehicleSearchCell> |
The runs of one driver, in seed order. |
fun best(d: VehicleSearchDriver): DoubleArray |
Best fitness of each seed's run of d. |
fun wins(a: VehicleSearchDriver, b: VehicleSearchDriver): Int |
Seeds at which a ended strictly above b. |
fun champion(d: VehicleSearchDriver): DoubleArray |
The best genome any run of d found. |
fun table(): String |
One line per driver: median, IQR, min, max of the per-seed bests, and wins against random. |
fun perSeed(): String |
Per-seed bests, one row per seed. |
fun curve(d: VehicleSearchDriver): Array<DoubleArray> |
Median best-so-far of d after each evaluation, with its quartiles: [q25, median, q75] rows. |
fun medianRun(d: VehicleSearchDriver): SearchRun<DoubleArray> |
The run of d 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 driver's median as a line, its interquartile range as a band, and its final median written at the right. Random is grey, hill climbing blue, evolution orange. Pass the same yRange to two plots to put them on one scale. |
override fun toString(): String |
table. |
alife_walls.kt
Walls for alife_script.kt: obstacles, swept circle collision, and radius-aware routing around
them.
See alife_script.kt for the scripting layer this completes, and alife.kt for the family
overview.
Why this exists
ScriptWorld shipped as a bare rectangle. The only spatial thing an agent could say was
BehaviourSteering.avoidEdges, which pushes away from the four sides of the arena; everything
else a scripted agent could steer around was another agent. That rules out the whole category of
behaviour that is about the environment — a corridor, a room with a door, a barrier you have to
go round rather than through — and it rules it out silently, because an agent pressing into a
wall that does not exist just flies on.
Three representations
Obstacles are a flat soup of line segments (ScriptObstacle, ObstacleField) and not
polygons-as-solids. The only predicate collision needs is "is this swept circle crossing a wall",
which a segment answers directly; making obstacles solid would mean a point-in-polygon test, a
winding rule, and an answer for what two overlapping solids mean. A soup accepts an open wall, a
concave room, a fence with a gap in it and two shapes that cross, all without a special case. The
price is stated rather than hidden: there is no "inside", so nothing stops an agent that starts
inside a closed shape from staying there — ScriptWorld.spawn pushes it out to the nearest
surface instead of teleporting it somewhere arbitrary.
Collision is swept, not discrete (ObstacleField.sweep). A discrete test asks whether the
agent's new position overlaps a wall, which is false for any agent whose per-step displacement is
larger than the wall is thick — and a wall here is a segment, so it is infinitely thin and a
discrete test can miss it at any speed at all. The swept test solves for the first fraction of the
step at which the moving circle touches, so a projectile at 600 units/s stops at the wall it would
otherwise have crossed in one tick. AlifeWallsGallery's tunnelling figure runs both.
Navigation is a uniform occupancy grid inflated by the agent's radius, searched with A*, then
string-pulled against the exact segment test (NavGrid). The obvious alternative is a
visibility graph, which is exact and gives shorter paths from a much smaller search. It was
rejected on construction, not on search: a visibility graph over a segment soup needs its
corners offset outward by the agent radius, and "the offset corner" is not defined where two
segments cross, nearly touch, or are collinear — which is precisely the geometry that appears
when a user draws a wall twice or butts two rectangles together. A grid has no corners to reason
about: it is built by one distance query per cell per segment, radius-awareness is a single
inflation number, and the one thing it gets wrong — paths that staircase along cell centres — is
removed afterwards by NavGrid.stringPull, which uses the same exact swept test the physics does.
A coarse grid can still over-block a narrow gap; NavGrid.coarse says when that happened rather
than leaving the caller to wonder why there was no route.
Determinism
The same property the rest of alife_script.kt depends on, and the same discipline. Segments
are held in insertion order in flat DoubleArrays. A's open set is ordered by (f, cellIndex),
a total* order, so two cells with equal estimated cost expand in a fixed order rather than in
whichever order a heap happened to leave them. Neighbours are visited through one fixed
eight-element offset table. The nav-grid cache is a LinkedHashMap keyed by the quantised
inflation radius. The per-step route budget is consumed in agent-id order, because drives are
evaluated in agent-id order. Nothing here reads a clock or a hash set.
src/jvmMain/kotlin/afield/simulation/agents/alife_walls.kt — read the source
Used in: Spatial artificial life
Top-level functions
Top-level values
| Declaration | Summary |
|---|---|
const val WALL_SKIN: Double |
How far outside a wall a contact is resolved to. |
const val PUSH_OUT_PASSES: Int |
How many times ObstacleField.pushOut will try to find non-overlapping ground. |
const val PROGRESS_EPSILON: Double |
How much closer to its waypoint BehaviourSteering.route has to get to count as having moved. |
const val WALL_RESPONSE_ITERATIONS: Int |
How many wall responses one integration step may resolve. |
WallResponse
What an agent does when its swept path meets a wall.
Constants: SLIDE, BOUNCE, STOP.
ScriptObstacle
class ScriptObstacle(val name: String, points: DoubleArray, val closed: Boolean)
A wall: a run of points, open (a polyline) or closed (a polygon outline).
| Declaration | Summary |
|---|---|
val points: DoubleArray |
x0, y0, x1, y1, …. A defensive copy; mutating what you passed in cannot move a wall. |
val vertexCount: Int |
How many points the outline has. |
val segmentCount: Int |
How many segments it contributes: one fewer than the vertices, or the same if closed. |
fun outline(z: Number = 0): FLine |
The outline as an FLine, for drawing the world the agents are actually in. |
override fun toString(): String |
WallHit
One swept contact: how far along the displacement it happened, and which way the wall faces there.
| Declaration | Summary |
|---|---|
val t: Double |
|
val nx: Double |
|
val ny: Double |
|
val obstacle: ScriptObstacle |
|
val segment: Int |
|
override fun toString(): String |
ObstacleField
Every wall in a world, indexed as one flat array of segments — the thing the physics and the navigation both ask.
| Declaration | Summary |
|---|---|
val obstacles: List<ScriptObstacle> |
The walls, in the order they were added. |
val segmentCount: Int |
How many segments all the walls come to. |
val isEmpty: Boolean |
True when there is nothing to collide with — the fast path the integrator takes. |
fun add(o: ScriptObstacle) |
Add o. Invalidates the packed index and any NavGrid built from this field. |
fun segments(): DoubleArray |
The packed segments, ax, ay, bx, by per segment, in insertion order. |
fun ownerOf(i: Int): ScriptObstacle |
The obstacle that owns segment i of segments. |
fun sweep(px: Double, py: Double, dx: Double, dy: Double, radius: Double): WallHit? |
The first wall a circle of radius at (px, py) touches while moving by (dx, dy), or null if it touches none. |
fun nearestInto(px: Double, py: Double, range: Double, out: DoubleArray): Boolean |
The nearest wall point to (px, py) within range, written into out as distance, wallX, wallY, normalX, normalY — the normal pointing from the wall toward the query. Returns false and leaves out alone when nothing is in range. |
fun distanceTo(px: Double, py: Double): Double |
The distance from (px, py) to the nearest wall, or Double.MAX_VALUE if there are none. |
fun clearPath(x0: Double, y0: Double, x1: Double, y1: Double, radius: Double): Boolean |
Whether a circle of radius could stand anywhere along the straight line from (x0, y0) to (x1, y1) — the test NavGrid.stringPull shortens a grid path with, and the one BehaviourSteering.route decides it has passed a waypoint on. |
fun lineOfSight(x0: Double, y0: Double, x1: Double, y1: Double): Boolean |
Whether the open segment from (x0, y0) to (x1, y1) crosses any wall — line of sight for a point, with no radius. |
fun pushOut(px: Double, py: Double, radius: Double, out: DoubleArray): Boolean |
Move (px, py) to the nearest position where a circle of radius is not overlapping a wall, writing x, y into out. Returns true if it had to move. |
override fun toString(): String |
NavGrid
A radius-aware occupancy grid over a world's walls, and the A* that searches it.
| Declaration | Summary |
|---|---|
val width: Double |
|
val height: Double |
|
val cols: Int |
|
val rows: Int |
|
val cellSize: Double |
|
val inflate: Double |
|
val coarse: Boolean |
|
val blockedCells: Int |
How many of the grid's cells a circle of inflate cannot stand in. |
val cellCount: Int |
Total cells. |
fun isBlocked(c: Int, r: Int): Boolean |
Whether the cell at column c, row r is unusable (out of range counts as blocked). |
fun colOf(x: Double): Int |
The column containing world x x, clamped into the grid. |
fun rowOf(y: Double): Int |
The row containing world y y, clamped into the grid. |
fun centreX(c: Int): Double |
The world x of the centre of column c. |
fun centreY(r: Int): Double |
The world y of the centre of row r. |
fun nearestFree(x: Double, y: Double, searchCells: Int = 24): Int |
The nearest free cell to (x, y) within searchCells rings, as a packed row * cols + col, or -1. |
fun route(sx: Double, sy: Double, gx: Double, gy: Double, maxExpansions: Int = 20_000): IntArray? |
A* from (sx, sy) to (gx, gy), returning the cell indices of the path including both ends, or null if there is no route inside maxExpansions. |
fun stringPull(path: IntArray, field: ObstacleField, startX: Double, startY: Double, goalX: Double, goalY: Double): DoubleArray |
Turn a cell path into a short list of world waypoints, dropping every one a circle of inflate could have skipped. |
fun blockedFLines(z: Number = 0): List<FLine> |
The blocked cells as one FLine per cell outline — for looking at what the router saw. |
override fun toString(): String |
ScalarField
A scalar quantity spread over the arena — light, a chemical, temperature — that an agent can sense and climb with BehaviourSteering.gradient.
| Declaration | Summary |
|---|---|
fun at(x: Double, y: Double): Double |
The value at (x, y). |
alife_ca.kt
Discrete cellular automata: life-like and outer-totalistic rules on a 2-D lattice, cyclic
multi-state CA, Wolfram's elementary 1-D rules as a space-time raster, and a 3-D life-like CA that
marches out to a TriMesh.
See alife.kt for the family overview and for IntGrid, the lattice everything here steps.
The design commitment worth stating: a rule is a value (LifeRule), separate from the grid it is
applied to. That is what lets the gallery run the same glider under B3/S23 and under a
deliberately-wrong neighbour rule and show that only one of them translates — and what lets
alife_continuous.kt's Lenia stepper be checked against this file's discrete stepper in the limit
where the two must agree.
src/jvmMain/kotlin/afield/simulation/automata/alife_ca.kt — read the source
Used in: Spatial artificial life, Three-dimensional Life, counted and traced
Top-level functions
| Declaration | Summary |
|---|---|
fun lifeRule(notation: String, radius: Int = 1, vonNeumann: Boolean = false): LifeRule |
Parse a life-like rule from the standard B3/S23 notation (or the equivalent 23/3 "survive/birth" form, or S23/B3), returning a LifeRule. |
fun IntGrid.step(rule: LifeRule): IntGrid |
One synchronous generation of rule over this grid, returned as a new IntGrid. The border is resolved by the grid's own IntGrid.wrap flag: a torus, or a bounded board whose outside is dead. |
fun IntGrid.step(rule: CyclicRule): IntGrid |
One synchronous generation of a CyclicRule. |
fun IntGrid.stepTimes(rule: LifeRule, generations: Int): IntGrid |
step applied generations times, returning only the final grid. |
fun IntGrid.stepTimes(rule: CyclicRule, generations: Int): IntGrid |
step applied generations times under a CyclicRule. |
fun IntGrid.history(rule: LifeRule, generations: Int): List<IntGrid> |
Every generation from this one to generations, inclusive of both ends — generations + 1 grids. Keeping the history is what lets a caller detect a cycle, or render a filmstrip, without stepping twice. |
fun IntGrid.runUntilCycle(rule: LifeRule, maxGenerations: Int = 2000): CycleReport |
Step until the grid repeats a state it has already been in, or until maxGenerations. |
fun IntGrid.placedOn(width: Int, height: Int, x: Int, y: Int, wrap: Boolean = true): IntGrid |
Place pattern with its top-left corner at (x, y) on a fresh width x height grid — the standard way to give a small named pattern room to run. |
fun IntGrid.centeredOn(width: Int, height: Int, wrap: Boolean = true): IntGrid |
placedOn with the pattern centred. |
fun cyclicAdvanceFraction(before: IntGrid, after: IntGrid): Double |
The fraction of cells that changed state between before and after — for a CyclicRule, the fraction that advanced, since the only legal change is s -> s + 1. |
fun elementaryStep(row: IntArray, rule: Int, wrap: Boolean = true): IntArray |
One generation of Wolfram's elementary rule rule (0..255) applied to a 1-D row of 0/1 cells. |
fun elementaryCa(rule: Int, width: Int = 201, generations: Int = 100, initial: IntArray? = null, wrap: Boolean = true): IntGrid |
Run elementary rule rule for generations steps and return the whole space-time diagram as an IntGrid: one row per generation, time increasing downward. grid.toImg() is then the classic picture, and grid.toImg().upscaleNearest(k) is the classic picture at a size you can see. |
fun binomialParity(n: Int, k: Int): Int |
C(n, k) mod 2 — the parity of a binomial coefficient, by Kummer's theorem: it is 1 exactly when k's binary digits are a subset of n's, i.e. n and k == k. |
fun sierpinskiReference(width: Int, generations: Int): IntGrid |
The exact space-time diagram of rule 90 from a single central seed, computed from binomialParity rather than by stepping — the reference the CA must reproduce cell for cell. |
fun IntGrid.centerColumn(): IntArray |
The centre column of an elementary rule's space-time diagram, as a bit sequence. |
fun longestRunLength(bits: IntArray): Int |
The longest run of identical values in bits — a crude but real structure detector for a bit sequence. |
fun lifeRule3D(notation: String): LifeRule3D |
Parse a 3-D rule from B…/S… where the counts are comma-separated (they run to 26, so the digit-per-count convention of 2-D notation cannot work). |
fun IntVolume.step(rule: LifeRule3D): IntVolume |
One synchronous generation of a LifeRule3D. |
fun IntVolume.stepTimes(rule: LifeRule3D, generations: Int): IntVolume |
step applied generations times under a LifeRule3D. |
fun IntVolume.asDensityField(smoothRadius: Int = 1, iso: Double = 0.5): ScalarField3D |
Adapt this volume to a ScalarField3D so mesh_sdf.kt's marchingCubes can extract its surface — the bridge from "a 3-D automaton ran" to "here is a mesh you can export, boolean and render". |
fun IntVolume.marchBounds(): AxisAlignedBoundingBox3D |
The march window for asDensityField: the volume's own voxel coordinates, grown by one voxel on every side so the surface of a blob touching the volume edge still closes. |
fun IntVolume.toMesh(resolution: Int = 96, smoothRadius: Int = 1, iso: Double = 0.5): TriMesh |
Surface mesh of this volume's filled voxels, via asDensityField and marchingCubes. Watertight by construction (the marcher shares vertices across cubes), which the gallery checks with TriMesh.isWatertight rather than assuming. |
fun IntVolume.slice(z: Int): IntGrid |
A z-slice of this volume as an IntGrid — the cheap way to look at a 3-D automaton without a renderer, and how the gallery shows a volume beside the mesh marched from it. |
fun IntVolume.depthProjection(): Img |
The volume's occupancy summed along z and normalised to 0..1 — a "density projection", which shows the internal structure a surface mesh hides. |
Top-level values
| Declaration | Summary |
|---|---|
val LIFE: LifeRule |
Conway's Life, B3/S23 — the rule every claim in this family's discrete half is checked against. |
val HIGH_LIFE: LifeRule |
HighLife, B36/S23: Life plus birth on 6, which gives it a small self-replicating pattern. |
val SEEDS: LifeRule |
Seeds, B2/S: nothing survives, so every generation is entirely new. Explosive and never static. |
val DAY_AND_NIGHT: LifeRule |
Day & Night, B3678/S34678: self-complementary, so patterns and their photographic negatives behave alike. |
val DIAMOEBA: LifeRule |
Diamoeba, B35678/S5678: large amoeba-like blobs with ragged, constantly-renegotiated borders. |
val MAZE: LifeRule |
"Maze", B3/S12345: Life-like birth with very tolerant survival, which grows corridor mazes. |
val CYCLIC_LABYRINTH: CyclicRule |
14 states, threshold 1, Moore radius 1 — Griffeath's original cyclic CA parameters. |
val CYCLIC_EXCITABLE: CyclicRule |
Greenberg–Hastings-like excitable medium: 6 states, threshold 3, radius 2 — fatter, slower fronts. |
val LIFE3D_CLOUDS: LifeRule3D |
"Clouds 1", B13,14,17,18,19/S13,…,26 — a 3-D rule that grows soft, rounded, cloud-like solids. |
val LIFE3D_CRYSTAL: LifeRule3D |
B4/S4 — a sparse 3-D rule whose survivors form thin, crystalline, coral-like shells. |
val LIFE3D_BLOB: LifeRule3D |
B5/S4,5 — the 3-D rule that gives compact blobs with a corrugated surface. |
LifeRule
An outer-totalistic two-state rule: a cell's next state depends only on its own state and on how many of its neighbours are alive, not on which.
| Declaration | Summary |
|---|---|
val birth: Set<Int> |
Neighbour counts at which a dead cell becomes alive. |
val survive: Set<Int> |
Neighbour counts at which a live cell stays alive. |
val radius: Int |
Chebyshev (or, with vonNeumann, Manhattan) neighbourhood radius; 1 is the classic 8-cell Moore neighbourhood, larger radii give the "Larger than Life" family. |
val vonNeumann: Boolean |
true counts only the diamond \|dx\| + \|dy\| <= radius instead of the square. |
val notation: String |
The B…/S… string this rule was parsed from, or a reconstruction of it. |
val neighborhoodSize: Int |
How many cells the neighbourhood contains, excluding the centre. |
fun next(state: Int, neighbors: Int): Int |
The next state of a cell currently in state with neighbors live neighbours. |
override fun toString(): String |
CycleReport
Result of running an automaton until it repeats.
| Declaration | Summary |
|---|---|
val period: Int |
The cycle length, or 0 if none was found within the step budget. |
val firstSeenAt: Int |
The generation the repeated state was first reached (so period == 1 and firstSeenAt == 0 means the seed was already a still life). |
val generations: Int |
How many generations were actually stepped. |
val finalState: IntGrid |
The grid at generations. |
val population: IntArray |
The population at each generation, 0..generations. |
val settled: Boolean |
true when a repeat was found within the budget. |
override fun toString(): String |
LifePatterns
Named Life patterns, written as ASCII so they can be read rather than decoded.
| Declaration | Summary |
|---|---|
val glider: IntGrid |
The glider: the smallest spaceship, translating (1, 1) every 4 generations. |
val block: IntGrid |
The block: the smallest still life; a step must leave it bit-identical. |
val beehive: IntGrid |
The beehive, a six-cell still life — a second still life, so the block is not a lucky special case. |
val blinker: IntGrid |
The blinker: period 2, so it is the control that a "still life" check must fail on. |
val toad: IntGrid |
The toad: period 2, and unlike the blinker it is not a subset of a line. |
val rPentomino: IntGrid |
The R-pentomino: the classic long-lived methuselah — 1103 generations to a population of 116. |
val acorn: IntGrid |
The acorn: a 7-cell methuselah, 5206 generations to a population of 633. |
val lightweightSpaceship: IntGrid |
The lightweight spaceship: translates (2, 0) every 4 generations — orthogonal, not diagonal. |
CyclicRule
A cyclic cellular automaton rule: a cell in state s advances to (s + 1) mod states as soon as at least threshold of its neighbours are already in state (s + 1) mod states; otherwise it waits.
| Declaration | Summary |
|---|---|
val states: Int |
Cycle length, >= 2. |
val threshold: Int |
How many neighbours in the successor state are needed to advance. |
val radius: Int |
Neighbourhood radius. |
val vonNeumann: Boolean |
Use the diamond neighbourhood rather than the square. |
override fun toString(): String |
IntVolume
A cubic lattice of integer cell states — the 3-D counterpart of IntGrid, stored cells[(z * height + y) * width + x].
| Declaration | Summary |
|---|---|
val width: Int |
Cells across, >= 1. |
val height: Int |
Cells down, >= 1. |
val depth: Int |
Cells deep, >= 1. |
val cells: IntArray |
State per voxel. |
val wrap: Boolean |
true for a 3-torus; otherwise outside counts as state 0. |
val size: Int |
Number of voxels. |
fun index(x: Int, y: Int, z: Int): Int |
Flat index of (x, y, z). No bounds checking; see get. |
operator fun get(x: Int, y: Int, z: Int): Int |
State at (x, y, z), resolving out-of-range coordinates through wrap (otherwise 0). |
operator fun set(x: Int, y: Int, z: Int, v: Int) |
Write v at (x, y, z) if it is inside; out-of-range writes on a bounded volume are ignored. |
fun blank(): IntVolume |
A same-shape volume of zeros. |
fun population(): Int |
How many voxels are non-zero. |
fun mooreCount(x: Int, y: Int, z: Int): Int |
How many of the 26 surrounding voxels are non-zero. |
override fun toString(): String |
LifeRule3D
class LifeRule3D(val birth: Set<Int>, val survive: Set<Int>)
A 3-D outer-totalistic rule over the 26-voxel Moore neighbourhood — the direct generalisation of LifeRule. Notation is the same B…/S…, with counts up to 26, so B5/S45 is written LifeRule3D(setOf(5), setOf(4, 5)).
| Declaration | Summary |
|---|---|
val birth: Set<Int> |
Neighbour counts at which an empty voxel fills. |
val survive: Set<Int> |
Neighbour counts at which a filled voxel stays filled. |
val notation: String |
The B…/S… string for this rule. |
override fun toString(): String |