| … | … | @@ -0,0 +1,240 @@ |
| 1 | /* coupling.c — can a WIRING rule privatize a public good, with NO container anywhere? |
| 2 | * |
| 3 | * THE QUESTION. Across 14 experiments the arc's wall was always the same: catalysis is a public |
| 4 | * good, parasites free-ride, and the ONLY thing that ever rescued it was a SUPPLIED compartment |
| 5 | * (ribozyme.c ignites at compartment size 4-8, fails well-mixed). Every attempt to make the |
| 6 | * boundary emerge for free failed -- spatial locality (bridges.c arm A), cis-acting (arm B), |
| 7 | * cohesion from complementarity (coemerge.c), metabolic/energetic/coded integration |
| 8 | * (integrated.c). All of those are CONTAINERS: put the chemistry in a bag, and the parasites end |
| 9 | * up in the bag too. |
| 10 | * |
| 11 | * The untested axis is whether the boundary has to be a container AT ALL. In Levin's systems what |
| 12 | * makes a pile of cells one individual is not a wall but gap-junctional COUPLING -- the individual |
| 13 | * is a wiring pattern, and you can move the boundary by changing the wiring without touching a |
| 14 | * membrane. So: can a set of molecules become one individual purely by whom they exchange with? |
| 15 | * |
| 16 | * WHAT IS SUPPLIED vs WHAT MUST EMERGE (the razor -- issue #6, "never encode the answer"): |
| 17 | * SUPPLIED: the OPTION to condition. Each unit carries a heritable trait k in [0,1] = how much it |
| 18 | * reallocates its own junction conductances toward partners that send flow back. k=0 is the OLD |
| 19 | * PUBLIC GOOD (give to everyone regardless). k=1 is full reciprocity. This is the same standard |
| 20 | * rung34.c met: we supply the law-space, selection decides. NOTHING rewards high k directly. |
| 21 | * MUST EMERGE: exclusion of parasites. There is NO rule anywhere that says "catalysts only wire to |
| 22 | * catalysts". A parasite can form junctions freely and free-ride. A unit cannot see anyone's |
| 23 | * type -- it observes only how much flow arrived down each of its own junctions. |
| 24 | * ABSENT ENTIRELY: containers, compartments, space, assortment, group selection. Replication is |
| 25 | * global Wright-Fisher over the whole population. |
| 26 | * |
| 27 | * MECHANISM. Unit i owns LINKS junctions; it controls the conductance w of each. Catalysis flows |
| 28 | * BOTH WAYS through a junction, scaled by each end's fold score phi: i sends (phi_i/PHIMAX)*w and |
| 29 | * receives (phi_j/PHIMAX)*w. So cutting a junction to a freeloader costs i almost nothing -- the |
| 30 | * freeloader was not sending anything back. That asymmetry is the whole hypothesis. Received flow |
| 31 | * sets a unit's PRIVATE copying fidelity, so fidelity stops being a public good and becomes a |
| 32 | * property of who you are wired to. |
| 33 | * |
| 34 | * Constants F0/F1/BETA/L/PHIMAX/TOTAL/GEN and phi()/replicate() are IDENTICAL to ribozyme.c so the |
| 35 | * result is directly comparable to a known failure: ribozyme well-mixed goes 5.99 -> 5.50. |
| 36 | * (Note replicate() flips each bit with probability f, so f->1 yields the COMPLEMENT; phi is |
| 37 | * complement-invariant, so high f does preserve fold score. Copied verbatim, not "fixed".) |
| 38 | * |
| 39 | * PRE-REGISTERED (written before the first run): |
| 40 | * P1 k frozen at 0 (unconditional giving) -> phi FALLS. Reproduces the public-goods tragedy. |
| 41 | * P2 k frozen at 1 (full reciprocity) -> phi RISES with no container. This is the real test. |
| 42 | * P3 k free to evolve -> k climbs from its low start AND phi rises. |
| 43 | * P4 CONTROL, the one that can kill P2/P3: a SHUFFLED arm where each unit's conductance updates |
| 44 | * are applied to a random permutation of its junctions -- same arithmetic, same budget, same |
| 45 | * concentration of w, but decoupled from who actually sent flow. If phi rises here too, then |
| 46 | * the effect is about concentrating conductance, NOT about reciprocity, and P2/P3 mean nothing. |
| 47 | * |
| 48 | * Honest note: if this works it is reciprocity / partner choice, which social evolution knows well |
| 49 | * (cf. Levin S., Nat Ecol Evol 2019, cited as ref 74 by the Phi_r paper). The claim would not be a |
| 50 | * new mechanism; it would be that the boundary can be a WIRING RULE rather than a WALL. |
| 51 | * Any prediction may fail. A null is a result. Do not tune to force a positive. |
| 52 | * |
| 53 | * build: cc -O2 -Wall -std=c11 -o coupling coupling.c -lm |
| 54 | */ |
| 55 | #include <math.h> |
| 56 | #include <stdio.h> |
| 57 | #include <stdint.h> |
| 58 | |
| 59 | /* ---------- identical to ribozyme.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 | /* ---------- coupling-specific ---------- */ |
| 69 | #define LINKS 4 |
| 70 | #define TROUND 8 |
| 71 | #define MUTK 0.03 |
| 72 | #define RSCALE 1.0 |
| 73 | |
| 74 | static unsigned long long RNG; |
| 75 | static unsigned long long xs(void){ RNG^=RNG<<13; RNG^=RNG>>7; RNG^=RNG<<17; return RNG; } |
| 76 | static double urand(void){ return (double)(xs()>>11)*(1.0/9007199254740992.0); } |
| 77 | 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); } |
| 78 | |
| 79 | 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; } |
| 80 | 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; } |
| 81 | |
| 82 | static uint32_t seq[TOTAL], nseq[TOTAL]; |
| 83 | static double kk[TOTAL], nkk[TOTAL]; |
| 84 | static int lnk[TOTAL][LINKS]; |
| 85 | static double ww[TOTAL][LINKS], inflow[TOTAL][LINKS]; |
| 86 | static double recv[TOTAL], fid[TOTAL], cumw[TOTAL]; |
| 87 | static int phic[TOTAL]; |
| 88 | |
| 89 | /* mode: 0 = k frozen at kfix; 1 = k evolvable. shuffled: apply conductance updates to a permuted |
| 90 | set of the unit's own junctions (same arithmetic, decoupled from who actually sent flow). */ |
| 91 | static double run(int evolve_k,double kfix,int shuffled,double *k_end,double *f_end,double *phi_start) |
| 92 | { |
| 93 | for(int i=0;i<TOTAL;i++){ |
| 94 | uint32_t s=0; for(int b=0;b<L;b++) if(urand()<0.5) s|=(1u<<b); |
| 95 | seq[i]=s; kk[i]= evolve_k? 0.10 : kfix; /* evolvable arm starts LOW, like rung34 */ |
| 96 | } |
| 97 | { double sp=0; for(int i=0;i<TOTAL;i++) sp+=phi(seq[i]); *phi_start=sp/TOTAL; } |
| 98 | |
| 99 | for(int g=0;g<GEN;g++){ |
| 100 | for(int i=0;i<TOTAL;i++) phic[i]=phi(seq[i]); |
| 101 | |
| 102 | /* fresh random wiring each generation: no persistent social structure is supplied */ |
| 103 | for(int i=0;i<TOTAL;i++){ |
| 104 | for(int l=0;l<LINKS;l++){ |
| 105 | int j; do { j=(int)(xs()%TOTAL); } while(j==i); |
| 106 | lnk[i][l]=j; ww[i][l]=1.0/LINKS; |
| 107 | } |
| 108 | } |
| 109 | for(int i=0;i<TOTAL;i++) recv[i]=0.0; |
| 110 | |
| 111 | for(int r=0;r<TROUND;r++){ |
| 112 | for(int i=0;i<TOTAL;i++) for(int l=0;l<LINKS;l++) inflow[i][l]=0.0; |
| 113 | /* bidirectional flow through each junction, scaled by each end's phi */ |
| 114 | for(int i=0;i<TOTAL;i++){ |
| 115 | double gi=(double)phic[i]/PHIMAX; |
| 116 | for(int l=0;l<LINKS;l++){ |
| 117 | int j=lnk[i][l]; double c=ww[i][l]; |
| 118 | double i2j = gi*c; |
| 119 | double j2i = ((double)phic[j]/PHIMAX)*c; |
| 120 | recv[j]+=i2j; recv[i]+=j2i; |
| 121 | inflow[i][l]=j2i; /* what i observes: flow down its own junction l */ |
| 122 | } |
| 123 | } |
| 124 | /* conductance update: reallocate toward junctions that delivered, in proportion to k */ |
| 125 | for(int i=0;i<TOTAL;i++){ |
| 126 | double tot=0; for(int l=0;l<LINKS;l++) tot+=inflow[i][l]; |
| 127 | if(tot<=0) continue; |
| 128 | double k=kk[i]; |
| 129 | double share[LINKS]; |
| 130 | for(int l=0;l<LINKS;l++) share[l]=inflow[i][l]/tot; |
| 131 | if(shuffled){ /* P4: same update, wrong addresses */ |
| 132 | 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; } |
| 133 | } |
| 134 | double s=0; |
| 135 | for(int l=0;l<LINKS;l++){ ww[i][l]=(1.0-k)*ww[i][l]+k*share[l]; s+=ww[i][l]; } |
| 136 | if(s>0) for(int l=0;l<LINKS;l++) ww[i][l]/=s; /* fixed giving budget */ |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | /* private fidelity from received flow */ |
| 141 | for(int i=0;i<TOTAL;i++){ |
| 142 | double R=recv[i]/TROUND/RSCALE; if(R<0)R=0; if(R>1)R=1; |
| 143 | fid[i]=F0+(F1-F0)*R; |
| 144 | } |
| 145 | |
| 146 | /* global Wright-Fisher. NO compartments, NO group selection. Parasites replicate faster. */ |
| 147 | double tot=0; |
| 148 | for(int i=0;i<TOTAL;i++){ tot+=1.0+BETA*(1.0-(double)phic[i]/PHIMAX); cumw[i]=tot; } |
| 149 | for(int i=0;i<TOTAL;i++){ |
| 150 | double r=urand()*tot; int lo=0,hi=TOTAL-1; |
| 151 | while(lo<hi){ int mid=(lo+hi)/2; if(cumw[mid]<r) lo=mid+1; else hi=mid; } |
| 152 | nseq[i]=replicate(seq[lo],fid[lo]); |
| 153 | double k2 = evolve_k ? kk[lo]+MUTK*randn() : kk[lo]; |
| 154 | if(k2<0)k2=0; |
| 155 | if(k2>1)k2=1; |
| 156 | nkk[i]=k2; |
| 157 | } |
| 158 | for(int i=0;i<TOTAL;i++){ seq[i]=nseq[i]; kk[i]=nkk[i]; } |
| 159 | } |
| 160 | |
| 161 | double sp=0,sk=0,sf=0; |
| 162 | for(int i=0;i<TOTAL;i++){ sp+=phi(seq[i]); sk+=kk[i]; sf+=fid[i]; } |
| 163 | *k_end=sk/TOTAL; *f_end=sf/TOTAL; |
| 164 | return sp/TOTAL; |
| 165 | } |
| 166 | |
| 167 | int main(void) |
| 168 | { |
| 169 | unsigned long long seeds[10]; |
| 170 | seeds[0]=0x9e3779b97f4a7c15ULL; seeds[1]=0xd1b54a32d192ed03ULL; seeds[2]=0x2545f4914f6cdd1dULL; |
| 171 | for(int i=3;i<10;i++) seeds[i]=0x9e3779b97f4a7c15ULL ^ (0x100000001b3ULL*(unsigned long long)(i+1)); |
| 172 | int NS=10; |
| 173 | |
| 174 | printf("=== COUPLING: can a wiring rule privatize catalysis with NO container? ===\n"); |
| 175 | printf(" Same chemistry as ribozyme.c (phi = hairpin fold, parasites %.0fx faster, f0=%.2f f1=%.3f).\n",1+BETA,F0,F1); |
| 176 | printf(" NO compartments, NO space, NO group selection. Global Wright-Fisher over %d units.\n",TOTAL); |
| 177 | printf(" Each unit owns %d junctions; catalysis flows BOTH ways; trait k = how much it\n",LINKS); |
| 178 | printf(" reallocates conductance toward junctions that delivered. %d exchange rounds/gen.\n",TROUND); |
| 179 | printf(" REFERENCE: ribozyme.c well-mixed (no compartments) went 5.99 -> 5.50, parasites won.\n\n"); |
| 180 | |
| 181 | printf(" arm | mean phi (start->end of %2d) | k end | fidelity end\n",PHIMAX); |
| 182 | printf(" ---------------------------------+-----------------------------+-------+-------------\n"); |
| 183 | |
| 184 | double base_s=0,base_e=0; |
| 185 | struct { const char*name; int ev; double kf; int sh; } arms[]={ |
| 186 | {"k=0 frozen (public good) ", 0, 0.0, 0}, |
| 187 | {"k=1 frozen (full reciprocity) ", 0, 1.0, 0}, |
| 188 | {"k free to evolve (start 0.10) ", 1, 0.0, 0}, |
| 189 | {"k=1 SHUFFLED (control) ", 0, 1.0, 1}, |
| 190 | }; |
| 191 | int NA=(int)(sizeof(arms)/sizeof(arms[0])); |
| 192 | double endphi[8], startphi[8], endk[8]; |
| 193 | for(int a=0;a<NA;a++){ |
| 194 | double S=0,E=0,K=0,F=0; double ev[16]; |
| 195 | for(int s=0;s<NS;s++){ |
| 196 | double ke,fe,ps; RNG=seeds[s]; |
| 197 | double e=run(arms[a].ev,arms[a].kf,arms[a].sh,&ke,&fe,&ps); |
| 198 | ev[s]=e; E+=e; S+=ps; K+=ke; F+=fe; |
| 199 | } |
| 200 | S/=NS;E/=NS;K/=NS;F/=NS; |
| 201 | double sd=0; for(int s=0;s<NS;s++) sd+=(ev[s]-E)*(ev[s]-E); sd=sqrt(sd/(NS-1)); |
| 202 | endphi[a]=E; startphi[a]=S; endk[a]=K; |
| 203 | if(a==0){ base_s=S; base_e=E; } |
| 204 | printf(" %s | %.2f -> %5.2f +- %.2f | %.3f | %.4f\n",arms[a].name,S,E,sd,K,F); |
| 205 | } |
| 206 | printf("\n"); |
| 207 | |
| 208 | int pub_fell = endphi[0] < startphi[0]-0.3; |
| 209 | int recip_rose = endphi[1] > startphi[1]+0.5; |
| 210 | int evo_rose = endphi[2] > startphi[2]+0.5; |
| 211 | int k_climbed = endk[2] > 0.30; |
| 212 | int shuf_rose = endphi[3] > startphi[3]+0.5; |
| 213 | |
| 214 | printf(" P1 public good (k=0) : phi %s (%.2f -> %.2f)\n", pub_fell?"FELL — tragedy reproduced":"did NOT fall", base_s, base_e); |
| 215 | printf(" P2 reciprocity (k=1) : phi %s (%.2f -> %.2f)\n", recip_rose?"ROSE":"did NOT rise", startphi[1], endphi[1]); |
| 216 | printf(" P3 k evolvable : phi %s (%.2f -> %.2f), k %s (0.10 -> %.3f)\n", |
| 217 | evo_rose?"ROSE":"did NOT rise", startphi[2], endphi[2], k_climbed?"CLIMBED":"did not climb", endk[2]); |
| 218 | printf(" P4 shuffled control : phi %s (%.2f -> %.2f)\n", shuf_rose?"ROSE — CONFOUND":"did NOT rise (good)", startphi[3], endphi[3]); |
| 219 | printf("\n"); |
| 220 | |
| 221 | if(recip_rose && pub_fell && !shuf_rose) |
| 222 | printf(" => COUPLING PRIVATIZES THE PUBLIC GOOD WITH NO CONTAINER. Cutting a junction to a\n" |
| 223 | " freeloader costs nothing because it was sending nothing back, so conditional\n" |
| 224 | " conductance starves parasites of fidelity while catalysts fund each other. The\n" |
| 225 | " shuffled control did NOT rise, so this is reciprocity and not merely concentrating\n" |
| 226 | " conductance. %s\n", |
| 227 | evo_rose&&k_climbed ? "AND k evolved up on its own — the wiring rule is itself selected." |
| 228 | : "NOTE: k did NOT evolve up on its own, so conditioning works but is not self-installing."); |
| 229 | else if(recip_rose && shuf_rose) |
| 230 | printf(" => CONFOUNDED. phi rose in the shuffled control too, so the effect is about\n" |
| 231 | " concentrating conductance onto few partners, NOT about reciprocity. Do not claim\n" |
| 232 | " a coupling result from this.\n"); |
| 233 | else if(!recip_rose) |
| 234 | printf(" => HONEST NULL. Wiring did not privatize catalysis here: parasites harvest enough\n" |
| 235 | " before conductances sort, or catalysts never find each other in a random network.\n" |
| 236 | " Containers remain the only thing in the arc that privatizes. Accept the null.\n"); |
| 237 | else |
| 238 | printf(" => Mixed/ambiguous — report the numbers as-is, claim nothing.\n"); |
| 239 | return 0; |
| 240 | } |