Spaces:
Sleeping
Sleeping
| """ | |
| 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 · 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 • 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) | |