feat: 增强日月食搜索、沙罗周期与内行星凌日
- 使用压缩表加速查找日月食沙罗周期信息 - 优化日月食搜索跳步,减少非食季朔望月扫描 - 新增本地日全食、日环食、月全食搜索接口,返回 ok 区分未找到结果 - 新增水星、金星地心凌日查询及测试
This commit is contained in:
@@ -0,0 +1,520 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
. "b612.me/astro/tools"
|
||||
)
|
||||
|
||||
const (
|
||||
planetTransitMeanSolarMotionDegPerDay = 360.0 / 365.2422
|
||||
planetTransitTropicalYearDays = 365.2422
|
||||
planetTransitSeasonProbeStepDays = 20.0
|
||||
planetTransitSearchLimit = 2400
|
||||
planetTransitSearchEpsilonDays = 1.0 / 86400.0
|
||||
planetTransitGreatestWindowDays = 1.2
|
||||
planetTransitGreatestToleranceDays = 0.25 / 86400.0
|
||||
planetTransitContactStepDays = 0.02
|
||||
planetTransitContactSpanDays = 1.0
|
||||
planetTransitContactToleranceDays = 0.25 / 86400.0
|
||||
planetTransitCoarseN = 16
|
||||
)
|
||||
|
||||
// PlanetTransitResult 表示一次地心行星凌日结果。
|
||||
//
|
||||
// Valid 为 false 时表示没有找到有效凌日。所有时刻字段均为 UT 儒略日。
|
||||
// MinimumSeparationArcsec、SunSemidiameterArcsec、PlanetSemidiameterArcsec 的单位均为角秒。
|
||||
type PlanetTransitResult struct {
|
||||
Valid bool
|
||||
|
||||
// PlanetIndex 为行星序号,1 表示水星,2 表示金星。
|
||||
PlanetIndex int
|
||||
|
||||
// ExternalIngress / ExternalEgress 为一触 / 四触。
|
||||
ExternalIngress float64
|
||||
ExternalEgress float64
|
||||
// InternalIngress / InternalEgress 为二触 / 三触。掠凌没有内切接触时为 0。
|
||||
InternalIngress float64
|
||||
InternalEgress float64
|
||||
// Greatest 为凌甚,即行星中心最接近太阳中心的时刻。
|
||||
Greatest float64
|
||||
|
||||
MinimumSeparationArcsec float64
|
||||
SunSemidiameterArcsec float64
|
||||
PlanetSemidiameterArcsec float64
|
||||
|
||||
HasExternal bool
|
||||
HasInternal bool
|
||||
}
|
||||
|
||||
type planetTransitConfig struct {
|
||||
planetIndex int
|
||||
|
||||
synodicPeriodDays float64
|
||||
anchorInferiorTT float64
|
||||
|
||||
seasonWindowDays float64
|
||||
latitudePrefilter float64
|
||||
conjunctionStepDay float64
|
||||
|
||||
apparentLoN func(float64, int) float64
|
||||
apparentBoN func(float64, int) float64
|
||||
apparentRaDecN func(float64, int) (float64, float64)
|
||||
semidiameterN func(float64, int) float64
|
||||
earthDistanceN func(float64, int) float64
|
||||
nodeN func(float64, int) float64
|
||||
}
|
||||
|
||||
type planetTransitState struct {
|
||||
jdTT float64
|
||||
separationArcsec float64
|
||||
separationSquared float64
|
||||
sunSemidiameter float64
|
||||
planetSemidiameter float64
|
||||
externalContactMetric float64
|
||||
internalContactMetric float64
|
||||
}
|
||||
|
||||
func mercuryTransitConfig() planetTransitConfig {
|
||||
return planetTransitConfig{
|
||||
planetIndex: 1,
|
||||
synodicPeriodDays: MERCURY_S_PERIOD,
|
||||
anchorInferiorTT: TD2UT(JDECalc(2019, 11, 11+(15+21.0/60+40.0/3600)/24), true),
|
||||
seasonWindowDays: 12,
|
||||
latitudePrefilter: 1.0,
|
||||
conjunctionStepDay: 0.00001,
|
||||
apparentLoN: MercuryApparentLoN,
|
||||
apparentBoN: MercuryApparentBoN,
|
||||
apparentRaDecN: MercuryApparentRaDecN,
|
||||
semidiameterN: MercurySemidiameterN,
|
||||
earthDistanceN: EarthMercuryAwayN,
|
||||
nodeN: MercuryAscendingNodeN,
|
||||
}
|
||||
}
|
||||
|
||||
func venusTransitConfig() planetTransitConfig {
|
||||
return planetTransitConfig{
|
||||
planetIndex: 2,
|
||||
synodicPeriodDays: VENUS_S_PERIOD,
|
||||
anchorInferiorTT: TD2UT(JDECalc(2012, 6, 6+(1+29.0/60)/24), true),
|
||||
seasonWindowDays: 8,
|
||||
latitudePrefilter: 0.8,
|
||||
conjunctionStepDay: 0.00001,
|
||||
apparentLoN: VenusApparentLoN,
|
||||
apparentBoN: VenusApparentBoN,
|
||||
apparentRaDecN: VenusApparentRaDecN,
|
||||
semidiameterN: VenusSemidiameterN,
|
||||
earthDistanceN: EarthVenusAwayN,
|
||||
nodeN: VenusAscendingNodeN,
|
||||
}
|
||||
}
|
||||
|
||||
// NextMercuryTransit 返回给定时刻之后的下一次地心水星凌日。
|
||||
func NextMercuryTransit(jde float64) PlanetTransitResult {
|
||||
result, _ := searchPlanetTransit(jde, mercuryTransitConfig(), 1, false)
|
||||
return result
|
||||
}
|
||||
|
||||
// LastMercuryTransit 返回给定时刻之前的上一次地心水星凌日。
|
||||
func LastMercuryTransit(jde float64) PlanetTransitResult {
|
||||
result, _ := searchPlanetTransit(jde, mercuryTransitConfig(), -1, true)
|
||||
return result
|
||||
}
|
||||
|
||||
// ClosestMercuryTransit 返回距给定时刻最近的一次地心水星凌日。
|
||||
func ClosestMercuryTransit(jde float64) PlanetTransitResult {
|
||||
return closestPlanetTransit(jde, mercuryTransitConfig())
|
||||
}
|
||||
|
||||
// NextVenusTransit 返回给定时刻之后的下一次地心金星凌日。
|
||||
func NextVenusTransit(jde float64) PlanetTransitResult {
|
||||
result, _ := searchPlanetTransit(jde, venusTransitConfig(), 1, false)
|
||||
return result
|
||||
}
|
||||
|
||||
// LastVenusTransit 返回给定时刻之前的上一次地心金星凌日。
|
||||
func LastVenusTransit(jde float64) PlanetTransitResult {
|
||||
result, _ := searchPlanetTransit(jde, venusTransitConfig(), -1, true)
|
||||
return result
|
||||
}
|
||||
|
||||
// ClosestVenusTransit 返回距给定时刻最近的一次地心金星凌日。
|
||||
func ClosestVenusTransit(jde float64) PlanetTransitResult {
|
||||
return closestPlanetTransit(jde, venusTransitConfig())
|
||||
}
|
||||
|
||||
func closestPlanetTransit(jde float64, cfg planetTransitConfig) PlanetTransitResult {
|
||||
last, hasLast := searchPlanetTransit(jde, cfg, -1, true)
|
||||
next, hasNext := searchPlanetTransit(jde, cfg, 1, false)
|
||||
switch {
|
||||
case hasLast && !hasNext:
|
||||
return last
|
||||
case !hasLast && hasNext:
|
||||
return next
|
||||
case !hasLast && !hasNext:
|
||||
return PlanetTransitResult{}
|
||||
}
|
||||
if math.Abs(last.Greatest-jde) <= math.Abs(next.Greatest-jde) {
|
||||
return last
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
func searchPlanetTransit(jde float64, cfg planetTransitConfig, direction int, includeCurrent bool) (PlanetTransitResult, bool) {
|
||||
if !isFiniteFloat(jde) || direction == 0 {
|
||||
return PlanetTransitResult{}, false
|
||||
}
|
||||
|
||||
targetTT := TD2UT(jde, true)
|
||||
probeTT := targetTT
|
||||
for i := 0; i < planetTransitSearchLimit; i++ {
|
||||
seasonTT, ok := nextPlanetTransitSeasonTT(probeTT, cfg, direction)
|
||||
if !ok {
|
||||
return PlanetTransitResult{}, false
|
||||
}
|
||||
|
||||
seedTT := nearestPlanetTransitInferiorSeedTT(seasonTT, cfg)
|
||||
if math.Abs(seedTT-seasonTT) <= cfg.seasonWindowDays {
|
||||
conjunctionTT := refinePlanetTransitInferiorConjunctionTT(seedTT, cfg)
|
||||
if math.Abs(conjunctionTT-seasonTT) <= cfg.seasonWindowDays+1 && isPotentialPlanetTransit(conjunctionTT, cfg) {
|
||||
resultTT, ok := planetTransitAtInferiorConjunctionTT(conjunctionTT, cfg)
|
||||
if ok && planetTransitMatchesDirection(resultTT.Greatest, targetTT, direction, includeCurrent) {
|
||||
return planetTransitResultTTToUT(resultTT), true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
probeTT = seasonTT + float64(direction)*planetTransitSeasonProbeStepDays
|
||||
}
|
||||
|
||||
return PlanetTransitResult{}, false
|
||||
}
|
||||
|
||||
func nextPlanetTransitSeasonTT(jdTT float64, cfg planetTransitConfig, direction int) (float64, bool) {
|
||||
best := math.NaN()
|
||||
for nodeOffset := 0; nodeOffset <= 1; nodeOffset++ {
|
||||
candidate := estimatePlanetTransitSeasonTT(jdTT, cfg, nodeOffset, direction)
|
||||
candidate = refinePlanetTransitSeasonTT(candidate, cfg, nodeOffset)
|
||||
for !planetTransitMatchesDirection(candidate, jdTT, direction, false) {
|
||||
candidate += float64(direction) * planetTransitTropicalYearDays
|
||||
candidate = refinePlanetTransitSeasonTT(candidate, cfg, nodeOffset)
|
||||
}
|
||||
if !isFiniteFloat(best) || math.Abs(candidate-jdTT) < math.Abs(best-jdTT) {
|
||||
best = candidate
|
||||
}
|
||||
}
|
||||
if !isFiniteFloat(best) {
|
||||
return 0, false
|
||||
}
|
||||
return best, true
|
||||
}
|
||||
|
||||
func estimatePlanetTransitSeasonTT(jdTT float64, cfg planetTransitConfig, nodeOffset int, direction int) float64 {
|
||||
sunLongitude := HSunApparentLoN(jdTT, planetTransitCoarseN)
|
||||
nodeLongitude := planetTransitNodeLongitude(jdTT, cfg, nodeOffset, planetTransitCoarseN)
|
||||
if direction > 0 {
|
||||
delta := Limit360(nodeLongitude - sunLongitude)
|
||||
if delta <= planetTransitSearchEpsilonDays {
|
||||
delta += 360
|
||||
}
|
||||
return jdTT + delta/planetTransitMeanSolarMotionDegPerDay
|
||||
}
|
||||
delta := Limit360(sunLongitude - nodeLongitude)
|
||||
if delta <= planetTransitSearchEpsilonDays {
|
||||
delta += 360
|
||||
}
|
||||
return jdTT - delta/planetTransitMeanSolarMotionDegPerDay
|
||||
}
|
||||
|
||||
func refinePlanetTransitSeasonTT(seedTT float64, cfg planetTransitConfig, nodeOffset int) float64 {
|
||||
current := seedTT
|
||||
for i := 0; i < 8; i++ {
|
||||
prev := current
|
||||
value := planetTransitSunNodeLongitudeDelta(prev, cfg, nodeOffset)
|
||||
slope := (planetTransitSunNodeLongitudeDelta(prev+0.5, cfg, nodeOffset) -
|
||||
planetTransitSunNodeLongitudeDelta(prev-0.5, cfg, nodeOffset)) / 1.0
|
||||
if slope == 0 || !isFiniteFloat(slope) {
|
||||
break
|
||||
}
|
||||
current = prev - value/slope
|
||||
if math.Abs(current-prev) <= 0.00001 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func planetTransitSunNodeLongitudeDelta(jdTT float64, cfg planetTransitConfig, nodeOffset int) float64 {
|
||||
return planetTransitAngleDelta(HSunApparentLoN(jdTT, planetTransitCoarseN) -
|
||||
planetTransitNodeLongitude(jdTT, cfg, nodeOffset, planetTransitCoarseN))
|
||||
}
|
||||
|
||||
func planetTransitNodeLongitude(jdTT float64, cfg planetTransitConfig, nodeOffset int, n int) float64 {
|
||||
return Limit360(cfg.nodeN(jdTT, n) + float64(nodeOffset)*180)
|
||||
}
|
||||
|
||||
func nearestPlanetTransitInferiorSeedTT(seasonTT float64, cfg planetTransitConfig) float64 {
|
||||
k := math.Round((seasonTT - cfg.anchorInferiorTT) / cfg.synodicPeriodDays)
|
||||
return cfg.anchorInferiorTT + k*cfg.synodicPeriodDays
|
||||
}
|
||||
|
||||
func refinePlanetTransitInferiorConjunctionTT(seedTT float64, cfg planetTransitConfig) float64 {
|
||||
current := seedTT
|
||||
for i := 0; i < 4; i++ {
|
||||
prev := current
|
||||
value := planetTransitLongitudeDeltaN(prev, cfg, planetTransitCoarseN)
|
||||
slope := (planetTransitLongitudeDeltaN(prev+cfg.conjunctionStepDay, cfg, planetTransitCoarseN) -
|
||||
planetTransitLongitudeDeltaN(prev-cfg.conjunctionStepDay, cfg, planetTransitCoarseN)) / (2 * cfg.conjunctionStepDay)
|
||||
if slope == 0 || !isFiniteFloat(slope) {
|
||||
break
|
||||
}
|
||||
current = prev - value/slope
|
||||
if math.Abs(current-prev) <= 30.0/86400.0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
for i := 0; i < 8; i++ {
|
||||
prev := current
|
||||
value := planetTransitLongitudeDeltaN(prev, cfg, -1)
|
||||
slope := (planetTransitLongitudeDeltaN(prev+cfg.conjunctionStepDay, cfg, -1) -
|
||||
planetTransitLongitudeDeltaN(prev-cfg.conjunctionStepDay, cfg, -1)) / (2 * cfg.conjunctionStepDay)
|
||||
if slope == 0 || !isFiniteFloat(slope) {
|
||||
break
|
||||
}
|
||||
current = prev - value/slope
|
||||
if math.Abs(current-prev) <= cfg.conjunctionStepDay {
|
||||
break
|
||||
}
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
func planetTransitLongitudeDeltaN(jdTT float64, cfg planetTransitConfig, n int) float64 {
|
||||
return planetTransitAngleDelta(cfg.apparentLoN(jdTT, n) - HSunApparentLoN(jdTT, n))
|
||||
}
|
||||
|
||||
func isPotentialPlanetTransit(conjunctionTT float64, cfg planetTransitConfig) bool {
|
||||
if cfg.earthDistanceN(conjunctionTT, planetTransitCoarseN) > EarthAwayN(conjunctionTT, planetTransitCoarseN) {
|
||||
return false
|
||||
}
|
||||
return math.Abs(cfg.apparentBoN(conjunctionTT, planetTransitCoarseN)) <= cfg.latitudePrefilter
|
||||
}
|
||||
|
||||
func planetTransitAtInferiorConjunctionTT(conjunctionTT float64, cfg planetTransitConfig) (PlanetTransitResult, bool) {
|
||||
greatestTT := greatestPlanetTransitTT(conjunctionTT, cfg)
|
||||
greatestState := planetTransitStateAt(greatestTT, cfg, -1)
|
||||
if !isFiniteFloat(greatestState.externalContactMetric) || greatestState.externalContactMetric > 0 {
|
||||
return PlanetTransitResult{}, false
|
||||
}
|
||||
|
||||
result := PlanetTransitResult{
|
||||
Valid: true,
|
||||
PlanetIndex: cfg.planetIndex,
|
||||
Greatest: greatestTT,
|
||||
MinimumSeparationArcsec: greatestState.separationArcsec,
|
||||
SunSemidiameterArcsec: greatestState.sunSemidiameter,
|
||||
PlanetSemidiameterArcsec: greatestState.planetSemidiameter,
|
||||
HasExternal: true,
|
||||
HasInternal: greatestState.internalContactMetric <= 0,
|
||||
}
|
||||
|
||||
externalIngress, ok := refinePlanetTransitContactTT(greatestTT, cfg, -1, false)
|
||||
if !ok {
|
||||
return PlanetTransitResult{}, false
|
||||
}
|
||||
externalEgress, ok := refinePlanetTransitContactTT(greatestTT, cfg, 1, false)
|
||||
if !ok || externalEgress <= externalIngress {
|
||||
return PlanetTransitResult{}, false
|
||||
}
|
||||
result.ExternalIngress = externalIngress
|
||||
result.ExternalEgress = externalEgress
|
||||
|
||||
if result.HasInternal {
|
||||
internalIngress, ok := refinePlanetTransitContactTT(greatestTT, cfg, -1, true)
|
||||
if ok {
|
||||
result.InternalIngress = internalIngress
|
||||
}
|
||||
internalEgress, ok := refinePlanetTransitContactTT(greatestTT, cfg, 1, true)
|
||||
if ok && internalEgress > internalIngress {
|
||||
result.InternalEgress = internalEgress
|
||||
}
|
||||
result.HasInternal = result.InternalIngress != 0 && result.InternalEgress != 0
|
||||
}
|
||||
|
||||
return result, true
|
||||
}
|
||||
|
||||
func greatestPlanetTransitTT(seedTT float64, cfg planetTransitConfig) float64 {
|
||||
left := seedTT - planetTransitGreatestWindowDays
|
||||
right := seedTT + planetTransitGreatestWindowDays
|
||||
goldenRatio := (math.Sqrt(5) - 1) / 2
|
||||
|
||||
x1 := right - goldenRatio*(right-left)
|
||||
x2 := left + goldenRatio*(right-left)
|
||||
f1 := planetTransitStateAt(x1, cfg, planetTransitCoarseN).separationSquared
|
||||
f2 := planetTransitStateAt(x2, cfg, planetTransitCoarseN).separationSquared
|
||||
|
||||
for i := 0; i < 80 && right-left > planetTransitGreatestToleranceDays; i++ {
|
||||
if f1 <= f2 {
|
||||
right = x2
|
||||
x2 = x1
|
||||
f2 = f1
|
||||
x1 = right - goldenRatio*(right-left)
|
||||
f1 = planetTransitStateAt(x1, cfg, planetTransitCoarseN).separationSquared
|
||||
continue
|
||||
}
|
||||
left = x1
|
||||
x1 = x2
|
||||
f1 = f2
|
||||
x2 = left + goldenRatio*(right-left)
|
||||
f2 = planetTransitStateAt(x2, cfg, planetTransitCoarseN).separationSquared
|
||||
}
|
||||
|
||||
center := (left + right) / 2
|
||||
left = center - 2.0/24.0
|
||||
right = center + 2.0/24.0
|
||||
x1 = right - goldenRatio*(right-left)
|
||||
x2 = left + goldenRatio*(right-left)
|
||||
f1 = planetTransitStateAt(x1, cfg, -1).separationSquared
|
||||
f2 = planetTransitStateAt(x2, cfg, -1).separationSquared
|
||||
for i := 0; i < 80 && right-left > planetTransitGreatestToleranceDays; i++ {
|
||||
if f1 <= f2 {
|
||||
right = x2
|
||||
x2 = x1
|
||||
f2 = f1
|
||||
x1 = right - goldenRatio*(right-left)
|
||||
f1 = planetTransitStateAt(x1, cfg, -1).separationSquared
|
||||
continue
|
||||
}
|
||||
left = x1
|
||||
x1 = x2
|
||||
f1 = f2
|
||||
x2 = left + goldenRatio*(right-left)
|
||||
f2 = planetTransitStateAt(x2, cfg, -1).separationSquared
|
||||
}
|
||||
return (left + right) / 2
|
||||
}
|
||||
|
||||
func planetTransitStateAt(jdTT float64, cfg planetTransitConfig, n int) planetTransitState {
|
||||
planetRA, planetDec := cfg.apparentRaDecN(jdTT, n)
|
||||
sunRA, sunDec := HSunApparentRaDecN(jdTT, n)
|
||||
separationArcsec := StarAngularSeparation(planetRA, planetDec, sunRA, sunDec) * 3600
|
||||
sunSemidiameter := SunSemidiameterN(jdTT, n)
|
||||
planetSemidiameter := cfg.semidiameterN(jdTT, n)
|
||||
return planetTransitState{
|
||||
jdTT: jdTT,
|
||||
separationArcsec: separationArcsec,
|
||||
separationSquared: separationArcsec * separationArcsec,
|
||||
sunSemidiameter: sunSemidiameter,
|
||||
planetSemidiameter: planetSemidiameter,
|
||||
externalContactMetric: separationArcsec - (sunSemidiameter + planetSemidiameter),
|
||||
internalContactMetric: separationArcsec - (sunSemidiameter - planetSemidiameter),
|
||||
}
|
||||
}
|
||||
|
||||
func refinePlanetTransitContactTT(greatestTT float64, cfg planetTransitConfig, direction int, internal bool) (float64, bool) {
|
||||
if direction != -1 && direction != 1 {
|
||||
return 0, false
|
||||
}
|
||||
metric := func(jdTT float64) float64 {
|
||||
state := planetTransitStateAt(jdTT, cfg, -1)
|
||||
if internal {
|
||||
return state.internalContactMetric
|
||||
}
|
||||
return state.externalContactMetric
|
||||
}
|
||||
|
||||
nearJD := greatestTT
|
||||
nearValue := metric(nearJD)
|
||||
if !isFiniteFloat(nearValue) || nearValue > 0 {
|
||||
return 0, false
|
||||
}
|
||||
maxSteps := int(math.Ceil(planetTransitContactSpanDays / planetTransitContactStepDays))
|
||||
for i := 1; i <= maxSteps; i++ {
|
||||
farJD := greatestTT + float64(direction)*planetTransitContactStepDays*float64(i)
|
||||
farValue := metric(farJD)
|
||||
if !isFiniteFloat(farValue) {
|
||||
continue
|
||||
}
|
||||
if farValue >= 0 {
|
||||
return bisectPlanetTransitContactTT(nearJD, nearValue, farJD, farValue, metric)
|
||||
}
|
||||
nearJD = farJD
|
||||
nearValue = farValue
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func bisectPlanetTransitContactTT(leftJD, leftValue, rightJD, rightValue float64, metric func(float64) float64) (float64, bool) {
|
||||
if leftJD > rightJD {
|
||||
leftJD, rightJD = rightJD, leftJD
|
||||
leftValue, rightValue = rightValue, leftValue
|
||||
}
|
||||
if leftValue == 0 {
|
||||
return leftJD, true
|
||||
}
|
||||
if rightValue == 0 {
|
||||
return rightJD, true
|
||||
}
|
||||
if leftValue*rightValue > 0 {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
for i := 0; i < 80 && rightJD-leftJD > planetTransitContactToleranceDays; i++ {
|
||||
midJD := (leftJD + rightJD) / 2
|
||||
midValue := metric(midJD)
|
||||
if !isFiniteFloat(midValue) {
|
||||
return 0, false
|
||||
}
|
||||
if midValue == 0 {
|
||||
return midJD, true
|
||||
}
|
||||
if leftValue*midValue <= 0 {
|
||||
rightJD = midJD
|
||||
rightValue = midValue
|
||||
continue
|
||||
}
|
||||
leftJD = midJD
|
||||
leftValue = midValue
|
||||
}
|
||||
return (leftJD + rightJD) / 2, true
|
||||
}
|
||||
|
||||
func planetTransitResultTTToUT(result PlanetTransitResult) PlanetTransitResult {
|
||||
result.Greatest = TD2UT(result.Greatest, false)
|
||||
result.ExternalIngress = TD2UT(result.ExternalIngress, false)
|
||||
result.ExternalEgress = TD2UT(result.ExternalEgress, false)
|
||||
if result.InternalIngress != 0 {
|
||||
result.InternalIngress = TD2UT(result.InternalIngress, false)
|
||||
}
|
||||
if result.InternalEgress != 0 {
|
||||
result.InternalEgress = TD2UT(result.InternalEgress, false)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func planetTransitMatchesDirection(eventJDE, targetJDE float64, direction int, includeCurrent bool) bool {
|
||||
delta := eventJDE - targetJDE
|
||||
if math.Abs(delta) <= planetTransitSearchEpsilonDays {
|
||||
return includeCurrent
|
||||
}
|
||||
if direction > 0 {
|
||||
return delta > 0
|
||||
}
|
||||
return delta < 0
|
||||
}
|
||||
|
||||
func planetTransitAngleDelta(diff float64) float64 {
|
||||
diff = Limit360(diff)
|
||||
if diff > 180 {
|
||||
diff -= 360
|
||||
}
|
||||
if diff < -180 {
|
||||
diff += 360
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
func isFiniteFloat(value float64) bool {
|
||||
return !math.IsNaN(value) && !math.IsInf(value, 0)
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestKnownMercuryTransits(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
query time.Time
|
||||
greatest time.Time
|
||||
}{
|
||||
{
|
||||
name: "2016 May",
|
||||
query: time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
greatest: time.Date(2016, 5, 9, 14, 57, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "2019 Nov",
|
||||
query: time.Date(2019, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
greatest: time.Date(2019, 11, 11, 15, 20, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := NextMercuryTransit(Date2JDE(tc.query))
|
||||
if !result.Valid {
|
||||
t.Fatal("expected valid transit")
|
||||
}
|
||||
got := JDE2DateByZone(result.Greatest, time.UTC, false)
|
||||
t.Logf("start=%s greatest=%s end=%s min=%.3f sun=%.3f planet=%.3f",
|
||||
JDE2DateByZone(result.ExternalIngress, time.UTC, false),
|
||||
got,
|
||||
JDE2DateByZone(result.ExternalEgress, time.UTC, false),
|
||||
result.MinimumSeparationArcsec,
|
||||
result.SunSemidiameterArcsec,
|
||||
result.PlanetSemidiameterArcsec,
|
||||
)
|
||||
if math.Abs(got.Sub(tc.greatest).Seconds()) > 20*60 {
|
||||
t.Fatalf("greatest mismatch: got %s want near %s", got, tc.greatest)
|
||||
}
|
||||
if !result.HasInternal {
|
||||
t.Fatalf("expected internal contacts")
|
||||
}
|
||||
if !(result.ExternalIngress < result.InternalIngress &&
|
||||
result.InternalIngress < result.Greatest &&
|
||||
result.Greatest < result.InternalEgress &&
|
||||
result.InternalEgress < result.ExternalEgress) {
|
||||
t.Fatalf("contacts out of order: %+v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestKnownVenusTransits(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
query time.Time
|
||||
greatest time.Time
|
||||
}{
|
||||
{
|
||||
name: "2004 Jun",
|
||||
query: time.Date(2004, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
greatest: time.Date(2004, 6, 8, 8, 20, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
name: "2012 Jun",
|
||||
query: time.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
greatest: time.Date(2012, 6, 6, 1, 29, 0, 0, time.UTC),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := NextVenusTransit(Date2JDE(tc.query))
|
||||
if !result.Valid {
|
||||
t.Fatal("expected valid transit")
|
||||
}
|
||||
got := JDE2DateByZone(result.Greatest, time.UTC, false)
|
||||
t.Logf("start=%s greatest=%s end=%s min=%.3f sun=%.3f planet=%.3f",
|
||||
JDE2DateByZone(result.ExternalIngress, time.UTC, false),
|
||||
got,
|
||||
JDE2DateByZone(result.ExternalEgress, time.UTC, false),
|
||||
result.MinimumSeparationArcsec,
|
||||
result.SunSemidiameterArcsec,
|
||||
result.PlanetSemidiameterArcsec,
|
||||
)
|
||||
if math.Abs(got.Sub(tc.greatest).Seconds()) > 20*60 {
|
||||
t.Fatalf("greatest mismatch: got %s want near %s", got, tc.greatest)
|
||||
}
|
||||
if !result.HasInternal {
|
||||
t.Fatalf("expected internal contacts")
|
||||
}
|
||||
if !(result.ExternalIngress < result.InternalIngress &&
|
||||
result.InternalIngress < result.Greatest &&
|
||||
result.Greatest < result.InternalEgress &&
|
||||
result.InternalEgress < result.ExternalEgress) {
|
||||
t.Fatalf("contacts out of order: %+v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransitSearchSkipsSparseEvents(t *testing.T) {
|
||||
mercuryResult := NextMercuryTransit(Date2JDE(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)))
|
||||
if !mercuryResult.Valid {
|
||||
t.Fatal("expected Mercury transit")
|
||||
}
|
||||
mercuryGreatest := JDE2DateByZone(mercuryResult.Greatest, time.UTC, false)
|
||||
if mercuryGreatest.Year() != 2032 || mercuryGreatest.Month() != time.November {
|
||||
t.Fatalf("unexpected next Mercury transit: %s", mercuryGreatest)
|
||||
}
|
||||
|
||||
venusResult := NextVenusTransit(Date2JDE(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)))
|
||||
if !venusResult.Valid {
|
||||
t.Fatal("expected Venus transit")
|
||||
}
|
||||
venusGreatest := JDE2DateByZone(venusResult.Greatest, time.UTC, false)
|
||||
if venusGreatest.Year() != 2117 || venusGreatest.Month() != time.December {
|
||||
t.Fatalf("unexpected next Venus transit: %s", venusGreatest)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNextMercuryTransitFrom2026(b *testing.B) {
|
||||
jd := Date2JDE(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := NextMercuryTransit(jd)
|
||||
if !result.Valid {
|
||||
b.Fatal("expected valid transit")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNextVenusTransitFrom2026(b *testing.B) {
|
||||
jd := Date2JDE(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := NextVenusTransit(jd)
|
||||
if !result.Valid {
|
||||
b.Fatal("expected valid transit")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user