/* 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;
}