From 53b4b339582c8b22c236e64211c38e4c19cb3897 Mon Sep 17 00:00:00 2001 From: Don Cross Date: Mon, 26 Sep 2022 22:07:47 -0400 Subject: [PATCH] Kotlin searchMoonPhase: allow searching backward in time. Enhanced the Kotlin function searchMoonPhase to allow searching forward in time when the `limitDays` argument is positive, or backward in time when `limitDays` is negative. Added unit test "moon_reverse" to verify this new feature. --- generate/dotnet/csharp_test/csharp_test.cs | 2 +- generate/template/astronomy.kt | 37 +++++++++++---- source/kotlin/doc/search-moon-phase.md | 2 +- .../github/cosinekitty/astronomy/astronomy.kt | 37 +++++++++++---- .../io/github/cosinekitty/astronomy/Tests.kt | 46 +++++++++++++++++++ 5 files changed, 102 insertions(+), 22 deletions(-) diff --git a/generate/dotnet/csharp_test/csharp_test.cs b/generate/dotnet/csharp_test/csharp_test.cs index d940cfe3..c3cb4f75 100644 --- a/generate/dotnet/csharp_test/csharp_test.cs +++ b/generate/dotnet/csharp_test/csharp_test.cs @@ -635,7 +635,7 @@ namespace csharp_test const int numNewMoons = 5000; var utList = new double[numNewMoons]; - double dtMin = 1000.0; + double dtMin = +1000.0; double dtMax = -1000.0; // Search forward in time from 1800 to find consecutive new moon events. diff --git a/generate/template/astronomy.kt b/generate/template/astronomy.kt index c5786a33..5bdbe5a6 100644 --- a/generate/template/astronomy.kt +++ b/generate/template/astronomy.kt @@ -39,6 +39,7 @@ import kotlin.math.cos import kotlin.math.floor import kotlin.math.hypot import kotlin.math.log10 +import kotlin.math.max import kotlin.math.min import kotlin.math.PI import kotlin.math.pow @@ -5829,7 +5830,9 @@ fun moonPhase(time: Time): Double = * The beginning of the time window in which to search for the Moon reaching the specified phase. * * @param limitDays - * The number of days after `startTime` that limits the time window for the search. + * The number of days away from `startTime` that limits the time window for the search. + * If the value is negative, the search is performed into the past from `startTime`. + * Otherwise, the search is performed into the future from `startTime`. * * @return * If successful, returns the date and time the moon reaches the phase specified by @@ -5846,18 +5849,32 @@ fun searchMoonPhase(targetLon: Double, startTime: Time, limitDays: Double): Time // I have seen more than 0.9 days away from the simple prediction. // To be safe, we take the predicted time of the event and search // +/-1.5 days around it (a 3-day wide window). - val moonOffset = SearchContext { time -> longitudeOffset(moonPhase(time) - targetLon) } - var ya = moonOffset.eval(startTime) - if (ya > 0.0) ya -= 360.0 // force searching forward in time, not backward val uncertainty = 1.5 - val estDt = -(MEAN_SYNODIC_MONTH * ya) / 360.0 - val dt1 = estDt - uncertainty - if (dt1 > limitDays) - return null // not possible for moon phase to occur within specified window (too short) - val dt2 = min(limitDays, estDt + uncertainty) + val moonOffset = SearchContext { time -> longitudeOffset(moonPhase(time) - targetLon) } + var estDt: Double + var dt1: Double + var dt2: Double + var ya = moonOffset.eval(startTime) + if (limitDays < 0.0) { + // Search backward in time. + if (ya < 0.0) ya += 360.0 + estDt = -(MEAN_SYNODIC_MONTH * ya) / 360.0 + dt2 = estDt + uncertainty + if (dt2 < limitDays) + return null // not possible for moon phase to occur within specified window (too short) + dt1 = max(limitDays, estDt - uncertainty) + } else { + // Search forward in time + if (ya > 0.0) ya -= 360.0 + estDt = -(MEAN_SYNODIC_MONTH * ya) / 360.0 + dt1 = estDt - uncertainty + if (dt1 > limitDays) + return null // not possible for moon phase to occur within specified window (too short) + dt2 = min(limitDays, estDt + uncertainty) + } val t1 = startTime.addDays(dt1) val t2 = startTime.addDays(dt2) - return search(t1, t2, 1.0, moonOffset) + return search(t1, t2, 0.1, moonOffset) } /** diff --git a/source/kotlin/doc/search-moon-phase.md b/source/kotlin/doc/search-moon-phase.md index 5c923c89..e04a9870 100644 --- a/source/kotlin/doc/search-moon-phase.md +++ b/source/kotlin/doc/search-moon-phase.md @@ -22,4 +22,4 @@ If successful, returns the date and time the moon reaches the phase specified by |---|---| | targetLon | The difference in geocentric longitude between the Sun and Moon that specifies the lunar phase being sought. This can be any value in the range [0, 360). Certain values have conventional names: 0 = new moon, 90 = first quarter, 180 = full moon, 270 = third quarter. | | startTime | The beginning of the time window in which to search for the Moon reaching the specified phase. | -| limitDays | The number of days after startTime that limits the time window for the search. | +| limitDays | The number of days away from startTime that limits the time window for the search. If the value is negative, the search is performed into the past from startTime. Otherwise, the search is performed into the future from startTime. | diff --git a/source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt b/source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt index 92dc8e54..02923185 100644 --- a/source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt +++ b/source/kotlin/src/main/kotlin/io/github/cosinekitty/astronomy/astronomy.kt @@ -39,6 +39,7 @@ import kotlin.math.cos import kotlin.math.floor import kotlin.math.hypot import kotlin.math.log10 +import kotlin.math.max import kotlin.math.min import kotlin.math.PI import kotlin.math.pow @@ -5829,7 +5830,9 @@ fun moonPhase(time: Time): Double = * The beginning of the time window in which to search for the Moon reaching the specified phase. * * @param limitDays - * The number of days after `startTime` that limits the time window for the search. + * The number of days away from `startTime` that limits the time window for the search. + * If the value is negative, the search is performed into the past from `startTime`. + * Otherwise, the search is performed into the future from `startTime`. * * @return * If successful, returns the date and time the moon reaches the phase specified by @@ -5846,18 +5849,32 @@ fun searchMoonPhase(targetLon: Double, startTime: Time, limitDays: Double): Time // I have seen more than 0.9 days away from the simple prediction. // To be safe, we take the predicted time of the event and search // +/-1.5 days around it (a 3-day wide window). - val moonOffset = SearchContext { time -> longitudeOffset(moonPhase(time) - targetLon) } - var ya = moonOffset.eval(startTime) - if (ya > 0.0) ya -= 360.0 // force searching forward in time, not backward val uncertainty = 1.5 - val estDt = -(MEAN_SYNODIC_MONTH * ya) / 360.0 - val dt1 = estDt - uncertainty - if (dt1 > limitDays) - return null // not possible for moon phase to occur within specified window (too short) - val dt2 = min(limitDays, estDt + uncertainty) + val moonOffset = SearchContext { time -> longitudeOffset(moonPhase(time) - targetLon) } + var estDt: Double + var dt1: Double + var dt2: Double + var ya = moonOffset.eval(startTime) + if (limitDays < 0.0) { + // Search backward in time. + if (ya < 0.0) ya += 360.0 + estDt = -(MEAN_SYNODIC_MONTH * ya) / 360.0 + dt2 = estDt + uncertainty + if (dt2 < limitDays) + return null // not possible for moon phase to occur within specified window (too short) + dt1 = max(limitDays, estDt - uncertainty) + } else { + // Search forward in time + if (ya > 0.0) ya -= 360.0 + estDt = -(MEAN_SYNODIC_MONTH * ya) / 360.0 + dt1 = estDt - uncertainty + if (dt1 > limitDays) + return null // not possible for moon phase to occur within specified window (too short) + dt2 = min(limitDays, estDt + uncertainty) + } val t1 = startTime.addDays(dt1) val t2 = startTime.addDays(dt2) - return search(t1, t2, 1.0, moonOffset) + return search(t1, t2, 0.1, moonOffset) } /** diff --git a/source/kotlin/src/test/kotlin/io/github/cosinekitty/astronomy/Tests.kt b/source/kotlin/src/test/kotlin/io/github/cosinekitty/astronomy/Tests.kt index 33f83482..cfdd91cd 100644 --- a/source/kotlin/src/test/kotlin/io/github/cosinekitty/astronomy/Tests.kt +++ b/source/kotlin/src/test/kotlin/io/github/cosinekitty/astronomy/Tests.kt @@ -956,6 +956,52 @@ class Tests { //---------------------------------------------------------------------------------------- + @Test + fun `Reverse chrono new moon search`() { + val numNewMoons = 5000 + val utList = arrayListOf() + var dtMin = +1000.0 + var dtMax = -1000.0 + + // Search forward in time from 1800 to find consecutive new moon events. + var time = Time(1800, 1, 1, 0, 0, 0.0) + var i = 0 + while (i < numNewMoons) { + val result = searchMoonPhase(0.0, time, +40.0) ?: + fail("Failed to find new moon after $time") + utList.add(result.ut) + if (i > 0) { + // Verify that consecutive new moons are reasonably close to the synodic period (29.5 days) apart. + val dt = utList[i] - utList[i-1] + if (dt < dtMin) dtMin = dt + if (dt > dtMax) dtMax = dt + } + time = result.addDays(+0.1) + ++i + } + + if (dtMin < 29.273 || dtMax > 29.832) + fail("Time between consecutive new moons is suspicious: dtMin=$dtMin, dtMax=$dtMax.") + + // Do a reverse chronological search and make sure the results are consistent with the forward search. + time = time.addDays(+20.0) + var maxDiff = 0.0 + i = numNewMoons - 1 + while (i >= 0) { + val result = searchMoonPhase(0.0, time, -40.0) ?: + fail("Failed to find new moon before $time") + val diff = 86400.0 * abs(result.ut - utList[i]) + if (diff > maxDiff) maxDiff = diff + time = result.addDays(-0.1) + --i + } + + if (maxDiff > 0.128) + fail("Excessive discrepancy in reverse search: $maxDiff seconds.") + } + + //---------------------------------------------------------------------------------------- + @Test fun `Rise set test`() { val filename = dataRootDir + "riseset/riseset.txt"