feat: 引入可切换星历模型并扩展天象观测链路

- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算
- 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询
- 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出
- 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解
- 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤
- 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持
- 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理
- 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
2026-10-15 23:54:15 +08:00
parent 16c62a97d5
commit 044f2f4381
674 changed files with 62983 additions and 8396 deletions
+8
View File
@@ -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)
}
+88
View File
@@ -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}
}
+54
View File
@@ -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
}
+9
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
//go:build !astro_legacy
// +build !astro_legacy
package ephem
// BuildDefault 本次构建的初始模型 / the model active at program start.
const BuildDefault = Semianalytic
+160
View File
@@ -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)
}
+63
View File
@@ -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)
}
}
+31
View File
@@ -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")
}
}
+44
View File
@@ -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])
}
}
+243
View File
@@ -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)
+127
View File
@@ -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)
}
}