Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

env:
CARGO_TERM_COLOR: always

jobs:
rust:
name: Rust Tests & Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: clippy

- name: Cache cargo registry & build
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

- name: Build
run: cargo build --verbose

- name: Run tests
run: cargo test --verbose

- name: Clippy (deny warnings)
run: cargo clippy -- -D warnings

python:
name: Python Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install dependencies
run: pip install -r requirements.txt

- name: Run tests
run: python -m pytest tests/ -v
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,9 @@ Thumbs.db
.vscode/
.idea/

# Generated docs artifacts (regenerated by pipeline)
docs/candidates.json
docs/validation_results.json

# Keep the data directory structure
!data/lightcurves/.gitkeep
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ tempfile = "3"
opt-level = 3
lto = true
codegen-units = 1
target-cpu = "native"
# Note: For native CPU optimizations, set RUSTFLAGS="-C target-cpu=native"
8 changes: 4 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@ all: setup download hunt analyze
# Install Python dependencies
setup:
@echo "📦 Installing dependencies..."
pip install lightkurve astroquery pandas numpy matplotlib tqdm --quiet
python3.11 -m pip install -r requirements.txt --quiet

# Download light curves
download:
@echo "🛰️ Downloading light curves..."
python python/download_lightcurves.py \
python3.11 python/download_lightcurves.py \
--mission $(MISSION) --sector $(SECTOR) \
--limit $(LIMIT) --catalog

# Download unconfirmed candidates (best for discovery!)
download-candidates:
@echo "🎯 Downloading unconfirmed TOI candidates..."
python python/download_lightcurves.py \
python3.11 python/download_lightcurves.py \
--candidates-only --limit $(LIMIT) --catalog

# Build and run Rust BLS engine
Expand All @@ -49,7 +49,7 @@ target/release/hunt: src/main.rs Cargo.toml
# Analyze candidates and generate plots
analyze:
@echo "📊 Analyzing candidates..."
python python/analyze_candidates.py \
python3.11 python/analyze_candidates.py \
--input candidates.json \
--lightcurves data/lightcurves/ \
--crossmatch \
Expand Down
24 changes: 16 additions & 8 deletions python/analyze_candidates.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
python analyze_candidates.py --input candidates.json --crossmatch # check if any are NEW
"""

from __future__ import annotations

import argparse
import json
import sys
Expand Down Expand Up @@ -40,7 +42,7 @@
"transit": "#ef4444",
}

def setup_style():
def setup_style() -> None:
plt.rcParams.update({
"figure.facecolor": COLORS["bg"],
"axes.facecolor": COLORS["bg"],
Expand All @@ -60,13 +62,15 @@ def setup_style():
# Phase-folded light curve plot
# ============================================================================

def plot_phase_folded(candidate: dict, lc_dir: Path, output_dir: Path):
def plot_phase_folded(candidate: dict, lc_dir: Path, output_dir: Path) -> Path | None:
"""Create a phase-folded light curve plot for a candidate."""
filepath = lc_dir / candidate["filename"]
if not filepath.exists():
return None

df = pd.read_csv(filepath)
if "time" not in df.columns or "flux" not in df.columns:
return None
time = df["time"].values
flux = df["flux"].values

Expand Down Expand Up @@ -138,7 +142,7 @@ def plot_phase_folded(candidate: dict, lc_dir: Path, output_dir: Path):
bbox=dict(boxstyle="round,pad=0.5", facecolor=COLORS["grid"], alpha=0.8))

plt.tight_layout()
safe_name = candidate["filename"].replace(".csv", "")
safe_name = Path(candidate["filename"]).stem
outpath = output_dir / f"phase_fold_{safe_name}.png"
fig.savefig(outpath, dpi=150, bbox_inches="tight")
plt.close(fig)
Expand All @@ -149,7 +153,7 @@ def plot_phase_folded(candidate: dict, lc_dir: Path, output_dir: Path):
# Overview plots
# ============================================================================

def plot_candidate_overview(candidates: list, output_dir: Path):
def plot_candidate_overview(candidates: list[dict], output_dir: Path) -> None:
"""Create overview scatter plots of all candidates."""
if not candidates:
return
Expand Down Expand Up @@ -206,7 +210,7 @@ def plot_candidate_overview(candidates: list, output_dir: Path):
# Cross-matching
# ============================================================================

def crossmatch_known_planets(candidates: list, catalog_path: Path) -> pd.DataFrame:
def crossmatch_known_planets(candidates: list[dict], catalog_path: Path) -> pd.DataFrame:
"""Cross-match candidates with the known exoplanet catalog."""
if not catalog_path.exists():
print(" ⚠️ No catalog file found. Run downloader with --catalog first.")
Expand Down Expand Up @@ -254,7 +258,7 @@ def crossmatch_known_planets(candidates: list, catalog_path: Path) -> pd.DataFra
# Report generation
# ============================================================================

def generate_report(report: dict, crossmatch_df: pd.DataFrame, output_dir: Path):
def generate_report(report: dict, crossmatch_df: pd.DataFrame, output_dir: Path) -> None:
"""Generate a markdown report."""
candidates = report["candidates"]

Expand Down Expand Up @@ -324,7 +328,7 @@ def generate_report(report: dict, crossmatch_df: pd.DataFrame, output_dir: Path)
# Main
# ============================================================================

def main():
def main() -> None:
parser = argparse.ArgumentParser(description="📊 Analyze BLS transit detection results")
parser.add_argument("--input", default="candidates.json", help="BLS results JSON")
parser.add_argument("--lightcurves", default="data/lightcurves", help="Light curve directory")
Expand All @@ -344,7 +348,11 @@ def main():
print("\n📊 Exoplanet Hunter — Analysis")
print("━" * 50)

with open(args.input) as f:
input_path = Path(args.input)
if not input_path.exists():
print(f" Error: {input_path} not found")
sys.exit(1)
with open(input_path) as f:
report = json.load(f)

candidates = report["candidates"]
Expand Down
73 changes: 41 additions & 32 deletions python/deep_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

Targets: TOI 133.01, TOI 210.01, TOI 155.01 (top 3 plausible planet candidates)
"""
from __future__ import annotations

import os
import sys
Expand All @@ -22,7 +23,9 @@
import matplotlib.pyplot as plt
from datetime import datetime

warnings.filterwarnings('ignore')
warnings.filterwarnings('ignore', category=FutureWarning)
warnings.filterwarnings('ignore', category=DeprecationWarning)
warnings.filterwarnings('ignore', message='.*overflow.*')

RESULTS_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'results')
DEEP_DIR = os.path.join(RESULTS_DIR, 'deep_analysis')
Expand All @@ -35,17 +38,15 @@
{"toi": "TOI 155.01", "tic": "TIC 129637892", "tic_id": 129637892, "period": 5.4504, "rp_earth": 5.3, "snr": 44.3},
]

findings = {}


def log(msg):
def log(msg: str) -> None:
print(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}")


# ===========================================================================
# STEP 1: Target Pixel Files + Centroid Analysis
# ===========================================================================
def step1_centroid_analysis():
def step1_centroid_analysis(findings: dict) -> None:
log("STEP 1: Downloading TPFs and running centroid analysis...")
import lightkurve as lk

Expand Down Expand Up @@ -166,7 +167,9 @@ def step1_centroid_analysis():
axes[2].legend(fontsize=8)

plt.tight_layout()
plot_path = os.path.join(DEEP_DIR, f'centroid_{name.replace(" ", "_").replace(".", "_")}.png')
safe_name = name.replace(" ", "_").replace(".", "_")
safe_name = os.path.basename(safe_name) # strip any directory components
plot_path = os.path.join(DEEP_DIR, f'centroid_{safe_name}.png')
plt.savefig(plot_path, dpi=150, bbox_inches='tight')
plt.close()
log(f" Saved centroid plot: {plot_path}")
Expand All @@ -179,7 +182,7 @@ def step1_centroid_analysis():
# ===========================================================================
# STEP 2: Gaia DR3 Nearby Source Check
# ===========================================================================
def step2_gaia_query():
def step2_gaia_query(findings: dict) -> None:
log("STEP 2: Querying Gaia DR3 for nearby contaminating sources...")
from astroquery.mast import Catalogs
from astroquery.gaia import Gaia
Expand Down Expand Up @@ -212,19 +215,19 @@ def step2_gaia_query():
search_radius = 120 # arcsec = 2 arcmin

log(f" Querying Gaia DR3 within {search_radius}\" of target...")
query = f"""
SELECT source_id, ra, dec, phot_g_mean_mag, parallax,
DISTANCE(
POINT('ICRS', ra, dec),
POINT('ICRS', {ra}, {dec})
) AS ang_sep
FROM gaiadr3.gaia_source
WHERE DISTANCE(
POINT('ICRS', ra, dec),
POINT('ICRS', {ra}, {dec})
) < {search_radius / 3600.0}
ORDER BY ang_sep ASC
"""
# Sanitize numeric inputs for ADQL (TAP doesn't support parameterized queries)
safe_ra = float(ra)
safe_dec = float(dec)
safe_radius = float(search_radius) / 3600.0
query = (
"SELECT source_id, ra, dec, phot_g_mean_mag, parallax,"
" DISTANCE(POINT('ICRS', ra, dec),"
f" POINT('ICRS', {safe_ra:.6f}, {safe_dec:.6f})) AS ang_sep"
" FROM gaiadr3.gaia_source"
" WHERE DISTANCE(POINT('ICRS', ra, dec),"
f" POINT('ICRS', {safe_ra:.6f}, {safe_dec:.6f})) < {safe_radius:.8f}"
" ORDER BY ang_sep ASC"
)

job = Gaia.launch_job(query)
results = job.get_results()
Expand Down Expand Up @@ -277,7 +280,7 @@ def step2_gaia_query():
# ===========================================================================
# STEP 3: Check NASA SPOC DV Reports on MAST
# ===========================================================================
def step3_dv_reports():
def step3_dv_reports(findings: dict) -> None:
log("STEP 3: Checking NASA SPOC Data Validation reports on MAST...")
from astroquery.mast import Observations

Expand Down Expand Up @@ -352,7 +355,7 @@ def step3_dv_reports():
# ===========================================================================
# STEP 4: Transit Least Squares (TLS)
# ===========================================================================
def step4_tls():
def step4_tls(findings: dict) -> None:
log("STEP 4: Running Transit Least Squares (TLS) analysis...")
try:
from transitleastsquares import transitleastsquares
Expand Down Expand Up @@ -469,7 +472,9 @@ def step4_tls():
axes[1].legend(fontsize=9)

plt.tight_layout()
plot_path = os.path.join(DEEP_DIR, f'tls_{name.replace(" ", "_").replace(".", "_")}.png')
safe_name = name.replace(" ", "_").replace(".", "_")
safe_name = os.path.basename(safe_name) # strip any directory components
plot_path = os.path.join(DEEP_DIR, f'tls_{safe_name}.png')
plt.savefig(plot_path, dpi=150, bbox_inches='tight')
plt.close()
log(f" Saved TLS plot: {plot_path}")
Expand All @@ -482,7 +487,7 @@ def step4_tls():
# ===========================================================================
# STEP 5: Multi-sector secondary eclipse check
# ===========================================================================
def step5_multisector_secondary():
def step5_multisector_secondary(findings: dict) -> None:
log("STEP 5: Multi-sector secondary eclipse search...")
import lightkurve as lk

Expand Down Expand Up @@ -568,7 +573,9 @@ def step5_multisector_secondary():
ax.set_title(f'{name} — Multi-sector Secondary Eclipse Search ({len(search)} sectors, {len(time)} pts)')
ax.legend(fontsize=8)
plt.tight_layout()
plot_path = os.path.join(DEEP_DIR, f'secondary_{name.replace(" ", "_").replace(".", "_")}.png')
safe_name = name.replace(" ", "_").replace(".", "_")
safe_name = os.path.basename(safe_name) # strip any directory components
plot_path = os.path.join(DEEP_DIR, f'secondary_{safe_name}.png')
plt.savefig(plot_path, dpi=150, bbox_inches='tight')
plt.close()

Expand All @@ -580,7 +587,7 @@ def step5_multisector_secondary():
# ===========================================================================
# WRITE RESULTS
# ===========================================================================
def write_results():
def write_results(findings: dict) -> None:
log("Writing results...")

# Save JSON
Expand Down Expand Up @@ -702,12 +709,14 @@ def write_results():
log("Targets: " + ", ".join(t['toi'] for t in TARGETS))
log("=" * 60)

step1_centroid_analysis()
step2_gaia_query()
step3_dv_reports()
step4_tls()
step5_multisector_secondary()
write_results()
findings: dict = {}

step1_centroid_analysis(findings)
step2_gaia_query(findings)
step3_dv_reports(findings)
step4_tls(findings)
step5_multisector_secondary(findings)
write_results(findings)

log("=" * 60)
log("DEEP ANALYSIS COMPLETE")
Expand Down
Loading
Loading