mmrech commited on
Commit
b87d86f
·
verified ·
1 Parent(s): 41d98e2

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +567 -512
app.py CHANGED
@@ -1,7 +1,15 @@
1
  """
2
- AsteroidNET Gradio UI (Gradio 4.44.0 compatible)
3
- Five-tab interactive demo of the AsteroidNET NEO detection pipeline.
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 = f"""
29
  @import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&display=swap');
30
- :root{{--bg:{BG};--panel:{PANEL};--accent:{ACCENT};--acc2:{ACC2};--ok:{OK};--subtle:{SUBTLE};--text:{TEXT};--dim:{DIM};}}
31
- body,.gradio-container{{background:var(--bg)!important;font-family:'Syne',sans-serif!important;color:var(--text)!important;}}
32
- .asteroid-header{{background:linear-gradient(135deg,{BG} 0%,#0A1628 60%);border-bottom:1px solid {ACCENT}33;padding:24px 32px 18px;position:relative;overflow:hidden;}}
33
- .asteroid-header::before{{content:'';position:absolute;inset:0;background:radial-gradient(ellipse 60% 80% at 80% 50%,{ACCENT}08,transparent 70%);pointer-events:none;}}
34
- .asteroid-title{{font-family:'Syne',sans-serif;font-weight:800;font-size:2.1rem;letter-spacing:-.04em;color:#FFF;margin:0;}}
35
- .asteroid-title span{{color:{ACCENT};}}
36
- .asteroid-sub{{font-family:'Space Mono',monospace;font-size:.68rem;color:{DIM};letter-spacing:.18em;text-transform:uppercase;margin-top:5px;}}
37
- .badge-row{{display:flex;gap:7px;margin-top:12px;flex-wrap:wrap;}}
38
- .badge{{font-family:'Space Mono',monospace;font-size:.6rem;letter-spacing:.1em;padding:3px 8px;border-radius:2px;font-weight:700;text-transform:uppercase;}}
39
- .bc{{background:{ACCENT}18;border:1px solid {ACCENT}55;color:{ACCENT};}}
40
- .ba{{background:{ACC2}18;border:1px solid {ACC2}55;color:{ACC2};}}
41
- .bg{{background:{OK}18;border:1px solid {OK}55;color:{OK};}}
42
- .tabs>.tab-nav{{background:var(--panel)!important;border-bottom:1px solid var(--subtle)!important;}}
43
- .tabs>.tab-nav>button{{font-family:'Space Mono',monospace!important;font-size:.68rem!important;letter-spacing:.1em!important;text-transform:uppercase!important;color:var(--dim)!important;border:none!important;border-bottom:2px solid transparent!important;background:transparent!important;padding:11px 16px!important;transition:all .2s!important;}}
44
- .tabs>.tab-nav>button.selected,.tabs>.tab-nav>button:hover{{color:{ACCENT}!important;border-bottom-color:{ACCENT}!important;}}
45
- .gr-box,.gr-form,.gr-panel{{background:var(--panel)!important;border:1px solid var(--subtle)!important;border-radius:4px!important;}}
46
- label,.gr-label{{font-family:'Space Mono',monospace!important;font-size:.66rem!important;letter-spacing:.1em!important;text-transform:uppercase!important;color:var(--dim)!important;}}
47
- button.primary{{background:{ACCENT}!important;color:{BG}!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;}}
48
- button.secondary{{background:transparent!important;color:{ACCENT}!important;border:1px solid {ACCENT}!important;font-family:'Space Mono',monospace!important;font-size:.7rem!important;letter-spacing:.1em!important;border-radius:4px!important;}}
49
- input,textarea,select{{background:{SUBTLE}!important;border:1px solid #2A3D50!important;color:var(--text)!important;font-family:'Space Mono',monospace!important;font-size:.78rem!important;border-radius:4px!important;}}
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
- # ── Helpers ─────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 f"data:image/png;base64,{b64}"
 
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(): sp.set_edgecolor(SUBTLE)
 
83
  ax.grid(color=SUBTLE, linewidth=0.4, alpha=0.5)
84
  return fig, ax
85
 
 
86
  def img_html(b64, label=""):
87
- lbl = (f'<p style="font-family:monospace;font-size:.65rem;color:{DIM};'
88
- f'text-transform:uppercase;letter-spacing:.1em;margin:0 0 6px">{label}</p>') if label else ""
89
- return f'<div style="margin:4px 0">{lbl}<img src="{b64}" style="width:100%;border-radius:4px;border:1px solid {SUBTLE}"></div>'
90
-
91
-
92
-
93
-
94
- # ── Tab 1: Pipeline Runner ───────────────────
95
- def run_pipeline(n_frames, n_asteroids, snr_min, snr_max, vel_min, vel_max, det_thresh):
96
- rng = np.random.default_rng(42)
97
- n_ast = int(n_asteroids)
98
- n_frm = int(n_frames)
99
- velocities = rng.uniform(vel_min, vel_max, n_ast)
100
- snrs = rng.uniform(snr_min, snr_max, n_ast)
101
- pas = rng.uniform(0, 360, n_ast)
102
- ra0 = rng.uniform(179.5, 180.5, n_ast)
103
- dec0 = rng.uniform(-0.5, 0.5, n_ast)
104
-
105
- n_stars = int(rng.integers(800, 1200))
106
- valid_trklets = int(rng.integers(max(1, n_ast - 2), n_ast + 1))
107
- confirmed = int(valid_trklets * rng.uniform(0.88, 1.0))
108
- false_pos = max(0, int(confirmed * rng.uniform(0, 0.015)))
109
- confirmed_real = confirmed - false_pos
110
-
111
- # ── Detection table ──────────────────────
112
- rows = []
113
- for i in range(confirmed_real):
114
- v = float(velocities[i % n_ast])
115
- snr = float(snrs[i % n_ast])
116
- pri = "HAZARDOUS" if v > 3.0 else ("HIGH" if v > 1.0 else "ROUTINE")
117
- a = max(0.5, round(2.5 / (v**0.3) + float(rng.normal(0, 0.1)), 3))
118
- rows.append({
119
- "ID": f"2026 {'ABCDEFGHJ'[i%9]}{chr(65+(i//9)%26)}{i+1}",
120
- "RA (°)": round(float(ra0[i%n_ast]) + v*0.0002, 4),
121
- "Dec (°)": round(float(dec0[i%n_ast]) + v*0.0001, 4),
122
- "Vel (″/s)": round(v, 4),
123
- "SNR": round(snr, 1),
124
- "RF Score": round(float(rng.uniform(0.71, 0.99)), 3),
125
- "CNN Score": round(float(rng.uniform(0.90, 0.999)), 3),
126
- "a (AU)": a,
127
- "Priority": pri,
128
- })
129
- df = pd.DataFrame(rows) if rows else pd.DataFrame(
130
- columns=["ID","RA (°)","Dec (°)","Vel (″/s)","SNR","RF Score","CNN Score","a (AU)","Priority"])
131
-
132
- # ── Sky chart ────────────────────────────
133
- fig1, ax = dark_fig(10, 5.2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  ax.set_facecolor("#020812")
135
- ax.scatter(rng.uniform(179.2,180.8,600), rng.uniform(-0.8,0.8,600),
136
- s=rng.uniform(1,8,600), alpha=rng.uniform(.05,.4,600), color="white", lw=0)
137
- dt_hr = n_frm * 10 / 60
138
- for i in range(n_ast):
139
- par = math.radians(float(pas[i]))
140
- re = float(ra0[i]) + velocities[i]*dt_hr*3600/3600*math.cos(par)/3600
141
- de = float(dec0[i]) + velocities[i]*dt_hr*3600/3600*math.sin(par)/3600*0.1
142
- col = ACC2 if velocities[i]>2.0 else (WARN if velocities[i]>1.0 else ACCENT)
143
- alph = 0.5 + 0.5*(snrs[i]-snr_min)/max(snr_max-snr_min,1)
144
- ax.annotate("", xy=(re,de), xytext=(float(ra0[i]),float(dec0[i])),
145
- arrowprops=dict(arrowstyle="-|>",color=col,lw=1.4,mutation_scale=10,alpha=alph))
146
- ax.scatter([ra0[i]],[dec0[i]],s=20,color=col,zorder=5,lw=0)
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("Candidate Motion Field", color=TEXT, fontfamily="monospace", fontsize=9)
155
- ax.invert_xaxis()
156
- sky_b64 = fig_to_b64(fig1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, label="Recovery rate")
164
- ax2.axvline(det_thresh, color=ACC2, lw=1.2, ls="--", label=f"Threshold {det_thresh}σ")
165
- ax2.axhline(90, color=OK, lw=0.8, ls=":", alpha=0.7, label="90% target")
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.set_ylim(0,105)
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
- stats_html = f"""
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
- # ── Detection table as HTML ──────────────
219
- if rows:
220
- th_style = f"padding:6px 10px;background:{SUBTLE};color:{ACCENT};font-family:monospace;font-size:.62rem;text-transform:uppercase;letter-spacing:.08em;text-align:left;border-bottom:1px solid {SUBTLE}"
221
- td_style = f"padding:5px 10px;font-family:monospace;font-size:.68rem;color:{TEXT};border-bottom:1px solid {SUBTLE}33"
222
- headers = ["ID","RA (°)","Dec (°)","Vel (″/s)","SNR","RF Score","CNN Score","a (AU)","Priority"]
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 astropy.coordinates import SkyCoord
253
- coord = SkyCoord(ra=float(ra_deg)*u.deg, dec=float(dec_deg)*u.deg)
254
- ra_h = int(coord.ra.hms.h); ra_m = int(coord.ra.hms.m); ra_s = coord.ra.hms.s
255
- ra_str = f"{ra_h:02d} {ra_m:02d} {ra_s:05.2f}"
256
- sign = "+" if coord.dec.deg >= 0 else "-"
257
- d = abs(coord.dec.deg); dd = int(d); dm = int((d-dd)*60); ds = ((d-dd)*60-dm)*60
258
- dec_str = f"{sign}{dd:02d} {dm:02d} {ds:04.1f}"
259
- date_str = f"{int(yr):04d} {int(mo):02d} {float(day_frac):08.5f}"
260
- try: mag_f = f"{float(mag):4.1f}"
261
- except: mag_f = " "
262
- buf = bytearray(b" " * 80)
263
- def place(s, start, w):
264
- buf[start:start+w] = s[:w].ljust(w).encode("ascii", errors="replace")
265
- place(str(desig)[:5].ljust(5), 0, 5); place("C", 8, 1)
266
- place(date_str[:8], 9, 8); place(date_str[8:16], 17, 8)
267
- place(ra_str, 26, 11); place(dec_str, 37, 11)
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
- return mpc_html, line
 
 
 
 
 
 
 
 
304
  except Exception as exc:
305
- return f'<div class="alert-err">✗ {exc}</div>', ""
 
306
 
 
307
 
308
- # ── Tab 3: Tracklet Visualizer ───────────────
309
  def visualise_tracklet(n_dets, velocity, pa, time_span, snr_val, show_unc):
310
- rng = np.random.default_rng(7)
311
- n = int(n_dets)
312
- times = np.linspace(0, float(time_span)*60, n)
313
- pa_r = math.radians(float(pa)); vel = float(velocity)
314
- ra_t = [180.0 + vel*t*math.sin(pa_r)/3600 for t in times]
315
- dec_t = [0.0 + vel*t*math.cos(pa_r)/3600*0.8 for t in times]
316
- noise = 1/max(float(snr_val),.1)*0.0005
317
- ra_o = [r+float(rng.normal(0,noise)) for r in ra_t]
318
- dec_o = [d+float(rng.normal(0,noise)) for d in dec_t]
 
319
 
320
  fig, axes = plt.subplots(1, 3, figsize=(14, 4.5))
321
  fig.patch.set_facecolor(BG)
322
 
323
- # Sky plane
324
- ax = axes[0]; ax.set_facecolor("#020812")
325
  ax.tick_params(colors=DIM, labelsize=7)
326
- for sp in ax.spines.values(): sp.set_edgecolor(SUBTLE)
 
327
  ax.grid(color=SUBTLE, linewidth=0.3, alpha=0.5)
328
- ax.scatter(rng.uniform(179.97,180.03,200),rng.uniform(-0.015,0.015,200),
329
- s=rng.uniform(1,7,200),alpha=rng.uniform(.1,.4,200),color="white",lw=0)
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,alpha=0.12,fill=True,lw=0))
335
- ax.scatter([ra_o[0]],[dec_o[0]],s=90,color=OK,zorder=6,lw=0,marker="*")
336
- ax.scatter([ra_o[-1]],[dec_o[-1]],s=70,color=ACC2,zorder=6,lw=0,marker="D")
337
- ax.set_xlabel("RA (°)",color=DIM,fontfamily="monospace",fontsize=7)
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.legend(framealpha=0,labelcolor=DIM,fontsize=7,prop={"family":"monospace"})
342
-
343
- # RA/Dec vs time
344
- ax2 = axes[1]; ax2.set_facecolor(PANEL)
345
- ax2.tick_params(colors=DIM,labelsize=7)
346
- for sp in ax2.spines.values(): sp.set_edgecolor(SUBTLE)
347
- ax2.grid(color=SUBTLE,linewidth=0.3,alpha=0.5)
348
- tm = times/60
349
- ax2.plot(tm,np.array(ra_t)-ra_t[0],color=ACCENT,lw=2,label="ΔRA")
350
- ax2.scatter(tm,np.array(ra_o)-ra_t[0],s=35,color=ACCENT,lw=0,zorder=5)
351
- ax2.plot(tm,np.array(dec_t)-dec_t[0],color=ACC2,lw=2,label="ΔDec")
352
- ax2.scatter(tm,np.array(dec_o)-dec_t[0],s=35,color=ACC2,lw=0,zorder=5)
353
- ax2.set_xlabel("Time (min)",color=DIM,fontfamily="monospace",fontsize=7)
354
- ax2.set_ylabel("Offset (°)",color=DIM,fontfamily="monospace",fontsize=7)
355
- ax2.set_title("ΔRA / ΔDec vs Time",color=TEXT,fontfamily="monospace",fontsize=8)
356
- ax2.legend(framealpha=0,labelcolor=DIM,fontsize=7,prop={"family":"monospace"})
357
-
358
- # Residuals
359
- ax3 = axes[2]; ax3.set_facecolor(PANEL)
360
- ax3.tick_params(colors=DIM,labelsize=7)
361
- for sp in ax3.spines.values(): sp.set_edgecolor(SUBTLE)
362
- ax3.grid(color=SUBTLE,linewidth=0.3,alpha=0.5)
363
- cr = np.polyfit(times,ra_o,1); cd = np.polyfit(times,dec_o,1)
364
- res = np.sqrt((np.array(ra_o)-np.polyval(cr,times))**2
365
- + (np.array(dec_o)-np.polyval(cd,times))**2)*3600
366
- rms = float(np.sqrt(np.mean(res**2)))
367
- bw = (tm[-1]-tm[0])/n*0.7 if len(tm)>1 else 0.5
368
- ax3.bar(tm,res,color=ACCENT,alpha=0.75,width=bw)
369
- ax3.axhline(rms,color=ACC2,lw=1.2,ls="--",label=f"RMS={rms:.3f}″")
370
- ax3.axhline(1.0,color=OK,lw=0.8,ls=":",alpha=0.6,label='1″ limit')
371
- ax3.set_xlabel("Time (min)",color=DIM,fontfamily="monospace",fontsize=7)
372
- ax3.set_ylabel("Residual (arcsec)",color=DIM,fontfamily="monospace",fontsize=7)
373
- ax3.set_title("Motion Residuals",color=TEXT,fontfamily="monospace",fontsize=8)
374
- ax3.legend(framealpha=0,labelcolor=DIM,fontsize=7,prop={"family":"monospace"})
 
 
 
 
 
 
 
 
 
 
375
 
376
  plt.tight_layout(pad=1.5)
377
- img_b64 = fig_to_b64(fig)
378
  status = (
379
- f'<div class="alert-info">Tracklet: {n} dets · {time_span} min · {velocity} ″/s · PA {pa}°</div>'
380
- f'<div class="{"alert-ok" if rms<1.0 else "alert-warn"}">'
381
- f'RMS = {rms:.4f}″ — {" PASS (< 1″ limit)" if rms<1.0 else " WARN (> 1″ limit)"}</div>'
 
 
382
  )
383
- return img_html(img_b64), status
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
- # ── About ────────────────────────────────────
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">&lt;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">&lt;4hr</div><div class="stat-label">Processing time</div></div>
503
- <div class="stat-card"><div class="stat-val">&gt;99.5%</div><div class="stat-label">Star removal</div></div>
504
- <div class="stat-card"><div class="stat-val">&lt;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
- # ── Build UI ─────────────────────────────────
531
- with gr.Blocks(css=CSS, theme=gr.themes.Base()) as demo:
 
 
532
 
 
533
  gr.HTML(HEADER)
534
 
535
  with gr.Tabs():
536
 
537
- with gr.Tab("⬡ Pipeline Runner"):
538
- gr.HTML(f'<p style="font-family:monospace;font-size:.66rem;color:{DIM};letter-spacing:.1em;'
539
- f'text-transform:uppercase;padding:10px 0 2px">Simulate a full pipeline run on synthetic FITS data</p>')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
  with gr.Row():
541
  with gr.Column(scale=1, min_width=240):
542
- n_fr = gr.Slider(5, 100, value=20, step=5, label="FITS Frames")
543
- n_as = gr.Slider(5, 80, value=20, step=1, label="Injected Asteroids")
544
- sm = gr.Slider(3, 30, value=5, step=0.5, label="SNR Min")
545
- sx = gr.Slider(5, 50, value=25, step=1, label="SNR Max")
546
- vm = gr.Slider(0.01, 2, value=0.05, step=0.01, label='Vel min (″/s)')
547
- vx = gr.Slider(0.5, 10, value=5, step=0.1, label='Vel max (″/s)')
548
- dt = gr.Slider(2.5, 8, value=3.0, step=0.5, label="Detection threshold (σ)")
549
- rb = gr.Button("▶ Run Pipeline", variant="primary")
550
  with gr.Column(scale=3):
551
- pipeline_out = gr.HTML() # stats + charts + table all in one HTML block
552
- rb.click(fn=run_pipeline,
553
- inputs=[n_fr, n_as, sm, sx, vm, vx, dt],
554
- outputs=[pipeline_out],
555
- api_name=False)
556
-
 
 
 
557
  with gr.Tab("⬡ MPC Formatter"):
558
- gr.HTML(f'<p style="font-family:monospace;font-size:.66rem;color:{DIM};letter-spacing:.1em;'
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 (3 chars)", value="F51", max_lines=1)
572
  mpc_bt = gr.Button("Generate MPC Record", variant="primary")
573
  with gr.Column(scale=2):
574
  mpc_out = gr.HTML()
575
- mpc_rw = gr.Textbox(label="Raw 80-column line (copy for submission)",
576
- interactive=False, lines=2)
577
- mpc_bt.click(fn=format_mpc,
578
- inputs=[mpc_d,mpc_ra,mpc_dc,mpc_yr,mpc_mo,mpc_dy,mpc_mg,mpc_bd,mpc_oc],
579
- outputs=[mpc_out, mpc_rw],
580
- api_name=False)
581
-
 
 
582
  with gr.Tab("⬡ Tracklet Visualizer"):
583
- gr.HTML(f'<p style="font-family:monospace;font-size:.66rem;color:{DIM};letter-spacing:.1em;'
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, value=5, step=1, label="Detections")
588
- t_v = gr.Slider(0.01, 8,value=0.5, step=0.01, label='Velocity (″/s)')
589
- t_pa = gr.Slider(0, 360, value=135, step=1, label="Position Angle (°)")
590
- t_sp = gr.Slider(30, 240, value=90, step=5, label="Time Span (min)")
591
- t_sn = gr.Slider(3, 30, value=10, step=0.5, label="SNR")
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(fn=visualise_tracklet,
598
- inputs=[t_n, t_v, t_pa, t_sp, t_sn, t_uc],
599
- outputs=[t_img, t_st],
600
- api_name=False)
601
-
602
- with gr.Tab("⬡ Orbit Inspector"):
603
- gr.HTML(f'<p style="font-family:monospace;font-size:.66rem;color:{DIM};letter-spacing:.1em;'
604
- f'text-transform:uppercase;padding:10px 0 2px">Preliminary Keplerian orbital elements via Gauss method</p>')
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(ABOUT_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 &middot; 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 &bull; 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)