PETRI DISH GAMES

The elite archive

The rule the structure pool culls by: keep the best occupants of each region of behaviour space rather than the best individuals overall.

The elite archive is the rule the site’s own structure pool culls by: keep the best two occupants of each region of behaviour space rather than the best individuals overall. It exists because ranking death by score alone deletes outliers, since an outlier is unrated far more often than it is bad, and what survives that ranking is one structure in twenty copies. The pool is an emergence loop of about two dozen individuals, run in one visitor’s browser, bred against that visitor’s own use of the page.

The partition

A niche is one region of behaviour space, and it is the address a judged individual is filed under. src/exotic/niche.ts owns the partition and nothing else: it holds a committed centroid table, the rule that places a point in it, and the two numbers a birth is steered by. The coordinate being partitioned is a measured position derived from what a visit did, not a property of how a genome is written.

ConstantValueWhat fixes it
Regions in the partition32One per possible occupant: the pool’s carrying capacity of 24 plus a backlog of 8 is its hard ceiling.
Individuals one region holds2Below the clone-family cap of 3, because sharing a niche is a stronger claim of sameness than sitting inside a genotype ball. Above 1, because the day-one descriptor separates behaviour only noisily.
Neighbours per region6A Voronoi partition has no grid adjacency to read off, so the neighbourhood is the six nearest other centroids, committed as a table.
Axes in the space5Five bits of address over 32 regions, matching the pool ceiling.

The partition is a centroidal Voronoi tessellation and not a grid, which decouples the region count from the number of axes. A regular grid over five axes binned three ways would be 243 cells, every individual would take a private one, nothing would ever overflow, and the archive would be a no-op with every test green.

Addressing is a nearest-centroid lookup. nicheOf takes a descriptor and returns the index of the closest centroid by squared Euclidean distance, with ties going to the lower index so a point on a bisector reports the same region every time. It returns -1 when the descriptor is missing, the wrong width, or not finite, and -1 is a real answer rather than a failure: a genome nobody has judged has no position at all.

The centroids are baked offline by scripts/bake-niches.mjs, which draws 4096 synthetic outcome vectors, runs k-means-plus-plus and 24 Lloyd relaxations over their descriptors, rounds the result and sorts it lexicographically, then rewrites the table between markers in the source. Nothing is normalised at runtime. A bound derived from the records currently on disk would move when a record arrived, and a moved bound would move every niche id, which would make the partition a function of arrival order. The table carries the basis string it was fitted in, and a mismatch against the live basis refuses the whole table: nicheOf then answers -1 for everything and the pool ranks death by the rule it had before.

The three populations in one pool

The pool is one array holding three sets, and only one of them is an archive.

SetWhat it holdsWhat ranks it for death
ArchiveIndividuals with a niche, all of which have been judged at least once.Niche occupancy. Everything past the second occupant of a region goes to the front of the death queue.
QueueIndividuals with no trials. Bounded by the backlog limit.Nothing. Unjudged individuals are never culled.
ResidualJudged individuals with no descriptor.The clone family, unchanged.

The residual is not an edge case. It covers a visitor whose browser refuses persistence, a pool that predates the record log, and a genome whose records have fallen off the front of the log’s ring.

The durable archive is the record log, not the pool. The pool holds at most capacity plus backlog and deletes the rest; src/exotic/records.ts keeps a ring of 300 outcome records under a key derived from the pool’s own, each carrying the code-stripped serialised genome of a structure that may be long dead and that parseGenome can rebuild. The pool is the live front of that log.

Births halt while the queue is full, and that gate is what bounds everything else. Children arrive one per verdict and cannot die until they have been judged, so refusing to make more of them is the only thing that can bound the pool. The bound is capacity plus backlog, which is what keeps the stored payload a known size.

The clone-family rule was kept rather than replaced. A partition over behaviour can never place a genome nobody has judged, because a descriptor exists only after a visit has been scored, so familyOverflow remains the pool’s only defence against a flood of near-copies among unjudged children. Both overflow sets are built over the whole pool, so an unjudged child still counts as a member of its family even though nothing can cull it yet.

Death order

cull runs once per verdict, inside the same call that records the fitness. It ranks the cullable set by overflow first and by score second, which is the one place score does not decide: a fourth near-copy of a survivor dies before a genome that is merely rated badly.

Which kind of copy an individual is depends on the individual and not on the pool. Where a genome has been placed in behaviour space, the niche it shares decides, because that is evidence about what it did. Where it has not, the clone family decides, because a genotype ball of radius 0.05 is still the best evidence there is about a genome nobody has used yet. The two rules combine per individual rather than compete per pool.

Below the overflow tier the sort key is unchanged: fitness plus 1.5 times a redundancy term, where redundancy is the distance to the nearest better-scoring peer and an individual nothing outranks takes the full term. The weight outweighs the entire 0..1 fitness range deliberately, so a near-duplicate of a better peer dies ahead of a low-scoring original.

Four limits bound what one pass can take.

  • A trial floor. Nothing with fewer than two trials is cullable, loosened to one once the pool reaches 1.3 times capacity. Nothing unjudged is reachable by a cull at all.
  • The structure and the rival on screen. A verdict is already being paid for on both.
  • A pool floor of four. No death may take the pool below it, and the floor overrides the archive, because a pool of two cannot recombine its way out of anywhere.
  • A take of half the cullable set. The crowding pass takes the smaller of the overshoot and half the cullable set rounded up, and senescence adds at most two deaths after that.

One term is deliberately absent from the sort key. nicheDistance is exported and tested and not used in the death score. Redundancy starts at 1 and is only lowered against better-scoring peers, and a described individual has only other described individuals to be lowered against, so a behaviour term would return 1 almost always and hand every described individual the full redundancy weight for being described at all. The pool would then delete its undescribed members first, which is the exact failure the rule exists to prevent.

Senescence is the deliberate counterweight to the archive, and the two disagree on purpose. Strict MAP-Elites keeps a bad elite in a lonely region forever; in a pool of two dozen that is a permanently spent slot and a permanently spent trial budget, so an individual with at least four trials sitting a margin below the pool’s median fitness is taken anyway, at most two per pass.

scripts/test-exotic.mjs holds both halves of the headline as a pair. An outlier alone in an empty niche survives a cull it loses without the record that placed it, and a genome whose behaviour turns out to be the fourth copy of something the pool already holds twice dies for it while the old genotype rule keeps it.

The steered birth

Nothing aims at a chosen empty region. The map from a genome to its behaviour runs through a visitor and is not invertible, so no function carries a chosen empty niche back to a genome that lands in it. What is achievable is where a birth starts and how far it steps.

A frontier reading is how much emptier one niche’s neighbourhood is than the pool’s own, in 0..1, and it is relative on purpose. An absolute empty-neighbour fraction inverts the property it exists to provide: against 32 regions and the handful of described individuals a live pool holds, nearly every neighbour is empty, so every parent would read as a frontier parent and the pool would breed differently from the first birth. The reading is taken against the mean local emptiness over the occupied niches instead, and the occupied set is summed in ascending id order so the answer is a function of the set rather than of the order a caller filled it in.

The reading is spent in three places, each of which is exactly the pre-archive behaviour at a reading of zero.

  • Parent selection. The tournament key is fitness plus a weighted frontier term, so a parent sitting where its peers are not is worth a little more than its score says.
  • Crossover share. Crossover draws whole coefficient groups from two parents, so a crossed child lands between two niches the pool already holds. Its share falls as the reading rises.
  • Mutation rate. Mutation drifts outward from one parent, and step length is the only thing here that decides whether a child leaves its parent’s niche at all. The rate rises with the reading.

A birth taken while the pool holds fewer than four distinct organisms goes back to the snapshot for a founder instead, and never reads the log. That quota is a genotype guarantee and behaviour space cannot supply it: no operator here can reinvent a terrain the pool has lost.

What is stored and what is recomputed

A niche id is never persisted. It is a pure function of a descriptor and a committed table, so a stored copy would be a cache of a derivation with a silent staleness mode and no saving at all. The descriptor is persisted instead, rounded to four decimals and stamped with the version of the space it was computed under, and a descriptor stamped with anything else is dropped whole rather than reinterpreted. That version is derived from the descriptor basis rather than typed in by hand, so training the first encoder cannot silently place old coordinates in a new space.

Held whereWhat it holds
Pool entrySerialised genome, mean fitness, trial count, last-seen time, and the descriptor of the best record the genome has produced with its version.
Record logUp to 300 outcome records, oldest trimmed first, each with its own schema version and its code-stripped genome.
RecomputedThe niche id, the occupied-niche set, both overflow sets, and every frontier reading.

The join between the two is a hash of the code-stripped serialised genome and not an id, because ids are reissued once a pool has forgotten them. The log is read once per pool, lazily, from the cull and the birth paths only, so a readout never pays for it. A descriptor is also handed straight into the verdict, because the record that produced it is not in storage yet at that moment and the cull runs two lines later.

The elite rule inside a genome’s own history is the niche of its highest-scoring record, not the mean of its descriptors and not the latest. A mean of two positions is not the position of any behaviour, and the latest re-addresses the genome on every trial.

What is live and what waits on records

Live today: the partition, the placement rule, the guards on both tables, the join between the pool and the log, the overflow tier, the frontier reading and all three places it is spent. Every record a visit produces is written, and the descriptor is computed from it in the same call that closes the trial.

Inert until records exist, which on a first visit is always:

  • The archive is empty. There are no outcome records on a device that has not been visited, so every individual is unplaced, no niche is occupied, and every frontier reading is zero. The pool culls by clone family and by score, exactly as it did before the archive existed, and it breeds with the same crossover share and mutation rate it used then.
  • The encoder is not trained. The shipped weights artifact carries no weights, so a descriptor is a fixed seeded orthonormal projection of the outcome vector rather than a learned encoding.
  • The centroids were fitted to an assumption. The bake had no records to cluster, so it drew from independent uniforms per axis corrected by the degenerate branches the record format documents. The numbers that say whether the assumption was right are the count of occupied niches and the largest occupancy, which the test prints rather than asserts on, and the fix once records exist is a re-bake rather than a correction factor.

A wrong prior degrades into an effectively smaller partition rather than into a wrong one, and a refused table degrades into the rule the pool already had. Neither failure is silent and neither stops the region from growing.

Bound to

The source this page's claims were read against, named by symbol and pinned by the digest of the file it lives in.

SymbolDigest
bake-niches.mjs unbound
BEHAVIOUR_BASIS unbound
closeTrial unbound
frontier unbound
loadPopulation unbound
nicheOf unbound
readRecords unbound
test-exotic.mjs unbound

Near this subject

These are the subjects this one is joined to in the field. Step out of this card and they are the wells around the one you are standing in.