| … | … | @@ -0,0 +1,287 @@ |
| 1 | /* partition.c — can a network that can CLOSE its junctions and BREAK APART privatize catalysis? |
| 2 | * |
| 3 | * WHERE THIS COMES FROM. coupling.c showed reciprocity is real and selected-for but insufficient, |
| 4 | * and its persistence hypothesis was falsified by its own lineage-decoupled control. Reading the |
| 5 | * failure structurally left two suspects, both topological rather than about the rule: |
| 6 | * (1) the network NEVER PARTITIONS -- every unit holds LINKS junctions into one giant component, |
| 7 | * so "the individual" is always the whole population and there is nothing to privatize; |
| 8 | * (2) conductance was renormalised to sum to 1, so a unit chose WHOM to give to but never HOW |
| 9 | * MUCH -- catalysts could redirect, never withhold. |
| 10 | * |
| 11 | * But re-reading coupling.c exposed a deeper flaw that subsumes both. There, each unit controlled |
| 12 | * its OWN junctions unilaterally, and flow in both directions passed through them. So a parasite |
| 13 | * could simply point a junction at a catalyst, hold it wide open, and harvest -- the catalyst had |
| 14 | * no say. Unilateral junctions cannot exclude anyone. That is not what a gap junction is: a real |
| 15 | * gap junction is BILATERAL, built from a hemichannel contributed by each cell, and EITHER side |
| 16 | * can close it. |
| 17 | * |
| 18 | * THE CHANGE. Junctions are undirected edges. Each endpoint holds its own willingness u, and the |
| 19 | * conductance is the PRODUCT g = u_a * u_b -- both must open. Willingness is not renormalised, so |
| 20 | * it can fall to zero and the edge effectively vanishes: the graph can fragment. A unit updates its |
| 21 | * willingness toward q = inflow/g, the amount received PER UNIT OF CHANNEL IT OPENED. That is a |
| 22 | * locally measurable ratio, not a type label -- nothing tells anyone who is a parasite. |
| 23 | * |
| 24 | * WHAT IS SUPPLIED vs WHAT MUST EMERGE (issue #6 razor): |
| 25 | * SUPPLIED: bilateral junctions (the actual physics of a gap junction), and the heritable OPTION |
| 26 | * to condition, trait k in [0,1]. k=0 = fixed willingness = the old public good. Nothing rewards |
| 27 | * high k directly. |
| 28 | * MUST EMERGE: exclusion, and any partitioning of the graph. No rule says catalysts wire to |
| 29 | * catalysts; no compartment, space, assortment or group selection exists; replication is global |
| 30 | * Wright-Fisher. If the population splits into a catalyst-rich component, it did that itself. |
| 31 | * |
| 32 | * Chemistry identical to ribozyme.c and coupling.c (phi = hairpin fold, parasites 3x faster, |
| 33 | * F0=0.85, F1=0.999, 4800 units, 200 gens) so results are directly comparable. |
| 34 | * Reference points: ribozyme well-mixed 5.99->5.50; coupling FRESH k=0 4.69, k=1 5.36. |
| 35 | * |
| 36 | * PRE-REGISTERED (written before the first run): |
| 37 | * Q1 k=0 (public good) -> phi FALLS. |
| 38 | * Q2 k=1 (bilateral gating) -> phi RISES above its start. This is the crossing, with no container. |
| 39 | * Q3 k free to evolve -> k climbs from 0.10 AND phi rises. |
| 40 | * Q4 CONTROL, shuffled: a unit's willingness updates are applied to a random permutation of its |
| 41 | * own edges -- same arithmetic, same closing, decoupled from who actually delivered. If phi |
| 42 | * rises here too, the effect is about closing junctions in general, not about gating, and |
| 43 | * Q2/Q3 mean nothing. |
| 44 | * Q5 THE TEST OF THE TEST: does the graph actually PARTITION? Report the largest connected |
| 45 | * component of the effective graph (g > EPS) and mean phi inside it vs outside. If the graph |
| 46 | * stays one giant component, the partitionable variant was never actually run, and a null |
| 47 | * says nothing -- exactly the mistake ribophi.c made with its "early" window. |
| 48 | * |
| 49 | * This is hypothesis #3; #1 (wiring alone) and #2 (persistence) both failed. If this fails too, |
| 50 | * that convergence is the result and the arc's conclusion stands: nothing privatizes a catalytic |
| 51 | * public good without a supplied unit of selection. Do not tune to force a positive. |
| 52 | * |
| 53 | * build: cc -O2 -Wall -std=c11 -o partition partition.c -lm |
| 54 | */ |
| 55 | #include <math.h> |
| 56 | #include <stdio.h> |
| 57 | #include <stdint.h> |
| 58 | |
| 59 | /* ---------- identical to ribozyme.c / coupling.c ---------- */ |
| 60 | #define L 24 |
| 61 | #define PHIMAX (L/2) |
| 62 | #define TOTAL 4800 |
| 63 | #define GEN 200 |
| 64 | #define F0 0.85 |
| 65 | #define F1 0.999 |
| 66 | #define BETA 2.0 |
| 67 | |
| 68 | /* ---------- network ---------- */ |
| 69 | #define LINKS 4 |
| 70 | #define NSTUB (TOTAL*LINKS) |
| 71 | #define NEMAX (NSTUB/2) |
| 72 | #define TROUND 8 |
| 73 | #define MUTK 0.03 |
| 74 | #define EPS 0.05 /* conductance above which an edge counts as present, for components */ |
| 75 | #define RSCALE 1.0 |
| 76 | |
| 77 | static unsigned long long RNG; |
| 78 | static unsigned long long xs(void){ RNG^=RNG<<13; RNG^=RNG>>7; RNG^=RNG<<17; return RNG; } |
| 79 | static double urand(void){ return (double)(xs()>>11)*(1.0/9007199254740992.0); } |
| 80 | 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); } |
| 81 | |
| 82 | 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; } |
| 83 | 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; } |
| 84 | |
| 85 | static uint32_t seq[TOTAL], nseq[TOTAL]; |
| 86 | static double kk[TOTAL], nkk[TOTAL]; |
| 87 | static double recv[TOTAL], fid[TOTAL], cumw[TOTAL]; |
| 88 | static int phic[TOTAL]; |
| 89 | |
| 90 | static int ea[NEMAX], eb[NEMAX]; /* undirected edges */ |
| 91 | static double ua[NEMAX], ub[NEMAX]; /* each endpoint's own willingness */ |
| 92 | static double qa[NEMAX], qb[NEMAX]; /* observed yield per unit channel */ |
| 93 | static int stub[NSTUB]; |
| 94 | static int inc[TOTAL][LINKS], ninc[TOTAL]; |
| 95 | static int uf[TOTAL], csz[TOTAL]; |
| 96 | |
| 97 | static int find(int x){ while(uf[x]!=x){ uf[x]=uf[uf[x]]; x=uf[x]; } return x; } |
| 98 | static void uni(int a,int b){ a=find(a); b=find(b); if(a!=b) uf[a]=b; } |
| 99 | |
| 100 | /* configuration-model random graph, degree <= LINKS */ |
| 101 | static int build_graph(void) |
| 102 | { |
| 103 | for(int i=0;i<TOTAL;i++){ ninc[i]=0; } |
| 104 | for(int i=0;i<TOTAL;i++) for(int l=0;l<LINKS;l++) stub[i*LINKS+l]=i; |
| 105 | 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; } |
| 106 | int ne=0; |
| 107 | for(int s=0;s+1<NSTUB;s+=2){ |
| 108 | int a=stub[s], b=stub[s+1]; |
| 109 | if(a==b) continue; |
| 110 | if(ninc[a]>=LINKS||ninc[b]>=LINKS) continue; |
| 111 | ea[ne]=a; eb[ne]=b; ua[ne]=0.5; ub[ne]=0.5; |
| 112 | inc[a][ninc[a]++]=ne; inc[b][ninc[b]++]=ne; |
| 113 | ne++; |
| 114 | } |
| 115 | return ne; |
| 116 | } |
| 117 | |
| 118 | /* returns final mean phi; reports k, fidelity, largest-component fraction, phi in/out of it */ |
| 119 | static double run(int evolve_k,double kfix,int shuffled,double cost, |
| 120 | double *k_end,double *f_end,double *phi_start, |
| 121 | double *lcc_frac,double *phi_in,double *phi_out) |
| 122 | { |
| 123 | for(int i=0;i<TOTAL;i++){ |
| 124 | uint32_t s=0; for(int b=0;b<L;b++) if(urand()<0.5) s|=(1u<<b); |
| 125 | seq[i]=s; kk[i]= evolve_k? 0.10 : kfix; |
| 126 | } |
| 127 | { double sp=0; for(int i=0;i<TOTAL;i++) sp+=phi(seq[i]); *phi_start=sp/TOTAL; } |
| 128 | |
| 129 | int ne=0; |
| 130 | for(int g=0;g<GEN;g++){ |
| 131 | for(int i=0;i<TOTAL;i++) phic[i]=phi(seq[i]); |
| 132 | ne=build_graph(); |
| 133 | for(int i=0;i<TOTAL;i++) recv[i]=0.0; |
| 134 | |
| 135 | double costg=cost; |
| 136 | for(int r=0;r<TROUND;r++){ |
| 137 | for(int e=0;e<ne;e++){ |
| 138 | int a=ea[e], b=eb[e]; |
| 139 | double gc=ua[e]*ub[e]; /* BILATERAL: both must open */ |
| 140 | double a2b=((double)phic[a]/PHIMAX)*gc; |
| 141 | double b2a=((double)phic[b]/PHIMAX)*gc; |
| 142 | recv[b]+=a2b; recv[a]+=b2a; |
| 143 | /* yield per unit of channel opened -- local, no type information */ |
| 144 | qa[e] = (gc>1e-12)? b2a/gc : 0.0; /* what a learns about b */ |
| 145 | qb[e] = (gc>1e-12)? a2b/gc : 0.0; /* what b learns about a */ |
| 146 | if(gc<=1e-12){ qa[e]=(double)phic[b]/PHIMAX*0.0; qb[e]=0.0; } |
| 147 | } |
| 148 | if(shuffled){ |
| 149 | /* Q4: permute the yields a unit sees across its OWN edges */ |
| 150 | for(int i=0;i<TOTAL;i++){ |
| 151 | int n=ninc[i]; if(n<2) continue; |
| 152 | double tmp[LINKS]; |
| 153 | for(int l=0;l<n;l++){ int e=inc[i][l]; tmp[l]= (ea[e]==i)? qa[e] : qb[e]; } |
| 154 | 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; } |
| 155 | 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]; } |
| 156 | } |
| 157 | } |
| 158 | for(int e=0;e<ne;e++){ |
| 159 | int a=ea[e], b=eb[e]; |
| 160 | double ka=kk[a], kb=kk[b]; |
| 161 | double ta=qa[e]-costg, tb=qb[e]-costg; /* net yield of keeping this one open */ |
| 162 | if(ta<0)ta=0; |
| 163 | if(tb<0)tb=0; |
| 164 | double na=(1.0-ka)*ua[e]+ka*ta; |
| 165 | double nb=(1.0-kb)*ub[e]+kb*tb; |
| 166 | if(na<0)na=0; |
| 167 | if(na>1)na=1; |
| 168 | if(nb<0)nb=0; |
| 169 | if(nb>1)nb=1; |
| 170 | ua[e]=na; ub[e]=nb; /* NOT renormalised: junctions can close */ |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | /* MAINTENANCE COST: holding a hemichannel open costs, so a junction yielding less than |
| 175 | the cost is net-negative and gets closed outright. The threshold EMERGES from cost; |
| 176 | it is not decreed, and cost is swept rather than picked. */ |
| 177 | for(int i=0;i<TOTAL;i++){ |
| 178 | double open=0; for(int l=0;l<ninc[i];l++){ int e=inc[i][l]; open += (ea[e]==i)? ua[e] : ub[e]; } |
| 179 | open = (ninc[i]>0)? open/ninc[i] : 0.0; |
| 180 | double R=recv[i]/TROUND/RSCALE - cost*open; |
| 181 | if(R<0)R=0; if(R>1)R=1; |
| 182 | fid[i]=F0+(F1-F0)*R; |
| 183 | } |
| 184 | |
| 185 | double tot=0; |
| 186 | for(int i=0;i<TOTAL;i++){ tot+=1.0+BETA*(1.0-(double)phic[i]/PHIMAX); cumw[i]=tot; } |
| 187 | for(int i=0;i<TOTAL;i++){ |
| 188 | double r=urand()*tot; int lo=0,hi=TOTAL-1; |
| 189 | while(lo<hi){ int mid=(lo+hi)/2; if(cumw[mid]<r) lo=mid+1; else hi=mid; } |
| 190 | nseq[i]=replicate(seq[lo],fid[lo]); |
| 191 | double k2 = evolve_k ? kk[lo]+MUTK*randn() : kk[lo]; |
| 192 | if(k2<0)k2=0; |
| 193 | if(k2>1)k2=1; |
| 194 | nkk[i]=k2; |
| 195 | } |
| 196 | for(int i=0;i<TOTAL;i++){ seq[i]=nseq[i]; kk[i]=nkk[i]; } |
| 197 | } |
| 198 | |
| 199 | /* Q5: did the effective graph actually partition? */ |
| 200 | for(int i=0;i<TOTAL;i++){ uf[i]=i; csz[i]=0; } |
| 201 | for(int e=0;e<ne;e++) if(ua[e]*ub[e] > EPS) uni(ea[e],eb[e]); |
| 202 | for(int i=0;i<TOTAL;i++) csz[find(i)]++; |
| 203 | int best=0,bi=0; |
| 204 | for(int i=0;i<TOTAL;i++) if(csz[i]>best){ best=csz[i]; bi=i; } |
| 205 | double sin_=0,sout=0; int nin=0,nout=0; |
| 206 | for(int i=0;i<TOTAL;i++){ |
| 207 | int p=phi(seq[i]); |
| 208 | if(find(i)==bi){ sin_+=p; nin++; } else { sout+=p; nout++; } |
| 209 | } |
| 210 | *lcc_frac=(double)best/TOTAL; |
| 211 | *phi_in = nin? sin_/nin : NAN; |
| 212 | *phi_out= nout? sout/nout : NAN; |
| 213 | |
| 214 | double sp=0,sk=0,sf=0; |
| 215 | for(int i=0;i<TOTAL;i++){ sp+=phi(seq[i]); sk+=kk[i]; sf+=fid[i]; } |
| 216 | *k_end=sk/TOTAL; *f_end=sf/TOTAL; |
| 217 | return sp/TOTAL; |
| 218 | } |
| 219 | |
| 220 | int main(void) |
| 221 | { |
| 222 | unsigned long long seeds[10]; |
| 223 | seeds[0]=0x9e3779b97f4a7c15ULL; seeds[1]=0xd1b54a32d192ed03ULL; seeds[2]=0x2545f4914f6cdd1dULL; |
| 224 | for(int i=3;i<10;i++) seeds[i]=0x9e3779b97f4a7c15ULL ^ (0x100000001b3ULL*(unsigned long long)(i+1)); |
| 225 | int NS=10; |
| 226 | |
| 227 | printf("=== PARTITION: bilateral junctions that can CLOSE — can the network break apart\n"); |
| 228 | printf(" and privatize catalysis with no container? ===\n"); |
| 229 | printf(" Junction conductance g = u_a * u_b (BOTH ends must open; either can close).\n"); |
| 230 | printf(" Willingness is NOT renormalised, so edges can vanish and the graph can fragment.\n"); |
| 231 | printf(" A unit updates toward yield-per-unit-channel-opened; no type information anywhere.\n"); |
| 232 | printf(" No compartments, no space, no assortment, no group selection. %d units, %d gens.\n",TOTAL,GEN); |
| 233 | printf(" REFERENCE: ribozyme well-mixed 5.99->5.50; coupling FRESH k=0 4.69, k=1 5.36.\n\n"); |
| 234 | |
| 235 | printf(" arm | mean phi (start->end of %2d) | k end | largest comp | phi in / out\n",PHIMAX); |
| 236 | printf(" -------------------------+-----------------------------+-------+--------------+--------------\n"); |
| 237 | |
| 238 | double costs[]={0.0,0.10,0.20,0.30,0.40,0.50}; |
| 239 | int NC=(int)(sizeof(costs)/sizeof(costs[0])); |
| 240 | struct { const char*name; int ev; double kf; int sh; } arms[]={ |
| 241 | {"k=0 public good ", 0, 0.0, 0}, |
| 242 | {"k=1 gating ", 0, 1.0, 0}, |
| 243 | {"k=1 SHUFFLED ", 0, 1.0, 1}, |
| 244 | {"k free (0.10) ", 1, 0.0, 0}, |
| 245 | }; |
| 246 | int NA=4; |
| 247 | double bestrise=-1e9; double bestcost=-1; int partitioned_anywhere=0; |
| 248 | |
| 249 | for(int c=0;c<NC;c++){ |
| 250 | printf(" --- maintenance cost = %.2f ---\n",costs[c]); |
| 251 | double e_k0=0,e_k1=0,e_sh=0; |
| 252 | for(int a=0;a<NA;a++){ |
| 253 | double S=0,E=0,K=0,F=0,Lc=0,Pi=0,Po=0; double ev[16]; |
| 254 | for(int s=0;s<NS;s++){ |
| 255 | double ke,fe,ps,lf,pi,po; RNG=seeds[s]; |
| 256 | double e=run(arms[a].ev,arms[a].kf,arms[a].sh,costs[c],&ke,&fe,&ps,&lf,&pi,&po); |
| 257 | ev[s]=e; E+=e; S+=ps; K+=ke; F+=fe; Lc+=lf; Pi+=pi; Po+= isnan(po)?pi:po; |
| 258 | } |
| 259 | S/=NS;E/=NS;K/=NS;F/=NS;Lc/=NS;Pi/=NS;Po/=NS; |
| 260 | double sd=0; for(int s=0;s<NS;s++) sd+=(ev[s]-E)*(ev[s]-E); sd=sqrt(sd/(NS-1)); |
| 261 | printf(" %s | %.2f -> %5.2f +- %.2f | k %.3f | largest comp %5.1f%% | phi in/out %.2f/%.2f\n", |
| 262 | arms[a].name,S,E,sd,K,100.0*Lc,Pi,Po); |
| 263 | if(a==0) e_k0=E; if(a==1){ e_k1=E; if(Lc<0.90) partitioned_anywhere=1; |
| 264 | if(E-S>bestrise){ bestrise=E-S; bestcost=costs[c]; } } |
| 265 | if(a==2) e_sh=E; |
| 266 | } |
| 267 | printf(" gating gain over public good %+.2f | over shuffled %+.2f\n\n", e_k1-e_k0, e_k1-e_sh); |
| 268 | } |
| 269 | |
| 270 | printf(" Q5 did the graph EVER partition (largest comp <90%%)? %s\n", partitioned_anywhere?"YES":"NO"); |
| 271 | printf(" best phi change from start, k=1 arm: %+.2f at cost %.2f\n\n", bestrise, bestcost); |
| 272 | if(!partitioned_anywhere) |
| 273 | printf(" => THE TEST STILL DID NOT RUN. Across the whole cost sweep the effective graph stayed\n" |
| 274 | " one giant component. Partitioning was never achieved, so nothing here speaks to\n" |
| 275 | " whether a partitioned network could privatize catalysis. Report as an invalid\n" |
| 276 | " experiment, NOT as a null.\n"); |
| 277 | else if(bestrise>0.5) |
| 278 | printf(" => phi ROSE above its random start somewhere in the sweep, in a partitioning regime.\n" |
| 279 | " Check the shuffled control at that cost before claiming a crossing.\n"); |
| 280 | else |
| 281 | printf(" => HONEST NULL, AND IT IS THE THIRD. The graph does partition somewhere in the sweep,\n" |
| 282 | " and catalysis is STILL not privatized: phi never rises above its random start.\n" |
| 283 | " Wiring, persistence and now a partitionable bilateral network with closing costs\n" |
| 284 | " all fail. That convergence is the result -- nothing privatizes a catalytic public\n" |
| 285 | " good without a supplied unit of selection.\n"); |
| 286 | return 0; |
| 287 | } |