Peer review: “from one force law to a battery”

A read of math.html against src/field/shaders.ts, noise.ts, main.ts and the slider defaults in field.html, judged against the stated goal: an energy-conserving particle life that keeps particle life's emergence and adds complexity on top.

Verdict in five sentences. The derivation is correct, the worked example checks to three decimals, and the “one potential, two derivatives” idea is genuinely good: it is the cleanest way I know to make a non-reciprocal force pay its way. The scheme has one structural flaw that the page treats as a numerical nuisance but is not: the hue rate $-P/W$ is singular exactly where burning sends every particle, so the leak the ledger books as “unpaid” is built in, not incidental. It has a clean fix (let the chase pull only as hard as the battery can fund) and a diagnosable cause (the bond's hue-dependence, ampS, moves the energy minimum away from $\Lambda=0$). Separately, the page is out of step with the code: the field defaults it lists are not the ones that ship, and the model that ships is plain overdamped particle life with every energy mechanic off. Finally, the goal itself has a tension the page does not name: particle life's emergence is a throughput phenomenon, and a closed conserving system runs down, so the useful target is a budgeted driven-dissipative system, which is what the ledger is already halfway to being.

  1. What is right
  2. Line check of the derivation
  3. The $P/W$ rule is singular at its own attractor
  4. Three fixes, ranked
  5. Against the goal: conservation vs emergence
  6. Page vs code drift
  7. Discretisation
  8. Smaller notes
  9. Experiments to run next, with predictions

2. Line check of the derivation

I recomputed the §8 table independently. Every entry agrees to the printed precision:

QuantityPageRecomputed
$S(0.8)$1.648$1.3 + 0.5\cdot 0.6967 = 1.6484$
$\Lambda(0.8)$0.502$0.7\cdot 0.7174 = 0.5022$
$W_i$1.256$0.7174 + 2\cdot0.55\cdot0.5022\cdot0.4877\cdot2 = 0.7174+0.5388 = 1.2562$
$\alpha_i W_i^2$0.803$0.509\cdot1.578 = 0.803$, equals $P_i$
$\dot h_j$−0.16$-(-0.127)(-1.256) = -0.160$; sign and the “away from $i$” reading are correct

The live sim's terms also match its own stated potential: with $V=-s^2$, $s = 1-r/R$, the code's $dV = 2s/R$, $dw = -2s/R$, $W = S'V + 2\lambda w\Lambda\Lambda'$ and $f_r = -U'_{\text{rep}} - S\,V' - \lambda\Lambda^2 w'$ are all consistent. The §10 shader implements the boxed $U_{ij}$ and its two derivatives faithfully. I found no algebra errors.

One logical slip in §3: “Because hue is an angle, $f$ only depends on the difference $d$.” Being an angle gives periodicity, not translation invariance in hue. The page knows this (§10a uses the full two-argument form) but the sentence as written is a non sequitur. Say “for sections 3–9 we assume $f$ depends only on $d$” and move on.

3. The $P/W$ rule is singular at its own attractor

The page presents the guard and the slew cap as numerical hygiene for the rare case $W_i \to 0$. It is not rare. Look at what the burn rule does to one particle's hue while $P_i > 0$ and the neighbourhood is momentarily fixed. Near a zero $h^*$ of $W_i(h_i)$, write $W_i \approx c\,(h_i - h^*)$. Then

$$ \dot h_i = -\frac{P_i}{c\,(h_i - h^*)} . $$

For $c > 0$ (a local minimum of $E$ over $h_i$) this flow points toward $h^*$ from both sides and reaches it in finite time, like $\dot x = -1/x$. So under burn every particle is sucked into the nearest minimum of its own hue landscape and arrives at the singularity with $|\dot h| \to \infty$. Under recharge ($P_i<0$) the same happens at maxima. The guard and the slew cap therefore engage at the destination of the dynamics, not in a corner case.

In the toy model of §3–9 this is harmless, because the only hue-dependent energy is $\lambda\Lambda^2$, whose minimum is at $\Lambda = 0$, where the chase vanishes too. Burning is self-limiting: by the time hue stalls there is nothing left to pay for. Question 1 on the questions page shows the ratio $P/W$ staying finite there for exactly this reason.

In the built model this coincidence is gone. The hue gradient the shader accumulates is

$$ W_i = \sum_j \Big[\underbrace{\partial_{h_i} S\;V(r)}_{\text{bond}} + \underbrace{\lambda\,w(r)\,B'(\Lambda)\,\partial_{h_i}\Lambda}_{\text{battery}}\Big], $$

and with ampS comparable to $\lambda\,\text{amp}_\Lambda^2$ (the page's field numbers give roughly $1$ against $0.7$) the minima of $E$ over $h_i$ sit at generic hues where $\Lambda \neq 0$. A “burned-out” particle in the field is therefore not one whose chase has died. It is one sitting at the bottom of its hue well, still being pushed at full strength, with no gradient left to pay along. Every joule of chase work from then on is booked as unpaid. This is the mechanism behind the ledger's unpaid column, and it means “unpaid” is not a discretisation error that shrinks with $\Delta t$. It is $O(1)$ in the model.

The store and the throttle are the same variable, and that is the design's strength and its trap. Hue mismatch both powers the chase and stores its cost, so in the toy model spending the store automatically switches off the load. The bond's hue-dependence breaks that link by giving hue a second reason to sit somewhere. Either restore the link or throttle the chase explicitly.

4. Three fixes, ranked

4a. RECOMMENDED Let the chase pull only as hard as the battery can fund

Compute the hue rate as now, clamp it, then compute what it actually pays, $\text{paid}_i = -\dot h_i W_i$, and scale the chase force applied this step by the fraction covered:

$$ \mathbf F^{\text{chase}}_i \;\leftarrow\; \mathbf F^{\text{chase}}_i \cdot \frac{\text{paid}_i}{s\,P_i}\qquad (\text{when } sP_i > 0,\ \text{else unchanged}). $$

The unpaid column then goes to zero by construction, and the ledger identity becomes a physical statement again rather than an accounting one. Physically it is the obvious rule: an exhausted battery cannot drive. It also gives the field a behaviour particle life does not have and which the goal asks for: chasers that run out, stall, and only resume when something recharges them. In the shader this is a few lines after the colour solve, since chase is already accumulated separately from F. The friction share must be excluded from the scaling (friction is real dissipation; only the chase is fictitious power), so scale by $(\text{paid}_i - sP^{\text{fric}}_i)/(sP^{\text{chase}}_i)$ when the chase share is positive.

The recharge side ($P_i<0$) needs no throttle: braking removes real kinetic energy, and whatever hue cannot absorb is correctly heat. That path already exists in the shader.

4b. WORTH TESTING Make the battery dominate the hue landscape

The singularity lands at $\Lambda \neq 0$ only because the bond term competes with the battery term for control of $h_i$. Two ways to give the battery back control:

4c. ALTERNATIVE ARCHITECTURE Separate fuel from phenotype

The page's thesis is that hue is the battery. A different design keeps hue as the thing that selects forces and gives each particle a scalar fuel $q_i \ge 0$ that pays for the chase directly: $\dot q_i = -P_i$, chase strength $\kappa\,\min(1, q_i/q_0)$, and a separate rule for how eating or braking moves hue. Energy conservation becomes trivial (no $W$, no guard, no slew), and all the design freedom moves to the replenishment and hue rules, which is where the interesting complexity lives. You lose the elegance of one potential and two derivatives, and you must add the self-limiting by hand. I would not switch now, but if 4a plus 4b still leaves the field feeling arbitrary, this is the fallback, and it is easier to reason about at $N=4000$.

5. Against the goal: conservation vs emergence

Particle life's gliders, chasers and self-propelled blobs exist because the system is driven and dissipative: non-reciprocal forces inject energy continuously and friction removes it continuously, and the shapes are the steady states of that throughput. They are also overdamped: $\mathbf v \propto \mathbf F$, first-order dynamics, which is why blobs hold their shape instead of ringing. Neither property survives strict energy conservation with inertia:

So the conserved quantity that actually holds, $E + \text{heat} - \text{unpaid} = \text{const}$, is an accounting identity: it is true of any model if heat is defined as the residual. That is not a criticism of the ledger, which is exactly the right instrument. It is a request to state the target plainly: a driven-dissipative system with an explicit budget, where the interesting question is not “is $E$ constant” but “where does the throughput go”. Concretely:

  1. Add a source. Something must inject hue mismatch: random hue kicks at a rate $\Phi$, particles re-entering at the boundary with fresh hue, or a designated “sun” region where $\lambda$ is temporarily raised. The ledger gains a column and the steady state is $\Phi = $ heat rate.
  2. Keep $\eta$ small but nonzero as the sink, and show the ledger as rates (power in, stored, dissipated, unpaid) rather than cumulants. Cumulants only grow; rates tell you whether the box is in steady state.
  3. Then the added complexity writes itself. Mismatch is now a resource that is carried in hue, so a mismatched particle is food, a clump that captures one burns it to move, and chasers stall when starved (fix 4a). That is genuinely beyond particle life and it comes for free once the books close.

The Step 1/2/3 plan in the main.ts comment (classic particle life first, then a well, then the colour physics) is the right path. The page should describe it, and each step should have a measured claim attached (“Step 1 reproduces particle life's phenomenology at $\eta=3$”; “Step 3 at $\eta=0.3$ keeps the same blobs with unpaid $<1\%$ of injected”).

6. Page vs code drift

The §2 “Field” column and §10 describe a build that no longer ships. Current field.html defaults:

KnobPage saysfield.htmlConsequence
$\lambda$0.550no battery
conserve $s$10nothing is charged to colour; hues never move
$\eta$03overdamped: this is classic particle life
$\eta_{\text{bond}}$40
$S_0$, amp$_S$, amp$_\Lambda$0.2, 1.0, 0.80, 3, 3
$r_0$, $k$0.1, 400.3, 6
$\rho$, $\sigma$0.2, 0.10.25, 0.06
$\Delta t$0.0010.004
profileGaussian well onlytriangle, $\beta=0.3$, wellDepth 0not on the page at all
speciesnot mentioned6, rectifiedsee the trap below

Three things on the page are therefore untrue of the running program: the radial shapes in §10b (the shader's default profile is the particle-life triangle with an optional well added, $V = -A - \text{wellDepth}\cdot GT$), the claim that the chase envelope follows the well (in triangle mode $g = t + \text{wellDepth}\cdot GT$ reaches $R$, which is precisely the case §10b argues against), and the cheat sheet. Either update the page to describe both profiles, or make the page point at the code as the source of truth for numbers and keep only the shapes here.

The species trap. With species = 6 the noise is held constant across each hue block, so $S$ and $\Lambda$ are piecewise constant and $\partial_{h_i}$ of both is zero except within about three table cells of a block edge (the B-spline's support, roughly $8°$ of hue). Turning conserve up in this mode puts nearly every particle under the guard: $W_i^2 < 10^{-12}$, hue frozen, every joule unpaid. Step 3 of the plan will look broken unless species is set to $0$ first. Worth a sentence on the page and a warning in the UI.

7. Discretisation

8. Smaller notes

9. Experiments to run next, with predictions

Each of these is a few minutes in lab.html or field.html and settles a claim above.

  1. Set amp$_S = 0$, species $=0$, conserve $=1$, $\eta=0$. Prediction: unpaid collapses to the discretisation floor (tracks $\Delta t$), because hue minima now coincide with $\Lambda=0$. If it does not, §3 of this review is wrong and the leak is somewhere else.
  2. Restore amp$_S$ and sweep $\lambda \in \{0.5, 1, 2, 4\}$. Prediction: unpaid falls monotonically as the battery term takes over the hue landscape.
  3. Implement 4a and repeat 2. Prediction: unpaid is identically zero; the visible change is chasers that stall and idle until bumped.
  4. Halve $\Delta t$ twice at fixed everything else. Prediction: the residual falls by $2\times$ per halving with the current scheme, $4\times$ with midpoint payment. Put the plot on the page.
  5. Run Step 3 with a source. Add random hue kicks at rate $\Phi$ with $\eta = 0.3$, and plot the four power rates. Prediction: a steady state with sustained chasers, which is the first configuration that is both budgeted and alive, and the one to build the “added complexity” on.

Reviewed against the working tree on 2026-09-16: docs/math.html, docs/questions.html, src/field/{shaders,noise,main}.ts, field.html.