"""Random-point sampling of orbital image coverage with the PDS Geosciences Orbital Data Explorer (ODE) REST interface. For N random points drawn uniformly on the sphere we ask ODE which image footprints contain a ~60 m box round the point: Moon : LRO LROC NAC EDR (product type EDRNAC4) Mars : MRO CTX EDR, and MRO HiRISE RDR (RDRV11) and store, per point, the number of images and their pixel scales and illumination angles. Output: data/moon_nac_samples.csv, data/mars_ctx_samples.csv, data/mars_hirise_samples.csv. The queries are slow (1-2 s each), so results are cached; run with --refresh to re-query. """ import json import os import sys import time import urllib.parse import numpy as np import pandas as pd from common import DATA, RAW, SEED, fetch_text ODE = "https://oderest.rsl.wustl.edu/live2/" def random_points(n, seed): rng = np.random.default_rng(seed) z = rng.uniform(-1, 1, n) lat = np.degrees(np.arcsin(z)) lon = rng.uniform(0, 360, n) return lat, lon def query_point(target, ihid, iid, pt, lat, lon, half=0.001, limit=400): q = dict(query="products", target=target, ihid=ihid, iid=iid, pt=pt, results="m", output="JSON", minlat=f"{max(lat - half, -90):.5f}", maxlat=f"{min(lat + half, 90):.5f}", westlon=f"{(lon - half) % 360:.5f}", eastlon=f"{(lon + half) % 360:.5f}", loc="f", limit=str(limit)) txt = fetch_text(ODE + "?" + urllib.parse.urlencode(q), timeout=120, retries=4) js = json.loads(txt)["ODEResults"] if js.get("Status") != "Success": raise RuntimeError(str(js)[:300]) prods = js.get("Products") if prods in (None, "", "No Products Found"): return 0, [] pl = prods["Product"] if isinstance(pl, dict): pl = [pl] out = [] for p in pl: def f(k): try: return float(p.get(k)) except Exception: return float("nan") res = f("Map_resolution") if not (res == res): res = f("Map_scale") # CTX / HiRISE products carry Map_scale (m/pixel) out.append((p.get("Product_name", ""), res, f("Incidence_angle"), f("Emission_angle"), p.get("UTC_start_time", "")[:10])) cnt = int(js.get("Count", len(out))) return cnt, out def run(tag, target, ihid, iid, pt, n, seed, refresh=False): out_csv = os.path.join(DATA, f"{tag}_samples.csv") cache = os.path.join(RAW, f"ode_{tag}.jsonl") if os.path.exists(out_csv) and not refresh: print(f"[ode] {out_csv} exists; skipping (use --refresh to re-query)") return done = {} if os.path.exists(cache) and not refresh: for line in open(cache): try: r = json.loads(line) done[r["k"]] = r except Exception: pass lat, lon = random_points(n, seed) fo = open(cache, "a" if not refresh else "w") rows = [] t0 = time.time() for k in range(n): if k in done: r = done[k] else: cnt, prods = query_point(target, ihid, iid, pt, lat[k], lon[k]) r = dict(k=k, lat=float(lat[k]), lon=float(lon[k]), count=cnt, prods=prods) fo.write(json.dumps(r) + "\n") fo.flush() if (k + 1) % 25 == 0: print(f"[ode] {tag} {k + 1}/{n} {time.time() - t0:.0f}s", flush=True) res = np.array([p[1] for p in r["prods"]], float) inc = np.array([p[2] for p in r["prods"]], float) emi = np.array([p[3] for p in r["prods"]], float) ok = np.isfinite(res) & (res > 0) good = ok & (inc > 20) & (inc < 85) # usable illumination for small-object detection rows.append(dict(k=k, lat=r["lat"], lon=r["lon"], n_images=r["count"], n_returned=len(r["prods"]), best_res=float(np.min(res[ok])) if ok.any() else np.nan, median_res=float(np.median(res[ok])) if ok.any() else np.nan, best_res_goodlight=float(np.min(res[good])) if good.any() else np.nan, n_le_1m=int(np.sum(ok & (res <= 1.0))), n_le_2m=int(np.sum(ok & (res <= 2.0))), min_inc=float(np.nanmin(inc)) if len(inc) else np.nan, max_inc=float(np.nanmax(inc)) if len(inc) else np.nan, min_emi=float(np.nanmin(emi)) if len(emi) else np.nan)) fo.close() pd.DataFrame(rows).to_csv(out_csv, index=False) print(f"[ode] wrote {out_csv}") if __name__ == "__main__": refresh = "--refresh" in sys.argv which = [a for a in sys.argv[1:] if not a.startswith("--")] or ["moon_nac", "mars_ctx", "mars_hirise"] if "moon_nac" in which: run("moon_nac", "moon", "LRO", "LROC", "EDRNAC4", 500, SEED + 1, refresh) if "mars_ctx" in which: run("mars_ctx", "mars", "MRO", "CTX", "EDR", 400, SEED + 2, refresh) if "mars_hirise" in which: run("mars_hirise", "mars", "MRO", "HIRISE", "RDRV11", 400, SEED + 2, refresh)