Skip to content

find_discrete is more reliable than find_risings/settings #998

Open
@aendie

Description

@aendie

I am running initial tests having converted my Nautical Almanac software to use find_risings() and find_settings() when Skyfield VERSION is 1.48 or higher. Lower versions still use find_discrete(). As both are implemented I began comparing them and I came across a discrepancy that I wish to describe in detail here.

To gain confidence in my 'quick and dirty' test program, I quote three test cases: the first two show correct results. I am aware that almanacs print time in UT (=UT1) and my test program simply prints UTC time, which is irrelevant here. Furthermore times in almanacs are rounded to minutes so a 24-hour search begins at 30 seconds before midnight. My test program begins at midnight (again irrelevant here).

TEST CASE 1: 31-May 2023 to 2-June 2023 at latitude 68°

I expect to see these results (my almanac data using find_discrete()) for Wed Thu Fri:

almanac_2023-05-31

The test Program prints results from find_risings() and find_settings() as 'moonrise' and 'moonset' and results from find_discrete() as 'moonevent' where '0' represents a moonset and '1' a moonrise:

lat_68_from_2023-05-31

The data looks good!

TEST CASE 2: 31-May 2023 to 2-June 2023 at latitude 72°

lat_72_from_2023-05-31

Again, the new routines match the data with the legacy find_discrete() logic. Looks good!

TEST CASE 3: 18-Feb 2023 to 20-Feb 2023 at latitude 70°

Here I expected to see this:

almanac_2023-02-18

but instead saw this:

almanac_2023-02-18_incorrect

Note that my old almanac code searches day-by-day forward for the next moonrise or moonset event to determine the current moon state. "All day above horizon" (a white rectangle) is printed because the next event detected is a moonset at 12:05 on Sunday. So what went wrong here? The Test Program shows this:

lat_70_from_2023-02-18

The "moonrise" at 11:35 on 19-Feb is 'False'. There is no moonrise on 19-Feb. I look into the official HMNAO Nautical Almanac and see a moonrise at 11:28 on Feb 19th. Exactly what find_discrete() prints (when rounded). Why did find_risings() miss it? I use a calculated horizon value but if you omit the last parameter "-horizon" from find_risings(), it makes no difference.

But the horizon value does have an impact on the results. Some reverse-engineering showed that increasing the value of "horizon" by 0.048 degrees not only fixes the problem but gives a result very close to 11:28, i.e. this works:

moonrise, yR = almanac.find_risings(observer, moon, t0, t1, -horizon-0.048)

Is find_risings() using an incorrect horizon value?

Another thing that looks suspicious: on Feb 18th: both "False" events have the same time '2023-02-18 10:34:20Z'.
I understand "False" events to mean that the moon vertical motion changes, even though this is below the horizon, i.e. not an actual moonrise or moonset. But I cannot believe that the moon changes direction twice within a split second.

I cannot dig deeper ....... I hope someone can look into this and either explain or fix the problem.

Here is my Test Program (note I use de421.bsp as my ephemeris):

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from datetime import date, datetime, timedelta
from math import atan, degrees
from skyfield import VERSION, almanac
from skyfield.api import Loader, Topos, wgs84, N, S, E, W
from skyfield.nutationlib import iau2000b

def getHorizon(t):
    # calculate the angle of the moon below the horizon at moonrise/set

    position = earth.at(t).observe(moon)   # at noontime (for daily average distance)
    distance = position.apparent().radec(epoch='date')[2]
    dist_km = distance.km
    sdm = degrees(atan(1737.4/dist_km))   # volumetric mean radius of moon = 1737.4 km
    horizon = sdm + 0.5666667	# moon's equatorial radius + 34' (atmospheric refraction)
    return horizon

def f_moon(topos, degBelowHorizon):
    # Build a function of time that returns the moon state.
    topos_at = (earth + topos).at

    def is_moon_up_at(t):
        """The function that this returns will expect a single argument that is a 
		:class:`~skyfield.timelib.Time` and will return ``True`` if the moon is up,
		else ``False``."""
        t._nutation_angles = iau2000b(t.tt)
        # Return `True` if the moon has risen by time `t`.
        return topos_at(t).observe(moon).apparent().altaz()[0].degrees > -degBelowHorizon

    is_moon_up_at.rough_period = 0.5  # twice a day
    return is_moon_up_at

d = date(2023, 2, 18)       # modify the starting date as required
lat = 70.0                  # modify latitude as required (use float for Topos)

load = Loader("./")
ts = load.timescale()
eph = load('de421.bsp')
earth   = eph['earth']
moon    = eph['moon']
dt = datetime(d.year, d.month, d.day, 0, 0, 0)
print("Skyfield version = {};  Results for latitude {} ...".format(VERSION,lat))

for i in range(3):      # sample 3 successive dates

    t0     = ts.ut1(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
    t0noon = ts.ut1(dt.year, dt.month, dt.day, dt.hour+12, dt.minute, dt.second)
    t1     = ts.ut1(dt.year, dt.month, dt.day+1, dt.hour, dt.minute, dt.second)

    topos = wgs84.latlon(lat, 0.0 * E, elevation_m=0.0)
    observer = earth + topos
    horizon = getHorizon(t0noon)

    moonrise, yR = almanac.find_risings(observer, moon, t0, t1, -horizon)  # -horizon-0.048 fixes problem
    moonset,  yS = almanac.find_settings(observer, moon, t0, t1, -horizon)

    locn = Topos(lat, "0.0 E")
    moonevent, y = almanac.find_discrete(t0, t1, f_moon(locn, horizon))

    print("{}:  {} x moonrise, {} x moonset;  {} x moonevent;  horizon = {:.4f}°".format(dt.strftime("%Y-%m-%d"),len(yR),len(yS),len(y),horizon))
    print("   moonrise ",moonrise.utc_iso(' '),"   ",yR)
    print("   moonset  ",moonset.utc_iso(' ') ,"   ",yS)
    print("   moonevent",moonevent.utc_iso(' '),"   ",y)
    
    dt += timedelta(days=1)

Sincere thanks to whoever looks into this case. I hope I provided a useful test case here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions