/* chemoton.c — compartments that GROW from their own chemistry's output. Does the stochastic
* corrector appear WITHOUT anyone decreeing group selection?
*
* WHERE THIS COMES FROM. byproduct.c applied the byproduct idea to the wrong half of what a
* compartment is. It coupled catalysis -> wall HEIGHT -> isolation, and measured that isolation
* alone does nothing: supplied walls at full height reach phi 5.65, no better than open space, while
* supplied walls PLUS between-site selection reach 10.78. So the barrier was never the active
* ingredient. What made compartments work is that they DIVIDE and are SELECTED on catalytic content
* -- and in ribozyme.c and byproduct.c that step was DECREED ("sample a parent site proportional to
* its mean phi"), never derived.
*
* THE FIX IS THE SAME IDEA, MOVED. A compartment divides when it grows big enough, so differential
* reproduction reduces to differential GROWTH. If the wall material is produced by the chemistry
* inside, growth rate is automatically a function of internal catalytic content:
* catalysis -> membrane material -> growth -> division
* A parasite produces no material, so a parasite-heavy compartment grows slowly and divides rarely.
* That yields the decreed rule as a CONSEQUENCE rather than an assertion. (This is Ganti's chemoton
* / the Szostak protocell coupling -- the field's standing answer, not a new idea. What is new here
* is testing it against our own measured claim that isolation is not enough.)
*
* AND A SECOND INGREDIENT byproduct.c LACKED: something LIMITING. There, sites were fixed at 600, so
* a dissolving region freed nothing and a thriving one had nowhere to expand. Decay without
* competition is not selection. Here MATERIAL IS CONSERVED: pool + compartment stores + units*cost
* is constant, uptake is rationed when demand exceeds supply, and dead units return their material.
* Compartments genuinely compete.
*
* WHAT IS SUPPLIED vs WHAT MUST EMERGE:
* SUPPLIED: the physical coupling activity -> material uptake, a division size, a per-unit death
* rate, and a conserved material pool. The division size is SWEPT, not picked.
* MUST EMERGE: differential reproduction of compartments. NOTHING samples parents by fitness.
* Compartments divide because they grew; they grew because they took up material; they took up
* material because their contents catalyzed. No group-selection step exists anywhere in the code.
* UNTOUCHED: the within-compartment tragedy. Parasites still replicate 3x faster and still take the
* growth slots, so a fast-growing compartment fills with the parasites its own growth carries.
*
* Chemistry identical to ribozyme.c / coupling.c / partition.c / signal.c / byproduct.c
* (phi = hairpin fold, parasites 3x faster, F0=0.85, F1=0.999, start phi ~5.99 = the neutral
* expectation 6.0). Reference: ribozyme well-mixed 5.99 -> 5.50; DECREED group selection at
* compartment size 8 -> 11.34, and it FAILS at size >= 16.
*
* PRE-REGISTERED:
* C1 the within-compartment tragedy still runs (parasites take growth slots) -- stated so that a
* rise cannot be explained by having quietly removed the problem.
* C2 phi RISES anyway, because catalyst-rich compartments divide faster. The corrector, emergent.
* C3 THE KEY TEST: the SIZE THRESHOLD reappears on its own. Sweeping division size, crossing at
* small sizes and failure at large ones -- matching ribozyme.c's decreed-selection result
* (works <= 8, fails >= 16). If growth-coupling really is the mechanism behind the decree, it
* must reproduce the decree's signature, not merely produce some rise.
* C4 CONTROL: uptake CONSTANT PER UNIT, independent of phi. Identical division, assortment, death
* and material limit; only the growth->catalysis coupling is cut. If phi rises here too, the
* effect is division-plus-assortment per se and C2/C3 mean nothing.
* C5 TEST OF THE TEST: do compartments actually divide and die, and does the population turn over?
* Report divisions and deaths per generation and the compartment count. If nothing divides, no
* compartment-level reproduction happened and a null says nothing.
*
* Do not tune to force a positive. A null with C5 passing is a real result: it would say the
* coupling is insufficient and the first unit of selection had to be supplied by the world.
*
* build: cc -O2 -Wall -std=c11 -o chemoton chemoton.c -lm
*/
#include <math.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#define L 24
#define PHIMAX (L/2)
#define F0 0.85
#define F1 0.999
#define BETA 2.0
#define GEN 300
#define MAXCOMP 3000
#define MAXSZ 48
#define COSTUNIT 1.0
#define TOTALMAT 6000.0 /* conserved: pool + stores + units*COSTUNIT */
#define UPTAKE 0.55
#define DEATH 0.10
static unsigned long long RNG;
static unsigned long long xs(void){ RNG^=RNG<<13; RNG^=RNG>>7; RNG^=RNG<<17; return RNG; }
static double urand(void){ return (double)(xs()>>11)*(1.0/9007199254740992.0); }
static int phi(uint32_t s){ int c=0; for(int i=0;i<PHIMAX;i++){ int a=(s>>i)&1,b=(s>>(L-1-i))&1; if(a!=b)c++; } return c; }
static uint32_t replicate(uint32_t t,double f){ uint32_t c=0; for(int i=0;i<L;i++){ int b=(t>>i)&1; int nb=(urand()<f)?(1-b):b; if(nb)c|=(1u<<i);} return c; }
typedef struct { uint32_t u[MAXSZ]; int n; double mat; } Comp;
static Comp C[MAXCOMP];
static int ncomp;
static double pool;
static double demand[MAXCOMP];
/* coupled=1: uptake tracks catalytic output. coupled=0: uptake is constant per unit (C4 control). */
static double run(int coupled,int divsz,int propagule,double *phi_start,double *ndiv,double *ndeath,
double *ncomp_end,double *msize,double *matcheck)
{
int seed_n = divsz/2 > 2 ? divsz/2 : 2;
ncomp = 600;
for(int c=0;c<ncomp;c++){
C[c].n=seed_n; C[c].mat=0.0;
for(int k=0;k<seed_n;k++){ uint32_t v=0; for(int b=0;b<L;b++) if(urand()<0.5) v|=(1u<<b); C[c].u[k]=v; }
}
{ double sp=0; long t=0; for(int c=0;c<ncomp;c++) for(int k=0;k<C[c].n;k++){ sp+=phi(C[c].u[k]); t++; } *phi_start=sp/t; }
{ long units=0; for(int c=0;c<ncomp;c++) units+=C[c].n; pool = TOTALMAT - units*COSTUNIT; }
double totdiv=0, totdeath=0;
for(int g=0;g<GEN;g++){
/* ---- uptake: demand from catalytic output, RATIONED when the pool cannot meet it ---- */
double D=0;
for(int c=0;c<ncomp;c++){
double out=0;
if(coupled){ for(int k=0;k<C[c].n;k++) out += (double)phi(C[c].u[k])/PHIMAX; }
else out = 0.5*C[c].n; /* control: same scale, blind to phi */
demand[c]=UPTAKE*out; D+=demand[c];
}
double frac = (D>0 && D>pool)? pool/D : 1.0;
for(int c=0;c<ncomp;c++){ double take=demand[c]*frac; C[c].mat+=take; pool-=take; }
/* ---- growth: spend material on new units. Parasites still take the slots (C1). ---- */
for(int c=0;c<ncomp;c++){
if(C[c].n<=0) continue;
double a=0; for(int k=0;k<C[c].n;k++) a+=phi(C[c].u[k]);
double f=F0+(F1-F0)*((a/C[c].n)/PHIMAX);
while(C[c].mat>=COSTUNIT && C[c].n<MAXSZ){
double tot=0,cum[MAXSZ];
for(int k=0;k<C[c].n;k++){ tot+=1.0+BETA*(1.0-(double)phi(C[c].u[k])/PHIMAX); cum[k]=tot; }
double r=urand()*tot; int lo=0,hi=C[c].n-1;
while(lo<hi){ int mid=(lo+hi)/2; if(cum[mid]<r) lo=mid+1; else hi=mid; }
C[c].u[C[c].n]=replicate(C[c].u[lo],f); C[c].n++; C[c].mat-=COSTUNIT;
}
}
/* ---- per-unit death: material returns to the pool ---- */
for(int c=0;c<ncomp;c++){
for(int k=0;k<C[c].n;){
if(urand()<DEATH){ C[c].u[k]=C[c].u[C[c].n-1]; C[c].n--; pool+=COSTUNIT; }
else k++;
}
}
/* ---- division at threshold: random assortment, material split ---- */
int base=ncomp;
for(int c=0;c<base;c++){
if(C[c].n>=divsz && ncomp<MAXCOMP){
Comp *p=&C[c], *q=&C[ncomp];
if(propagule){
/* BOTTLENECK: the offspring is ONE randomly chosen unit -- it may equally be a
parasite. Nothing selects which unit buds; only the VARIANCE among offspring
compartments changes, which is the quantity the relatedness literature says
does the work (a zygote is the n=1 limit of a small compartment). */
int pick=(int)(xs()%(unsigned long long)p->n);
double share=p->mat/(double)p->n;
q->n=0; q->u[q->n++]=p->u[pick]; q->mat=share; p->mat-=share;
p->u[pick]=p->u[p->n-1]; p->n--;
} else {
q->n=0; q->mat=p->mat*0.5; p->mat*=0.5;
int w=0;
for(int k=0;k<p->n;k++){
if(urand()<0.5) q->u[q->n++]=p->u[k];
else p->u[w++]=p->u[k];
}
p->n=w;
}
ncomp++; totdiv++;
}
}
/* ---- compartment death when empty: stored material returns ---- */
for(int c=0;c<ncomp;){
if(C[c].n<=0){ pool+=C[c].mat; C[c]=C[ncomp-1]; ncomp--; totdeath++; }
else c++;
}
if(ncomp<=0) break;
}
double sp=0; long t=0;
for(int c=0;c<ncomp;c++) for(int k=0;k<C[c].n;k++){ sp+=phi(C[c].u[k]); t++; }
double stored=0; for(int c=0;c<ncomp;c++) stored+=C[c].mat;
*ndiv=totdiv/GEN; *ndeath=totdeath/GEN; *ncomp_end=ncomp;
*msize = ncomp? (double)t/ncomp : 0;
*matcheck = pool+stored+t*COSTUNIT;
return t? sp/t : 0.0;
}
int main(void)
{
unsigned long long seeds[6];
seeds[0]=0x9e3779b97f4a7c15ULL; seeds[1]=0xd1b54a32d192ed03ULL; seeds[2]=0x2545f4914f6cdd1dULL;
for(int i=3;i<6;i++) seeds[i]=0x9e3779b97f4a7c15ULL ^ (0x100000001b3ULL*(unsigned long long)(i+1));
int NS=6;
printf("=== CHEMOTON: compartments that grow from their own chemistry's output ===\n");
printf(" catalysis -> material uptake -> growth -> division. NOTHING samples parents by fitness;\n");
printf(" no group-selection step exists in the code. Material is CONSERVED (%.0f) and rationed,\n",TOTALMAT);
printf(" so compartments compete. Parasites still replicate %.0fx faster and still take growth slots.\n",1+BETA);
printf(" REFERENCE: ribozyme, with DECREED group selection, crosses at size 8 (11.34) and FAILS\n");
printf(" at size >= 16. If growth-coupling is the mechanism behind that decree, the SIZE THRESHOLD\n");
printf(" must reappear here on its own.\n\n");
printf(" div size | FISSION phi | fis CONTROL | PROPAGULE phi | prop CONTROL | fis mean sz | prop mean sz\n");
printf(" ---------+-------------+-------------+---------------+--------------+-------------+-------------\n");
int sizes[]={4,6,8,10,12,16,20,24,32};
int NZ=(int)(sizeof(sizes)/sizeof(sizes[0]));
double start=0; int turned_over=0;
double co[16], ct[16], cb[16], cz[16], fs[16], bsz[16];
for(int z=0;z<NZ;z++){
double ec=0,ek=0,eb=0,ez=0,bd=0,bs=0,nd=0,nx=0,nc=0,ms=0,mc=0,ps=0;
for(int s=0;s<NS;s++){
double p,d,x,n,m,mm;
RNG=seeds[s]; ec+=run(1,sizes[z],0,&p,&d,&x,&n,&m,&mm); nd+=d; nx+=x; nc+=n; ms+=m; mc+=mm; ps+=p;
RNG=seeds[s]; ek+=run(0,sizes[z],0,&p,&d,&x,&n,&m,&mm);
RNG=seeds[s]; eb+=run(1,sizes[z],1,&p,&d,&x,&n,&m,&mm); bd+=d; bs+=m;
RNG=seeds[s]; ez+=run(0,sizes[z],1,&p,&d,&x,&n,&m,&mm);
}
ec/=NS;ek/=NS;eb/=NS;ez/=NS;bd/=NS;bs/=NS;nd/=NS;nx/=NS;nc/=NS;ms/=NS;mc/=NS;ps/=NS;
start=ps; co[z]=ec; ct[z]=ek;
printf(" %2d | %5.2f | %5.2f | %5.2f | %5.2f | %5.2f | %5.2f\n",
sizes[z],ec,ek,eb,ez,ms,bs);
cb[z]=eb; cz[z]=ez; fs[z]=ms; bsz[z]=bs;
if(nd>1.0 && nx>1.0) turned_over=1;
}
printf("\n start phi (= neutral expectation) %.2f [material must stay ~%.0f]\n\n",start,TOTALMAT);
int small_wins=0, large_fails=0, ctrl_flat=1;
for(int z=0;z<NZ;z++){
if(sizes[z]<=8 && co[z]>start+0.5) small_wins++;
if(sizes[z]>=16 && co[z]<start+0.5) large_fails++;
if(ct[z]>start+0.5) ctrl_flat=0;
}
/* ---- THE CONFOUND CHECK: propagule mode also SHRINKS realized compartment size, so a rescue
at matched DIVISION THRESHOLD may simply be smaller compartments by another route. Plot phi
against MEAN REALIZED SIZE for both modes: if they trace one curve, size is the variable and
variance adds nothing. If propagule sits above fission at matched size, variance is real. ---- */
printf(" phi vs MEAN REALIZED SIZE (the confound check)\n");
printf(" fission :");
for(int z=0;z<NZ;z++) printf(" %.2f/%.1f", co[z], fs[z]);
printf("\n propagule:");
for(int z=0;z<NZ;z++) printf(" %.2f/%.1f", cb[z], bsz[z]);
printf("\n (read as phi/size pairs)\n");
{
double worst=1e9, sumd=0; int nd2=0;
for(int z=0;z<NZ;z++){
double tsz=bsz[z]; if(tsz<=fs[0]||tsz>=fs[NZ-1]) continue;
int lo=-1;
for(int y=0;y+1<NZ;y++) if(fs[y]<=tsz && fs[y+1]>=tsz){ lo=y; break; }
if(lo<0) continue;
double t=(tsz-fs[lo])/(fs[lo+1]-fs[lo]);
double fis_interp=co[lo]+t*(co[lo+1]-co[lo]);
double d=cb[z]-fis_interp;
printf(" at size %.1f : propagule %.2f vs fission-interpolated %.2f (diff %+.2f)\n", tsz, cb[z], fis_interp, d);
sumd+=d; nd2++; if(d<worst) worst=d;
}
if(nd2) printf(" mean advantage of propagule at MATCHED size: %+.2f over %d points\n\n", sumd/nd2, nd2);
else printf(" (no overlapping sizes to compare)\n\n");
}
int fis_small=0, fis_large=0, prop_small=0, prop_large=0, prop_ctrl_flat=1;
for(int z=0;z<NZ;z++){
if(sizes[z]<=8 && co[z]>start+0.5) fis_small++;
if(sizes[z]>=16 && co[z]>start+0.5) fis_large++;
if(sizes[z]<=8 && cb[z]>start+0.5) prop_small++;
if(sizes[z]>=16 && cb[z]>start+0.5) prop_large++;
if(cz[z]>start+0.5) prop_ctrl_flat=0;
}
printf(" C5 turnover : %s\n", turned_over?"YES":"NO — test did not run");
printf(" FISSION crosses at small / large div size : %d/2 %d/2\n", fis_small, fis_large);
printf(" PROPAGULE crosses at small / large div size : %d/2 %d/2\n", prop_small, prop_large);
printf(" A3 CONTROL propagule + uptake blind to phi : %s\n\n", prop_ctrl_flat?"flat (good)":"ROSE — CONFOUNDED");
if(!turned_over)
printf(" => THE TEST DID NOT RUN.\n");
else if(!prop_ctrl_flat)
printf(" => CONFOUNDED. The phi-blind control rises under propagule reproduction, so the change is\n"
" a demographic artifact of the reproduction mode, not selection. Claim nothing.\n");
else if(prop_large>fis_large)
printf(" => The bottleneck rescues large DIVISION THRESHOLDS (fission %d, propagule %d of the\n"
" large sizes). But propagule mode ALSO shrinks realized compartment size, so read the\n"
" matched-size comparison above before concluding anything: if the advantage at matched\n"
" size is ~0, size is the operative variable and the bottleneck merely produces smaller\n"
" compartments by another route. Only a clear positive there licenses the claim that\n"
" ASSORTMENT VARIANCE is what does the work.\n", fis_large, prop_large);
else if(prop_large==fis_large && prop_small>=fis_small)
printf(" => NO RESCUE. A single-unit bottleneck does NOT save large compartments (%d/2 vs %d/2),\n"
" so the size threshold is NOT about assortment variance. The remaining candidate is\n"
" within-compartment competition TIME: a large compartment gives parasites more\n"
" generations to take over before it ever divides, and no amount of offspring variance\n"
" fixes that. Informative negative -- it rules out the relatedness reading.\n",
fis_large, prop_large);
else
printf(" => Mixed. Report the numbers as-is.\n");
return 0;
}