OverviewFilesCommitsIssuesDocsReleasesPeers

basal-agent / replisome.c

211 lines · c ref:
/* replisome.c — the closed genotype/phenotype loop. Can it bootstrap?
 *
 * WHERE THIS COMES FROM. genome.c v1/v2 gave templates a fair test and they failed twice, but the
 * second failure exposed something I had been getting for free: TEMPLATE COPYING WAS FREE PHYSICS
 * (child = par, perfect, no cost). In reality copying a genome faithfully needs a REPLICASE, which
 * is itself a catalyst, which is itself specified by a template. That is the loop, and it is exactly
 * rung 1→2.
 *
 * Two flaws in genome.c are fixed here:
 *  (1) a picked template ALWAYS became a catalyst when reading was on, so reading was a dead end for
 *      the template lineage — it could be useful or persist, never both. Now a picked template is
 *      READ with probability PREAD and otherwise COPIED TRUE, exactly as a catalyst is copied true.
 *      Reading is a FUNCTION templates provide, not a route by which they propagate.
 *  (2) template fidelity was free. Now it is CONDITIONAL on the compartment holding a catalyst of
 *      the replicase type: with one, templates copy at MU*RLO; without, at MU*RHI.
 *
 * THE RAZOR. This is where encoding the answer is easiest, so, precisely:
 *  SUPPLIED as physics: "a type-REPTYPE catalyst improves template fidelity in its compartment" —
 *    a function, the same shape as ribozyme.c's supplied fold→catalysis map.
 *  NOT SUPPLIED: any reward for being or holding a replicase. REPTYPE is EXCLUDED FROM THE TARGET
 *    SET, so it contributes nothing to payoff directly — it is pure overhead unless the template
 *    loop pays for itself. It is not seeded, not protected, not favoured in replication. Non-folders
 *    still replicate faster, so the parasite advantage is intact and the replicase is a PUBLIC GOOD
 *    within its compartment, free-ridable exactly as catalysis was in ribozyme.c.
 *  MEASURED: whether lineages acquire and HOLD the replicase, and whether function is higher for it.
 *
 * THE CONTROL, built to today's matched-condition lesson. Turning the replicase effect on also
 * changes the average template mutation rate, which would confound any gain. So the control applies
 * the IDENTICAL two-rate structure conditioned on a COIN FLIP per compartment instead of on
 * replicase presence — same mutation-rate distribution, no type to select for. The realized
 * low-fidelity fraction is reported for both arms so the match is visible rather than assumed.
 *
 * PRE-REGISTERED:
 *  R1 replicase frequency RISES above the control's. Lineages hold a type that pays them nothing
 *     directly, because it protects the templates that carry everything else.
 *  R2 target repertoire is HIGHER with the replicase effect than with the coin-flip control.
 *  R3 TEST OF THE TEST: catalysts and templates coexist, and the two arms' low-fidelity fractions
 *     are comparable. If they diverge sharply the comparison is confounded by mutation load.
 *  R4 the advantage should GROW with base mu — the replicase matters most where templates decay fast.
 *
 * HONEST PRIOR: partial. The replicase is a public good inside its compartment, so free-riding should
 * bite exactly as it did in ribozyme.c — one level up. A null here is a real result.
 *
 * build:  cc -O2 -Wall -std=c11 -o replicase replisome.c -lm
 */
#include <math.h>
#include <stdio.h>
#include <stdint.h>

#define NFOLD  16
#define NPAIR  (NFOLD/2)
#define THETA  6
#define NTYPE  8
#define REPTYPE 0                  /* the replicase type — excluded from the target set */
#define NTARG  3                   /* target types, drawn from types 1..7 */
#define NCOMP  600
#define CSZ    8
#define GEN    200
#define BETA   2.0
#define PREAD  0.50
#define RLO    0.25                /* template mutation multiplier WITH a replicase present */
#define RHI    1.50                /* ... and without */


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 TARGET[NTYPE];
static int phi(uint32_t s){ int c=0; for(int i=0;i<NPAIR;i++){ int a=(s>>i)&1,b=(s>>(NFOLD-1-i))&1; if(a!=b)c++; } return c; }
static int isfold(uint32_t s){ return phi(s)>=THETA; }
static int typeof_(uint32_t s){ return (int)((s>>NFOLD)&7); }
static uint32_t mut(uint32_t s,double mu){ for(int i=0;i<NFOLD+3;i++) if(urand()<mu) s^=(1u<<i); return s; }

/* Each slot holds a PAIR of strands — a minimal physical complex, copied together (linkage).
   PRIVATE: a template's fidelity is high only if ITS OWN partner is a replicase.
   SHARED : a template's fidelity is high if ANY catalyst in the compartment is a replicase
            (this is replicase.c's behaviour, which gave a complete null).
   One knob: the SCALE at which the replicase's benefit is privatised. */
static uint32_t A[NCOMP][CSZ], B[NCOMP][CSZ], nA[NCOMP][CSZ], nB[NCOMP][CSZ];
static double cum[NCOMP];

static int payoff_of(int c){
	int seen[NTYPE]; for(int t=0;t<NTYPE;t++) seen[t]=0;
	int d=0;
	for(int k=0;k<CSZ;k++){
		if(isfold(A[c][k])){ int t=typeof_(A[c][k]); if(!seen[t]){ seen[t]=1; if(TARGET[t]) d++; } }
		if(isfold(B[c][k])){ int t=typeof_(B[c][k]); if(!seen[t]){ seen[t]=1; if(TARGET[t]) d++; } }
	}
	return d;
}
static int is_rep(uint32_t s){ return isfold(s) && typeof_(s)==REPTYPE; }
static int comp_has_rep(int c){ for(int k=0;k<CSZ;k++) if(is_rep(A[c][k])||is_rep(B[c][k])) return 1; return 0; }
static double rep_freq_of(int c){ int n=0; for(int k=0;k<CSZ;k++){ n+=is_rep(A[c][k]); n+=is_rep(B[c][k]); } return (double)n/(2*CSZ); }

/* mode 0 = PRIVATE (replisome scale), 1 = SHARED (compartment scale, = replicase.c) */
static void run(int mode,double mu,double *rep_freq,double *pay_out,double *lofrac)
{
	for(int t=0;t<NTYPE;t++) TARGET[t]=0;
	for(int got=0;got<NTARG;){ int t=1+(int)(xs()%(NTYPE-1)); if(!TARGET[t]){ TARGET[t]=1; got++; } }
	for(int c=0;c<NCOMP;c++) for(int k=0;k<CSZ;k++){
		uint32_t v=0,w=0;
		for(int i=0;i<NFOLD+3;i++){ if(urand()<0.5) v|=(1u<<i); if(urand()<0.5) w|=(1u<<i); }
		A[c][k]=v; B[c][k]=w;
	}
	double losum=0; long lon=0;

	for(int g=0;g<GEN;g++){
		for(int c=0;c<NCOMP;c++){
			int shared_rep = comp_has_rep(c);
			double tot=0,cw[CSZ];
			for(int k=0;k<CSZ;k++){
				double q = 2.0 + BETA*(1.0-(double)phi(A[c][k])/NPAIR) + BETA*(1.0-(double)phi(B[c][k])/NPAIR);
				tot+=q; cw[k]=tot;
			}
			int mach=-1; for(int k=0;k<CSZ;k++){ if(isfold(A[c][k])){mach=k;break;} if(isfold(B[c][k])){mach=k;break;} }
			for(int k=0;k<CSZ;k++){
				double r=urand()*tot; int lo=0,hi=CSZ-1;
				while(lo<hi){ int mid=(lo+hi)/2; if(cw[mid]<r) lo=mid+1; else hi=mid; }
				/* the PAIR is copied together — linkage, so a complex is inherited as a unit */
				int priv = is_rep(A[c][lo]) || is_rep(B[c][lo]);
				int lo_fid = (mode==0)? priv : shared_rep;
				losum+=lo_fid; lon++;
				double tmu = mu*(lo_fid?RLO:RHI);
				uint32_t src[2]={A[c][lo],B[c][lo]}, out[2];
				for(int h=0;h<2;h++){
					uint32_t par=src[h], child; double emu;
					if(!isfold(par)){
						if(mach>=0 && urand()<PREAD){
							uint32_t fold = isfold(A[c][mach])? A[c][mach] : B[c][mach];
							child=(fold&((1u<<NFOLD)-1))|((uint32_t)typeof_(par)<<NFOLD); emu=mu;
						} else { child=par; emu=tmu; }
					} else { child=par; emu=mu; }
					out[h]=mut(child,emu);
				}
				nA[c][k]=out[0]; nB[c][k]=out[1];
			}
		}
		for(int c=0;c<NCOMP;c++) for(int k=0;k<CSZ;k++){ A[c][k]=nA[c][k]; B[c][k]=nB[c][k]; }

		double tot=0;
		for(int c=0;c<NCOMP;c++){ tot += 0.05 + (double)payoff_of(c); cum[c]=tot; }
		for(int c=0;c<NCOMP;c++){
			double r=urand()*tot; int lo=0,hi=NCOMP-1;
			while(lo<hi){ int mid=(lo+hi)/2; if(cum[mid]<r) lo=mid+1; else hi=mid; }
			for(int k=0;k<CSZ;k++){ int q=(int)(xs()%CSZ); nA[c][k]=A[lo][q]; nB[c][k]=B[lo][q]; }
		}
		for(int c=0;c<NCOMP;c++) for(int k=0;k<CSZ;k++){ A[c][k]=nA[c][k]; B[c][k]=nB[c][k]; }
	}
	double rf=0,pay=0;
	for(int c=0;c<NCOMP;c++){ rf+=rep_freq_of(c); pay+=payoff_of(c); }
	*rep_freq=rf/NCOMP; *pay_out=pay/NCOMP; *lofrac=losum/(double)lon;
}

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("=== REPLISOME SCALE: does privatising the replicase to its own complex close the loop? ===\n");
	printf("  replicase.c gave a complete null: a replicase improves fidelity for EVERY template in its\n");
	printf("  compartment including free-riders', so within-compartment selection purges it. A boundary\n");
	printf("  can only privatise a good it can exclude non-contributors from, and everything inside a\n");
	printf("  compartment is inside it. So: shrink the scale.\n");
	printf("  Each slot is a PAIR copied together (linkage). PRIVATE = a pair gets high fidelity only if\n");
	printf("  ITS OWN partner is a replicase. SHARED = replicase.c's rule. One knob: the scale.\n");
	printf("  NOTE the confound direction: SHARED grants low-mutation more often at equal replicase\n");
	printf("  frequency, so SHARED enjoys LESS mutation load. If PRIVATE still wins, it wins against it.\n\n");

	printf("    mu   | PRIVATE rep-freq / payoff | SHARED rep-freq / payoff | lo-fid P / S\n");
	printf("   ------+---------------------------+--------------------------+-------------\n");
	double mus[]={0.005,0.01,0.02,0.04,0.08};
	int nM=5, w1=0, w2=0; double g0=0,g1=0;
	for(int i=0;i<nM;i++){
		double a1=0,a2=0,a3=0,b1=0,b2=0,b3=0;
		for(int s=0;s<NS;s++){
			double rf,pv,lf;
			RNG=seeds[s]; run(0,mus[i],&rf,&pv,&lf); a1+=rf; a2+=pv; a3+=lf;
			RNG=seeds[s]; run(1,mus[i],&rf,&pv,&lf); b1+=rf; b2+=pv; b3+=lf;
		}
		a1/=NS;a2/=NS;a3/=NS;b1/=NS;b2/=NS;b3/=NS;
		printf("   %.3f |      %.3f / %.2f        |     %.3f / %.2f       |  %.2f / %.2f\n",
			mus[i],a1,a2,b1,b2,a3,b3);
		if(a1>b1+0.01) w1++;
		if(a2>b2+0.05) w2++;
		if(i==0) g0=a1-b1;
		g1=a1-b1;
	}
	printf("\n   rep-freq = fraction of strands that are replicases; payoff = target types (max %d)\n\n",NTARG);
	printf("   P1 PRIVATE holds more replicase than SHARED : %d/%d\n",w1,nM);
	printf("   P2 PRIVATE payoff higher                    : %d/%d\n",w2,nM);
	printf("   P3 replicase-frequency gap vs mu            : %+.3f -> %+.3f\n\n",g0,g1);

	if(w1>=nM-1 && w2>=nM-1)
		printf("   => SCALE IS THE ANSWER. Privatising the replicase to its own complex — and copying that\n"
		       "      complex as a unit — lets lineages hold a catalyst that pays them nothing directly,\n"
		       "      and carry more function for it. The same good, the same cost, the same free-riders:\n"
		       "      only the SCOPE of the benefit changed, and that was the whole difference. It also\n"
		       "      wins against the confound, since SHARED enjoys the lower mutation load.\n");
	else if(w1>=nM-1)
		printf("   => REPLICASE HELD, FUNCTION NOT RAISED (%d/%d vs %d/%d). Privatisation retains the\n"
		       "      replicase but the loop does not yet pay off in function. Partial.\n",w1,nM,w2,nM);
	else
		printf("   => HONEST NULL. Shrinking the scale of privatisation does not rescue the replicase\n"
		       "      either. Three designs, three nulls: the genotype/phenotype loop does not bootstrap\n"
		       "      in this family of models, and the missing ingredient is still not identified.\n");
	return 0;
}