astro/basic/apsis.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

254 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package basic
import (
"math"
"sort"
"time"
)
const (
earthApsisBaseTTJDE = 2451547.507
earthApsisMeanYearDays = 365.2596358
earthApsisQuadraticTerm = 0.0000000156
earthApsisBaseYear = 2000.01
earthApsisSeedScale = 0.99997
earthApsisBracketHalfWidth = 5.0
earthApsisSampleStep = 0.25
earthApsisDerivativeStep = 1e-3
earthApsisToleranceDays = 1e-8
earthApsisMaxIterations = 24
moonApsisBaseTTJDE = 2451534.6698
moonApsisMeanMonthDays = 27.55454989
moonApsisBaseCycle = 1325.55
moonApsisQuadraticTerm = -0.0006691
moonApsisCubicTerm = -0.000001098
moonApsisQuarticTerm = 0.0000000052
moonApsisBracketHalfWidth = 2.0
moonApsisSampleStep = 0.125
moonApsisDerivativeStep = 1e-4
moonApsisToleranceDays = 1e-8
moonApsisMaxIterations = 24
)
// ApsisEvent 轨道极值事件 / orbital distance extremum event.
type ApsisEvent struct {
// JDE 是事件发生时刻对应的世界时儒略日 / event time as UTC-based Julian day.
JDE float64
// Distance 是极值距离;地球相关事件单位 AU月球相关事件单位 km / extremum distance.
Distance float64
}
type apsisSearchConfig struct {
bracketHalfWidth float64
sampleStep float64
derivativeStep float64
toleranceDays float64
maxIterations int
maximize bool
}
// EarthPerihelion 地球指定年份的近日点 / Earth perihelion in the given year.
func EarthPerihelion(year int) ApsisEvent {
return earthApsis(year, false)
}
// EarthAphelion 地球指定年份的远日点 / Earth aphelion in the given year.
func EarthAphelion(year int) ApsisEvent {
return earthApsis(year, true)
}
// MoonPerigees 指定年月内的所有月球近地点 / all lunar perigees in the given Gregorian month.
func MoonPerigees(year int, month time.Month) []ApsisEvent {
return moonApsisInMonth(year, month, false)
}
// MoonApogees 指定年月内的所有月球远地点 / all lunar apogees in the given Gregorian month.
func MoonApogees(year int, month time.Month) []ApsisEvent {
return moonApsisInMonth(year, month, true)
}
func earthApsis(year int, aphelion bool) ApsisEvent {
seedTT := earthApsisSeedTT(year, aphelion)
cfg := apsisSearchConfig{
bracketHalfWidth: earthApsisBracketHalfWidth,
sampleStep: earthApsisSampleStep,
derivativeStep: earthApsisDerivativeStep,
toleranceDays: earthApsisToleranceDays,
maxIterations: earthApsisMaxIterations,
maximize: aphelion,
}
eventTT, distanceAU := refineDistanceExtremum(seedTT, cfg, EarthAway)
return ApsisEvent{
JDE: TD2UT(eventTT, false),
Distance: distanceAU,
}
}
func moonApsisInMonth(year int, month time.Month, apogee bool) []ApsisEvent {
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-moonApsisBaseTTJDE)/moonApsisMeanMonthDays)) - 1
kEnd := int(math.Ceil((endTT-moonApsisBaseTTJDE)/moonApsisMeanMonthDays)) + 1
phase := 0.0
if apogee {
phase = 0.5
}
cfg := apsisSearchConfig{
bracketHalfWidth: moonApsisBracketHalfWidth,
sampleStep: moonApsisSampleStep,
derivativeStep: moonApsisDerivativeStep,
toleranceDays: moonApsisToleranceDays,
maxIterations: moonApsisMaxIterations,
maximize: apogee,
}
events := make([]ApsisEvent, 0, 2)
for k := kStart; k <= kEnd; k++ {
seedTT := moonApsisSeedTT(float64(k) + phase)
eventTT, distanceKM := refineDistanceExtremum(seedTT, cfg, HMoonAway)
eventUT := TD2UT(eventTT, false)
eventTimeUTC := JDE2DateByZone(eventUT, time.UTC, false)
if eventTimeUTC.Before(startUTC) || !eventTimeUTC.Before(endUTC) {
continue
}
events = append(events, ApsisEvent{
JDE: eventUT,
Distance: distanceKM,
})
}
sort.Slice(events, func(i, j int) bool {
return events[i].JDE < events[j].JDE
})
return events
}
func earthApsisSeedTT(year int, aphelion bool) float64 {
k := math.Round(earthApsisSeedScale * (float64(year) - earthApsisBaseYear))
if aphelion {
k += 0.5
}
return earthApsisBaseTTJDE + earthApsisMeanYearDays*k + earthApsisQuadraticTerm*k*k
}
func moonApsisSeedTT(k float64) float64 {
t := k / moonApsisBaseCycle
return moonApsisBaseTTJDE +
moonApsisMeanMonthDays*k +
moonApsisQuadraticTerm*t*t +
moonApsisCubicTerm*t*t*t +
moonApsisQuarticTerm*t*t*t*t
}
func refineDistanceExtremum(seed float64, cfg apsisSearchConfig, distanceFn func(float64) float64) (float64, float64) {
best := seed
bestDistance := distanceFn(seed)
for sample := seed - cfg.bracketHalfWidth; sample <= seed+cfg.bracketHalfWidth+1e-12; sample += cfg.sampleStep {
dist := distanceFn(sample)
if distanceBetter(dist, bestDistance, cfg.maximize) {
best = sample
bestDistance = dist
}
}
left, right, ok := apsisDerivativeBracket(best, seed, cfg, distanceFn)
if !ok {
return best, bestDistance
}
leftDeriv := apsisDistanceDerivative(distanceFn, left, cfg.derivativeStep)
rightDeriv := apsisDistanceDerivative(distanceFn, right, cfg.derivativeStep)
current := best
for i := 0; i < cfg.maxIterations; i++ {
first, second := apsisDistanceDerivatives(distanceFn, current, cfg.derivativeStep)
next := current
if math.Abs(second) > 0 {
next = current - first/second
}
if !(next > left && next < right) || math.IsNaN(next) || math.IsInf(next, 0) {
next = (left + right) / 2
}
nextDeriv := apsisDistanceDerivative(distanceFn, next, cfg.derivativeStep)
if leftDeriv == 0 {
right = next
rightDeriv = nextDeriv
} else if leftDeriv*nextDeriv <= 0 {
right = next
rightDeriv = nextDeriv
} else {
left = next
leftDeriv = nextDeriv
}
if math.Abs(next-current) <= cfg.toleranceDays || math.Abs(right-left) <= cfg.toleranceDays {
current = next
break
}
current = next
_ = rightDeriv
}
return current, distanceFn(current)
}
func apsisDerivativeBracket(best, seed float64, cfg apsisSearchConfig, distanceFn func(float64) float64) (float64, float64, bool) {
leftBound := seed - cfg.bracketHalfWidth
rightBound := seed + cfg.bracketHalfWidth
left := best - cfg.sampleStep
right := best + cfg.sampleStep
if left < leftBound {
left = leftBound
}
if right > rightBound {
right = rightBound
}
leftDeriv := apsisDistanceDerivative(distanceFn, left, cfg.derivativeStep)
rightDeriv := apsisDistanceDerivative(distanceFn, right, cfg.derivativeStep)
for i := 0; i < cfg.maxIterations; i++ {
if leftDeriv == 0 || rightDeriv == 0 || leftDeriv*rightDeriv < 0 {
return left, right, true
}
if left > leftBound {
left -= cfg.sampleStep
if left < leftBound {
left = leftBound
}
leftDeriv = apsisDistanceDerivative(distanceFn, left, cfg.derivativeStep)
}
if right < rightBound {
right += cfg.sampleStep
if right > rightBound {
right = rightBound
}
rightDeriv = apsisDistanceDerivative(distanceFn, right, cfg.derivativeStep)
}
}
return 0, 0, false
}
func apsisDistanceDerivative(distanceFn func(float64) float64, jd, h float64) float64 {
return (distanceFn(jd+h) - distanceFn(jd-h)) / (2 * h)
}
func apsisDistanceDerivatives(distanceFn func(float64) float64, jd, h float64) (float64, float64) {
prev := distanceFn(jd - h)
curr := distanceFn(jd)
next := distanceFn(jd + h)
first := (next - prev) / (2 * h)
second := (next - 2*curr + prev) / (h * h)
return first, second
}
func distanceBetter(candidate, current float64, maximize bool) bool {
if maximize {
return candidate > current
}
return candidate < current
}