OverviewFilesCommitsIssuesDocsReleasesPeers

hypercore-kb / basal-agent / partition.c

272 lines · c ref:
/* partition.c — can a network that can CLOSE its junctions and BREAK APART privatize catalysis?
 *
 * WHERE THIS COMES FROM. coupling.c showed reciprocity is real and selected-for but insufficient,
 * and its persistence hypothesis was falsified by its own lineage-decoupled control. Reading the
 * failure structurally left two suspects, both topological rather than about the rule:
 *   (1) the network NEVER PARTITIONS -- every unit holds LINKS junctions into one giant component,
 *       so "the individual" is always the whole population and there is nothing to privatize;
 *   (2) conductance was renormalised to sum to 1, so a unit chose WHOM to give to but never HOW
 *       MUCH -- catalysts could redirect, never withhold.
 *
 * But re-reading coupling.c exposed a deeper flaw that subsumes both. There, each unit controlled
 * its OWN junctions unilaterally, and flow in both directions passed through them. So a parasite
 * could simply point a junction at a catalyst, hold it wide open, and harvest -- the catalyst had
 * no say. Unilateral junctions cannot exclude anyone. That is not what a gap junction is: a real
 * gap junction is BILATERAL, built from a hemichannel contributed by each cell, and EITHER side
 * can close it.
 *
 * THE CHANGE. Junctions are undirected edges. Each endpoint holds its own willingness u, and the
 * conductance is the PRODUCT g = u_a * u_b -- both must open. Willingness is not renormalised, so
 * it can fall to zero and the edge effectively vanishes: the graph can fragment. A unit updates its
 * willingness toward q = inflow/g, the amount received PER UNIT OF CHANNEL IT OPENED. That is a
 * locally measurable ratio, not a type label -- nothing tells anyone who is a parasite.
 *
 * WHAT IS SUPPLIED vs WHAT MUST EMERGE (issue #6 razor):
 *  SUPPLIED: bilateral junctions (the actual physics of a gap junction), and the heritable OPTION
 *    to condition, trait k in [0,1]. k=0 = fixed willingness = the old public good. Nothing rewards
 *    high k directly.
 *  MUST EMERGE: exclusion, and any partitioning of the graph. No rule says catalysts wire to
 *    catalysts; no compartment, space, assortment or group selection exists; replication is global
 *    Wright-Fisher. If the population splits into a catalyst-rich component, it did that itself.
 *
 * Chemistry identical to ribozyme.c and coupling.c (phi = hairpin fold, parasites 3x faster,
 * F0=0.85, F1=0.999, 4800 units, 200 gens) so results are directly comparable.
 * Reference points: ribozyme well-mixed 5.99->5.50; coupling FRESH k=0 4.69, k=1 5.36.
 *
 * PRE-REGISTERED (written before the first run):
 *  Q1 k=0 (public good)      -> phi FALLS.
 *  Q2 k=1 (bilateral gating) -> phi RISES above its start. This is the crossing, with no container.
 *  Q3 k free to evolve       -> k climbs from 0.10 AND phi rises.
 *  Q4 CONTROL, shuffled: a unit's willingness updates are applied to a random permutation of its
 *     own edges -- same arithmetic, same closing, decoupled from who actually delivered. If phi
 *     rises here too, the effect is about closing junctions in general, not about gating, and
 *     Q2/Q3 mean nothing.
 *  Q5 THE TEST OF THE TEST: does the graph actually PARTITION? Report the largest connected
 *     component of the effective graph (g > EPS) and mean phi inside it vs outside. If the graph
 *     stays one giant component, the partitionable variant was never actually run, and a null
 *     says nothing -- exactly the mistake ribophi.c made with its "early" window.
 *
 * This is hypothesis #3; #1 (wiring alone) and #2 (persistence) both failed. If this fails too,
 * that convergence is the result and the arc's conclusion stands: nothing privatizes a catalytic
 * public good without a supplied unit of selection. Do not tune to force a positive.
 *
 * build:  cc -O2 -Wall -std=c11 -o partition partition.c -lm
 */
#include <math.h>
#include <stdio.h>
#include <stdint.h>

/* ---------- identical to ribozyme.c / coupling.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

/* ---------- network ---------- */
#define LINKS  4
#define NSTUB  (TOTAL*LINKS)
#define NEMAX  (NSTUB/2)
#define TROUND 8
#define MUTK   0.03
#define EPS    0.05        /* conductance above which an edge counts as present, for components */
#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 double   recv[TOTAL], fid[TOTAL], cumw[TOTAL];
static int      phic[TOTAL];

static int      ea[NEMAX], eb[NEMAX];      /* undirected edges */
static unsigned char sev[NEMAX];           /* 1 = severed this generation (exogenous fragmentation) */
static double   ua[NEMAX], ub[NEMAX];      /* each endpoint's own willingness */
static double   qa[NEMAX], qb[NEMAX];      /* observed yield per unit channel */
static int      stub[NSTUB];
static int      inc[TOTAL][LINKS], ninc[TOTAL];
static int      uf[TOTAL], csz[TOTAL];

static int find(int x){ while(uf[x]!=x){ uf[x]=uf[uf[x]]; x=uf[x]; } return x; }
static void uni(int a,int b){ a=find(a); b=find(b); if(a!=b) uf[a]=b; }

/* configuration-model random graph, degree <= LINKS */
static int build_graph(void)
{
	for(int i=0;i<TOTAL;i++){ ninc[i]=0; }
	for(int i=0;i<TOTAL;i++) for(int l=0;l<LINKS;l++) stub[i*LINKS+l]=i;
	for(int i=NSTUB-1;i>0;i--){ int j=(int)(xs()%(unsigned long long)(i+1)); int t=stub[i]; stub[i]=stub[j]; stub[j]=t; }
	int ne=0;
	for(int s=0;s+1<NSTUB;s+=2){
		int a=stub[s], b=stub[s+1];
		if(a==b) continue;
		if(ninc[a]>=LINKS||ninc[b]>=LINKS) continue;
		ea[ne]=a; eb[ne]=b; ua[ne]=0.5; ub[ne]=0.5;
		inc[a][ninc[a]++]=ne; inc[b][ninc[b]++]=ne;
		ne++;
	}
	return ne;
}

/* returns final mean phi; reports k, fidelity, largest-component fraction, phi in/out of it */
static double run(int evolve_k,double kfix,int shuffled,double cost,double frag,
                  double *k_end,double *f_end,double *phi_start,
                  double *lcc_frac,double *phi_in,double *phi_out)
{
	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;
	}
	{ double sp=0; for(int i=0;i<TOTAL;i++) sp+=phi(seq[i]); *phi_start=sp/TOTAL; }

	int ne=0;
	for(int g=0;g<GEN;g++){
		for(int i=0;i<TOTAL;i++) phic[i]=phi(seq[i]);
		ne=build_graph();
		/* EXOGENOUS FRAGMENTATION: sever a random fraction of edges, independent of cost and of
		   anyone's willingness. This is the knob that varies partitioning WITHOUT varying cost. */
		for(int e=0;e<ne;e++) sev[e] = (urand()<frag)?1:0;
		for(int i=0;i<TOTAL;i++) recv[i]=0.0;

		double costg=cost;
		for(int r=0;r<TROUND;r++){
			for(int e=0;e<ne;e++){
				if(sev[e]){ qa[e]=0; qb[e]=0; continue; }
				int a=ea[e], b=eb[e];
				double gc=ua[e]*ub[e];                 /* BILATERAL: both must open */
				double a2b=((double)phic[a]/PHIMAX)*gc;
				double b2a=((double)phic[b]/PHIMAX)*gc;
				recv[b]+=a2b; recv[a]+=b2a;
				/* yield per unit of channel opened -- local, no type information */
				qa[e] = (gc>1e-12)? b2a/gc : 0.0;      /* what a learns about b */
				qb[e] = (gc>1e-12)? a2b/gc : 0.0;      /* what b learns about a */
				if(gc<=1e-12){ qa[e]=(double)phic[b]/PHIMAX*0.0; qb[e]=0.0; }
			}
			if(shuffled){
				/* Q4: permute the yields a unit sees across its OWN edges */
				for(int i=0;i<TOTAL;i++){
					int n=ninc[i]; if(n<2) continue;
					double tmp[LINKS];
					for(int l=0;l<n;l++){ int e=inc[i][l]; tmp[l]= (ea[e]==i)? qa[e] : qb[e]; }
					for(int l=n-1;l>0;l--){ int m=(int)(xs()%(unsigned long long)(l+1)); double t=tmp[l]; tmp[l]=tmp[m]; tmp[m]=t; }
					for(int l=0;l<n;l++){ int e=inc[i][l]; if(ea[e]==i) qa[e]=tmp[l]; else qb[e]=tmp[l]; }
				}
			}
			for(int e=0;e<ne;e++){
				int a=ea[e], b=eb[e];
				double ka=kk[a], kb=kk[b];
				double ta=qa[e]-costg, tb=qb[e]-costg;   /* net yield of keeping this one open */
				if(ta<0)ta=0;
				if(tb<0)tb=0;
				double na=(1.0-ka)*ua[e]+ka*ta;
				double nb=(1.0-kb)*ub[e]+kb*tb;
				if(na<0)na=0;
				if(na>1)na=1;
				if(nb<0)nb=0;
				if(nb>1)nb=1;
				ua[e]=na; ub[e]=nb;                    /* NOT renormalised: junctions can close */
			}
		}

		/* MAINTENANCE COST: holding a hemichannel open costs, so a junction yielding less than
		   the cost is net-negative and gets closed outright. The threshold EMERGES from cost;
		   it is not decreed, and cost is swept rather than picked. */
		for(int i=0;i<TOTAL;i++){
			double open=0; for(int l=0;l<ninc[i];l++){ int e=inc[i][l]; open += (ea[e]==i)? ua[e] : ub[e]; }
			open = (ninc[i]>0)? open/ninc[i] : 0.0;
			double R=recv[i]/TROUND/RSCALE - cost*open;
			if(R<0)R=0; if(R>1)R=1;
			fid[i]=F0+(F1-F0)*R;
		}

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

	/* Q5: did the effective graph actually partition? */
	for(int i=0;i<TOTAL;i++){ uf[i]=i; csz[i]=0; }
	for(int e=0;e<ne;e++) if(!sev[e] && ua[e]*ub[e] > EPS) uni(ea[e],eb[e]);
	for(int i=0;i<TOTAL;i++) csz[find(i)]++;
	int best=0,bi=0;
	for(int i=0;i<TOTAL;i++) if(csz[i]>best){ best=csz[i]; bi=i; }
	double sin_=0,sout=0; int nin=0,nout=0;
	for(int i=0;i<TOTAL;i++){
		int p=phi(seq[i]);
		if(find(i)==bi){ sin_+=p; nin++; } else { sout+=p; nout++; }
	}
	*lcc_frac=(double)best/TOTAL;
	*phi_in = nin? sin_/nin : NAN;
	*phi_out= nout? sout/nout : NAN;

	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[8];
	seeds[0]=0x9e3779b97f4a7c15ULL; seeds[1]=0xd1b54a32d192ed03ULL; seeds[2]=0x2545f4914f6cdd1dULL;
	for(int i=3;i<8;i++) seeds[i]=0x9e3779b97f4a7c15ULL ^ (0x100000001b3ULL*(unsigned long long)(i+1));
	int NS=8;

	printf("=== PARTITION CHECK AT MATCHED PARTITIONING ===\n");
	printf("  The original claim -- \"partitioning and gating are anti-correlated\" -- came from a sweep\n");
	printf("  where MAINTENANCE COST moved both variables at once. Partitioning was never varied\n");
	printf("  independently. Here cost is held FIXED and the graph is fragmented EXOGENOUSLY by\n");
	printf("  severing a random fraction of edges, which is blind to cost, willingness and quality.\n");
	printf("  If gating's advantage over the shuffled control falls as fragmentation rises at CONSTANT\n");
	printf("  cost, the causal claim holds. If it stays flat, the original correlation was cost.\n\n");

	double costs[]={0.00,0.10};
	double frags[]={0.00,0.20,0.40,0.60,0.80};
	int NC=2, NF=5;

	for(int c=0;c<NC;c++){
		printf("  --- maintenance cost held at %.2f ---\n",costs[c]);
		printf("  sever frac | k=1 gating | shuffled | largest comp | GAIN vs shuffled\n");
		printf("  -----------+------------+----------+--------------+-----------------\n");
		double g0=0,gl=0; int nrow=0;
		for(int f=0;f<NF;f++){
			double eg=0,es=0,lc=0;
			for(int s=0;s<NS;s++){
				double ke,fe,ps,lf,pi,po;
				RNG=seeds[s]; eg+=run(0,1.0,0,costs[c],frags[f],&ke,&fe,&ps,&lf,&pi,&po); lc+=lf;
				RNG=seeds[s]; es+=run(0,1.0,1,costs[c],frags[f],&ke,&fe,&ps,&lf,&pi,&po);
			}
			eg/=NS;es/=NS;lc/=NS;
			double gain=eg-es;
			printf("     %.2f    |   %5.2f    |  %5.2f   |    %5.1f%%    |      %+.2f\n",
				frags[f],eg,es,100.0*lc,gain);
			if(f==0) g0=gain;
			gl=gain; nrow++;
		}
		printf("      gain at 0%% severing %+.2f -> at 80%% severing %+.2f   (change %+.2f)\n\n", g0, gl, gl-g0);
	}

	printf("  READ: if GAIN collapses toward zero as severing rises while cost is constant, the\n");
	printf("  original causal claim is supported. If GAIN is roughly flat across the severing sweep,\n");
	printf("  fragmentation per se does NOT destroy gating and the original anti-correlation was\n");
	printf("  driven by maintenance cost -- the same confound that retracted Test B's T4.\n");
	return 0;
}