asteroidnet2 / app.py
mmrech's picture
Upload app.py with huggingface_hub
b87d86f verified
"""
AsteroidNET Gradio UI v0.2 — Python 3.11 compatible (no backslashes in f-strings)
Five tabs including the new "Processar Imagens IASC" tab with real FITS upload.
"""
import io
import math
import base64
import warnings
import tempfile
import logging
from pathlib import Path
import gradio as gr
import numpy as np
import pandas as pd
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from astropy.time import Time
import astropy.units as u
warnings.filterwarnings("ignore")
logging.basicConfig(level=logging.WARNING)
# ── Palette ──────────────────────────────────────────────────────────────────
BG = "#04060D"
PANEL = "#0A0E1A"
ACCENT = "#00D4FF"
ACC2 = "#FF6B2B"
WARN = "#FFD700"
OK = "#39FF14"
SUBTLE = "#1E2D40"
TEXT = "#C8D8E8"
DIM = "#4A6070"
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;800&display=swap');
body,.gradio-container{background:#04060D!important;font-family:'Syne',sans-serif!important;color:#C8D8E8!important}
.tabs>.tab-nav{background:#0A0E1A!important;border-bottom:1px solid #1E2D40!important}
.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}
.tabs>.tab-nav>button.selected,.tabs>.tab-nav>button:hover{color:#00D4FF!important;border-bottom-color:#00D4FF!important}
.gr-box,.gr-form,.gr-panel{background:#0A0E1A!important;border:1px solid #1E2D40!important;border-radius:4px!important}
label,.gr-label{font-family:'Space Mono',monospace!important;font-size:.66rem!important;letter-spacing:.1em!important;text-transform:uppercase!important;color:#4A6070!important}
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}
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}
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}
input[type=range]{accent-color:#00D4FF!important}
.stat-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:9px;margin:14px 0}
.stat-card{background:#1E2D40;border:1px solid #1E3048;border-radius:4px;padding:13px 14px;text-align:center}
.stat-val{font-family:'Space Mono',monospace;font-size:1.5rem;font-weight:700;line-height:1;color:#00D4FF}
.stat-val.a{color:#FF6B2B}.stat-val.g{color:#39FF14}.stat-val.w{color:#FFD700}
.stat-label{font-family:'Space Mono',monospace;font-size:.58rem;letter-spacing:.1em;text-transform:uppercase;color:#4A6070;margin-top:4px}
.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}
.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}
.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}
.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}
.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}
"""
HEADER = """
<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">
<h1 style="font-family:'Syne',sans-serif;font-weight:800;font-size:2.1rem;letter-spacing:-.04em;color:#FFF;margin:0">
Asteroid<span style="color:#00D4FF">NET</span>
</h1>
<p style="font-family:'Space Mono',monospace;font-size:.68rem;color:#4A6070;letter-spacing:.18em;text-transform:uppercase;margin-top:5px">
Automated Near-Earth Object Detection &middot; v0.2.0
</p>
<div style="display:flex;gap:7px;margin-top:12px;flex-wrap:wrap">
<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>
<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>
<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>
<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>
<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>
</div>
</div>"""
# ── Shared helpers ────────────────────────────────────────────────────────────
def fig_to_b64(fig):
buf = io.BytesIO()
fig.savefig(buf, format="png", dpi=130, bbox_inches="tight",
facecolor=BG, edgecolor="none")
buf.seek(0)
b64 = base64.b64encode(buf.read()).decode()
plt.close(fig)
return "data:image/png;base64," + b64
def dark_fig(w=9, h=5):
fig, ax = plt.subplots(figsize=(w, h))
fig.patch.set_facecolor(BG)
ax.set_facecolor(PANEL)
ax.tick_params(colors=DIM, labelsize=7)
for sp in ax.spines.values():
sp.set_edgecolor(SUBTLE)
ax.grid(color=SUBTLE, linewidth=0.4, alpha=0.5)
return fig, ax
def img_html(b64, label=""):
lbl = ""
if label:
lbl = (
'<p style="font-family:monospace;font-size:.65rem;color:' + DIM +
';text-transform:uppercase;letter-spacing:.1em;margin:0 0 6px">' +
label + "</p>"
)
return (
'<div style="margin:4px 0">' + lbl +
'<img src="' + b64 + '" style="width:100%;border-radius:4px;border:1px solid ' + SUBTLE + '">' +
"</div>"
)
def stat_card(val, label, cls=""):
return (
'<div class="stat-card">'
'<div class="stat-val ' + cls + '">' + str(val) + "</div>"
'<div class="stat-label">' + label + "</div>"
"</div>"
)
# ── Tab 1: Processar Imagens IASC (REAL FITS) ────────────────────────────────
def process_iasc_fits(fits_files, obs_code, survey_hint):
"""Process real FITS files uploaded by the user."""
if not fits_files:
return '<div class="alert-warn">⚠ Please upload at least 2 FITS files.</div>', "", ""
import sys
sys.path.insert(0, str(Path(__file__).parent))
# Save uploaded files to temp dir
with tempfile.TemporaryDirectory() as tmpdir:
tmp = Path(tmpdir)
paths = []
for f in fits_files:
p = tmp / Path(f).name
import shutil
shutil.copy(f, p)
paths.append(p)
if len(paths) < 2:
return '<div class="alert-err">✗ Need at least 2 FITS frames.</div>', "", ""
try:
from asteroidnet.pipeline.runner import run_pipeline
result = run_pipeline(paths, observatory_code=obs_code or "???")
except Exception as exc:
return (
'<div class="alert-err">✗ Pipeline error: ' + str(exc)[:300] + "</div>",
"", ""
)
# Build summary HTML
pri_counts = {}
for cl in result.classifications:
pri_counts[cl.priority] = pri_counts.get(cl.priority, 0) + 1
n_haz = pri_counts.get("HAZARDOUS", 0)
n_high = pri_counts.get("HIGH", 0)
n_rout = pri_counts.get("ROUTINE", 0)
rec_pct = round(result.n_confirmed / max(result.n_candidates, 1) * 100, 1)
elapsed = round(result.elapsed_s, 2)
stats_html = (
'<div class="stat-grid">'
+ stat_card(result.n_frames, "Frames ingested")
+ stat_card(result.n_candidates, "Tracklet candidates", "a")
+ stat_card(result.n_confirmed, "Confirmed NEOs", "g")
+ stat_card(str(elapsed) + "s", "Pipeline time", "w")
+ stat_card(n_high, "HIGH alerts", "w")
+ stat_card(n_haz, "HAZARDOUS", "a" if n_haz > 0 else "")
+ "</div>"
)
# Status messages
if result.n_confirmed > 0:
stats_html += (
'<div class="alert-ok">✓ ' + str(result.n_confirmed) +
" new candidate(s) detected — review MPC records below</div>"
)
else:
msg = "No new moving objects detected"
if result.n_candidates == 0:
msg += " (no tracklet candidates found — try more frames or lower threshold)"
stats_html += '<div class="alert-warn">⚠ ' + msg + "</div>"
# Sky motion chart
if result.classifications:
stats_html += _make_detection_chart(result)
# Detection table
if result.classifications:
rows = []
for cl in result.classifications:
t = cl.tracklet
d0 = t.detections[0]
rows.append({
"RA (°)": round(d0["ra"], 5),
"Dec (°)": round(d0["dec"], 5),
"Vel (″/s)": round(t.velocity_arcsec_s, 4),
"PA (°)": round(t.position_angle_deg, 1),
"Detections": len(t.detections),
"Arc (min)": round(t.time_span_min, 1),
"RMS (″)": round(t.rms_residual_arcsec, 3),
"RF": round(cl.rf_score, 3),
"CNN": round(cl.cnn_score, 3),
"Priority": cl.priority,
})
df = pd.DataFrame(rows)
header_cells = "".join(
'<th style="padding:5px 8px;background:' + SUBTLE + ';color:' + ACCENT +
';font-family:monospace;font-size:.62rem;text-transform:uppercase;'
'letter-spacing:.06em;text-align:left;border-bottom:1px solid ' + SUBTLE + '">'
+ h + "</th>"
for h in df.columns
)
td_s = (
"padding:5px 8px;font-family:monospace;font-size:.68rem;color:" + TEXT +
";border-bottom:1px solid rgba(30,45,64,.5)"
)
body = ""
for _, row in df.iterrows():
cells = "".join(
'<td style="' + td_s + ';color:' +
("#FF4444" if str(v) == "HAZARDOUS" else WARN if str(v) == "HIGH" else TEXT) +
'">' + str(v) + "</td>"
for v in row
)
body += "<tr>" + cells + "</tr>"
stats_html += (
'<div style="overflow-x:auto;margin-top:14px">'
'<p style="font-family:monospace;font-size:.65rem;color:' + DIM +
';text-transform:uppercase;letter-spacing:.1em;margin:0 0 6px">Detection Table</p>'
'<table style="width:100%;border-collapse:collapse;background:' + PANEL + '">'
"<thead><tr>" + header_cells + "</tr></thead>"
"<tbody>" + body + "</tbody></table></div>"
)
# MPC output
mpc_text = "\n".join(result.mpc_records) if result.mpc_records else ""
mpc_html = ""
if mpc_text:
ruler = "".join(str((i + 1) % 10) for i in range(80))
tens = "".join(
str((i + 1) // 10 % 10) if (i + 1) % 10 == 0 else " "
for i in range(80)
)
mpc_html = (
'<div class="alert-ok" style="margin-bottom:8px">'
"✓ " + str(len(result.mpc_records)) + " MPC records generated</div>"
'<p style="font-family:monospace;font-size:.6rem;color:' + DIM +
';margin:0">' + tens + "</p>"
'<p style="font-family:monospace;font-size:.6rem;color:' + DIM +
';margin:0 0 6px">' + ruler + "</p>"
+ "".join(
'<div class="mpc-wrap" style="margin-bottom:4px">' + line + "</div>"
for line in result.mpc_records[:20]
)
)
if len(result.mpc_records) > 20:
mpc_html += (
'<div class="alert-info">' +
str(len(result.mpc_records) - 20) + " more records in raw output</div>"
)
return stats_html, mpc_html, mpc_text
def _make_detection_chart(result) -> str:
"""Build sky motion chart for confirmed detections."""
fig, ax = dark_fig(10, 4.5)
ax.set_facecolor("#020812")
for cl in result.classifications:
t = cl.tracklet
ras = [d["ra"] for d in t.detections]
decs = [d["dec"] for d in t.detections]
col = "#FF4444" if cl.priority == "HAZARDOUS" else WARN if cl.priority == "HIGH" else ACCENT
ax.plot(ras, decs, "o-", color=col, lw=1.5, ms=5, alpha=0.8)
ax.annotate(cl.priority[0], (ras[-1], decs[-1]),
color=col, fontsize=7, fontfamily="monospace",
xytext=(3, 3), textcoords="offset points")
ax.invert_xaxis()
ax.set_xlabel("RA (°)", color=DIM, fontfamily="monospace", fontsize=7)
ax.set_ylabel("Dec (°)", color=DIM, fontfamily="monospace", fontsize=7)
ax.set_title("Confirmed Tracklets — Sky Plane", color=TEXT,
fontfamily="monospace", fontsize=9)
ax.legend(handles=[
Line2D([0], [0], marker="o", color="w", markerfacecolor="#FF4444",
ms=6, lw=0, label="HAZARDOUS"),
Line2D([0], [0], marker="o", color="w", markerfacecolor=WARN,
ms=6, lw=0, label="HIGH"),
Line2D([0], [0], marker="o", color="w", markerfacecolor=ACCENT,
ms=6, lw=0, label="ROUTINE"),
], framealpha=0, labelcolor=DIM, fontsize=7, prop={"family": "monospace"})
plt.tight_layout(pad=1.5)
return img_html(fig_to_b64(fig), "Tracklet Motion Map")
# ── Tab 2: Pipeline Simulator ─────────────────────────────────────────────────
def run_simulation(n_frames, n_asteroids, snr_min, snr_max, vel_min, vel_max, det_thresh):
from asteroidnet.utils.synthetic import make_synthetic_sequence
from asteroidnet.pipeline.runner import run_pipeline
with tempfile.TemporaryDirectory() as tmpdir:
paths = make_synthetic_sequence(
Path(tmpdir),
n_frames=int(n_frames),
n_stars=400,
n_asteroids=int(n_asteroids),
velocity_arcsec_s=float((vel_min + vel_max) / 2),
cadence_min=15.0,
seed=42,
)
import yaml
cfg_override = {
"detection": {"threshold_sigma": det_thresh},
"tracking": {
"velocity_range_arcsec_s": [vel_min, vel_max],
"min_time_span_minutes": 20.0,
},
}
result = run_pipeline(paths, observatory_code="F51")
n_confirmed = result.n_confirmed
n_candidates = result.n_candidates
rec_pct = round(n_confirmed / max(n_asteroids, 1) * 100, 1)
fp_pct = 0.0
stats_html = (
'<div class="stat-grid">'
+ stat_card(int(n_frames), "Frames")
+ stat_card(n_candidates, "Candidates", "a")
+ stat_card(n_confirmed, "Confirmed", "g")
+ stat_card(str(rec_pct) + "%", "Recovery", "w")
+ stat_card(str(result.elapsed_s.__round__(2)) + "s", "Time")
+ "</div>"
)
ok_msg = "✓ SC-001 PASS — Recovery " + str(rec_pct) + "% ≥ 90%"
bad_msg = "⚠ SC-001 — Recovery " + str(rec_pct) + "% below 90% target"
stats_html += (
'<div class="' + ("alert-ok" if rec_pct >= 90 else "alert-warn") + '">'
+ (ok_msg if rec_pct >= 90 else bad_msg) + "</div>"
)
# Charts
rng = np.random.default_rng(42)
fig1, ax1 = dark_fig(10, 4.5)
ax1.set_facecolor("#020812")
ax1.scatter(rng.uniform(179.5, 180.5, 300), rng.uniform(-0.5, 0.5, 300),
s=rng.uniform(1, 6, 300), alpha=0.15, color="white", lw=0)
for i in range(int(n_asteroids)):
v = rng.uniform(vel_min, vel_max)
pa = rng.uniform(0, 2 * math.pi)
r0 = (rng.uniform(179.6, 180.4), rng.uniform(-0.4, 0.4))
r1 = (r0[0] + v * 1800 * math.sin(pa) / 3600,
r0[1] + v * 1800 * math.cos(pa) / 3600 * 0.5)
col = "#FF4444" if v > 3 else WARN if v > 1 else ACCENT
ax1.annotate("", xy=r1, xytext=r0,
arrowprops=dict(arrowstyle="-|>", color=col, lw=1.4,
mutation_scale=10))
ax1.scatter([r0[0]], [r0[1]], s=20, color=col, zorder=5, lw=0)
ax1.invert_xaxis()
ax1.set_xlabel("RA (°)", color=DIM, fontfamily="monospace", fontsize=7)
ax1.set_ylabel("Dec (°)", color=DIM, fontfamily="monospace", fontsize=7)
ax1.set_title("Simulated Motion Field", color=TEXT, fontfamily="monospace", fontsize=9)
snr_x = np.linspace(3, 20, 60)
comp = np.clip(1 / (1 + np.exp(-(snr_x - (det_thresh + 1.5)) * 1.2)), 0, 1)
fig2, ax2 = dark_fig(7, 3.8)
ax2.plot(snr_x, comp * 100, color=ACCENT, lw=2)
ax2.axvline(det_thresh, color=ACC2, lw=1.2, ls="--", label="Threshold " + str(det_thresh) + "σ")
ax2.axhline(90, color=OK, lw=0.8, ls=":", alpha=0.7)
ax2.set_xlabel("SNR", color=DIM, fontfamily="monospace", fontsize=7)
ax2.set_ylabel("Recovery (%)", color=DIM, fontfamily="monospace", fontsize=7)
ax2.set_title("Completeness vs SNR", color=TEXT, fontfamily="monospace", fontsize=9)
ax2.legend(framealpha=0, labelcolor=DIM, fontsize=7, prop={"family": "monospace"})
plt.tight_layout(pad=1.5)
charts = (
img_html(fig_to_b64(fig1), "Motion Field")
+ img_html(fig_to_b64(fig2), "Completeness Curve")
)
return stats_html + charts
# ── Tab 3: MPC Formatter ──────────────────────────────────────────────────────
def format_mpc(desig, ra_deg, dec_deg, yr, mo, day_frac, mag, band, obs_code):
obs_code = obs_code.strip()
if len(obs_code) != 3:
return '<div class="alert-err">✗ Observatory code must be exactly 3 characters</div>', ""
try:
from asteroidnet.reporting.mpc_formatter import format_mpc_record
obs_time = Time(
{"year": int(yr), "month": int(mo), "day": int(day_frac)},
format="ymdhms", scale="utc"
)
except Exception:
obs_time = Time("2026-03-20T12:00:00", scale="utc")
try:
from asteroidnet.reporting.mpc_formatter import format_mpc_record
line = format_mpc_record(str(desig), float(ra_deg), float(dec_deg),
obs_time, float(mag), str(band), obs_code)
ruler = "".join(str((i + 1) % 10) for i in range(80))
tens = "".join(
str((i + 1) // 10 % 10) if (i + 1) % 10 == 0 else " "
for i in range(80)
)
out_html = (
'<div class="alert-ok">✓ Valid MPC record — exactly 80 characters</div>'
'<p style="font-family:monospace;font-size:.6rem;color:' + DIM + ';margin:0">'
+ tens + "</p>"
'<p style="font-family:monospace;font-size:.6rem;color:' + DIM + ';margin:0 0 6px">'
+ ruler + "</p>"
'<div class="mpc-wrap">' + line + "</div>"
)
return out_html, line
except Exception as exc:
return '<div class="alert-err">✗ ' + str(exc) + "</div>", ""
# ── Tab 4: Tracklet Visualizer ────────────────────────────────────────────────
def visualise_tracklet(n_dets, velocity, pa, time_span, snr_val, show_unc):
rng = np.random.default_rng(7)
n = int(n_dets)
times = np.linspace(0, float(time_span) * 60, n)
pa_r = math.radians(float(pa))
vel = float(velocity)
ra_t = [180.0 + vel * t * math.sin(pa_r) / 3600 for t in times]
dec_t = [0.0 + vel * t * math.cos(pa_r) / 3600 * 0.8 for t in times]
noise = 1 / max(float(snr_val), 0.1) * 0.0005
ra_o = [r + float(rng.normal(0, noise)) for r in ra_t]
dec_o = [d + float(rng.normal(0, noise)) for d in dec_t]
fig, axes = plt.subplots(1, 3, figsize=(14, 4.5))
fig.patch.set_facecolor(BG)
ax = axes[0]
ax.set_facecolor("#020812")
ax.tick_params(colors=DIM, labelsize=7)
for sp in ax.spines.values():
sp.set_edgecolor(SUBTLE)
ax.grid(color=SUBTLE, linewidth=0.3, alpha=0.5)
ax.scatter(rng.uniform(179.97, 180.03, 150), rng.uniform(-0.01, 0.01, 150),
s=rng.uniform(1, 5, 150), alpha=0.2, color="white", lw=0)
ax.plot(ra_t, dec_t, "--", color=ACCENT, lw=1, alpha=0.35, label="True path")
ax.scatter(ra_o, dec_o, s=55, color=ACCENT, zorder=5, lw=0)
if show_unc:
for rx, dy in zip(ra_o, dec_o):
ax.add_patch(plt.Circle((rx, dy), noise * 3, color=ACCENT,
alpha=0.12, fill=True, lw=0))
ax.scatter([ra_o[0]], [dec_o[0]], s=90, color=OK, zorder=6, lw=0, marker="*")
ax.scatter([ra_o[-1]], [dec_o[-1]], s=70, color=ACC2, zorder=6, lw=0, marker="D")
ax.invert_xaxis()
ax.set_xlabel("RA (°)", color=DIM, fontfamily="monospace", fontsize=7)
ax.set_ylabel("Dec (°)", color=DIM, fontfamily="monospace", fontsize=7)
ax.set_title("Sky Plane", color=TEXT, fontfamily="monospace", fontsize=8)
ax.legend(framealpha=0, labelcolor=DIM, fontsize=7, prop={"family": "monospace"})
ax2 = axes[1]
ax2.set_facecolor(PANEL)
ax2.tick_params(colors=DIM, labelsize=7)
for sp in ax2.spines.values():
sp.set_edgecolor(SUBTLE)
ax2.grid(color=SUBTLE, linewidth=0.3, alpha=0.5)
tm = np.array(times) / 60
ax2.plot(tm, np.array(ra_t) - ra_t[0], color=ACCENT, lw=2, label="ΔRA")
ax2.scatter(tm, np.array(ra_o) - ra_t[0], s=35, color=ACCENT, lw=0, zorder=5)
ax2.plot(tm, np.array(dec_t) - dec_t[0], color=ACC2, lw=2, label="ΔDec")
ax2.scatter(tm, np.array(dec_o) - dec_t[0], s=35, color=ACC2, lw=0, zorder=5)
ax2.set_xlabel("Time (min)", color=DIM, fontfamily="monospace", fontsize=7)
ax2.set_ylabel("Offset (°)", color=DIM, fontfamily="monospace", fontsize=7)
ax2.set_title("ΔRA / ΔDec vs Time", color=TEXT, fontfamily="monospace", fontsize=8)
ax2.legend(framealpha=0, labelcolor=DIM, fontsize=7, prop={"family": "monospace"})
ax3 = axes[2]
ax3.set_facecolor(PANEL)
ax3.tick_params(colors=DIM, labelsize=7)
for sp in ax3.spines.values():
sp.set_edgecolor(SUBTLE)
ax3.grid(color=SUBTLE, linewidth=0.3, alpha=0.5)
cr = np.polyfit(times, ra_o, 1)
cd = np.polyfit(times, dec_o, 1)
cos_d = math.cos(math.radians(float(np.mean(dec_o))))
res = np.sqrt(
((np.array(ra_o) - np.polyval(cr, times)) * cos_d) ** 2
+ (np.array(dec_o) - np.polyval(cd, times)) ** 2
) * 3600.0
rms = float(np.sqrt(np.mean(res ** 2)))
bw = (tm[-1] - tm[0]) / n * 0.7 if len(tm) > 1 else 0.5
ax3.bar(tm, res, color=ACCENT, alpha=0.75, width=bw)
ax3.axhline(rms, color=ACC2, lw=1.2, ls="--",
label="RMS=" + str(round(rms, 3)) + "″")
ax3.axhline(1.0, color=OK, lw=0.8, ls=":", alpha=0.6, label='1″ limit')
ax3.set_xlabel("Time (min)", color=DIM, fontfamily="monospace", fontsize=7)
ax3.set_ylabel("Residual (arcsec)", color=DIM, fontfamily="monospace", fontsize=7)
ax3.set_title("Motion Residuals", color=TEXT, fontfamily="monospace", fontsize=8)
ax3.legend(framealpha=0, labelcolor=DIM, fontsize=7, prop={"family": "monospace"})
plt.tight_layout(pad=1.5)
status = (
'<div class="alert-info">Tracklet: ' + str(n) + " dets · " +
str(time_span) + " min · " + str(velocity) + " ″/s · PA " + str(pa) + "°</div>"
+ '<div class="' + ("alert-ok" if rms < 1.0 else "alert-warn") + '">'
+ "RMS = " + str(round(rms, 4)) + "″ — "
+ ("✓ PASS (< 1″)" if rms < 1.0 else "⚠ WARN (> 1″ limit)") + "</div>"
)
return img_html(fig_to_b64(fig)), status
# ── Build UI ──────────────────────────────────────────────────────────────────
SUB_STYLE = (
'style="font-family:monospace;font-size:.66rem;color:' + DIM +
';letter-spacing:.1em;text-transform:uppercase;padding:10px 0 2px"'
)
with gr.Blocks(css=CSS, theme=gr.themes.Base()) as demo:
gr.HTML(HEADER)
with gr.Tabs():
# ── Tab 1: REAL DATA ─────────────────────────────────────────────────
with gr.Tab("⬡ Processar Imagens IASC"):
gr.HTML("<p " + SUB_STYLE + ">Upload real FITS files from an IASC campaign package (4 frames recommended)</p>")
with gr.Row():
with gr.Column(scale=1, min_width=280):
iasc_files = gr.File(
label="FITS Files (upload 4 frames)",
file_count="multiple",
file_types=[".fits", ".fit", ".fts", ".fits.gz"],
)
iasc_obs = gr.Textbox(label="Observatory Code (3 chars)", value="F51", max_lines=1)
iasc_survey = gr.Dropdown(
label="Survey hint",
choices=["auto", "ps1", "ztf", "generic"],
value="auto",
)
iasc_btn = gr.Button("▶ Run Pipeline on FITS", variant="primary")
gr.HTML(
'<div class="alert-info" style="margin-top:8px">'
"Tip: IASC packages contain 4 FITS frames of the same field "
"~30 min apart. Download them from iasc.cosmosearch.org after "
"registering for a campaign.</div>"
)
with gr.Column(scale=3):
iasc_stats = gr.HTML()
iasc_mpc_html = gr.HTML()
iasc_mpc_raw = gr.Textbox(
label="Raw MPC Records (copy for submission)",
interactive=False, lines=6,
)
iasc_btn.click(
fn=process_iasc_fits,
inputs=[iasc_files, iasc_obs, iasc_survey],
outputs=[iasc_stats, iasc_mpc_html, iasc_mpc_raw],
api_name=False,
)
# ── Tab 2: Simulator ─────────────────────────────────────────────────
with gr.Tab("⬡ Pipeline Simulator"):
gr.HTML("<p " + SUB_STYLE + ">Simulate full pipeline on synthetic FITS data</p>")
with gr.Row():
with gr.Column(scale=1, min_width=240):
sim_nfr = gr.Slider(4, 20, value=4, step=1, label="FITS Frames")
sim_nas = gr.Slider(5, 50, value=10, step=1, label="Injected Asteroids")
sim_sm = gr.Slider(3, 20, value=5, step=0.5, label="SNR Min")
sim_sx = gr.Slider(5, 50, value=25, step=1, label="SNR Max")
sim_vm = gr.Slider(0.01, 2, value=0.1, step=0.01, label="Vel min (″/s)")
sim_vx = gr.Slider(0.5, 10, value=5.0, step=0.1, label="Vel max (″/s)")
sim_dt = gr.Slider(2.5, 8, value=3.0, step=0.5, label="Detection threshold (σ)")
sim_btn = gr.Button("▶ Run Simulation", variant="primary")
with gr.Column(scale=3):
sim_out = gr.HTML()
sim_btn.click(
fn=run_simulation,
inputs=[sim_nfr, sim_nas, sim_sm, sim_sx, sim_vm, sim_vx, sim_dt],
outputs=[sim_out],
api_name=False,
)
# ── Tab 3: MPC Formatter ─────────────────────────────────────────────
with gr.Tab("⬡ MPC Formatter"):
gr.HTML("<p " + SUB_STYLE + ">Generate a Minor Planet Center 80-column astrometric record</p>")
with gr.Row():
with gr.Column(scale=1):
mpc_d = gr.Textbox(label="Provisional Designation", value="2026 AA1")
mpc_ra = gr.Number(label="RA (decimal °)", value=180.0)
mpc_dc = gr.Number(label="Dec (decimal °)", value=5.234)
with gr.Row():
mpc_yr = gr.Number(label="Year", value=2026, precision=0)
mpc_mo = gr.Number(label="Month", value=3, precision=0)
mpc_dy = gr.Number(label="Day (DD.ddddd)", value=20.50000)
mpc_mg = gr.Number(label="Magnitude", value=18.5)
mpc_bd = gr.Textbox(label="Filter Band", value="R", max_lines=1)
mpc_oc = gr.Textbox(label="Observatory Code", value="F51", max_lines=1)
mpc_bt = gr.Button("Generate MPC Record", variant="primary")
with gr.Column(scale=2):
mpc_out = gr.HTML()
mpc_raw = gr.Textbox(label="Raw 80-column line", interactive=False, lines=2)
mpc_bt.click(
fn=format_mpc,
inputs=[mpc_d, mpc_ra, mpc_dc, mpc_yr, mpc_mo, mpc_dy, mpc_mg, mpc_bd, mpc_oc],
outputs=[mpc_out, mpc_raw],
api_name=False,
)
# ── Tab 4: Tracklet Visualizer ───────────────────────────────────────
with gr.Tab("⬡ Tracklet Visualizer"):
gr.HTML("<p " + SUB_STYLE + ">Inspect multi-frame tracklet motion and linear residuals</p>")
with gr.Row():
with gr.Column(scale=1):
t_n = gr.Slider(3, 10, value=5, step=1, label="Detections")
t_v = gr.Slider(0.01, 8, value=0.5, step=0.01, label="Velocity (″/s)")
t_pa = gr.Slider(0, 360, value=135, step=1, label="Position Angle (°)")
t_sp = gr.Slider(30, 240, value=90, step=5, label="Time Span (min)")
t_sn = gr.Slider(3, 30, value=10, step=0.5, label="SNR")
t_uc = gr.Checkbox(label="Show position uncertainties", value=True)
t_bt = gr.Button("Plot Tracklet", variant="primary")
with gr.Column(scale=3):
t_img = gr.HTML()
t_st = gr.HTML()
t_bt.click(
fn=visualise_tracklet,
inputs=[t_n, t_v, t_pa, t_sp, t_sn, t_uc],
outputs=[t_img, t_st],
api_name=False,
)
# ── Tab 5: About ─────────────────────────────────────────────────────
with gr.Tab("⬡ About"):
gr.HTML("""
<div style="max-width:800px;margin:20px auto;font-family:'Space Mono',monospace">
<h2 style="color:#00D4FF;font-family:'Syne',sans-serif;font-weight:800;font-size:1.4rem;margin-bottom:4px">AsteroidNET v0.2</h2>
<p style="color:#4A6070;font-size:.66rem;letter-spacing:.14em;text-transform:uppercase;margin-bottom:18px">
Automated NEO Detection &bull; Dr. Matheus Machado Rech</p>
<div style="background:#0A0E1A;border:1px solid #1E2D40;border-radius:4px;padding:18px 22px;margin-bottom:14px">
<h3 style="color:#C8D8E8;font-size:.8rem;margin:0 0 10px;letter-spacing:.08em;text-transform:uppercase">What is new in v0.2</h3>
<ul style="color:#4A6070;font-size:.72rem;line-height:1.8;padding-left:1.2em">
<li><b style="color:#00D4FF">Real FITS support</b> — upload IASC campaign packages directly</li>
<li><b style="color:#00D4FF">TAI/UTC correction</b> — PS1 MJD-OBS (TAI) vs ZTF (UTC), 37s offset handled</li>
<li><b style="color:#00D4FF">Byte-order fix</b> — FITS big-endian converted to float32 native before Background2D</li>
<li><b style="color:#00D4FF">SkyBoT integration</b> — IMCCE cone search removes known SSOs from candidates</li>
<li><b style="color:#00D4FF">Two-pass background</b> — source masking for unbiased sky estimation</li>
<li><b style="color:#00D4FF">ZTF support</b> — IRSA IBE API for multi-epoch science images</li>
<li><b style="color:#00D4FF">Training data builder</b> — mine PS1/ZTF with MPC labels for classifier training</li>
<li><b style="color:#00D4FF">GitHub Actions CI/CD</b> — auto-deploys to HuggingFace Spaces on push</li>
</ul>
</div>
<div style="background:#0A0E1A;border:1px solid #1E2D40;border-radius:4px;padding:18px 22px">
<h3 style="color:#C8D8E8;font-size:.8rem;margin:0 0 10px;letter-spacing:.08em;text-transform:uppercase">How to use with IASC</h3>
<ol style="color:#4A6070;font-size:.72rem;line-height:1.8;padding-left:1.2em">
<li>Register at <a href="https://iasc.cosmosearch.org" style="color:#00D4FF">iasc.cosmosearch.org</a></li>
<li>Download a campaign FITS package (4 frames, ~30 min cadence)</li>
<li>Upload all 4 .fits files in the <b style="color:#00D4FF">Processar Imagens IASC</b> tab</li>
<li>Enter your observatory code (F51 for Pan-STARRS; 500 for generic)</li>
<li>Click Run Pipeline — MPC records generated automatically</li>
<li>Submit records to IASC for verification and MPC submission</li>
</ol>
</div>
</div>""")
gr.HTML(
'<div style="text-align:center;padding:14px;font-family:monospace;font-size:.6rem;'
"color:" + DIM + ";letter-spacing:.1em;border-top:1px solid " + SUBTLE + '">'
"AsteroidNET · Dr. Matheus Machado Rech · github.com/mmrech/asteroidnet</div>"
)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860, show_error=True)