Repository navigation
SPACalculator: near the poles, rise/set can return a time from a different day #294
Description
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.
Activity