Files
astro/basic/moon_max_declination.go
T
b612 044f2f4381 feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算
- 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询
- 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出
- 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解
- 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤
- 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持
- 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理
- 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
2026-10-15 23:54:15 +08:00

346 lines
13 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 {
// JD 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day.
JD 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,
},
}
// MoonMaximumNorthDeclinationsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonMaximumNorthDeclinationsWithInstance(year int, month time.Month, inst *Instance) []DeclinationEvent {
return moonMaximumDeclinationsInMonth(year, month, moonMaxDeclinationNorthCoefficients, inst)
}
// MoonMaximumSouthDeclinationsWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func MoonMaximumSouthDeclinationsWithInstance(year int, month time.Month, inst *Instance) []DeclinationEvent {
return moonMaximumDeclinationsInMonth(year, month, moonMaxDeclinationSouthCoefficients, inst)
}
// LastMoonMaximumNorthDeclinationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMoonMaximumNorthDeclinationWithInstance(jd float64, inst *Instance) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationNorthCoefficients, -1, true, inst)
}
// NextMoonMaximumNorthDeclinationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMoonMaximumNorthDeclinationWithInstance(jd float64, inst *Instance) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationNorthCoefficients, 1, false, inst)
}
// ClosestMoonMaximumNorthDeclinationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func ClosestMoonMaximumNorthDeclinationWithInstance(jd float64, inst *Instance) DeclinationEvent {
return moonClosestMaximumDeclination(jd, moonMaxDeclinationNorthCoefficients, inst)
}
// LastMoonMaximumSouthDeclinationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastMoonMaximumSouthDeclinationWithInstance(jd float64, inst *Instance) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationSouthCoefficients, -1, true, inst)
}
// NextMoonMaximumSouthDeclinationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextMoonMaximumSouthDeclinationWithInstance(jd float64, inst *Instance) DeclinationEvent {
return moonMaximumDeclinationSearch(jd, moonMaxDeclinationSouthCoefficients, 1, false, inst)
}
// ClosestMoonMaximumSouthDeclinationWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func ClosestMoonMaximumSouthDeclinationWithInstance(jd float64, inst *Instance) DeclinationEvent {
return moonClosestMaximumDeclination(jd, moonMaxDeclinationSouthCoefficients, inst)
}
func moonMaximumDeclinationsInMonth(year int, month time.Month, coeffs moonMaxDeclinationCoefficients, instances ...*Instance) []DeclinationEvent {
inst := instanceValue(instances)
startUTC := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC)
endUTC := startUTC.AddDate(0, 1, 0)
startTT := UTC2TT(Date2JD(startUTC))
endTT := UTC2TT(Date2JD(endUTC))
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, inst)
eventTimeUTC := JD2DateByZone(event.JD, 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].JD < events[j].JD
})
return events
}
func moonMaximumDeclinationEvent(k int, coeffs moonMaxDeclinationCoefficients, cfg apsisSearchConfig, instances ...*Instance) DeclinationEvent {
inst := instanceValue(instances)
seedTT := moonMaximumDeclinationSeedTT(k, coeffs)
eventTT, declination := refineDistanceExtremum(seedTT, cfg, func(sampleTT float64) float64 {
return HMoonTrueDecNWithInstance(sampleTT, -1, inst)
})
return DeclinationEvent{
JD: TT2UTC(eventTT),
Declination: declination,
}
}
func moonMaximumDeclinationSearch(jd float64, coeffs moonMaxDeclinationCoefficients, direction int, includeCurrent bool, instances ...*Instance) DeclinationEvent {
inst := instanceValue(instances)
if !finite(jd) {
// 非有限查询算不出周期序号(centerK 会把 NaN/±Inf 压成 MinInt64,种子外推到 1e40 后步长小于一个 ULP)。
return DeclinationEvent{}
}
cfg := moonMaximumDeclinationSearchConfig(coeffs)
centerK := moonMaximumDeclinationOffset(jd, coeffs)
step := 1
if direction < 0 {
step = -1
}
event := moonMaximumDeclinationEvent(centerK, coeffs, cfg, inst)
// 事件时刻随周期序号单调递增:该方向上最近的候选就是 centerK 沿该方向第一个满足方向不变量
// 的周期;命中后只需再回退检查更早的周期是否同样满足,可达范围仍旧是 ±3 个周期。
if !moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
event = moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg, inst)
if moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
return event
}
}
return DeclinationEvent{}
}
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
previous := moonMaximumDeclinationEvent(centerK-step*offset, coeffs, cfg, inst)
if !moonMaximumDeclinationMatchesDirection(previous.JD-jd, direction, includeCurrent) {
break
}
event = previous
}
return event
}
func moonClosestMaximumDeclination(jd float64, coeffs moonMaxDeclinationCoefficients, instances ...*Instance) DeclinationEvent {
inst := instanceValue(instances)
if !finite(jd) {
return DeclinationEvent{}
}
cfg := moonMaximumDeclinationSearchConfig(coeffs)
centerK := moonMaximumDeclinationOffset(jd, coeffs)
center := moonMaximumDeclinationEvent(centerK, coeffs, cfg, inst)
var last, next DeclinationEvent
if center.JD <= jd {
last = center
next = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, 1, false, inst)
} else {
next = center
last = moonMaximumDeclinationAround(jd, coeffs, cfg, centerK, -1, true, inst)
}
lastDistance := math.Abs(jd - last.JD)
nextDistance := math.Abs(next.JD - jd)
if lastDistance <= nextDistance {
return last
}
return next
}
// moonMaximumDeclinationAround 从 centerK 沿 step 方向找第一个满足方向不变量的周期;±3 个周期内没有就返回零事件。
func moonMaximumDeclinationAround(jd float64, coeffs moonMaxDeclinationCoefficients, cfg apsisSearchConfig,
centerK, step int, includeCurrent bool, instances ...*Instance) DeclinationEvent {
inst := instanceValue(instances)
direction := 1
if step < 0 {
direction = -1
}
for offset := 1; offset <= moonMaxDeclinationSearchSpan; offset++ {
event := moonMaximumDeclinationEvent(centerK+step*offset, coeffs, cfg, inst)
if moonMaximumDeclinationMatchesDirection(event.JD-jd, direction, includeCurrent) {
return event
}
}
return DeclinationEvent{}
}
func moonMaximumDeclinationSearchConfig(coeffs moonMaxDeclinationCoefficients) apsisSearchConfig {
return apsisSearchConfig{
bracketHalfWidth: moonApsisBracketHalfWidth,
sampleStep: moonApsisSampleStep,
derivativeStep: moonApsisDerivativeStep,
toleranceDays: moonApsisToleranceDays,
maxIterations: moonApsisMaxIterations,
maximize: coeffs.sign > 0,
}
}
// moonMaximumDeclinationOffset 查询时刻落在哪个平均周期(种子多项式的中心序号)。
func moonMaximumDeclinationOffset(jd float64, coeffs moonMaxDeclinationCoefficients) int {
return int(math.Round((UTC2TT(jd) - coeffs.JDE0) / moonMaxDeclinationMeanMonthDays))
}
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.JD < b.JD
}
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)
}