OverviewFilesCommitsIssuesDocsReleasesPeers

basal-agent / fidfloor.c

236 lines · c ref:
/* fidfloor.c — does EMERGENT compartment selection lower the fidelity floor? (rung 1+4 -> 2)
 *
 * FIXED after a first run that was INVALID. Three faults, all found and corrected:
 *  (1) MIXED was not well-mixed — 120 isolated compartments with division off, i.e. MORE
 *      compartmentalized than the arms it controls for. Now strands are SHUFFLED globally every
 *      step, making it one pool, which is what polymer.c is.
 *  (2) POPULATIONS COLLAPSED because uptake was below replacement by construction:
 *      UPTAKE*0.5 = 0.275 against degradation 0.30. BLIND could never sustain itself, and the
 *      apparent 'SELECT wins' was simply SELECT being the only arm able to exceed replacement
 *      once function climbed. Per-unit growth rate is now GROW=0.45 > D=0.30 for every arm.
 *  (3) CONFOUND: selection changed TOTAL uptake, hence population and replication count (8099 vs
 *      3308). Now the total material moved per step is IDENTICAL across arms by construction —
 *      only its ALLOCATION differs (by mean match, or by size, or uniformly). This is the
 *      matched-condition discipline applied at design time rather than after the fact.
 *
 * ---- original rationale follows ----
 *
 * fidfloor.c — does EMERGENT compartment selection lower the fidelity floor? (rung 1+4 -> 2)
 *
 * THE PARADOX BEING TESTED. polymer.c supplied no copier: the only reproduction physics is
 * template-directed synthesis with per-base fidelity f. It found heritable FUNCTION emerges only
 * above f ~ 0.98, while un-catalyzed templating supplies ~0.85 — and the replicase that would close
 * the gap is itself a functional sequence needing that fidelity FIRST. Eigen's bootstrap paradox,
 * reproduced from below.
 *
 * THE REASON IT MIGHT NOT BE A WALL. That threshold was measured in a SINGLE-LEVEL population. Error
 * thresholds are known to relax under multilevel selection — that is the stochastic corrector's whole
 * point. And chemoton.c now gives compartment-level selection that EMERGES (growth coupled to the
 * chemistry's own output, no fitness-proportional sampling anywhere) rather than being decreed. So:
 * run polymer.c's dynamics inside chemoton.c's compartments and ask whether the floor drops from
 * 0.98 toward 0.85. If it does, the paradox dissolves in the other order — you do not get accurate
 * copying and then compartments, you get compartments and THEN accurate copying becomes affordable.
 *
 * WHAT IS COPIED FROM WHERE, VERBATIM:
 *  from polymer.c — binary strands, canonical form min(s,~s) so a strand and its complement are one
 *    functional unit, fitness exp(SS*matches) against a random target motif, and replicate() as
 *    template-directed synthesis complementing each bit with probability f. Nothing rewards a
 *    particular replicator identity, only the world-supplied function.
 *  from chemoton.c — conserved rationed material, uptake driving growth, division at a size
 *    threshold with random assortment, per-strand death returning material. No group-selection step.
 *
 * ARMS (all three share the SAME compartment machinery; only uptake differs):
 *  1 SELECT  uptake proportional to the compartment's mean functional match — the emergent
 *            between-compartment selection.
 *  2 BLIND   uptake proportional to compartment SIZE, blind to function. Compartments exist, divide
 *            and die identically; only the selection is removed. THIS IS THE CONTROL — it separates
 *            compartment SELECTION from mere compartmentalization.
 *  3 MIXED   one compartment holding everything: the polymer.c single-level baseline.
 *
 * PRE-REGISTERED:
 *  F1 MIXED reproduces polymer.c — function emerges only at f ≈ 0.98, not at 0.85–0.90.
 *  F2 SELECT lowers the floor — function emerges at a fidelity where MIXED fails.
 *  F3 CONTROL: BLIND does NOT lower it. If it does, compartmentalization alone is responsible and
 *     the emergent selection is doing nothing.
 *  F4 TEST OF THE TEST: compartments must actually divide and turn over, or there was no
 *     compartment-level reproduction to select on.
 *
 * MATCHED-CONDITION DISCIPLINE (today's lesson: one knob must not move two variables). Turning
 * uptake-selection on also changes population size and total replication count. Both are REPORTED
 * per arm so the reader can see whether the arms are comparable; if they diverge sharply, any floor
 * shift is confounded by sheer number of replication events and must be read as such.
 *
 * build:  cc -O2 -Wall -std=c11 -o fidfloor fidfloor.c -lm
 */
#include <math.h>
#include <stdio.h>
#include <stdint.h>

#define L        16            /* polymer.c's shorter case, where it found f* = 0.98 */
#define SS       0.30          /* selection strength, verbatim from polymer.c */
#define D        0.30          /* per-step degradation, verbatim */
#define STEPS    140           /* verbatim */
#define MAXCOMP  2000
#define MAXSZ    24
#define COSTUNIT 1.0
#define TOTALMAT 3000.0
#define GROW     0.45          /* per-unit material inflow; MUST exceed D or every arm decays */

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 uint64_t MASK, FTAR_C;
static uint64_t canon(uint64_t s){ uint64_t c=(~s)&MASK; return s<c?s:c; }
static int matchC(uint64_t s){ return L - __builtin_popcountll(canon(s) ^ FTAR_C); }
static uint64_t replicate(uint64_t t,double f){
	uint64_t c=0;
	for(int i=0;i<L;i++){ int b=(int)((t>>i)&1); int nb=(urand()<f)?(1-b):b; if(nb) c|=(1ULL<<i); }
	return c;
}

typedef struct { uint64_t s[MAXSZ]; int n; double mat; } Comp;
static Comp C[MAXCOMP];
static int ncomp;
static double pool;
static double demand[MAXCOMP];

/* mode 0 = SELECT (uptake ~ mean match), 1 = BLIND (uptake ~ size), 2 = MIXED (one big compartment) */
static double run(int mode,double f,int divsz,double *start,double *ndiv,double *pop,double *nrep)
{
	MASK=(1ULL<<L)-1;
	uint64_t ft=0; for(int i=0;i<L;i++) if(urand()<0.5) ft|=(1ULL<<i); FTAR_C=canon(ft);

	int seed_n = (mode==2)? MAXSZ : (divsz/2>2?divsz/2:2);
	ncomp = (mode==2)? 120 : 600;
	for(int c=0;c<ncomp;c++){
		C[c].n=seed_n; C[c].mat=0;
		for(int k=0;k<seed_n;k++){ uint64_t v=0; for(int i=0;i<L;i++) if(urand()<0.5) v|=(1ULL<<i); C[c].s[k]=v; }
	}
	{ double m=0; long t=0; for(int c=0;c<ncomp;c++) for(int k=0;k<C[c].n;k++){ m+=matchC(C[c].s[k]); t++; } *start=m/((double)t*L); }
	{ long u=0; for(int c=0;c<ncomp;c++) u+=C[c].n; pool=TOTALMAT-u*COSTUNIT; }

	double totdiv=0, totrep=0, popsum=0;

	for(int g=0;g<STEPS;g++){
		/* degradation, verbatim from polymer.c; material returns to the pool */
		for(int c=0;c<ncomp;c++)
			for(int k=0;k<C[c].n;){
				if(urand()<D){ C[c].s[k]=C[c].s[C[c].n-1]; C[c].n--; pool+=COSTUNIT; }
				else k++;
			}

		/* uptake. TOTAL material moved is identical across arms (GROW per surviving unit, capped by
		   the pool); only its ALLOCATION differs. This removes the population/replication confound
		   by construction rather than checking for it afterwards. */
		long units=0; for(int c=0;c<ncomp;c++) units+=C[c].n;
		double budget = GROW*(double)units; if(budget>pool) budget=pool;
		double Dm=0;
		for(int c=0;c<ncomp;c++){
			double out;
			if(mode==0){ double m=0; for(int k=0;k<C[c].n;k++) m+=matchC(C[c].s[k]); out = C[c].n? (m/(C[c].n*(double)L))*C[c].n : 0; }
			else out = C[c].n;              /* BLIND and MIXED: allocate by size, blind to function */
			demand[c]=out; Dm+=demand[c];
		}
		if(Dm>0) for(int c=0;c<ncomp;c++){ double t=budget*demand[c]/Dm; C[c].mat+=t; pool-=t; }

		/* growth: material buys template-directed replication, parent by fitness WITHIN compartment */
		for(int c=0;c<ncomp;c++){
			int cap = (mode==2)? MAXSZ : MAXSZ;
			while(C[c].mat>=COSTUNIT && C[c].n<cap && C[c].n>0){
				double tot=0,cum[MAXSZ];
				for(int k=0;k<C[c].n;k++){ tot+=exp(SS*matchC(C[c].s[k])); 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].s[C[c].n]=replicate(C[c].s[lo],f); C[c].n++; C[c].mat-=COSTUNIT; totrep++;
			}
		}

		/* division + compartment death (skipped in MIXED, which is one big well-mixed pool) */
		if(mode!=2){
			int base=ncomp;
			for(int c=0;c<base;c++){
				if(C[c].n>=divsz && ncomp<MAXCOMP){
					Comp *p=&C[c],*q=&C[ncomp];
					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->s[q->n++]=p->s[k]; else p->s[w++]=p->s[k]; }
					p->n=w; ncomp++; totdiv++;
				}
			}
		}
		for(int c=0;c<ncomp;){ if(C[c].n<=0){ pool+=C[c].mat; C[c]=C[ncomp-1]; ncomp--; } else c++; }
		if(ncomp<=0) break;

		/* MIXED: shuffle strands across all compartments so the population is genuinely ONE
		   well-mixed pool — the polymer.c single-level baseline. */
		if(mode==2 && ncomp>1){
			for(int rep=0;rep<4;rep++)
			for(int c=0;c<ncomp;c++){
				if(C[c].n<=0) continue;
				int d=(int)(xs()%(unsigned)ncomp); if(d==c||C[d].n<=0) continue;
				int a=(int)(xs()%(unsigned)C[c].n), b=(int)(xs()%(unsigned)C[d].n);
				uint64_t t=C[c].s[a]; C[c].s[a]=C[d].s[b]; C[d].s[b]=t;
			}
		}
		{ long u=0; for(int c=0;c<ncomp;c++) u+=C[c].n; popsum+=u; }
	}

	double m=0; long t=0;
	for(int c=0;c<ncomp;c++) for(int k=0;k<C[c].n;k++){ m+=matchC(C[c].s[k]); t++; }
	*ndiv=totdiv/STEPS; *pop=popsum/STEPS; *nrep=totrep;
	return t? m/((double)t*L) : 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("=== DIVISION-SIZE SWEEP: is there a size both small enough to CORRECT and large\n");
	printf("    enough to REMEMBER? ===\n");
	printf("  At divsz 8, compartments made things WORSE (MIXED 0.905 > SELECT 0.817 > BLIND 0.632 at\n");
	printf("  f=0.98): units of ~4-8 strands lose sequences to drift faster than between-compartment\n");
	printf("  selection spreads them. But ribozyme.c/chemoton.c need SMALL compartments for the\n");
	printf("  corrector. Opposite demands on one parameter. Sweeping it settles whether any size wins.\n");
	printf("  NOTE: as divsz -> MAXSZ=%d, SELECT converges on MIXED trivially (one big compartment IS\n",MAXSZ);
	printf("  well-mixed). The question is whether SELECT ever EXCEEDS MIXED, not whether it approaches.\n\n");

	double fs[]={0.90,0.95,0.98};
	int szs[]={4,6,8,12,16,24};
	int nF=3, nZ=6;

	for(int i=0;i<nF;i++){
		/* MIXED does not divide, so it is one constant reference per fidelity */
		double mx=0,mp=0,mr=0;
		for(int s=0;s<NS;s++){ double s1,dd,pp,rr; RNG=seeds[s]; mx+=run(2,fs[i],8,&s1,&dd,&pp,&rr); mp+=pp; mr+=rr; }
		mx/=NS; mp/=NS; mr/=NS;
		printf("  --- f = %.2f   (MIXED reference %.3f, pop %.0f, reps %.0f) ---\n",fs[i],mx,mp,mr);
		printf("   divsz | BLIND | SELECT | SELECT-MIXED | mean sz | reps(SEL)\n");
		printf("   ------+-------+--------+--------------+---------+----------\n");
		double best=-9; int bestz=-1;
		for(int z=0;z<nZ;z++){
			double b=0,a=0,ms=0,rr2=0;
			for(int s=0;s<NS;s++){
				double s1,dd,pp,rr;
				RNG=seeds[s]; b+=run(1,fs[i],szs[z],&s1,&dd,&pp,&rr);
				RNG=seeds[s]; a+=run(0,fs[i],szs[z],&s1,&dd,&pp,&rr); ms+=pp; rr2+=rr;
			}
			b/=NS;a/=NS;ms/=NS;rr2/=NS;
			double d=a-mx;
			printf("    %2d   | %.3f | %.3f  |    %+.3f    |  %5.0f  |  %6.0f\n",szs[z],b,a,d,ms,rr2);
			if(d>best){ best=d; bestz=szs[z]; }
		}
		printf("      best SELECT-MIXED %+.3f at divsz %d  %s\n\n", best, bestz,
			best>0.02?"<-- SELECT WINS somewhere":"(compartments never beat well-mixed)");
	}

	printf("  READ: a positive SELECT-MIXED at some intermediate divsz would mean an optimum exists —\n");
	printf("  compartments small enough to correct AND large enough to remember. If the column is\n");
	printf("  negative everywhere and merely rises toward 0 as divsz grows, compartmentalization is a\n");
	printf("  pure COST for sequence maintenance at every granularity, and multilevel selection cannot\n");
	printf("  lower Eigen's floor by any choice of compartment size.\n");
	return 0;
}