Files
astro/planet/internal/semianalytic/state_test.go
T

154 lines
4.8 KiB
Go
Raw Normal View History

// 被测契约:内置表经 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)
}
}