- 使用压缩表加速查找日月食沙罗周期信息 - 优化日月食搜索跳步,减少非食季朔望月扫描 - 新增本地日全食、日环食、月全食搜索接口,返回 ok 区分未找到结果 - 新增水星、金星地心凌日查询及测试
521 lines
17 KiB
Go
521 lines
17 KiB
Go
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)
|
||
}
|