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