feat: 扩展天文计算能力
- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息 - 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算 - 新增木星伽利略卫星位置、现象与接触事件计算 - 新增恒星星表、星座判定、自行修正与观测辅助能力 - 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包 - 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算 - 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试 - 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试 - 更新中文和英文 README,补充示例、精度说明、SVG 配图
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type earthApsisSample struct {
|
||||
Kind string `json:"kind"`
|
||||
Year int `json:"year"`
|
||||
TimeUTC string `json:"time_utc"`
|
||||
DistanceAU float64 `json:"distance_au"`
|
||||
}
|
||||
|
||||
type moonApsisSample struct {
|
||||
Kind string `json:"kind"`
|
||||
Year int `json:"year"`
|
||||
Month int `json:"month"`
|
||||
TimeUTC string `json:"time_utc"`
|
||||
DistanceKM float64 `json:"distance_km"`
|
||||
}
|
||||
|
||||
type moonApsisMonthState struct {
|
||||
perigees []ApsisEvent
|
||||
apogees []ApsisEvent
|
||||
perigeeI int
|
||||
apogeeI int
|
||||
}
|
||||
|
||||
func TestEarthApsisMatchesHorizonsBaseline(t *testing.T) {
|
||||
data, err := os.ReadFile("testdata/earth_apsis_baseline.json")
|
||||
if err != nil {
|
||||
t.Fatalf("read baseline: %v", err)
|
||||
}
|
||||
|
||||
var samples []earthApsisSample
|
||||
if err := json.Unmarshal(data, &samples); err != nil {
|
||||
t.Fatalf("decode baseline: %v", err)
|
||||
}
|
||||
|
||||
const timeTolerance = 2 * time.Minute
|
||||
const distanceToleranceAU = 5e-8
|
||||
|
||||
var maxTimeDiff time.Duration
|
||||
var maxDistanceDiff float64
|
||||
for _, sample := range samples {
|
||||
wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC)
|
||||
if err != nil {
|
||||
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
|
||||
}
|
||||
|
||||
var got ApsisEvent
|
||||
switch sample.Kind {
|
||||
case "perihelion":
|
||||
got = EarthPerihelion(sample.Year)
|
||||
case "aphelion":
|
||||
got = EarthAphelion(sample.Year)
|
||||
default:
|
||||
t.Fatalf("unknown earth apsis kind %q", sample.Kind)
|
||||
}
|
||||
|
||||
gotTime := JDE2DateByZone(got.JDE, time.UTC, false)
|
||||
timeDiff := gotTime.Sub(wantTime)
|
||||
if timeDiff < 0 {
|
||||
timeDiff = -timeDiff
|
||||
}
|
||||
if timeDiff > maxTimeDiff {
|
||||
maxTimeDiff = timeDiff
|
||||
}
|
||||
if timeDiff > timeTolerance {
|
||||
t.Fatalf("%s %d time mismatch: got %s want %s tolerance %v", sample.Kind, sample.Year, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, timeTolerance)
|
||||
}
|
||||
|
||||
distanceDiff := math.Abs(got.Distance - sample.DistanceAU)
|
||||
if distanceDiff > maxDistanceDiff {
|
||||
maxDistanceDiff = distanceDiff
|
||||
}
|
||||
if distanceDiff > distanceToleranceAU {
|
||||
t.Fatalf("%s %d distance mismatch: got %.12f want %.12f tolerance %.12f", sample.Kind, sample.Year, got.Distance, sample.DistanceAU, distanceToleranceAU)
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("earth apsis max diff: time=%v distance=%.12f AU", maxTimeDiff, maxDistanceDiff)
|
||||
}
|
||||
|
||||
func TestMoonApsisMatchesHorizonsBaseline(t *testing.T) {
|
||||
// Baseline is generated from JPL Horizons by scripts/generate_moon_apsis_baseline.sh.
|
||||
data, err := os.ReadFile("testdata/moon_apsis_baseline.json")
|
||||
if err != nil {
|
||||
t.Fatalf("read baseline: %v", err)
|
||||
}
|
||||
|
||||
var samples []moonApsisSample
|
||||
if err := json.Unmarshal(data, &samples); err != nil {
|
||||
t.Fatalf("decode baseline: %v", err)
|
||||
}
|
||||
|
||||
const timeTolerance = 20 * time.Minute
|
||||
const distanceToleranceKM = 50.0
|
||||
|
||||
states := make(map[int]*moonApsisMonthState)
|
||||
var maxTimeDiff time.Duration
|
||||
var maxDistanceDiff float64
|
||||
|
||||
for _, sample := range samples {
|
||||
wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC)
|
||||
if err != nil {
|
||||
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
|
||||
}
|
||||
|
||||
key := sample.Year*100 + sample.Month
|
||||
state := states[key]
|
||||
if state == nil {
|
||||
state = &moonApsisMonthState{
|
||||
perigees: MoonPerigees(sample.Year, time.Month(sample.Month)),
|
||||
apogees: MoonApogees(sample.Year, time.Month(sample.Month)),
|
||||
}
|
||||
states[key] = state
|
||||
}
|
||||
|
||||
var got ApsisEvent
|
||||
switch sample.Kind {
|
||||
case "perigee":
|
||||
if state.perigeeI >= len(state.perigees) {
|
||||
t.Fatalf("%04d-%02d missing perigee #%d", sample.Year, sample.Month, state.perigeeI+1)
|
||||
}
|
||||
got = state.perigees[state.perigeeI]
|
||||
state.perigeeI++
|
||||
case "apogee":
|
||||
if state.apogeeI >= len(state.apogees) {
|
||||
t.Fatalf("%04d-%02d missing apogee #%d", sample.Year, sample.Month, state.apogeeI+1)
|
||||
}
|
||||
got = state.apogees[state.apogeeI]
|
||||
state.apogeeI++
|
||||
default:
|
||||
t.Fatalf("unknown moon apsis kind %q", sample.Kind)
|
||||
}
|
||||
|
||||
gotTime := JDE2DateByZone(got.JDE, time.UTC, false)
|
||||
timeDiff := gotTime.Sub(wantTime)
|
||||
if timeDiff < 0 {
|
||||
timeDiff = -timeDiff
|
||||
}
|
||||
if timeDiff > maxTimeDiff {
|
||||
maxTimeDiff = timeDiff
|
||||
}
|
||||
if timeDiff > timeTolerance {
|
||||
t.Fatalf("%s %04d-%02d time mismatch: got %s want %s tolerance %v", sample.Kind, sample.Year, sample.Month, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, timeTolerance)
|
||||
}
|
||||
|
||||
distanceDiff := math.Abs(got.Distance - sample.DistanceKM)
|
||||
if distanceDiff > maxDistanceDiff {
|
||||
maxDistanceDiff = distanceDiff
|
||||
}
|
||||
if distanceDiff > distanceToleranceKM {
|
||||
t.Fatalf("%s %04d-%02d distance mismatch: got %.6f want %.6f tolerance %.6f", sample.Kind, sample.Year, sample.Month, got.Distance, sample.DistanceKM, distanceToleranceKM)
|
||||
}
|
||||
}
|
||||
|
||||
for key, state := range states {
|
||||
year := key / 100
|
||||
month := key % 100
|
||||
if state.perigeeI != len(state.perigees) {
|
||||
t.Fatalf("%04d-%02d unconsumed perigees: got %d of %d", year, month, state.perigeeI, len(state.perigees))
|
||||
}
|
||||
if state.apogeeI != len(state.apogees) {
|
||||
t.Fatalf("%04d-%02d unconsumed apogees: got %d of %d", year, month, state.apogeeI, len(state.apogees))
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("moon apsis max diff: time=%v distance=%.6f km", maxTimeDiff, maxDistanceDiff)
|
||||
}
|
||||
Reference in New Issue
Block a user