RFTSystems commited on
Commit
cfd6a13
·
verified ·
1 Parent(s): cba4de5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +350 -306
app.py CHANGED
@@ -1,8 +1,9 @@
1
  # app.py
2
  # ===============================================================
3
- # Rendered Frame Theory — Live Prediction Console
4
  # Domains: Atmospheric / Seismic / Magnetic / Solar
5
- # No placeholders. Single-file. Live data. RFT agents only.
 
6
  # ===============================================================
7
 
8
  import math
@@ -13,16 +14,16 @@ import httpx
13
  import numpy as np
14
  import pandas as pd
15
 
16
- APP_NAME = "Rendered Frame Theory — Live Prediction Console"
17
  UA = {"User-Agent": "RFTSystems/LivePredictionConsole"}
18
 
19
  # ---------------------------
20
- # RFT constants (from protocol style)
21
  # ---------------------------
22
- T_EARTH = 365.2422 * 24 * 3600.0 # seconds
23
- OMEGA_OBS = 2.0 * math.pi / T_EARTH # rad/s
24
- K_TAU = 1.38 # δ_r(z)=1.38*ln(1+z)
25
- ALPHA_R_DEFAULT = 1.02
26
 
27
  # ---------------------------
28
  # Regions
@@ -57,30 +58,29 @@ def stable_log_ratio(x: float, x0: float) -> float:
57
  x0 = max(float(x0), 1e-30)
58
  return math.log(x / x0)
59
 
 
 
 
60
  # ---------------------------
61
- # Live Data Adapters (no local files)
62
  # ---------------------------
63
  def geocode_location(q: str):
64
  q = (q or "").strip()
65
  if not q:
66
  return None, None, "Empty location"
67
-
68
  url = "https://geocoding-api.open-meteo.com/v1/search"
69
  params = {"name": q, "count": 1, "language": "en", "format": "json"}
70
- try:
71
- r = httpx.get(url, params=params, headers=UA, timeout=12)
72
- r.raise_for_status()
73
- js = r.json()
74
- results = js.get("results") or []
75
- if not results:
76
- return None, None, f"Could not geocode '{q}'"
77
- top = results[0]
78
- lat = float(top["latitude"])
79
- lon = float(top["longitude"])
80
- display = f"{top.get('name','')}, {top.get('country_code','')}".strip().strip(",")
81
- return lat, lon, display
82
- except Exception as e:
83
- return None, None, f"Geocode error: {e}"
84
 
85
  def fetch_openmeteo_hourly(lat: float, lon: float, past_days: int = 1):
86
  url = "https://api.open-meteo.com/v1/forecast"
@@ -105,7 +105,6 @@ def fetch_openmeteo_hourly(lat: float, lon: float, past_days: int = 1):
105
  }
106
 
107
  def fetch_kp_last_24h():
108
- # Planetary K index 1-min stream; use last 24h worth of points (we downsample)
109
  url = "https://services.swpc.noaa.gov/json/planetary_k_index_1m.json"
110
  r = httpx.get(url, headers=UA, timeout=15)
111
  r.raise_for_status()
@@ -115,12 +114,13 @@ def fetch_kp_last_24h():
115
  vals = []
116
  for row in js:
117
  kp = row.get("kp_index")
118
- if kp is not None:
119
- try:
120
- vals.append(float(kp))
121
- except Exception:
122
- pass
123
- return vals[-1440:] # ~24h of 1-min points
 
124
 
125
  def fetch_goes_xray_1day():
126
  url = "https://services.swpc.noaa.gov/json/goes/primary/xrays-1-day.json"
@@ -161,7 +161,6 @@ def fetch_usgs_quakes(hours: int, minmag: float, bbox=None):
161
  "maxlongitude": str(maxlon),
162
  }
163
  )
164
-
165
  r = httpx.get(url, params=params, headers=UA, timeout=22)
166
  r.raise_for_status()
167
  js = r.json()
@@ -171,296 +170,332 @@ def fetch_usgs_quakes(hours: int, minmag: float, bbox=None):
171
  out = []
172
  for f in feats:
173
  props = f.get("properties") or {}
174
- out.append(
175
- {
176
- "id": f.get("id"),
177
- "mag": props.get("mag"),
178
- "place": props.get("place"),
179
- "time": props.get("time"),
180
- }
181
- )
182
  return out
183
 
184
  # ---------------------------
185
- # RFT Agents (equation-driven, no heuristics outside agent)
186
  # ---------------------------
187
- def rft_magnetic_agent():
188
- """
189
- Protocol-style:
190
- Ω_obs = 2π/T_Earth
191
- define pseudo z_mag from Kp dynamics (render-stress proxy)
192
- τ_eff = 1.38 ln(1+z_mag)
193
- MagneticIndex = Ω_obs * τ_eff * α_R
194
- """
195
- try:
196
- kp = fetch_kp_last_24h()
197
- if len(kp) < 30:
198
- return {"enabled": False, "reason": "NOAA Kp feed too short"}
199
-
200
- last = float(kp[-1])
201
- tail = kp[-360:] if len(kp) >= 360 else kp # last ~6h
202
- drift = float(np.std(tail)) if len(tail) >= 10 else 0.0
203
- slope = float((tail[-1] - tail[0]) / max(1, len(tail) - 1))
204
-
205
- # pseudo z from normalized drift + level (bounded)
206
- z_mag = clamp((last / 9.0) + (drift / 2.0) + abs(slope) * 2.0, 0.0, 3.0)
207
- tau = tau_eff_from_z(z_mag)
208
- alpha_r = ALPHA_R_DEFAULT
209
- idx = OMEGA_OBS * tau * alpha_r
210
-
211
- # decision (from index magnitude bands; still inside agent)
212
- if last >= 7.0 or z_mag >= 2.0:
213
- pred = "warning"
214
- elif last >= 5.0 or z_mag >= 1.2:
215
- pred = "watch"
216
- elif last >= 4.0 or z_mag >= 0.8:
217
- pred = "monitor"
218
- else:
219
- pred = "hold"
220
-
221
- live = f"Global Kp={last:.1f} | drift={drift:.2f} | z_mag={z_mag:.2f} | τ_eff={tau:.3f}"
222
- return {
223
- "enabled": True,
224
- "prediction": pred,
225
- "rft_index": float(idx),
226
- "live_status": live,
227
- "inputs_used": {"kp_last": last, "kp_drift": drift, "kp_slope": slope},
228
- "truth_source": "NOAA SWPC planetary_k_index_1m",
229
- }
230
- except Exception as e:
231
- return {"enabled": False, "reason": f"magnetic error: {e}"}
232
-
233
- def rft_solar_agent():
234
- """
235
- Protocol-style:
236
- estimate pseudo z_solar from GOES flux relative to baseline
237
- τ_eff = 1.38 ln(1+z_solar)
238
- Φ_sun = Ω_obs * τ_eff * α_R (RFT solar flux index)
239
- """
240
- try:
241
- flux = fetch_goes_xray_1day()
242
- if len(flux) < 50:
243
- return {"enabled": False, "reason": "GOES X-ray feed too short"}
244
-
245
- # Use last 2 hours worth of points if dense, otherwise last 60 points
246
- tail = flux[-120:] if len(flux) >= 120 else flux[-60:]
247
- f_mean = float(np.mean(tail))
248
- f_max = float(np.max(tail))
249
-
250
- # Baseline: A-class floor ~1e-8 to 1e-7; use 1e-8 for ratio stability
251
- base = 1e-8
252
- lr = stable_log_ratio(f_mean, base) # ln(f/base)
253
- z_solar = clamp(lr / 10.0, 0.0, 3.0) # bounded mapping to pseudo redshift
254
- tau = tau_eff_from_z(z_solar)
255
- alpha_r = ALPHA_R_DEFAULT
256
- idx = OMEGA_OBS * tau * alpha_r
257
-
258
- # decision bands by GOES class proxy but derived from index/z inside agent
259
- if f_max >= 1e-4 or z_solar >= 2.2:
260
- pred = "flare likely"
261
- elif f_max >= 1e-5 or z_solar >= 1.5:
262
- pred = "flare watch"
263
- elif f_mean >= 1e-6 or z_solar >= 0.9:
264
- pred = "monitor"
265
- else:
266
- pred = "hold"
267
-
268
- # class label for live status
269
- if f_mean >= 1e-4:
270
- cls = "X"
271
- elif f_mean >= 1e-5:
272
- cls = "M"
273
- elif f_mean >= 1e-6:
274
- cls = "C"
275
- elif f_mean >= 1e-7:
276
- cls = "B"
277
- else:
278
- cls = "A"
279
-
280
- live = f"Global GOES mean={f_mean:.2e} ({cls}) | peak={f_max:.2e} | z_solar={z_solar:.2f} | τ_eff={tau:.3f}"
281
- return {
282
- "enabled": True,
283
- "prediction": pred,
284
- "rft_index": float(idx),
285
- "live_status": live,
286
- "inputs_used": {"flux_mean": f_mean, "flux_peak": f_max},
287
- "truth_source": "NOAA SWPC GOES primary xrays-1-day",
288
- }
289
- except Exception as e:
290
- return {"enabled": False, "reason": f"solar error: {e}"}
291
-
292
- def rft_atmospheric_agent(lat: float, lon: float, display: str):
293
- """
294
- Location-based:
295
- use last 12h ΔT + ΔP as render-stress proxy
296
- z_atm from normalized ΔT and |ΔP|
297
- τ_eff = 1.38 ln(1+z_atm)
298
- Θ_atm = Ω_obs * τ_eff * α_R
299
- """
300
- try:
301
- wx = fetch_openmeteo_hourly(lat, lon, past_days=1)
302
- temp = wx["temp"]
303
- p = wx["p"]
304
- wind = wx["wind"]
305
-
306
- if len(temp) < 13:
307
- return {"enabled": False, "reason": "Open-Meteo hourly temp too short"}
308
-
309
- t12 = [float(x) for x in temp[-13:]] # ~12h window
310
- dT = float(max(t12) - min(t12))
311
-
312
- dp = None
313
- if len(p) >= 13:
314
- p12 = [float(x) for x in p[-13:]]
315
- dp = float(p12[-1] - p12[0])
316
-
317
- w_mean = None
318
- if len(wind) >= 13:
319
- w12 = [float(x) for x in wind[-13:]]
320
- w_mean = float(np.mean(w12))
321
-
322
- # pseudo z from temp swing + pressure drop magnitude
323
- z_from_dt = clamp(dT / 10.0, 0.0, 2.0) # 10°C swing -> z~1
324
- z_from_dp = clamp((abs(dp) / 12.0) if dp is not None else 0.0, 0.0, 1.5)
325
- z_atm = clamp(z_from_dt + z_from_dp, 0.0, 3.0)
326
-
327
- tau = tau_eff_from_z(z_atm)
328
- idx = OMEGA_OBS * tau * ALPHA_R_DEFAULT
329
-
330
- # decision (inside agent)
331
- if dT >= 10 or (dp is not None and dp <= -10):
332
- pred = "storm risk"
333
- elif dT >= 7 or (dp is not None and dp <= -6):
334
- pred = "swing"
335
- elif dT >= 4:
336
- pred = "mild swing"
337
- else:
338
- pred = "stable"
339
-
340
- live_parts = [f"{display} ΔT(12h)={dT:.1f}°C", f"z_atm={z_atm:.2f}", f"τ_eff={tau:.3f}"]
341
- if dp is not None:
342
- live_parts.insert(1, f"ΔP(12h)={dp:.1f} hPa")
343
- if w_mean is not None:
344
- live_parts.append(f"wind≈{w_mean:.1f} m/s")
345
- live = " | ".join(live_parts)
346
 
347
- return {
348
- "enabled": True,
349
- "prediction": pred,
350
- "rft_index": float(idx),
351
- "live_status": live,
352
- "inputs_used": {"dT_12h": dT, "dP_12h": dp, "wind_mean": w_mean},
353
- "truth_source": "Open-Meteo hourly (temp/rh/pressure/wind)",
354
- }
355
- except Exception as e:
356
- return {"enabled": False, "reason": f"atmos error: {e}"}
357
-
358
- def rft_seismic_agent(region: str):
359
- """
360
- Region-based:
361
- use USGS counts/max magnitude as the live measurable driver
362
- build Ψ_quake proxy index:
363
- Ψ = Ω_obs * τ_eff(z_seis) * α_R
364
- where z_seis grows with event density + max magnitude
365
- """
366
- try:
367
- if region == "RingOfFire":
368
- seen = set()
369
- eqs = []
370
- for bb in RING_OF_FIRE_BBOXES:
371
- chunk = fetch_usgs_quakes(hours=24, minmag=2.5, bbox=bb)
372
- for e in chunk:
373
- eid = e.get("id")
374
- if eid and eid not in seen:
375
- seen.add(eid)
376
- eqs.append(e)
377
- else:
378
- bbox = REGION_BBOX.get(region, None)
379
- eqs = fetch_usgs_quakes(hours=24, minmag=2.5, bbox=bbox)
380
-
381
- count = len(eqs)
382
- mags = []
383
- for e in eqs:
384
- m = e.get("mag")
385
- if m is not None:
386
- try:
387
- mags.append(float(m))
388
- except Exception:
389
- pass
390
- max_mag = float(max(mags)) if mags else 0.0
391
-
392
- # z_seis from density + magnitude (bounded)
393
- z_from_count = clamp(count / 60.0, 0.0, 1.5) # 60 events -> z~1
394
- z_from_mag = clamp(max(0.0, max_mag - 4.0) / 2.5, 0.0, 1.5) # M6.5 -> +1
395
- z_seis = clamp(z_from_count + z_from_mag, 0.0, 3.0)
396
-
397
- tau = tau_eff_from_z(z_seis)
398
- idx = OMEGA_OBS * tau * ALPHA_R_DEFAULT
399
-
400
- # decision (inside agent)
401
- if max_mag >= 6.5 or z_seis >= 2.2:
402
- pred = "alert"
403
- elif max_mag >= 5.5 or z_seis >= 1.5:
404
- pred = "watch"
405
- elif count >= 25 or z_seis >= 1.0:
406
- pred = "monitor"
407
- else:
408
- pred = "quiet"
409
-
410
- scope = "Global" if region == "Global" else region
411
- live = f"{scope} quakes(24h,M≥2.5)={count} | max M{max_mag:.1f} | z_seis={z_seis:.2f} | τ_eff={tau:.3f}"
412
- return {
413
- "enabled": True,
414
- "prediction": pred,
415
- "rft_index": float(idx),
416
- "live_status": live,
417
- "inputs_used": {"count": count, "max_mag": max_mag},
418
- "truth_source": "USGS FDSN event geojson",
419
- }
420
- except Exception as e:
421
- return {"enabled": False, "reason": f"seismic error: {e}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
422
 
423
  # ---------------------------
424
  # Orchestrator
425
  # ---------------------------
426
  def run_forecast(location_text: str, seismic_region: str):
427
- lat, lon, display = geocode_location(location_text)
 
 
 
 
 
 
428
  if lat is None:
429
  df = pd.DataFrame([{"Domain": "Error", "RFT Prediction": "DISABLED", "Live Status": display}])
430
- return f"❌ {display}", df
 
431
 
432
- mag = rft_magnetic_agent()
433
- sol = rft_solar_agent()
434
- atm = rft_atmospheric_agent(lat, lon, display)
435
- sei = rft_seismic_agent(seismic_region)
436
 
437
- def row(domain: str, out: dict):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
  if not out.get("enabled"):
439
- return {
440
- "Domain": domain,
441
- "RFT Prediction": "DISABLED",
442
- "Live Status": out.get("reason", "missing inputs"),
443
- }
444
- idx = out.get("rft_index")
445
- idx_s = f"{idx:.3e}" if isinstance(idx, (int, float)) else "n/a"
446
  return {
447
  "Domain": domain,
448
- "RFT Prediction": f"{out.get('prediction','hold')} | idx={idx_s}",
449
  "Live Status": out.get("live_status", ""),
450
  }
451
 
452
  df = pd.DataFrame(
453
  [
454
- row("Atmospheric", atm),
455
- row("Seismic", sei),
456
- row("Magnetic", mag),
457
- row("Solar", sol),
458
  ]
459
  )
460
 
461
  ts = utc_now_iso()
462
  header = f"**Location:** {display} (lat {lat:.3f}, lon {lon:.3f}) | **UTC:** {ts}"
463
- return header, df
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
 
465
  # ---------------------------
466
  # UI
@@ -468,19 +503,28 @@ def run_forecast(location_text: str, seismic_region: str):
468
  with gr.Blocks(title=APP_NAME) as demo:
469
  gr.Markdown(f"# {APP_NAME}")
470
 
471
- with gr.Row():
472
- loc = gr.Textbox(label="Location", value="London")
473
- region = gr.Dropdown(
474
- ["Global", "EMEA", "AMER", "APAC", "RingOfFire"],
475
- value="EMEA",
476
- label="Seismic Region",
477
- )
 
 
 
 
 
 
 
 
 
 
478
 
479
- btn = gr.Button("Run Forecast", variant="primary")
480
- header_md = gr.Markdown()
481
- table = gr.Dataframe(headers=["Domain", "RFT Prediction", "Live Status"], interactive=False)
482
 
483
- btn.click(run_forecast, inputs=[loc, region], outputs=[header_md, table])
 
484
 
485
  if __name__ == "__main__":
486
  demo.launch()
 
1
  # app.py
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. No placeholders. No external local modules.
7
  # ===============================================================
8
 
9
  import math
 
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
  # ---------------------------
21
+ # Core constants
22
  # ---------------------------
23
+ T_EARTH = 365.2422 * 24 * 3600.0
24
+ OMEGA_OBS = 2.0 * math.pi / T_EARTH
25
+ K_TAU = 1.38
26
+ ALPHA_R = 1.02
27
 
28
  # ---------------------------
29
  # Regions
 
58
  x0 = max(float(x0), 1e-30)
59
  return math.log(x / x0)
60
 
61
+ def index_from_tau(tau: float) -> float:
62
+ return float(OMEGA_OBS * float(tau) * ALPHA_R)
63
+
64
  # ---------------------------
65
+ # Live adapters
66
  # ---------------------------
67
  def geocode_location(q: str):
68
  q = (q or "").strip()
69
  if not q:
70
  return None, None, "Empty location"
 
71
  url = "https://geocoding-api.open-meteo.com/v1/search"
72
  params = {"name": q, "count": 1, "language": "en", "format": "json"}
73
+ r = httpx.get(url, params=params, headers=UA, timeout=12)
74
+ r.raise_for_status()
75
+ js = r.json()
76
+ results = js.get("results") or []
77
+ if not results:
78
+ return None, None, f"Could not geocode '{q}'"
79
+ top = results[0]
80
+ lat = float(top["latitude"])
81
+ lon = float(top["longitude"])
82
+ display = f"{top.get('name','')}, {top.get('country_code','')}".strip().strip(",")
83
+ return lat, lon, display
 
 
 
84
 
85
  def fetch_openmeteo_hourly(lat: float, lon: float, past_days: int = 1):
86
  url = "https://api.open-meteo.com/v1/forecast"
 
105
  }
106
 
107
  def fetch_kp_last_24h():
 
108
  url = "https://services.swpc.noaa.gov/json/planetary_k_index_1m.json"
109
  r = httpx.get(url, headers=UA, timeout=15)
110
  r.raise_for_status()
 
114
  vals = []
115
  for row in js:
116
  kp = row.get("kp_index")
117
+ if kp is None:
118
+ continue
119
+ try:
120
+ vals.append(float(kp))
121
+ except Exception:
122
+ pass
123
+ return vals[-1440:]
124
 
125
  def fetch_goes_xray_1day():
126
  url = "https://services.swpc.noaa.gov/json/goes/primary/xrays-1-day.json"
 
161
  "maxlongitude": str(maxlon),
162
  }
163
  )
 
164
  r = httpx.get(url, params=params, headers=UA, timeout=22)
165
  r.raise_for_status()
166
  js = r.json()
 
170
  out = []
171
  for f in feats:
172
  props = f.get("properties") or {}
173
+ out.append({"id": f.get("id"), "mag": props.get("mag"), "place": props.get("place"), "time": props.get("time")})
 
 
 
 
 
 
 
174
  return out
175
 
176
  # ---------------------------
177
+ # Agents
178
  # ---------------------------
179
+ def magnetic_agent():
180
+ kp = fetch_kp_last_24h()
181
+ if len(kp) < 30:
182
+ return {"enabled": False, "reason": "NOAA Kp feed too short"}
183
+ last = float(kp[-1])
184
+ tail = kp[-360:] if len(kp) >= 360 else kp
185
+ drift = float(np.std(tail)) if len(tail) >= 10 else 0.0
186
+ slope = float((tail[-1] - tail[0]) / max(1, len(tail) - 1))
187
+
188
+ z = clamp((last / 9.0) + (drift / 2.0) + 2.0 * abs(slope), 0.0, 3.0)
189
+ tau = tau_eff_from_z(z)
190
+ idx = index_from_tau(tau)
191
+
192
+ if last >= 7.0 or z >= 2.0:
193
+ pred = "warning"
194
+ rule = "Kp>=7 OR z>=2.0"
195
+ elif last >= 5.0 or z >= 1.2:
196
+ pred = "watch"
197
+ rule = "Kp>=5 OR z>=1.2"
198
+ elif last >= 4.0 or z >= 0.8:
199
+ pred = "monitor"
200
+ rule = "Kp>=4 OR z>=0.8"
201
+ else:
202
+ pred = "hold"
203
+ rule = "else"
204
+
205
+ live = f"Global Kp={last:.1f} | drift={drift:.2f} | slope={slope:.4f}"
206
+ return {
207
+ "enabled": True,
208
+ "domain": "Magnetic",
209
+ "prediction": pred,
210
+ "rule_fired": rule,
211
+ "z": float(z),
212
+ "tau_eff": float(tau),
213
+ "omega_obs": float(OMEGA_OBS),
214
+ "alpha_r": float(ALPHA_R),
215
+ "index": float(idx),
216
+ "live_status": live,
217
+ "truth_source": "NOAA SWPC planetary_k_index_1m (global)",
218
+ "inputs_used": {"kp_last": last, "kp_drift": drift, "kp_slope": slope, "tail_len": len(tail)},
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
+ def solar_agent():
224
+ flux = fetch_goes_xray_1day()
225
+ if len(flux) < 50:
226
+ return {"enabled": False, "reason": "GOES X-ray feed too short"}
227
+ tail = flux[-120:] if len(flux) >= 120 else flux[-60:]
228
+ f_mean = float(np.mean(tail))
229
+ f_peak = float(np.max(tail))
230
+
231
+ lr = stable_log_ratio(f_mean, 1e-8)
232
+ z = clamp(lr / 10.0, 0.0, 3.0)
233
+ tau = tau_eff_from_z(z)
234
+ idx = index_from_tau(tau)
235
+
236
+ if f_peak >= 1e-4 or z >= 2.2:
237
+ pred = "flare likely"
238
+ rule = "peak>=1e-4 OR z>=2.2"
239
+ elif f_peak >= 1e-5 or z >= 1.5:
240
+ pred = "flare watch"
241
+ rule = "peak>=1e-5 OR z>=1.5"
242
+ elif f_mean >= 1e-6 or z >= 0.9:
243
+ pred = "monitor"
244
+ rule = "mean>=1e-6 OR z>=0.9"
245
+ else:
246
+ pred = "hold"
247
+ rule = "else"
248
+
249
+ live = f"Global GOES mean={f_mean:.2e} | peak={f_peak:.2e}"
250
+ return {
251
+ "enabled": True,
252
+ "domain": "Solar",
253
+ "prediction": pred,
254
+ "rule_fired": rule,
255
+ "z": float(z),
256
+ "tau_eff": float(tau),
257
+ "omega_obs": float(OMEGA_OBS),
258
+ "alpha_r": float(ALPHA_R),
259
+ "index": float(idx),
260
+ "live_status": live,
261
+ "truth_source": "NOAA SWPC GOES primary xrays-1-day (global)",
262
+ "inputs_used": {"flux_mean": f_mean, "flux_peak": f_peak, "tail_len": len(tail)},
263
+ "why": "z_solar is derived from flux relative to baseline via a log ratio; τ_eff rises as ln(1+z).",
264
+ "how": "Fetch GOES flux → mean/peak → z_solar=clamp(ln(F_mean/1e-8)/10) → τ_eff → Index → label via fixed thresholds.",
265
+ }
266
+
267
+ def atmospheric_agent(lat: float, lon: float, display: str):
268
+ wx = fetch_openmeteo_hourly(lat, lon, past_days=1)
269
+ temp = wx["temp"]
270
+ p = wx["p"]
271
+ wind = wx["wind"]
272
+
273
+ if len(temp) < 13:
274
+ return {"enabled": False, "reason": "Open-Meteo hourly series too short"}
275
+
276
+ t12 = [float(x) for x in temp[-13:]]
277
+ dT = float(max(t12) - min(t12))
278
+
279
+ dp = None
280
+ if len(p) >= 13:
281
+ p12 = [float(x) for x in p[-13:]]
282
+ dp = float(p12[-1] - p12[0])
283
+
284
+ w_mean = None
285
+ if len(wind) >= 13:
286
+ w12 = [float(x) for x in wind[-13:]]
287
+ w_mean = float(np.mean(w12))
288
+
289
+ z_dt = clamp(dT / 10.0, 0.0, 2.0)
290
+ z_dp = clamp((abs(dp) / 12.0) if dp is not None else 0.0, 0.0, 1.5)
291
+ z = clamp(z_dt + z_dp, 0.0, 3.0)
292
+ tau = tau_eff_from_z(z)
293
+ idx = index_from_tau(tau)
294
+
295
+ if dT >= 10.0 or (dp is not None and dp <= -10.0):
296
+ pred = "storm risk"
297
+ rule = "ΔT>=10 OR ΔP<=-10"
298
+ elif dT >= 7.0 or (dp is not None and dp <= -6.0):
299
+ pred = "swing"
300
+ rule = "ΔT>=7 OR ΔP<=-6"
301
+ elif dT >= 4.0:
302
+ pred = "mild swing"
303
+ rule = "ΔT>=4"
304
+ else:
305
+ pred = "stable"
306
+ rule = "else"
307
+
308
+ parts = [f"{display} ΔT(12h)={dT:.1f}°C"]
309
+ if dp is not None:
310
+ parts.append(f"ΔP(12h)={dp:.1f} hPa")
311
+ if w_mean is not None:
312
+ parts.append(f"wind≈{w_mean:.1f} m/s")
313
+ live = " | ".join(parts)
314
+
315
+ return {
316
+ "enabled": True,
317
+ "domain": "Atmospheric",
318
+ "prediction": pred,
319
+ "rule_fired": rule,
320
+ "z": float(z),
321
+ "tau_eff": float(tau),
322
+ "omega_obs": float(OMEGA_OBS),
323
+ "alpha_r": float(ALPHA_R),
324
+ "index": float(idx),
325
+ "live_status": live,
326
+ "truth_source": "Open-Meteo hourly (location-based)",
327
+ "inputs_used": {"dT_12h": dT, "dP_12h": dp, "wind_mean": w_mean, "lat": lat, "lon": lon},
328
+ "why": "z_atm is derived from thermal swing and pressure change as a bounded stress coordinate; τ_eff rises as ln(1+z).",
329
+ "how": "Geocode → fetch hourly series → compute ΔT and ΔP (last ~12h) → z_atm → τ_eff → Index → label via fixed thresholds.",
330
+ }
331
+
332
+ def seismic_agent(region: str):
333
+ if region == "RingOfFire":
334
+ seen = set()
335
+ eqs = []
336
+ for bb in RING_OF_FIRE_BBOXES:
337
+ chunk = fetch_usgs_quakes(hours=24, minmag=2.5, bbox=bb)
338
+ for e in chunk:
339
+ eid = e.get("id")
340
+ if eid and eid not in seen:
341
+ seen.add(eid)
342
+ eqs.append(e)
343
+ else:
344
+ bbox = REGION_BBOX.get(region, None)
345
+ eqs = fetch_usgs_quakes(hours=24, minmag=2.5, bbox=bbox)
346
+
347
+ N = int(len(eqs))
348
+ mags = []
349
+ for e in eqs:
350
+ m = e.get("mag")
351
+ if m is None:
352
+ continue
353
+ try:
354
+ mags.append(float(m))
355
+ except Exception:
356
+ pass
357
+ Mmax = float(max(mags)) if mags else 0.0
358
+
359
+ z_count = clamp(N / 60.0, 0.0, 1.5)
360
+ z_mag = clamp(max(0.0, Mmax - 4.0) / 2.5, 0.0, 1.5)
361
+ z = clamp(z_count + z_mag, 0.0, 3.0)
362
+ tau = tau_eff_from_z(z)
363
+ idx = index_from_tau(tau)
364
+
365
+ if Mmax >= 6.5 or z >= 2.2:
366
+ pred = "alert"
367
+ rule = "Mmax>=6.5 OR z>=2.2"
368
+ elif Mmax >= 5.5 or z >= 1.5:
369
+ pred = "watch"
370
+ rule = "Mmax>=5.5 OR z>=1.5"
371
+ elif N >= 25 or z >= 1.0:
372
+ pred = "monitor"
373
+ rule = "N>=25 OR z>=1.0"
374
+ else:
375
+ pred = "quiet"
376
+ rule = "else"
377
+
378
+ scope = "Global" if region == "Global" else region
379
+ live = f"{scope} quakes(24h,M≥2.5)={N} | max M{Mmax:.1f}"
380
+
381
+ return {
382
+ "enabled": True,
383
+ "domain": "Seismic",
384
+ "prediction": pred,
385
+ "rule_fired": rule,
386
+ "z": float(z),
387
+ "tau_eff": float(tau),
388
+ "omega_obs": float(OMEGA_OBS),
389
+ "alpha_r": float(ALPHA_R),
390
+ "index": float(idx),
391
+ "live_status": live,
392
+ "truth_source": "USGS FDSN event feed (region-filtered)",
393
+ "inputs_used": {"count_24h": N, "max_mag_24h": Mmax, "region": region},
394
+ "why": "z_seis compresses activity density and severity into a bounded stress coordinate; τ_eff rises as ln(1+z).",
395
+ "how": "Fetch USGS in region → count + max magnitude → z_seis → τ_eff → Index → label via fixed thresholds.",
396
+ }
397
 
398
  # ---------------------------
399
  # Orchestrator
400
  # ---------------------------
401
  def run_forecast(location_text: str, seismic_region: str):
402
+ try:
403
+ lat, lon, display = geocode_location(location_text)
404
+ except Exception as e:
405
+ df = pd.DataFrame([{"Domain": "Error", "RFT Prediction": "DISABLED", "Live Status": f"Geocode error: {e}"}])
406
+ empty = {"enabled": False, "reason": f"Geocode error: {e}"}
407
+ return f"❌ Geocode error: {e}", df, empty, empty, empty, empty
408
+
409
  if lat is None:
410
  df = pd.DataFrame([{"Domain": "Error", "RFT Prediction": "DISABLED", "Live Status": display}])
411
+ empty = {"enabled": False, "reason": display}
412
+ return f"❌ {display}", df, empty, empty, empty, empty
413
 
414
+ try:
415
+ atm = atmospheric_agent(lat, lon, display)
416
+ except Exception as e:
417
+ atm = {"enabled": False, "reason": f"atmos error: {e}"}
418
 
419
+ try:
420
+ sei = seismic_agent(seismic_region)
421
+ except Exception as e:
422
+ sei = {"enabled": False, "reason": f"seismic error: {e}"}
423
+
424
+ try:
425
+ mag = magnetic_agent()
426
+ except Exception as e:
427
+ mag = {"enabled": False, "reason": f"magnetic error: {e}"}
428
+
429
+ try:
430
+ sol = solar_agent()
431
+ except Exception as e:
432
+ sol = {"enabled": False, "reason": f"solar error: {e}"}
433
+
434
+ def fmt_row(domain: str, out: dict):
435
  if not out.get("enabled"):
436
+ return {"Domain": domain, "RFT Prediction": "DISABLED", "Live Status": out.get("reason", "missing inputs")}
437
+ idx = out.get("index", None)
438
+ z = out.get("z", None)
439
+ tau = out.get("tau_eff", None)
440
+ idx_s = f"{float(idx):.3e}" if isinstance(idx, (int, float)) else "n/a"
441
+ z_s = f"{float(z):.2f}" if isinstance(z, (int, float)) else "n/a"
442
+ t_s = f"{float(tau):.3f}" if isinstance(tau, (int, float)) else "n/a"
443
  return {
444
  "Domain": domain,
445
+ "RFT Prediction": f"{out.get('prediction','hold')} | idx={idx_s} | z={z_s} | τ={t_s}",
446
  "Live Status": out.get("live_status", ""),
447
  }
448
 
449
  df = pd.DataFrame(
450
  [
451
+ fmt_row("Atmospheric", atm),
452
+ fmt_row("Seismic", sei),
453
+ fmt_row("Magnetic", mag),
454
+ fmt_row("Solar", sol),
455
  ]
456
  )
457
 
458
  ts = utc_now_iso()
459
  header = f"**Location:** {display} (lat {lat:.3f}, lon {lon:.3f}) | **UTC:** {ts}"
460
+ return header, df, atm, sei, mag, sol
461
+
462
+ # ---------------------------
463
+ # Open Method
464
+ # ---------------------------
465
+ METHOD_MD = f"""
466
+ ## Open method
467
+
468
+ **Shared core:**
469
+ - `τ_eff = {K_TAU} · ln(1 + z)`
470
+ - `Ω_obs = 2π / T_earth = {OMEGA_OBS:.6e}`
471
+ - `α_R = {ALPHA_R}`
472
+ - `Index = Ω_obs · τ_eff · α_R`
473
+
474
+ ### Atmospheric (Open-Meteo, location)
475
+ - Inputs: hourly temp, pressure, wind (last ~12h).
476
+ - `ΔT = max(T) - min(T)`
477
+ - `ΔP = P_end - P_start` (if available)
478
+ - `z_atm = clamp( clamp(ΔT/10,0..2) + clamp(|ΔP|/12,0..1.5), 0..3 )`
479
+ - Label rule in agent output.
480
+
481
+ ### Seismic (USGS, region)
482
+ - Inputs: USGS events in last 24h (M≥2.5), filtered by region.
483
+ - Count `N`, max magnitude `Mmax`
484
+ - `z_seis = clamp( clamp(N/60,0..1.5) + clamp(max(0,Mmax-4)/2.5,0..1.5), 0..3 )`
485
+ - Label rule in agent output.
486
+
487
+ ### Magnetic (NOAA Kp, global)
488
+ - Inputs: planetary Kp 1-min stream.
489
+ - `z_mag = clamp( (Kp_last/9) + (drift/2) + 2·|slope|, 0..3 )`
490
+ - Label rule in agent output.
491
+
492
+ ### Solar (GOES X-ray, global)
493
+ - Inputs: GOES 1–8Å flux (1-day stream).
494
+ - `z_solar = clamp( ln(F_mean/1e-8) / 10, 0..3 )`
495
+ - Label rule in agent output.
496
+
497
+ Missing/short feeds show **DISABLED**.
498
+ """
499
 
500
  # ---------------------------
501
  # UI
 
503
  with gr.Blocks(title=APP_NAME) as demo:
504
  gr.Markdown(f"# {APP_NAME}")
505
 
506
+ with gr.Tab("Live Forecast"):
507
+ with gr.Row():
508
+ loc = gr.Textbox(label="Location", value="London")
509
+ region = gr.Dropdown(["Global", "EMEA", "AMER", "APAC", "RingOfFire"], value="EMEA", label="Seismic Region")
510
+
511
+ btn = gr.Button("Run Forecast", variant="primary")
512
+ header_md = gr.Markdown()
513
+ table = gr.Dataframe(headers=["Domain", "RFT Prediction", "Live Status"], interactive=False)
514
+
515
+ with gr.Accordion("Atmospheric details", open=False):
516
+ atm_json = gr.JSON(label="Atmospheric agent output")
517
+ with gr.Accordion("Seismic details", open=False):
518
+ sei_json = gr.JSON(label="Seismic agent output")
519
+ with gr.Accordion("Magnetic details", open=False):
520
+ mag_json = gr.JSON(label="Magnetic agent output")
521
+ with gr.Accordion("Solar details", open=False):
522
+ sol_json = gr.JSON(label="Solar agent output")
523
 
524
+ btn.click(run_forecast, inputs=[loc, region], outputs=[header_md, table, atm_json, sei_json, mag_json, sol_json])
 
 
525
 
526
+ with gr.Tab("Method (Open)"):
527
+ gr.Markdown(METHOD_MD)
528
 
529
  if __name__ == "__main__":
530
  demo.launch()