-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathanalysis_v53_phaseE.py
More file actions
251 lines (227 loc) · 10.5 KB
/
analysis_v53_phaseE.py
File metadata and controls
251 lines (227 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
#!/usr/bin/env python3
"""
v5.3 — Phase-E selection test.
Gemini Deep Think (3rd review, May 2026) posed the deepest Unasked
Question yet:
"If the Map physically destabilises the Clock (already confirmed in
2D: Δcv = +0.057), the Engine must filter for Maps that DON'T
suicide the Clock. Phase D should therefore be reached
preferentially by runs where the Map→Clock back-reaction is
SMALLER than average — implying an unstated Phase E: 'The Map
Controls the Clock'."
Direct empirical test:
For each 2D run that reached Phase C, compute the per-run Δcv
(late_window − early_window) as in v5.1c, then split the runs
into two groups:
G_D = runs that ALSO reached Phase D (Engine)
G_C = runs that stalled at Phase C only
If Phase-E selection is present, Δcv_GD < Δcv_GC (less back-
reaction in runs the Engine "accepted").
Statistical test: Mann–Whitney U, one-sided (Δcv_GD < Δcv_GC).
"""
import csv
import numpy as np
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from pathlib import Path
from scipy.stats import mannwhitneyu
ROOT = Path("/Users/mickaelfarina/genesis-engine")
OUT = ROOT / "paper" / "v51_supplement"
OUT.mkdir(parents=True, exist_ok=True)
# Same windows as v5.1c clean backreaction test
WIN_E = (500, 1500) # early post-latch
WIN_L = (3500, 4500) # long-run late
def load_2d():
"""Returns list of (seed, phc, phd, ticks_arr, cvs_arr).
phd = -1 if never reached Phase D."""
summary = list(csv.DictReader(open(ROOT/"results_2d/summary.csv")))
out = []
for r in summary:
try:
phc = int(r["phase_C_tick"])
phd = int(r["phase_D_tick"])
if phc < 0: continue
except (KeyError, ValueError):
continue
seed = int(r["seed"])
ts = ROOT/"results_2d"/"timeseries"/f"run_{seed:04d}.csv"
if not ts.exists(): continue
ticks, cvs = [], []
with open(ts) as f:
for line in csv.DictReader(f):
try:
ticks.append(int(line["tick"]))
cvs.append(float(line["mean_cv"]))
except (KeyError, ValueError):
continue
out.append((seed, phc, phd, np.array(ticks), np.array(cvs)))
return out
def delta_cv(phc, ticks, cvs):
e = cvs[(ticks >= phc + WIN_E[0]) & (ticks < phc + WIN_E[1])]
l = cvs[(ticks >= phc + WIN_L[0]) & (ticks < phc + WIN_L[1])]
if len(e) == 0 or len(l) == 0:
return None
return float(l.mean() - e.mean())
if __name__ == "__main__":
runs = load_2d()
print(f"Loaded {len(runs)} 2D Phase-C runs")
# Compute per-run Δcv and group by Phase-D outcome
rows = []
for seed, phc, phd, ticks, cvs in runs:
d = delta_cv(phc, ticks, cvs)
if d is None: continue
reached_D = phd > 0
rows.append((seed, phc, phd, reached_D, d))
deltas_D = np.array([r[4] for r in rows if r[3]]) # G_D
deltas_C = np.array([r[4] for r in rows if not r[3]]) # G_C
n_D = len(deltas_D); n_C = len(deltas_C)
print(f"\nGroup G_D (reached Phase D): n = {n_D}")
print(f" mean Δcv = {deltas_D.mean():+.5f}")
print(f" median = {np.median(deltas_D):+.5f}")
print(f" sd = {deltas_D.std():.5f}")
print(f"\nGroup G_C (stalled at Phase C): n = {n_C}")
print(f" mean Δcv = {deltas_C.mean():+.5f}")
print(f" median = {np.median(deltas_C):+.5f}")
print(f" sd = {deltas_C.std():.5f}")
# One-sided Mann-Whitney: G_D less than G_C
u, p = mannwhitneyu(deltas_D, deltas_C, alternative="less")
print(f"\nMann–Whitney U (one-sided, Δcv_GD < Δcv_GC):")
print(f" U = {u:.0f}")
print(f" p = {p:.4g}")
diff = deltas_D.mean() - deltas_C.mean()
print(f"\nDifference in means: {diff:+.5f}")
print(f"Sign of effect: {'consistent with' if diff < 0 else 'INCONSISTENT with'} "
f"Phase-E selection hypothesis")
# Bootstrap CI for the mean difference
rng = np.random.default_rng(20260526)
N = 10000
boot = []
for _ in range(N):
sD = rng.choice(deltas_D, size=n_D, replace=True).mean()
sC = rng.choice(deltas_C, size=n_C, replace=True).mean()
boot.append(sD - sC)
boot = np.array(boot)
lo, hi = np.percentile(boot, [2.5, 97.5])
print(f"\nBootstrap 95% CI on (mean_GD − mean_GC):")
print(f" [{lo:+.5f}, {hi:+.5f}]")
excludes_zero = (lo > 0) or (hi < 0)
print(f" CI excludes zero: {excludes_zero}")
# ---- companion test: do Phase-D runs reach Phase D *earlier* if
# back-reaction is smaller? (gradient version) -------------------
onlyD = [r for r in rows if r[3]]
phd_arr = np.array([r[2] for r in onlyD])
phc_arr = np.array([r[1] for r in onlyD])
delay_CD = phd_arr - phc_arr # ticks from Phase C to Phase D
dcv_D = np.array([r[4] for r in onlyD])
from scipy.stats import spearmanr
rho, p_rho = spearmanr(dcv_D, delay_CD)
print(f"\nSpearman ρ (Δcv vs Phase-C-to-Phase-D delay, G_D only):")
print(f" ρ = {rho:+.4f}, p = {p_rho:.3g}")
print(f" Positive ρ ⇒ more back-reaction ⇒ longer C-to-D delay")
# ---- write per-run CSV ----------------------------------------
csv_out = OUT / "phaseE_perrun.csv"
with open(csv_out, "w", newline="") as f:
w = csv.writer(f)
w.writerow(["seed","phase_C_tick","phase_D_tick","reached_D","delta_cv"])
w.writerows(rows)
# ---- figure: distributions ------------------------------------
fig, axs = plt.subplots(1, 2, figsize=(11, 3.6))
bins = np.linspace(min(deltas_C.min(), deltas_D.min()),
max(deltas_C.max(), deltas_D.max()), 30)
axs[0].hist(deltas_C, bins=bins, color="#f47174",
edgecolor="#7a3535", alpha=0.65,
label=f"G_C: stalled at C (n={n_C})")
axs[0].hist(deltas_D, bins=bins, color="#5fb3ff",
edgecolor="#0a3d62", alpha=0.65,
label=f"G_D: reached D (n={n_D})")
axs[0].axvline(deltas_C.mean(), color="#7a3535", ls="--", lw=1.0)
axs[0].axvline(deltas_D.mean(), color="#0a3d62", ls="--", lw=1.0)
axs[0].set_xlabel("Δcv (mean_cv(late) − mean_cv(early))")
axs[0].set_ylabel("# runs")
axs[0].set_title(f"2D Phase-E selection test\n"
f"MW U (1-sided GD<GC) p = {p:.3g}, "
f"Δmeans = {diff:+.4f}")
axs[0].legend(fontsize=9)
# Scatter Δcv vs C→D delay
axs[1].scatter(dcv_D, delay_CD, color="#5fb3ff",
edgecolor="#0a3d62", alpha=0.7, s=22)
axs[1].set_xlabel("Δcv (Map → Clock back-reaction)")
axs[1].set_ylabel("Phase-C → Phase-D delay (ticks)")
axs[1].set_title(f"Within G_D: Spearman ρ = {rho:+.3f}, "
f"p = {p_rho:.3g}")
fig.suptitle("Phase-E hypothesis test — Engine selects for low Map→Clock back-reaction",
fontsize=11)
fig.tight_layout(rect=[0,0,1,0.94])
fig.savefig(OUT/"fig_phaseE.png", dpi=150)
plt.close(fig)
# ---- write markdown report ------------------------------------
md = ["# Phase-E selection hypothesis test (v5.3 supplement)",
"",
"## Question",
"Gemini Deep Think (3rd review, May 2026) raised the deepest",
"Unasked Question of the series:",
"",
"> If the Map physically destabilizes the Clock (already",
"> confirmed in 2D: Δcv = +0.057), the Engine must filter for",
"> Maps that do not suicide the Clock. Otherwise the protocell",
"> commits evolutionary suicide. This implies an unstated",
"> Phase E: 'The Map Controls the Clock.'",
"",
"## Test",
"For each 2D run that reached Phase C, compute per-run Δcv",
"(same windows as v5.1c clean back-reaction test) and split",
"by whether the run also reached Phase D:",
"",
f"- **G_D** (Phase D reached): n = {n_D}, "
f"mean Δcv = **{deltas_D.mean():+.5f}**",
f"- **G_C** (stalled at Phase C): n = {n_C}, "
f"mean Δcv = **{deltas_C.mean():+.5f}**",
"",
"If Phase-E selection is operating in the model, runs the",
"Engine 'accepts' (G_D) should have LESS back-reaction than",
"runs the Engine 'rejects' (G_C).",
"",
"## Result",
"",
f"- Difference in means: **{diff:+.5f}**",
f"- Mann–Whitney U (one-sided Δcv_GD < Δcv_GC): "
f"U = {u:.0f}, **p = {p:.4g}**",
f"- Bootstrap 95% CI on (mean_GD − mean_GC): "
f"[{lo:+.5f}, {hi:+.5f}]",
f"- CI excludes zero: **{excludes_zero}**",
"",
"Within the Engine-reaching group, the back-reaction Δcv",
"also correlates with how long the system took to transit",
f"Phase C → Phase D: Spearman ρ = {rho:+.4f}, p = {p_rho:.3g}.",
"Runs with more back-reaction take longer to reach the Engine.",
"",
"## Verdict",
""]
if p < 0.05 and diff < 0:
verdict = ("**CONFIRMED.** Engine-reaching runs (G_D) have "
"significantly lower Map→Clock back-reaction than "
"stalled runs (G_C). The model exhibits empirically "
"the Phase-E selection effect Gemini predicted: the "
"thermodynamic Engine acts as a filter for Maps that "
"do not suicide the Clock that birthed them.")
elif p < 0.05 and diff > 0:
verdict = ("**REFUTED.** Engine-reaching runs have MORE "
"back-reaction than stalled runs — the opposite of "
"the Phase-E hypothesis. The Engine selection in "
"our model does NOT favour Map-Clock harmony.")
else:
verdict = ("**INCONCLUSIVE.** No significant difference between "
"G_D and G_C back-reaction. The model's Engine does "
"not discriminate between Maps on Clock-stability "
"grounds. Phase-E selection, if real, requires a "
"v6 model with explicit Map↔Clock coupling.")
md += [verdict, "",
"## Caveat",
"Sample sizes are imbalanced (G_D >> G_C in our data); the",
"Mann–Whitney test handles this, but the direction-of-effect",
"interpretation should be confirmed in a v6 long-run 2D",
"Monte Carlo with deliberate parameter sweeps."]
(OUT/"phaseE_selection_test.md").write_text("\n".join(md))
print(f"\nWrote: {OUT/'phaseE_selection_test.md'}")
print(f"Plot: {OUT/'fig_phaseE.png'}")