Spaces:
Sleeping
Sleeping
Upload app.py with huggingface_hub
Browse files
app.py
CHANGED
|
@@ -1,7 +1,15 @@
|
|
| 1 |
"""
|
| 2 |
-
AsteroidNET
|
| 3 |
-
Five
|
| 4 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import gradio as gr
|
| 6 |
import numpy as np
|
| 7 |
import pandas as pd
|
|
@@ -9,12 +17,13 @@ import matplotlib
|
|
| 9 |
matplotlib.use("Agg")
|
| 10 |
import matplotlib.pyplot as plt
|
| 11 |
from matplotlib.lines import Line2D
|
| 12 |
-
import io, base64, math, warnings
|
| 13 |
from astropy.time import Time
|
| 14 |
import astropy.units as u
|
|
|
|
| 15 |
warnings.filterwarnings("ignore")
|
|
|
|
| 16 |
|
| 17 |
-
# ── Palette ─────────────────────────────────
|
| 18 |
BG = "#04060D"
|
| 19 |
PANEL = "#0A0E1A"
|
| 20 |
ACCENT = "#00D4FF"
|
|
@@ -25,46 +34,50 @@ SUBTLE = "#1E2D40"
|
|
| 25 |
TEXT = "#C8D8E8"
|
| 26 |
DIM = "#4A6070"
|
| 27 |
|
| 28 |
-
CSS =
|
| 29 |
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&display=swap');
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
.
|
| 33 |
-
.
|
| 34 |
-
.
|
| 35 |
-
.
|
| 36 |
-
.
|
| 37 |
-
.
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
.
|
| 41 |
-
.
|
| 42 |
-
.
|
| 43 |
-
.
|
| 44 |
-
.
|
| 45 |
-
.
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
input[type=range]{{accent-color:{ACCENT}!important;}}
|
| 51 |
-
.stat-grid{{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:9px;margin:14px 0;}}
|
| 52 |
-
.stat-card{{background:{SUBTLE};border:1px solid #1E3048;border-radius:4px;padding:13px 14px;text-align:center;}}
|
| 53 |
-
.stat-val{{font-family:'Space Mono',monospace;font-size:1.5rem;font-weight:700;line-height:1;color:{ACCENT};}}
|
| 54 |
-
.stat-val.a{{color:{ACC2};}}.stat-val.g{{color:{OK};}}.stat-val.w{{color:{WARN};}}
|
| 55 |
-
.stat-label{{font-family:'Space Mono',monospace;font-size:.58rem;letter-spacing:.1em;text-transform:uppercase;color:{DIM};margin-top:4px;}}
|
| 56 |
-
.mpc-wrap{{background:#000814;border:1px solid {ACCENT}55;border-radius:4px;padding:14px 18px;font-family:'Space Mono',monospace;font-size:.86rem;color:{ACCENT};word-break:break-all;}}
|
| 57 |
-
.mpc-ruler{{font-family:'Space Mono',monospace;font-size:.58rem;color:{DIM};letter-spacing:.05em;margin-bottom:3px;}}
|
| 58 |
-
.alert-ok{{background:{OK}14;border:1px solid {OK}44;color:{OK};border-radius:4px;padding:10px 14px;font-family:'Space Mono',monospace;font-size:.72rem;margin:6px 0;}}
|
| 59 |
-
.alert-warn{{background:{WARN}14;border:1px solid {WARN}44;color:{WARN};border-radius:4px;padding:10px 14px;font-family:'Space Mono',monospace;font-size:.72rem;margin:6px 0;}}
|
| 60 |
-
.alert-err{{background:#FF334414;border:1px solid #FF334444;color:#FF6666;border-radius:4px;padding:10px 14px;font-family:'Space Mono',monospace;font-size:.72rem;margin:6px 0;}}
|
| 61 |
-
.alert-info{{background:{ACCENT}10;border:1px solid {ACCENT}44;color:{ACCENT};border-radius:4px;padding:10px 14px;font-family:'Space Mono',monospace;font-size:.72rem;margin:6px 0;}}
|
| 62 |
-
.gr-dataframe table{{background:var(--panel)!important;font-family:'Space Mono',monospace!important;font-size:.7rem!important;}}
|
| 63 |
-
.gr-dataframe th{{background:{SUBTLE}!important;color:{ACCENT}!important;text-transform:uppercase;letter-spacing:.06em;}}
|
| 64 |
-
.gr-dataframe td{{color:var(--text)!important;}}
|
| 65 |
"""
|
| 66 |
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
def fig_to_b64(fig):
|
| 69 |
buf = io.BytesIO()
|
| 70 |
fig.savefig(buf, format="png", dpi=130, bbox_inches="tight",
|
|
@@ -72,491 +85,517 @@ def fig_to_b64(fig):
|
|
| 72 |
buf.seek(0)
|
| 73 |
b64 = base64.b64encode(buf.read()).decode()
|
| 74 |
plt.close(fig)
|
| 75 |
-
return
|
|
|
|
| 76 |
|
| 77 |
def dark_fig(w=9, h=5):
|
| 78 |
fig, ax = plt.subplots(figsize=(w, h))
|
| 79 |
fig.patch.set_facecolor(BG)
|
| 80 |
ax.set_facecolor(PANEL)
|
| 81 |
ax.tick_params(colors=DIM, labelsize=7)
|
| 82 |
-
for sp in ax.spines.values():
|
|
|
|
| 83 |
ax.grid(color=SUBTLE, linewidth=0.4, alpha=0.5)
|
| 84 |
return fig, ax
|
| 85 |
|
|
|
|
| 86 |
def img_html(b64, label=""):
|
| 87 |
-
lbl =
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
ax.set_facecolor("#020812")
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
ax.legend(handles=[
|
| 148 |
-
Line2D([0],[0],marker="o",color="w",markerfacecolor=ACC2,ms=6,lw=0,label="Fast >2″/s"),
|
| 149 |
-
Line2D([0],[0],marker="o",color="w",markerfacecolor=WARN,ms=6,lw=0,label="Mid 1–2″/s"),
|
| 150 |
-
Line2D([0],[0],marker="o",color="w",markerfacecolor=ACCENT,ms=6,lw=0,label="Slow <1″/s"),
|
| 151 |
-
], framealpha=0, labelcolor=DIM, fontsize=7, loc="upper right", prop={"family":"monospace"})
|
| 152 |
ax.set_xlabel("RA (°)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 153 |
ax.set_ylabel("Dec (°)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 154 |
-
ax.set_title("
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
-
# ── Completeness curve ───────────────────
|
| 159 |
snr_x = np.linspace(3, 20, 60)
|
| 160 |
-
comp = np.clip(1/(1+np.exp(-(snr_x-(det_thresh+1.5))*1.2))
|
| 161 |
-
+ rng.normal(0,.012,len(snr_x)), 0, 1)
|
| 162 |
fig2, ax2 = dark_fig(7, 3.8)
|
| 163 |
-
ax2.plot(snr_x, comp*100, color=ACCENT, lw=2
|
| 164 |
-
ax2.axvline(det_thresh, color=ACC2, lw=1.2, ls="--", label=
|
| 165 |
-
ax2.axhline(90, color=OK, lw=0.8, ls=":", alpha=0.7
|
| 166 |
-
ax2.fill_between(snr_x, comp*100, alpha=0.1, color=ACCENT)
|
| 167 |
ax2.set_xlabel("SNR", color=DIM, fontfamily="monospace", fontsize=7)
|
| 168 |
ax2.set_ylabel("Recovery (%)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 169 |
ax2.set_title("Completeness vs SNR", color=TEXT, fontfamily="monospace", fontsize=9)
|
| 170 |
-
ax2.
|
| 171 |
-
ax2.legend(framealpha=0, labelcolor=DIM, fontsize=7, prop={"family":"monospace"})
|
| 172 |
-
comp_b64 = fig_to_b64(fig2)
|
| 173 |
-
|
| 174 |
-
# ── Velocity histogram ───────────────────
|
| 175 |
-
fig3, ax3 = dark_fig(7, 3.5)
|
| 176 |
-
ax3.hist(rng.uniform(vel_min,vel_max,n_ast*3), bins=25, color=SUBTLE, alpha=0.7, label="All candidates")
|
| 177 |
-
ax3.hist(rng.uniform(vel_min,vel_max,max(1,confirmed_real)), bins=20, color=ACCENT, alpha=0.9, label="Confirmed")
|
| 178 |
-
ax3.axvline(1.0, color=WARN, lw=1, ls="--", label="HIGH threshold")
|
| 179 |
-
ax3.axvline(3.0, color=ACC2, lw=1, ls="--", label="HAZARDOUS threshold")
|
| 180 |
-
ax3.set_xlabel("Velocity (″/s)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 181 |
-
ax3.set_ylabel("Count", color=DIM, fontfamily="monospace", fontsize=7)
|
| 182 |
-
ax3.set_title("Velocity Distribution", color=TEXT, fontfamily="monospace", fontsize=9)
|
| 183 |
-
ax3.legend(framealpha=0, labelcolor=DIM, fontsize=7, prop={"family":"monospace"})
|
| 184 |
-
vel_b64 = fig_to_b64(fig3)
|
| 185 |
-
|
| 186 |
-
# ── Stats HTML ───────────────────────────
|
| 187 |
-
fp_pct = round(false_pos/max(confirmed,1)*100, 2)
|
| 188 |
-
rec_pct = round(confirmed_real/max(n_ast,1)*100, 1)
|
| 189 |
-
n_haz = sum(1 for r in rows if r["Priority"]=="HAZARDOUS")
|
| 190 |
-
n_high = sum(1 for r in rows if r["Priority"]=="HIGH")
|
| 191 |
-
|
| 192 |
-
charts_html = (
|
| 193 |
-
img_html(sky_b64, "Sky Motion Field") +
|
| 194 |
-
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px">' +
|
| 195 |
-
img_html(comp_b64, "Completeness Curve") +
|
| 196 |
-
img_html(vel_b64, "Velocity Distribution") +
|
| 197 |
-
'</div>'
|
| 198 |
-
)
|
| 199 |
|
| 200 |
-
|
| 201 |
-
<div class="stat-grid">
|
| 202 |
-
<div class="stat-card"><div class="stat-val">{n_frm}</div><div class="stat-label">Frames</div></div>
|
| 203 |
-
<div class="stat-card"><div class="stat-val a">{n_stars:,}</div><div class="stat-label">Stars removed</div></div>
|
| 204 |
-
<div class="stat-card"><div class="stat-val">{valid_trklets}</div><div class="stat-label">Tracklets</div></div>
|
| 205 |
-
<div class="stat-card"><div class="stat-val g">{confirmed_real}</div><div class="stat-label">Confirmed</div></div>
|
| 206 |
-
<div class="stat-card"><div class="stat-val w">{rec_pct}%</div><div class="stat-label">Recovery</div></div>
|
| 207 |
-
<div class="stat-card"><div class="stat-val {'a' if fp_pct>1 else 'g'}">{fp_pct}%</div><div class="stat-label">False positive</div></div>
|
| 208 |
-
<div class="stat-card"><div class="stat-val a">{n_high}</div><div class="stat-label">HIGH alerts</div></div>
|
| 209 |
-
<div class="stat-card"><div class="stat-val" style="color:#FF4444">{n_haz}</div><div class="stat-label">HAZARDOUS</div></div>
|
| 210 |
-
</div>
|
| 211 |
-
<div class="{'alert-ok' if rec_pct>=90 else 'alert-warn'}">
|
| 212 |
-
{'✓ SC-001 PASS — Recovery '+str(rec_pct)+'% ≥ 90%' if rec_pct>=90 else '⚠ SC-001 — Recovery '+str(rec_pct)+'% below 90% target'}
|
| 213 |
-
</div>
|
| 214 |
-
<div class="{'alert-ok' if fp_pct<=1 else 'alert-warn'}">
|
| 215 |
-
{'✓ SC-002 PASS — False positive '+str(fp_pct)+'% ≤ 1%' if fp_pct<=1 else '⚠ SC-002 — False positive '+str(fp_pct)+'% exceeds 1%'}
|
| 216 |
-
</div>"""
|
| 217 |
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
pri_color = {"ROUTINE": DIM, "HIGH": WARN, "HAZARDOUS": "#FF4444"}
|
| 224 |
-
tbl_rows = ""
|
| 225 |
-
for r in rows[:50]: # cap at 50 rows for display
|
| 226 |
-
cells = ""
|
| 227 |
-
for h in headers:
|
| 228 |
-
val = r[h]
|
| 229 |
-
color = pri_color.get(val, TEXT) if h == "Priority" else TEXT
|
| 230 |
-
cells += f'<td style="{td_style};color:{color}">{val}</td>'
|
| 231 |
-
tbl_rows += f"<tr>{cells}</tr>"
|
| 232 |
-
header_cells = "".join(f'<th style="{th_style}">{h}</th>' for h in headers)
|
| 233 |
-
table_html = (
|
| 234 |
-
f'<div style="overflow-x:auto;margin-top:14px">'
|
| 235 |
-
f'<p style="font-family:monospace;font-size:.65rem;color:{DIM};text-transform:uppercase;letter-spacing:.1em;margin:0 0 6px">Detection Table ({len(rows)} confirmed)</p>'
|
| 236 |
-
f'<table style="width:100%;border-collapse:collapse;background:{PANEL}">'
|
| 237 |
-
f'<thead><tr>{header_cells}</tr></thead>'
|
| 238 |
-
f'<tbody>{tbl_rows}</tbody></table></div>'
|
| 239 |
-
)
|
| 240 |
-
else:
|
| 241 |
-
table_html = '<div class="alert-warn">No detections — try lowering the threshold or increasing asteroid count.</div>'
|
| 242 |
|
| 243 |
-
return stats_html + charts_html + table_html
|
| 244 |
|
|
|
|
| 245 |
|
| 246 |
-
# ── Tab 2: MPC Formatter ────────────────────
|
| 247 |
def format_mpc(desig, ra_deg, dec_deg, yr, mo, day_frac, mag, band, obs_code):
|
| 248 |
obs_code = obs_code.strip()
|
| 249 |
if len(obs_code) != 3:
|
| 250 |
return '<div class="alert-err">✗ Observatory code must be exactly 3 characters</div>', ""
|
| 251 |
try:
|
| 252 |
-
from
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
place(mag_f, 56, 4); place((band[0] if band else "R"), 61, 1)
|
| 269 |
-
place(obs_code.rjust(3), 77, 3)
|
| 270 |
-
line = buf.decode("ascii")
|
| 271 |
-
if len(line) != 80:
|
| 272 |
-
return f'<div class="alert-err">✗ Length error: {len(line)}</div>', ""
|
| 273 |
-
ruler = "".join(str((i+1)%10) for i in range(80))
|
| 274 |
-
tens = "".join(str((i+1)//10%10) if (i+1)%10==0 else " " for i in range(80))
|
| 275 |
-
fields = [
|
| 276 |
-
("1–5", line[0:5].strip() or "(blank)", "Provisional designation"),
|
| 277 |
-
("9", line[8], "Note 2 (C = CCD)"),
|
| 278 |
-
("10–17", line[9:17], "Date YYYY MM "),
|
| 279 |
-
("18–25", line[17:25], "Day DD.ddddd"),
|
| 280 |
-
("27–37", line[26:37], "RA HH MM SS.ss"),
|
| 281 |
-
("38–48", line[37:48], "Dec ±DD MM SS.s"),
|
| 282 |
-
("57–60", line[56:60].strip() or "—", "Magnitude"),
|
| 283 |
-
("62", line[61], "Filter band"),
|
| 284 |
-
("78–80", line[77:80], "Observatory code"),
|
| 285 |
-
]
|
| 286 |
-
rows_html = "".join(
|
| 287 |
-
f"<tr><td style='color:{DIM};font-size:.62rem;padding:3px 8px;font-family:monospace'>{c}</td>"
|
| 288 |
-
f"<td style='color:{ACCENT};font-size:.7rem;padding:3px 8px;font-family:monospace'><b>{v}</b></td>"
|
| 289 |
-
f"<td style='color:{TEXT};font-size:.65rem;padding:3px 8px;font-family:monospace'>{d}</td></tr>"
|
| 290 |
-
for c,v,d in fields)
|
| 291 |
-
mpc_html = (
|
| 292 |
-
f'<div class="alert-ok" style="margin-bottom:8px">✓ Valid MPC record — exactly 80 characters</div>'
|
| 293 |
-
f'<div class="mpc-ruler">{tens}</div><div class="mpc-ruler">{ruler}</div>'
|
| 294 |
-
f'<div class="mpc-wrap">{line}</div>'
|
| 295 |
-
f'<p style="font-family:monospace;font-size:.62rem;color:{DIM};margin:12px 0 4px;text-transform:uppercase;letter-spacing:.1em">Field Breakdown</p>'
|
| 296 |
-
f'<table style="width:100%;border-collapse:collapse;background:{PANEL}">'
|
| 297 |
-
f'<thead><tr>'
|
| 298 |
-
f'<th style="color:{DIM};font-size:.6rem;text-align:left;padding:5px 8px;font-family:monospace;border-bottom:1px solid {SUBTLE};text-transform:uppercase">Col</th>'
|
| 299 |
-
f'<th style="color:{DIM};font-size:.6rem;text-align:left;padding:5px 8px;font-family:monospace;border-bottom:1px solid {SUBTLE};text-transform:uppercase">Value</th>'
|
| 300 |
-
f'<th style="color:{DIM};font-size:.6rem;text-align:left;padding:5px 8px;font-family:monospace;border-bottom:1px solid {SUBTLE};text-transform:uppercase">Field</th>'
|
| 301 |
-
f'</tr></thead><tbody>{rows_html}</tbody></table>'
|
| 302 |
)
|
| 303 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
except Exception as exc:
|
| 305 |
-
return
|
|
|
|
| 306 |
|
|
|
|
| 307 |
|
| 308 |
-
# ── Tab 3: Tracklet Visualizer ───────────────
|
| 309 |
def visualise_tracklet(n_dets, velocity, pa, time_span, snr_val, show_unc):
|
| 310 |
-
rng
|
| 311 |
-
n
|
| 312 |
-
times = np.linspace(0, float(time_span)*60, n)
|
| 313 |
-
pa_r = math.radians(float(pa))
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
|
|
|
| 319 |
|
| 320 |
fig, axes = plt.subplots(1, 3, figsize=(14, 4.5))
|
| 321 |
fig.patch.set_facecolor(BG)
|
| 322 |
|
| 323 |
-
|
| 324 |
-
ax
|
| 325 |
ax.tick_params(colors=DIM, labelsize=7)
|
| 326 |
-
for sp in ax.spines.values():
|
|
|
|
| 327 |
ax.grid(color=SUBTLE, linewidth=0.3, alpha=0.5)
|
| 328 |
-
ax.scatter(rng.uniform(179.97,180.03,
|
| 329 |
-
s=rng.uniform(1,
|
| 330 |
-
ax.plot(ra_t,dec_t,"--",color=ACCENT,lw=1,alpha=0.35,label="True path")
|
| 331 |
-
ax.scatter(ra_o,dec_o,s=55,color=ACCENT,zorder=5,lw=0)
|
| 332 |
if show_unc:
|
| 333 |
-
for rx,dy in zip(ra_o,dec_o):
|
| 334 |
-
ax.add_patch(plt.Circle((rx,dy),noise*3,color=ACCENT,
|
| 335 |
-
|
| 336 |
-
ax.scatter([ra_o[
|
| 337 |
-
ax.
|
| 338 |
-
ax.set_ylabel("Dec (°)",color=DIM,fontfamily="monospace",fontsize=7)
|
| 339 |
-
ax.set_title("Sky Plane",color=TEXT,fontfamily="monospace",fontsize=8)
|
| 340 |
ax.invert_xaxis()
|
| 341 |
-
ax.
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
ax2.
|
| 348 |
-
|
| 349 |
-
ax2.
|
| 350 |
-
|
| 351 |
-
ax2.
|
| 352 |
-
|
| 353 |
-
ax2.
|
| 354 |
-
ax2.
|
| 355 |
-
ax2.
|
| 356 |
-
ax2.
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
ax3
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
|
| 376 |
plt.tight_layout(pad=1.5)
|
| 377 |
-
img_b64 = fig_to_b64(fig)
|
| 378 |
status = (
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
|
|
|
|
|
|
| 382 |
)
|
| 383 |
-
return img_html(
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
# ── Tab 4: Orbit Inspector ───────────────────
|
| 387 |
-
def inspect_orbit(a, e, i, omega, Omega, M0, arc_hr):
|
| 388 |
-
scale = max(1.0, 2.0/float(arc_hr))
|
| 389 |
-
sigma = dict(a=round(.01*scale,4), e=round(.05*scale,4), i=round(1.0*scale,2),
|
| 390 |
-
om=round(10.0*scale,1), Om=round(5.0*scale,1), M=round(15.0*scale,1))
|
| 391 |
-
a_f, e_f = float(a), float(e)
|
| 392 |
-
if e_f>=1.0: cls = "HYPERBOLIC (interstellar?)"
|
| 393 |
-
elif a_f<1.3: cls = "NEO (Apollo/Aten/Amor)"
|
| 394 |
-
elif a_f<2.0: cls = "Mars Crosser"
|
| 395 |
-
elif a_f<3.2: cls = "Main Belt"
|
| 396 |
-
elif a_f<5.2: cls = "Outer Belt / Hildas"
|
| 397 |
-
else: cls = "Jupiter Trojans / TNO"
|
| 398 |
-
|
| 399 |
-
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(13, 5.5))
|
| 400 |
-
fig.patch.set_facecolor(BG)
|
| 401 |
-
for ax in (ax1,ax2):
|
| 402 |
-
ax.set_facecolor("#010610")
|
| 403 |
-
ax.tick_params(colors=DIM,labelsize=7)
|
| 404 |
-
for sp in ax.spines.values(): sp.set_edgecolor(SUBTLE)
|
| 405 |
-
ax.grid(color=SUBTLE,linewidth=0.3,alpha=0.4)
|
| 406 |
-
for r,lbl,col in [(.387,"Mercury","#888"),(.723,"Venus","#AA8"),(1.0,"Earth","#3A8"),
|
| 407 |
-
(1.524,"Mars","#B44"),(2.77,"Ceres","#665")]:
|
| 408 |
-
th = np.linspace(0,2*math.pi,360)
|
| 409 |
-
ax1.plot(r*np.cos(th),r*np.sin(th),color=col,lw=0.6,alpha=0.4,ls=":")
|
| 410 |
-
ax1.text(r*.71,r*.71,lbl,color=col,fontsize=5,fontfamily="monospace",alpha=0.5)
|
| 411 |
-
ax1.scatter([0],[0],s=110,color=WARN,zorder=10,lw=0)
|
| 412 |
-
try:
|
| 413 |
-
om_r = math.radians(float(omega))
|
| 414 |
-
nu = np.linspace(0,2*math.pi,720)
|
| 415 |
-
r_o = a_f*(1-e_f**2)/(1+e_f*np.cos(nu))
|
| 416 |
-
ax1.plot(r_o*np.cos(nu+om_r),r_o*np.sin(nu+om_r),color=ACCENT,lw=1.8,zorder=5)
|
| 417 |
-
M_r = math.radians(float(M0)); nu0 = M_r+2*e_f*math.sin(M_r)
|
| 418 |
-
r0 = a_f*(1-e_f**2)/(1+e_f*math.cos(nu0))
|
| 419 |
-
ax1.scatter([r0*math.cos(nu0+om_r)],[r0*math.sin(nu0+om_r)],s=80,color=ACC2,zorder=9,lw=0,marker="D")
|
| 420 |
-
except Exception: pass
|
| 421 |
-
lim = max(2.0, a_f*1.35); ax1.set_xlim(-lim,lim); ax1.set_ylim(-lim,lim)
|
| 422 |
-
ax1.set_aspect("equal")
|
| 423 |
-
ax1.set_xlabel("X (AU) — Ecliptic",color=DIM,fontfamily="monospace",fontsize=7)
|
| 424 |
-
ax1.set_ylabel("Y (AU)",color=DIM,fontfamily="monospace",fontsize=7)
|
| 425 |
-
ax1.set_title("Ecliptic Projection",color=TEXT,fontfamily="monospace",fontsize=8)
|
| 426 |
-
|
| 427 |
-
ax2.set_facecolor(PANEL); ax2.tick_params(colors=DIM,labelsize=7)
|
| 428 |
-
for sp in ax2.spines.values(): sp.set_edgecolor(SUBTLE)
|
| 429 |
-
ax2.grid(color=SUBTLE,linewidth=0.3,alpha=0.4)
|
| 430 |
-
names = ["a (AU)","e","i (°)","ω (°)","Ω (°)","M (°)"]
|
| 431 |
-
errs = list(sigma.values())
|
| 432 |
-
yp = np.arange(len(names))
|
| 433 |
-
ax2.barh(yp,errs,color=ACCENT,alpha=0.7,height=0.5)
|
| 434 |
-
ax2.set_yticks(yp); ax2.set_yticklabels(names,color=TEXT,fontfamily="monospace",fontsize=7)
|
| 435 |
-
ax2.set_xlabel("1σ Uncertainty",color=DIM,fontfamily="monospace",fontsize=7)
|
| 436 |
-
ax2.set_title(f"Uncertainties (arc={arc_hr}h)",color=TEXT,fontfamily="monospace",fontsize=8)
|
| 437 |
-
ax2.invert_yaxis()
|
| 438 |
-
plt.tight_layout(pad=1.8)
|
| 439 |
-
orb_b64 = fig_to_b64(fig)
|
| 440 |
-
|
| 441 |
-
ep = Time.now().isot[:10]
|
| 442 |
-
data = [("Semi-major axis a",f"{a} AU",f"± {sigma['a']} AU"),
|
| 443 |
-
("Eccentricity e",str(e),f"± {sigma['e']}"),
|
| 444 |
-
("Inclination i",f"{i}°",f"± {sigma['i']}°"),
|
| 445 |
-
("Arg. perihelion ω",f"{omega}°",f"± {sigma['om']}°"),
|
| 446 |
-
("Long. asc. node Ω",f"{Omega}°",f"± {sigma['Om']}°"),
|
| 447 |
-
("Mean anomaly M",f"{M0}°",f"± {sigma['M']}°"),
|
| 448 |
-
("Epoch (TDB)",ep,"—"),
|
| 449 |
-
("Method","Gauss (single-night)","—")]
|
| 450 |
-
rows = "".join(
|
| 451 |
-
f"<tr><td style='color:{TEXT};font-size:.7rem;padding:4px 8px;font-family:monospace'>{n}</td>"
|
| 452 |
-
f"<td style='color:{ACCENT};font-size:.7rem;padding:4px 8px;font-family:monospace;text-align:right'><b>{v}</b></td>"
|
| 453 |
-
f"<td style='color:{DIM};font-size:.65rem;padding:4px 8px;font-family:monospace;text-align:right'>{s}</td></tr>"
|
| 454 |
-
for n,v,s in data)
|
| 455 |
-
html = (
|
| 456 |
-
f'<div class="alert-info">Object class: <b>{cls}</b></div>'
|
| 457 |
-
+ img_html(orb_b64, "Ecliptic Diagram + Uncertainty Bars")
|
| 458 |
-
+ f'<table style="width:100%;border-collapse:collapse;background:{PANEL}">'
|
| 459 |
-
+ f'<thead><tr>'
|
| 460 |
-
+ f'<th style="color:{DIM};font-size:.6rem;padding:5px 8px;font-family:monospace;border-bottom:1px solid {SUBTLE};text-align:left;text-transform:uppercase">Element</th>'
|
| 461 |
-
+ f'<th style="color:{DIM};font-size:.6rem;padding:5px 8px;font-family:monospace;border-bottom:1px solid {SUBTLE};text-align:right;text-transform:uppercase">Value</th>'
|
| 462 |
-
+ f'<th style="color:{DIM};font-size:.6rem;padding:5px 8px;font-family:monospace;border-bottom:1px solid {SUBTLE};text-align:right;text-transform:uppercase">±1σ</th>'
|
| 463 |
-
+ f'</tr></thead><tbody>{rows}</tbody></table>'
|
| 464 |
-
)
|
| 465 |
-
if float(arc_hr) < 2:
|
| 466 |
-
html += '<div class="alert-warn">⚠ Short arc — large uncertainties. Follow-up astrometry strongly recommended.</div>'
|
| 467 |
-
return html
|
| 468 |
|
| 469 |
|
| 470 |
-
# ──
|
| 471 |
-
ABOUT_HTML = f"""
|
| 472 |
-
<div style="max-width:800px;margin:20px auto;font-family:'Space Mono',monospace">
|
| 473 |
-
<h2 style="color:{ACCENT};font-family:'Syne',sans-serif;font-weight:800;font-size:1.4rem;
|
| 474 |
-
letter-spacing:-.03em;margin-bottom:4px">AsteroidNET</h2>
|
| 475 |
-
<p style="color:{DIM};font-size:.66rem;letter-spacing:.14em;text-transform:uppercase;margin-bottom:18px">
|
| 476 |
-
Automated Near-Earth Object Detection · Dr. Matheus Machado Rech</p>
|
| 477 |
-
<div style="background:{PANEL};border:1px solid {SUBTLE};border-radius:4px;padding:18px 22px;margin-bottom:14px">
|
| 478 |
-
<h3 style="color:{TEXT};font-size:.8rem;font-weight:700;margin:0 0 10px;letter-spacing:.08em;text-transform:uppercase">Pipeline Stages</h3>
|
| 479 |
-
<table style="width:100%;border-collapse:collapse">
|
| 480 |
-
""" + "".join(
|
| 481 |
-
f"<tr style='border-bottom:1px solid {SUBTLE}'>"
|
| 482 |
-
f"<td style='padding:6px 10px;color:{ACCENT};font-size:.7rem;white-space:nowrap'>{s}</td>"
|
| 483 |
-
f"<td style='padding:6px 10px;color:{DIM};font-size:.66rem'>{m}</td>"
|
| 484 |
-
f"<td style='padding:6px 10px;color:{TEXT};font-size:.66rem'>{d}</td></tr>"
|
| 485 |
-
for s,m,d in [
|
| 486 |
-
("Stage 1","fits_ingestor", "Memory-mapped FITS I/O · Header validation · Calibration"),
|
| 487 |
-
("Stage 2","image_preprocessor","Background subtraction · Cosmic ray rejection · Alignment"),
|
| 488 |
-
("Stage 3","source_extractor", "Two-pass DAOStarFinder (5σ/3σ) · Aperture photometry"),
|
| 489 |
-
("Stage 4","catalog_matcher", "Gaia DR3 cross-match with PM correction · JPL Horizons"),
|
| 490 |
-
("Stage 5","tracklet_linker", "Hough-transform pairs · KD-tree track extension"),
|
| 491 |
-
("Stage 6","classifier", "Random Forest (0.7) → CNN (0.9) · Satellite filter"),
|
| 492 |
-
("+", "orbit_determination","Gauss method via sbpy · Topocentric correction"),
|
| 493 |
-
("+", "reporting", "MPC 80-column · PostgreSQL provenance · Alert dispatch"),
|
| 494 |
-
]) + f"""
|
| 495 |
-
</table></div>
|
| 496 |
-
<div style="background:{PANEL};border:1px solid {SUBTLE};border-radius:4px;padding:18px 22px;margin-bottom:14px">
|
| 497 |
-
<h3 style="color:{TEXT};font-size:.8rem;font-weight:700;margin:0 0 10px;letter-spacing:.08em;text-transform:uppercase">Performance Targets</h3>
|
| 498 |
-
<div class="stat-grid">
|
| 499 |
-
<div class="stat-card"><div class="stat-val g">≥90%</div><div class="stat-label">Recovery (SNR≥5)</div></div>
|
| 500 |
-
<div class="stat-card"><div class="stat-val g"><1%</div><div class="stat-label">False positive</div></div>
|
| 501 |
-
<div class="stat-card"><div class="stat-val a">500+</div><div class="stat-label">Frames / night</div></div>
|
| 502 |
-
<div class="stat-card"><div class="stat-val a"><4hr</div><div class="stat-label">Processing time</div></div>
|
| 503 |
-
<div class="stat-card"><div class="stat-val">>99.5%</div><div class="stat-label">Star removal</div></div>
|
| 504 |
-
<div class="stat-card"><div class="stat-val"><5s</div><div class="stat-label">Gaia cross-match</div></div>
|
| 505 |
-
</div></div>
|
| 506 |
-
<div style="background:{PANEL};border:1px solid {SUBTLE};border-radius:4px;padding:18px 22px">
|
| 507 |
-
<h3 style="color:{TEXT};font-size:.8rem;font-weight:700;margin:0 0 10px;letter-spacing:.08em;text-transform:uppercase">Tech Stack</h3>
|
| 508 |
-
<div style="display:flex;flex-wrap:wrap;gap:7px">
|
| 509 |
-
""" + "".join(
|
| 510 |
-
f'<span class="badge {"bc" if i%3==0 else "ba" if i%3==1 else "bg"}">{p}</span>'
|
| 511 |
-
for i,p in enumerate(["astropy ≥6.0","photutils ≥1.10","astroquery ≥0.4.7","reproject ≥0.13",
|
| 512 |
-
"astroscrappy ≥1.1","sbpy ≥0.4","scikit-learn ≥1.4","PyTorch ≥2.2",
|
| 513 |
-
"SQLAlchemy ≥2.0","Celery ≥5.3","FastAPI ≥0.110","PostgreSQL+PostGIS"])
|
| 514 |
-
) + "</div></div></div>"
|
| 515 |
-
|
| 516 |
-
# ── Header ───────────────────────────────────
|
| 517 |
-
HEADER = f"""
|
| 518 |
-
<div class="asteroid-header">
|
| 519 |
-
<h1 class="asteroid-title">Asteroid<span>NET</span></h1>
|
| 520 |
-
<p class="asteroid-sub">Automated Near-Earth Object Detection System · v0.1.0</p>
|
| 521 |
-
<div class="badge-row">
|
| 522 |
-
<span class="badge bc">6-Stage Pipeline</span>
|
| 523 |
-
<span class="badge bc">Astropy-Native</span>
|
| 524 |
-
<span class="badge ba">RF + CNN Classifier</span>
|
| 525 |
-
<span class="badge bg">MPC-Compliant</span>
|
| 526 |
-
<span class="badge bc">Gauss Orbit Determination</span>
|
| 527 |
-
</div>
|
| 528 |
-
</div>"""
|
| 529 |
|
| 530 |
-
|
| 531 |
-
|
|
|
|
|
|
|
| 532 |
|
|
|
|
| 533 |
gr.HTML(HEADER)
|
| 534 |
|
| 535 |
with gr.Tabs():
|
| 536 |
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
with gr.Row():
|
| 541 |
with gr.Column(scale=1, min_width=240):
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
with gr.Column(scale=3):
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
|
|
|
|
|
|
|
|
|
| 557 |
with gr.Tab("⬡ MPC Formatter"):
|
| 558 |
-
gr.HTML(
|
| 559 |
-
f'text-transform:uppercase;padding:10px 0 2px">Generate a Minor Planet Center 80-column astrometric record</p>')
|
| 560 |
with gr.Row():
|
| 561 |
with gr.Column(scale=1):
|
| 562 |
mpc_d = gr.Textbox(label="Provisional Designation", value="2026 AA1")
|
|
@@ -568,63 +607,79 @@ with gr.Blocks(css=CSS, theme=gr.themes.Base()) as demo:
|
|
| 568 |
mpc_dy = gr.Number(label="Day (DD.ddddd)", value=20.50000)
|
| 569 |
mpc_mg = gr.Number(label="Magnitude", value=18.5)
|
| 570 |
mpc_bd = gr.Textbox(label="Filter Band", value="R", max_lines=1)
|
| 571 |
-
mpc_oc = gr.Textbox(label="Observatory Code
|
| 572 |
mpc_bt = gr.Button("Generate MPC Record", variant="primary")
|
| 573 |
with gr.Column(scale=2):
|
| 574 |
mpc_out = gr.HTML()
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
|
|
|
|
|
|
| 582 |
with gr.Tab("⬡ Tracklet Visualizer"):
|
| 583 |
-
gr.HTML(
|
| 584 |
-
f'text-transform:uppercase;padding:10px 0 2px">Inspect multi-frame tracklet motion and linear residuals</p>')
|
| 585 |
with gr.Row():
|
| 586 |
with gr.Column(scale=1):
|
| 587 |
-
t_n = gr.Slider(3, 10,
|
| 588 |
-
t_v = gr.Slider(0.01, 8,value=0.5, step=0.01, label=
|
| 589 |
-
t_pa = gr.Slider(0, 360, value=135,
|
| 590 |
-
t_sp = gr.Slider(30, 240, value=90,
|
| 591 |
-
t_sn = gr.Slider(3, 30, value=10,
|
| 592 |
t_uc = gr.Checkbox(label="Show position uncertainties", value=True)
|
| 593 |
t_bt = gr.Button("Plot Tracklet", variant="primary")
|
| 594 |
with gr.Column(scale=3):
|
| 595 |
t_img = gr.HTML()
|
| 596 |
t_st = gr.HTML()
|
| 597 |
-
t_bt.click(
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
with gr.Row():
|
| 606 |
-
with gr.Column(scale=1):
|
| 607 |
-
o_a = gr.Slider(0.3, 6, value=2.5, step=0.01, label="Semi-major axis a (AU)")
|
| 608 |
-
o_e = gr.Slider(0.0, 0.99,value=0.12, step=0.01, label="Eccentricity e")
|
| 609 |
-
o_i = gr.Slider(0, 90, value=8.5, step=0.1, label="Inclination i (°)")
|
| 610 |
-
o_om = gr.Slider(0, 360, value=45, step=1, label="Arg. Perihelion ω (°)")
|
| 611 |
-
o_Om = gr.Slider(0, 360, value=120, step=1, label="Long. Asc. Node Ω (°)")
|
| 612 |
-
o_M = gr.Slider(0, 360, value=200, step=1, label="Mean Anomaly M (°)")
|
| 613 |
-
o_arc= gr.Slider(0.25, 12, value=1.0, step=0.25, label="Observation arc (hours)")
|
| 614 |
-
o_bt = gr.Button("Compute Orbit", variant="primary")
|
| 615 |
-
with gr.Column(scale=2):
|
| 616 |
-
o_out = gr.HTML()
|
| 617 |
-
o_bt.click(fn=inspect_orbit,
|
| 618 |
-
inputs=[o_a,o_e,o_i,o_om,o_Om,o_M,o_arc],
|
| 619 |
-
outputs=[o_out],
|
| 620 |
-
api_name=False)
|
| 621 |
-
|
| 622 |
with gr.Tab("⬡ About"):
|
| 623 |
-
gr.HTML(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
|
| 625 |
-
gr.HTML(f'<div style="text-align:center;padding:14px;font-family:monospace;font-size:.6rem;'
|
| 626 |
-
f'color:{DIM};letter-spacing:.1em;border-top:1px solid {SUBTLE}">'
|
| 627 |
-
f'AsteroidNET · Dr. Matheus Machado Rech · Spec-Driven Development + Denario</div>')
|
| 628 |
|
| 629 |
if __name__ == "__main__":
|
| 630 |
demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
|
|
|
|
| 1 |
"""
|
| 2 |
+
AsteroidNET Gradio UI v0.2 — Python 3.11 compatible (no backslashes in f-strings)
|
| 3 |
+
Five tabs including the new "Processar Imagens IASC" tab with real FITS upload.
|
| 4 |
"""
|
| 5 |
+
import io
|
| 6 |
+
import math
|
| 7 |
+
import base64
|
| 8 |
+
import warnings
|
| 9 |
+
import tempfile
|
| 10 |
+
import logging
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
import gradio as gr
|
| 14 |
import numpy as np
|
| 15 |
import pandas as pd
|
|
|
|
| 17 |
matplotlib.use("Agg")
|
| 18 |
import matplotlib.pyplot as plt
|
| 19 |
from matplotlib.lines import Line2D
|
|
|
|
| 20 |
from astropy.time import Time
|
| 21 |
import astropy.units as u
|
| 22 |
+
|
| 23 |
warnings.filterwarnings("ignore")
|
| 24 |
+
logging.basicConfig(level=logging.WARNING)
|
| 25 |
|
| 26 |
+
# ── Palette ──────────────────────────────────────────────────────────────────
|
| 27 |
BG = "#04060D"
|
| 28 |
PANEL = "#0A0E1A"
|
| 29 |
ACCENT = "#00D4FF"
|
|
|
|
| 34 |
TEXT = "#C8D8E8"
|
| 35 |
DIM = "#4A6070"
|
| 36 |
|
| 37 |
+
CSS = """
|
| 38 |
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&display=swap');
|
| 39 |
+
body,.gradio-container{background:#04060D!important;font-family:'Syne',sans-serif!important;color:#C8D8E8!important}
|
| 40 |
+
.tabs>.tab-nav{background:#0A0E1A!important;border-bottom:1px solid #1E2D40!important}
|
| 41 |
+
.tabs>.tab-nav>button{font-family:'Space Mono',monospace!important;font-size:.68rem!important;letter-spacing:.1em!important;text-transform:uppercase!important;color:#4A6070!important;border:none!important;border-bottom:2px solid transparent!important;background:transparent!important;padding:11px 16px!important;transition:all .2s!important}
|
| 42 |
+
.tabs>.tab-nav>button.selected,.tabs>.tab-nav>button:hover{color:#00D4FF!important;border-bottom-color:#00D4FF!important}
|
| 43 |
+
.gr-box,.gr-form,.gr-panel{background:#0A0E1A!important;border:1px solid #1E2D40!important;border-radius:4px!important}
|
| 44 |
+
label,.gr-label{font-family:'Space Mono',monospace!important;font-size:.66rem!important;letter-spacing:.1em!important;text-transform:uppercase!important;color:#4A6070!important}
|
| 45 |
+
button.primary{background:#00D4FF!important;color:#04060D!important;border:none!important;font-family:'Space Mono',monospace!important;font-size:.7rem!important;font-weight:700!important;letter-spacing:.1em!important;text-transform:uppercase!important;padding:10px 18px!important;border-radius:4px!important}
|
| 46 |
+
button.secondary{background:transparent!important;color:#00D4FF!important;border:1px solid #00D4FF!important;font-family:'Space Mono',monospace!important;font-size:.7rem!important;letter-spacing:.1em!important;border-radius:4px!important}
|
| 47 |
+
input,textarea,select{background:#1E2D40!important;border:1px solid #2A3D50!important;color:#C8D8E8!important;font-family:'Space Mono',monospace!important;font-size:.78rem!important;border-radius:4px!important}
|
| 48 |
+
input[type=range]{accent-color:#00D4FF!important}
|
| 49 |
+
.stat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:9px;margin:14px 0}
|
| 50 |
+
.stat-card{background:#1E2D40;border:1px solid #1E3048;border-radius:4px;padding:13px 14px;text-align:center}
|
| 51 |
+
.stat-val{font-family:'Space Mono',monospace;font-size:1.5rem;font-weight:700;line-height:1;color:#00D4FF}
|
| 52 |
+
.stat-val.a{color:#FF6B2B}.stat-val.g{color:#39FF14}.stat-val.w{color:#FFD700}
|
| 53 |
+
.stat-label{font-family:'Space Mono',monospace;font-size:.58rem;letter-spacing:.1em;text-transform:uppercase;color:#4A6070;margin-top:4px}
|
| 54 |
+
.mpc-wrap{background:#000814;border:1px solid rgba(0,212,255,.33);border-radius:4px;padding:14px 18px;font-family:'Space Mono',monospace;font-size:.86rem;color:#00D4FF;word-break:break-all}
|
| 55 |
+
.alert-ok{background:rgba(57,255,20,.08);border:1px solid rgba(57,255,20,.27);color:#39FF14;border-radius:4px;padding:10px 14px;font-family:'Space Mono',monospace;font-size:.72rem;margin:6px 0}
|
| 56 |
+
.alert-warn{background:rgba(255,215,0,.08);border:1px solid rgba(255,215,0,.27);color:#FFD700;border-radius:4px;padding:10px 14px;font-family:'Space Mono',monospace;font-size:.72rem;margin:6px 0}
|
| 57 |
+
.alert-err{background:rgba(255,51,68,.08);border:1px solid rgba(255,51,68,.27);color:#FF6666;border-radius:4px;padding:10px 14px;font-family:'Space Mono',monospace;font-size:.72rem;margin:6px 0}
|
| 58 |
+
.alert-info{background:rgba(0,212,255,.06);border:1px solid rgba(0,212,255,.27);color:#00D4FF;border-radius:4px;padding:10px 14px;font-family:'Space Mono',monospace;font-size:.72rem;margin:6px 0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
"""
|
| 60 |
|
| 61 |
+
HEADER = """
|
| 62 |
+
<div style="background:linear-gradient(135deg,#04060D 0%,#0A1628 60%);border-bottom:1px solid rgba(0,212,255,.2);padding:24px 32px 18px;position:relative;overflow:hidden">
|
| 63 |
+
<h1 style="font-family:'Syne',sans-serif;font-weight:800;font-size:2.1rem;letter-spacing:-.04em;color:#FFF;margin:0">
|
| 64 |
+
Asteroid<span style="color:#00D4FF">NET</span>
|
| 65 |
+
</h1>
|
| 66 |
+
<p style="font-family:'Space Mono',monospace;font-size:.68rem;color:#4A6070;letter-spacing:.18em;text-transform:uppercase;margin-top:5px">
|
| 67 |
+
Automated Near-Earth Object Detection · v0.2.0
|
| 68 |
+
</p>
|
| 69 |
+
<div style="display:flex;gap:7px;margin-top:12px;flex-wrap:wrap">
|
| 70 |
+
<span style="background:rgba(0,212,255,.1);border:1px solid rgba(0,212,255,.33);color:#00D4FF;font-family:'Space Mono',monospace;font-size:.6rem;letter-spacing:.1em;padding:3px 8px;border-radius:2px;font-weight:700;text-transform:uppercase">IASC/Pan-STARRS</span>
|
| 71 |
+
<span style="background:rgba(0,212,255,.1);border:1px solid rgba(0,212,255,.33);color:#00D4FF;font-family:'Space Mono',monospace;font-size:.6rem;letter-spacing:.1em;padding:3px 8px;border-radius:2px;font-weight:700;text-transform:uppercase">ZTF Support</span>
|
| 72 |
+
<span style="background:rgba(255,107,43,.1);border:1px solid rgba(255,107,43,.33);color:#FF6B2B;font-family:'Space Mono',monospace;font-size:.6rem;letter-spacing:.1em;padding:3px 8px;border-radius:2px;font-weight:700;text-transform:uppercase">SkyBoT Integration</span>
|
| 73 |
+
<span style="background:rgba(57,255,20,.1);border:1px solid rgba(57,255,20,.33);color:#39FF14;font-family:'Space Mono',monospace;font-size:.6rem;letter-spacing:.1em;padding:3px 8px;border-radius:2px;font-weight:700;text-transform:uppercase">MPC-Compliant</span>
|
| 74 |
+
<span style="background:rgba(0,212,255,.1);border:1px solid rgba(0,212,255,.33);color:#00D4FF;font-family:'Space Mono',monospace;font-size:.6rem;letter-spacing:.1em;padding:3px 8px;border-radius:2px;font-weight:700;text-transform:uppercase">TAI/UTC Corrected</span>
|
| 75 |
+
</div>
|
| 76 |
+
</div>"""
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# ── Shared helpers ────────────────────────────────────────────────────────────
|
| 80 |
+
|
| 81 |
def fig_to_b64(fig):
|
| 82 |
buf = io.BytesIO()
|
| 83 |
fig.savefig(buf, format="png", dpi=130, bbox_inches="tight",
|
|
|
|
| 85 |
buf.seek(0)
|
| 86 |
b64 = base64.b64encode(buf.read()).decode()
|
| 87 |
plt.close(fig)
|
| 88 |
+
return "data:image/png;base64," + b64
|
| 89 |
+
|
| 90 |
|
| 91 |
def dark_fig(w=9, h=5):
|
| 92 |
fig, ax = plt.subplots(figsize=(w, h))
|
| 93 |
fig.patch.set_facecolor(BG)
|
| 94 |
ax.set_facecolor(PANEL)
|
| 95 |
ax.tick_params(colors=DIM, labelsize=7)
|
| 96 |
+
for sp in ax.spines.values():
|
| 97 |
+
sp.set_edgecolor(SUBTLE)
|
| 98 |
ax.grid(color=SUBTLE, linewidth=0.4, alpha=0.5)
|
| 99 |
return fig, ax
|
| 100 |
|
| 101 |
+
|
| 102 |
def img_html(b64, label=""):
|
| 103 |
+
lbl = ""
|
| 104 |
+
if label:
|
| 105 |
+
lbl = (
|
| 106 |
+
'<p style="font-family:monospace;font-size:.65rem;color:' + DIM +
|
| 107 |
+
';text-transform:uppercase;letter-spacing:.1em;margin:0 0 6px">' +
|
| 108 |
+
label + "</p>"
|
| 109 |
+
)
|
| 110 |
+
return (
|
| 111 |
+
'<div style="margin:4px 0">' + lbl +
|
| 112 |
+
'<img src="' + b64 + '" style="width:100%;border-radius:4px;border:1px solid ' + SUBTLE + '">' +
|
| 113 |
+
"</div>"
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
|
| 117 |
+
def stat_card(val, label, cls=""):
|
| 118 |
+
return (
|
| 119 |
+
'<div class="stat-card">'
|
| 120 |
+
'<div class="stat-val ' + cls + '">' + str(val) + "</div>"
|
| 121 |
+
'<div class="stat-label">' + label + "</div>"
|
| 122 |
+
"</div>"
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
# ── Tab 1: Processar Imagens IASC (REAL FITS) ────────────────────────────────
|
| 127 |
+
|
| 128 |
+
def process_iasc_fits(fits_files, obs_code, survey_hint):
|
| 129 |
+
"""Process real FITS files uploaded by the user."""
|
| 130 |
+
if not fits_files:
|
| 131 |
+
return '<div class="alert-warn">⚠ Please upload at least 2 FITS files.</div>', "", ""
|
| 132 |
+
|
| 133 |
+
import sys
|
| 134 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
| 135 |
+
|
| 136 |
+
# Save uploaded files to temp dir
|
| 137 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 138 |
+
tmp = Path(tmpdir)
|
| 139 |
+
paths = []
|
| 140 |
+
for f in fits_files:
|
| 141 |
+
p = tmp / Path(f).name
|
| 142 |
+
import shutil
|
| 143 |
+
shutil.copy(f, p)
|
| 144 |
+
paths.append(p)
|
| 145 |
+
|
| 146 |
+
if len(paths) < 2:
|
| 147 |
+
return '<div class="alert-err">✗ Need at least 2 FITS frames.</div>', "", ""
|
| 148 |
+
|
| 149 |
+
try:
|
| 150 |
+
from asteroidnet.pipeline.runner import run_pipeline
|
| 151 |
+
result = run_pipeline(paths, observatory_code=obs_code or "???")
|
| 152 |
+
except Exception as exc:
|
| 153 |
+
return (
|
| 154 |
+
'<div class="alert-err">✗ Pipeline error: ' + str(exc)[:300] + "</div>",
|
| 155 |
+
"", ""
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
# Build summary HTML
|
| 159 |
+
pri_counts = {}
|
| 160 |
+
for cl in result.classifications:
|
| 161 |
+
pri_counts[cl.priority] = pri_counts.get(cl.priority, 0) + 1
|
| 162 |
+
|
| 163 |
+
n_haz = pri_counts.get("HAZARDOUS", 0)
|
| 164 |
+
n_high = pri_counts.get("HIGH", 0)
|
| 165 |
+
n_rout = pri_counts.get("ROUTINE", 0)
|
| 166 |
+
|
| 167 |
+
rec_pct = round(result.n_confirmed / max(result.n_candidates, 1) * 100, 1)
|
| 168 |
+
elapsed = round(result.elapsed_s, 2)
|
| 169 |
+
|
| 170 |
+
stats_html = (
|
| 171 |
+
'<div class="stat-grid">'
|
| 172 |
+
+ stat_card(result.n_frames, "Frames ingested")
|
| 173 |
+
+ stat_card(result.n_candidates, "Tracklet candidates", "a")
|
| 174 |
+
+ stat_card(result.n_confirmed, "Confirmed NEOs", "g")
|
| 175 |
+
+ stat_card(str(elapsed) + "s", "Pipeline time", "w")
|
| 176 |
+
+ stat_card(n_high, "HIGH alerts", "w")
|
| 177 |
+
+ stat_card(n_haz, "HAZARDOUS", "a" if n_haz > 0 else "")
|
| 178 |
+
+ "</div>"
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
# Status messages
|
| 182 |
+
if result.n_confirmed > 0:
|
| 183 |
+
stats_html += (
|
| 184 |
+
'<div class="alert-ok">✓ ' + str(result.n_confirmed) +
|
| 185 |
+
" new candidate(s) detected — review MPC records below</div>"
|
| 186 |
+
)
|
| 187 |
+
else:
|
| 188 |
+
msg = "No new moving objects detected"
|
| 189 |
+
if result.n_candidates == 0:
|
| 190 |
+
msg += " (no tracklet candidates found — try more frames or lower threshold)"
|
| 191 |
+
stats_html += '<div class="alert-warn">⚠ ' + msg + "</div>"
|
| 192 |
+
|
| 193 |
+
# Sky motion chart
|
| 194 |
+
if result.classifications:
|
| 195 |
+
stats_html += _make_detection_chart(result)
|
| 196 |
+
|
| 197 |
+
# Detection table
|
| 198 |
+
if result.classifications:
|
| 199 |
+
rows = []
|
| 200 |
+
for cl in result.classifications:
|
| 201 |
+
t = cl.tracklet
|
| 202 |
+
d0 = t.detections[0]
|
| 203 |
+
rows.append({
|
| 204 |
+
"RA (°)": round(d0["ra"], 5),
|
| 205 |
+
"Dec (°)": round(d0["dec"], 5),
|
| 206 |
+
"Vel (″/s)": round(t.velocity_arcsec_s, 4),
|
| 207 |
+
"PA (°)": round(t.position_angle_deg, 1),
|
| 208 |
+
"Detections": len(t.detections),
|
| 209 |
+
"Arc (min)": round(t.time_span_min, 1),
|
| 210 |
+
"RMS (″)": round(t.rms_residual_arcsec, 3),
|
| 211 |
+
"RF": round(cl.rf_score, 3),
|
| 212 |
+
"CNN": round(cl.cnn_score, 3),
|
| 213 |
+
"Priority": cl.priority,
|
| 214 |
+
})
|
| 215 |
+
df = pd.DataFrame(rows)
|
| 216 |
+
header_cells = "".join(
|
| 217 |
+
'<th style="padding:5px 8px;background:' + SUBTLE + ';color:' + ACCENT +
|
| 218 |
+
';font-family:monospace;font-size:.62rem;text-transform:uppercase;'
|
| 219 |
+
'letter-spacing:.06em;text-align:left;border-bottom:1px solid ' + SUBTLE + '">'
|
| 220 |
+
+ h + "</th>"
|
| 221 |
+
for h in df.columns
|
| 222 |
+
)
|
| 223 |
+
td_s = (
|
| 224 |
+
"padding:5px 8px;font-family:monospace;font-size:.68rem;color:" + TEXT +
|
| 225 |
+
";border-bottom:1px solid rgba(30,45,64,.5)"
|
| 226 |
+
)
|
| 227 |
+
body = ""
|
| 228 |
+
for _, row in df.iterrows():
|
| 229 |
+
cells = "".join(
|
| 230 |
+
'<td style="' + td_s + ';color:' +
|
| 231 |
+
("#FF4444" if str(v) == "HAZARDOUS" else WARN if str(v) == "HIGH" else TEXT) +
|
| 232 |
+
'">' + str(v) + "</td>"
|
| 233 |
+
for v in row
|
| 234 |
+
)
|
| 235 |
+
body += "<tr>" + cells + "</tr>"
|
| 236 |
+
stats_html += (
|
| 237 |
+
'<div style="overflow-x:auto;margin-top:14px">'
|
| 238 |
+
'<p style="font-family:monospace;font-size:.65rem;color:' + DIM +
|
| 239 |
+
';text-transform:uppercase;letter-spacing:.1em;margin:0 0 6px">Detection Table</p>'
|
| 240 |
+
'<table style="width:100%;border-collapse:collapse;background:' + PANEL + '">'
|
| 241 |
+
"<thead><tr>" + header_cells + "</tr></thead>"
|
| 242 |
+
"<tbody>" + body + "</tbody></table></div>"
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
# MPC output
|
| 246 |
+
mpc_text = "\n".join(result.mpc_records) if result.mpc_records else ""
|
| 247 |
+
mpc_html = ""
|
| 248 |
+
if mpc_text:
|
| 249 |
+
ruler = "".join(str((i + 1) % 10) for i in range(80))
|
| 250 |
+
tens = "".join(
|
| 251 |
+
str((i + 1) // 10 % 10) if (i + 1) % 10 == 0 else " "
|
| 252 |
+
for i in range(80)
|
| 253 |
+
)
|
| 254 |
+
mpc_html = (
|
| 255 |
+
'<div class="alert-ok" style="margin-bottom:8px">'
|
| 256 |
+
"✓ " + str(len(result.mpc_records)) + " MPC records generated</div>"
|
| 257 |
+
'<p style="font-family:monospace;font-size:.6rem;color:' + DIM +
|
| 258 |
+
';margin:0">' + tens + "</p>"
|
| 259 |
+
'<p style="font-family:monospace;font-size:.6rem;color:' + DIM +
|
| 260 |
+
';margin:0 0 6px">' + ruler + "</p>"
|
| 261 |
+
+ "".join(
|
| 262 |
+
'<div class="mpc-wrap" style="margin-bottom:4px">' + line + "</div>"
|
| 263 |
+
for line in result.mpc_records[:20]
|
| 264 |
+
)
|
| 265 |
+
)
|
| 266 |
+
if len(result.mpc_records) > 20:
|
| 267 |
+
mpc_html += (
|
| 268 |
+
'<div class="alert-info">' +
|
| 269 |
+
str(len(result.mpc_records) - 20) + " more records in raw output</div>"
|
| 270 |
+
)
|
| 271 |
+
|
| 272 |
+
return stats_html, mpc_html, mpc_text
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def _make_detection_chart(result) -> str:
|
| 276 |
+
"""Build sky motion chart for confirmed detections."""
|
| 277 |
+
fig, ax = dark_fig(10, 4.5)
|
| 278 |
ax.set_facecolor("#020812")
|
| 279 |
+
|
| 280 |
+
for cl in result.classifications:
|
| 281 |
+
t = cl.tracklet
|
| 282 |
+
ras = [d["ra"] for d in t.detections]
|
| 283 |
+
decs = [d["dec"] for d in t.detections]
|
| 284 |
+
col = "#FF4444" if cl.priority == "HAZARDOUS" else WARN if cl.priority == "HIGH" else ACCENT
|
| 285 |
+
ax.plot(ras, decs, "o-", color=col, lw=1.5, ms=5, alpha=0.8)
|
| 286 |
+
ax.annotate(cl.priority[0], (ras[-1], decs[-1]),
|
| 287 |
+
color=col, fontsize=7, fontfamily="monospace",
|
| 288 |
+
xytext=(3, 3), textcoords="offset points")
|
| 289 |
+
|
| 290 |
+
ax.invert_xaxis()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
ax.set_xlabel("RA (°)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 292 |
ax.set_ylabel("Dec (°)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 293 |
+
ax.set_title("Confirmed Tracklets — Sky Plane", color=TEXT,
|
| 294 |
+
fontfamily="monospace", fontsize=9)
|
| 295 |
+
ax.legend(handles=[
|
| 296 |
+
Line2D([0], [0], marker="o", color="w", markerfacecolor="#FF4444",
|
| 297 |
+
ms=6, lw=0, label="HAZARDOUS"),
|
| 298 |
+
Line2D([0], [0], marker="o", color="w", markerfacecolor=WARN,
|
| 299 |
+
ms=6, lw=0, label="HIGH"),
|
| 300 |
+
Line2D([0], [0], marker="o", color="w", markerfacecolor=ACCENT,
|
| 301 |
+
ms=6, lw=0, label="ROUTINE"),
|
| 302 |
+
], framealpha=0, labelcolor=DIM, fontsize=7, prop={"family": "monospace"})
|
| 303 |
+
|
| 304 |
+
plt.tight_layout(pad=1.5)
|
| 305 |
+
return img_html(fig_to_b64(fig), "Tracklet Motion Map")
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
# ── Tab 2: Pipeline Simulator ─────────────────────────────────────────────────
|
| 309 |
+
|
| 310 |
+
def run_simulation(n_frames, n_asteroids, snr_min, snr_max, vel_min, vel_max, det_thresh):
|
| 311 |
+
from asteroidnet.utils.synthetic import make_synthetic_sequence
|
| 312 |
+
from asteroidnet.pipeline.runner import run_pipeline
|
| 313 |
+
|
| 314 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 315 |
+
paths = make_synthetic_sequence(
|
| 316 |
+
Path(tmpdir),
|
| 317 |
+
n_frames=int(n_frames),
|
| 318 |
+
n_stars=400,
|
| 319 |
+
n_asteroids=int(n_asteroids),
|
| 320 |
+
velocity_arcsec_s=float((vel_min + vel_max) / 2),
|
| 321 |
+
cadence_min=15.0,
|
| 322 |
+
seed=42,
|
| 323 |
+
)
|
| 324 |
+
import yaml
|
| 325 |
+
cfg_override = {
|
| 326 |
+
"detection": {"threshold_sigma": det_thresh},
|
| 327 |
+
"tracking": {
|
| 328 |
+
"velocity_range_arcsec_s": [vel_min, vel_max],
|
| 329 |
+
"min_time_span_minutes": 20.0,
|
| 330 |
+
},
|
| 331 |
+
}
|
| 332 |
+
result = run_pipeline(paths, observatory_code="F51")
|
| 333 |
+
|
| 334 |
+
n_confirmed = result.n_confirmed
|
| 335 |
+
n_candidates = result.n_candidates
|
| 336 |
+
rec_pct = round(n_confirmed / max(n_asteroids, 1) * 100, 1)
|
| 337 |
+
fp_pct = 0.0
|
| 338 |
+
|
| 339 |
+
stats_html = (
|
| 340 |
+
'<div class="stat-grid">'
|
| 341 |
+
+ stat_card(int(n_frames), "Frames")
|
| 342 |
+
+ stat_card(n_candidates, "Candidates", "a")
|
| 343 |
+
+ stat_card(n_confirmed, "Confirmed", "g")
|
| 344 |
+
+ stat_card(str(rec_pct) + "%", "Recovery", "w")
|
| 345 |
+
+ stat_card(str(result.elapsed_s.__round__(2)) + "s", "Time")
|
| 346 |
+
+ "</div>"
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
ok_msg = "✓ SC-001 PASS — Recovery " + str(rec_pct) + "% ≥ 90%"
|
| 350 |
+
bad_msg = "⚠ SC-001 — Recovery " + str(rec_pct) + "% below 90% target"
|
| 351 |
+
stats_html += (
|
| 352 |
+
'<div class="' + ("alert-ok" if rec_pct >= 90 else "alert-warn") + '">'
|
| 353 |
+
+ (ok_msg if rec_pct >= 90 else bad_msg) + "</div>"
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
# Charts
|
| 357 |
+
rng = np.random.default_rng(42)
|
| 358 |
+
fig1, ax1 = dark_fig(10, 4.5)
|
| 359 |
+
ax1.set_facecolor("#020812")
|
| 360 |
+
ax1.scatter(rng.uniform(179.5, 180.5, 300), rng.uniform(-0.5, 0.5, 300),
|
| 361 |
+
s=rng.uniform(1, 6, 300), alpha=0.15, color="white", lw=0)
|
| 362 |
+
for i in range(int(n_asteroids)):
|
| 363 |
+
v = rng.uniform(vel_min, vel_max)
|
| 364 |
+
pa = rng.uniform(0, 2 * math.pi)
|
| 365 |
+
r0 = (rng.uniform(179.6, 180.4), rng.uniform(-0.4, 0.4))
|
| 366 |
+
r1 = (r0[0] + v * 1800 * math.sin(pa) / 3600,
|
| 367 |
+
r0[1] + v * 1800 * math.cos(pa) / 3600 * 0.5)
|
| 368 |
+
col = "#FF4444" if v > 3 else WARN if v > 1 else ACCENT
|
| 369 |
+
ax1.annotate("", xy=r1, xytext=r0,
|
| 370 |
+
arrowprops=dict(arrowstyle="-|>", color=col, lw=1.4,
|
| 371 |
+
mutation_scale=10))
|
| 372 |
+
ax1.scatter([r0[0]], [r0[1]], s=20, color=col, zorder=5, lw=0)
|
| 373 |
+
ax1.invert_xaxis()
|
| 374 |
+
ax1.set_xlabel("RA (°)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 375 |
+
ax1.set_ylabel("Dec (°)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 376 |
+
ax1.set_title("Simulated Motion Field", color=TEXT, fontfamily="monospace", fontsize=9)
|
| 377 |
|
|
|
|
| 378 |
snr_x = np.linspace(3, 20, 60)
|
| 379 |
+
comp = np.clip(1 / (1 + np.exp(-(snr_x - (det_thresh + 1.5)) * 1.2)), 0, 1)
|
|
|
|
| 380 |
fig2, ax2 = dark_fig(7, 3.8)
|
| 381 |
+
ax2.plot(snr_x, comp * 100, color=ACCENT, lw=2)
|
| 382 |
+
ax2.axvline(det_thresh, color=ACC2, lw=1.2, ls="--", label="Threshold " + str(det_thresh) + "σ")
|
| 383 |
+
ax2.axhline(90, color=OK, lw=0.8, ls=":", alpha=0.7)
|
|
|
|
| 384 |
ax2.set_xlabel("SNR", color=DIM, fontfamily="monospace", fontsize=7)
|
| 385 |
ax2.set_ylabel("Recovery (%)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 386 |
ax2.set_title("Completeness vs SNR", color=TEXT, fontfamily="monospace", fontsize=9)
|
| 387 |
+
ax2.legend(framealpha=0, labelcolor=DIM, fontsize=7, prop={"family": "monospace"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
|
| 389 |
+
plt.tight_layout(pad=1.5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
|
| 391 |
+
charts = (
|
| 392 |
+
img_html(fig_to_b64(fig1), "Motion Field")
|
| 393 |
+
+ img_html(fig_to_b64(fig2), "Completeness Curve")
|
| 394 |
+
)
|
| 395 |
+
return stats_html + charts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
|
|
|
|
| 397 |
|
| 398 |
+
# ── Tab 3: MPC Formatter ──────────────────────────────────────────────────────
|
| 399 |
|
|
|
|
| 400 |
def format_mpc(desig, ra_deg, dec_deg, yr, mo, day_frac, mag, band, obs_code):
|
| 401 |
obs_code = obs_code.strip()
|
| 402 |
if len(obs_code) != 3:
|
| 403 |
return '<div class="alert-err">✗ Observatory code must be exactly 3 characters</div>', ""
|
| 404 |
try:
|
| 405 |
+
from asteroidnet.reporting.mpc_formatter import format_mpc_record
|
| 406 |
+
obs_time = Time(
|
| 407 |
+
{"year": int(yr), "month": int(mo), "day": int(day_frac)},
|
| 408 |
+
format="ymdhms", scale="utc"
|
| 409 |
+
)
|
| 410 |
+
except Exception:
|
| 411 |
+
obs_time = Time("2026-03-20T12:00:00", scale="utc")
|
| 412 |
+
|
| 413 |
+
try:
|
| 414 |
+
from asteroidnet.reporting.mpc_formatter import format_mpc_record
|
| 415 |
+
line = format_mpc_record(str(desig), float(ra_deg), float(dec_deg),
|
| 416 |
+
obs_time, float(mag), str(band), obs_code)
|
| 417 |
+
ruler = "".join(str((i + 1) % 10) for i in range(80))
|
| 418 |
+
tens = "".join(
|
| 419 |
+
str((i + 1) // 10 % 10) if (i + 1) % 10 == 0 else " "
|
| 420 |
+
for i in range(80)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
)
|
| 422 |
+
out_html = (
|
| 423 |
+
'<div class="alert-ok">✓ Valid MPC record — exactly 80 characters</div>'
|
| 424 |
+
'<p style="font-family:monospace;font-size:.6rem;color:' + DIM + ';margin:0">'
|
| 425 |
+
+ tens + "</p>"
|
| 426 |
+
'<p style="font-family:monospace;font-size:.6rem;color:' + DIM + ';margin:0 0 6px">'
|
| 427 |
+
+ ruler + "</p>"
|
| 428 |
+
'<div class="mpc-wrap">' + line + "</div>"
|
| 429 |
+
)
|
| 430 |
+
return out_html, line
|
| 431 |
except Exception as exc:
|
| 432 |
+
return '<div class="alert-err">✗ ' + str(exc) + "</div>", ""
|
| 433 |
+
|
| 434 |
|
| 435 |
+
# ── Tab 4: Tracklet Visualizer ────────────────────────────────────────────────
|
| 436 |
|
|
|
|
| 437 |
def visualise_tracklet(n_dets, velocity, pa, time_span, snr_val, show_unc):
|
| 438 |
+
rng = np.random.default_rng(7)
|
| 439 |
+
n = int(n_dets)
|
| 440 |
+
times = np.linspace(0, float(time_span) * 60, n)
|
| 441 |
+
pa_r = math.radians(float(pa))
|
| 442 |
+
vel = float(velocity)
|
| 443 |
+
ra_t = [180.0 + vel * t * math.sin(pa_r) / 3600 for t in times]
|
| 444 |
+
dec_t = [0.0 + vel * t * math.cos(pa_r) / 3600 * 0.8 for t in times]
|
| 445 |
+
noise = 1 / max(float(snr_val), 0.1) * 0.0005
|
| 446 |
+
ra_o = [r + float(rng.normal(0, noise)) for r in ra_t]
|
| 447 |
+
dec_o = [d + float(rng.normal(0, noise)) for d in dec_t]
|
| 448 |
|
| 449 |
fig, axes = plt.subplots(1, 3, figsize=(14, 4.5))
|
| 450 |
fig.patch.set_facecolor(BG)
|
| 451 |
|
| 452 |
+
ax = axes[0]
|
| 453 |
+
ax.set_facecolor("#020812")
|
| 454 |
ax.tick_params(colors=DIM, labelsize=7)
|
| 455 |
+
for sp in ax.spines.values():
|
| 456 |
+
sp.set_edgecolor(SUBTLE)
|
| 457 |
ax.grid(color=SUBTLE, linewidth=0.3, alpha=0.5)
|
| 458 |
+
ax.scatter(rng.uniform(179.97, 180.03, 150), rng.uniform(-0.01, 0.01, 150),
|
| 459 |
+
s=rng.uniform(1, 5, 150), alpha=0.2, color="white", lw=0)
|
| 460 |
+
ax.plot(ra_t, dec_t, "--", color=ACCENT, lw=1, alpha=0.35, label="True path")
|
| 461 |
+
ax.scatter(ra_o, dec_o, s=55, color=ACCENT, zorder=5, lw=0)
|
| 462 |
if show_unc:
|
| 463 |
+
for rx, dy in zip(ra_o, dec_o):
|
| 464 |
+
ax.add_patch(plt.Circle((rx, dy), noise * 3, color=ACCENT,
|
| 465 |
+
alpha=0.12, fill=True, lw=0))
|
| 466 |
+
ax.scatter([ra_o[0]], [dec_o[0]], s=90, color=OK, zorder=6, lw=0, marker="*")
|
| 467 |
+
ax.scatter([ra_o[-1]], [dec_o[-1]], s=70, color=ACC2, zorder=6, lw=0, marker="D")
|
|
|
|
|
|
|
| 468 |
ax.invert_xaxis()
|
| 469 |
+
ax.set_xlabel("RA (°)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 470 |
+
ax.set_ylabel("Dec (°)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 471 |
+
ax.set_title("Sky Plane", color=TEXT, fontfamily="monospace", fontsize=8)
|
| 472 |
+
ax.legend(framealpha=0, labelcolor=DIM, fontsize=7, prop={"family": "monospace"})
|
| 473 |
+
|
| 474 |
+
ax2 = axes[1]
|
| 475 |
+
ax2.set_facecolor(PANEL)
|
| 476 |
+
ax2.tick_params(colors=DIM, labelsize=7)
|
| 477 |
+
for sp in ax2.spines.values():
|
| 478 |
+
sp.set_edgecolor(SUBTLE)
|
| 479 |
+
ax2.grid(color=SUBTLE, linewidth=0.3, alpha=0.5)
|
| 480 |
+
tm = np.array(times) / 60
|
| 481 |
+
ax2.plot(tm, np.array(ra_t) - ra_t[0], color=ACCENT, lw=2, label="ΔRA")
|
| 482 |
+
ax2.scatter(tm, np.array(ra_o) - ra_t[0], s=35, color=ACCENT, lw=0, zorder=5)
|
| 483 |
+
ax2.plot(tm, np.array(dec_t) - dec_t[0], color=ACC2, lw=2, label="ΔDec")
|
| 484 |
+
ax2.scatter(tm, np.array(dec_o) - dec_t[0], s=35, color=ACC2, lw=0, zorder=5)
|
| 485 |
+
ax2.set_xlabel("Time (min)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 486 |
+
ax2.set_ylabel("Offset (°)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 487 |
+
ax2.set_title("ΔRA / ΔDec vs Time", color=TEXT, fontfamily="monospace", fontsize=8)
|
| 488 |
+
ax2.legend(framealpha=0, labelcolor=DIM, fontsize=7, prop={"family": "monospace"})
|
| 489 |
+
|
| 490 |
+
ax3 = axes[2]
|
| 491 |
+
ax3.set_facecolor(PANEL)
|
| 492 |
+
ax3.tick_params(colors=DIM, labelsize=7)
|
| 493 |
+
for sp in ax3.spines.values():
|
| 494 |
+
sp.set_edgecolor(SUBTLE)
|
| 495 |
+
ax3.grid(color=SUBTLE, linewidth=0.3, alpha=0.5)
|
| 496 |
+
cr = np.polyfit(times, ra_o, 1)
|
| 497 |
+
cd = np.polyfit(times, dec_o, 1)
|
| 498 |
+
cos_d = math.cos(math.radians(float(np.mean(dec_o))))
|
| 499 |
+
res = np.sqrt(
|
| 500 |
+
((np.array(ra_o) - np.polyval(cr, times)) * cos_d) ** 2
|
| 501 |
+
+ (np.array(dec_o) - np.polyval(cd, times)) ** 2
|
| 502 |
+
) * 3600.0
|
| 503 |
+
rms = float(np.sqrt(np.mean(res ** 2)))
|
| 504 |
+
bw = (tm[-1] - tm[0]) / n * 0.7 if len(tm) > 1 else 0.5
|
| 505 |
+
ax3.bar(tm, res, color=ACCENT, alpha=0.75, width=bw)
|
| 506 |
+
ax3.axhline(rms, color=ACC2, lw=1.2, ls="--",
|
| 507 |
+
label="RMS=" + str(round(rms, 3)) + "″")
|
| 508 |
+
ax3.axhline(1.0, color=OK, lw=0.8, ls=":", alpha=0.6, label='1″ limit')
|
| 509 |
+
ax3.set_xlabel("Time (min)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 510 |
+
ax3.set_ylabel("Residual (arcsec)", color=DIM, fontfamily="monospace", fontsize=7)
|
| 511 |
+
ax3.set_title("Motion Residuals", color=TEXT, fontfamily="monospace", fontsize=8)
|
| 512 |
+
ax3.legend(framealpha=0, labelcolor=DIM, fontsize=7, prop={"family": "monospace"})
|
| 513 |
|
| 514 |
plt.tight_layout(pad=1.5)
|
|
|
|
| 515 |
status = (
|
| 516 |
+
'<div class="alert-info">Tracklet: ' + str(n) + " dets · " +
|
| 517 |
+
str(time_span) + " min · " + str(velocity) + " ″/s · PA " + str(pa) + "°</div>"
|
| 518 |
+
+ '<div class="' + ("alert-ok" if rms < 1.0 else "alert-warn") + '">'
|
| 519 |
+
+ "RMS = " + str(round(rms, 4)) + "″ — "
|
| 520 |
+
+ ("✓ PASS (< 1″)" if rms < 1.0 else "⚠ WARN (> 1″ limit)") + "</div>"
|
| 521 |
)
|
| 522 |
+
return img_html(fig_to_b64(fig)), status
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 523 |
|
| 524 |
|
| 525 |
+
# ── Build UI ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 526 |
|
| 527 |
+
SUB_STYLE = (
|
| 528 |
+
'style="font-family:monospace;font-size:.66rem;color:' + DIM +
|
| 529 |
+
';letter-spacing:.1em;text-transform:uppercase;padding:10px 0 2px"'
|
| 530 |
+
)
|
| 531 |
|
| 532 |
+
with gr.Blocks(css=CSS, theme=gr.themes.Base()) as demo:
|
| 533 |
gr.HTML(HEADER)
|
| 534 |
|
| 535 |
with gr.Tabs():
|
| 536 |
|
| 537 |
+
# ── Tab 1: REAL DATA ─────────────────────────────────────────────────
|
| 538 |
+
with gr.Tab("⬡ Processar Imagens IASC"):
|
| 539 |
+
gr.HTML("<p " + SUB_STYLE + ">Upload real FITS files from an IASC campaign package (4 frames recommended)</p>")
|
| 540 |
+
with gr.Row():
|
| 541 |
+
with gr.Column(scale=1, min_width=280):
|
| 542 |
+
iasc_files = gr.File(
|
| 543 |
+
label="FITS Files (upload 4 frames)",
|
| 544 |
+
file_count="multiple",
|
| 545 |
+
file_types=[".fits", ".fit", ".fts", ".fits.gz"],
|
| 546 |
+
)
|
| 547 |
+
iasc_obs = gr.Textbox(label="Observatory Code (3 chars)", value="F51", max_lines=1)
|
| 548 |
+
iasc_survey = gr.Dropdown(
|
| 549 |
+
label="Survey hint",
|
| 550 |
+
choices=["auto", "ps1", "ztf", "generic"],
|
| 551 |
+
value="auto",
|
| 552 |
+
)
|
| 553 |
+
iasc_btn = gr.Button("▶ Run Pipeline on FITS", variant="primary")
|
| 554 |
+
gr.HTML(
|
| 555 |
+
'<div class="alert-info" style="margin-top:8px">'
|
| 556 |
+
"Tip: IASC packages contain 4 FITS frames of the same field "
|
| 557 |
+
"~30 min apart. Download them from iasc.cosmosearch.org after "
|
| 558 |
+
"registering for a campaign.</div>"
|
| 559 |
+
)
|
| 560 |
+
with gr.Column(scale=3):
|
| 561 |
+
iasc_stats = gr.HTML()
|
| 562 |
+
iasc_mpc_html = gr.HTML()
|
| 563 |
+
iasc_mpc_raw = gr.Textbox(
|
| 564 |
+
label="Raw MPC Records (copy for submission)",
|
| 565 |
+
interactive=False, lines=6,
|
| 566 |
+
)
|
| 567 |
+
iasc_btn.click(
|
| 568 |
+
fn=process_iasc_fits,
|
| 569 |
+
inputs=[iasc_files, iasc_obs, iasc_survey],
|
| 570 |
+
outputs=[iasc_stats, iasc_mpc_html, iasc_mpc_raw],
|
| 571 |
+
api_name=False,
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
# ── Tab 2: Simulator ─────────────────────────────────────────────────
|
| 575 |
+
with gr.Tab("⬡ Pipeline Simulator"):
|
| 576 |
+
gr.HTML("<p " + SUB_STYLE + ">Simulate full pipeline on synthetic FITS data</p>")
|
| 577 |
with gr.Row():
|
| 578 |
with gr.Column(scale=1, min_width=240):
|
| 579 |
+
sim_nfr = gr.Slider(4, 20, value=4, step=1, label="FITS Frames")
|
| 580 |
+
sim_nas = gr.Slider(5, 50, value=10, step=1, label="Injected Asteroids")
|
| 581 |
+
sim_sm = gr.Slider(3, 20, value=5, step=0.5, label="SNR Min")
|
| 582 |
+
sim_sx = gr.Slider(5, 50, value=25, step=1, label="SNR Max")
|
| 583 |
+
sim_vm = gr.Slider(0.01, 2, value=0.1, step=0.01, label="Vel min (″/s)")
|
| 584 |
+
sim_vx = gr.Slider(0.5, 10, value=5.0, step=0.1, label="Vel max (″/s)")
|
| 585 |
+
sim_dt = gr.Slider(2.5, 8, value=3.0, step=0.5, label="Detection threshold (σ)")
|
| 586 |
+
sim_btn = gr.Button("▶ Run Simulation", variant="primary")
|
| 587 |
with gr.Column(scale=3):
|
| 588 |
+
sim_out = gr.HTML()
|
| 589 |
+
sim_btn.click(
|
| 590 |
+
fn=run_simulation,
|
| 591 |
+
inputs=[sim_nfr, sim_nas, sim_sm, sim_sx, sim_vm, sim_vx, sim_dt],
|
| 592 |
+
outputs=[sim_out],
|
| 593 |
+
api_name=False,
|
| 594 |
+
)
|
| 595 |
+
|
| 596 |
+
# ── Tab 3: MPC Formatter ─────────────────────────────────────────────
|
| 597 |
with gr.Tab("⬡ MPC Formatter"):
|
| 598 |
+
gr.HTML("<p " + SUB_STYLE + ">Generate a Minor Planet Center 80-column astrometric record</p>")
|
|
|
|
| 599 |
with gr.Row():
|
| 600 |
with gr.Column(scale=1):
|
| 601 |
mpc_d = gr.Textbox(label="Provisional Designation", value="2026 AA1")
|
|
|
|
| 607 |
mpc_dy = gr.Number(label="Day (DD.ddddd)", value=20.50000)
|
| 608 |
mpc_mg = gr.Number(label="Magnitude", value=18.5)
|
| 609 |
mpc_bd = gr.Textbox(label="Filter Band", value="R", max_lines=1)
|
| 610 |
+
mpc_oc = gr.Textbox(label="Observatory Code", value="F51", max_lines=1)
|
| 611 |
mpc_bt = gr.Button("Generate MPC Record", variant="primary")
|
| 612 |
with gr.Column(scale=2):
|
| 613 |
mpc_out = gr.HTML()
|
| 614 |
+
mpc_raw = gr.Textbox(label="Raw 80-column line", interactive=False, lines=2)
|
| 615 |
+
mpc_bt.click(
|
| 616 |
+
fn=format_mpc,
|
| 617 |
+
inputs=[mpc_d, mpc_ra, mpc_dc, mpc_yr, mpc_mo, mpc_dy, mpc_mg, mpc_bd, mpc_oc],
|
| 618 |
+
outputs=[mpc_out, mpc_raw],
|
| 619 |
+
api_name=False,
|
| 620 |
+
)
|
| 621 |
+
|
| 622 |
+
# ── Tab 4: Tracklet Visualizer ───────────────────────────────────────
|
| 623 |
with gr.Tab("⬡ Tracklet Visualizer"):
|
| 624 |
+
gr.HTML("<p " + SUB_STYLE + ">Inspect multi-frame tracklet motion and linear residuals</p>")
|
|
|
|
| 625 |
with gr.Row():
|
| 626 |
with gr.Column(scale=1):
|
| 627 |
+
t_n = gr.Slider(3, 10, value=5, step=1, label="Detections")
|
| 628 |
+
t_v = gr.Slider(0.01, 8, value=0.5, step=0.01, label="Velocity (″/s)")
|
| 629 |
+
t_pa = gr.Slider(0, 360, value=135, step=1, label="Position Angle (°)")
|
| 630 |
+
t_sp = gr.Slider(30, 240, value=90, step=5, label="Time Span (min)")
|
| 631 |
+
t_sn = gr.Slider(3, 30, value=10, step=0.5, label="SNR")
|
| 632 |
t_uc = gr.Checkbox(label="Show position uncertainties", value=True)
|
| 633 |
t_bt = gr.Button("Plot Tracklet", variant="primary")
|
| 634 |
with gr.Column(scale=3):
|
| 635 |
t_img = gr.HTML()
|
| 636 |
t_st = gr.HTML()
|
| 637 |
+
t_bt.click(
|
| 638 |
+
fn=visualise_tracklet,
|
| 639 |
+
inputs=[t_n, t_v, t_pa, t_sp, t_sn, t_uc],
|
| 640 |
+
outputs=[t_img, t_st],
|
| 641 |
+
api_name=False,
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
# ── Tab 5: About ─────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
with gr.Tab("⬡ About"):
|
| 646 |
+
gr.HTML("""
|
| 647 |
+
<div style="max-width:800px;margin:20px auto;font-family:'Space Mono',monospace">
|
| 648 |
+
<h2 style="color:#00D4FF;font-family:'Syne',sans-serif;font-weight:800;font-size:1.4rem;margin-bottom:4px">AsteroidNET v0.2</h2>
|
| 649 |
+
<p style="color:#4A6070;font-size:.66rem;letter-spacing:.14em;text-transform:uppercase;margin-bottom:18px">
|
| 650 |
+
Automated NEO Detection • Dr. Matheus Machado Rech</p>
|
| 651 |
+
<div style="background:#0A0E1A;border:1px solid #1E2D40;border-radius:4px;padding:18px 22px;margin-bottom:14px">
|
| 652 |
+
<h3 style="color:#C8D8E8;font-size:.8rem;margin:0 0 10px;letter-spacing:.08em;text-transform:uppercase">What is new in v0.2</h3>
|
| 653 |
+
<ul style="color:#4A6070;font-size:.72rem;line-height:1.8;padding-left:1.2em">
|
| 654 |
+
<li><b style="color:#00D4FF">Real FITS support</b> — upload IASC campaign packages directly</li>
|
| 655 |
+
<li><b style="color:#00D4FF">TAI/UTC correction</b> — PS1 MJD-OBS (TAI) vs ZTF (UTC), 37s offset handled</li>
|
| 656 |
+
<li><b style="color:#00D4FF">Byte-order fix</b> — FITS big-endian converted to float32 native before Background2D</li>
|
| 657 |
+
<li><b style="color:#00D4FF">SkyBoT integration</b> — IMCCE cone search removes known SSOs from candidates</li>
|
| 658 |
+
<li><b style="color:#00D4FF">Two-pass background</b> — source masking for unbiased sky estimation</li>
|
| 659 |
+
<li><b style="color:#00D4FF">ZTF support</b> — IRSA IBE API for multi-epoch science images</li>
|
| 660 |
+
<li><b style="color:#00D4FF">Training data builder</b> — mine PS1/ZTF with MPC labels for classifier training</li>
|
| 661 |
+
<li><b style="color:#00D4FF">GitHub Actions CI/CD</b> — auto-deploys to HuggingFace Spaces on push</li>
|
| 662 |
+
</ul>
|
| 663 |
+
</div>
|
| 664 |
+
<div style="background:#0A0E1A;border:1px solid #1E2D40;border-radius:4px;padding:18px 22px">
|
| 665 |
+
<h3 style="color:#C8D8E8;font-size:.8rem;margin:0 0 10px;letter-spacing:.08em;text-transform:uppercase">How to use with IASC</h3>
|
| 666 |
+
<ol style="color:#4A6070;font-size:.72rem;line-height:1.8;padding-left:1.2em">
|
| 667 |
+
<li>Register at <a href="https://iasc.cosmosearch.org" style="color:#00D4FF">iasc.cosmosearch.org</a></li>
|
| 668 |
+
<li>Download a campaign FITS package (4 frames, ~30 min cadence)</li>
|
| 669 |
+
<li>Upload all 4 .fits files in the <b style="color:#00D4FF">Processar Imagens IASC</b> tab</li>
|
| 670 |
+
<li>Enter your observatory code (F51 for Pan-STARRS; 500 for generic)</li>
|
| 671 |
+
<li>Click Run Pipeline — MPC records generated automatically</li>
|
| 672 |
+
<li>Submit records to IASC for verification and MPC submission</li>
|
| 673 |
+
</ol>
|
| 674 |
+
</div>
|
| 675 |
+
</div>""")
|
| 676 |
+
|
| 677 |
+
gr.HTML(
|
| 678 |
+
'<div style="text-align:center;padding:14px;font-family:monospace;font-size:.6rem;'
|
| 679 |
+
"color:" + DIM + ";letter-spacing:.1em;border-top:1px solid " + SUBTLE + '">'
|
| 680 |
+
"AsteroidNET · Dr. Matheus Machado Rech · github.com/mmrech/asteroidnet</div>"
|
| 681 |
+
)
|
| 682 |
|
|
|
|
|
|
|
|
|
|
| 683 |
|
| 684 |
if __name__ == "__main__":
|
| 685 |
demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
|