OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / basal-agent / chemoton.c

297 lines · c ref:
/* 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;
}