feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package orbit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// movingPerturbationEngine 提供随时间变化的太阳与行星位置 / supplies time-varying Sun and planet positions.
|
||||
type movingPerturbationEngine struct{}
|
||||
|
||||
func (movingPerturbationEngine) State(target, center Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
||||
if target == center {
|
||||
return pos, vel, nil
|
||||
}
|
||||
if center != 0 {
|
||||
return pos, vel, ErrPerturbationEngine
|
||||
}
|
||||
angle := func(period float64) float64 { return 2 * math.Pi * (jdTDB - 2451545) / period }
|
||||
switch target {
|
||||
case 10:
|
||||
phase := angle(4332.6)
|
||||
pos = [3]float64{-742000 * math.Cos(phase), -742000 * math.Sin(phase), 0}
|
||||
case 3:
|
||||
phase := angle(365.25)
|
||||
pos = [3]float64{149597870 * math.Cos(phase), 149597870 * math.Sin(phase), 0}
|
||||
case 5:
|
||||
phase := angle(4332.6)
|
||||
pos = [3]float64{778000000 * math.Cos(phase), 778000000 * math.Sin(phase), 0}
|
||||
default:
|
||||
return pos, vel, ErrPerturbationEngine
|
||||
}
|
||||
return pos, vel, nil
|
||||
}
|
||||
|
||||
func (movingPerturbationEngine) Coverage(target Body) (start, end float64, ok bool) {
|
||||
return 0, 3000000, true
|
||||
}
|
||||
|
||||
func TestEphemerisEngineMatchesFreshPropagationInAnyOrder(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
options := PropagationOptions{Perturbers: []Body{5, 3}, Engine: movingPerturbationEngine{}, Step: 0.7, Tolerance: 1e-9}
|
||||
queries := []float64{2451545 + 400.3, 2451545 + 12.25, 2451545 - 90.1, 2451545 + 400.3, 2451545 + 33.6, 2451545 - 3.5, 2451545, 2451545 + 1000}
|
||||
engine := NewEphemerisEngine(Body(12345), elements, options)
|
||||
for _, jd := range queries {
|
||||
gotPos, gotVel, err := engine.State(Body(12345), 10, jd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPos, wantVel, err := StateWithOptions(elements, jd, options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotPos != wantPos || gotVel != wantVel {
|
||||
t.Fatalf("jd %.2f: engine %v %v, fresh %v %v", jd, gotPos, gotVel, wantPos, wantVel)
|
||||
}
|
||||
}
|
||||
// 逆序查询同样逐位一致 / reverse order also matches bit for bit.
|
||||
reverse := NewEphemerisEngine(Body(12345), elements, options)
|
||||
for index := len(queries) - 1; index >= 0; index-- {
|
||||
gotPos, _, err := reverse.State(Body(12345), 10, queries[index])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPos, _, _ := StateWithOptions(elements, queries[index], options)
|
||||
if gotPos != wantPos {
|
||||
t.Fatalf("reverse jd %.2f: engine %v, fresh %v", queries[index], gotPos, wantPos)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEphemerisEngineKeepsConstructionElements(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
options := PropagationOptions{Perturbers: []Body{5}, Engine: movingPerturbationEngine{}, Step: 1}
|
||||
engine := NewEphemerisEngine(Body(12345), elements, options)
|
||||
if _, _, err := engine.State(Body(12345), 10, 2451545+50); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
options.Perturbers[0] = 3
|
||||
got := engine.Elements()
|
||||
got.A = 9
|
||||
gotPos, _, err := engine.State(Body(12345), 10, 2451545+80)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPos, _, _ := StateWithOptions(elements, 2451545+80, PropagationOptions{Perturbers: []Body{5}, Engine: movingPerturbationEngine{}, Step: 1})
|
||||
if gotPos != wantPos || engine.Elements() != elements || engine.Target() != Body(12345) {
|
||||
t.Fatalf("engine state changed after caller mutation: %v versus %v", gotPos, wantPos)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateWithOptionsPerihelionFormStartsAtOsculatingEpoch(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545 + 120, Q: 0.6, E: 0.9, I: 30, Omega: 40, W: 50, TpJD: 2451545}
|
||||
options := PropagationOptions{Perturbers: []Body{5}, Engine: movingPerturbationEngine{}, Step: 1, Tolerance: 1e-10}
|
||||
gotPos, gotVel, err := StateWithOptions(elements, elements.EpochJD, options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPos, wantVel, err := State(elements, elements.EpochJD)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotPos != wantPos || gotVel != wantVel {
|
||||
t.Fatalf("osculating epoch state %v %v, want two-body %v %v", gotPos, gotVel, wantPos, wantVel)
|
||||
}
|
||||
// 未给 EpochJD 时仍以 TpJD 为起点 / without EpochJD the integration still starts at TpJD.
|
||||
noEpoch := elements
|
||||
noEpoch.EpochJD = 0
|
||||
gotPos, _, err = StateWithOptions(noEpoch, noEpoch.TpJD, options)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPos, _, _ = State(noEpoch, noEpoch.TpJD)
|
||||
if gotPos != wantPos {
|
||||
t.Fatalf("TpJD start state %v, want %v", gotPos, wantPos)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateWithOptionsRejectsUnsupportedPerturbers(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
for _, perturbers := range [][]Body{{5, 9}, {2000001}, {3, 399}, {301, 3}} {
|
||||
_, _, err := StateWithOptions(elements, elements.EpochJD+5, PropagationOptions{Perturbers: perturbers, Engine: movingPerturbationEngine{}})
|
||||
if !errors.Is(err, ErrUnsupportedPerturber) {
|
||||
t.Fatalf("perturbers %v error = %v, want ErrUnsupportedPerturber", perturbers, err)
|
||||
}
|
||||
engine := NewEphemerisEngine(Body(12345), elements, PropagationOptions{Perturbers: perturbers, Engine: movingPerturbationEngine{}})
|
||||
if _, _, err := engine.State(Body(12345), 10, elements.EpochJD+5); !errors.Is(err, ErrUnsupportedPerturber) {
|
||||
t.Fatalf("engine perturbers %v error = %v, want ErrUnsupportedPerturber", perturbers, err)
|
||||
}
|
||||
}
|
||||
plain, _, _ := State(elements, elements.EpochJD+30)
|
||||
withEMB, _, err := StateWithOptions(elements, elements.EpochJD+30, PropagationOptions{Perturbers: []Body{3}, Engine: movingPerturbationEngine{}, Tolerance: 1e-10})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if withEMB == plain {
|
||||
t.Fatal("EMB perturbation did not change the state")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateWithOptionsRejectsMissingClassicalEpoch(t *testing.T) {
|
||||
elements := Elements{A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
_, _, err := StateWithOptions(elements, 2451545, PropagationOptions{Perturbers: []Body{3}, Engine: movingPerturbationEngine{}})
|
||||
if !errors.Is(err, ErrInvalidPropagationElements) {
|
||||
t.Fatalf("error = %v, want ErrInvalidPropagationElements", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user