OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / basal-agent / coupling.c

290 lines · c ref:
/* coupling.c — can a WIRING rule privatize a public good, with NO container anywhere?
 *
 * THE QUESTION. Across 14 experiments the arc's wall was always the same: catalysis is a public
 * good, parasites free-ride, and the ONLY thing that ever rescued it was a SUPPLIED compartment
 * (ribozyme.c ignites at compartment size 4-8, fails well-mixed). Every attempt to make the
 * boundary emerge for free failed -- spatial locality (bridges.c arm A), cis-acting (arm B),
 * cohesion from complementarity (coemerge.c), metabolic/energetic/coded integration
 * (integrated.c). All of those are CONTAINERS: put the chemistry in a bag, and the parasites end
 * up in the bag too.
 *
 * The untested axis is whether the boundary has to be a container AT ALL. In Levin's systems what
 * makes a pile of cells one individual is not a wall but gap-junctional COUPLING -- the individual
 * is a wiring pattern, and you can move the boundary by changing the wiring without touching a
 * membrane. So: can a set of molecules become one individual purely by whom they exchange with?
 *
 * WHAT IS SUPPLIED vs WHAT MUST EMERGE (the razor -- issue #6, "never encode the answer"):
 *  SUPPLIED: the OPTION to condition. Each unit carries a heritable trait k in [0,1] = how much it
 *    reallocates its own junction conductances toward partners that send flow back. k=0 is the OLD
 *    PUBLIC GOOD (give to everyone regardless). k=1 is full reciprocity. This is the same standard
 *    rung34.c met: we supply the law-space, selection decides. NOTHING rewards high k directly.
 *  MUST EMERGE: exclusion of parasites. There is NO rule anywhere that says "catalysts only wire to
 *    catalysts". A parasite can form junctions freely and free-ride. A unit cannot see anyone's
 *    type -- it observes only how much flow arrived down each of its own junctions.
 *  ABSENT ENTIRELY: containers, compartments, space, assortment, group selection. Replication is
 *    global Wright-Fisher over the whole population.
 *
 * MECHANISM. Unit i owns LINKS junctions; it controls the conductance w of each. Catalysis flows
 * BOTH WAYS through a junction, scaled by each end's fold score phi: i sends (phi_i/PHIMAX)*w and
 * receives (phi_j/PHIMAX)*w. So cutting a junction to a freeloader costs i almost nothing -- the
 * freeloader was not sending anything back. That asymmetry is the whole hypothesis. Received flow
 * sets a unit's PRIVATE copying fidelity, so fidelity stops being a public good and becomes a
 * property of who you are wired to.
 *
 * Constants F0/F1/BETA/L/PHIMAX/TOTAL/GEN and phi()/replicate() are IDENTICAL to ribozyme.c so the
 * result is directly comparable to a known failure: ribozyme well-mixed goes 5.99 -> 5.50.
 * (Note replicate() flips each bit with probability f, so f->1 yields the COMPLEMENT; phi is
 * complement-invariant, so high f does preserve fold score. Copied verbatim, not "fixed".)
 *
 * PRE-REGISTERED (written before the first run):
 *  P1 k frozen at 0 (unconditional giving) -> phi FALLS. Reproduces the public-goods tragedy.
 *  P2 k frozen at 1 (full reciprocity)     -> phi RISES with no container. This is the real test.
 *  P3 k free to evolve                     -> k climbs from its low start AND phi rises.
 *  P4 CONTROL, the one that can kill P2/P3: a SHUFFLED arm where each unit's conductance updates
 *     are applied to a random permutation of its junctions -- same arithmetic, same budget, same
 *     concentration of w, but decoupled from who actually sent flow. If phi rises here too, then
 *     the effect is about concentrating conductance, NOT about reciprocity, and P2/P3 mean nothing.
 *
 * Honest note: if this works it is reciprocity / partner choice, which social evolution knows well
 * (cf. Levin S., Nat Ecol Evol 2019, cited as ref 74 by the Phi_r paper). The claim would not be a
 * new mechanism; it would be that the boundary can be a WIRING RULE rather than a WALL.
 * Any prediction may fail. A null is a result. Do not tune to force a positive.
 *
 * build:  cc -O2 -Wall -std=c11 -o coupling coupling.c -lm
 */
#include <math.h>
#include <stdio.h>
#include <stdint.h>

/* ---------- identical to ribozyme.c ---------- */
#define L      24
#define PHIMAX (L/2)
#define TOTAL  4800
#define GEN    200
#define F0     0.85
#define F1     0.999
#define BETA   2.0

/* ---------- coupling-specific ---------- */
#define LINKS  4
#define TROUND 8
#define MUTK   0.03
#define RSCALE 1.0

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 double randn(void){ double u=urand(),v=urand(); if(u<1e-300)u=1e-300; return sqrt(-2*log(u))*cos(6.283185307*v); }

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

static uint32_t seq[TOTAL], nseq[TOTAL];
static double   kk[TOTAL], nkk[TOTAL];
static int      lnk[TOTAL][LINKS];
static double   ww[TOTAL][LINKS], inflow[TOTAL][LINKS];
static double   recv[TOTAL], fid[TOTAL], cumw[TOTAL];
static int      phic[TOTAL];
static int      lnk_o[TOTAL][LINKS]; static double ww_o[TOTAL][LINKS];
static int      par[TOTAL], ocount[TOTAL], ostart[TOTAL], olist[TOTAL];

/* mode: 0 = k frozen at kfix; 1 = k evolvable. shuffled: apply conductance updates to a permuted
   set of the unit's own junctions (same arithmetic, decoupled from who actually sent flow). */
/* persist: 0 = fresh random wiring every generation (original)
             1 = offspring INHERIT the parent's junctions, remapped to the partners' own offspring
             2 = offspring inherit a RANDOM unit's junctions (control: persistence without lineage) */
static double run(int evolve_k,double kfix,int shuffled,int persist,double *k_end,double *f_end,double *phi_start)
{
	for(int i=0;i<TOTAL;i++){
		uint32_t s=0; for(int b=0;b<L;b++) if(urand()<0.5) s|=(1u<<b);
		seq[i]=s; kk[i]= evolve_k? 0.10 : kfix;      /* evolvable arm starts LOW, like rung34 */
	}
	{ double sp=0; for(int i=0;i<TOTAL;i++) sp+=phi(seq[i]); *phi_start=sp/TOTAL; }

	if(persist){                                  /* seed the network once; it then persists */
		for(int i=0;i<TOTAL;i++) for(int l=0;l<LINKS;l++){
			int j; do { j=(int)(xs()%TOTAL); } while(j==i);
			lnk[i][l]=j; ww[i][l]=1.0/LINKS;
		}
	}

	for(int g=0;g<GEN;g++){
		for(int i=0;i<TOTAL;i++) phic[i]=phi(seq[i]);

		if(!persist){   /* fresh random wiring each generation: no persistent structure supplied */
			for(int i=0;i<TOTAL;i++){
				for(int l=0;l<LINKS;l++){
					int j; do { j=(int)(xs()%TOTAL); } while(j==i);
					lnk[i][l]=j; ww[i][l]=1.0/LINKS;
				}
			}
		}
		for(int i=0;i<TOTAL;i++) recv[i]=0.0;

		for(int r=0;r<TROUND;r++){
			for(int i=0;i<TOTAL;i++) for(int l=0;l<LINKS;l++) inflow[i][l]=0.0;
			/* bidirectional flow through each junction, scaled by each end's phi */
			for(int i=0;i<TOTAL;i++){
				double gi=(double)phic[i]/PHIMAX;
				for(int l=0;l<LINKS;l++){
					int j=lnk[i][l]; double c=ww[i][l];
					double i2j = gi*c;
					double j2i = ((double)phic[j]/PHIMAX)*c;
					recv[j]+=i2j; recv[i]+=j2i;
					inflow[i][l]=j2i;              /* what i observes: flow down its own junction l */
				}
			}
			/* conductance update: reallocate toward junctions that delivered, in proportion to k */
			for(int i=0;i<TOTAL;i++){
				double tot=0; for(int l=0;l<LINKS;l++) tot+=inflow[i][l];
				if(tot<=0) continue;
				double k=kk[i];
				double share[LINKS];
				for(int l=0;l<LINKS;l++) share[l]=inflow[i][l]/tot;
				if(shuffled){                       /* P4: same update, wrong addresses */
					for(int l=LINKS-1;l>0;l--){ int m=(int)(xs()%(unsigned long long)(l+1)); double t=share[l]; share[l]=share[m]; share[m]=t; }
				}
				double s=0;
				for(int l=0;l<LINKS;l++){ ww[i][l]=(1.0-k)*ww[i][l]+k*share[l]; s+=ww[i][l]; }
				if(s>0) for(int l=0;l<LINKS;l++) ww[i][l]/=s;   /* fixed giving budget */
			}
		}

		/* private fidelity from received flow */
		for(int i=0;i<TOTAL;i++){
			double R=recv[i]/TROUND/RSCALE; if(R<0)R=0; if(R>1)R=1;
			fid[i]=F0+(F1-F0)*R;
		}

		/* global Wright-Fisher. NO compartments, NO group selection. Parasites replicate faster. */
		double tot=0;
		for(int i=0;i<TOTAL;i++){ tot+=1.0+BETA*(1.0-(double)phic[i]/PHIMAX); cumw[i]=tot; }
		for(int i=0;i<TOTAL;i++){
			double r=urand()*tot; int lo=0,hi=TOTAL-1;
			while(lo<hi){ int mid=(lo+hi)/2; if(cumw[mid]<r) lo=mid+1; else hi=mid; }
			par[i]=lo;
			nseq[i]=replicate(seq[lo],fid[lo]);
			double k2 = evolve_k ? kk[lo]+MUTK*randn() : kk[lo];
			if(k2<0)k2=0;
			if(k2>1)k2=1;
			nkk[i]=k2;
		}
		for(int i=0;i<TOTAL;i++){ seq[i]=nseq[i]; kk[i]=nkk[i]; }

		if(persist){
			/* A junction persists by pointing at a DESCENDANT of the old partner. If that partner
			   left no descendants the junction is dead and is replaced by a random new one. */
			for(int i=0;i<TOTAL;i++) for(int l=0;l<LINKS;l++){ lnk_o[i][l]=lnk[i][l]; ww_o[i][l]=ww[i][l]; }
			for(int q=0;q<TOTAL;q++) ocount[q]=0;
			for(int i=0;i<TOTAL;i++) ocount[par[i]]++;
			int acc=0; for(int q=0;q<TOTAL;q++){ ostart[q]=acc; acc+=ocount[q]; }
			{ static int fill[TOTAL]; for(int q=0;q<TOTAL;q++) fill[q]=0;
			  for(int i=0;i<TOTAL;i++){ int q=par[i]; olist[ostart[q]+fill[q]]=i; fill[q]++; } }
			for(int i=0;i<TOTAL;i++){
				int src = (persist==2) ? (int)(xs()%TOTAL) : par[i];   /* mode 2: lineage decoupled */
				double s2=0;
				for(int l=0;l<LINKS;l++){
					int oldj=lnk_o[src][l];
					if(ocount[oldj]>0){
						int pick=olist[ostart[oldj]+(int)(xs()%(unsigned long long)ocount[oldj])];
						lnk[i][l] = (pick==i)? (int)(xs()%TOTAL) : pick;
						ww[i][l]  = ww_o[src][l];
					} else {
						int j; do { j=(int)(xs()%TOTAL); } while(j==i);
						lnk[i][l]=j; ww[i][l]=1.0/LINKS;      /* dead partner -> new random junction */
					}
					s2+=ww[i][l];
				}
				if(s2>0) for(int l=0;l<LINKS;l++) ww[i][l]/=s2;
			}
		}
	}

	double sp=0,sk=0,sf=0;
	for(int i=0;i<TOTAL;i++){ sp+=phi(seq[i]); sk+=kk[i]; sf+=fid[i]; }
	*k_end=sk/TOTAL; *f_end=sf/TOTAL;
	return sp/TOTAL;
}

int main(void)
{
	unsigned long long seeds[10];
	seeds[0]=0x9e3779b97f4a7c15ULL; seeds[1]=0xd1b54a32d192ed03ULL; seeds[2]=0x2545f4914f6cdd1dULL;
	for(int i=3;i<10;i++) seeds[i]=0x9e3779b97f4a7c15ULL ^ (0x100000001b3ULL*(unsigned long long)(i+1));
	int NS=10;

	printf("=== COUPLING: can a wiring rule privatize catalysis with NO container? ===\n");
	printf("  Same chemistry as ribozyme.c (phi = hairpin fold, parasites %.0fx faster, f0=%.2f f1=%.3f).\n",1+BETA,F0,F1);
	printf("  NO compartments, NO space, NO group selection. Global Wright-Fisher over %d units.\n",TOTAL);
	printf("  Each unit owns %d junctions; catalysis flows BOTH ways; trait k = how much it\n",LINKS);
	printf("  reallocates conductance toward junctions that delivered. %d exchange rounds/gen.\n",TROUND);
	printf("  REFERENCE: ribozyme.c well-mixed (no compartments) went 5.99 -> 5.50, parasites won.\n\n");

	printf("  arm                              | mean phi (start->end of %2d) | k end | fidelity end\n",PHIMAX);
	printf("  ---------------------------------+-----------------------------+-------+-------------\n");


	struct { const char*name; int ev; double kf; int sh; int pe; } arms[]={
		{"FRESH  k=0 (public good)      ", 0, 0.0, 0, 0},
		{"FRESH  k=1 (reciprocity)      ", 0, 1.0, 0, 0},
		{"FRESH  k free (start 0.10)    ", 1, 0.0, 0, 0},
		{"FRESH  k=1 SHUFFLED (ctrl)    ", 0, 1.0, 1, 0},
		{"PERSIST k=0 (public good)     ", 0, 0.0, 0, 1},
		{"PERSIST k=1 (reciprocity)     ", 0, 1.0, 0, 1},
		{"PERSIST k free (start 0.10)   ", 1, 0.0, 0, 1},
		{"PERSIST k=1 SHUFFLED (ctrl)   ", 0, 1.0, 1, 1},
		{"PERSIST-RANDOM k=1 (ctrl)     ", 0, 1.0, 0, 2},
	};
	int NA=(int)(sizeof(arms)/sizeof(arms[0]));
	double endphi[16], startphi[16], endk[16];
	for(int a=0;a<NA;a++){
		double S=0,E=0,K=0,F=0; double ev[16]; 
		for(int s=0;s<NS;s++){
			double ke,fe,ps; RNG=seeds[s];
			double e=run(arms[a].ev,arms[a].kf,arms[a].sh,arms[a].pe,&ke,&fe,&ps);
			ev[s]=e; E+=e; S+=ps; K+=ke; F+=fe;
		}
		S/=NS;E/=NS;K/=NS;F/=NS;
		double sd=0; for(int s=0;s<NS;s++) sd+=(ev[s]-E)*(ev[s]-E); sd=sqrt(sd/(NS-1));
		endphi[a]=E; startphi[a]=S; endk[a]=K;

		printf("  %s | %.2f -> %5.2f +- %.2f         | %.3f | %.4f\n",arms[a].name,S,E,sd,K,F);
	}
	printf("\n");

	printf("  FRESH   k=0 %.2f | k=1 %.2f | k free %.2f (k->%.3f) | shuffled %.2f\n",
		endphi[0],endphi[1],endphi[2],endk[2],endphi[3]);
	printf("  PERSIST k=0 %.2f | k=1 %.2f | k free %.2f (k->%.3f) | shuffled %.2f | from-RANDOM %.2f\n\n",
		endphi[4],endphi[5],endphi[6],endk[6],endphi[7],endphi[8]);

	int pub_fell   = endphi[0] < startphi[0]-0.3;
	int recip_gain = endphi[1] > endphi[0]+0.3;
	int shuf_flat  = endphi[3] < endphi[0]+0.3;
	int persist_helps = endphi[5] > endphi[1]+0.15;
	int lineage_matters = endphi[5] > endphi[8]+0.15;
	int any_rose   = endphi[5] > startphi[5]+0.5;

	printf("  P1 public good falls               : %s (%.2f -> %.2f)\n", pub_fell?"YES":"no", startphi[0], endphi[0]);
	printf("  P2 reciprocity beats public good   : %s (%.2f vs %.2f)\n", recip_gain?"YES":"no", endphi[1], endphi[0]);
	printf("  P3 shuffled control stays flat     : %s (%.2f vs %.2f)\n", shuf_flat?"YES — effect is reciprocity":"NO — confounded", endphi[3], endphi[0]);
	printf("  P5 persistence beats fresh wiring  : %s (%.2f vs %.2f)\n", persist_helps?"YES":"NO", endphi[5], endphi[1]);
	printf("  P7 lineage matters vs from-RANDOM  : %s (%.2f vs %.2f)\n", lineage_matters?"YES":"NO", endphi[5], endphi[8]);
	printf("  P6 phi RISES anywhere (a crossing) : %s\n\n", any_rose?"YES":"NO");

	if(!any_rose && !persist_helps && !lineage_matters)
		printf("  => NULL, AND THE PERSISTENCE HYPOTHESIS IS FALSIFIED. Making junctions persist does not\n"
		       "     help, and inheriting a RANDOM unit\'s junctions works just as well as inheriting your\n"
		       "     parent\'s -- so nothing about lineage consolidation is doing work. Reciprocity is real\n"
		       "     (P2/P3 hold) but it only slows the tragedy.\n"
		       "     STRUCTURAL READING: the network never PARTITIONS. Every unit keeps %d junctions into one\n"
		       "     giant component, so \"the individual\" is always the whole population and there is nothing\n"
		       "     to privatize. Also, conductance is renormalised to sum to 1: a unit can choose WHOM to\n"
		       "     give to but never HOW MUCH -- it always exports its full output. Catalysts can redirect,\n"
		       "     never withhold.\n", LINKS);
	else if(any_rose)
		printf("  => phi ROSE. Check the controls before claiming anything.\n");
	else
		printf("  => Mixed -- report the numbers as-is, claim nothing.\n");
	return 0;
}