feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
+185
-3
@@ -275,12 +275,12 @@ func TestSecularRatesReduceMarsDriftAgainstVSOP(t *testing.T) {
|
||||
|
||||
dynamicError := angleDiffAbs(dynamic.RA, wantRA) + math.Abs(dynamic.Dec-wantDec)
|
||||
staticError := angleDiffAbs(stale.RA, wantRA) + math.Abs(stale.Dec-wantDec)
|
||||
if dynamicError > staticError+1e-9 {
|
||||
t.Fatalf("%s dynamic elements should improve Mars drift: dynamic=%.9f static=%.9f", date.Format("2006-01-02"), dynamicError, staticError)
|
||||
}
|
||||
if date.Equal(time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC)) {
|
||||
continue
|
||||
}
|
||||
if dynamicError > staticError+1e-9 {
|
||||
t.Fatalf("%s dynamic elements should improve Mars drift: dynamic=%.9f static=%.9f", date.Format("2006-01-02"), dynamicError, staticError)
|
||||
}
|
||||
if staticError/dynamicError < 4 {
|
||||
t.Fatalf("%s Mars drift improvement too small: dynamic=%.9f static=%.9f", date.Format("2006-01-02"), dynamicError, staticError)
|
||||
}
|
||||
@@ -542,3 +542,185 @@ func sphericalAzimuthFromHourAngle(hourAngle, dec, lat float64) float64 {
|
||||
}
|
||||
return azimuth
|
||||
}
|
||||
|
||||
func TestStateWithOptionsZeroMatchesState(t *testing.T) {
|
||||
elements := Elements{
|
||||
EpochJD: 2451545, A: 2.7, E: 0.08, I: 10, Omega: 80, W: 73, M0: 231,
|
||||
}
|
||||
for _, jd := range []float64{2451545, 2452000, 2449000} {
|
||||
wantPos, wantVel, wantErr := State(elements, jd)
|
||||
gotPos, gotVel, gotErr := StateWithOptions(elements, jd, PropagationOptions{})
|
||||
if gotPos != wantPos || gotVel != wantVel || gotErr != wantErr {
|
||||
t.Fatalf("jd %.1f: got %v %v %v, want %v %v %v", jd, gotPos, gotVel, gotErr, wantPos, wantVel, wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type staticPerturbationEngine struct{}
|
||||
|
||||
func (staticPerturbationEngine) 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
|
||||
}
|
||||
switch target {
|
||||
case 10:
|
||||
return pos, vel, nil
|
||||
case 5:
|
||||
pos[0] = 778000000
|
||||
return pos, vel, nil
|
||||
default:
|
||||
return pos, vel, ErrPerturbationEngine
|
||||
}
|
||||
}
|
||||
|
||||
func (staticPerturbationEngine) Coverage(target Body) (start, end float64, ok bool) {
|
||||
return 0, 3000000, true
|
||||
}
|
||||
|
||||
func TestStateWithOptionsPerturbationChangesState(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
plain, _, err := State(elements, elements.EpochJD+30)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
perturbed, perturbedVelocity, err := StateWithOptions(elements, elements.EpochJD+30, PropagationOptions{
|
||||
Perturbers: []Body{5},
|
||||
Engine: staticPerturbationEngine{},
|
||||
Step: 2,
|
||||
Tolerance: 1e-10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if perturbed == plain {
|
||||
t.Fatal("perturbation did not change the state")
|
||||
}
|
||||
if !finite(vectorNorm(perturbed)) || !finite(vectorNorm(perturbedVelocity)) || vectorNorm(perturbedVelocity) > 1e7 {
|
||||
t.Fatalf("perturbed state is unstable: position=%v velocity=%v", perturbed, perturbedVelocity)
|
||||
}
|
||||
}
|
||||
|
||||
type countingPerturbationEngine struct {
|
||||
calls map[Body]int
|
||||
}
|
||||
|
||||
func (engine *countingPerturbationEngine) State(target, center Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
||||
if target == center {
|
||||
return pos, vel, nil
|
||||
}
|
||||
engine.calls[target]++
|
||||
if center != 0 {
|
||||
return pos, vel, ErrPerturbationEngine
|
||||
}
|
||||
switch target {
|
||||
case 10:
|
||||
return pos, vel, nil
|
||||
case 5:
|
||||
pos[0] = 778000000
|
||||
return pos, vel, nil
|
||||
default:
|
||||
return pos, vel, ErrPerturbationEngine
|
||||
}
|
||||
}
|
||||
|
||||
func (*countingPerturbationEngine) Coverage(target Body) (start, end float64, ok bool) {
|
||||
return 0, 3000000, true
|
||||
}
|
||||
|
||||
func TestStateWithOptionsCachesRepeatedSamples(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
provider := &countingPerturbationEngine{calls: make(map[Body]int)}
|
||||
_, _, err := StateWithOptions(elements, elements.EpochJD+1, PropagationOptions{
|
||||
Perturbers: []Body{5},
|
||||
Engine: provider,
|
||||
Step: 1,
|
||||
Tolerance: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, body := range []Body{5, 10} {
|
||||
if got, want := provider.calls[body], 5; got != want {
|
||||
t.Fatalf("body %d provider calls = %d, want %d", body, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateWithOptionsDeduplicatesPerturbers(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
provider := &countingPerturbationEngine{calls: make(map[Body]int)}
|
||||
singlePos, singleVel, err := StateWithOptions(elements, elements.EpochJD+10, PropagationOptions{
|
||||
Perturbers: []Body{5},
|
||||
Engine: provider,
|
||||
Step: 2,
|
||||
Tolerance: 1e-8,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
duplicatePos, duplicateVel, err := StateWithOptions(elements, elements.EpochJD+10, PropagationOptions{
|
||||
Perturbers: []Body{5, 5, 10, 0},
|
||||
Engine: provider,
|
||||
Step: 2,
|
||||
Tolerance: 1e-8,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if duplicatePos != singlePos || duplicateVel != singleVel {
|
||||
t.Fatalf("duplicate perturbers changed state: %v %v versus %v %v", duplicatePos, duplicateVel, singlePos, singleVel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateWithOptionsRejectsNonFiniteTarget(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
if _, _, err := StateWithOptions(elements, math.NaN(), PropagationOptions{}); err != ErrInvalidPropagationElements {
|
||||
t.Fatalf("NaN target error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEphemerisEngineSunPerturberKeepsTwoBodyPath(t *testing.T) {
|
||||
elements := Elements{EpochJD: 2451545, A: 1.5, E: 0.02, I: 3, Omega: 20, W: 30, M0: 10}
|
||||
engine := NewEphemerisEngine(Body(12345), elements, PropagationOptions{Perturbers: []Body{10}})
|
||||
gotPos, gotVel, err := engine.State(Body(12345), 10, elements.EpochJD+10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantPos, wantVel, err := State(elements, elements.EpochJD+10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotPos != wantPos || gotVel != wantVel {
|
||||
t.Fatalf("Sun-only perturbation changed two-body state: %v %v versus %v %v", gotPos, gotVel, wantPos, wantVel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateWithOptionsZeroMatchesExistingPositionMath(t *testing.T) {
|
||||
cases := []Elements{
|
||||
{EpochJD: 2451545, A: 2.7, E: 0.08, I: 10, Omega: 80, W: 73, M0: 231},
|
||||
{Q: 0.9, E: 1, I: 30, Omega: 40, W: 50, TpJD: 2451545},
|
||||
{Q: 1.2, E: 1.05, I: 30, Omega: 40, W: 50, TpJD: 2451545},
|
||||
}
|
||||
for _, elements := range cases {
|
||||
jd := 2451545.0 + 20
|
||||
got, _, err := StateWithOptions(elements, jd, PropagationOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
basicVector := basic.OrbitHeliocentricXYZJ2000(jd, toBasicElements(elements))
|
||||
epsilon := basic.EclipticObliquity(2451545, false) * math.Pi / 180
|
||||
want := [3]float64{
|
||||
basicVector[0] * 149597870.7,
|
||||
(basicVector[1]*math.Cos(epsilon) - basicVector[2]*math.Sin(epsilon)) * 149597870.7,
|
||||
(basicVector[1]*math.Sin(epsilon) + basicVector[2]*math.Cos(epsilon)) * 149597870.7,
|
||||
}
|
||||
for index := range got {
|
||||
if math.Abs(got[index]-want[index]) > 1e-6 {
|
||||
t.Fatalf("elements %+v component %d = %.12f, want %.12f", elements, index, got[index], want[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user