Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,603 +1,272 @@
|
|
| 1 |
-
|
| 2 |
# ===============================================================
|
| 3 |
-
# Rendered Frame Theory —
|
| 4 |
-
# Domains: Atmospheric / Seismic / Magnetic / Solar
|
| 5 |
-
# Full transparency: exact inputs + computed z, τ_eff, Ω_obs, α_R, index, and decision rule.
|
| 6 |
-
# Single-file.
|
| 7 |
# ===============================================================
|
| 8 |
|
| 9 |
-
import math
|
| 10 |
-
from datetime import datetime, timezone, timedelta
|
| 11 |
-
|
| 12 |
import gradio as gr
|
| 13 |
-
import
|
| 14 |
-
import numpy as np
|
| 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 |
-
return
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
if not q:
|
| 64 |
-
return None, None, "Empty location"
|
| 65 |
-
url = "https://geocoding-api.open-meteo.com/v1/search"
|
| 66 |
-
params = {"name": q, "count": 1, "language": "en", "format": "json"}
|
| 67 |
-
r = httpx.get(url, params=params, headers=UA, timeout=12)
|
| 68 |
-
r.raise_for_status()
|
| 69 |
-
js = r.json()
|
| 70 |
-
results = js.get("results") or []
|
| 71 |
-
if not results:
|
| 72 |
-
return None, None, f"Could not geocode '{q}'"
|
| 73 |
-
top = results[0]
|
| 74 |
-
lat = float(top["latitude"])
|
| 75 |
-
lon = float(top["longitude"])
|
| 76 |
-
display = f"{top.get('name','')}, {top.get('country_code','')}".strip().strip(",")
|
| 77 |
-
return lat, lon, display
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def fetch_openmeteo_hourly(lat: float, lon: float, past_days: int = 1):
|
| 81 |
-
url = "https://api.open-meteo.com/v1/forecast"
|
| 82 |
-
params = {
|
| 83 |
-
"latitude": lat,
|
| 84 |
-
"longitude": lon,
|
| 85 |
-
"hourly": "temperature_2m,relative_humidity_2m,pressure_msl,wind_speed_10m",
|
| 86 |
-
"past_days": past_days,
|
| 87 |
-
"forecast_days": 1,
|
| 88 |
-
"timezone": "UTC",
|
| 89 |
-
}
|
| 90 |
-
r = httpx.get(url, params=params, headers=UA, timeout=18)
|
| 91 |
-
r.raise_for_status()
|
| 92 |
-
js = r.json()
|
| 93 |
-
hourly = js.get("hourly") or {}
|
| 94 |
-
return {
|
| 95 |
-
"time": hourly.get("time") or [],
|
| 96 |
-
"temp": hourly.get("temperature_2m") or [],
|
| 97 |
-
"rh": hourly.get("relative_humidity_2m") or [],
|
| 98 |
-
"p": hourly.get("pressure_msl") or [],
|
| 99 |
-
"wind": hourly.get("wind_speed_10m") or [],
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
def fetch_kp_last_24h():
|
| 104 |
-
url = "https://services.swpc.noaa.gov/json/planetary_k_index_1m.json"
|
| 105 |
-
r = httpx.get(url, headers=UA, timeout=15)
|
| 106 |
-
r.raise_for_status()
|
| 107 |
-
js = r.json()
|
| 108 |
-
if not isinstance(js, list) or not js:
|
| 109 |
-
return []
|
| 110 |
-
vals = []
|
| 111 |
-
for row in js:
|
| 112 |
-
kp = row.get("kp_index")
|
| 113 |
-
if kp is None:
|
| 114 |
-
continue
|
| 115 |
-
try:
|
| 116 |
-
vals.append(float(kp))
|
| 117 |
-
except Exception:
|
| 118 |
-
pass
|
| 119 |
-
return vals[-1440:]
|
| 120 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
return []
|
| 129 |
-
out = []
|
| 130 |
-
for row in js:
|
| 131 |
-
f = row.get("flux")
|
| 132 |
-
if f is None:
|
| 133 |
-
continue
|
| 134 |
try:
|
| 135 |
-
|
| 136 |
except Exception:
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
"
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
"minlatitude": str(minlat),
|
| 157 |
-
"minlongitude": str(minlon),
|
| 158 |
-
"maxlatitude": str(maxlat),
|
| 159 |
-
"maxlongitude": str(maxlon),
|
| 160 |
-
}
|
| 161 |
-
)
|
| 162 |
-
r = httpx.get(url, params=params, headers=UA, timeout=22)
|
| 163 |
-
r.raise_for_status()
|
| 164 |
-
js = r.json()
|
| 165 |
-
feats = js.get("features") if isinstance(js, dict) else None
|
| 166 |
-
if not feats:
|
| 167 |
-
return []
|
| 168 |
-
out = []
|
| 169 |
-
for f in feats:
|
| 170 |
-
props = f.get("properties") or {}
|
| 171 |
-
out.append({"id": f.get("id"), "mag": props.get("mag"), "place": props.get("place"), "time": props.get("time")})
|
| 172 |
-
return out
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
def magnetic_agent():
|
| 176 |
-
kp = fetch_kp_last_24h()
|
| 177 |
-
if len(kp) < 30:
|
| 178 |
-
return {"enabled": False, "reason": "NOAA Kp feed too short"}
|
| 179 |
-
last = float(kp[-1])
|
| 180 |
-
tail = kp[-360:] if len(kp) >= 360 else kp
|
| 181 |
-
drift = float(np.std(tail)) if len(tail) >= 10 else 0.0
|
| 182 |
-
slope = float((tail[-1] - tail[0]) / max(1, len(tail) - 1))
|
| 183 |
-
|
| 184 |
-
z = clamp((last / 9.0) + (drift / 2.0) + 2.0 * abs(slope), 0.0, 3.0)
|
| 185 |
-
tau = tau_eff_from_z(z)
|
| 186 |
-
idx = index_from_tau(tau)
|
| 187 |
-
|
| 188 |
-
if last >= 7.0 or z >= 2.0:
|
| 189 |
-
pred = "warning"
|
| 190 |
-
rule = "Kp>=7 OR z>=2.0"
|
| 191 |
-
elif last >= 5.0 or z >= 1.2:
|
| 192 |
-
pred = "watch"
|
| 193 |
-
rule = "Kp>=5 OR z>=1.2"
|
| 194 |
-
elif last >= 4.0 or z >= 0.8:
|
| 195 |
-
pred = "monitor"
|
| 196 |
-
rule = "Kp>=4 OR z>=0.8"
|
| 197 |
-
else:
|
| 198 |
-
pred = "hold"
|
| 199 |
-
rule = "else"
|
| 200 |
-
|
| 201 |
-
live = f"Global Kp={last:.1f} | drift={drift:.2f} | slope={slope:.4f}"
|
| 202 |
-
return {
|
| 203 |
-
"enabled": True,
|
| 204 |
-
"domain": "Magnetic",
|
| 205 |
-
"prediction": pred,
|
| 206 |
-
"rule_fired": rule,
|
| 207 |
-
"z": float(z),
|
| 208 |
-
"tau_eff": float(tau),
|
| 209 |
-
"omega_obs": float(OMEGA_OBS),
|
| 210 |
-
"alpha_r": float(ALPHA_R),
|
| 211 |
-
"index": float(idx),
|
| 212 |
-
"live_status": live,
|
| 213 |
-
"truth_source": "NOAA SWPC planetary_k_index_1m (global)",
|
| 214 |
-
"inputs_used": {"kp_last": last, "kp_drift": drift, "kp_slope": slope, "tail_len": len(tail)},
|
| 215 |
-
"location_effect": "Location input does not change Magnetic. Kp is a global index.",
|
| 216 |
-
"do": "Use this to track global geomagnetic storm regime shifts.",
|
| 217 |
-
"dont": "Do not treat this as a city-level magnetometer or outage predictor.",
|
| 218 |
-
"what_it_is_not": "Not a local geomagnetic forecast. Not a power-grid impact model.",
|
| 219 |
-
"why": "z_mag compresses magnitude+variability into a bounded stress coordinate; τ_eff rises as ln(1+z).",
|
| 220 |
-
"how": "Fetch Kp → compute last/variability/slope → z_mag → τ_eff=1.38 ln(1+z) → Index=Ω_obs·τ_eff·α_R → label via fixed thresholds.",
|
| 221 |
-
}
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
def solar_agent():
|
| 225 |
-
flux = fetch_goes_xray_1day()
|
| 226 |
-
if len(flux) < 50:
|
| 227 |
-
return {"enabled": False, "reason": "GOES X-ray feed too short"}
|
| 228 |
-
tail = flux[-120:] if len(flux) >= 120 else flux[-60:]
|
| 229 |
-
f_mean = float(np.mean(tail))
|
| 230 |
-
f_peak = float(np.max(tail))
|
| 231 |
-
|
| 232 |
-
lr = stable_log_ratio(f_mean, 1e-8)
|
| 233 |
-
z = clamp(lr / 10.0, 0.0, 3.0)
|
| 234 |
-
tau = tau_eff_from_z(z)
|
| 235 |
-
idx = index_from_tau(tau)
|
| 236 |
-
|
| 237 |
-
if f_peak >= 1e-4 or z >= 2.2:
|
| 238 |
-
pred = "flare likely"
|
| 239 |
-
rule = "peak>=1e-4 OR z>=2.2"
|
| 240 |
-
elif f_peak >= 1e-5 or z >= 1.5:
|
| 241 |
-
pred = "flare watch"
|
| 242 |
-
rule = "peak>=1e-5 OR z>=1.5"
|
| 243 |
-
elif f_mean >= 1e-6 or z >= 0.9:
|
| 244 |
-
pred = "monitor"
|
| 245 |
-
rule = "mean>=1e-6 OR z>=0.9"
|
| 246 |
-
else:
|
| 247 |
-
pred = "hold"
|
| 248 |
-
rule = "else"
|
| 249 |
-
|
| 250 |
-
live = f"Global GOES mean={f_mean:.2e} | peak={f_peak:.2e}"
|
| 251 |
-
return {
|
| 252 |
-
"enabled": True,
|
| 253 |
-
"domain": "Solar",
|
| 254 |
-
"prediction": pred,
|
| 255 |
-
"rule_fired": rule,
|
| 256 |
-
"z": float(z),
|
| 257 |
-
"tau_eff": float(tau),
|
| 258 |
-
"omega_obs": float(OMEGA_OBS),
|
| 259 |
-
"alpha_r": float(ALPHA_R),
|
| 260 |
-
"index": float(idx),
|
| 261 |
-
"live_status": live,
|
| 262 |
-
"truth_source": "NOAA SWPC GOES primary xrays-1-day (global)",
|
| 263 |
-
"inputs_used": {"flux_mean": f_mean, "flux_peak": f_peak, "tail_len": len(tail)},
|
| 264 |
-
"location_effect": "Location input does not change Solar. GOES flux is global.",
|
| 265 |
-
"do": "Use this to track global solar radiative regime changes.",
|
| 266 |
-
"dont": "Do not treat this as a flare time predictor or CME trajectory forecast.",
|
| 267 |
-
"what_it_is_not": "Not a flare timing model. Not a CME arrival model.",
|
| 268 |
-
"why": "z_solar is derived from flux relative to baseline via a log ratio; τ_eff rises as ln(1+z).",
|
| 269 |
-
"how": "Fetch GOES flux → mean/peak → z_solar=clamp(ln(F_mean/1e-8)/10) → τ_eff → Index → label via fixed thresholds.",
|
| 270 |
-
}
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
def atmospheric_agent(lat: float, lon: float, display: str):
|
| 274 |
-
wx = fetch_openmeteo_hourly(lat, lon, past_days=1)
|
| 275 |
-
temp = wx["temp"]
|
| 276 |
-
p = wx["p"]
|
| 277 |
-
wind = wx["wind"]
|
| 278 |
-
|
| 279 |
-
if len(temp) < 13:
|
| 280 |
-
return {"enabled": False, "reason": "Open-Meteo hourly series too short"}
|
| 281 |
-
|
| 282 |
-
t12 = [float(x) for x in temp[-13:]]
|
| 283 |
-
dT = float(max(t12) - min(t12))
|
| 284 |
-
|
| 285 |
-
dp = None
|
| 286 |
-
if len(p) >= 13:
|
| 287 |
-
p12 = [float(x) for x in p[-13:]]
|
| 288 |
-
dp = float(p12[-1] - p12[0])
|
| 289 |
-
|
| 290 |
-
w_mean = None
|
| 291 |
-
if len(wind) >= 13:
|
| 292 |
-
w12 = [float(x) for x in wind[-13:]]
|
| 293 |
-
w_mean = float(np.mean(w12))
|
| 294 |
-
|
| 295 |
-
z_dt = clamp(dT / 10.0, 0.0, 2.0)
|
| 296 |
-
z_dp = clamp((abs(dp) / 12.0) if dp is not None else 0.0, 0.0, 1.5)
|
| 297 |
-
z = clamp(z_dt + z_dp, 0.0, 3.0)
|
| 298 |
-
tau = tau_eff_from_z(z)
|
| 299 |
-
idx = index_from_tau(tau)
|
| 300 |
-
|
| 301 |
-
if dT >= 10.0 or (dp is not None and dp <= -10.0):
|
| 302 |
-
pred = "storm risk"
|
| 303 |
-
rule = "ΔT>=10 OR ΔP<=-10"
|
| 304 |
-
elif dT >= 7.0 or (dp is not None and dp <= -6.0):
|
| 305 |
-
pred = "swing"
|
| 306 |
-
rule = "ΔT>=7 OR ΔP<=-6"
|
| 307 |
-
elif dT >= 4.0:
|
| 308 |
-
pred = "mild swing"
|
| 309 |
-
rule = "ΔT>=4"
|
| 310 |
-
else:
|
| 311 |
-
pred = "stable"
|
| 312 |
-
rule = "else"
|
| 313 |
-
|
| 314 |
-
parts = [f"{display} ΔT(12h)={dT:.1f}°C"]
|
| 315 |
-
if dp is not None:
|
| 316 |
-
parts.append(f"ΔP(12h)={dp:.1f} hPa")
|
| 317 |
-
if w_mean is not None:
|
| 318 |
-
parts.append(f"wind≈{w_mean:.1f} m/s")
|
| 319 |
-
live = " | ".join(parts)
|
| 320 |
-
|
| 321 |
-
return {
|
| 322 |
-
"enabled": True,
|
| 323 |
-
"domain": "Atmospheric",
|
| 324 |
-
"prediction": pred,
|
| 325 |
-
"rule_fired": rule,
|
| 326 |
-
"z": float(z),
|
| 327 |
-
"tau_eff": float(tau),
|
| 328 |
-
"omega_obs": float(OMEGA_OBS),
|
| 329 |
-
"alpha_r": float(ALPHA_R),
|
| 330 |
-
"index": float(idx),
|
| 331 |
-
"live_status": live,
|
| 332 |
-
"truth_source": "Open-Meteo hourly (location-based)",
|
| 333 |
-
"inputs_used": {"dT_12h": dT, "dP_12h": dp, "wind_mean": w_mean, "lat": lat, "lon": lon},
|
| 334 |
-
"location_effect": "Location input changes Atmospheric because it queries the selected lat/lon.",
|
| 335 |
-
"do": "Use this as a short-term stability detector from temperature swing and pressure change.",
|
| 336 |
-
"dont": "Do not treat this as a rain/snow probability forecast or a full numerical weather model.",
|
| 337 |
-
"what_it_is_not": "Not a precipitation forecast. Not a synoptic model. Not a radar nowcast.",
|
| 338 |
-
"why": "z_atm is derived from thermal swing and pressure change as a bounded stress coordinate; τ_eff rises as ln(1+z).",
|
| 339 |
-
"how": "Geocode → fetch hourly series → compute ΔT and ΔP (last ~12h) → z_atm → τ_eff → Index → label via fixed thresholds.",
|
| 340 |
-
}
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
def seismic_agent(region: str):
|
| 344 |
-
if region == "RingOfFire":
|
| 345 |
-
seen = set()
|
| 346 |
-
eqs = []
|
| 347 |
-
for bb in RING_OF_FIRE_BBOXES:
|
| 348 |
-
chunk = fetch_usgs_quakes(hours=24, minmag=2.5, bbox=bb)
|
| 349 |
-
for e in chunk:
|
| 350 |
-
eid = e.get("id")
|
| 351 |
-
if eid and eid not in seen:
|
| 352 |
-
seen.add(eid)
|
| 353 |
-
eqs.append(e)
|
| 354 |
-
else:
|
| 355 |
-
bbox = REGION_BBOX.get(region, None)
|
| 356 |
-
eqs = fetch_usgs_quakes(hours=24, minmag=2.5, bbox=bbox)
|
| 357 |
-
|
| 358 |
-
N = int(len(eqs))
|
| 359 |
-
mags = []
|
| 360 |
-
for e in eqs:
|
| 361 |
-
m = e.get("mag")
|
| 362 |
-
if m is None:
|
| 363 |
-
continue
|
| 364 |
try:
|
| 365 |
-
|
| 366 |
except Exception:
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
elif N >= 25 or z >= 1.0:
|
| 383 |
-
pred = "monitor"
|
| 384 |
-
rule = "N>=25 OR z>=1.0"
|
| 385 |
-
else:
|
| 386 |
-
pred = "quiet"
|
| 387 |
-
rule = "else"
|
| 388 |
-
|
| 389 |
-
scope = "Global" if region == "Global" else region
|
| 390 |
-
live = f"{scope} quakes(24h,M≥2.5)={N} | max M{Mmax:.1f}"
|
| 391 |
-
|
| 392 |
-
return {
|
| 393 |
-
"enabled": True,
|
| 394 |
-
"domain": "Seismic",
|
| 395 |
-
"prediction": pred,
|
| 396 |
-
"rule_fired": rule,
|
| 397 |
-
"z": float(z),
|
| 398 |
-
"tau_eff": float(tau),
|
| 399 |
-
"omega_obs": float(OMEGA_OBS),
|
| 400 |
-
"alpha_r": float(ALPHA_R),
|
| 401 |
-
"index": float(idx),
|
| 402 |
-
"live_status": live,
|
| 403 |
-
"truth_source": "USGS FDSN event feed (region-filtered)",
|
| 404 |
-
"inputs_used": {"count_24h": N, "max_mag_24h": Mmax, "region": region},
|
| 405 |
-
"location_effect": "Location input does not change Seismic in region mode. It filters by selected region, not by city.",
|
| 406 |
-
"do": "Use this as a regional seismic stress monitor (regime detection).",
|
| 407 |
-
"dont": "Do not treat this as a time-and-epicenter earthquake prediction system.",
|
| 408 |
-
"what_it_is_not": "Not an earthquake time predictor. Not a rupture location predictor.",
|
| 409 |
-
"why": "z_seis compresses activity density and severity into a bounded stress coordinate; τ_eff rises as ln(1+z).",
|
| 410 |
-
"how": "Fetch USGS in region → count + max magnitude → z_seis → τ_eff → Index → label via fixed thresholds.",
|
| 411 |
-
}
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
def run_forecast(location_text: str, seismic_region: str):
|
| 415 |
-
try:
|
| 416 |
-
lat, lon, display = geocode_location(location_text)
|
| 417 |
-
except Exception as e:
|
| 418 |
-
df = pd.DataFrame([{"Domain": "Error", "RFT Prediction": "DISABLED", "Live Status": f"Geocode error: {e}"}])
|
| 419 |
-
empty = {"enabled": False, "reason": f"Geocode error: {e}"}
|
| 420 |
-
return f"❌ Geocode error: {e}", df, empty, empty, empty, empty
|
| 421 |
-
|
| 422 |
-
if lat is None:
|
| 423 |
-
df = pd.DataFrame([{"Domain": "Error", "RFT Prediction": "DISABLED", "Live Status": display}])
|
| 424 |
-
empty = {"enabled": False, "reason": display}
|
| 425 |
-
return f"❌ {display}", df, empty, empty, empty, empty
|
| 426 |
-
|
| 427 |
try:
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
| 437 |
try:
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
"
|
| 458 |
-
|
| 459 |
-
"Live Status": out.get("live_status", ""),
|
| 460 |
-
}
|
| 461 |
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 469 |
)
|
| 470 |
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
- This is not a precipitation forecast and not a full numerical weather model.
|
| 502 |
-
"""
|
| 503 |
-
|
| 504 |
-
SEIS_MD = """
|
| 505 |
-
## Seismic
|
| 506 |
-
- This is a regional seismic stress monitor from activity density and peak magnitude.
|
| 507 |
-
- This is not a time/epicenter prediction system.
|
| 508 |
-
"""
|
| 509 |
-
|
| 510 |
-
MAG_MD = """
|
| 511 |
-
## Magnetic
|
| 512 |
-
- This tracks global geomagnetic storm regime shifts using NOAA Kp.
|
| 513 |
-
- This is not a local magnetometer reading or grid-impact forecast.
|
| 514 |
-
"""
|
| 515 |
-
|
| 516 |
-
SOL_MD = """
|
| 517 |
-
## Solar
|
| 518 |
-
- This tracks global solar radiative regime shifts using GOES X-ray flux.
|
| 519 |
-
- This is not a flare timing predictor or CME arrival model.
|
| 520 |
-
"""
|
| 521 |
-
|
| 522 |
-
METHOD_MD = f"""
|
| 523 |
-
## Open method equations
|
| 524 |
-
|
| 525 |
-
Shared core:
|
| 526 |
-
- τ_eff = {K_TAU} · ln(1 + z)
|
| 527 |
-
- Ω_obs = 2π / T_earth = {OMEGA_OBS:.6e}
|
| 528 |
-
- α_R = {ALPHA_R}
|
| 529 |
-
- Index = Ω_obs · τ_eff · α_R
|
| 530 |
-
|
| 531 |
-
z definitions:
|
| 532 |
-
- Atmospheric: z_atm = clamp( clamp(ΔT/10,0..2) + clamp(|ΔP|/12,0..1.5), 0..3 )
|
| 533 |
-
- Seismic: z_seis = clamp( clamp(N/60,0..1.5) + clamp(max(0,Mmax-4)/2.5,0..1.5), 0..3 )
|
| 534 |
-
- Magnetic: z_mag = clamp( (Kp_last/9) + (drift/2) + 2·|slope|, 0..3 )
|
| 535 |
-
- Solar: z_solar= clamp( ln(F_mean/1e-8)/10, 0..3 )
|
| 536 |
-
|
| 537 |
-
Decision thresholds are shown per-domain in the agent output under “rule_fired”.
|
| 538 |
-
"""
|
| 539 |
-
|
| 540 |
-
|
| 541 |
with gr.Blocks(title=APP_NAME) as demo:
|
| 542 |
-
gr.Markdown(f"# {APP_NAME}")
|
| 543 |
|
| 544 |
with gr.Tab("Live Forecast"):
|
| 545 |
with gr.Row():
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
"- Seismic is region-filtered in this build.\n\n"
|
| 552 |
-
"If a location cannot be resolved, predictions are disabled instead of guessed."
|
| 553 |
-
)
|
| 554 |
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
gr.
|
| 558 |
-
|
| 559 |
-
"- Filters USGS earthquakes by large region.\n"
|
| 560 |
-
"- Counts are not “near your city” unless radius mode is implemented.\n"
|
| 561 |
-
"- Not a time/epicenter prediction system."
|
| 562 |
-
)
|
| 563 |
|
| 564 |
-
|
| 565 |
-
gr.Markdown(
|
| 566 |
-
"**Run Forecast**\n\n"
|
| 567 |
-
"- Pulls live data and recomputes from scratch.\n"
|
| 568 |
-
"- No auto-refresh.\n"
|
| 569 |
-
"- No stored memory.\n"
|
| 570 |
-
"- No guessing when data is missing."
|
| 571 |
-
)
|
| 572 |
|
| 573 |
-
|
| 574 |
gr.Markdown(
|
| 575 |
-
"
|
| 576 |
-
"
|
| 577 |
-
"-
|
| 578 |
-
"-
|
|
|
|
|
|
|
| 579 |
)
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
gr.Markdown(ATM_MD)
|
| 584 |
-
atm_json = gr.JSON(label="Atmospheric agent output")
|
| 585 |
-
with gr.Accordion("Seismic details", open=False):
|
| 586 |
-
gr.Markdown(SEIS_MD)
|
| 587 |
-
sei_json = gr.JSON(label="Seismic agent output")
|
| 588 |
-
with gr.Accordion("Magnetic details", open=False):
|
| 589 |
-
gr.Markdown(MAG_MD)
|
| 590 |
-
mag_json = gr.JSON(label="Magnetic agent output")
|
| 591 |
-
with gr.Accordion("Solar details", open=False):
|
| 592 |
-
gr.Markdown(SOL_MD)
|
| 593 |
-
sol_json = gr.JSON(label="Solar agent output")
|
| 594 |
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
gr.Markdown(INSTRUCTIONS_MD)
|
| 599 |
-
gr.Markdown(METHOD_MD)
|
| 600 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 601 |
|
| 602 |
if __name__ == "__main__":
|
| 603 |
demo.launch()
|
|
|
|
|
|
|
| 1 |
# ===============================================================
|
| 2 |
+
# Rendered Frame Theory — Forecast Lab Console (Instant Falsifiability Links)
|
|
|
|
|
|
|
|
|
|
| 3 |
# ===============================================================
|
| 4 |
|
|
|
|
|
|
|
|
|
|
| 5 |
import gradio as gr
|
| 6 |
+
from datetime import datetime, timezone
|
| 7 |
+
import pandas as pd, numpy as np, httpx, io, csv
|
| 8 |
+
|
| 9 |
+
from rft_core.geocode import geocode_location
|
| 10 |
+
from data_adapters.noaa_geomag import fetch_k_index
|
| 11 |
+
from data_adapters.goes_xray import fetch_goes_flux
|
| 12 |
+
from data_adapters.metar_weather import fetch_meteo
|
| 13 |
+
from data_adapters.usgs_quakes import fetch_usgs_recent
|
| 14 |
+
|
| 15 |
+
from scoreboard_core import load_entries
|
| 16 |
+
from scoreboard_jobs import run_daily_cycle
|
| 17 |
+
|
| 18 |
+
APP_NAME = "Rendered Frame Theory — Forecast Lab Console"
|
| 19 |
+
|
| 20 |
+
# ---------- Helpers -----------------------------------------------------------
|
| 21 |
+
def classify_kp(kp: float) -> str:
|
| 22 |
+
if kp >= 7: return f"Kp={kp} (severe storm)"
|
| 23 |
+
if kp >= 5: return f"Kp={kp} (storm)"
|
| 24 |
+
if kp >= 4: return f"Kp={kp} (active)"
|
| 25 |
+
if kp >= 3: return f"Kp={kp} (unsettled)"
|
| 26 |
+
return f"Kp={kp} (quiet)"
|
| 27 |
+
|
| 28 |
+
def classify_flux(flux: float) -> str:
|
| 29 |
+
# GOES 1–8 Å classes
|
| 30 |
+
if flux >= 1e-4: return f"{flux:.2e} (X-class)"
|
| 31 |
+
if flux >= 1e-5: return f"{flux:.2e} (M-class)"
|
| 32 |
+
if flux >= 1e-6: return f"{flux:.2e} (C-class)"
|
| 33 |
+
if flux >= 1e-7: return f"{flux:.2e} (B-class)"
|
| 34 |
+
return f"{flux:.2e} (A-class)"
|
| 35 |
+
|
| 36 |
+
def build_verification_links(lat: float, lon: float, region: str) -> str:
|
| 37 |
+
"""
|
| 38 |
+
Returns a Markdown block with direct verification links so users can falsify instantly.
|
| 39 |
+
"""
|
| 40 |
+
# Magnetic (Kp)
|
| 41 |
+
swpc_kp_page = "https://www.swpc.noaa.gov/products/planetary-k-index"
|
| 42 |
+
swpc_kp_json = "https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json"
|
| 43 |
+
|
| 44 |
+
# Solar (GOES X-ray)
|
| 45 |
+
goes_plot_page = "https://www.swpc.noaa.gov/products/goes-x-ray-flux"
|
| 46 |
+
goes_primary_csv = "https://services.swpc.noaa.gov/text/goes-xray-flux-primary.csv"
|
| 47 |
+
goes_secondary_csv = "https://services.swpc.noaa.gov/text/goes-xray-flux-secondary.csv"
|
| 48 |
+
|
| 49 |
+
# Atmospheric (Open-Meteo)
|
| 50 |
+
# This is a direct URL to a forecast call for that lat/lon that anyone can open.
|
| 51 |
+
open_meteo_link = (
|
| 52 |
+
"https://api.open-meteo.com/v1/forecast"
|
| 53 |
+
f"?latitude={lat:.5f}&longitude={lon:.5f}"
|
| 54 |
+
"&hourly=temperature_2m&past_days=1&forecast_days=1&timezone=UTC"
|
| 55 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
# Seismic (USGS)
|
| 58 |
+
# Past day, minimum magnitude 2.5, global feed.
|
| 59 |
+
usgs_map = "https://earthquake.usgs.gov/earthquakes/map/"
|
| 60 |
+
usgs_query = (
|
| 61 |
+
"https://earthquake.usgs.gov/fdsnws/event/1/query"
|
| 62 |
+
"?format=geojson&starttime=&endtime=&minmagnitude=2.5"
|
| 63 |
+
)
|
| 64 |
+
usgs_past_day_geojson = "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson"
|
| 65 |
+
|
| 66 |
+
return (
|
| 67 |
+
"### Verify these results instantly (official sources)\n"
|
| 68 |
+
f"- **Magnetic (Kp):** {swpc_kp_page} \n"
|
| 69 |
+
f" Live feed JSON: {swpc_kp_json}\n"
|
| 70 |
+
f"- **Solar (GOES X-ray):** {goes_plot_page} \n"
|
| 71 |
+
f" Live CSV: {goes_primary_csv} \n"
|
| 72 |
+
f" Backup CSV: {goes_secondary_csv}\n"
|
| 73 |
+
f"- **Atmospheric (Temp history near your location):** {open_meteo_link}\n"
|
| 74 |
+
f"- **Seismic (USGS map):** {usgs_map} \n"
|
| 75 |
+
f" Past-day M≥2.5 GeoJSON: {usgs_past_day_geojson}\n"
|
| 76 |
+
"\n"
|
| 77 |
+
"**Note:** In the current build, the seismic summary is global (past 24h), so it won’t change with location. "
|
| 78 |
+
"If you want it to be location-aware, we filter USGS by radius around your lat/lon."
|
| 79 |
+
)
|
| 80 |
|
| 81 |
+
# ---------- Live domain logic -------------------------------------------------
|
| 82 |
+
def magnetic_block():
|
| 83 |
+
"""
|
| 84 |
+
Primary: SWPC JSON feed; fallback: Kyoto WDC mirror (open endpoint).
|
| 85 |
+
"""
|
| 86 |
+
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
try:
|
| 88 |
+
data = fetch_k_index(24)
|
| 89 |
except Exception:
|
| 90 |
+
url = "https://wdc.kugi.kyoto-u.ac.jp/kp/kp.json"
|
| 91 |
+
r = httpx.get(url, timeout=10)
|
| 92 |
+
j = r.json()
|
| 93 |
+
data = [{"kp": float(v)} for v in j[-24:]] if isinstance(j, list) else []
|
| 94 |
+
if not data:
|
| 95 |
+
return ("hold", "no data")
|
| 96 |
+
last_kp = float(data[-1]["kp"])
|
| 97 |
+
drift = np.std([d["kp"] for d in data[-6:]]) if len(data) >= 6 else 0.0
|
| 98 |
+
rft_pred = "storm soon" if last_kp >= 5 or drift > 1.2 else "quiet"
|
| 99 |
+
live = classify_kp(round(last_kp, 1))
|
| 100 |
+
return (rft_pred, live)
|
| 101 |
+
except Exception:
|
| 102 |
+
return ("hold", "fetch error")
|
| 103 |
+
|
| 104 |
+
def solar_block():
|
| 105 |
+
"""
|
| 106 |
+
Primary: GOES flux CSV; fallback: secondary mirror if blocked.
|
| 107 |
+
"""
|
| 108 |
+
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
try:
|
| 110 |
+
data = fetch_goes_flux(24)
|
| 111 |
except Exception:
|
| 112 |
+
url = "https://services.swpc.noaa.gov/text/goes-xray-flux-secondary.csv"
|
| 113 |
+
r = httpx.get(url, timeout=10)
|
| 114 |
+
reader = csv.DictReader(io.StringIO(r.text))
|
| 115 |
+
data = [{"flux": float(row["flux"])} for row in reader if row.get("flux")]
|
| 116 |
+
if not data:
|
| 117 |
+
return ("hold", "no data")
|
| 118 |
+
tail = data[-30:] if len(data) >= 30 else data
|
| 119 |
+
flux_mean = float(np.mean([d["flux"] for d in tail]))
|
| 120 |
+
rft_pred = "flare watch" if flux_mean > 1e-6 else "calm"
|
| 121 |
+
live = classify_flux(flux_mean)
|
| 122 |
+
return (rft_pred, live)
|
| 123 |
+
except Exception:
|
| 124 |
+
return ("hold", "fetch error")
|
| 125 |
+
|
| 126 |
+
def atmos_block(lat: float, lon: float):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
try:
|
| 128 |
+
data = fetch_meteo(lat, lon)
|
| 129 |
+
if not data:
|
| 130 |
+
return ("hold", "no data")
|
| 131 |
+
temps = [d["temp"] for d in data[-12:]] if len(data) >= 12 else [d["temp"] for d in data]
|
| 132 |
+
dT = (max(temps) - min(temps)) if temps else 0.0
|
| 133 |
+
rft_pred = "swing" if dT >= 6 else "stable"
|
| 134 |
+
live = f"ΔT(12 h) = {dT:.1f} °C"
|
| 135 |
+
return (rft_pred, live)
|
| 136 |
+
except Exception:
|
| 137 |
+
return ("hold", "fetch error")
|
| 138 |
+
|
| 139 |
+
def seismic_block(region: str):
|
| 140 |
try:
|
| 141 |
+
eqs = fetch_usgs_recent(24, 2.5)
|
| 142 |
+
if not eqs:
|
| 143 |
+
return ("quiet", "0 events (M≥2.5)")
|
| 144 |
+
count = len(eqs)
|
| 145 |
+
mags = [e["mag"] for e in eqs if e.get("mag") is not None]
|
| 146 |
+
max_mag = max(mags) if mags else 0.0
|
| 147 |
+
rft_pred = "watch" if max_mag >= 5.0 or count >= 20 else "quiet"
|
| 148 |
+
live = f"{count} events, max M{max_mag:.1f}"
|
| 149 |
+
return (rft_pred, live)
|
| 150 |
+
except Exception:
|
| 151 |
+
return ("hold", "fetch error")
|
| 152 |
+
|
| 153 |
+
# ---------- Orchestrator ------------------------------------------------------
|
| 154 |
+
def run_live_dashboard(loc_name: str, region: str):
|
| 155 |
+
lat, lon, display = geocode_location(loc_name)
|
| 156 |
+
if lat is None:
|
| 157 |
+
return (
|
| 158 |
+
f"❌ Could not find location '{loc_name}'",
|
| 159 |
+
pd.DataFrame(columns=["Domain","RFT Prediction","Live Status"]),
|
| 160 |
+
""
|
| 161 |
+
)
|
|
|
|
|
|
|
| 162 |
|
| 163 |
+
mag_pred, mag_live = magnetic_block()
|
| 164 |
+
sol_pred, sol_live = solar_block()
|
| 165 |
+
atm_pred, atm_live = atmos_block(lat, lon)
|
| 166 |
+
sei_pred, sei_live = seismic_block(region)
|
| 167 |
+
|
| 168 |
+
rows = [
|
| 169 |
+
{"Domain": "Magnetic", "RFT Prediction": mag_pred, "Live Status": mag_live},
|
| 170 |
+
{"Domain": "Solar", "RFT Prediction": sol_pred, "Live Status": sol_live},
|
| 171 |
+
{"Domain": "Atmospheric", "RFT Prediction": atm_pred, "Live Status": atm_live},
|
| 172 |
+
{"Domain": "Seismic", "RFT Prediction": sei_pred, "Live Status": sei_live},
|
| 173 |
+
]
|
| 174 |
+
df = pd.DataFrame(rows)
|
| 175 |
+
ts = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
| 176 |
+
header = f"**Location:** {display} | **UTC:** {ts}"
|
| 177 |
+
links_md = build_verification_links(lat, lon, region)
|
| 178 |
+
return header, df, links_md
|
| 179 |
+
|
| 180 |
+
# ---------- Scoreboard helpers -----------------------------------------------
|
| 181 |
+
def run_daily_cycle_ui() -> str:
|
| 182 |
+
records = run_daily_cycle()
|
| 183 |
+
if not records:
|
| 184 |
+
return "No records written (check scoreboard_jobs wiring)."
|
| 185 |
+
win = records[0]["window"]
|
| 186 |
+
return (
|
| 187 |
+
f"Logged {len(records)} scoreboard entries for window "
|
| 188 |
+
f"{win['start']} → {win['end']} (magnetic, solar, METAR)."
|
| 189 |
)
|
| 190 |
|
| 191 |
+
def build_scoreboard_df() -> pd.DataFrame:
|
| 192 |
+
entries = load_entries(limit=200)
|
| 193 |
+
if not entries:
|
| 194 |
+
return pd.DataFrame(
|
| 195 |
+
columns=[
|
| 196 |
+
"timestamp", "domain", "window_start", "window_end",
|
| 197 |
+
"event", "baseline_p", "rft_p",
|
| 198 |
+
"brier_baseline", "brier_rft"
|
| 199 |
+
]
|
| 200 |
+
)
|
| 201 |
+
rows = []
|
| 202 |
+
for e in entries:
|
| 203 |
+
ts = datetime.fromtimestamp(e["timestamp"], tz=timezone.utc).isoformat().replace("+00:00", "Z")
|
| 204 |
+
win = e.get("window", {})
|
| 205 |
+
rows.append({
|
| 206 |
+
"timestamp": ts,
|
| 207 |
+
"domain": e.get("domain", ""),
|
| 208 |
+
"window_start": win.get("start", ""),
|
| 209 |
+
"window_end": win.get("end", ""),
|
| 210 |
+
"event": e.get("event"),
|
| 211 |
+
"baseline_p": e.get("baseline_p"),
|
| 212 |
+
"rft_p": e.get("rft_p"),
|
| 213 |
+
"brier_baseline": e.get("brier_baseline"),
|
| 214 |
+
"brier_rft": e.get("brier_rft"),
|
| 215 |
+
})
|
| 216 |
+
df = pd.DataFrame(rows)
|
| 217 |
+
df.sort_values("timestamp", ascending=False, inplace=True)
|
| 218 |
+
return df
|
| 219 |
+
|
| 220 |
+
# ---------- UI ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
with gr.Blocks(title=APP_NAME) as demo:
|
| 222 |
+
gr.Markdown(f"# {APP_NAME}\nLive multi-domain forecast — simple, clear, always live.")
|
| 223 |
|
| 224 |
with gr.Tab("Live Forecast"):
|
| 225 |
with gr.Row():
|
| 226 |
+
loc_name = gr.Textbox(label="Location (city or region)", value="London")
|
| 227 |
+
region = gr.Dropdown(
|
| 228 |
+
["Global","EMEA","AMER","APAC","RingOfFire"],
|
| 229 |
+
value="EMEA", label="Seismic Region"
|
| 230 |
+
)
|
|
|
|
|
|
|
|
|
|
| 231 |
|
| 232 |
+
run_btn = gr.Button("Run Forecast", variant="primary")
|
| 233 |
+
header_md = gr.Markdown()
|
| 234 |
+
table = gr.Dataframe(headers=["Domain","RFT Prediction","Live Status"], interactive=False)
|
| 235 |
+
verify_md = gr.Markdown()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
|
| 237 |
+
run_btn.click(run_live_dashboard, [loc_name, region], [header_md, table, verify_md])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
|
| 239 |
+
with gr.Tab("Scoreboard"):
|
| 240 |
gr.Markdown(
|
| 241 |
+
"### RFT Falsifiability Scoreboard\n"
|
| 242 |
+
"This tab logs **synthetic but structured** daily tests for:\n"
|
| 243 |
+
"- Geomagnetic Kp storms\n"
|
| 244 |
+
"- GOES X-ray flux pulses\n"
|
| 245 |
+
"- METAR temperature swings\n\n"
|
| 246 |
+
"Each row: event flag, baseline probability, RFT probability, and Brier scores."
|
| 247 |
)
|
| 248 |
+
cycle_btn = gr.Button("Run daily scoring cycle (yesterday UTC)")
|
| 249 |
+
cycle_status = gr.Markdown()
|
| 250 |
+
cycle_btn.click(run_daily_cycle_ui, inputs=None, outputs=cycle_status)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
+
refresh_btn = gr.Button("Refresh scoreboard view")
|
| 253 |
+
board_df = gr.Dataframe(interactive=False)
|
| 254 |
+
refresh_btn.click(build_scoreboard_df, inputs=None, outputs=board_df)
|
|
|
|
|
|
|
| 255 |
|
| 256 |
+
with gr.Tab("Notes"):
|
| 257 |
+
gr.Markdown(
|
| 258 |
+
"## What this Space is doing\n"
|
| 259 |
+
"- Pulls public live feeds on-demand when you press **Run Forecast**.\n"
|
| 260 |
+
"- Computes simple, inspectable signals and prints both **RFT Prediction** and **Live Status**.\n"
|
| 261 |
+
"- Provides direct links for instant falsifiability (shown under the table after each run).\n\n"
|
| 262 |
+
"## What this Space is NOT doing\n"
|
| 263 |
+
"- It is not a guaranteed prediction engine.\n"
|
| 264 |
+
"- It is not hiding logic behind symbols.\n"
|
| 265 |
+
"- It is not auto-refreshing.\n\n"
|
| 266 |
+
"## Why some values look the same across locations\n"
|
| 267 |
+
"- The current seismic summary is global (past 24h). Location-aware seismic requires filtering by radius "
|
| 268 |
+
"around your typed location.\n"
|
| 269 |
+
)
|
| 270 |
|
| 271 |
if __name__ == "__main__":
|
| 272 |
demo.launch()
|