PETRI DISH GAMES

Behaviour space

The small coordinate space a bred artifact is placed in by what happened when somebody used it, rather than by what its genome contains.

Behaviour space is a small coordinate space in which a bred artifact is placed by what happened when somebody used it, rather than by what its genome contains. A point in it is a descriptor: five numbers computed from the twenty-entry outcome vector of one outcome record. Two artifacts whose parameters are unrelated but whose use came out the same land in the same place, which is a collapse a fitness score cannot perform.

The descriptor

descriptor() in src/exotic/behaviour.ts is the only function that turns an outcome vector into a position. It is total: exactly five finite numbers come back for any input, including a short vector, a long one, one carrying NaN, and a value that is not an array. Entries are clamped into 0..1 and centred on the half before either branch runs, because both branches are measured on centred inputs.

descriptorOf() takes a whole record and answers null when the record cannot supply a position: a record written under another schema version, or one with no interaction. Null is an answer rather than a failure, and the caller’s job is to fall back rather than to invent a coordinate.

The space has five dimensions because two to the fifth is thirty two, and thirty two is the elite archive pool’s own hard ceiling: a default capacity of twenty four plus a backlog of a third of it. Four bits would leave half the pool sharing addresses permanently, and eight would mean no two individuals ever collide, which would stop an occupancy rule firing at all.

Every sum runs in ascending index order and the arithmetic is confined to the operators ECMAScript requires to be exact or correctly rounded. Softsign stands in for tanh on that ground alone, since a library difference in a transcendental would put a different coordinate on a different machine for the same record.

The two branches

Two branches produce a point under one convention. Which one runs is decided by whether src/exotic/behaviour.json holds usable weights.

The projection, which is what runs todayA trained encoder
Where the five numbers come fromA fixed seeded orthonormal projection of the outcome vectorThe bottleneck of an autoencoder trained over a corpus of records
What the axes areDirections fixed by a seed, carrying no meaning of their ownWhatever directions explain the variance in what visitors actually did
Bin edgesThe default cuts, a single edge at zero on each axisEdges measured from the training corpus, per axis
Named regionsNonek-means clusters over the descriptors, each carrying a name and a gloss
BEHAVIOUR_BASISprojection, plus the format, the key count and the dimension counttrained, the same three, plus the digest of the weights

The projection is a real descriptor and not a stand-in. The draw is Rademacher, one sign per entry; modified Gram-Schmidt over the rows in index order makes them orthonormal; the rows are then scaled so that the expected squared distance between two descriptors is a stated multiple of the squared distance between their inputs. That scale is the square root of twelve, which is derived rather than tuned: under the only prior available with no records, each entry of an outcome vector independent and uniform on 0..1, an entry has variance one twelfth, and the square root of twelve carries the component along any unit row to unit variance. The trained branch’s variance floor pushes its own axes toward the same scale.

What survives the swap to a trained encoder is that convention, decorrelated axes at unit scale. The numbers do not survive it. A trained encoder’s row space is not a rotation of a fixed random one, so the first appearance of trained weights re-addresses every stored descriptor and no fit softens it. Retrainings after that are different: the bottleneck is linear, an orthogonal rotation folds into the encoder’s second weight matrix at no cost in reconstruction, and a retrained artifact can therefore be aligned onto its predecessor over a shared anchor set instead of moving everything.

Two bins per axis is one bit of address per axis, and five axes address thirty two cells. A coordinate exactly on an edge falls into the lower bin, which is a case the corpus contains rather than a boundary nothing reaches, since the untrained edges are all zero and an all-neutral outcome vector produces a descriptor of exactly zero. Measuring edges from a live pool is refused: on a corpus that is half one repeated vector, the median sits on the repeat and the whole mode crosses to the same side of every axis at once, and a measured run of that shape filled fewer cells than the fixed edges did. Calibration is a training-time operation and is called by no page.

A stale or damaged artifact is rejected whole rather than repaired. usableWeights checks the format, the schema version, the key count and the shape of every matrix, and a single failure returns null and falls the build back to the projection. A half-read set of weights would place every individual somewhere plausible and wrong.

Training, and where it runs

Training runs from scripts/build-behaviour.mjs, which a person invokes. No build step and no page reaches it. Training inside a site build would make the deployment depend on a corpus that is not in the repository and would move the artifact without a reviewed diff; training in a browser would give every device its own axes, so no two visitors’ addresses would name the same behaviour, and a concept discovered inside one session could not be read by anybody.

The corpus is exported by hand out of the record logs a visitor’s browser holds. The log keeps at most three hundred records per key, so the usable range is one hundred and fifty to six hundred records rather than an open-ended few hundred.

A run that fails an entry check writes nothing, exits non-zero, and leaves the committed artifact byte-identical. The checks are:

  • Fewer than one hundred and fifty usable records.
  • Fewer than one hundred and twenty distinct outcome vectors, since one duel writes two records for a single trial.
  • More than sixty per cent of the corpus being one repeated vector.
  • Fewer than three outcome keys moving at all.
  • Explained variance under a half once training has finished.
  • Aligned drift past its bound, or too few addresses kept, unless the run is forced.

The network is twenty inputs to twelve hidden units to five bottleneck dimensions and back out symmetrically, with softsign on both hidden layers and a linear bottleneck and output. The optimiser is full-batch gradient descent with momentum: no minibatches, no shuffling, a fixed epoch count and no early stopping, so run-to-run variation has no source. Axes come back ordered widest first, ties to the lower index, which is what lets two runs be compared column by column. A rerun over an unchanged corpus produces byte-identical output, and the artifact carries no date field anywhere, because a timestamp would make that comparison impossible.

Collapse, and the two terms in the objective

A joint-embedding objective has one dominant failure: everything maps to the same representation. Here that renders as a space with no cells, and it is invisible from the loss alone, because a collapsed encoder produces a small reconstruction error rather than a signal of its own. Both countermeasures are therefore terms in the objective rather than checks run afterwards.

  • The variance floor penalises each bottleneck dimension whose standard deviation falls below the target, which keeps an axis from folding.
  • The covariance penalty sums the squared off-diagonal entries of the bottleneck’s covariance, which stops a spare axis meeting the floor by copying a live one. Decorrelated axes are also the reason an axis carries information the others do not.

Collapse takes two shapes and each gate catches only one of them. Two axes can fold beside healthy neighbours, keeping a small spread relative to the survivors; or every axis can shrink together in proportion, leaving the ratios between them intact and an order of magnitude of scale gone. aliveAxes applies both gates: an axis must clear a fraction of the widest axis’s spread, and it must also clear a fraction of the unit scale the format promises. Occupancy cannot state this. Edges measured as quantiles split a folded axis at its own median and report a full grid for a space that has lost two of its five directions.

Each assertion in scripts/test-behaviour.mjs is paired with a second training in which the term it depends on is set to zero: with the covariance penalty the axes are decorrelated and without it they are not, with the variance floor every axis holds a spread and without it one folds and the alive-axis count drops. Weight decay is tested separately and is not a substitute for the floor. An assertion that still passes with its countermeasure deleted reports a guarantee nothing provides.

The three model jobs

Three jobs are separated because they fail differently.

JobInputOutputCost of being wrong
MutatorElite genomes or codeA proposed variantA bad variant scores badly and dies.
DescriberOne outcome recordA sentence about what happenedA bad label pollutes one dimension.
NamerA cluster of outcomesA name for the regionA bad name misleads a reader.

No mutator of that kind exists. Mutation is arithmetic in src/exotic/operators.ts, and nothing under src/exotic makes a network call.

The Describer and the Namer are implemented as arithmetic in src/exotic/describe.ts, which ranks an outcome vector’s entries by how far each sits from the neutral half and writes a clause for each of the largest. scripts/name-behaviour.mjs wraps that offline answer in an optional model call, and a missing API key changes which sentence is written rather than whether one is written. No module under src/ calls a model at all.

A model is never the fitness function. Under search pressure a judge is selected against, and optimisation concentrates wherever the judge is wrong. Describing and naming are safe precisely because nothing selects on them: a name is a caption on a region, not an input to anything that ranks. For the same reason the Describer’s sentence is not yet embedded into the outcome vector, which would put a model-derived coordinate inside the space the archive selects on.

What is live, and what waits on records

The machinery ships before it can do anything.

Live now:

  • A descriptor is computed on every recorded trial. The record is built at the close of a trial and its descriptor is handed to the pool before the cull runs, so a descriptor arriving later would arrive one cull too late.
  • The projection branch is the branch that executes, because the committed src/exotic/behaviour.json carries an empty key list and is refused by usableWeights.
  • A visit is recorded when it acquired something, gave a verdict, or stayed mounted at least 1200 ms. Anything below that bar is not written down and produces no position.

Waiting on records:

  • No encoder in this repository has been trained. There are zero outcome records on disk, and every figure the tests report comes from synthetic seeded corpora rather than from visits.
  • The artifact holds no calibrated edges and no clusters, so no region of the space has a name or a gloss, and the trained branch does nothing whatever until somebody runs the build script over a real corpus by hand.
  • A fresh pool holds no records, so on a first visit descriptorOf answers null for every founder and whatever reads it falls back to the rule the site already had.

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
behaviour.json unbound
build-behaviour.mjs unbound
closeTrial unbound
describeOffline unbound
descriptor unbound
outcomeVector unbound
projectionMatrix unbound
test-behaviour.mjs unbound
trainBehaviour 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.