Skip to content

Cap the meridian transit search step at skyfield's own default - #5

Open
jrx-code wants to merge 4 commits into
okkine:mainfrom
jrx-code:fix/meridian-step-days
Open

jrx-code wants to merge 4 commits into
okkine:mainfrom
jrx-code:fix/meridian-step-days

Conversation

@jrx-code

Copy link
Copy Markdown

next_transit, previous_transit, next_antitransit and previous_antitransit each set

f = almanac.meridian_transits(self._eph, self._body, self._observer_location)
window_days = (self._search_end - self._search_start).total_seconds() / 86400.0
f.step_days = 0.4 * window_days

Skyfield ships step_days = 0.4 on the function meridian_transits() returns ("twice a day"), which is half the spacing between an upper and a lower transit. Scaling that step with the search window means any window longer than a day samples the sign function too coarsely and find_discrete steps over whole transits.

This PR lets the window shrink the step but never grow it.

What it costs today

Checked against a reference search at step_days = 0.05, 10 bodies, 6 window lengths, 53.5 N 14.5 E:

window step before step after bodies with a wrong result before after
0.25 d 0.1 0.1 0/10 0/10
1 d 0.4 0.4 0/10 0/10
2 d 0.8 0.4 9/10 0/10
7 d 2.8 0.4 10/10 0/10
14 d 5.6 0.4 10/10 0/10
30 d 12.0 0.4 10/10 0/10

39 of 60 combinations returned the wrong set of transits before, 0 after.

The 14 day window is the one TransitSensor uses for its previous attribute (_RISE_SET_PRIOR_WINDOW_DAYS): the old step found 18 of the 28 transits actually in the window, which is also why the code needs the "keep searching forward" loop afterwards.

A concrete miss, 2 day window over 2026-10-26 Europe/Warsaw:

uranus  before: ['10-27 01:52']
        after:  ['10-26 01:56', '10-27 01:52']

What it costs in time

Nothing measurable. A 14 day Uranus search takes 29.0 ms at the old step and 31.5 ms at 0.4, because find_discrete refines around each crossing either way.

🤖 Generated with Claude Code

jrx-code and others added 4 commits October 26, 2026 19:36
Adding a timedelta to a timezone-aware datetime is wall-clock arithmetic
and resets `fold`. Inside the autumn DST fold `candidate + timedelta(
seconds=1)` can therefore land a full hour BEFORE `candidate` in absolute
time, so the `while search_pos < ...` loops in TransitSensor and
AntitransitSensor keep re-finding the same event and never terminate.

Because the search runs synchronously in `async_added_to_hass`, the whole
Home Assistant event loop is blocked: the instance accepts TCP connections
on 8123 but never answers them.

Reproduced with Europe/Warsaw on 2026-10-25, when the Uranus transit fell
at 02:00:24 fold=1:

    candidate      -> UTC 2026-10-25 01:00:24
    candidate + 1s -> UTC 2026-10-25 00:00:25   (one hour earlier)

Since `_RISE_SET_PRIOR_WINDOW_DAYS` is 14, every restart in the two weeks
after a DST change hits this.

Fix: `_advance_one_second()` performs the step in UTC, keeping it
monotonic, and three `if candidate <= last_found: break` guards make the
loops terminate even if another non-monotonic step is ever introduced.

Verified offline against real ephemerides with ZoneInfo('Europe/Warsaw'):
uranus went from >300 iterations without terminating to 4 iterations in
0.1 s; mercury, saturn and neptune returned identical results before and
after.

Refs: okkine#3, okkine#2

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Keeps the fork installable from HACS as a custom repository until the
DST fix lands upstream (okkine#4).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
next_transit, previous_transit, next_antitransit and previous_antitransit
set the find_discrete step to 0.4 * window_days. Skyfield's
almanac.meridian_transits() ships step_days = 0.4, which is half the ~0.5 day
spacing between an upper and a lower transit, so any window longer than a day
makes the sampler step over whole transits and miss them.

The window may now only shrink the step, never grow it.

Checked against a reference search at step_days = 0.05 for 10 bodies over
6 window lengths (0.25, 1, 2, 7, 14 and 30 days) at 53.5 N 14.5 E:
39 of 60 combinations returned the wrong set of transits before, 0 after.
Windows of a day or less are unaffected. Cost is unchanged: a 14 day search
for Uranus takes 29.0 ms at the old step and 31.5 ms at 0.4, because
find_discrete refines around each crossing either way.

The 14 day window is the one TransitSensor uses for the "previous" attribute,
where the old step found 18 of the 28 transits actually in the window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant