Spaces:
Running
Running
| <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>artatopics — the exact phasor, solved</title> | |
| <style> | |
| :root { --gold:#8A6D0B; --blue:#1746DC; --bg:#fff; --card:#f6f7f9; --ink:#1a2330; --ink2:#4a5568; --ink3:#6b7686; --line:#e3e6ea; } | |
| @media (prefers-color-scheme: dark) { :root { --gold:#E8B923; --blue:#6f8dff; --bg:#010C17; --card:#06121E; --ink:#eef2f7; --ink2:#b8c2cf; --ink3:#8b98a8; --line:#12283c; } } | |
| * { box-sizing:border-box } body { margin:0; background:var(--bg); color:var(--ink); font:15px/1.55 -apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif } | |
| main { max-width:760px; margin:0 auto; padding:36px 20px 70px } h1 { font-size:26px; margin:0 } h1 b { color:var(--gold) } h2 { font-size:16px; margin:34px 0 8px } | |
| p { color:var(--ink2); margin:8px 0 } table { border-collapse:collapse; width:100%; font-size:14px } th,td { text-align:left; padding:6px 10px; border-bottom:1px solid var(--line) } | |
| th { color:var(--ink3); font-size:11px; text-transform:uppercase; letter-spacing:.08em } td.n { font-variant-numeric:tabular-nums; text-align:right } | |
| .mono { font-family:ui-monospace,Menlo,monospace; background:var(--card); border:1px solid var(--line); border-radius:10px; padding:10px 14px; overflow-x:auto; font-size:13.5px } | |
| select,button { padding:8px 12px; border-radius:10px; border:1px solid var(--line); background:var(--card); color:var(--ink); font-size:14px } button { cursor:pointer } | |
| #pyout { white-space:pre-wrap; font:12.5px/1.5 ui-monospace,Menlo,monospace; background:var(--card); border:1px solid var(--line); border-radius:10px; padding:10px 14px; min-height:40px } | |
| .row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin:8px 0 } .cap { font-size:12.5px; color:var(--ink3) } a { color:var(--blue) } footer { margin-top:44px; font-size:12.5px; color:var(--ink3) } | |
| </style></head><body><main> | |
| <h1>arta<b>topics</b> · the exact phasor, solved analytically</h1> | |
| <p>The model this campaign was built around, solved in closed form — no optimiser, no seed — and | |
| tested on the finest record we have: every arXiv submission since 1991, day by day, 127 categories.</p> | |
| <h2>1 · The model and its exact solution</h2> | |
| <p class="mono">y(t) = | b + Σᵢ aᵢ · e<sup> i(θᵢ(t) − pᵢ)</sup> |²</p> | |
| <p>The only inputs are θᵢ(t): the sidereal (Lahiri) longitudes of Sun, Moon, Mercury, Venus, Mars, | |
| Jupiter, Saturn and the true node from kerykeion (Swiss Ephemeris). The unknowns b, aᵢ, pᵢ are | |
| solved analytically. Expanding the square exactly:</p> | |
| <p class="mono">y = b² + Σaᵢ² + Σᵢ 2b·aᵢ·cos(θᵢ−pᵢ) + Σᵢ<ₖ 2aᵢaₖ·cos((θᵢ−pᵢ)−(θₖ−pₖ))</p> | |
| <p>Every term is linear in the fixed basis {1, cos θᵢ, sin θᵢ, cos(θᵢ−θₖ), sin(θᵢ−θₖ)}, so one | |
| least-squares solve gives its coefficients c. Then, exactly: <b>pᵢ = atan2(βᵢ, αᵢ)</b>; | |
| <b>Mᵢ = √(αᵢ²+βᵢ²) = 2b·aᵢ</b>; and from c₀ = b² + ΣMᵢ²/4b² the quadratic | |
| 4b⁴ − 4c₀b² + ΣMᵢ² = 0 gives <b>b² = (c₀ + √(c₀² − ΣMᵢ²))/2</b> — the '+' root, the only one that | |
| reproduces the fitted curve. Self-test on data generated by the exact model: b recovered to six | |
| decimals, aᵢ and pᵢ to 10⁻¹³; with 5% noise, R² 0.9976.</p> | |
| <p>The form <em>over-determines</em> itself: the aspect coefficients must equal 2aᵢaₖcos(pᵢ−pₖ) with the | |
| aᵢ, pᵢ the transit terms already fixed. How far the freely fitted aspects sit from that is the | |
| <b>aspect residual</b> — the part of the data the phasor cannot be. If disc = c₀² − ΣMᵢ² < 0, no | |
| real (b, aᵢ) exists at all: the fit is <b>infeasible</b>.</p> | |
| <h2>2 · What it scored on the daily record</h2> | |
| <p>Each of 127 arXiv categories fitted independently on the first 80% of its reliable days (the | |
| series as a ratio to its own trailing-365-day level, so b carries the level and the arrows the | |
| timing), scored on the last 20% as a <b>peak detector</b>: does the forecast rank the days before | |
| a submission peak above the rest? One AUC per category, averaged.</p> | |
| <table><tr><th>exact phasor, kerykeion sidereal</th><th>level AUC</th><th>rise AUC</th><th>shift-null</th><th>feasible</th><th>held-out R² (median)</th></tr><tr><td>all 8 bodies, unregularised</td><td class='n'>0.4999</td><td class='n'>0.5002</td><td class='n'>nan</td><td class='n'>22%</td><td class='n'>-1.327</td></tr><tr><td>Sun–Mars only (cycle-complete)</td><td class='n'>0.5001</td><td class='n'>0.4958</td><td class='n'>0.4973</td><td class='n'>83%</td><td class='n'>-0.411</td></tr><tr><td><b>all 8, L2, λ per category on an inner wall</b></td><td class='n'>0.4932</td><td class='n'>0.4972</td><td class='n'>0.5068</td><td class='n'>95%</td><td class='n'>-0.052</td></tr></table> | |
| <p class="cap">Chance for these autocorrelated labels is the circular-shift null, ~0.50–0.52. Under L2 the | |
| inner walls chose λ=0.0001: 4 · λ=0.1: 3 · λ=10.0: 2 · λ=100.0: 118 — in 118 of 127 categories the strongest shrinkage on offer, i.e. the model | |
| prefers b alone. Aspect residual is 1.000 throughout: the aspect terms the data wants bear no relation | |
| to the ones the transit terms determine.</p> | |
| <p>On the yearly citation-share task — where the campaign's <em>record</em> model scores −3.58 against | |
| carry-forward's −2.56 (per-field R² vs holdout mean) — the exact form with the same horizon anchor, | |
| selection on inner walls, scores <b>−8.25</b> on the live board. The record model fits | |
| √y ≈ b + Σaᵢcos(θᵢ−pᵢ), which drops the (Σaᵢ sin)² term of |z|²; keeping it makes the fit stiffer, | |
| not better. That approximation was carrying the record model.</p> | |
| <h2>3 · Solve it yourself</h2> | |
| <p>Pick a category. The button loads Python (Pyodide + numpy, ~10 MB), runs the <em>same closed-form | |
| solution</em> on the same kerykeion sky and the same daily series, and prints b, every aᵢ, every pᵢ, | |
| the feasibility, the aspect residual and the held-out peak AUC. Nothing here is precomputed.</p> | |
| <div class="row"><select id="cat"></select><button id="run">Solve</button><span id="st" class="cap"></span></div> | |
| <div id="pyout">(not run yet)</div> | |
| <footer>No causal claims. Ephemeris cross-checked against three engines (<a href="https://github.com/ArtaQuest/artatopics/blob/main/analysis/arxivtopics/competition/daily/EPHEMERIS_CHECK.md">the check</a>) · | |
| <a href="https://github.com/ArtaQuest/artatopics/blob/main/analysis/arxivtopics/competition/phasor_exact.py">solver source</a> · | |
| <a href="index.html">main results</a> · <a href="ensemble.html">the ensemble stack</a> · <a href="https://huggingface.co/spaces/artaquest/artatopics">also on Hugging Face</a></footer> | |
| </main> | |
| <script> | |
| (async () => { | |
| const D = await (await fetch("data/phasor.json")).json(); | |
| const sel = document.getElementById("cat"), out = document.getElementById("pyout"), st = document.getElementById("st"); | |
| Object.keys(D.series).forEach(c => { const o = document.createElement("option"); o.textContent = c; sel.appendChild(o); }); | |
| let py = null; | |
| const CODE = ` | |
| import numpy as np, json, datetime as dt | |
| D = json.loads(DATA); LON = np.array(D["lon"], float); e0 = dt.date.fromisoformat(D["e0"]) | |
| def basis(TH): | |
| T,B = TH.shape; C=[np.ones(T)] | |
| for i in range(B): C += [np.cos(TH[:,i]), np.sin(TH[:,i])] | |
| for i in range(B): | |
| for k in range(i+1,B): d=TH[:,i]-TH[:,k]; C += [np.cos(d), np.sin(d)] | |
| return np.stack(C,1) | |
| def solve(TH, y, ridge): | |
| T,B = TH.shape; Phi = basis(TH); R = np.eye(Phi.shape[1]); R[0,0]=0 | |
| c = np.linalg.solve(Phi.T@Phi + ridge*R + 1e-12*np.eye(Phi.shape[1]), Phi.T@y) | |
| c0=c[0]; al=c[1:1+2*B:2]; be=c[2:2+2*B:2]; p=np.arctan2(be,al); M=np.sqrt(al**2+be**2) | |
| disc=c0**2-(M**2).sum(); feas = disc>=0 and c0>0 | |
| if not feas: M=M*min(1.0, np.sqrt(max(c0,1e-12)**2/max((M**2).sum(),1e-24))); disc=max(c0**2-(M**2).sum(),0) | |
| b=np.sqrt(max((c0+np.sqrt(disc))/2,1e-18)); a=M/(2*b) | |
| gf=c[1+2*B::2]; df_=c[2+2*B::2]; gi=[]; di=[] | |
| for i in range(B): | |
| for k in range(i+1,B): gi.append(2*a[i]*a[k]*np.cos(p[i]-p[k])); di.append(2*a[i]*a[k]*np.sin(p[i]-p[k])) | |
| fr=np.concatenate([gf,df_]); im=np.concatenate([gi,di]); res=float(np.sqrt(((fr-im)**2).sum())/max(np.sqrt((fr**2).sum()),1e-12)) | |
| return b,a,p,feas,res | |
| def predict(TH,b,a,p): return np.abs(b + (a[None,:]*np.exp(1j*(TH-p[None,:]))).sum(1))**2 | |
| def auc(y,s): | |
| o=np.argsort(s); r=np.empty(len(s)); r[o]=np.arange(1,len(s)+1); n1=y.sum(); n0=len(y)-n1 | |
| return float((r[y==1].sum()-n1*(n1+1)/2)/(n1*n0)) if n1>0 and n0>0 else float("nan") | |
| S = D["series"][CAT]; x=np.array(S["x"],float); start=dt.date.fromisoformat(S["start"]); off=(start-e0).days | |
| TH=np.deg2rad(LON[off:off+len(x)]); n=len(x); cut=int(n*0.8) | |
| lvl=np.array([x[max(0,i-365):i].mean() if i>60 else x[:60].mean() for i in range(n)]); xr=x/np.maximum(lvl,1e-9) | |
| # peaks: 7-day centred mean, local max above trailing-90 median + max(20%, 1sd) | |
| s7=np.convolve(x,np.ones(7)/7,mode="same"); med=np.array([np.median(s7[max(0,i-90):i]) if i>30 else np.nan for i in range(n)]) | |
| sd=np.array([np.std(s7[max(0,i-90):i]-np.nanmean(med[max(0,i-90):i])) if i>30 else np.nan for i in range(n)]) | |
| thr=np.where(np.isnan(med),np.inf,med+np.maximum(0.2*np.nan_to_num(med),np.nan_to_num(sd,nan=np.inf))) | |
| peak=np.zeros(n,bool) | |
| for i in range(3,n-3): | |
| if s7[i]>=s7[i-3:i+4].max() and s7[i]>thr[i]: peak[i]=True | |
| y=np.zeros(n,int) | |
| for i in np.where(peak)[0]: y[max(0,i-7):i]=1 | |
| best=None | |
| for lam in (1e-4,1e-3,1e-2,1e-1,1.0,10.0,100.0): | |
| k=int(cut*0.75); b,a,p,f,r=solve(TH[:k],xr[:k],lam); yh=predict(TH,b,a,p) | |
| r2=1-((xr[k:cut]-yh[k:cut])**2).sum()/((xr[k:cut]-xr[:k].mean())**2).sum() | |
| if best is None or r2>best[0]: best=(r2,lam) | |
| b,a,p,feas,res=solve(TH[:cut],xr[:cut],best[1]); yh=predict(TH,b,a,p); te=slice(cut,n) | |
| r2=1-((xr[te]-yh[te])**2).sum()/((xr[te]-xr[:cut].mean())**2).sum() | |
| print(f"{CAT}: {n} days from {start} · {int(peak.sum())} peaks · fit on first {cut} days, scored on the last {n-cut}") | |
| print(f"lambda chosen on the inner split: {best[1]}") | |
| print(f"b = {b:.4f} feasible: {feas} aspect residual: {res:.3f}") | |
| for nm,ai,pi in zip(D["bodies"],a,p): print(f" {nm:<10} a = {ai:.4f} p = {np.rad2deg(pi)%360:7.2f} deg") | |
| print(f"held-out R2 (ratio to level): {r2:+.4f} · peak-detection AUC (level): {auc(y[te],yh[te]):.4f} (rise): {auc(y[te],np.diff(yh,prepend=yh[0])[te]):.4f}") | |
| `; | |
| document.getElementById("run").onclick = async () => { | |
| try { | |
| st.textContent = "loading Pyodide…"; | |
| if (!py) { const m = await import("https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.mjs"); py = await m.loadPyodide(); await py.loadPackage("numpy"); } | |
| st.textContent = "solving…"; py.globals.set("DATA", JSON.stringify(D)); py.globals.set("CAT", sel.value); | |
| let buf = ""; py.setStdout({ batched: s => { buf += s + "\n"; out.textContent = buf; } }); | |
| await py.runPythonAsync(CODE); st.textContent = "done"; | |
| } catch (e) { st.textContent = ""; out.textContent = "failed: " + e; } | |
| }; | |
| })(); | |
| </script></body></html> |