A Loop With No Brake
Starvation on a full account
The two-century run ends with 1,470 starvation deaths out of 1,499, while the bed still holds 376.281 of the 528 nutrient units it opened with. Both books replay and both close. Diagnose only the loops the code actually walks: draw each arrow from a measured transfer, write the quantity on it, and count which arrows push back against growth.
The valley starved on a full account because full is not the same as reachable. Nutrient can rot onto rock, settle under pond water, or leave over the rim in a seed. None of those places pays a root.
Randomness is not the suspect. The run with luck removed keeps the peak and still empties the standing crop. The failure is feedback: mass buys seed, seed buys neighbours, neighbours draw the bed down, and no term falls as crowding rises.
Loop gain
The instrument is a second command against the same module. It founds the valley cmd/ages founds, runs the ticks cmd/ages runs, and changes nothing at all about either. What it does differently is take its readings at both ends of a year and subtract, so that a line of its output is one turn of the circle instead of one snapshot of the valley.
// cmd/loops/main.go
// gain reads the reproduction loop one year at a time. A line is one
// turn of the loop: what was standing at a midsummer, what it put into
// the air the autumn after, and how many of those seeds were standing
// plants by the midsummer following. The last column is the gain.
func gain(seed uint64, light float64, years int) {
v, g := open(seed, light)
planted := v.Found(3)
...
midsummer(v)
for y := 1; y <= years; y++ {
roll := v.Take()[scrub]
fuel := v.Bed.Held().Nutrient
was := [2]int{v.Seed.Released[scrub], v.Seed.Fate[scrub][terra.Grew]}
midsummer(v)
sent := v.Seed.Released[scrub] - was[0]
took := v.Seed.Fate[scrub][terra.Grew] - was[1]
per := 0.0
if roll.N > 0 {
per = float64(took) / float64(roll.N)
}
fmt.Printf(" %5d %6d %9.1f %7.2f %10.3f %9d %8d %9.3f\n",
y, roll.N, roll.Mass, roll.Mean(), fuel, sent, took, per)
}
}
$ go run ./cmd/loops -mode gain -years 20
loops: one turn of the reproduction loop a line, seed 5, 44 soil cells
founded with 3 of every row, 12 took a cell
a line is what stood at one midsummer and what it left standing at the next
year scrub grams mean in the bed seeds took per plant
1 3 493.1 164.35 137.400 184 15 5.000
2 18 1263.6 70.20 89.079 701 13 0.722
3 29 1412.2 48.70 14.267 796 12 0.414
4 32 1399.2 43.72 26.489 836 11 0.344
5 32 1352.2 42.26 37.714 805 14 0.438
6 34 1321.8 38.88 50.524 946 16 0.471
7 36 1289.5 35.82 64.974 1225 20 0.556
8 41 1787.4 43.60 80.151 1256 22 0.537
9 39 1700.8 43.61 95.924 1146 21 0.538
10 39 1622.6 41.60 111.679 1017 24 0.615
11 37 1528.1 41.30 126.645 945 24 0.649
12 39 1457.9 37.38 140.092 892 25 0.641
13 37 1387.2 37.49 152.035 831 26 0.703
14 36 1344.2 37.34 164.080 796 26 0.722
15 36 1302.2 36.17 175.161 757 27 0.750
16 33 1267.5 38.41 185.556 773 28 0.848
17 34 1420.1 41.77 195.044 749 25 0.735
18 35 1380.2 39.44 204.242 717 24 0.686
19 33 1334.5 40.44 214.156 681 23 0.697
20 31 1305.4 42.11 223.455 653 22 0.710
The seeds column is the loop's output and it climbs for eight straight years: 184, 701, 796, 836, 805, 946, 1225, 1256. Nothing about the valley filling up made this population produce less. At the year the valley is at its fullest, with a plant standing on every one of its 44 cells of soil, it is putting more seed into the air than in any year before it.
Now look at the last column, because it is a trap and the trap is instructive. New plants per standing plant reads 5.000, then 0.722, then 0.414, and a number that falls that sharply looks exactly like a population discovering its own limit. It is not doing that. A new plant needs an empty cell, and by the second midsummer 18 of 44 cells are taken, so no matter how many hundreds of seeds come down that column cannot exceed 26 ÷ 18 = 1.444. By the third year the arithmetic ceiling on it is 15 ÷ 29 = 0.517. That column stopped measuring the plants some time in year two and started measuring the room left over.
Numbers first. Three scrubs stood at the first midsummer. Fifteen new ones were standing at the second, so each of the three left five behind: call that five the gain of one turn. Push it forward with nothing pushing back. Three plants become 3 + 15 = 18, eighteen become 18 + 90 = 108, and a hundred and eight become 648. The valley has 44 cells. On this arithmetic the third year is when the plants run out of ground, and the third year is exactly where the run's soil column falls to 14.267 and the mean mass of a scrub has already dropped from 164.35 grams to 48.70.
Write the quantity going round the loop x, the number of turns n, and the gain g, which is what one turn multiplies x by. One turn is a multiplication, and n turns is that multiplication done n times:
x(n+1) = g · x(n) and so x(n) = x(0) · gⁿ
Three cases and no others. With g below 1 the quantity fades to nothing. With g at exactly 1 it holds still. With g above 1 it multiplies, and it keeps multiplying, and it does not slow down on its own at any value of x whatsoever, because g does not depend on x. Here g = 6: each plant is still there and has left five more. One more turn is 3,888, and there are 44 cells.
So a loop with gain above one always ends the same way, by running into something outside itself. The interesting question is never whether it stops. It is what stops it, and there are two very different answers to that, which the next section separates.
Neighbour cost
There is competition in this code and it is not imaginary. A crown writes shade into the light field before any plant reads out of it, and root discs overlap, so two plants close together share ground that one plant would have had alone. Both of those take something off a plant when a neighbour arrives. The question is how much, and the way to find out is to vary the crowding on purpose and hold everything else still.
// crowd founds n scrubs and nothing else, and reads them at their first
// midsummer, before any of them has seeded. It is the density question
// asked directly: what does one more neighbour cost a plant, and what
// does the whole standing crop have to spend on seed once it has paid
// for them.
func crowd(seed uint64, light float64) {
for _, n := range []int{1, 2, 4, 8, 16, 32, 44} {
v, _ := open(seed, light)
took := v.FoundRow(scrub, n)
midsummer(v)
roll := v.Take()[scrub]
var lit, offered float64
clutch := 0
for _, st := range v.Stands {
lit += v.Sky.At(st.At)
offered += v.Bed.Offer(st.At, st.Plant.Root, 0).Nutrient
clutch += st.Kind.Clutch(st.Plant.Mass)
}
...
}
}
$ go run ./cmd/loops -mode crowd
loops: n scrubs founded alone on the seed-5 valley and read at the
first midsummer, with nothing else standing anywhere in it
asked took grams mean its light its ground seeds
1 1 214.9 214.87 14.294 180.707 107
2 2 429.7 214.87 14.294 120.707 214
4 4 859.5 214.87 14.294 104.859 428
8 8 1442.5 180.32 10.418 96.330 717
16 14 2364.5 168.90 9.279 70.886 1175
32 24 3421.5 148.76 7.376 39.090 1698
44 29 3845.4 137.33 6.478 26.962 1909
The first three rows are identical to the last digit printed. One scrub, two scrubs and four scrubs each hold 214.87 grams, because at those densities the crowns do not meet and the plants are light-limited anyway, so the shared soil never becomes the binding constraint. The first three neighbours are free. From eight tenants on, the coupling is real and it grows: 180.32 grams each, then 168.90, then 148.76, then 137.33.
So there is a negative term in this model. Now price it. Going from one plant to twenty-nine multiplies the tenants by 29 and the mass of each by 137.33 ÷ 214.87 = 0.639. The standing crop is those two multiplied together, 29 × 0.639 = 18.5 times what one plant held, and the seed the crop pays for goes 107 → 1909, a factor of 17.8. Crowding the valley twenty-nine-fold made it produce almost eighteen times the seed.
That comparison is the whole test, and it makes a rule. For crowding to hold a population steady, the mass of one plant has to fall at least as fast as the number of plants rises: double the tenants and each one must end with half as much, so the crop and its seed stay put. Here twenty-nine times the tenants cost each of them 36 per cent. In the language of the interlude, the gain of this loop is still far above one at every density the valley can hold, and it stays above one right up to the last free cell.
Which leaves the difference the chapter turns on. Something that lowers the rate at which a loop feeds itself is a brake: the loop slows as the quantity grows, and it settles at whatever level makes the gain exactly one. Something that removes units of the quantity from outside the loop is a wall: the loop goes on demanding growth at full strength, and the only reason the number stops rising is that the surplus is being killed. Braked systems settle. Walled systems pile up against the wall and die there. This valley has two walls, 44 cells and starvation, and no brake at all, and the register at the end of the run is the receipt: 1,470 starved of 1,499.
Stranded nutrient
A loop that only ever pushes one way still cannot manufacture matter. Everything the plants are made of came out of 528 nutrient units, and every gram of tissue that dies is supposed to rot back into the ground the next generation spends. That returning arrow is the reason a valley can go round this loop more than once, so the next thing to measure is how much has actually travelled along each arrow.
$ go run ./cmd/loops -mode arrows
loops: every arrow in the loop, and what ran along it in 200 years
grams
light fixed into new tissue 858610.8
tissue burned to pay upkeep 842106.0
tissue spent on seed 15947.5
seed carried over the rim 2231.0
dead tissue rotted away 14283.2
still standing 1.8
nutrient units
the valley opened with 528.000
went from a body into a pile 60608.719
rotted out of a pile into a cell 60608.611
carried over the rim in seed 151.500
held in living tissue now 0.111
lying in litter now 0.108
seeds released 30147, seedlings that took 1488, one in 20.3
The recycling arrow carried 60,608.719 units, and the valley only ever owned 528 of them. Divide: 60,608.719 ÷ 528 = 114.8. Everything in this world went round the circle about a hundred and fifteen times over. That number reframes the leak the seeding chapter warned about. The 151.500 units that blew over the rim are 28.7 per cent of the whole stock, which sounds like a catastrophe, but spread across a hundred and fifteen turns it is a quarter of one per cent per turn. A leak that small is invisible in any run short enough to watch, and on a loop that turns this often it is fatal.
One in 20.3 seeds became a plant. The other nineteen went somewhere, and each of them was carrying nutrient when it got there.
With the arrows totalled, the loop went on paper, and the paper made a prediction. Draw it: soil pays income, income builds mass, mass makes seed, seed makes plants, plants die, litter rots, and the nutrient goes back into the soil to be spent again. One arrow leaves that circle, the seed that clears the rim, and it carried 151.500 units. So the valley keeps 528 − 151.5 = 376.5, which is 71.3 per cent of its founding stock, and the census agrees to three decimals: 376.281 in the bed at year 200. A valley with 71 per cent of its nutrient should carry roughly 71 per cent of the 1517.5 grams it opened with. Call it eleven hundred grams, standing, for ever.
The run ends with 1.8 grams. The prediction is wrong by a factor of six hundred, and it is wrong in the direction that matters, so the diagram has an arrow in it that the code does not have. The suspect arrow is the return: litter to soil, drawn as though every pile of litter feeds every root. Testing it needed a total the bed had never been asked for.
// Rooted is what the bed holds in the cells a root can live in. It is
// not the same number as Held: litter falls wherever a seed does, bare
// rock and open water included, and the nutrient that rots out of it
// goes into the cell it was lying on whether or not anything can ever
// put a root there. A plant can only spend this one.
func (b *Bed) Rooted() Cell {
var t Cell
for y := 0; y < b.H; y++ {
for x := 0; x < b.W; x++ {
c := sim.Coord{X: x, Y: y}
if !b.Rootable(c) {
continue
}
s := b.cells[b.index(c)]
t.Moisture += s.Moisture
t.Nutrient += s.Nutrient
}
}
return t
}
$ go run ./cmd/loops -mode ground
loops: the column the census calls "in soil", asked which cells it is in
year in the bed a root reaches under rock under water
1 137.400 137.400 0.000 0.000
3 14.267 1.769 8.508 3.989
10 111.679 1.234 66.587 43.858
50 342.920 0.181 210.920 131.820
100 373.715 0.015 230.480 143.220
200 376.281 0.001 232.310 143.970
44 cells of soil, 36 of rock, 16 of open water
where the 30147 seeds of this run ended, and the nutrient each
parcel of them was carrying when it got there
germinated 1488 49.900
blown off the valley 4168 151.500
landed on rock 6550 232.310
landed in the pond 4125 143.970
aged out of the bank 13816 486.340
the one plant left standing, at 7,1, holding 1.8439 grams
its roots reach 15 cells and they offer it 0.001092 nutrient units
light on its cell 19.158, water under it 18.110, so the limit is nutrient
it can fix 0.0182 energy a tick and its upkeep is 0.0148
the least mass that pays for one seed of its row is 2.00 grams
The reachable ground holds 0.001 units. Not 376. The whole 376.281 is sitting on the 36 cells of bare rock and under the 16 cells of open water, where 10,675 seeds landed over two centuries and rotted, and the arithmetic closes exactly: 232.310 on the rock plus 143.970 in the pond is 376.280 of the 376.281 the bed is holding. Add the 151.500 that left over the rim and 527.780 of the valley's 528 units are somewhere no root will ever reach them again.
The mechanism is two loops in the code that walk different sets of cells and were drawn
as one arrow. Bed.Rot sweeps every pile of litter on the grid and puts what
it burns into the cell underneath, rock and water included, because a pile that is nowhere
would be a hole in the books. Bed.Roots filters a disc down to cells that are
soil. So the arrow into the ground covers 96 cells and the arrow out of it covers 44, and
nothing anywhere moves a nutrient unit sideways between them. Bed.Regen
exists and Valley.Tick never calls it; the tick runs the shower, the
evaporation and the rot, and nothing in it carries a nutrient unit from one cell to
another.
The last plant is the whole diagnosis in one readout. Its cell is in full daylight at 19.158 and standing over 18.110 units of water, and it is limited by nutrient, on 0.001092 units, at the centre of a valley whose books say it is two-thirds full. It fixes 0.0182 energy a tick against an upkeep of 0.0148, so it grows by a rounding error and sits at 1.8439 grams, and its row needs 2.00 grams to pay for a single seed. It cannot starve and it cannot breed. A total is not a supply. Adding a quantity across cells that cannot trade with each other invents an average nothing can spend.
The density brake calculation
Control theory has one idea at its centre and this is the place to meet it. If you want a quantity to sit at a value instead of running away from it, you measure the quantity and feed the measurement back into the thing that drives it, with the sign reversed, so that more of the quantity means less driving. A thermostat is that sentence in a box: the warmer the room reads, the less the heater runs. The loop is still a loop, and it still has gain, but the gain now falls as the quantity rises, so there is exactly one level where it equals one and the system walks to it and stays.
For a valley, the quantity to measure is the crowding and the thing to drive is growth. Give
a plant a coefficient it multiplies its income by, and let that coefficient fall as the
count of standing plants rises toward the number of cells the valley has. Write it
c = 1 − N/K: at one plant on 44 cells the coefficient is 0.977 and almost
nothing changes, at 22 plants it is one half, and at 44 it is zero and nothing grows at all.
None of that is written yet. It can still be costed, because the numbers it would multiply
have all been measured.
// brake works the arithmetic of a growth coefficient that falls as the
// neighbours crowd in, and then asks what that coefficient does to the
// two arrows that leave. Nothing here changes a valley: the coefficient
// is multiplied into numbers already measured, and every line below is
// a prediction rather than a result.
func brake(seed uint64, light float64) {
// M: one scrub with the ground to itself, at its first midsummer.
lone, g := open(seed, light)
lone.FoundRow(scrub, 1)
midsummer(lone)
m := lone.Take()[scrub].Mean()
k := g.Count(sim.Soil)
...
for _, n := range []int{6, 11, 22, 29, 33, 41, 44} {
c := 1 - float64(n)/float64(k)
mean := c * m
fmt.Printf(" %6d %13.3f %12.2f %14.1f %10d\n",
n, c, mean, float64(n)*mean, n*row.Clutch(mean))
}
}
$ go run ./cmd/loops -mode brake
loops: what a growth coefficient c = 1 - N/K would do, worked on paper
against numbers already measured. No valley is run with it.
K, the cells of this valley a root can live in 44
M, one scrub's grams with the ground to itself 214.87
N c = 1 - N/K mean grams standing grams seeds
6 0.864 185.57 1113.4 552
11 0.750 161.15 1772.7 880
22 0.500 107.44 2363.6 1166
29 0.341 73.25 2124.3 1044
33 0.250 53.72 1772.7 858
41 0.068 14.65 600.7 287
44 0.000 0.00 0.0 0
the largest standing crop is at N = K/2 = 22, and it is K*M/4 = 2363.6 grams,
holding 2363.6 x 0.06 = 141.8 of the valley's 528.0 nutrient units
and what that crop does to the two arrows that leave:
seeds it releases every autumn, for ever 1166
the share of a seed rain that strands, measured 0.492
nutrient units one scrub seed carries 0.030
units leaving reach every year 17.22
years of that the valley's 528 units cover 30.7
the run without a brake released 30147 seeds in two centuries,
which is 151 a year, because a dying crop stops seeding
The top half is encouraging and the arithmetic behind it is useful. With the
coefficient in, a plant's ceiling is c × M and the standing crop is
N × c × M, which written out is N(1 − N/K)M.
That expression is largest exactly halfway along, at N = K/2, where it comes
to KM/4. Twenty-two plants, 107.44 grams each, 2363.6 grams standing. The run
with no brake touched 2703.3 grams in year 3 and was past it in year 4. The braked figure
is smaller, and the difference between them is that this one is a level and the other one
was a peak.
The bottom half is the part to be honest about. A braked valley does not stop seeding: it holds 22 plants at 107 grams and releases 1,166 seeds every autumn for as long as it stands. The run that crashed averaged 151 a year, because a population that is dying is a population that has stopped reproducing. Nearly half of any seed rain in this valley strands, measured over the run that already happened, at 0.492. So a stable crop pushes 1166 × 0.492 × 0.030 = 17.22 nutrient units a year out of reach, and 528 ÷ 17.22 is 30.7 years. Under the other three world seeds the same arithmetic gives 30.9, 53.9 and 41.5.
On this evidence a brake on its own is not a cure, and would arrive at the same ending sooner. It fixes the runaway and it does nothing whatever about the two arrows that leave, and by holding the population up it makes those arrows carry more. That is a prediction, not a result, and it is a first-order one: it multiplies a coefficient into a static crop and assumes the stranding fraction of a braked seed rain matches the crashing one, which is the weakest link in it and easy to imagine going either way. It is written down here so it can be checked rather than remembered.
A valley that stays green needs work on all three findings, and they are independent. The runaway wants the density coefficient above. The stranding wants some way for nutrient to move between cells, so that what rots on the rim or in the shallows can reach a root that needs it, the same kind of neighbour sweep the fire and water rules already use, which would close the return arrow the first diagram assumed. The leak over the rim wants a decision rather than a mechanism: a valley whose edges return their seed, or a trickle of nutrient arriving from outside, or an accepted slow bleed with the run length chosen honestly around it. Guessing which of the three matters most is cheap. Sweeping them is not, and it is the only way to find out.
Arrows and signs
The method under this page generalizes past valleys, and it runs in three passes. First establish the sign of every arrow: for each link, does more of the source mean more of the target or less of it. Signs are cheap, they can usually be read straight out of the code, and a loop whose arrows are all the same sign is a loop that has no brake in it, whatever the numbers turn out to be. That pass alone predicted the boom.
Then size the negative terms, because a brake with the right sign and the wrong magnitude buys nothing. Shade and shared roots are real negative terms in this model and they cost a plant 36 per cent of its mass across a twenty-nine-fold crowding, which was never going to hold a population that multiplies sixfold in a year. Sign told us the loop was positive; only the measurement told us how far from balance it was, and the measurement is the one that says whether a fix has to be a new mechanism or a tuned constant.
Then check the reach of every quantity you have been adding up. This is the pass that is easy to skip and it is the one that cost a wrong prediction here. The bed's total was a real number, correctly computed, closing to six decimals against a ledger that balances, and it answered a question nobody had asked: what is on the grid, when what was wanted was what a root can spend. Aggregates hide geometry by construction. Any time a total is summed over cells that cannot pass anything between them, the total is an accounting fact and not a resource, and the only way to tell the two apart is to sum it again over the cells that can.
The last thing this run is evidence for is smaller than it looks, and saying so is part of the finding. Four world seeds crashed the same way and all four ended with their nutrient stranded and leaked, so the finding is structural. It is not evidence that the species table is wrong, that the map is too small, or that any un-managed valley must die. It is evidence about these arrows, and every one of them was drawn by reading the code and then measuring what ran along it.
- Given a loop's gain for one turn, you can push it forward by hand, say which of the three cases it falls into, and work out the turn on which it runs out of room.
- state what separates a brake from a wall, and point at the two walls this valley has and the receipt in its death register.
- Shown a per-capita rate that falls as a population grows, you can work out whether it is falling because of the organisms or because of the space left, and compute the arithmetic ceiling that column is really tracking.
- price a negative term by holding everything else still and varying the crowding on purpose, and say how fast the mass per plant would have to fall to hold a crop level.
- Handed a total summed over a grid, you can ask which cells it is in and which of those cells the consumer can actually reach, and you can name the two methods in this module that walk different sets of cells.
- multiply a proposed coefficient into measured numbers to cost a fix before writing it, and say which assumption in that costing is the one most likely to be wrong.
Exercise 1 — dim the sky and re-price the neighbours. Less light
means smaller plants, and smaller plants shade each other less. Predict what halving the
daylight does to the crowding curve, then run
go run ./cmd/loops -mode crowd -light 6.
One scrub alone comes out at 107.44 grams, exactly half of 214.87, because a plant on open ground here is light-limited and its ceiling is linear in the light. The interesting column is the last one. Twenty-nine tenants now hold 83.41 grams each against a lone plant's 107.44, a ratio of 0.776 where the bright valley gave 0.639, and the seed the crop pays for climbs 53 → 1153, a factor of 21.8 against 17.8.
So halving the sky made the loop less self-limiting, not more. Dimmer plants are shorter and narrower, their crowns overlap later, and the only negative term with any size in this model gets weaker. A change that makes every individual worse off can make the population's runaway worse at the same time, and nothing but measuring the crowding curve would have told you which way it went.
Exercise 2 — a valley with more soil and less water. Seed 7 has
54 cells of soil and only 6 of open water, against seed 5's 44 and 16. Predict whether
it strands less of its nutrient, then run
go run ./cmd/loops -mode ground -seed 7.
It strands more, not less: 379.600 units on rock and 84.270 in the water at year 200, against seed 5's 232.310 and 143.970. The pond is smaller so less goes into it, the rock catches the difference, and the reachable ground finishes at 0.055 units out of the 463.925 the bed is holding. A bigger, wetter-looking valley bought 0.054 units.
The ratio of rock to water changed which sink filled up and changed nothing about whether the sinks fill. That is what makes this structural: the seeds are aimed by a wind field at a map with edges, most of them miss, and there is no arrow back from wherever they land.
Exercise 3 — cost the brake on the other three worlds. The
thirty-year figure came off one map. Predict whether it is a property of seed 5, then
run go run ./cmd/loops -mode brake -seed 3 and the same for 7 and 11.
Seed 3 gives 30.9 years, seed 7 gives 53.9 and seed 11 gives 41.5. Seed 7 does best by a wide margin and the reason is in its own output: its stranding share is 0.280 against seed 5's 0.492, because it has the most soil and the least water, so more of its seed rain lands somewhere a root can get at it later.
None of the four reaches a century. The number moves with the terrain and the verdict does not, which is the answer you want from a sensitivity check: if the prediction had come out at 30 years on one map and 3,000 on another, the arithmetic would have been telling you it depends on something you had not measured yet.
The diagnosis is finished and nothing in the valley has been changed. There are three repairs on the table, one coefficient costed at 2363.6 grams held instead of 2703.3 grams passed through, and a prediction on record that the coefficient by itself buys about thirty years. All of that is argument. The way to settle it is to write the brake, sweep it across a band of values instead of trusting the one that looked right on paper, give the nutrient somewhere to travel, and then run a valley long enough that a wrong answer has time to show.