044f2f4381
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
154 lines
4.8 KiB
Go
154 lines
4.8 KiB
Go
// 被测契约:内置表经 ICRF 适配后与 DE441 几何状态一致到表本身的精度;质心推算误差不影响光行差。
|
|
package semianalytic
|
|
|
|
import (
|
|
"bufio"
|
|
"math"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"b612.me/astro/internal/ephem"
|
|
)
|
|
|
|
type sample struct {
|
|
target, center ephem.Body
|
|
jd float64
|
|
p, v [3]float64
|
|
}
|
|
|
|
func loadSamples(t *testing.T) []sample {
|
|
f, err := os.Open("testdata/de441-states.csv")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer f.Close()
|
|
var out []sample
|
|
sc := bufio.NewScanner(f)
|
|
for sc.Scan() {
|
|
parts := strings.Split(sc.Text(), ",")
|
|
var v [9]float64
|
|
for i := range v {
|
|
if v[i], err = strconv.ParseFloat(parts[i], 64); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
out = append(out, sample{ephem.Body(v[0]), ephem.Body(v[1]), v[2], [3]float64{v[3], v[4], v[5]}, [3]float64{v[6], v[7], v[8]}})
|
|
}
|
|
if len(out) < 500 {
|
|
t.Fatalf("only %d samples", len(out))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func norm(a, b [3]float64) float64 {
|
|
return math.Sqrt((a[0]-b[0])*(a[0]-b[0]) + (a[1]-b[1])*(a[1]-b[1]) + (a[2]-b[2])*(a[2]-b[2]))
|
|
}
|
|
|
|
func modern(jd float64) bool { return jd >= 2086302.5 && jd <= 2816787.5 }
|
|
|
|
func TestStatesMatchDE441(t *testing.T) {
|
|
type key struct {
|
|
target, center ephem.Body
|
|
modern bool
|
|
}
|
|
type acc struct{ n, dp, dv, maxDp, maxDv, maxAng float64 }
|
|
stats := map[key]*acc{}
|
|
e := Ephemeris{}
|
|
for _, s := range loadSamples(t) {
|
|
p, v, err := e.State(s.target, s.center, s.jd)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
k := key{s.target, s.center, modern(s.jd)}
|
|
if stats[k] == nil {
|
|
stats[k] = &acc{}
|
|
}
|
|
a := stats[k]
|
|
dp, dv := norm(p, s.p), norm(v, s.v)/86.4
|
|
r := math.Sqrt(s.p[0]*s.p[0] + s.p[1]*s.p[1] + s.p[2]*s.p[2])
|
|
a.n++
|
|
a.dp += dp * dp
|
|
a.dv += dv * dv
|
|
a.maxDp = math.Max(a.maxDp, dp)
|
|
a.maxDv = math.Max(a.maxDv, dv)
|
|
a.maxAng = math.Max(a.maxAng, dp/r*206264.806)
|
|
}
|
|
// 上限:最大位置差 km、最大速度差 m/s;太阳相对质心的残差来自表外天体(冥王星、阋神星等)。
|
|
type limit struct{ km, ms float64 }
|
|
limits := map[key]limit{
|
|
{ephem.Sun, ephem.SSB, true}: {300, 1e-3}, {ephem.Sun, ephem.SSB, false}: {300, 1e-3},
|
|
{ephem.Earth, ephem.SSB, true}: {300, 0.01}, {ephem.Earth, ephem.SSB, false}: {500, 0.05},
|
|
{ephem.Earth, ephem.Sun, true}: {40, 0.01}, {ephem.Earth, ephem.Sun, false}: {250, 0.05},
|
|
{ephem.Moon, ephem.Earth, true}: {0.6, 0.005}, {ephem.Moon, ephem.Earth, false}: {1.2, 0.008},
|
|
{ephem.Mars, ephem.SSB, true}: {400, 0.04}, {ephem.Mars, ephem.SSB, false}: {1200, 0.1},
|
|
{ephem.Jupiter, ephem.SSB, true}: {2000, 0.03}, {ephem.Jupiter, ephem.SSB, false}: {4000, 0.05},
|
|
{ephem.Saturn, ephem.SSB, true}: {2500, 0.02}, {ephem.Saturn, ephem.SSB, false}: {7000, 0.04},
|
|
}
|
|
for k, a := range stats {
|
|
t.Logf("%3d-%3d modern=%-5v n=%2.0f pos rms %.3g km max %.3g km (%.4f″) vel rms %.3g m/s max %.3g m/s",
|
|
k.target, k.center, k.modern, a.n, math.Sqrt(a.dp/a.n), a.maxDp, a.maxAng, math.Sqrt(a.dv/a.n), a.maxDv)
|
|
l, ok := limits[k]
|
|
if !ok {
|
|
t.Errorf("%d-%d modern=%v: no limit defined", k.target, k.center, k.modern)
|
|
continue
|
|
}
|
|
if a.maxDp > l.km || a.maxDv > l.ms {
|
|
t.Errorf("%d-%d modern=%v: max %.3g km / %.3g m/s exceeds %.3g km / %.3g m/s",
|
|
k.target, k.center, k.modern, a.maxDp, a.maxDv, l.km, l.ms)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStateAntisymmetricAndComposable(t *testing.T) {
|
|
e := Ephemeris{}
|
|
bodies := []ephem.Body{ephem.SSB, ephem.Sun, ephem.Mercury, ephem.EMB, ephem.Earth, ephem.Moon, ephem.Neptune}
|
|
for _, jd := range []float64{2451545.0, 2460000.5, 1000000.5} {
|
|
for _, a := range bodies {
|
|
for _, b := range bodies {
|
|
p1, v1, err1 := e.State(a, b, jd)
|
|
p2, v2, err2 := e.State(b, a, jd)
|
|
if err1 != nil || err2 != nil {
|
|
t.Fatal(err1, err2)
|
|
}
|
|
for i := range p1 {
|
|
if math.Abs(p1[i]+p2[i]) > 1e-6 || math.Abs(v1[i]+v2[i]) > 1e-9 {
|
|
t.Fatalf("%d-%d jd %.1f: not antisymmetric", a, b, jd)
|
|
}
|
|
}
|
|
}
|
|
// 经质心中转与直接求差一致
|
|
ps, _, _ := e.State(a, ephem.SSB, jd)
|
|
pe, _, _ := e.State(ephem.Earth, ephem.SSB, jd)
|
|
pd, _, _ := e.State(a, ephem.Earth, jd)
|
|
for i := range pd {
|
|
if math.Abs(ps[i]-pe[i]-pd[i]) > 1e-4 {
|
|
t.Fatalf("%d via SSB differs from direct by %.3g km", a, ps[i]-pe[i]-pd[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnsupportedBodyAndCoverage(t *testing.T) {
|
|
e := Ephemeris{}
|
|
if _, _, err := e.State(ephem.Body(999), ephem.Sun, 2451545); err != ephem.ErrUnsupportedBody {
|
|
t.Fatalf("want ErrUnsupportedBody, got %v", err)
|
|
}
|
|
if _, _, ok := e.Coverage(ephem.Body(999)); ok {
|
|
t.Fatal("unsupported body reported coverage")
|
|
}
|
|
s, end, ok := e.Coverage(ephem.Moon)
|
|
if !ok || s >= 2086302.5 || end <= 2816787.5 {
|
|
t.Fatalf("coverage %v..%v ok=%v", s, end, ok)
|
|
}
|
|
}
|
|
|
|
func BenchmarkEarthBarycentric(b *testing.B) {
|
|
e := Ephemeris{}
|
|
for i := 0; i < b.N; i++ {
|
|
e.State(ephem.Earth, ephem.SSB, 2451545.0+float64(i%7300)*10)
|
|
}
|
|
}
|