astro/basic/moon_max_declination.go
starainrt 3ffdbe0034
feat: 扩展天文计算能力
- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息
- 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算
- 新增木星伽利略卫星位置、现象与接触事件计算
- 新增恒星星表、星座判定、自行修正与观测辅助能力
- 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包
- 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算
- 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试
- 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试
- 更新中文和英文 README,补充示例、精度说明、SVG 配图
2026-05-01 22:38:44 +08:00

290 lines
10 KiB
Go

package basic
import (
"math"
"sort"
"time"
. "b612.me/astro/tools"
)
const (
moonMaxDeclinationMeanMonthDays = 27.321582247
moonMaxDeclinationBaseCycle = 1336.86
moonMaxDeclinationSearchSpan = 3
)
// DeclinationEvent 赤纬极值事件 / declination extremum event.
type DeclinationEvent struct {
// JDE 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day.
JDE float64
// Declination 是该时刻月心地心赤纬,单位度 / geocentric lunar declination at the event, in degrees.
Declination float64
}
type moonMaxDeclinationCoefficients struct {
D0 float64
M0 float64
MP0 float64
F0 float64
JDE0 float64
sign float64
tc [44]float64
dc [37]float64
}
var moonMaxDeclinationNorthCoefficients = moonMaxDeclinationCoefficients{
D0: 152.2029,
M0: 14.8591,
MP0: 4.6881,
F0: 325.8867,
JDE0: 2451562.5897,
sign: 1,
tc: [44]float64{
0.8975, -0.4726, -0.1030, -0.0976, -0.0462, -0.0461, -0.0438, 0.0162,
-0.0157, 0.0145, 0.0136, -0.0095, -0.0091, -0.0089, 0.0075, -0.0068,
0.0061, -0.0047, -0.0043, -0.0040, -0.0037, 0.0031, 0.0030, -0.0029,
-0.0029, -0.0027, 0.0024, -0.0021, 0.0019, 0.0018, 0.0018, 0.0017,
0.0017, -0.0014, 0.0013, 0.0013, 0.0012, 0.0011, -0.0011, 0.0010,
0.0010, -0.0009, 0.0007, -0.0007,
},
dc: [37]float64{
5.1093, 0.2658, 0.1448, -0.0322, 0.0133, 0.0125, -0.0124, -0.0101,
0.0097, -0.0087, 0.0074, 0.0067, 0.0063, 0.0060, -0.0057, -0.0056,
0.0052, 0.0041, -0.0040, 0.0038, -0.0034, -0.0029, 0.0029, -0.0028,
-0.0028, -0.0023, -0.0021, 0.0019, 0.0018, 0.0017, 0.0015, 0.0014,
-0.0012, -0.0012, -0.0010, -0.0010, 0.0006,
},
}
var moonMaxDeclinationSouthCoefficients = moonMaxDeclinationCoefficients{
D0: 345.6676,
M0: 1.3951,
MP0: 186.21,
F0: 145.1633,
JDE0: 2451548.9289,
sign: -1,
tc: [44]float64{
-0.8975, -0.4726, -0.1030, -0.0976, 0.0541, 0.0516, -0.0438, 0.0112,
0.0157, 0.0023, -0.0136, 0.0110, 0.0091, 0.0089, 0.0075, -0.0030,
-0.0061, -0.0047, -0.0043, 0.0040, -0.0037, -0.0031, 0.0030, 0.0029,
-0.0029, -0.0027, 0.0024, -0.0021, -0.0019, -0.0006, -0.0018, -0.0017,
0.0017, 0.0014, -0.0013, -0.0013, 0.0012, 0.0011, 0.0011, 0.0010,
0.0010, -0.0009, -0.0007, -0.0007,
},
dc: [37]float64{
-5.1093, 0.2658, -0.1448, 0.0322, 0.0133, 0.0125, -0.0015, 0.0101,
-0.0097, 0.0087, 0.0074, 0.0067, -0.0063, -0.0060, 0.0057, -0.0056,
-0.0052, -0.0041, -0.0040, -0.0038, 0.0034, -0.0029, 0.0029, 0.0028,
-0.0028, 0.0023, 0.0021, 0.0019, 0.0018, -0.0017, 0.0015, 0.0014,
0.0012, -0.0012, 0.0010, -0.0010, 0.0037,
},
}
// MoonMaximumNorthDeclinations 指定年月内的所有月球最大北赤纬事件 / all maximum northern lunar declination events in the given Gregorian month.
func MoonMaximumNorthDeclinations(year int, month time.Month) []DeclinationEvent {
return moonMaximumDeclinationsInMonth(year, month, moonMaxDeclinationNorthCoefficients)
}
// MoonMaximumSouthDeclinations 指定年月内的所有月球最大南赤纬事件 / all maximum southern lunar declination events in the given Gregorian month.
func MoonMaximumSouthDeclinations(year int, month time.Month) []DeclinationEvent {
return moonMaximumDeclinationsInMonth(year, month, moonMaxDeclinationSouthCoefficients)
}
// LastMoonMaximumNorthDeclination 指定时刻之前最近一次月球最大北赤纬 / last maximum northern lunar declination at or before jd.
func LastMoonMaximumNorthDeclination(jd float64) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationNorthCoefficients, -1, true)
}
// NextMoonMaximumNorthDeclination 指定时刻之后最近一次月球最大北赤纬 / next maximum northern lunar declination after jd.
func NextMoonMaximumNorthDeclination(jd float64) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationNorthCoefficients, 1, false)
}
// ClosestMoonMaximumNorthDeclination 离指定时刻最近一次月球最大北赤纬 / closest maximum northern lunar declination to jd.
func ClosestMoonMaximumNorthDeclination(jd float64) DeclinationEvent {
return moonClosestMaximumDeclination(jd, moonMaxDeclinationNorthCoefficients)
}
// LastMoonMaximumSouthDeclination 指定时刻之前最近一次月球最大南赤纬 / last maximum southern lunar declination at or before jd.
func LastMoonMaximumSouthDeclination(jd float64) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationSouthCoefficients, -1, true)
}
// NextMoonMaximumSouthDeclination 指定时刻之后最近一次月球最大南赤纬 / next maximum southern lunar declination after jd.
func NextMoonMaximumSouthDeclination(jd float64) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationSouthCoefficients, 1, false)
}
// ClosestMoonMaximumSouthDeclination 离指定时刻最近一次月球最大南赤纬 / closest maximum southern lunar declination to jd.
func ClosestMoonMaximumSouthDeclination(jd float64) DeclinationEvent {
return moonClosestMaximumDeclination(jd, moonMaxDeclinationSouthCoefficients)
}
func moonMaximumDeclinationsInMonth(year int, month time.Month, coeffs moonMaxDeclinationCoefficients) []DeclinationEvent {
startUTC := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
endUTC := startUTC.AddDate(0, 1, 0)
startTT := TD2UT(Date2JDE(startUTC), true)
endTT := TD2UT(Date2JDE(endUTC), true)
kStart := int(math.Floor((startTT-coeffs.JDE0)/moonMaxDeclinationMeanMonthDays)) - 1
kEnd := int(math.Ceil((endTT-coeffs.JDE0)/moonMaxDeclinationMeanMonthDays)) + 1
cfg := apsisSearchConfig{
bracketHalfWidth: moonApsisBracketHalfWidth,
sampleStep: moonApsisSampleStep,
derivativeStep: moonApsisDerivativeStep,
toleranceDays: moonApsisToleranceDays,
maxIterations: moonApsisMaxIterations,
maximize: coeffs.sign > 0,
}
events := make([]DeclinationEvent, 0, 2)
for k := kStart; k <= kEnd; k++ {
event := moonMaximumDeclinationEvent(k, coeffs, cfg)
eventTimeUTC := JDE2DateByZone(event.JDE, time.UTC, false)
if eventTimeUTC.Before(startUTC) || !eventTimeUTC.Before(endUTC) {
continue
}
events = append(events, event)
}
sort.Slice(events, func(i, j int) bool {
return events[i].JDE < events[j].JDE
})
return events
}
func moonMaximumDeclinationEvent(k int, coeffs moonMaxDeclinationCoefficients, cfg apsisSearchConfig) DeclinationEvent {
seedTT := moonMaximumDeclinationSeedTT(k, coeffs)
eventTT, declination := refineDistanceExtremum(seedTT, cfg, func(sampleTT float64) float64 {
return HMoonTrueDecN(sampleTT, -1)
})
return DeclinationEvent{
JDE: TD2UT(eventTT, false),
Declination: declination,
}
}
func moonMaximumDeclinationSearch(jd float64, coeffs moonMaxDeclinationCoefficients, direction int, includeCurrent bool) DeclinationEvent {
cfg := apsisSearchConfig{
bracketHalfWidth: moonApsisBracketHalfWidth,
sampleStep: moonApsisSampleStep,
derivativeStep: moonApsisDerivativeStep,
toleranceDays: moonApsisToleranceDays,
maxIterations: moonApsisMaxIterations,
maximize: coeffs.sign > 0,
}
targetTT := TD2UT(jd, true)
centerK := int(math.Round((targetTT - coeffs.JDE0) / moonMaxDeclinationMeanMonthDays))
found := false
bestDistance := math.Inf(1)
var best DeclinationEvent
for offset := -moonMaxDeclinationSearchSpan; offset <= moonMaxDeclinationSearchSpan; offset++ {
event := moonMaximumDeclinationEvent(centerK+offset, coeffs, cfg)
delta := event.JDE - jd
if !moonMaximumDeclinationMatchesDirection(delta, direction, includeCurrent) {
continue
}
distance := math.Abs(delta)
if !found || distance < bestDistance || (distance == bestDistance && moonMaximumDeclinationEarlier(event, best)) {
best = event
bestDistance = distance
found = true
}
}
return best
}
func moonClosestMaximumDeclination(jd float64, coeffs moonMaxDeclinationCoefficients) DeclinationEvent {
last := moonMaximumDeclinationSearch(jd, coeffs, -1, true)
next := moonMaximumDeclinationSearch(jd, coeffs, 1, false)
lastDistance := math.Abs(jd - last.JDE)
nextDistance := math.Abs(next.JDE - jd)
if lastDistance <= nextDistance {
return last
}
return next
}
func moonMaximumDeclinationMatchesDirection(delta float64, direction int, includeCurrent bool) bool {
switch direction {
case -1:
if includeCurrent {
return delta <= 0
}
return delta < 0
case 1:
if includeCurrent {
return delta >= 0
}
return delta > 0
default:
return true
}
}
func moonMaximumDeclinationEarlier(a, b DeclinationEvent) bool {
return a.JDE < b.JDE
}
func moonMaximumDeclinationSeedTT(k int, coeffs moonMaxDeclinationCoefficients) float64 {
cycle := float64(k)
T := cycle / moonMaxDeclinationBaseCycle
D := Limit360(coeffs.D0 + 333.0705546*cycle - 0.0004214*T*T + 0.00000011*T*T*T)
M := Limit360(coeffs.M0 + 26.9281592*cycle - 0.0000355*T*T - 0.0000001*T*T*T)
MP := Limit360(coeffs.MP0 + 356.9562794*cycle + 0.0103066*T*T + 0.00001251*T*T*T)
F := Limit360(coeffs.F0 + 1.4467807*cycle - 0.0020690*T*T - 0.00000215*T*T*T)
E := 1 - 0.002516*T - 0.0000074*T*T
return coeffs.JDE0 +
moonMaxDeclinationMeanMonthDays*cycle +
0.000119804*T*T -
0.000000141*T*T*T +
coeffs.tc[0]*Cos(F) +
coeffs.tc[1]*Sin(MP) +
coeffs.tc[2]*Sin(2*F) +
coeffs.tc[3]*Sin(2*D-MP) +
coeffs.tc[4]*Cos(MP-F) +
coeffs.tc[5]*Cos(MP+F) +
coeffs.tc[6]*Sin(2*D) +
coeffs.tc[7]*Sin(M)*E +
coeffs.tc[8]*Cos(3*F) +
coeffs.tc[9]*Sin(MP+2*F) +
coeffs.tc[10]*Cos(2*D-F) +
coeffs.tc[11]*Cos(2*D-MP-F) +
coeffs.tc[12]*Cos(2*D-MP+F) +
coeffs.tc[13]*Cos(2*D+F) +
coeffs.tc[14]*Sin(2*MP) +
coeffs.tc[15]*Sin(MP-2*F) +
coeffs.tc[16]*Cos(2*MP-F) +
coeffs.tc[17]*Sin(MP+3*F) +
coeffs.tc[18]*Sin(2*D-M-MP)*E +
coeffs.tc[19]*Cos(MP-2*F) +
coeffs.tc[20]*Sin(2*(D-MP)) +
coeffs.tc[21]*Sin(F) +
coeffs.tc[22]*Sin(2*D+MP) +
coeffs.tc[23]*Cos(MP+2*F) +
coeffs.tc[24]*Sin(2*D-M)*E +
coeffs.tc[25]*Sin(MP+F) +
coeffs.tc[26]*Sin(M-MP)*E +
coeffs.tc[27]*Sin(MP-3*F) +
coeffs.tc[28]*Sin(2*MP+F) +
coeffs.tc[29]*Cos(2*(D-MP)-F) +
coeffs.tc[30]*Sin(3*F) +
coeffs.tc[31]*Cos(MP+3*F) +
coeffs.tc[32]*Cos(2*MP) +
coeffs.tc[33]*Cos(2*D-MP) +
coeffs.tc[34]*Cos(2*D+MP+F) +
coeffs.tc[35]*Cos(MP) +
coeffs.tc[36]*Sin(3*MP+F) +
coeffs.tc[37]*Sin(2*D-MP+F) +
coeffs.tc[38]*Cos(2*(D-MP)) +
coeffs.tc[39]*Cos(D+F) +
coeffs.tc[40]*Sin(M+MP)*E +
coeffs.tc[41]*Sin(2*(D-F)) +
coeffs.tc[42]*Cos(2*MP+F) +
coeffs.tc[43]*Cos(3*MP+F)
}