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.
This commit is contained in:
Don Cross
2022-09-26 22:07:47 -04:00
parent 3bed4a9bdc
commit 53b4b33958
5 changed files with 102 additions and 22 deletions

View File

@@ -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.

View File

@@ -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)
}
/**

View File

@@ -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. |

View File

@@ -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)
}
/**

View File

@@ -956,6 +956,52 @@ class Tests {
//----------------------------------------------------------------------------------------
@Test
fun `Reverse chrono new moon search`() {
val numNewMoons = 5000
val utList = arrayListOf<Double>()
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"