| |
| """Evaluate Keplerian two-body baseline on test sets. |
| |
| Usage: |
| python scripts/eval_sgp4.py --spacecraft iss |
| python scripts/eval_sgp4.py --spacecraft all |
| """ |
| import argparse |
| import json |
| import logging |
| import sys |
| from pathlib import Path |
|
|
| import numpy as np |
| import pandas as pd |
| import yaml |
|
|
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) |
| from src.models.baseline_sgp4 import SGP4Baseline |
| from src.data.preprocessing import OrbitPreprocessor |
|
|
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") |
| log = logging.getLogger("sgp4-baseline") |
|
|
| RESULTS_DIR = Path("results") |
| RESULTS_DIR.mkdir(exist_ok=True) |
|
|
|
|
| def load_config(): |
| with open("config.yaml") as f: |
| return yaml.safe_load(f) |
|
|
|
|
| def compute_kepler_mae(spacecraft_id, config): |
| """Compute Keplerian two-body baseline MAE on test set.""" |
| log.info(f"Computing Keplerian baseline for {spacecraft_id}") |
|
|
| proc = OrbitPreprocessor() |
| raw_path = Path(f"data/raw/{spacecraft_id}_2023-01-01_2025-12-31.parquet") |
| if not raw_path.exists(): |
| log.error(f"Data not found: {raw_path}") |
| return float("nan") |
|
|
| df = pd.read_parquet(raw_path) |
| processed = proc.preprocess(df, spacecraft_id) |
| stats = proc.stats |
|
|
| |
| stats_dir = RESULTS_DIR / "norm_stats" |
| stats_dir.mkdir(exist_ok=True) |
| with open(stats_dir / f"{spacecraft_id}_norm_stats.json", "w") as f: |
| json.dump(stats, f, indent=2) |
|
|
| time_res = config["model"]["time_resolution_minutes"] |
| input_steps = (config["model"]["input_hours"] * 60) // time_res |
| horizon_steps = (6 * 60) // time_res |
| stride_steps = horizon_steps |
|
|
| pos_cols = ["x_gse", "y_gse", "z_gse"] |
| vel_cols = ["vx_gse", "vy_gse", "vz_gse"] |
| all_cols = pos_cols + vel_cols |
|
|
| inputs_raw, targets_raw = [], [] |
| for _, seg in processed.groupby("segment_id"): |
| if len(seg) < input_steps + horizon_steps: |
| continue |
| feats = seg[all_cols].values |
| tgts = seg[pos_cols].values |
| for i in range(0, len(seg) - input_steps - horizon_steps, stride_steps): |
| inputs_raw.append(feats[i:i + input_steps]) |
| targets_raw.append(tgts[i + input_steps:i + input_steps + horizon_steps]) |
|
|
| inputs_raw = np.array(inputs_raw, dtype=np.float64) |
| targets_raw = np.array(targets_raw, dtype=np.float64) |
|
|
| |
| test_start = int(0.85 * len(inputs_raw)) |
| test_inputs = inputs_raw[test_start:] |
| test_targets = targets_raw[test_start:] |
| log.info(f"Test set: {len(test_inputs)} windows") |
|
|
| dt_seconds = time_res * 60.0 |
| all_distances = [] |
|
|
| for i in range(len(test_inputs)): |
| last_pos = test_inputs[i, -1, :3] |
| last_vel = test_inputs[i, -1, 3:6] |
| pred = SGP4Baseline.simple_kepler_propagate(last_pos, last_vel, dt_seconds, horizon_steps) |
| distances = np.sqrt(np.sum((pred - test_targets[i])**2, axis=-1)) |
| all_distances.append(distances) |
|
|
| if not all_distances: |
| log.warning(f"{spacecraft_id}: no valid test windows (data may be too sparse)") |
| return float("nan") |
|
|
| all_distances = np.concatenate(all_distances) |
| mae = float(np.mean(all_distances)) |
| rmse = float(np.sqrt(np.mean(all_distances**2))) |
| log.info(f"{spacecraft_id} Keplerian: MAE={mae:.1f} km, RMSE={rmse:.1f} km") |
| return mae |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument("--spacecraft", default="all") |
| args = parser.parse_args() |
|
|
| config = load_config() |
| spacecraft_list = ( |
| list(config["spacecraft"].keys()) if args.spacecraft == "all" |
| else [args.spacecraft] |
| ) |
|
|
| results = {} |
| results["keplerian"] = {} |
| for sc in spacecraft_list: |
| results["keplerian"][sc] = compute_kepler_mae(sc, config) |
|
|
| out_path = RESULTS_DIR / "sgp4_baselines.json" |
| if out_path.exists(): |
| with open(out_path) as f: |
| existing = json.load(f) |
| existing.update(results) |
| results = existing |
|
|
| with open(out_path, "w") as f: |
| json.dump(results, f, indent=2) |
| log.info(f"Saved to {out_path}") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|