From aa4f699b4335df89124645ebf87939b59f566be6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 2 May 2026 12:48:39 +0000 Subject: [PATCH] =?UTF-8?q?add=20test=5Flast=5Ffull=5Fmoon.py=20=E2=80=94?= =?UTF-8?q?=20smoke-test=20for=20moon=20phase=20+=20sun=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finds the most recent full moon (within 35 days), prints illumination, phase angle, alt/az, and parallactic angle, then calls sun_events_in_range() for the surrounding day. The sun_events call is the direct regression path for the sunrise_sunset() observer fix (bare topos, not earth+topos). https://claude.ai/code/session_01JieSeNQZ3X6fhsb11YyJrK --- test_last_full_moon.py | 66 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 test_last_full_moon.py diff --git a/test_last_full_moon.py b/test_last_full_moon.py new file mode 100644 index 0000000..f03a577 --- /dev/null +++ b/test_last_full_moon.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""test_last_full_moon.py — smoke-test moon_phase.py against the most recent full moon. + +Exercises: + - full_moons_in_range() — phase detection + - sun_events_in_range() — sunrise/sunset almanac (requires bare topos, not earth+topos) + - illumination() / altaz() / parallactic_angle() + +Run from the sky-cam directory: + + python3 test_last_full_moon.py +""" + +import sys +import pathlib +from datetime import datetime, timedelta, timezone + +HERE = pathlib.Path(__file__).resolve().parent +sys.path.insert(0, str(HERE)) + +import moon_phase as mp + + +def main() -> int: + now = datetime.now(timezone.utc) + + # Last full moon: search back up to 35 days + candidates = mp.full_moons_in_range(now - timedelta(days=35), now) + if not candidates: + print("ERROR: no full moon found in the last 35 days", file=sys.stderr) + return 1 + fm = candidates[-1] + + print(f"last full moon (UTC) : {fm.strftime('%Y-%m-%dT%H:%M:%SZ')}") + print(f"local : {mp._format_local(fm)}") + + # Moon stats at exact full moon moment + illum = mp.illumination(fm) + pa = mp.phase_angle(fm) + alt, az = mp.altaz(fm) + para = mp.parallactic_angle(fm) + print(f"illumination : {illum:.4f} ({illum*100:.1f}%)") + print(f"phase angle : {pa:.2f}° (0° = full)") + print(f"altitude / azimuth : {alt:.2f}° / {az:.2f}°") + print(f"parallactic angle : {para:.2f}°") + print() + + # Sunrise/sunset for the 24-hour window around the full moon — exercises the fix + day_start = fm.replace(hour=0, minute=0, second=0, microsecond=0) + day_end = day_start + timedelta(days=1) + events = mp.sun_events_in_range(day_start, day_end) + if events: + print(f"sun events on {day_start.strftime('%Y-%m-%d')} (UTC):") + for t, is_rise in events: + label = "sunrise" if is_rise else "sunset " + print(f" {label} {t.strftime('%H:%M:%S')} UTC ({mp._format_local(t)})") + else: + print(f"no sun events found on {day_start.strftime('%Y-%m-%d')} (polar location?)") + + print() + print("OK") + return 0 + + +if __name__ == "__main__": + sys.exit(main())