import csv
import math
import os
import sys
import tempfile
try:
import spiceypy as sp
except ImportError:
sys.exit("ERROR: spiceypy not found — run from /tmp/kshana-oracles/.venv")
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.dirname(SCRIPT_DIR)
ORACLE_KRN = "/tmp/kshana-oracles/kernels"
_bpc_local = os.path.join(REPO_ROOT, "xval", "anise-lunar-od", "kernels",
"moon_pa_de440_200625.bpc")
_bpc_oracle = os.path.join(ORACLE_KRN, "moon_pa_de440_200625.bpc")
LSK_PATH = os.path.join(ORACLE_KRN, "naif0012.tls")
PCK_PATH = os.path.join(ORACLE_KRN, "pck00011.tpc")
SPK_PATH = (os.path.join(REPO_ROOT, "xval", "anise-lunar-od", "kernels", "de440s.bsp")
if os.path.isfile(os.path.join(REPO_ROOT, "xval", "anise-lunar-od", "kernels", "de440s.bsp"))
else os.path.join(ORACLE_KRN, "de440s.bsp"))
BPC_PATH = _bpc_local if os.path.isfile(_bpc_local) else _bpc_oracle
OUT_DIR = os.path.join(REPO_ROOT, "tests", "fixtures", "llr_geometry")
OUT_PATH = os.path.join(OUT_DIR, "de440_moon_pa.csv")
SPICEYPY_VERSION = "8.1.2"
FRAME_KERNEL = """\
KPL/FK
\\begintext
Companion frame kernel for moon_pa_de440_200625.bpc.
MOON_PA_DE440 is frame ID 31008 (verified via pckfrm on the BPC file).
CLASS = 2, CLASS_ID = 31008 routes pxform to the binary-PCK time-series
data rather than the analytic text-PCK polynomial model.
\\begindata
FRAME_MOON_PA_DE440 = 31008
FRAME_31008_NAME = 'MOON_PA_DE440'
FRAME_31008_CLASS = 2
FRAME_31008_CLASS_ID = 31008
FRAME_31008_CENTER = 301
"""
J2000_JD = 2_451_545.0 SEC_PER_JC = 36_525.0 * 86_400.0
START_TDB = "2024-01-01 00:00:00 TDB"
N_DAYS = 730
CADENCE_D = 1
def main() -> None:
for p in [LSK_PATH, PCK_PATH, SPK_PATH, BPC_PATH]:
if not os.path.isfile(p):
sys.exit(f"ERROR: kernel not found: {p}")
tf_fd, tf_path = tempfile.mkstemp(suffix=".tf")
try:
with os.fdopen(tf_fd, "w") as fh:
fh.write(FRAME_KERNEL)
sp.kclear()
sp.furnsh(LSK_PATH)
sp.furnsh(SPK_PATH)
sp.furnsh(PCK_PATH) sp.furnsh(BPC_PATH) sp.furnsh(tf_path)
ids = sp.stypes.SPICEINT_CELL(100)
sp.pckfrm(BPC_PATH, ids)
bpc_ids = list(ids)
if 31008 not in bpc_ids:
sys.exit(f"ERROR: frame 31008 not found in BPC (got {bpc_ids})")
print(f"BPC frame IDs confirmed: {bpc_ids}")
et_start = sp.str2et(START_TDB)
jd_start = et_start / 86_400.0 + J2000_JD print(f"Start ET: {et_start:.3f} (~JD {jd_start:.1f})")
os.makedirs(OUT_DIR, exist_ok=True)
rows = []
for day in range(N_DAYS + 1):
et = et_start + day * 86_400.0
jd_tdb = J2000_JD + et / 86_400.0 t_tt_jc = (jd_tdb - J2000_JD) / 36_525.0
r = sp.pxform("MOON_PA_DE440", "J2000", et)
rows.append([
t_tt_jc,
r[0][0], r[0][1], r[0][2],
r[1][0], r[1][1], r[1][2],
r[2][0], r[2][1], r[2][2],
])
header = ["t_tt_jc",
"r00", "r01", "r02",
"r10", "r11", "r12",
"r20", "r21", "r22"]
with open(OUT_PATH, "w", newline="") as fh:
w = csv.writer(fh)
w.writerow(header)
for row in rows:
w.writerow([f"{row[0]:.15e}"] + [f"{v:.18e}" for v in row[1:]])
print(f"Wrote {len(rows)} rows → {OUT_PATH}")
lons, lats = [], []
for day in range(0, N_DAYS + 1, 5):
et = et_start + day * 86_400.0
r_earth, _ = sp.spkpos("399", et, "J2000", "NONE", "301")
n = math.sqrt(sum(x * x for x in r_earth))
e_hat = [x / n for x in r_earth]
r_i2b = sp.pxform("J2000", "MOON_PA_DE440", et)
e_body = [sum(r_i2b[i][j] * e_hat[j] for j in range(3)) for i in range(3)]
lon = math.atan2(e_body[1], e_body[0]) * 180.0 / math.pi
lat = math.asin(max(-1.0, min(1.0, e_body[2]))) * 180.0 / math.pi
lons.append(lon)
lats.append(lat)
lon_amp = (max(lons) - min(lons)) / 2.0
lat_amp = (max(lats) - min(lats)) / 2.0
print(f"\nSanity check — sub-Earth point libration amplitude (730-day window):")
print(f" Longitude: {min(lons):.3f}° to {max(lons):.3f}° amplitude = {lon_amp:.3f}°")
print(f" Latitude: {min(lats):.3f}° to {max(lats):.3f}° amplitude = {lat_amp:.3f}°")
if lon_amp < 1.0:
sys.exit("SANITY FAIL: longitude amplitude <1° — orientation is essentially constant; "
"frame/kernel setup is WRONG, do NOT commit this fixture.")
if lat_amp < 1.0:
sys.exit("SANITY FAIL: latitude amplitude <1° — orientation is essentially constant; "
"frame/kernel setup is WRONG, do NOT commit this fixture.")
print(f"\nSANITY PASS: real DE440 optical+physical libration confirmed "
f"(lon ±{lon_amp:.3f}°, lat ±{lat_amp:.3f}° >1° threshold).")
import hashlib
with open(OUT_PATH, "rb") as fh:
sha256 = hashlib.sha256(fh.read()).hexdigest()
print(f"\nde440_moon_pa.csv SHA-256: {sha256}")
print(f"Rows: {len(rows)}")
finally:
os.unlink(tf_path)
sp.kclear()
if __name__ == "__main__":
main()