Skip to content

SPACalculator: near the poles, rise/set can return a time from a different day #294

Closed

Description

@dickermoshe

Summary

Close to the poles, SPACalculator.getUTCSunrise / getUTCSunset sometimes return a real crossing of the requested angle, but one that happens days before or after the requested date. The result is wrapped to 0–24 hours, so it looks like an ordinary time on the requested day. The correct answer in these cases is NaN, because the Sun doesn't reach the angle on the requested day at all.

This is separate from #293, which handles the iteration not converging. Here the iteration converges properly, just onto the wrong day, so the residual check in #293 can't catch it.

Examples

All three use elevation 0, adjustForElevation = false and the default SPACalculator (ΔT on). I confirmed the values on CI with the current master (11fd247):

SPACalculator calc = new SPACalculator();
GeoLocation a = new GeoLocation("A", 86.4, 56, 0, ZoneId.of("UTC"));
calc.getUTCSunrise(LocalDate.parse("2014-10-26"), a, 106, false); // 17.91085938, expected NaN
Location Date Zenith Event Returned (UTC h) Solar noon (UTC h) What it actually is
A 86.4, 56 2014-10-26 106 (16° below the horizon) sunrise 17.9109 8.0000 A crossing on Oct 27 at 17:55 UTC, 1.4 days after the requested noon
B 83.9, 35 2041-09-22 96 (civil dawn) sunrise 19.8838 9.5438 A crossing on Sep 23 at 19:53 UTC, 1.4 days after the requested noon
C -84.3, -2 2039-08-26 106 sunset 19.2637 12.1647 A crossing on Aug 13, 12.7 days earlier

In each case the Sun does not cross the requested zenith in the half-day where that event belongs (between the previous midnight and noon for sunrise, between noon and the next midnight for sunset). So the method should return NaN.

Why it happens

solveRiseSet starts from the closed-form guess and runs the secant iteration on the topocentric zenith. Near the poles, on days when the Sun only skims the requested angle, the closed-form guess says the event happens. The iteration then finds no crossing nearby, wanders along the time axis, and can settle on a genuine crossing on a neighbouring day (or, as in #293, on nothing at all). Nothing checks that the solution is on the requested day before it's wrapped by (riseSet % 24 + 24) % 24.

How often

In a randomized run of 2 million sunrise/sunset requests at |latitude| 80°–90° (dates 2000–2050, zeniths 96–116), about 365,000 returned a time. Of those, 57 were more than 12 hours from that day's solar noon, i.e. on another day. In all 57, no crossing existed in the correct half-day. I didn't see it below about 81°.

Possible fix (not tested against the full suite)

After the iteration, return NaN if the solution is more than 12 hours from the date's solar noon. noonMin is already computed in solveRiseSet:

if (Math.abs(t1 - noonMin) > 720) {
    return Double.NaN; // converged onto a crossing on a different day
}

In the run above, this would have caught all 57 and rejected no answer that had a crossing on the correct day. Being stricter (requiring sunrise to come before noon and sunset after it) may also be worth considering. Very near the poles, though, "sunrise" and "sunset" can be close to noon, so that needs more care.

Found while comparing KosherJava with the Rust port kosher-rust, which has the same behaviour.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions