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,110 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type planetRiseSetBaselineSample struct {
|
||||
Body string `json:"body"`
|
||||
Site string `json:"site"`
|
||||
InputUTC string `json:"input_utc"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
Latitude float64 `json:"latitude"`
|
||||
RiseUTC string `json:"rise_utc"`
|
||||
TransitUTC string `json:"transit_utc"`
|
||||
SetUTC string `json:"set_utc"`
|
||||
}
|
||||
|
||||
func TestPlanetRiseSetMatchesHorizonsBaseline(t *testing.T) {
|
||||
data, err := os.ReadFile("testdata/planet_rise_set_baseline.json")
|
||||
if err != nil {
|
||||
t.Fatalf("read baseline: %v", err)
|
||||
}
|
||||
|
||||
var samples []planetRiseSetBaselineSample
|
||||
if err := json.Unmarshal(data, &samples); err != nil {
|
||||
t.Fatalf("decode baseline: %v", err)
|
||||
}
|
||||
|
||||
type observationCase struct {
|
||||
rise func(float64, float64, float64, float64, float64, float64) (float64, error)
|
||||
transit func(float64, float64, float64) float64
|
||||
set func(float64, float64, float64, float64, float64, float64) (float64, error)
|
||||
}
|
||||
|
||||
cases := map[string]observationCase{
|
||||
"mercury": {rise: MercuryRiseTime, transit: MercuryCulminationTime, set: MercurySetTime},
|
||||
"venus": {rise: VenusRiseTime, transit: VenusCulminationTime, set: VenusSetTime},
|
||||
"mars": {rise: MarsRiseTime, transit: MarsCulminationTime, set: MarsSetTime},
|
||||
"jupiter": {rise: JupiterRiseTime, transit: JupiterCulminationTime, set: JupiterSetTime},
|
||||
"saturn": {rise: SaturnRiseTime, transit: SaturnCulminationTime, set: SaturnSetTime},
|
||||
"uranus": {rise: UranusRiseTime, transit: UranusCulminationTime, set: UranusSetTime},
|
||||
"neptune": {rise: NeptuneRiseTime, transit: NeptuneCulminationTime, set: NeptuneSetTime},
|
||||
}
|
||||
|
||||
const tolerance = 2 * time.Minute
|
||||
var maxRiseDiff time.Duration
|
||||
var maxTransitDiff time.Duration
|
||||
var maxSetDiff time.Duration
|
||||
|
||||
for _, sample := range samples {
|
||||
tc, ok := cases[sample.Body]
|
||||
if !ok {
|
||||
t.Fatalf("unknown body %q", sample.Body)
|
||||
}
|
||||
|
||||
inputTime, err := time.Parse(time.RFC3339, sample.InputUTC)
|
||||
if err != nil {
|
||||
t.Fatalf("parse input time %q: %v", sample.InputUTC, err)
|
||||
}
|
||||
jd := Date2JDE(inputTime.UTC())
|
||||
|
||||
riseJD, err := tc.rise(jd, sample.Longitude, sample.Latitude, 0, 1, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s rise error: %v", sample.Body, sample.Site, err)
|
||||
}
|
||||
riseDiff := assertEventTimeClose(t, sample.Body+"."+sample.Site+".rise", riseJD, sample.RiseUTC, tolerance)
|
||||
if riseDiff > maxRiseDiff {
|
||||
maxRiseDiff = riseDiff
|
||||
}
|
||||
|
||||
transitJD := tc.transit(jd, sample.Longitude, 0)
|
||||
transitDiff := assertEventTimeClose(t, sample.Body+"."+sample.Site+".transit", transitJD, sample.TransitUTC, tolerance)
|
||||
if transitDiff > maxTransitDiff {
|
||||
maxTransitDiff = transitDiff
|
||||
}
|
||||
|
||||
setJD, err := tc.set(jd, sample.Longitude, sample.Latitude, 0, 1, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s set error: %v", sample.Body, sample.Site, err)
|
||||
}
|
||||
setDiff := assertEventTimeClose(t, sample.Body+"."+sample.Site+".set", setJD, sample.SetUTC, tolerance)
|
||||
if setDiff > maxSetDiff {
|
||||
maxSetDiff = setDiff
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("planet rise/set max diff: rise=%v transit=%v set=%v", maxRiseDiff, maxTransitDiff, maxSetDiff)
|
||||
}
|
||||
|
||||
func assertEventTimeClose(t *testing.T, name string, gotJD float64, wantUTC string, tolerance time.Duration) time.Duration {
|
||||
t.Helper()
|
||||
|
||||
wantTime, err := time.Parse(time.RFC3339, wantUTC)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %s baseline time %q: %v", name, wantUTC, err)
|
||||
}
|
||||
|
||||
gotTime := JDE2DateByZone(gotJD, time.UTC, false)
|
||||
diff := gotTime.Sub(wantTime)
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
if diff > tolerance {
|
||||
t.Fatalf("%s mismatch: got %s want %s tolerance %v", name, gotTime.Format(time.RFC3339), wantUTC, tolerance)
|
||||
}
|
||||
return diff
|
||||
}
|
||||
Reference in New Issue
Block a user