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
- The even/odd split is the right first move. Any $f(h_i,h_j)$ splits uniquely under argument swap, the symmetric half is a legitimate pair potential, and the antisymmetric half is provably not. §5's argument (a pair potential gives equal and opposite forces, so a same-direction force cannot come from one) is correct for any translation-invariant $U$.
- The energy balance in §6c is exact algebra. $\dot E = \sum_i (P_i + W_i \dot h_i)$ after the conservative cancellation, and $\dot h_i = -P_i/W_i$ zeroes it term by term. No hidden assumption.
- The refusal to soften the denominator is correct and well argued. $P\varepsilon/(W^2+\varepsilon)$ is a leak that is largest where it is least visible.
- Differentiating the interpolant rather than the table (§10a, cubic B-spline) is the right call, and the measured “quarter of the friction energy with bilinear” is exactly the per-crossing error one expects. This is the best engineering decision on the page.
- Evaluating each pair from both sides and relying on exchange symmetry of $U_{ij}$ is valid: $S(a,b)=S(b,a)$ and $B(\Lambda(a,b)) = B(-\Lambda(b,a)) = B(\Lambda(b,a))$ since $B$ is even, so both sides compute the same $U_{ij}$ and the half-energies sum correctly.
- Bond friction on the radial relative velocity is momentum-conserving and drains only the breathing mode. Routing its power into the same balance as the chase is consistent, and the shortfall attribution in the shader (friction's share to heat first, the chase's share to unpaid) is defensible bookkeeping.
- Pedagogy. Building with $1/r^2$ and a cosine law, then swapping shapes in §10, is the right order. The worked example and the live two-body sim make the sign of $\alpha$ tangible.
2. Line check of the derivation
I recomputed the §8 table independently. Every entry agrees to the printed precision:
| Quantity | Page | Recomputed |
|---|---|---|
| $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.
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:
- Raise $\lambda$ until $\lambda\,\text{amp}_\Lambda^2\,w \gg \text{amp}_S\,|V|$. Roughly $\lambda \gtrsim 3$ at the page's amplitudes. Then hue minima sit close to $\Lambda=0$ and the chase self-limits again. Cost: the same power moves hue less, so chases last longer, which may be what you want anyway.
- Change the store shape to something whose gradient does not vanish at $\Lambda=0$ and has no cusp: the pseudo-Huber $B(\Lambda) = \sqrt{\Lambda^2 + \epsilon^2} - \epsilon$. This is the standard resolution of the $\Lambda^2$-sloshes / $|\Lambda|$-chatters dilemma in §6a and Q4, and it replaces the radio button with one continuous knob $\epsilon$.
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:
- A closed conserving system has bounded total energy, so all persistent motion is funded by initial mismatch and unwinds. The page says this itself in §10e (“they all unwind unless something feeds mismatch back in”), which is the honest conclusion and should be promoted to the introduction rather than left in a table cell.
- Inertial clumps ring, orbit and heat up. The page has already been forced to add bond friction and a heat ledger to fix this, which is dissipation by another name.
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:
- 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.
- 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.
- 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:
| Knob | Page says | field.html | Consequence |
|---|---|---|---|
| $\lambda$ | 0.55 | 0 | no battery |
| conserve $s$ | 1 | 0 | nothing is charged to colour; hues never move |
| $\eta$ | 0 | 3 | overdamped: this is classic particle life |
| $\eta_{\text{bond}}$ | 4 | 0 | |
| $S_0$, amp$_S$, amp$_\Lambda$ | 0.2, 1.0, 0.8 | 0, 3, 3 | |
| $r_0$, $k$ | 0.1, 40 | 0.3, 6 | |
| $\rho$, $\sigma$ | 0.2, 0.1 | 0.25, 0.06 | |
| $\Delta t$ | 0.001 | 0.004 | |
| profile | Gaussian well only | triangle, $\beta=0.3$, wellDepth 0 | not on the page at all |
| species | not mentioned | 6, rectified | see 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.
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
- The continuous law conserves exactly; the discrete scheme does not, at $O(\Delta t)$ per step. Semi-implicit Euler is symplectic for the conservative part, but the hue payment uses $P_i$ from $\mathbf v_n$ and $W_i$ from $h_n$, while the kinetic energy actually gained from the chase over the step is $\mathbf F^{\text{chase}}\cdot\tfrac12(\mathbf v_n + \mathbf v_{n+1})\,\Delta t$. The mismatch is a first-order drift with a definite sign bias. The page's “two percent and shrinking with $\Delta t$” is consistent with this. Two cheap improvements: pay with the midpoint velocity (one extra multiply, since $\mathbf v_{n+1}$ is known in the same pass once $\mathbf F$ is summed), and confirm the order by halving $\Delta t$ twice and plotting the residual, which the page should show as a figure since it is the central claim.
- Discrete-exact payment is available. Because $U$ is a table lookup, each particle can evaluate its own half-energy at the trial hue $h_i + \dot h_i\Delta t$ and Newton-correct $\dot h_i$ once so that $\Delta e_i = -P_i\Delta t$ to round-off. Cross terms from the neighbour's simultaneous hue move remain at $O(\Delta t^2)$. This is the version that would let the residual be quoted in parts per million.
- Stiffness. $k_{\text{rep}}$ and $\sigma$ set the step. With the page's field numbers ($k=40$, $\sigma=0.1$) at $\Delta t = 0.001$ you are fine; at the shipped $\Delta t = 0.004$ with $\sigma=0.06$ the well's curvature is $\sim 1/\sigma^2 \approx 280$, giving $\omega\Delta t \approx 0.07$, still fine for symplectic Euler but worth a line in §2.
- Hue step vs table resolution. The slew cap of $3$ rad/s at $\Delta t = 0.004$ moves hue $0.012$ rad per step against a cell of $0.049$ rad, so the spline is sampled adequately. If slew is raised past about $12$ this stops being true and the per-crossing argument in §10a reappears.
8. Smaller notes
- Momentum is never mentioned. The chase force is the same vector on both particles, so total momentum is not conserved and a chase pair self-propels. In a periodic box the whole system can acquire a drift, which is why the page's remark that global $\eta$ “removes centre-of-mass motion too” matters. Say so in §5, where the reader is already being told what the chase breaks. Angular momentum likewise.
- Sign conventions disagree across the page and the code. §1 defines $f>0$ as repel. §3's legend calls $f = S+\Lambda$ “what a particle-life matrix shows”. §4 then flips the reciprocal force to $-S$ so that $S>0$ attracts. The matrix drawing in
main.tspaints $S - \Lambda$ as the pull on $i$. Pick one convention in §1 (positive $=$ attract is the one everything downstream uses) and carry it through. - “$S_0 > a$ is required” (§3 caption) contradicts §2, where $S_0$ below the amplitude is described as the regime that sorts clumps by hue, and the shipped default is $S_0 = 0$. It is a regime, not a requirement; reword.
- Three radial families, not two. §4–8 use $1/r^2$, the §9 demo uses $(1-r/R)$ shapes and claims they are “the code's real fall-off shapes (appendix)”, and §10 uses Gaussian times taper. The demo caption is wrong. Either port the demo to §10's shapes or label it as a third, gate-era family.
- Common-mode hue drift. In the $d$-only model, shifting both hues equally costs nothing, and the per-particle rule excites that mode: in the §8 example both $\dot h_i$ and $\dot h_j$ are negative. The pair's mean hue random-walks at zero energy cost. Harmless, but it is why colours wander over long runs in the toy sim, and the full torus noise removes it. One sentence.
- “$\alpha$ is not a knob” is true, but guard and slew are, and they decide how much goes unpaid. After fix 4a the sentence becomes fully true; until then it oversells.
- The per-particle demand is a choice. §6c demands $\dot E = 0$ “for every particle separately”. Demanding it globally (one $\alpha = \sum P_i / \sum W_i^2$) is always well-posed and needs one reduction per step, but loses the local burn/recharge sign, which is most of the phenomenology. Mention that the local choice was deliberate and why.
- Battery capacity is tiny and the page should say the number. Maximum stored mismatch per pair is $\lambda\,\text{amp}_\Lambda^2\,w \le 0.35$ at the page's constants, against a well depth of order $1$ and typical chase kinetic energies of the same order. This is why most captured vibration ends as heat and why chases are short. It argues for the larger $\lambda$ in 4b.
- Shortfall attribution is order-dependent. The shader attributes a negative shortfall to friction first, up to $s\,P^{\text{fric}}_i$, and only the remainder to the chase. When both are braking that is a convention, not a derivation. Fine, but say so where the ledger is defined.
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.
- 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.
- 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.
- Implement 4a and repeat 2. Prediction: unpaid is identically zero; the visible change is chasers that stall and idle until bumped.
- 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.
- 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.