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,8 @@
|
||||
package ephem
|
||||
|
||||
// ApparentEngine 支持高精度视位置的星历引擎;Apparent 返回 ICRF 视方向单位矢量(含光行时、太阳引力偏折、相对论光行差)、
|
||||
// 几何地心距 km 与光行时(日) / ephemeris engine with apparent positions.
|
||||
type ApparentEngine interface {
|
||||
Engine
|
||||
Apparent(target Body, jdTT float64) (dir [3]float64, distKM, tauDays float64, err error)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package ephem
|
||||
|
||||
import "math"
|
||||
|
||||
const (
|
||||
genericCKMPerDay = 299792.458 * 86400
|
||||
genericAUKM = 149597870.7
|
||||
// 太阳史瓦西半径 2GM☉/c²,AU。
|
||||
genericSunSchwarzschildAU = 1.97412574336e-8
|
||||
)
|
||||
|
||||
// genericApparent 用 Engine.State 求地心视方向:迭代光行时、太阳引力偏折、相对论光行差,均在 ICRF 中进行。
|
||||
// 引擎不能给出相对太阳系质心的地球、太阳或目标状态时返回 ErrUnsupportedApparent。
|
||||
func genericApparent(engine Engine, target Body, jdTT float64) (dir [3]float64, distKM, tauDays float64, err error) {
|
||||
earth, earthVel, err := engine.State(Earth, SSB, jdTT)
|
||||
if err != nil {
|
||||
return dir, 0, 0, ErrUnsupportedApparent
|
||||
}
|
||||
now, _, err := engine.State(target, SSB, jdTT)
|
||||
if err != nil {
|
||||
return dir, 0, 0, ErrUnsupportedApparent
|
||||
}
|
||||
g := genericSub(now, earth)
|
||||
distKM = genericLen(g)
|
||||
if !(distKM > 0) || math.IsInf(distKM, 0) {
|
||||
return dir, 0, 0, ErrUnsupportedApparent
|
||||
}
|
||||
tauDays = distKM / genericCKMPerDay
|
||||
var retarded [3]float64
|
||||
for k := 0; k < 3; k++ {
|
||||
if retarded, _, err = engine.State(target, SSB, jdTT-tauDays); err != nil {
|
||||
return dir, 0, 0, ErrUnsupportedApparent
|
||||
}
|
||||
g = genericSub(retarded, earth)
|
||||
tauDays = genericLen(g) / genericCKMPerDay
|
||||
}
|
||||
q := genericScale(g, 1/genericLen(g))
|
||||
if target != Sun && target != Moon && target != Earth {
|
||||
sunThen, _, sunErr := engine.State(Sun, SSB, jdTT-tauDays)
|
||||
sunNow, _, sunNowErr := engine.State(Sun, SSB, jdTT)
|
||||
if sunErr == nil && sunNowErr == nil {
|
||||
q = genericDeflect(q, genericSub(retarded, sunThen), genericSub(earth, sunNow))
|
||||
}
|
||||
}
|
||||
return genericAberrate(q, earthVel), distKM, tauDays, nil
|
||||
}
|
||||
|
||||
// genericDeflect 太阳引力偏折;p 目标方向,t 目标日心位置,e 地球日心位置。
|
||||
func genericDeflect(p, t, e [3]float64) [3]float64 {
|
||||
el, tl := genericLen(e), genericLen(t)
|
||||
if el <= 0 || tl <= 0 {
|
||||
return p
|
||||
}
|
||||
eu, qu := genericScale(e, 1/el), genericScale(t, 1/tl)
|
||||
qpe := genericDot(qu, eu) + 1
|
||||
if qpe < 1e-9 {
|
||||
return p
|
||||
}
|
||||
w := genericSunSchwarzschildAU / (el / genericAUKM) / qpe
|
||||
pe, pq := genericDot(p, eu), genericDot(p, qu)
|
||||
for i := range p {
|
||||
p[i] += w * (eu[i]*pq - qu[i]*pe)
|
||||
}
|
||||
return genericScale(p, 1/genericLen(p))
|
||||
}
|
||||
|
||||
// genericAberrate 相对论光行差;v 观测者相对质心速度 km/日。
|
||||
func genericAberrate(p, v [3]float64) [3]float64 {
|
||||
b := genericScale(v, 1/genericCKMPerDay)
|
||||
bm := 1 / math.Sqrt(1-genericDot(b, b))
|
||||
pdv := genericDot(p, b)
|
||||
w1 := 1 + pdv/(1+1/bm)
|
||||
w2 := 1 / (1 + pdv)
|
||||
var r [3]float64
|
||||
for i := range p {
|
||||
r[i] = (p[i]/bm + w1*b[i]) * w2
|
||||
}
|
||||
return genericScale(r, 1/genericLen(r))
|
||||
}
|
||||
|
||||
func genericDot(a, b [3]float64) float64 { return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] }
|
||||
func genericLen(a [3]float64) float64 { return math.Sqrt(genericDot(a, a)) }
|
||||
func genericSub(a, b [3]float64) [3]float64 {
|
||||
return [3]float64{a[0] - b[0], a[1] - b[1], a[2] - b[2]}
|
||||
}
|
||||
func genericScale(a [3]float64, k float64) [3]float64 {
|
||||
return [3]float64{a[0] * k, a[1] * k, a[2] * k}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package ephem
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Body NAIF 天体编号 / NAIF body code.
|
||||
type Body int
|
||||
|
||||
const (
|
||||
SSB Body = 0
|
||||
Mercury Body = 1
|
||||
Venus Body = 2
|
||||
EMB Body = 3
|
||||
Mars Body = 4
|
||||
Jupiter Body = 5
|
||||
Saturn Body = 6
|
||||
Uranus Body = 7
|
||||
Neptune Body = 8
|
||||
Sun Body = 10
|
||||
Moon Body = 301
|
||||
Earth Body = 399
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrOutOfRange 时刻超出星历覆盖 / epoch outside the ephemeris coverage.
|
||||
ErrOutOfRange = errors.New("ephem: epoch outside coverage")
|
||||
// ErrUnsupportedBody 星历不含该天体 / body not provided by the ephemeris.
|
||||
ErrUnsupportedBody = errors.New("ephem: unsupported body")
|
||||
// ErrNoEngine 实例未配置引擎 / instance has no engine.
|
||||
ErrNoEngine = errors.New("ephem: instance has no engine")
|
||||
// ErrUnsupportedApparent 引擎不支持视位置 / engine does not support apparent positions.
|
||||
ErrUnsupportedApparent = errors.New("ephem: apparent positions are unsupported")
|
||||
)
|
||||
|
||||
// Engine 几何星历:ICRF 赤道,km 与 km/日,时间 TDB;只读、可并发 / geometric ephemeris in ICRF, km and km/day, TDB.
|
||||
type Engine interface {
|
||||
State(target, center Body, jdTDB float64) (pos, vel [3]float64, err error)
|
||||
Coverage(target Body) (start, end float64, ok bool)
|
||||
}
|
||||
|
||||
// IsNilEngine 引擎为 nil 或持有 nil 指针等空值 / engine is nil or wraps a nil value.
|
||||
func IsNilEngine(engine Engine) bool {
|
||||
if engine == nil {
|
||||
return true
|
||||
}
|
||||
value := reflect.ValueOf(engine)
|
||||
switch value.Kind() {
|
||||
case reflect.Ptr, reflect.Map, reflect.Slice, reflect.Func, reflect.Interface:
|
||||
return value.IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build astro_legacy
|
||||
// +build astro_legacy
|
||||
|
||||
package ephem
|
||||
|
||||
// BuildDefault 本次构建的初始模型 / the model active at program start.
|
||||
//
|
||||
// 构建标签 astro_legacy 让全库以旧级数模型为默认运行。
|
||||
const BuildDefault = Legacy
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !astro_legacy
|
||||
// +build !astro_legacy
|
||||
|
||||
package ephem
|
||||
|
||||
// BuildDefault 本次构建的初始模型 / the model active at program start.
|
||||
const BuildDefault = Semianalytic
|
||||
@@ -0,0 +1,160 @@
|
||||
// Package ephem 全局星历模型状态与实例接口 / global ephemeris model state and instance interfaces.
|
||||
package ephem
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Model 星历模型 / ephemeris model.
|
||||
type Model int32
|
||||
|
||||
const (
|
||||
// Legacy VSOP87 + ELP2000 截断级数 / truncated VSOP87 and ELP2000 series.
|
||||
Legacy Model = iota
|
||||
// Semianalytic 按 DE441 重拟的半解析表 / semi-analytic tables refitted to DE441.
|
||||
Semianalytic
|
||||
// Custom 外部 Engine 实例,不对应内置模型 / external Engine instance without a built-in model.
|
||||
Custom Model = -1
|
||||
)
|
||||
|
||||
var (
|
||||
active = int32(BuildDefault)
|
||||
generation uint64 = 1
|
||||
modelEngines = make(map[Model]func() Engine)
|
||||
modelEnginesMu sync.RWMutex
|
||||
)
|
||||
|
||||
// Active 当前模型 / the active model.
|
||||
func Active() Model { return Model(atomic.LoadInt32(&active)) }
|
||||
|
||||
// Generation 模型切换世代,起始为 1 / model switch generation, starting at 1.
|
||||
func Generation() uint64 { return atomic.LoadUint64(&generation) }
|
||||
|
||||
// SetActive 切换模型,返回是否发生变化 / switches the model and reports whether it changed.
|
||||
func SetActive(m Model) bool {
|
||||
if m != Legacy && m != Semianalytic {
|
||||
return false
|
||||
}
|
||||
globalInstanceMu.Lock()
|
||||
defer globalInstanceMu.Unlock()
|
||||
if atomic.SwapInt32(&active, int32(m)) == int32(m) {
|
||||
return false
|
||||
}
|
||||
atomic.AddUint64(&generation, 1)
|
||||
globalInstance.Store(NewModelInstance(m))
|
||||
return true
|
||||
}
|
||||
|
||||
// RegisterModelEngine 注册内置模型引擎工厂 / register a built-in model engine factory.
|
||||
func RegisterModelEngine(m Model, factory func() Engine) {
|
||||
if m != Legacy && m != Semianalytic || factory == nil {
|
||||
return
|
||||
}
|
||||
modelEnginesMu.Lock()
|
||||
modelEngines[m] = factory
|
||||
modelEnginesMu.Unlock()
|
||||
}
|
||||
|
||||
// Instance 星历实例,可包含配置和缓存 / ephemeris instance with configuration and cache.
|
||||
type Instance struct {
|
||||
model Model
|
||||
engine Engine
|
||||
}
|
||||
|
||||
// NewInstance 创建自定义星历实例 / create an instance from a custom engine.
|
||||
func NewInstance(engine Engine) *Instance {
|
||||
if IsNilEngine(engine) {
|
||||
engine = nil
|
||||
}
|
||||
return &Instance{model: Custom, engine: engine}
|
||||
}
|
||||
|
||||
// NewModelInstance 创建内置模型实例 / create an instance for a built-in model.
|
||||
func NewModelInstance(m Model) *Instance {
|
||||
if m != Legacy && m != Semianalytic {
|
||||
m = BuildDefault
|
||||
}
|
||||
modelEnginesMu.RLock()
|
||||
factory := modelEngines[m]
|
||||
modelEnginesMu.RUnlock()
|
||||
var engine Engine
|
||||
if factory != nil {
|
||||
engine = factory()
|
||||
}
|
||||
return &Instance{model: m, engine: engine}
|
||||
}
|
||||
|
||||
// Model 获取实例使用的模型 / get model used by this instance.
|
||||
func (inst *Instance) Model() Model {
|
||||
if inst == nil {
|
||||
return Custom
|
||||
}
|
||||
return inst.model
|
||||
}
|
||||
|
||||
// Engine 获取底层星历引擎 / get the underlying ephemeris engine.
|
||||
func (inst *Instance) Engine() Engine {
|
||||
if inst == nil {
|
||||
return nil
|
||||
}
|
||||
return inst.engine
|
||||
}
|
||||
|
||||
// State 计算目标相对中心的状态 / calculate target state relative to center.
|
||||
func (inst *Instance) State(target, center Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
||||
if inst == nil || inst.engine == nil {
|
||||
return pos, vel, ErrNoEngine
|
||||
}
|
||||
return inst.engine.State(target, center, jdTDB)
|
||||
}
|
||||
|
||||
// Coverage 返回目标覆盖范围 / return target coverage.
|
||||
func (inst *Instance) Coverage(target Body) (start, end float64, ok bool) {
|
||||
if inst == nil || inst.engine == nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
return inst.engine.Coverage(target)
|
||||
}
|
||||
|
||||
// Apparent 计算地心视方向 / calculate apparent geocentric direction.
|
||||
func (inst *Instance) Apparent(target Body, jdTT float64) (dir [3]float64, distKM, tauDays float64, err error) {
|
||||
if inst == nil || inst.engine == nil {
|
||||
return dir, 0, 0, ErrNoEngine
|
||||
}
|
||||
if engine, ok := inst.engine.(ApparentEngine); ok {
|
||||
return engine.Apparent(target, jdTT)
|
||||
}
|
||||
// 只给几何状态的引擎(如 SPK)走通用矢量链。
|
||||
return genericApparent(inst.engine, target, jdTT)
|
||||
}
|
||||
|
||||
var (
|
||||
globalInstance atomic.Value
|
||||
globalInstanceMu sync.Mutex
|
||||
)
|
||||
|
||||
// Global 全局默认星历实例 / global default ephemeris instance.
|
||||
func Global() *Instance {
|
||||
if inst, ok := globalInstance.Load().(*Instance); ok && inst != nil {
|
||||
return inst
|
||||
}
|
||||
globalInstanceMu.Lock()
|
||||
defer globalInstanceMu.Unlock()
|
||||
if inst, ok := globalInstance.Load().(*Instance); ok && inst != nil {
|
||||
return inst
|
||||
}
|
||||
inst := NewModelInstance(Active())
|
||||
globalInstance.Store(inst)
|
||||
return inst
|
||||
}
|
||||
|
||||
// SetGlobal 设置全局默认星历实例 / set global default ephemeris instance.
|
||||
func SetGlobal(inst *Instance) {
|
||||
if inst == nil {
|
||||
return
|
||||
}
|
||||
globalInstanceMu.Lock()
|
||||
defer globalInstanceMu.Unlock()
|
||||
globalInstance.Store(inst)
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package ephem
|
||||
|
||||
import "testing"
|
||||
|
||||
type testEngine struct {
|
||||
marker float64
|
||||
}
|
||||
|
||||
func (engine testEngine) State(target, center Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
||||
pos[0] = engine.marker + float64(target-center)
|
||||
vel[0] = jdTDB
|
||||
return pos, vel, nil
|
||||
}
|
||||
|
||||
func (engine testEngine) Coverage(target Body) (start, end float64, ok bool) {
|
||||
return engine.marker, engine.marker + float64(target), true
|
||||
}
|
||||
|
||||
func (engine testEngine) Apparent(target Body, jdTT float64) (dir [3]float64, distKM, tauDays float64, err error) {
|
||||
dir[0] = engine.marker + float64(target)
|
||||
return dir, jdTT, engine.marker, nil
|
||||
}
|
||||
|
||||
func TestInstanceUsesItsOwnEngine(t *testing.T) {
|
||||
first := NewInstance(testEngine{marker: 1})
|
||||
second := NewInstance(testEngine{marker: 2})
|
||||
firstPos, _, err := first.State(Mars, Earth, 2451545)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
secondPos, _, err := second.State(Mars, Earth, 2451545)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if firstPos[0] == secondPos[0] || first.Engine() == second.Engine() {
|
||||
t.Fatal("instances are not isolated")
|
||||
}
|
||||
if got, want := first.Model(), Custom; got != want {
|
||||
t.Fatalf("custom model = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceApparentAndCoverage(t *testing.T) {
|
||||
instance := NewInstance(testEngine{marker: 3})
|
||||
start, end, ok := instance.Coverage(Mars)
|
||||
if !ok || start != 3 || end != 7 {
|
||||
t.Fatalf("coverage = %v, %v, %v", start, end, ok)
|
||||
}
|
||||
dir, distance, tau, err := instance.Apparent(Mars, 2451545)
|
||||
if err != nil || dir[0] != 7 || distance != 2451545 || tau != 3 {
|
||||
t.Fatalf("apparent = %v, %v, %v, %v", dir, distance, tau, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceWithoutEngineReportsExplicitErrors(t *testing.T) {
|
||||
instance := &Instance{model: BuildDefault}
|
||||
if _, _, err := instance.State(Mars, Earth, 2451545); err != ErrNoEngine {
|
||||
t.Fatalf("State error = %v, want %v", err, ErrNoEngine)
|
||||
}
|
||||
if _, _, _, err := instance.Apparent(Mars, 2451545); err != ErrNoEngine {
|
||||
t.Fatalf("Apparent error = %v, want %v", err, ErrNoEngine)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package ephem
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGlobalDefaultInstanceIsStable(t *testing.T) {
|
||||
model, generation := Active(), Generation()
|
||||
first := Global()
|
||||
if first == nil || first.Model() != model {
|
||||
t.Fatalf("default instance = %v, active model = %v", first, model)
|
||||
}
|
||||
if second := Global(); second != first {
|
||||
t.Error("lazy default must retain its instance identity")
|
||||
}
|
||||
if Active() != model || Generation() != generation {
|
||||
t.Error("reading the default changed the active model or generation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalExplicitInstanceIsStable(t *testing.T) {
|
||||
previous := Global()
|
||||
t.Cleanup(func() { SetGlobal(previous) })
|
||||
custom := NewInstance(testEngine{marker: 7})
|
||||
SetGlobal(custom)
|
||||
if Global() != custom || Global() != custom {
|
||||
t.Error("explicit default lost its instance identity")
|
||||
}
|
||||
SetGlobal(nil)
|
||||
if Global() != custom {
|
||||
t.Error("nil must not replace an explicit default")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// 被测契约:持有 nil 指针的 Engine 与裸 nil 等价,构造器必须拦下而不是留到调用时 panic。
|
||||
package ephem
|
||||
|
||||
import "testing"
|
||||
|
||||
type nilCheckEngine struct{ offset float64 }
|
||||
|
||||
func (engine *nilCheckEngine) State(target, center Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
||||
pos[0] = engine.offset
|
||||
return pos, vel, nil
|
||||
}
|
||||
|
||||
func (engine *nilCheckEngine) Coverage(target Body) (start, end float64, ok bool) {
|
||||
return 0, engine.offset, true
|
||||
}
|
||||
|
||||
func TestConstructorsRejectTypedNilEngine(t *testing.T) {
|
||||
var typedNil *nilCheckEngine
|
||||
cases := map[string]func() error{
|
||||
"NewInstance": func() error {
|
||||
_, _, err := NewInstance(typedNil).State(Mars, Sun, 2451545)
|
||||
return err
|
||||
},
|
||||
"NewStack": func() error {
|
||||
_, _, err := NewInstance(NewStack(typedNil)).State(Mars, Sun, 2451545)
|
||||
return err
|
||||
},
|
||||
}
|
||||
for name, call := range cases {
|
||||
if err := call(); err == nil {
|
||||
t.Errorf("%s: err = nil, want an error", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceKeepsUsableEngine(t *testing.T) {
|
||||
pos, _, err := NewInstance(&nilCheckEngine{offset: 7}).State(Mars, Sun, 2451545)
|
||||
if err != nil {
|
||||
t.Fatalf("State err = %v", err)
|
||||
}
|
||||
if pos[0] != 7 {
|
||||
t.Errorf("pos[0] = %v, want 7", pos[0])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
package ephem
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// BodyProvider 可选地报告引擎能够触及的天体编号 / optionally reports bodies reachable by an engine.
|
||||
type BodyProvider interface {
|
||||
Bodies() []Body
|
||||
}
|
||||
|
||||
// Stack 按顺序叠加多个只读星历引擎 / ordered stack of read-only ephemeris engines.
|
||||
type Stack struct {
|
||||
engines []Engine
|
||||
// extra 为各源声明、且不在 chainBodies 中的天体,创建时算好。
|
||||
extra []Body
|
||||
}
|
||||
|
||||
// stackInlineBodies 中心链搜索在栈上预留的候选数,超出时 append 退回堆分配。
|
||||
const stackInlineBodies = 24
|
||||
|
||||
type stackNode struct {
|
||||
body Body
|
||||
pos, vel [3]float64
|
||||
}
|
||||
|
||||
// chainBodies 中心链搜索的固定候选,顺序决定同等路径的取舍。
|
||||
var chainBodies = [...]Body{SSB, Sun, Mercury, Venus, EMB, Earth, Moon, Mars, Jupiter, Saturn, Uranus, Neptune}
|
||||
|
||||
// NewStack 创建多源星历;先传入的引擎优先 / creates a multi-source ephemeris; earlier engines have priority.
|
||||
func NewStack(engines ...Engine) *Stack {
|
||||
usable := make([]Engine, 0, len(engines))
|
||||
for _, engine := range engines {
|
||||
if !IsNilEngine(engine) {
|
||||
usable = append(usable, engine)
|
||||
}
|
||||
}
|
||||
stack := &Stack{engines: usable}
|
||||
for _, body := range stack.Bodies() {
|
||||
if !containsBody(chainBodies[:], body) {
|
||||
stack.extra = append(stack.extra, body)
|
||||
}
|
||||
}
|
||||
return stack
|
||||
}
|
||||
|
||||
// Engines 返回底层引擎快照 / returns a snapshot of the underlying engines.
|
||||
func (stack *Stack) Engines() []Engine {
|
||||
if stack == nil {
|
||||
return nil
|
||||
}
|
||||
return append([]Engine(nil), stack.engines...)
|
||||
}
|
||||
|
||||
// Bodies 返回各源声明的天体编号 / returns body identifiers declared by the sources.
|
||||
func (stack *Stack) Bodies() []Body {
|
||||
if stack == nil {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[Body]bool)
|
||||
result := make([]Body, 0)
|
||||
for _, engine := range stack.engines {
|
||||
provider, ok := engine.(BodyProvider)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, body := range provider.Bodies() {
|
||||
if !seen[body] {
|
||||
seen[body] = true
|
||||
result = append(result, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(result, func(left, right int) bool { return result[left] < result[right] })
|
||||
return result
|
||||
}
|
||||
|
||||
// Coverage 返回所有源覆盖区间的包络 / returns the envelope of all source coverage intervals.
|
||||
func (stack *Stack) Coverage(target Body) (start, end float64, ok bool) {
|
||||
if stack == nil {
|
||||
return 0, 0, false
|
||||
}
|
||||
for _, engine := range stack.engines {
|
||||
engineStart, engineEnd, engineOK := engine.Coverage(target)
|
||||
if !engineOK {
|
||||
continue
|
||||
}
|
||||
if !ok || engineStart < start {
|
||||
start = engineStart
|
||||
}
|
||||
if !ok || engineEnd > end {
|
||||
end = engineEnd
|
||||
}
|
||||
ok = true
|
||||
}
|
||||
return start, end, ok
|
||||
}
|
||||
|
||||
// State 返回目标相对中心的状态,并在源之间拼接中心链 / returns target-relative state, composing center chains across sources.
|
||||
func (stack *Stack) State(target, center Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
||||
if target == center {
|
||||
return pos, vel, nil
|
||||
}
|
||||
if stack == nil || len(stack.engines) == 0 {
|
||||
return pos, vel, ErrNoEngine
|
||||
}
|
||||
if !finiteEpoch(jdTDB) {
|
||||
return pos, vel, ErrOutOfRange
|
||||
}
|
||||
failures := &stackFailures{}
|
||||
if pos, vel, ok := stack.direct(target, center, jdTDB, failures); ok {
|
||||
return pos, vel, nil
|
||||
}
|
||||
|
||||
var candidateBuf [stackInlineBodies]Body
|
||||
candidates := stack.candidates(candidateBuf[:0], target, center)
|
||||
var queueBuf [stackInlineBodies + 1]stackNode
|
||||
queue := append(queueBuf[:0], stackNode{body: target})
|
||||
var visitedBuf [stackInlineBodies]bool
|
||||
visited := visitedBuf[:0]
|
||||
for _, candidate := range candidates {
|
||||
visited = append(visited, candidate == target)
|
||||
}
|
||||
for head := 0; head < len(queue); head++ {
|
||||
current := queue[head]
|
||||
if current.body == center {
|
||||
return current.pos, current.vel, nil
|
||||
}
|
||||
for index, candidate := range candidates {
|
||||
if visited[index] || candidate == current.body {
|
||||
continue
|
||||
}
|
||||
edgePos, edgeVel, ok := stack.direct(current.body, candidate, jdTDB, failures)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
addVector(&edgePos, current.pos)
|
||||
addVector(&edgeVel, current.vel)
|
||||
visited[index] = true
|
||||
queue = append(queue, stackNode{body: candidate, pos: edgePos, vel: edgeVel})
|
||||
}
|
||||
}
|
||||
return pos, vel, failures.err()
|
||||
}
|
||||
|
||||
func (stack *Stack) direct(target, center Body, jdTDB float64, failures *stackFailures) (pos, vel [3]float64, ok bool) {
|
||||
for _, engine := range stack.engines {
|
||||
start, end, covered := engine.Coverage(target)
|
||||
if !covered || jdTDB < start || jdTDB > end {
|
||||
if covered && (jdTDB < start || jdTDB > end) {
|
||||
failures.add(ErrOutOfRange)
|
||||
}
|
||||
start, end, covered = engine.Coverage(center)
|
||||
if !covered || jdTDB < start || jdTDB > end {
|
||||
if covered && (jdTDB < start || jdTDB > end) {
|
||||
failures.add(ErrOutOfRange)
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
pos, vel, err := engine.State(target, center, jdTDB)
|
||||
if err == nil {
|
||||
return pos, vel, true
|
||||
}
|
||||
failures.add(err)
|
||||
}
|
||||
return pos, vel, false
|
||||
}
|
||||
|
||||
// candidates 按 center、固定候选、target、各源额外天体的顺序去重。
|
||||
func (stack *Stack) candidates(result []Body, target, center Body) []Body {
|
||||
result = appendUniqueBody(result, center)
|
||||
for _, body := range chainBodies {
|
||||
result = appendUniqueBody(result, body)
|
||||
}
|
||||
result = appendUniqueBody(result, target)
|
||||
for _, body := range stack.extra {
|
||||
result = appendUniqueBody(result, body)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func appendUniqueBody(bodies []Body, body Body) []Body {
|
||||
if containsBody(bodies, body) {
|
||||
return bodies
|
||||
}
|
||||
return append(bodies, body)
|
||||
}
|
||||
|
||||
func containsBody(bodies []Body, body Body) bool {
|
||||
for _, existing := range bodies {
|
||||
if existing == body {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type stackFailures struct {
|
||||
last error
|
||||
outOfRange bool
|
||||
unsupported bool
|
||||
}
|
||||
|
||||
func (failures *stackFailures) add(err error) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
failures.last = err
|
||||
if errors.Is(err, ErrOutOfRange) {
|
||||
failures.outOfRange = true
|
||||
}
|
||||
if errors.Is(err, ErrUnsupportedBody) {
|
||||
failures.unsupported = true
|
||||
}
|
||||
}
|
||||
|
||||
func (failures *stackFailures) err() error {
|
||||
if failures.outOfRange {
|
||||
return ErrOutOfRange
|
||||
}
|
||||
if failures.unsupported {
|
||||
return ErrUnsupportedBody
|
||||
}
|
||||
if failures.last != nil {
|
||||
return failures.last
|
||||
}
|
||||
return ErrUnsupportedBody
|
||||
}
|
||||
|
||||
func finiteEpoch(value float64) bool {
|
||||
return value == value && value < 1.7976931348623157e308 && value > -1.7976931348623157e308
|
||||
}
|
||||
|
||||
func addVector(dst *[3]float64, src [3]float64) {
|
||||
for index := range dst {
|
||||
dst[index] += src[index]
|
||||
}
|
||||
}
|
||||
|
||||
var _ Engine = (*Stack)(nil)
|
||||
var _ BodyProvider = (*Stack)(nil)
|
||||
@@ -0,0 +1,127 @@
|
||||
package ephem
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type stackTestEngine struct {
|
||||
bodies []Body
|
||||
pairs map[[2]Body][3]float64
|
||||
start float64
|
||||
end float64
|
||||
}
|
||||
|
||||
func (engine stackTestEngine) State(target, center Body, jdTDB float64) (pos, vel [3]float64, err error) {
|
||||
if value, ok := engine.pairs[[2]Body{target, center}]; ok && jdTDB >= engine.start && jdTDB <= engine.end {
|
||||
pos = value
|
||||
return pos, vel, nil
|
||||
}
|
||||
for _, body := range engine.bodies {
|
||||
if body == target || body == center {
|
||||
return pos, vel, ErrOutOfRange
|
||||
}
|
||||
}
|
||||
return pos, vel, ErrUnsupportedBody
|
||||
}
|
||||
|
||||
func (engine stackTestEngine) Coverage(target Body) (start, end float64, ok bool) {
|
||||
for _, body := range engine.bodies {
|
||||
if body == target {
|
||||
return engine.start, engine.end, true
|
||||
}
|
||||
}
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
func (engine stackTestEngine) Bodies() []Body { return append([]Body(nil), engine.bodies...) }
|
||||
|
||||
func TestStackComposesAcrossSources(t *testing.T) {
|
||||
first := stackTestEngine{
|
||||
bodies: []Body{Sun, SSB},
|
||||
pairs: map[[2]Body][3]float64{
|
||||
{Sun, SSB}: {10, 20, 30},
|
||||
},
|
||||
start: 0,
|
||||
end: 3000000,
|
||||
}
|
||||
second := stackTestEngine{
|
||||
bodies: []Body{Body(2000001), Sun},
|
||||
pairs: map[[2]Body][3]float64{
|
||||
{Body(2000001), Sun}: {1, 2, 3},
|
||||
},
|
||||
start: 0,
|
||||
end: 3000000,
|
||||
}
|
||||
stack := NewStack(first, second)
|
||||
pos, _, err := stack.State(Body(2000001), SSB, 2451545)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pos != [3]float64{11, 22, 33} {
|
||||
t.Fatalf("position = %v", pos)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackFirstSourceWinsDirectState(t *testing.T) {
|
||||
first := stackTestEngine{bodies: []Body{Mars, Earth}, pairs: map[[2]Body][3]float64{{Mars, Earth}: {1, 2, 3}}, start: 0, end: 10}
|
||||
second := stackTestEngine{bodies: []Body{Mars, Earth}, pairs: map[[2]Body][3]float64{{Mars, Earth}: {4, 5, 6}}, start: 0, end: 10}
|
||||
pos, _, err := NewStack(first, second).State(Mars, Earth, 5)
|
||||
if err != nil || pos != [3]float64{1, 2, 3} {
|
||||
t.Fatalf("state = %v, %v", pos, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackErrorPriority(t *testing.T) {
|
||||
engine := stackTestEngine{bodies: []Body{Mars}, start: 0, end: 1}
|
||||
_, _, err := NewStack(engine).State(Mars, Earth, 2)
|
||||
if !errors.Is(err, ErrOutOfRange) {
|
||||
t.Fatalf("error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackGenericApparentAcceptsArbitraryBody(t *testing.T) {
|
||||
engine := stackTestEngine{
|
||||
bodies: []Body{Earth, Sun, Body(2000001), SSB},
|
||||
pairs: map[[2]Body][3]float64{
|
||||
{Earth, SSB}: {0, 0, 0},
|
||||
{Sun, SSB}: {1000000, 0, 0},
|
||||
{Body(2000001), SSB}: {1001000, 0, 0},
|
||||
},
|
||||
start: 0,
|
||||
end: 3000000,
|
||||
}
|
||||
instance := NewInstance(NewStack(engine))
|
||||
if _, distance, _, err := instance.Apparent(Body(2000001), 2451545); err != nil || distance != 1001000 {
|
||||
t.Fatalf("apparent distance = %.3f, err = %v", distance, err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStackStateChain(b *testing.B) {
|
||||
first := stackTestEngine{bodies: []Body{Sun, SSB}, pairs: map[[2]Body][3]float64{{Sun, SSB}: {10, 20, 30}}, start: 0, end: 3000000}
|
||||
second := stackTestEngine{bodies: []Body{Body(2000001), Sun}, pairs: map[[2]Body][3]float64{{Body(2000001), Sun}: {1, 2, 3}}, start: 0, end: 3000000}
|
||||
stack := NewStack(first, second)
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, _, err := stack.State(Body(2000001), SSB, 2451545); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStackChainBeyondInlineCapacity(t *testing.T) {
|
||||
bodies := []Body{SSB}
|
||||
pairs := map[[2]Body][3]float64{}
|
||||
previous := SSB
|
||||
for index := 0; index < 2*stackInlineBodies; index++ {
|
||||
body := Body(3000000 + index)
|
||||
bodies = append(bodies, body)
|
||||
pairs[[2]Body{body, previous}] = [3]float64{1, 0, 0}
|
||||
previous = body
|
||||
}
|
||||
engine := stackTestEngine{bodies: bodies, pairs: pairs, start: 0, end: 10}
|
||||
pos, _, err := NewStack(engine).State(previous, SSB, 5)
|
||||
if err != nil || pos != [3]float64{2 * stackInlineBodies, 0, 0} {
|
||||
t.Fatalf("state = %v, %v", pos, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user