RFTSystems commited on
Commit
19d90e1
·
verified ·
1 Parent(s): f932dcc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +243 -574
app.py CHANGED
@@ -1,603 +1,272 @@
1
-
2
  # ===============================================================
3
- # Rendered Frame Theory — Live Prediction Console (Open Method)
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 httpx
14
- import numpy as np
15
- import pandas as pd
16
-
17
- APP_NAME = "Rendered Frame Theory — Live Prediction Console (Open Method)"
18
- UA = {"User-Agent": "RFTSystems/LivePredictionConsole"}
19
-
20
- T_EARTH = 365.2422 * 24 * 3600.0
21
- OMEGA_OBS = 2.0 * math.pi / T_EARTH
22
- K_TAU = 1.38
23
- ALPHA_R = 1.02
24
-
25
- REGION_BBOX = {
26
- "Global": None,
27
- "EMEA": (-35.0, -20.0, 70.0, 60.0),
28
- "AMER": (-60.0, -170.0, 72.0, -30.0),
29
- "APAC": (-50.0, 60.0, 60.0, 180.0),
30
- }
31
- RING_OF_FIRE_BBOXES = [
32
- (-60.0, 120.0, 60.0, 180.0),
33
- (-60.0, -180.0, 60.0, -100.0),
34
- (10.0, -90.0, 60.0, -60.0),
35
- ]
36
-
37
-
38
- def utc_now_iso() -> str:
39
- return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
40
-
41
-
42
- def clamp(x: float, a: float, b: float) -> float:
43
- return max(a, min(b, x))
44
-
45
-
46
- def tau_eff_from_z(z: float) -> float:
47
- z = max(0.0, float(z))
48
- return K_TAU * math.log(1.0 + z)
49
-
50
-
51
- def stable_log_ratio(x: float, x0: float) -> float:
52
- x = max(float(x), 1e-30)
53
- x0 = max(float(x0), 1e-30)
54
- return math.log(x / x0)
55
-
56
-
57
- def index_from_tau(tau: float) -> float:
58
- return float(OMEGA_OBS * float(tau) * ALPHA_R)
59
-
60
-
61
- def geocode_location(q: str):
62
- q = (q or "").strip()
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
- def fetch_goes_xray_1day():
123
- url = "https://services.swpc.noaa.gov/json/goes/primary/xrays-1-day.json"
124
- r = httpx.get(url, headers=UA, timeout=15)
125
- r.raise_for_status()
126
- js = r.json()
127
- if not isinstance(js, list) or not js:
128
- return []
129
- out = []
130
- for row in js:
131
- f = row.get("flux")
132
- if f is None:
133
- continue
134
  try:
135
- out.append(float(f))
136
  except Exception:
137
- pass
138
- return out
139
-
140
-
141
- def fetch_usgs_quakes(hours: int, minmag: float, bbox=None):
142
- url = "https://earthquake.usgs.gov/fdsnws/event/1/query"
143
- end = datetime.now(timezone.utc)
144
- start = end - timedelta(hours=int(hours))
145
- params = {
146
- "format": "geojson",
147
- "starttime": start.isoformat().replace("+00:00", "Z"),
148
- "endtime": end.isoformat().replace("+00:00", "Z"),
149
- "minmagnitude": str(float(minmag)),
150
- "orderby": "time",
151
- }
152
- if bbox is not None:
153
- minlat, minlon, maxlat, maxlon = bbox
154
- params.update(
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
- mags.append(float(m))
366
  except Exception:
367
- pass
368
- Mmax = float(max(mags)) if mags else 0.0
369
-
370
- z_count = clamp(N / 60.0, 0.0, 1.5)
371
- z_mag = clamp(max(0.0, Mmax - 4.0) / 2.5, 0.0, 1.5)
372
- z = clamp(z_count + z_mag, 0.0, 3.0)
373
- tau = tau_eff_from_z(z)
374
- idx = index_from_tau(tau)
375
-
376
- if Mmax >= 6.5 or z >= 2.2:
377
- pred = "alert"
378
- rule = "Mmax>=6.5 OR z>=2.2"
379
- elif Mmax >= 5.5 or z >= 1.5:
380
- pred = "watch"
381
- rule = "Mmax>=5.5 OR z>=1.5"
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
- atm = atmospheric_agent(lat, lon, display)
429
- except Exception as e:
430
- atm = {"enabled": False, "reason": f"atmos error: {e}"}
431
-
432
- try:
433
- sei = seismic_agent(seismic_region)
434
- except Exception as e:
435
- sei = {"enabled": False, "reason": f"seismic error: {e}"}
436
-
 
 
 
437
  try:
438
- mag = magnetic_agent()
439
- except Exception as e:
440
- mag = {"enabled": False, "reason": f"magnetic error: {e}"}
441
-
442
- try:
443
- sol = solar_agent()
444
- except Exception as e:
445
- sol = {"enabled": False, "reason": f"solar error: {e}"}
446
-
447
- def fmt_row(domain: str, out: dict):
448
- if not out.get("enabled"):
449
- return {"Domain": domain, "RFT Prediction": "DISABLED", "Live Status": out.get("reason", "missing inputs")}
450
- idx = out.get("index", None)
451
- z = out.get("z", None)
452
- tau = out.get("tau_eff", None)
453
- idx_s = f"{float(idx):.3e}" if isinstance(idx, (int, float)) else "n/a"
454
- z_s = f"{float(z):.2f}" if isinstance(z, (int, float)) else "n/a"
455
- t_s = f"{float(tau):.3f}" if isinstance(tau, (int, float)) else "n/a"
456
- return {
457
- "Domain": domain,
458
- "RFT Prediction": f"{out.get('prediction','hold')} | idx={idx_s} | z={z_s} | τ={t_s}",
459
- "Live Status": out.get("live_status", ""),
460
- }
461
 
462
- df = pd.DataFrame(
463
- [
464
- fmt_row("Atmospheric", atm),
465
- fmt_row("Seismic", sei),
466
- fmt_row("Magnetic", mag),
467
- fmt_row("Solar", sol),
468
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
469
  )
470
 
471
- ts = utc_now_iso()
472
- header = f"**Location:** {display} (lat {lat:.3f}, lon {lon:.3f}) | **UTC:** {ts}"
473
- return header, df, atm, sei, mag, sol
474
-
475
-
476
- INSTRUCTIONS_MD = """
477
- ## Use and interpretation
478
-
479
- **Location input**
480
- - Used for Atmospheric.
481
- - Not used for Solar or Magnetic (global signals).
482
- - Seismic is region-filtered (not city-level) in this build.
483
-
484
- **Seismic region**
485
- - Filters USGS earthquakes by large tectonic region.
486
- - Counts are not “near your city” unless a radius mode is implemented.
487
-
488
- **Run Forecast**
489
- - Pulls live data and recomputes from scratch.
490
- - No auto-refresh. No memory. No smoothing.
491
-
492
- **Reading the table**
493
- - RFT Prediction shows model state + index + z + τ_eff.
494
- - Live Status shows the raw physical measurements used.
495
- - DISABLED means missing/insufficient live data; no guessing is performed.
496
- """
497
-
498
- ATM_MD = """
499
- ## Atmospheric
500
- - This is a short-term stability detector from temperature swing and pressure change.
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
- loc = gr.Textbox(label="Location", value="London")
547
- gr.Markdown(
548
- "**Location input**\n\n"
549
- "- Used for Atmospheric.\n"
550
- "- Not used for Solar or Magnetic (global signals).\n"
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
- with gr.Row():
556
- region = gr.Dropdown(["Global", "EMEA", "AMER", "APAC", "RingOfFire"], value="EMEA", label="Seismic Region")
557
- gr.Markdown(
558
- "**Seismic region selector**\n\n"
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
- btn = gr.Button("Run Forecast", variant="primary")
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
- header_md = gr.Markdown()
574
  gr.Markdown(
575
- "**How to read the table**\n\n"
576
- "- RFT Prediction shows model state + index + z + τ_eff.\n"
577
- "- Live Status shows the raw physical measurements used.\n"
578
- "- DISABLED means missing/insufficient live data; no guessing is performed."
 
 
579
  )
580
- table = gr.Dataframe(headers=["Domain", "RFT Prediction", "Live Status"], interactive=False)
581
-
582
- with gr.Accordion("Atmospheric details", open=False):
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
- btn.click(run_forecast, inputs=[loc, region], outputs=[header_md, table, atm_json, sei_json, mag_json, sol_json])
596
-
597
- with gr.Tab("Method (Open)"):
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()