Add waxing/waning crescent phases; fix phase-shadow double-application
moon_detect.py - detect_moon() accepts saturated_threshold / min_roundness / max_halo_ratio so crescent phases (dim, non-circular arc) can be detected with relaxed thresholds without changing defaults for full/quarter detection. moon_phase.py - crescent_times_in_range(): 2-hour scan returning the UTC moment illumination crosses a target fraction (default 28%) on the waxing or waning side. Used for scheduling just like phase_events_in_range() for named phases. moon_composite.py - Remove _apply_phase_shadow() from composite_full_moon. NASA SVS Dial-a-Moon renders already include the correct terminator, libration and earthshine for the exact hour; applying the shadow on top double-darkened the unlit limb on every non-full phase. The function stays available for callers that need it. moon_phase_monthly.py - Add waxing-crescent (🌒) and waning-crescent (🌘) to PHASE_SPEC. Timing via crescent_times_in_range(); scheduling identical to other phases. - CRESCENT_* tuning constants (all overridable from sky-cam.conf): MOON_CRESCENT_TARGET_ILLUM default 0.28 MOON_CRESCENT_MIN_QUALITY default 0.35 MOON_CRESCENT_SATURATED_THR default 180 MOON_CRESCENT_MIN_ROUNDNESS default 0.15 MOON_CRESCENT_MAX_HALO_RATIO default 12.0 - run_phase() and auto_run() branch on spec['crescent'] to use crescent timing and detection params. - Atmospheric background naturally captures twilight colours (dawn for waning crescent, dusk/dawn for waxing) from the real east frame. https://claude.ai/code/session_01HuJ83KvMvshiY6HxJbtMsc
This commit is contained in:
@@ -107,6 +107,46 @@ def full_moons_in_range(start: datetime, end: datetime) -> list[datetime]:
|
||||
return phase_events_in_range(start, end, 2)
|
||||
|
||||
|
||||
def crescent_times_in_range(
|
||||
start: datetime,
|
||||
end: datetime,
|
||||
target_illum: float = 0.28,
|
||||
waxing_side: bool = True,
|
||||
) -> list[datetime]:
|
||||
"""Return UTC datetimes when illumination crosses target_illum on the given side.
|
||||
|
||||
Scans in 2-hour steps and records each moment illumination passes through
|
||||
target_illum while waxing (waxing_side=True) or waning (waxing_side=False).
|
||||
Yields one event per lunar cycle, used for scheduling just like
|
||||
phase_events_in_range() is used for quarter/full events.
|
||||
"""
|
||||
_lazy()
|
||||
step = timedelta(hours=2)
|
||||
results: list[datetime] = []
|
||||
t = _to_utc(start)
|
||||
end_dt = _to_utc(end)
|
||||
prev_illum: float | None = None
|
||||
prev_t: datetime | None = None
|
||||
|
||||
while t <= end_dt:
|
||||
illum_val = illumination(t)
|
||||
is_wax = waxing(t)
|
||||
if is_wax == waxing_side:
|
||||
if prev_illum is not None:
|
||||
if (prev_illum - target_illum) * (illum_val - target_illum) < 0:
|
||||
# Linear interpolation to the crossing moment
|
||||
frac = (target_illum - prev_illum) / (illum_val - prev_illum)
|
||||
results.append(prev_t + frac * (t - prev_t))
|
||||
prev_illum = illum_val
|
||||
prev_t = t
|
||||
else:
|
||||
prev_illum = None
|
||||
prev_t = None
|
||||
t += step
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def phase_events_in_range(start: datetime, end: datetime, phase_index: int) -> list[datetime]:
|
||||
"""Return UTC datetimes of every occurrence of `phase_index` between start and end.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user