#!/usr/bin/env python3 """ TASK: gixx_amp=0 Discriminator — full experiment. Obey TASK_GIXX_ZERO_DISCRIMINATOR.md protocol exactly. """ import zmq, json, time, struct, numpy as np, os, sys from datetime import datetime, timezone ANALYSIS_DIR = "/mnt/d/resonance-engine-active/analysis" HANDOFF_FILE = "/mnt/d/resonance-engine-active/TASK_GIXX_ZERO_DISCRIMINATOR.md" CHRONICLE_FILE = "/mnt/d/resonance-engine-active/analysis/observer_chronicle.jsonl" def chronicle(kind, data, note=""): """Append to chronicle JSONL.""" entry = { "ts": datetime.now(timezone.utc).isoformat(), "actor": "cline-agent", "kind": kind, "data": data, "note": note } with open(CHRONICLE_FILE, "a", encoding="utf-8") as f: f.write(json.dumps(entry) + "\n") print(f" [CHRONICLE] {kind}: {note}") def main(): ctx = zmq.Context() # --- Sockets --- # Telemetry SUB (port 5556) tel_sub = ctx.socket(zmq.SUB) tel_sub.connect("tcp://127.0.0.1:5556") tel_sub.setsockopt_string(zmq.SUBSCRIBE, "") # Command PUB (port 5557) cmd_pub = ctx.socket(zmq.PUB) cmd_pub.connect("tcp://127.0.0.1:5557") # Ack SUB (port 5559) ack_sub = ctx.socket(zmq.SUB) ack_sub.connect("tcp://127.0.0.1:5559") ack_sub.setsockopt_string(zmq.SUBSCRIBE, "") # Coarse SUB (port 5561) coarse_sub = ctx.socket(zmq.SUB) coarse_sub.connect("tcp://127.0.0.1:5561") coarse_sub.setsockopt_string(zmq.SUBSCRIBE, "") time.sleep(1.0) # Allow ZMQ connections to establish # --- Helpers --- def drain_socket(sock, timeout=0.5): sock.setsockopt(zmq.RCVTIMEO, int(timeout * 1000)) while True: try: sock.recv(flags=zmq.NOBLOCK) except zmq.ZMQError: break sock.setsockopt(zmq.RCVTIMEO, -1) def recv_telemetry(timeout=5.0): """Receive one telemetry JSON frame. Returns dict or None.""" tel_sub.setsockopt(zmq.RCVTIMEO, int(timeout * 1000)) try: data = tel_sub.recv_string() tel_sub.setsockopt(zmq.RCVTIMEO, -1) return json.loads(data) except zmq.ZMQError: tel_sub.setsockopt(zmq.RCVTIMEO, -1) return None def recv_coarse_frames(n_frames, timeout=120.0): """Receive n_frames from coarse stream (5561). Returns list of (cycle, 32x32x6 array).""" coarse_sub.setsockopt(zmq.RCVTIMEO, int(timeout * 1000)) frames = [] while len(frames) < n_frames: try: data = coarse_sub.recv() if len(data) < 16 + 4 * 6144: continue if data[:4] != b"KGCF": continue cycle = struct.unpack_from(" 0.001: print(f"WARNING: gixx_amp={gixx_amp_val} not canonical. Proceeding anyway — handoff says compiled-in defaults.") chronicle("telemetry_check", {"cycle": tel["cycle"], "coherence": tel["coherence"], "gixx_amp": gixx_amp_val}, "Daemon live, params verified") # ========================================== # STEP 2: Settle ≥ 2,000 cycles # ========================================== print("\n" + "=" * 60) print("STEP 2: Settle to cycle ≥ 2,000") print("=" * 60) current_cycle = tel["cycle"] if current_cycle < 2000: wait_cycles = 2000 - current_cycle wait_sec = wait_cycles / 70.0 + 2 # ~70 cycles/s print(f" At cycle {current_cycle}, waiting ~{wait_sec:.0f}s to reach 2000...") time.sleep(wait_sec) # Confirm tel = recv_telemetry(timeout=5.0) print(f" Settled at cycle {tel['cycle']}, coh={tel['coherence']:.4f}") chronicle("settle", {"cycle": tel["cycle"]}, "Settled ≥ 2,000 cycles") # ========================================== # STEP 3: Baseline capture B0 (~200 frames) # ========================================== print("\n" + "=" * 60) print("STEP 3: Baseline capture B0 (~200 frames)") print("=" * 60) drain_socket(coarse_sub) time.sleep(1) b0_frames = recv_coarse_frames(200, timeout=60.0) if len(b0_frames) < 180: print(f"FATAL: only {len(b0_frames)} baseline frames") return 1 b0_cycles = [f[0] for f in b0_frames] b0_fields = [f[1] for f in b0_frames] save_npz("gixx0_B0.npz", b0_cycles, b0_fields, "Baseline at canonical gixx_amp=0.008") # ========================================== # STEP 4: Set gixx_amp = 0.0 # ========================================== print("\n" + "=" * 60) print("STEP 4: Set gixx_amp = 0.0") print("=" * 60) accepted, ack = send_cmd_verified({"cmd": "set_param", "param": "gixx_amp", "value": 0.0}, timeout=8.0) if not accepted: print(f"FATAL: set_param gixx_amp=0.0 failed or rejected. ACK: {ack}") chronicle("set_param_failed", {"param": "gixx_amp", "value": 0.0, "ack": ack}, "Set gixx_amp=0.0 FAILED") return 1 set_cycle = ack.get("cycle", 0) print(f" gixx_amp=0.0 accepted at cycle {set_cycle}") chronicle("set_param", {"param": "gixx_amp", "value": 0.0, "cycle": set_cycle, "ack": ack}, "gixx_amp set to 0.0") # ========================================== # STEP 5: Hold ≥10,000 cycles, capture W1/W2/W3 # ========================================== print("\n" + "=" * 60) print("STEP 5: Hold ≥10,000 cycles, capture W1, W2, W3") print("=" * 60) # W1: +500 cycles after set wait_sec = 500 / 70.0 + 2 print(f" Waiting {wait_sec:.0f}s for W1 (+500 cycles)...") time.sleep(wait_sec) drain_socket(coarse_sub) time.sleep(0.5) w1_frames = recv_coarse_frames(150, timeout=60.0) if len(w1_frames) < 100: print(f"WARNING: only {len(w1_frames)} W1 frames") else: w1c = [f[0] for f in w1_frames] w1f = [f[1] for f in w1_frames] save_npz("gixx0_W1.npz", w1c, w1f, f"W1: +500 after gixx_amp=0.0, cycles {w1c[0]}-{w1c[-1]}") # Health check tel = recv_telemetry(timeout=5.0) if tel: print(f" W1 health: cycle={tel['cycle']} coh={tel['coherence']:.4f} asym={tel['asymmetry']:.2f}") # W2: +5,000 cycles after set elapsed = (w1_frames[-1][0] if w1_frames else set_cycle + 500) - set_cycle wait_more = max(0, (5000 - elapsed) / 70.0 + 2) print(f"\n Waiting {wait_more:.0f}s for W2 (+5,000 cycles)...") time.sleep(wait_more) drain_socket(coarse_sub) time.sleep(0.5) w2_frames = recv_coarse_frames(150, timeout=60.0) if len(w2_frames) < 100: print(f"WARNING: only {len(w2_frames)} W2 frames") else: w2c = [f[0] for f in w2_frames] w2f = [f[1] for f in w2_frames] save_npz("gixx0_W2.npz", w2c, w2f, f"W2: +5,000 after gixx_amp=0.0, cycles {w2c[0]}-{w2c[-1]}") tel = recv_telemetry(timeout=5.0) if tel: print(f" W2 health: cycle={tel['cycle']} coh={tel['coherence']:.4f} asym={tel['asymmetry']:.2f}") # W3: +9,000 cycles after set elapsed = (w2_frames[-1][0] if w2_frames else set_cycle + 5000) - set_cycle wait_more = max(0, (9000 - elapsed) / 70.0 + 2) print(f"\n Waiting {wait_more:.0f}s for W3 (+9,000 cycles)...") time.sleep(wait_more) drain_socket(coarse_sub) time.sleep(0.5) w3_frames = recv_coarse_frames(150, timeout=60.0) if len(w3_frames) < 100: print(f"WARNING: only {len(w3_frames)} W3 frames") else: w3c = [f[0] for f in w3_frames] w3f = [f[1] for f in w3_frames] save_npz("gixx0_W3.npz", w3c, w3f, f"W3: +9,000 after gixx_amp=0.0, cycles {w3c[0]}-{w3c[-1]}") tel = recv_telemetry(timeout=5.0) if tel: print(f" W3 health: cycle={tel['cycle']} coh={tel['coherence']:.4f} asym={tel['asymmetry']:.2f}") chronicle("hold_complete", {"set_cycle": set_cycle, "final_cycle": w3c[-1] if w3_frames else 0}, "≥10,000-cycle hold at gixx_amp=0.0 complete") # ========================================== # STEP 6: Restore gixx_amp = 0.008, capture recovery # ========================================== print("\n" + "=" * 60) print("STEP 6: Restore gixx_amp = 0.008") print("=" * 60) accepted, ack = send_cmd_verified({"cmd": "set_param", "param": "gixx_amp", "value": 0.008}, timeout=8.0) if not accepted: print(f"FATAL: restore gixx_amp=0.008 failed. ACK: {ack}") chronicle("set_param_failed", {"param": "gixx_amp", "value": 0.008, "ack": ack}, "Restore FAILED") return 1 print(f" gixx_amp=0.008 restored at cycle {ack.get('cycle',0)}") chronicle("set_param", {"param": "gixx_amp", "value": 0.008, "cycle": ack.get("cycle",0), "ack": ack}, "gixx_amp restored") # Recovery window print(" Collecting recovery window (~100 frames)...") time.sleep(2) drain_socket(coarse_sub) time.sleep(1) r_frames = recv_coarse_frames(100, timeout=60.0) if len(r_frames) >= 80: rc = [f[0] for f in r_frames] rf = [f[1] for f in r_frames] save_npz("gixx0_R.npz", rc, rf, f"Recovery after restore gixx_amp=0.008, cycles {rc[0]}-{rc[-1]}") tel = recv_telemetry(timeout=5.0) if tel: print(f" Recovery health: cycle={tel['cycle']} coh={tel['coherence']:.4f}") if tel['coherence'] > 0.7: print(" Coherence recovering toward canonical band.") # ========================================== # STEP 7: Spectral analysis # ========================================== print("\n" + "=" * 60) print("STEP 7: Spectral analysis (periodogram, Hann window)") print("=" * 60) def spectral_analysis(frames, label, channel=4): """ channel=4 is sxy (index 5 if 0-based: rho=0, ux=1, uy=2, sxx=3, syy=4, sxy=5) Wait - the spec says [rho, ux, uy, sxx, syy, sxy] per tile. Channel 4 = syy, channel 5 = sxy. Let me use channel 5 for sxy. """ if len(frames) < 20: return {"error": "too few frames"} fields = np.stack(frames) # (n_frames, 32, 32, 6) # Global spatial mean of |sxy| sxy = fields[:, :, :, 5] # channel 5 = sxy signal = np.mean(np.abs(sxy), axis=(1, 2)) # (n_frames,) # Also ux for secondary ux = fields[:, :, :, 1] # channel 1 = ux ux_signal = np.mean(np.abs(ux), axis=(1, 2)) n = len(signal) # Frame spacing = 10 cycles # Periodogram with Hann window window = np.hanning(n) signal_dt = signal - np.mean(signal) ux_dt = ux_signal - np.mean(ux_signal) # Compute FFT fft = np.fft.rfft(signal_dt * window) fft_ux = np.fft.rfft(ux_dt * window) power = np.abs(fft) ** 2 power_ux = np.abs(fft_ux) ** 2 # Frequency bins in cycles per FRAME. Convert to cycles: period = (n * 10) / k # Actually: bin k corresponds to period = (n * frame_spacing) / k # frame_spacing = 10 cycles freqs = np.fft.rfftfreq(n, d=1.0) # cycles/frame periods = np.where(np.arange(len(power)) > 0, n * 10.0 / np.arange(1, len(power) + 1), np.inf) # Top-4 spectral peaks (excluding DC) # Sort by power, skip DC (bin 0) sorted_idx = np.argsort(power[1:])[::-1] + 1 top4_sxy = [(int(periods[i]), float(power[i])) for i in sorted_idx[:4] if i < len(periods)] sorted_idx_ux = np.argsort(power_ux[1:])[::-1] + 1 top4_ux = [(int(periods[i]), float(power_ux[i])) for i in sorted_idx_ux[:4] if i < len(periods)] # Fraction of total AC power in 100-150 cycle band ac_total = np.sum(power[1:]) band_mask = (periods >= 100) & (periods <= 150) # Apply mask skipping DC valid_mask = np.zeros(len(power), dtype=bool) valid_mask[1:] = band_mask[1:] band_power = np.sum(power[valid_mask]) band_fraction = band_power / ac_total if ac_total > 0 else 0.0 # top-2 spectral peak in 90-150? top2_in_band = any(90 <= p <= 150 for p, _ in top4_sxy[:2]) # Also 57-63 band for ux ux_ac_total = np.sum(power_ux[1:]) ux_band_57_63 = np.sum(power_ux[(periods >= 57) & (periods <= 63)]) ux_57_63_frac = ux_band_57_63 / ux_ac_total if ux_ac_total > 0 else 0.0 return { "label": label, "n_frames": n, "top4_sxy": top4_sxy, "top4_ux": top4_ux, "band_fraction_100_150": band_fraction, "top2_in_90_150": top2_in_band, "ac_total": float(ac_total), "band_power": float(band_power), "ux_57_63_frac": ux_57_63_frac, } # Analyze B0 and W3 (the decision windows) b0_analysis = spectral_analysis(b0_fields, "B0 (baseline, gixx_amp=0.008)") w3_analysis = spectral_analysis(w3f if w3_frames else b0_fields, "W3 (gixx_amp=0.0, +9k cycles)") print(f"\n B0 (baseline):") print(f" Top-4 sxy periods: {b0_analysis.get('top4_sxy', [])}") print(f" 100-150 band fraction: {b0_analysis['band_fraction_100_150']:.4f}") print(f" Top-2 in 90-150: {b0_analysis['top2_in_90_150']}") print(f"\n W3 (gixx_amp=0.0):") print(f" Top-4 sxy periods: {w3_analysis.get('top4_sxy', [])}") print(f" 100-150 band fraction: {w3_analysis['band_fraction_100_150']:.4f}") print(f" Top-2 in 90-150: {w3_analysis['top2_in_90_150']}") # Secondary: ux 57-63 band print(f"\n B0 ux 57-63 band frac: {b0_analysis['ux_57_63_frac']:.4f}") print(f" W3 ux 57-63 band frac: {w3_analysis['ux_57_63_frac']:.4f}") # ========================================== # STEP 8: Decision rule # ========================================== print("\n" + "=" * 60) print("STEP 8: Pre-committed decision rule") print("=" * 60) b0_band_frac = b0_analysis["band_fraction_100_150"] w3_band_frac = w3_analysis["band_fraction_100_150"] w3_top2_in_band = w3_analysis["top2_in_90_150"] verdict = "AMBIGUOUS" verdict_reason = "none of the gates fired" if w3_band_frac >= 0.5 * b0_band_frac and w3_top2_in_band: verdict = "SURVIVES" verdict_reason = f"W3 band-fraction ({w3_band_frac:.4f}) ≥ 0.5 × B0 ({0.5*b0_band_frac:.4f}) AND top-2 peak in 90-150" elif w3_band_frac < 0.1 * b0_band_frac: verdict = "DIES" verdict_reason = f"W3 band-fraction ({w3_band_frac:.4f}) < 0.1 × B0 ({0.1*b0_band_frac:.4f})" print(f" B0 100-150 band fraction: {b0_band_frac:.4f}") print(f" W3 100-150 band fraction: {w3_band_frac:.4f}") print(f" W3 top-2 in 90-150: {w3_top2_in_band}") print(f"\n VERDICT: **{verdict}**") print(f" Reason: {verdict_reason}") chronicle("verdict", { "b0_band_frac": b0_band_frac, "w3_band_frac": w3_band_frac, "w3_top2_in_90_150": w3_top2_in_band, "verdict": verdict, "reason": verdict_reason }, f"gixx_amp=0 discriminator: {verdict}") # ========================================== # STEP 9: Write RESULTS to handoff file # ========================================== print("\n" + "=" * 60) print("STEP 9: Writing RESULTS to handoff file") print("=" * 60) results_text = f""" ## RESULTS (auto-written by cline-agent, {datetime.now(timezone.utc).isoformat()}) ### Execution - Daemon launched, canonical params confirmed (gixx_amp=0.008) - Baseline B0: {len(b0_frames)} frames, cycles {b0_cycles[0]}-{b0_cycles[-1]} - gixx_amp=0.0 set at cycle {set_cycle}, ack-verified - W1 (+500): {len(w1_frames) if w1_frames else 0} frames - W2 (+5k): {len(w2_frames) if w2_frames else 0} frames - W3 (+9k): {len(w3_frames) if w3_frames else 0} frames, cycles {w3c[0] if w3_frames else '?'}-{w3c[-1] if w3_frames else '?'} - gixx_amp=0.008 restored, ack-verified - Recovery R: {len(r_frames) if r_frames else 0} frames ### Spectral Analysis **B0 (baseline, gixx_amp=0.008):** - Top-4 sxy periods (cycles): {b0_analysis.get('top4_sxy', [])} - 100-150 cycle band fraction: {b0_band_frac:.4f} - ux 57-63 band fraction: {b0_analysis['ux_57_63_frac']:.4f} **W3 (gixx_amp=0.0, +9,000 cycles):** - Top-4 sxy periods (cycles): {w3_analysis.get('top4_sxy', [])} - 100-150 cycle band fraction: {w3_band_frac:.4f} - Top-2 in 90-150: {w3_top2_in_band} - ux 57-63 band fraction: {w3_analysis['ux_57_63_frac']:.4f} ### Verdict **`{verdict}`** — {verdict_reason} - SURVIVES gate: band-fraction ≥ 0.5×B0 (need {0.5*b0_band_frac:.4f}, got {w3_band_frac:.4f}) AND top-2 in 90-150 (need True, got {w3_top2_in_band}) - DIES gate: band-fraction < 0.1×B0 (need <{0.1*b0_band_frac:.4f}, got {w3_band_frac:.4f}) ### Recovery - Coherence after restore: {tel['coherence']:.4f} (canonical band ~0.737-0.740) ### Artifacts - `analysis/gixx0_B0.npz` — baseline - `analysis/gixx0_W1.npz` — +500 cycles after gixx_amp=0.0 - `analysis/gixx0_W2.npz` — +5,000 cycles - `analysis/gixx0_W3.npz` — +9,000 cycles - `analysis/gixx0_R.npz` — recovery after restore """ with open(HANDOFF_FILE, "a", encoding="utf-8") as f: f.write(results_text) print(" RESULTS appended to TASK_GIXX_ZERO_DISCRIMINATOR.md") # Cleanup cmd_pub.close() tel_sub.close() ack_sub.close() coarse_sub.close() ctx.term() print("\n" + "=" * 60) print(f"EXPERIMENT COMPLETE. Verdict: {verdict}") print("=" * 60) return 0 if __name__ == "__main__": sys.exit(main())