vibespire.ai / Culture /

how it's made

open the dish β†—

Vibespire presents

How Culture Is Made

Nothing in this program knows what a cell is. There are six kinds of particle, a table of thirty-six numbers saying how each kind feels about the others, and one rule about distance. No membrane is drawn and no nucleus is placed anywhere. What follows is the five things that turned out to have to be true before anybody looking at it uses the word: two of them physics, one a camera, one a census that runs after the fact, and the last a rule about what the interface is allowed to claim.

36 NUMBERS ONE RULE NOBODY DREW THIS Ξ² peak

Β§1 Β· Introduction

What the program contains, and what it has to fake

Every particle belongs to one of six species. Each species holds one opinion about each of the six: a single number between βˆ’1.4 and 1.4, positive for attraction and negative for repulsion. Thirty-six numbers, and that is the whole content of a world. Change one entry and you are somewhere else entirely.

Sixty times a second, every particle asks every neighbour inside one interaction radius the same question and adds the answers up. Write r for the distance in interaction radii and a for the matrix entry, and the whole force law fits in four lines:

r < Ξ³        f = (Ξ³/Ξ² βˆ’ 1) βˆ’ ((Ξ³/r)⁴ βˆ’ 1)     hard floor, universal
Ξ³ ≀ r < Ξ²    f = r/Ξ² βˆ’ 1                       soft core, universal
Ξ² ≀ r < 1    f = a Β· (1 βˆ’ |2r βˆ’ 1 βˆ’ Ξ²| / (1βˆ’Ξ²)) the table
r β‰₯ 1        f = 0

Ξ² is where the universal core ends. It is 0.3 by default and runs from 0.25 to 0.41 across the worlds that ship. Ξ³ is 0.45 Ξ². Below Ξ² nobody gets an opinion: the repulsion is universal and every species obeys it. Above Ξ² the table takes over as a tent, peaking at (1+Ξ²)/2 and falling to nothing at one radius. Velocities are then damped toward zero with a half-life of a few tens of milliseconds, 39 to 77 ms across the shipped worlds, and positions integrate. Exactly one quantity is clamped anywhere: no particle may cross more than a third of an interaction radius in a step. Past that, the neighbourhood it was solved against has stopped being true, and a hot world tunnels straight through its own core.

The opinions are not symmetric, and this is where all of the behaviour comes from. Row i, column j is how i feels about j, and nothing requires it to match how j feels about i. A pair where verdant chases rose while rose runs from verdant cannot settle into anything. It has to keep moving. Give that pair a shell and it is a swimmer; leave it bare and it orbits, because orbiting is the only stable answer to a chase neither side can win. Every kind of body in the field guide is a consequence of some lopsided pair, and not one of them is named in the code that produces it.

The law is not mine. The tent function, the Ξ² core and the asymmetric matrix are Jeffrey Ventrella's Clusters, by way of the formulation CodeParade popularised, and they are what most particle-life projects run. What this project adds starts one line further up, at the hard floor underneath the core, and everything after this section is downstream of it.

That is the entire simulation. There is no membrane in it, no interior, no individual, no identity, and no vocabulary. Getting from that to a dish somebody reads as biological turned out to need five separate things to be true, and I got each of them wrong at least once first. They are what the rest of this article is:

Β§2 Β· Requirement one

Matter needs a size

A body has an inside. Nothing has an inside until there is a distance below which two particles will not go, and the textbook force law does not provide one at scale.

src/sim/world.ts

Β§3 Β· Requirement two

There has to be somewhere to be separate

Two bodies read as two only if there is black between them. Whether the world has room for that is arithmetic, not taste, and the first version failed it.

src/sim/universe.ts

Β§4 Β· Requirement three

A body needs an edge

Something has to distinguish an interior from a rim. One mechanism here is real physics and the stronger one is the camera, and they are easy to confuse.

src/render/glow.ts

Β§5 Β· Requirement four

It has to still be there a second later

An individual is a thing that persists. Since the simulation has no concept of a body, something separate has to go and find them, then match them up across time.

src/sim/clusters.ts

Β§6 Β· Requirement five

You have to be able to ask what it is

The last layer is language, and it is the one that can undo all the others: a caption that claims more than was measured turns the whole thing back into a screensaver.

src/ui

Two more sections follow those five. Β§7 is the offline search that picks which of those thirty-six-number tables you are actually shown, and Β§8 is the control experiment: netting a single body out of the dish into a jar to find out whether it was ever one thing. Every figure below runs the showcase's own code rather than a reimplementation for the article, and every number is from the source or from a bench run that is checked in.

Β§2 Β· Method Β· Requirement one

Giving the particles a size

Start with the smallest useful experiment. Two particles of a species that likes itself, released three quarters of an interaction radius apart, every matrix entry set to 1.4, Ξ² at 0.3. test/sanity.ts runs it for fifteen simulated seconds and measures the gap.

They settle at 0.300 radii. Exactly Ξ², which is where the ramp crosses zero, and they settle there whatever the attraction is set to: above Ξ² the tent is what pulls, below Ξ² the ramp is what pushes, and the two meet at a point neither of them controls. That distance is the diameter of matter in this world. Everything the rest of the article calls anatomy is a stack of it.

The textbook core is one line, f = r/Ξ² βˆ’ 1, a straight ramp from βˆ’1 at zero distance up to 0 at Ξ². The hardest shove one particle can give another is 1, exactly the same magnitude as the strongest pull. At a thousand particles that is completely fine. At twelve thousand it falls apart, and the reason is arithmetic rather than bad luck.

Take a clump at number density ρ and ask what pressure holds it apart. Both sides of the balance are sums over pairs, so both go as ρ². The cohesive pressure from the attractive band is roughly Ο€ΟΒ²Δβˆ« tent(r) rΒ² dr over [Ξ², 1]. The repulsive pressure from a ramp core is Ο€ΟΒ²βˆ« (1 βˆ’ r/Ξ²) rΒ² dr over [0, Ξ²], which evaluates to (Ξ²R)Β³/12. Divide one by the other and the density cancels. Whether repulsion wins does not depend on how tightly packed the clump already is, so a matrix where attraction wins never finds a spacing to stop at. It just keeps going.

It does not announce itself as a physics problem. Measured at twelve thousand particles on the first four worlds an early search turned up: 3,600 neighbours inside one interaction radius, nearest-neighbour spacing at a tenth of Ξ², and a step costing between 47 and 176 milliseconds. I spent most of a day looking for a bug in the spatial grid. There wasn't one. The grid was working perfectly. There were genuinely that many neighbours to visit.

The fix is to make the repulsion integral diverge, so that pressure grows faster than ρ² and there is a real crossing point. Below γ = 0.45 β the ramp is replaced by (γ/r)⁴, clamped at 60. The integral stops converging, pressure goes as ρ2.5, and matter acquires a size. Same worlds, nothing else changed: 7 milliseconds a step.

Figure 1 β€” the force law, plotted by forceAt() from src/sim/world.ts: the readable copy of the arithmetic the step loop has inlined, which a sanity test keeps honest against it. The dashed line is the textbook ramp. Turn the hard floor off to see how little there is between the two of them, and drag Ξ² to watch the whole attractive band slide.

Both halves of that core turn out to matter, which I found out by overcorrecting. An early attempt replaced the whole core with 1 βˆ’ (Ξ²/r)⁴ rather than only its floor. That is stiff all the way out to Ξ², so it competes with attraction across the entire range where attraction is supposed to be deciding what shape things are. The result was a jammed, motionless, perfectly uniform foam that filled the world edge to edge and scored 27 out of 100. The soft ramp has to survive where it does its work. Only the floor underneath it needed replacing, and the whole of the change is one constant: CORE_INNER = 0.45.

One visible consequence, since it is the kind of thing that is easy to mistake for a regression. Structures under the fixed law are grainier than the near-solid blobs the broken version produced, and you can count the individual dots in a ring. That is also what a real photograph of this sort of thing looks like, so I stopped trying to get the smoothness back.

Result. The textbook law describes particles with no size. Two of them under the fixed law sit at 0.300 radii and stay there, and every structure in the rest of this article is built out of that one distance.

Β§3 Β· Method Β· Requirement two

Leaving room for things to be separate

The second requirement is the interaction radius, and it is not a taste question at all. Set it wrong and no force law will save you, because there is physically nowhere for separate structures to go.

A body packs at roughly Ξ² interaction radii between neighbours. That is what the core now guarantees, and Β§2's two particles are the proof of it. So the fraction of the world its bodies occupy is Ο† = 0.866 Β· n Β· Ξ²Β² Β· rmaxΒ²: the hexagonal packing area of one particle, times the population. Put the radius the first version shipped with through that formula β€” 0.028 of the world width at twelve thousand particles, at a 16:10 viewport β€” and Ο† comes out at 1.17.

Which is to say: the population, packed as loosely as the core permits, needs more area than the world has. There is no room for a void anywhere, and the correct answer to "where do the bodies go" is that there are no bodies, only foam. It was not a tuning problem and no amount of matrix search would have fixed it. DEFAULTS.rMax is now 0.013, which puts Ο† at about a quarter: a world that is three parts black.

The radius does not set the neighbourhood, which is the part that took me longest to believe. The number of neighbours a particle has out beyond its own core is Ο€(1 βˆ’ Ξ²Β²) / (0.866 Ξ²Β²) β‰ˆ 37 whatever the radius is, because the two cancel. Turning that dial is not adjusting how strongly things interact. It is adjusting how much emptiness there is for them to be separate in.

So the radius is stored against a reference population of 12,000 and rescaled by √(REF_COUNT / n) whenever a world is built at a different size, which holds the neighbourhood constant rather than the radius. One consequence is worth stating outright because it sounds wrong the first time: bodies keep their size in particles and lose it in world units. Benched on one universe at 3,500 and at 18,000 particles, the census reports 26.6 bodies per thousand at both, a median body of 24 particles against 25, and a packing of 24Γ— against 26Γ—. Triple the population and you get three times as many bodies, each the same size, each smaller on screen.

That is where the default population comes from, and it is a framing decision more than a performance one. Twelve thousand fills the frame with several hundred specks whose membranes you cannot resolve. Six thousand, framed slightly tighter than the whole torus, puts about ninety bodies on screen at ninety device pixels across, and ninety pixels is the scale at which a cell reads as a cell. A phone drops to 4,500 and rests the camera tighter still, 1.85Γ— rather than 1.45Γ—, because a portrait viewport renders that same body about thirty CSS pixels across, and thirty is where a cell and a plain lump stop being distinguishable. That distinction is the one thing the whole interface is asking you to make, so it sets the zoom.

test/sanity.ts guards the invariance directly. The same universe run at 5,000 and at 15,000 particles has to agree on occupancy to within 35%, on bodies-per-thousand to within 45%, and on its overall score to within 18 points. Before that check existed, the offline search shipped a list of worlds that all "died" the moment they were re-scored at the population anybody would actually run.

One disclosure about what you are watching during the first half-minute. The dish does not begin full. It is inoculated as a droplet at two and a half times the final density, and the rest of the population arrives at the frontier as the colony spreads, which keeps local density roughly constant instead of crushing six thousand particles into a disc a tenth of the width. That spreading is on a clock: twenty-six seconds to the edges, not emergent. The first version measured the cloud's real radius and followed it out, which works for a world that wants to expand and does nothing at all for a cohesive one. Three random worlds in a row sat as a bright speck holding a quarter of their population, at 6% occupancy and a packing of 600. grow() now widens a disc at whatever rate keeps the arriving density constant, regardless of what the matrix would have preferred.

Figure 2 β€” a real World at 2,600 particles, drawn by the showcase's real Glow renderer, censused and scored by the real vitals(). Nothing here is a recording. The first three are from the shipped list; "a random table" draws thirty-six fresh numbers, and is a fair sample of what Β§7 is sifting through.

Result. The interaction radius looks like a strength dial and is really a void-fraction dial. At Ο† = 1.17 there are no individuals to find, whatever the thirty-six numbers say.

Β§4 Β· Method Β· Requirement three

Where the membrane comes from

Two different things in this project put a rim around a body, and only one of them is physics. The stronger one is the camera, and it is worth walking through in detail because it is doing more of the work of the illusion than anything in the simulation.

Every particle is a single point sprite with a tight core and a wide weak skirt, exp(βˆ’5dΒ²) + 0.22Β·exp(βˆ’2.6d) with d running 0 to 1 across the sprite, drawn with blendFunc(ONE, ONE) into an RGBA16F buffer that is allowed to run far past white. Nothing is clamped until the final pass, where an exposure curve brings it back: 1 βˆ’ eβˆ’cΒ·exposure per channel, then a 1/2.2 gamma.

Call A the accumulated brightness at a pixel before the tonemap sees it. One particle at the default intensity peaks at A = 1.83 dead centre. The tonemap saturates each channel independently, and each channel carries the species' own weight β€” azure is (0.14, 0.89, 1.0) β€” so azure's red needs about seven times more accumulation than its blue to reach the same output. At A = 1.83 the red lands at 0.56 and the blue at 0.96. Pale, and unmistakably azure. For the weakest channel to saturate as well, which is what it takes for a pixel to read as plain white, A has to reach about 17.

A dense body's middle runs well past 17, so it goes white. Its outer edge does not. Somewhere in between is a contour where A falls back through a couple of units, the weak channel drops out of saturation, and the hue returns. That contour is a ring of colour around a white middle. Nobody drew it, nothing in the simulation put a different species there, and it is exactly what a bright blob looks like through a real camera. Any sufficiently dense clump of a single colour renders with a membrane.

AZURE, THROUGH THE EXPOSURE CURVE 0.1 0.5 1 4 8 40 accumulated brightness A, before the tonemap (log) one particle Β· 1.83 white Β· β‰ˆ17 one particle peak A 1.83 a dense stack peak A near 40
Figure 3 β€” every colour above is the arithmetic rather than a picture of it: azure at accumulated brightness A, through 1 βˆ’ eβˆ’1.3AΒ·k per channel and a 1/2.2 gamma. The left disc is one real sprite profile. The right is the same ramp read outward from a stack forty times brighter, which is where the white core and the coloured rim come from.

The physics makes real membranes too, and keeping the two apart is the whole discipline of this section. A species drawn to a second but repelled by its own kind cannot collapse inward, so it spreads into a shell, and whatever the shell is holding becomes a nucleus. That is a genuine arrangement of two different species in space, and the classifier in Β§5 tests for it by species membership rather than by brightness. The exposure rim and the physical shell look similar on screen and are not the same claim, which is why the field guide is only allowed to describe an anatomy when the census has actually measured one.

None of it happens without the half-float target. With an ordinary 8-bit buffer the additive pass clips at 1.0, every clump becomes the same flat disc, the rim disappears, and the bloom threshold has nothing left to find. On top of the scene sits a three-level bloom β€” a bright pass at 0.62, then downsample-and-blur at a half, a quarter and an eighth, sampled back in the composite at 0.5, 0.32 and 0.18 β€” and an optional persistence, the previous frame kept at a fraction of its brightness. That persistence is reprojected through the camera before it is added, with a zoom scalar and a pan offset, so trails stay stuck to the world. Without the reprojection, panning smears the entire image sideways.

Two smaller decisions do the same kind of work. The world is a torus, so it tiles: the renderer draws the nine nearest copies with an offset uniform, skipping any whose bounding box is off screen, which is why a colony sitting on the seam is whole from either side rather than sliced in half. And dot size is tied to the interaction radius rather than fixed, clamp(rMax Β· scale Β· 0.085, 2.2, 22) device pixels, so zooming in grows the particles along with the structures they are in and a clump never turns into a solid puddle with no grain left in it.

Result. Two mechanisms produce nearly the same picture, and one of them is an artefact of exposure. The rule the rest of the project runs on is that the interface may only claim the other one, and only when something has measured it.

Β§5 Β· Measurement Β· Requirement four

Finding the bodies after the fact

The simulation still has no concept of a body. To count them, name them, or score a world on whether it makes any, something separate has to go and look. That is src/sim/clusters.ts, and it runs about once a second over whatever is there. It is a naturalist with a quadrat, not a designer with a spec.

Union-find over every pair of particles closer than 0.62 rMax, which is inside the attractive band's peak, walked through the same uniform grid the force loop uses. Components of fewer than twelve particles are discarded as coincidence. What survives gets a page of shape statistics: radius of gyration; the two eigenvalues of the position covariance, from which elongation is 1 βˆ’ √(Ξ»β‚‚/λ₁); the fraction of members inside 0.55 Rg, which is hollowness; angular momentum about the centroid, normalised by the largest it could possibly be; a species histogram; and the dominant species inside 0.5 Rg and outside 1.0 Rg, which are the nucleus and the membrane.

The torus makes the centroid awkward, and it is a nice small example of the kind of thing that has to be right before any of the rest means anything. A body sitting on the seam has half its members near x = 0 and half near x = w. The arithmetic mean of those puts its centre in the middle of the world, which is the one place it is not. So the centroid is a circular mean: map each coordinate onto a circle, average the unit vectors, take the angle back. The sanity test builds a ring straddling the seam and insists the census finds one body, centred on the seam.

Then eight tests, run in exactly this order, first match wins:

Motefewer than 26 particles, and almost all one species
Colonyover 420 particles β€” too big to be one thing
Filamentelongation above 0.72 β€” a chain, an arc, a thread
Ringunder 7% of its members inside 0.55 Rg
Cella nucleus of one species inside a membrane of another
Rotornormalised angular momentum past 0.45
Swimmercentroid moving faster than 1.2 radii a second
Nodulea lump with nothing left to test positive for

Two of those thresholds are where they are because the panel disagreed with my eyes. Colony had to move above Cell. A four-hundred-particle blob almost always has some radial species gradient, so the core-differs-from-shell test was labelling a thing the size of a fist as a cell and filing it in the bestiary next to the actual cells. Cell then needed a purity test: the leading species has to hold 55% of the nucleus and 50% of the membrane. Without it, any mixed lump with a faint gradient qualified, 95% of every world came back "structured", and the measurement was worthless for ranking anything.

It is a decision list, not a model, and that is worth being plain about. Eight archetypes, thresholds in classify(), first match wins. There is no notion of a body being two things at once, no confidence attached to anything, and no way for it to say "I have not seen this before", which is a real gap in a project whose search exists to find novelty and can only report what it finds in eight fixed words.

Bodies are matched across censuses by greedy nearest-centroid with a size guard: no more than 2.6Γ— bigger or smaller, no further than 1.3 radii of gyration. A thing that persists therefore keeps its identity and accrues an age. Not Hungarian, not a Kalman filter β€” bodies here either drift slowly or stop being the same body, and the worst a cheap matcher does is occasionally mint a new number for an old cell.

A census costs 1.8 ms at the default population and 5.2 ms at the largest the showcase offers, and it used to cost three times that. The saving was not in the union-find, it was in the garbage: members held as arrays of arrays meant several thousand boxed numbers a second. Everything now lands in one flat Int32Array via the same counting sort the grid uses.

Result. A body that survives from one census to the next gets a number and an age, and that number is what everything downstream β€” the card, the field guide, the score, the jar in Β§8 β€” actually refers to.

Β§6 Β· Description Β· Requirement five

Saying what a thing is, and nothing more than that

Tap a body and it tells you what it is. This is the last layer of the illusion and the one under the strictest rule, because it is also the layer that can undo the other four: every sentence on that card has to be a reading, and nothing on it may claim anything the census did not measure.

The name is two lookups: the adjective belonging to the dominant species outside 1.0 Rg, then the archetype. Azure Cell. Rose Rotor. Under it, liveLine() assembles a sentence out of the same numbers the classifier used. "Travelling at 1.8 body-lengths a second" is the matched-centroid displacement. "Turning clockwise" is the sign of the normalised angular momentum. "Open in the middle" is hollowness past 0.4. Speeds are given in body-lengths rather than interaction radii because "two body-lengths a second" is something you can check by watching, and "0.9 radii a second" is not.

The rule earned itself the hard way. An early version put Rose Nodule at the top of a card and "a shell of rose around a core of verdant" directly underneath. Both sentences came out of the same census, but a nodule classification means the core-and-shell purity test failed, and the sentence generator was reaching past that failure for the raw dominant species anyway. A card that contradicts its own headline is worse than a card that says less. Only a Cell or a Ring may claim an anatomy now. Everything else gets "mixed, and mostly rose".

Behind the card is a field guide with the eight kinds: what each one is, why it exists mechanically, and a look for line that is the classifier's own threshold in plain English. That last part is deliberate. A reader who learns to spot a rotor by eye is learning the same rule the code uses, and is therefore in a position to catch it being wrong. Entries fill in as you meet them and persist between sessions, one at a time: the first census of a healthy dish finds seven kinds in a single frame, and seven notifications stacking up reads as an error rather than a discovery, so they queue at one every 1.5 seconds. Each entry is illustrated by the best real example your session produced, chosen by age Β· 2 + size / 25 rather than sampled, because picking simply the oldest gave a Ring entry illustrated by a smudge. A kind you have not met yet gets a dashed silhouette instead of a photograph, so the guide can tell you what to look for without pretending you have already seen one.

And when the thing you are following comes apart between one census and the next, the card says so β€” "It has come apart. Whatever it was, it is not that any more." β€” rather than quietly freezing on its last good reading. A body dissolving is among the most common events in the dish. An interface that hid it would be telling you the wrong thing about what these are.

Result. Everything on the card is a measurement and everything in the guide is a mechanism, which means a reader who does not believe a label has somewhere to go and check it.

Β§7 Β· Selection

Choosing which world you get

The five requirements above are properties of the engine. Which thirty-six numbers you are actually handed is a separate question, and answering it offline is not a small contribution to the impression. Twelve numbers with a target band on each, in src/sim/vitals.ts. npm run hunt draws random matrices, simulates each one, scores it, hill-climbs the best and writes what survives to src/data/worlds.json. That file is the world list the showcase ships with, and nothing in it was written by hand except the names.

Most of the twelve are counts the census already has: what share of particles belong to a body at all, how many bodies there are per thousand particles, how many particles the median body holds, how many of the eight archetypes are present at once, how mixed the average body is. Two measure the failure modes rather than the successes. Dominance is the largest body's share of the population, which is how a percolating web β€” everything touching everything, which the cluster finder cheerfully reports as one enormous healthy body β€” gives itself away. Packing is the fullest grid cell over the average, which is both "did this collapse" and, not coincidentally, the thing that sets what a step costs.

Two more need a second census, taken a couple of seconds after the first. Persistence is the share of particle pairs that shared a body then and still share one now. Pairs rather than particles, because a pair count is indifferent to bodies merging, being relabelled, or shedding a few members, all of which happen constantly and none of which mean the structure died. It falls out of a sparse contingency table between the two censuses' labels, keyed by particle identity rather than array position, since the position arrays get re-sorted into grid order on every single step. Motility comes free from the same table: match each old body to whichever new body inherited most of it, and measure how far its centre went.

Two details in the scoring are load-bearing. The bands are Lorentzian outside their edges, 1/(1 + dΒ²), so being wrong by a lot is only somewhat worse than being wrong by a little and no single reading can drive the total to zero on its own. And the total is a weighted geometric mean, not an average: a world can be splendid on eleven counts and dead on the twelfth, and it is dead. An arithmetic mean calls that an 87.

One band's units had to be rewritten twice, and it is a decent warning about measuring anything on a grid. Occupancy is the share of bins containing anything, and it depends entirely on how big a bin is. At 1.5 interaction radii a bin is smaller than a body, so every body lights three or four of them, and a world of well-separated bodies with black between them measured 85% full, which is not what anyone looking at it would say. At 0.6 radii a bin is smaller than the gaps as well as the bodies: the same world reads 32%, and a uniform gas, which ought to be the honest top of the scale, reads 68%.

The draws are not uniform noise either, and the sampler is already an opinion about where the interesting matrices live. Uniform entries give symmetric pairs about as often as anything else, and a symmetric pair cannot chase. So each pair gets a shared magnitude and an antisymmetric split, a chase term in [βˆ’1, 1], and the diagonal is biased positive, because a species that will not hold on to itself never forms a body for anything else to wrap around.

Two stages, because the expensive part is the simulation and most candidates do not deserve one. Stage one runs a 4,000-particle world with a gate at four seconds: anything packed sixty times tighter than average is a collapse, and anything filling more than 60% of the bins is a gas. Survivors run to fourteen seconds, get two censuses two seconds apart, and are scored. Stage two takes the distinct leaders, matrices more than 1.6 apart in L2, and hill-climbs each one with a (1+1) evolution strategy: three temperatures, coarse first, six mutations each, keep anything that improves.

The run that produced the shipped list drew 4,000 matrices, of which 3,117 got past the gate, in 760 seconds across nine worker processes. Their scores ran from 38 to 91 with a median of 60, a distribution with enough spread in it to rank, which is the only thing the bands really have to achieve. Sixty distinct leaders were hill-climbed, typically gaining three to five points and occasionally fifteen, and the best thirty were re-scored at the reference population of twelve thousand, because a world's score moves when its neighbourhood does. Twelve of those thirty are in the panel.

The last step is not automatic, and it is where the proxy gets checked. Twelve metrics with hand-set bands stand in for "worth looking at", and a proxy is exactly as good as its worst case; every band here was moved the last time the ranking let something through that clearly did not belong, which means they are fitted to the failures I happened to notice. So npm run contact photographs every finalist through the real renderer and lays them out as a contact sheet, and a person looks at it, writes the names, and writes one line about each. Two of the twelve are in the list as arguments against the score: Fair Weather, middling on every single reading and pleasant to watch, and Silt, which is crowded and shapeless and kept because a poor score should be something you can look at rather than a number.

Result. Once the core and the density were right, most matrices from that sampler make something structured. "Almost every universe is dead" was true of the version with the broken force law and stopped being true when the force law was fixed. The search is for ranking and improvement, not rescue.

Β§8 Β· Control

Testing it: one body, one jar

Everything above adds up to an impression that the dish is full of individuals. The obvious way to test an impression like that is the one a biologist would reach for: isolate a specimen and see whether it is still itself. Any body can be netted out of the dish into a tank, a square chip of the same world ten to twenty-six interaction radii across, running the same matrix at the same radius. Nothing about the physics is adjusted. The only two things that change are how much world there is around it and how much company.

The particles come across exactly as they are, positions, velocities and species, which is harder than it sounds. The census's own member list is indexed by array position and the arrays are re-sorted into grid order on every step, so it is stale within a frame. The transfer goes through labelByPid instead: walk the live particles, ask what each one's identity was labelled at the last census, keep the matches. The body's mean velocity is subtracted on the way in, because a swimmer netted at speed otherwise spends its first ten seconds lapping the jar, and a specimen jar you have to chase is not a specimen jar.

A tank starts empty apart from what you put in it. The first version filled it with medium at half the dish's density, on the theory that a body needs material to work with, and within a minute that medium had condensed into eight to sixteen bodies of its own and the thing you had netted was one of a crowd again. Which is what you netted it to get away from. So a jar is a vacuum, and the one verb is feed: a dose of fresh particles at the dish's density, rejection-sampled outside whatever is already in there so it does not land inside a body and blow it apart from the middle.

What that buys is a controlled experiment, and it answers in both directions. A twelve-particle mote, alone in a jar, is a mote forever: thirty seconds, sixty, as long as you leave it. Feed it once and it is a fifty-particle cell inside ten seconds, then a rotor. The guide's entry for Mote claims that motes are "the raw material β€” a mote that drifts into the right neighbourhood gets a shell wrapped around it and becomes a cell". That sentence was written from the force law, months before there was anywhere to check it. The jar is where you can make it happen on purpose and watch it be true.

And the reverse, which I did not expect and which is the better finding. A six-hundred-particle colony netted out of a busy dish comes apart within thirty seconds into a dozen nodules. It was not one body. It was several, held in a single shape by the pressure of everything around it, and taking that pressure away is all a jar does.

Result. Some of what reads as an individual in the dish is an individual and some of it is a crowd standing close together, and the only way to tell which is to take it out and see what falls apart.

The dish is running. Pick something in it and it will tell you what it is β€” and if you are not convinced it is really one thing, there is a jar.

Open the dish β†—
Marcin β€” creator of vibespire.ai

About

Hi, I'm Marcin.

I build things quickly, mostly by hand and increasingly with a model sitting next to me. Most of what I make starts as a question about how something works and turns into a small machine for finding out.

vibespire is where those experiments live. Poke at them, break them, read how they're made β€” and if something sparks an idea for you too, say hello.