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,46 @@
|
||||
package lunar
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//go:embed lunar.bin
|
||||
var defaultTable []byte
|
||||
|
||||
var (
|
||||
defaultOnce sync.Once
|
||||
defaultModel *Model
|
||||
)
|
||||
|
||||
// Default 内置表,首次调用时解析 / the built-in table, parsed on first use.
|
||||
func Default() *Model {
|
||||
defaultOnce.Do(func() {
|
||||
m, err := Parse(defaultTable)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defaultModel = m
|
||||
})
|
||||
return defaultModel
|
||||
}
|
||||
|
||||
//go:embed fast.bin
|
||||
var fastTable []byte
|
||||
|
||||
var (
|
||||
fastOnce sync.Once
|
||||
fastModel *Model
|
||||
)
|
||||
|
||||
// Fast 快速档表(n ≥ 0 用),首次调用时解析 / the fast-tier table used for n ≥ 0, parsed on first use.
|
||||
func Fast() *Model {
|
||||
fastOnce.Do(func() {
|
||||
m, err := Parse(fastTable)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fastModel = m
|
||||
})
|
||||
return fastModel
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,513 @@
|
||||
// Package lunar 月球半解析级数运行时(地心,J2000 平黄道)。
|
||||
package lunar
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
j2000 = 2451545.0
|
||||
century = 36525.0
|
||||
arcsec = math.Pi / 648000.0
|
||||
fundCount = 13
|
||||
maxEnvelope = 8
|
||||
)
|
||||
|
||||
type coordinate struct {
|
||||
args []uint16
|
||||
nodes []int32
|
||||
// 包络阶数为 0 的项单独成段:flat0 为 [cos,sin] 交错
|
||||
nodes0 []int32
|
||||
flat0 []float32
|
||||
// 其余项(阶数 ≥1)
|
||||
nodesH []int32
|
||||
envH []uint8
|
||||
coefH []float32
|
||||
env []uint8
|
||||
secular []float64
|
||||
coef []float32
|
||||
}
|
||||
|
||||
// Model 已解析的月球级数表 / parsed lunar series table.
|
||||
type Model struct {
|
||||
s2 [5][]float64
|
||||
args [fundCount][5]float64
|
||||
lonPoly [5]float64
|
||||
p, q [5]float64
|
||||
maxMult [fundCount]int
|
||||
powOff [fundCount]int
|
||||
coords [3]coordinate
|
||||
maxEnv int
|
||||
// 前缀树:节点 i 的复相位 = 节点 parent[i] × pow[factor[i]],parent 恒小于 i(-1 为根)
|
||||
nodeParent []int32
|
||||
nodeFactor []int32
|
||||
argNode []int32
|
||||
// pow 槽位 → (基本幅角, 乘数),用于速度计算中的相位变化率
|
||||
slotFund []int8
|
||||
slotMult []float64
|
||||
powLen int
|
||||
legLen int
|
||||
// Parse 之后 Model 只读;每次求值的缓冲从池中取,可并发调用
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
// scratch 单次求值的工作区。
|
||||
type scratch struct {
|
||||
pow []complex128
|
||||
node []complex128
|
||||
nodeRate []float64
|
||||
leg []float64
|
||||
dleg []float64
|
||||
env [maxEnvelope + 1]float64
|
||||
denv [maxEnvelope + 1]float64
|
||||
}
|
||||
|
||||
func (m *Model) getScratch() *scratch {
|
||||
if s, ok := m.pool.Get().(*scratch); ok {
|
||||
return s
|
||||
}
|
||||
return &scratch{
|
||||
pow: make([]complex128, m.powLen),
|
||||
node: make([]complex128, len(m.nodeParent)),
|
||||
nodeRate: make([]float64, len(m.nodeParent)),
|
||||
leg: make([]float64, m.legLen),
|
||||
dleg: make([]float64, m.legLen),
|
||||
}
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
b []byte
|
||||
off int
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *reader) take(n int) []byte {
|
||||
if r.err != nil || r.off+n > len(r.b) {
|
||||
r.err = errors.New("lunar: truncated table")
|
||||
return make([]byte, n)
|
||||
}
|
||||
s := r.b[r.off : r.off+n]
|
||||
r.off += n
|
||||
return s
|
||||
}
|
||||
|
||||
func (r *reader) u8() uint8 { return r.take(1)[0] }
|
||||
func (r *reader) u16() uint16 { return binary.LittleEndian.Uint16(r.take(2)) }
|
||||
func (r *reader) f64() float64 { return math.Float64frombits(binary.LittleEndian.Uint64(r.take(8))) }
|
||||
func (r *reader) f32() float32 { return math.Float32frombits(binary.LittleEndian.Uint32(r.take(4))) }
|
||||
|
||||
// Parse 解析 LM7 v2 二进制表 / parse an LM7 v2 table.
|
||||
func Parse(b []byte) (*Model, error) {
|
||||
r := &reader{b: b}
|
||||
if string(r.take(4)) != "LM7\x02" {
|
||||
return nil, errors.New("lunar: bad magic")
|
||||
}
|
||||
m := &Model{}
|
||||
nargs := int(r.u16())
|
||||
s2n := int(r.u16())
|
||||
secDeg := int(r.u8())
|
||||
for j := 0; j < 5; j++ {
|
||||
m.s2[j] = make([]float64, s2n)
|
||||
for k := range m.s2[j] {
|
||||
m.s2[j][k] = r.f64()
|
||||
}
|
||||
}
|
||||
for i := 0; i < fundCount; i++ {
|
||||
for k := 0; k < 5; k++ {
|
||||
m.args[i][k] = r.f64()
|
||||
}
|
||||
}
|
||||
for k := range m.lonPoly {
|
||||
m.lonPoly[k] = r.f64()
|
||||
}
|
||||
for k := range m.p {
|
||||
m.p[k] = r.f64()
|
||||
}
|
||||
for k := range m.q {
|
||||
m.q[k] = r.f64()
|
||||
}
|
||||
r.f64() // 距离比例已折入拟合系数
|
||||
mults := make([][fundCount]int8, nargs)
|
||||
for a := range mults {
|
||||
mask := r.u16()
|
||||
for i := 0; i < 16; i++ {
|
||||
if mask&(1<<uint(i)) == 0 {
|
||||
continue
|
||||
}
|
||||
if i >= fundCount {
|
||||
return nil, errors.New("lunar: multiplier index out of range")
|
||||
}
|
||||
k := int8(r.u8())
|
||||
mults[a][i] = k
|
||||
if v := int(math.Abs(float64(k))); v > m.maxMult[i] {
|
||||
m.maxMult[i] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
m.buildTrie(mults)
|
||||
for c := 0; c < 3; c++ {
|
||||
n := int(r.u16())
|
||||
co := &m.coords[c]
|
||||
co.args = make([]uint16, n)
|
||||
for j := range co.args {
|
||||
co.args[j] = r.u16()
|
||||
if int(co.args[j]) >= nargs {
|
||||
return nil, errors.New("lunar: argument index out of range")
|
||||
}
|
||||
}
|
||||
co.env = r.take(n)
|
||||
ncoef := 0
|
||||
for _, e := range co.env {
|
||||
if int(e) > maxEnvelope {
|
||||
return nil, errors.New("lunar: envelope degree too high")
|
||||
}
|
||||
if int(e) > m.maxEnv {
|
||||
m.maxEnv = int(e)
|
||||
}
|
||||
ncoef += 2 * (int(e) + 1)
|
||||
}
|
||||
co.secular = make([]float64, secDeg+1)
|
||||
for k := range co.secular {
|
||||
co.secular[k] = r.f64()
|
||||
}
|
||||
raw := make([]float32, ncoef)
|
||||
for k := range raw {
|
||||
raw[k] = r.f32()
|
||||
}
|
||||
co.coef = interleave(co.env, raw)
|
||||
}
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.off != len(b) {
|
||||
return nil, errors.New("lunar: trailing bytes")
|
||||
}
|
||||
for c := range m.coords {
|
||||
co := &m.coords[c]
|
||||
co.nodes = make([]int32, len(co.args))
|
||||
pos := 0
|
||||
for j, a := range co.args {
|
||||
co.nodes[j] = m.argNode[a]
|
||||
e := int(co.env[j])
|
||||
if e == 0 {
|
||||
co.nodes0 = append(co.nodes0, co.nodes[j])
|
||||
co.flat0 = append(co.flat0, co.coef[pos], co.coef[pos+1])
|
||||
} else {
|
||||
co.nodesH = append(co.nodesH, co.nodes[j])
|
||||
co.envH = append(co.envH, co.env[j])
|
||||
co.coefH = append(co.coefH, co.coef[pos:pos+2*(e+1)]...)
|
||||
}
|
||||
pos += 2 * (e + 1)
|
||||
}
|
||||
}
|
||||
m.legLen = max3(s2n, secDeg+1, m.maxEnv+1)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Model) buildTrie(mults [][fundCount]int8) {
|
||||
total := 0
|
||||
for i := 0; i < fundCount; i++ {
|
||||
m.powOff[i] = total + m.maxMult[i]
|
||||
total += 2*m.maxMult[i] + 1
|
||||
}
|
||||
m.powLen = total
|
||||
m.slotFund = make([]int8, total)
|
||||
m.slotMult = make([]float64, total)
|
||||
for i := 0; i < fundCount; i++ {
|
||||
for k := -m.maxMult[i]; k <= m.maxMult[i]; k++ {
|
||||
m.slotFund[m.powOff[i]+k] = int8(i)
|
||||
m.slotMult[m.powOff[i]+k] = float64(k)
|
||||
}
|
||||
}
|
||||
type edge struct{ parent, factor int32 }
|
||||
index := map[edge]int32{}
|
||||
m.argNode = make([]int32, len(mults))
|
||||
for a, mv := range mults {
|
||||
cur := int32(-1)
|
||||
for i := 0; i < fundCount; i++ {
|
||||
if mv[i] == 0 {
|
||||
continue
|
||||
}
|
||||
e := edge{cur, int32(m.powOff[i] + int(mv[i]))}
|
||||
n, ok := index[e]
|
||||
if !ok {
|
||||
n = int32(len(m.nodeParent))
|
||||
index[e] = n
|
||||
m.nodeParent = append(m.nodeParent, e.parent)
|
||||
m.nodeFactor = append(m.nodeFactor, e.factor)
|
||||
}
|
||||
cur = n
|
||||
}
|
||||
m.argNode[a] = cur
|
||||
}
|
||||
}
|
||||
|
||||
// cmul 绕开内建复数乘法的 Inf/NaN 分支。
|
||||
func cmul(a, b complex128) complex128 {
|
||||
ar, ai, br, bi := real(a), imag(a), real(b), imag(b)
|
||||
return complex(ar*br-ai*bi, ar*bi+ai*br)
|
||||
}
|
||||
|
||||
func max3(a, b, c int) int {
|
||||
if b > a {
|
||||
a = b
|
||||
}
|
||||
if c > a {
|
||||
a = c
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func poly(c *[5]float64, t float64) float64 {
|
||||
return c[0] + t*(c[1]+t*(c[2]+t*(c[3]+t*c[4])))
|
||||
}
|
||||
|
||||
// phase 与 poly 同值,但线性项单独做补偿归约:c1·t 在 ±1 万年达 1e6 rad,直接相加会丢掉约 1e-10 rad。
|
||||
func phase(c *[5]float64, t, tLow float64) float64 {
|
||||
const twoPiLow = 2.4492935982947064e-16
|
||||
product := c[1] * t
|
||||
roundoff := math.FMA(c[1], t, -product) + c[1]*tLow
|
||||
turns := math.Round(product / (2 * math.Pi))
|
||||
lin := math.FMA(-turns, 2*math.Pi, product) + (roundoff - turns*twoPiLow)
|
||||
return lin + (c[0] + t*t*(c[2]+t*(c[3]+t*c[4])))
|
||||
}
|
||||
|
||||
// dpoly 返回 poly 的一阶导数。
|
||||
func dpoly(c *[5]float64, t float64) float64 {
|
||||
return c[1] + t*(2*c[2]+t*(3*c[3]+t*4*c[4]))
|
||||
}
|
||||
|
||||
// legendreD 写入 P0..P(n-1)(x);withD 时同时写入导数 P'k(x) 到 d。
|
||||
func legendreD(x float64, out, d []float64, withD bool) {
|
||||
out[0] = 1
|
||||
if len(out) > 1 {
|
||||
out[1] = x
|
||||
}
|
||||
for k := 2; k < len(out); k++ {
|
||||
out[k] = ((2*float64(k)-1)*x*out[k-1] - (float64(k)-1)*out[k-2]) / float64(k)
|
||||
}
|
||||
if !withD {
|
||||
return
|
||||
}
|
||||
d[0] = 0
|
||||
if len(d) > 1 {
|
||||
d[1] = 1
|
||||
}
|
||||
// P'k = P'(k-2) + (2k-1)·P(k-1)
|
||||
for k := 2; k < len(d); k++ {
|
||||
d[k] = d[k-2] + (2*float64(k)-1)*out[k-1]
|
||||
}
|
||||
}
|
||||
|
||||
func dot(c []float64, l []float64) float64 {
|
||||
s := 0.0
|
||||
for k, v := range c {
|
||||
s += v * l[k]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// PositionJ2000 地心月球 J2000 平黄道几何位置(km)/ geocentric Moon, J2000 mean ecliptic, km.
|
||||
// 可并发调用。
|
||||
func (m *Model) PositionJ2000(jdTT float64) [3]float64 {
|
||||
p, _ := m.eval(jdTT, false)
|
||||
return p
|
||||
}
|
||||
|
||||
// PositionVelocityJ2000 位置(km)与解析速度(km/日,对 TT)/ position (km) and analytic velocity (km/day).
|
||||
// 可并发调用。
|
||||
func (m *Model) PositionVelocityJ2000(jdTT float64) (pos, vel [3]float64) {
|
||||
return m.eval(jdTT, true)
|
||||
}
|
||||
|
||||
// eval 所有量先对 t(儒略世纪)求导,最后换算到每日。
|
||||
func (m *Model) eval(jdTT float64, wantVel bool) (pos, vel [3]float64) {
|
||||
s := m.getScratch()
|
||||
defer m.pool.Put(s)
|
||||
days := jdTT - j2000
|
||||
origin := days - jdTT
|
||||
daysLow := (jdTT - (days - origin)) - (j2000 + origin)
|
||||
t := days / century
|
||||
// tLow 保留历元相减与世纪除法的舍入,远离 J2000 时线性相位才不丢精度。
|
||||
tLow := (math.FMA(-t, century, days) + daysLow) / century
|
||||
x := (t + 20) / 100
|
||||
const dxdt = 0.01
|
||||
leg, dleg := s.leg, s.dleg
|
||||
// 递推前缀稳定,一次算到最长,S2 修正与各坐标长期项共用。
|
||||
legendreD(x, leg[:m.legLen], dleg[:m.legLen], wantVel)
|
||||
var dc, ddc [5]float64
|
||||
for j := range dc {
|
||||
dc[j] = dot(m.s2[j], leg)
|
||||
if wantVel {
|
||||
ddc[j] = dot(m.s2[j], dleg) * dxdt
|
||||
}
|
||||
}
|
||||
// ζ 随 W1 修正,D/F/l/l′ 各自修正
|
||||
var corr, dcorr, rate [fundCount]float64
|
||||
corr[0], corr[1], corr[2], corr[3], corr[12] = dc[1], dc[2], dc[3], dc[4], dc[0]
|
||||
dcorr[0], dcorr[1], dcorr[2], dcorr[3], dcorr[12] = ddc[1], ddc[2], ddc[3], ddc[4], ddc[0]
|
||||
for i := 0; i < fundCount; i++ {
|
||||
k := m.maxMult[i]
|
||||
if k == 0 {
|
||||
continue
|
||||
}
|
||||
rate[i] = dpoly(&m.args[i], t) + dcorr[i]
|
||||
sn, cs := math.Sincos(phase(&m.args[i], t, tLow) + corr[i])
|
||||
e := complex(cs, sn)
|
||||
p := s.pow[m.powOff[i]-k : m.powOff[i]+k+1]
|
||||
p[k] = 1
|
||||
for n := 1; n <= k; n++ {
|
||||
p[k+n] = cmul(p[k+n-1], e)
|
||||
p[k-n] = complex(real(p[k+n]), -imag(p[k+n]))
|
||||
}
|
||||
}
|
||||
// 热循环按 wantVel 拆开,纯位置路径不写 nodeRate
|
||||
node, factor := s.node, m.nodeFactor
|
||||
if wantVel {
|
||||
nr := s.nodeRate
|
||||
for n, parent := range m.nodeParent {
|
||||
fac := factor[n]
|
||||
f := s.pow[fac]
|
||||
r := m.slotMult[fac] * rate[m.slotFund[fac]]
|
||||
if parent >= 0 {
|
||||
f = cmul(node[parent], f)
|
||||
r += nr[parent]
|
||||
}
|
||||
node[n] = f
|
||||
nr[n] = r
|
||||
}
|
||||
} else {
|
||||
for n, parent := range m.nodeParent {
|
||||
f := s.pow[factor[n]]
|
||||
if parent >= 0 {
|
||||
f = cmul(node[parent], f)
|
||||
}
|
||||
node[n] = f
|
||||
}
|
||||
}
|
||||
env, denv := s.env[:m.maxEnv+1], s.denv[:m.maxEnv+1]
|
||||
legendreD(x, env, denv, wantVel)
|
||||
var v, dv [3]float64
|
||||
for c := 0; c < 3; c++ {
|
||||
co := &m.coords[c]
|
||||
sum := dot(co.secular, leg)
|
||||
dsum := 0.0
|
||||
if wantVel {
|
||||
dsum = dot(co.secular, dleg) * dxdt
|
||||
}
|
||||
f0 := co.flat0
|
||||
if wantVel {
|
||||
nr := s.nodeRate
|
||||
for j, n := range co.nodes0 {
|
||||
z := node[n]
|
||||
a, b := float64(f0[2*j]), float64(f0[2*j+1])
|
||||
sum += a*real(z) + b*imag(z)
|
||||
dsum += nr[n] * (b*real(z) - a*imag(z))
|
||||
}
|
||||
pos := 0
|
||||
for j, e := range co.envH {
|
||||
n := co.nodesH[j]
|
||||
z := node[n]
|
||||
var ac, as, dac, das float64
|
||||
for d := 0; d <= int(e); d++ {
|
||||
cc, cn := float64(co.coefH[pos]), float64(co.coefH[pos+1])
|
||||
ac += env[d] * cc
|
||||
as += env[d] * cn
|
||||
dac += denv[d] * cc
|
||||
das += denv[d] * cn
|
||||
pos += 2
|
||||
}
|
||||
sum += ac*real(z) + as*imag(z)
|
||||
dsum += dxdt*(dac*real(z)+das*imag(z)) + nr[n]*(as*real(z)-ac*imag(z))
|
||||
}
|
||||
} else {
|
||||
for j, n := range co.nodes0 {
|
||||
z := node[n]
|
||||
sum += float64(f0[2*j])*real(z) + float64(f0[2*j+1])*imag(z)
|
||||
}
|
||||
pos := 0
|
||||
for j, e := range co.envH {
|
||||
z := node[co.nodesH[j]]
|
||||
var ac, as float64
|
||||
for d := 0; d <= int(e); d++ {
|
||||
ac += env[d] * float64(co.coefH[pos])
|
||||
as += env[d] * float64(co.coefH[pos+1])
|
||||
pos += 2
|
||||
}
|
||||
sum += ac*real(z) + as*imag(z)
|
||||
}
|
||||
}
|
||||
v[c], dv[c] = sum, dsum
|
||||
}
|
||||
lon := v[0]*arcsec + phase(&m.lonPoly, t, tLow) + dc[0]
|
||||
lat := v[1] * arcsec
|
||||
rad := v[2]
|
||||
sl, cl := math.Sincos(lon)
|
||||
sb, cb := math.Sincos(lat)
|
||||
x0, y0, z0 := rad*cb*cl, rad*cb*sl, rad*sb
|
||||
p := poly(&m.p, t) * t
|
||||
q := poly(&m.q, t) * t
|
||||
ra := 2 * math.Sqrt(1-p*p-q*q)
|
||||
pos = [3]float64{
|
||||
(1-2*p*p)*x0 + 2*p*q*y0 + p*ra*z0,
|
||||
2*p*q*x0 + (1-2*q*q)*y0 - q*ra*z0,
|
||||
-p*ra*x0 + q*ra*y0 + (1-2*p*p-2*q*q)*z0,
|
||||
}
|
||||
if !wantVel {
|
||||
return pos, vel
|
||||
}
|
||||
dlon := dv[0]*arcsec + dpoly(&m.lonPoly, t) + ddc[0]
|
||||
dlat := dv[1] * arcsec
|
||||
drad := dv[2]
|
||||
// 球坐标 → 直角坐标的时间导数
|
||||
dx0 := drad*cb*cl - rad*sb*cl*dlat - rad*cb*sl*dlon
|
||||
dy0 := drad*cb*sl - rad*sb*sl*dlat + rad*cb*cl*dlon
|
||||
dz0 := drad*sb + rad*cb*dlat
|
||||
// 历元黄道 → J2000 旋转矩阵本身随 P、Q 变化
|
||||
dp := dpoly(&m.p, t)*t + poly(&m.p, t)
|
||||
dq := dpoly(&m.q, t)*t + poly(&m.q, t)
|
||||
dra := -4 * (p*dp + q*dq) / ra
|
||||
dpq := 2 * (dp*q + p*dq)
|
||||
vel = [3]float64{
|
||||
(1-2*p*p)*dx0 + 2*p*q*dy0 + p*ra*dz0 +
|
||||
-4*p*dp*x0 + dpq*y0 + (dp*ra+p*dra)*z0,
|
||||
2*p*q*dx0 + (1-2*q*q)*dy0 - q*ra*dz0 +
|
||||
dpq*x0 - 4*q*dq*y0 - (dq*ra+q*dra)*z0,
|
||||
-p*ra*dx0 + q*ra*dy0 + (1-2*p*p-2*q*q)*dz0 +
|
||||
-(dp*ra+p*dra)*x0 + (dq*ra+q*dra)*y0 - 4*(p*dp+q*dq)*z0,
|
||||
}
|
||||
for i := range vel {
|
||||
vel[i] /= century
|
||||
}
|
||||
return pos, vel
|
||||
}
|
||||
|
||||
// interleave 把按阶数分块(每阶先 cos 块后 sin 块)的系数重排为逐项 [d0cos,d0sin,d1cos,…]。
|
||||
func interleave(env []uint8, raw []float32) []float32 {
|
||||
out := make([]float32, len(raw))
|
||||
start := make([]int, len(env))
|
||||
for j := 1; j < len(env); j++ {
|
||||
start[j] = start[j-1] + 2*(int(env[j-1])+1)
|
||||
}
|
||||
pos := 0
|
||||
for d := 0; ; d++ {
|
||||
var idx []int
|
||||
for j, e := range env {
|
||||
if int(e) >= d {
|
||||
idx = append(idx, j)
|
||||
}
|
||||
}
|
||||
if len(idx) == 0 {
|
||||
break
|
||||
}
|
||||
for n, j := range idx {
|
||||
out[start[j]+2*d] = raw[pos+n]
|
||||
out[start[j]+2*d+1] = raw[pos+len(idx)+n]
|
||||
}
|
||||
pos += 2 * len(idx)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// 被测契约:内置表与参考向量一致(< 0.1 m),解析速度与差分一致,可并发求值。
|
||||
package lunar
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func sq(x float64) float64 { return x * x }
|
||||
|
||||
func TestMatchesReferenceVectors(t *testing.T) { checkVectors(t, Default(), "testdata/vectors.csv") }
|
||||
|
||||
func TestFastMatchesReferenceVectors(t *testing.T) { checkVectors(t, Fast(), "testdata/fast.csv") }
|
||||
|
||||
func checkVectors(t *testing.T, m *Model, path string) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
sc := bufio.NewScanner(f)
|
||||
n, worst := 0, 0.0
|
||||
for sc.Scan() {
|
||||
parts := strings.Split(sc.Text(), ",")
|
||||
var v [4]float64
|
||||
for i := range v {
|
||||
if v[i], err = strconv.ParseFloat(parts[i], 64); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
got := m.PositionJ2000(v[0])
|
||||
if d := math.Sqrt(sq(got[0]-v[1]) + sq(got[1]-v[2]) + sq(got[2]-v[3])); d > worst {
|
||||
worst = d
|
||||
}
|
||||
n++
|
||||
}
|
||||
if n < 40 {
|
||||
t.Fatalf("only %d vectors", n)
|
||||
}
|
||||
if worst > 1e-4 {
|
||||
t.Fatalf("max diff %.3e km over %d vectors", worst, n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVelocityMatchesFiniteDifference(t *testing.T) {
|
||||
m := Default()
|
||||
const h = 0.05
|
||||
worst := 0.0
|
||||
for _, jd := range []float64{990545.5, 1500000.25, 2299160.5, 2451545.0, 2460676.5, 2816787.5, 3912545.0} {
|
||||
p, v := m.PositionVelocityJ2000(jd)
|
||||
if q := m.PositionJ2000(jd); q != p {
|
||||
t.Fatalf("jd=%.1f position mismatch", jd)
|
||||
}
|
||||
a, b := m.PositionJ2000(jd+h), m.PositionJ2000(jd-h)
|
||||
a2, b2 := m.PositionJ2000(jd+2*h), m.PositionJ2000(jd-2*h)
|
||||
for i := range v {
|
||||
fd := (8*(a[i]-b[i]) - (a2[i] - b2[i])) / (12 * h)
|
||||
if d := math.Abs(fd - v[i]); d > worst {
|
||||
worst = d
|
||||
}
|
||||
}
|
||||
}
|
||||
if worst > 2e-3 {
|
||||
t.Fatalf("max velocity diff %.3e km/day", worst)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRejectsTruncated(t *testing.T) {
|
||||
for _, n := range []int{0, 3, 5, 40, len(defaultTable) / 2, len(defaultTable) - 1} {
|
||||
if _, err := Parse(defaultTable[:n]); err == nil {
|
||||
t.Errorf("len %d: expected error", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentEvaluation(t *testing.T) {
|
||||
m := Default()
|
||||
const n = 64
|
||||
var want [n][3]float64
|
||||
for i := range want {
|
||||
want[i] = m.PositionJ2000(2451545.0 + float64(i)*13.7)
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
var bad [8]bool
|
||||
for g := 0; g < 8; g++ {
|
||||
wg.Add(1)
|
||||
go func(g int) {
|
||||
defer wg.Done()
|
||||
for k := 0; k < n; k++ {
|
||||
i := (k + g*7) % n
|
||||
if m.PositionJ2000(2451545.0+float64(i)*13.7) != want[i] {
|
||||
bad[g] = true
|
||||
}
|
||||
}
|
||||
}(g)
|
||||
}
|
||||
wg.Wait()
|
||||
for g, b := range bad {
|
||||
if b {
|
||||
t.Fatalf("goroutine %d saw a mismatch", g)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPositionJ2000(b *testing.B) {
|
||||
m := Default()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = m.PositionJ2000(2460000.5 + float64(i%10000)*0.37)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
-1931455,-263046.57528520969,297684.18718319025,-24345.864066160168
|
||||
-1785355,24378.534686367391,-400825.36392118747,-44672.568525917093
|
||||
-1639255,221175.99144970218,325206.96587989992,-14472.844556519216
|
||||
-1493155,-319978.52720147866,-179819.4818149425,-7729.6582925465245
|
||||
-1347055,362622.46972198499,20286.814075592443,4137.3575503146285
|
||||
-1200955,-346235.18691354321,185914.73322113504,30174.27456151383
|
||||
-1054855,171052.68131930399,-366289.65810158779,26232.886379638556
|
||||
-908755,98623.637690714822,376297.77810421737,40802.587272379009
|
||||
-762655,-253665.11515733672,-272838.23852057324,19911.166048921259
|
||||
-616555,335485.16883375292,146456.59956479297,14870.504114287725
|
||||
-470455,-383645.40573639271,65997.21790989369,-826.92611041679697
|
||||
-324355,276927.4702533714,-295829.19680496032,-30322.013752343068
|
||||
-178255,-26435.785915690412,386079.73800098576,-28015.326875332928
|
||||
-32155,-173562.98376470321,-327340.8539868113,-37468.408479997677
|
||||
113945,284098.82846766489,239256.46597111417,-25343.748850755001
|
||||
260045,-385157.83176014025,-49369.527013191553,-15281.761574469898
|
||||
406145,345160.97807879257,-214636.8383361497,3414.5771813550259
|
||||
552245,-134108.10483846199,361980.9990905014,29968.389469456466
|
||||
698345,-83984.239959958926,-355799.14247223095,28817.078112816904
|
||||
844445,223226.00566205085,299354.17357796693,36909.979945451472
|
||||
990545,-362607.45994305814,-147502.75304507115,28081.953043893249
|
||||
1136645,383606.60880971845,-132531.90493783797,8832.002235690501
|
||||
1282745,-216360.67463345666,321769.59784516064,-8957.0037364521304
|
||||
1428845,7433.8731166889338,-359577.9208704764,-27286.722728085369
|
||||
1574945,157042.40898349817,338967.54339773324,-30844.406074648126
|
||||
1721045,-328371.10510008538,-221529.93877308263,-36739.865212875557
|
||||
1867145,400234.08615918033,-57984.149689135251,-23513.254374541601
|
||||
2013245,-274021.09564645251,273929.27390877635,-2133.8352285152291
|
||||
2159345,92791.766991493409,-345234.67832922813,12342.071461675338
|
||||
2305445,88338.286030037401,363169.93015197461,26462.650120618968
|
||||
2451545,-291607.24183591013,-274978.62186731066,36267.30787189916
|
||||
2597645,401391.47803711233,3266.8554273079903,31443.354743021413
|
||||
2743745,-312245.28483573761,223268.80530781401,14449.195688692611
|
||||
2889845,167261.68852763134,-317547.01628260425,854.67113282198795
|
||||
3035945,22710.051466743793,377536.27831278293,-18254.778814458063
|
||||
3182045,-258568.79979545044,-310953.08002426871,-31511.794215090176
|
||||
3328145,391597.73715416581,50492.738155701489,-35663.250569429547
|
||||
3474245,-336640.78667349386,169381.01759883287,-24138.799220522917
|
||||
3620345,229894.62683911348,-283245.2596621954,-10124.478719948
|
||||
3766445,-30725.076385326527,386554.31412064168,4630.937622600225
|
||||
3912545,-232347.00044598628,-331529.30967142241,31692.736681196671
|
||||
4058645,373153.27734119527,88709.654120304229,33189.966274715975
|
||||
4204745,-352665.5780016224,110675.94346091514,29168.298060724752
|
||||
4350845,279862.12418001605,-250761.84026383076,22491.093918623097
|
||||
4496945,-63300.801294757643,393827.49900787073,-4549.380602734378
|
||||
4643045,-211639.87281130059,-339806.65624159103,-18216.154066961921
|
||||
4789145,345869.06789046078,125142.00856319141,-35847.90651925862
|
||||
4935245,-365917.02341126435,47072.64876266574,-31135.795917215175
|
||||
5081345,313235.01489341311,-233557.38203767358,-25005.730334995031
|
||||
5227445,-71563.036314018507,398823.35837659024,-9633.2749911986139
|
||||
5373545,-186164.91514019907,-338423.50248531549,24637.566759055353
|
||||
|
@@ -0,0 +1,51 @@
|
||||
-1931455,-263012.14372776932,297721.55430688168,-24324.987254599062
|
||||
-1785355,24414.613429926769,-400822.30277713871,-44670.197726442522
|
||||
-1639255,221200.37674641001,325184.92725227826,-14474.943560636351
|
||||
-1493155,-319973.35559277143,-179836.67922932078,-7722.0931910639329
|
||||
-1347055,362620.6275818554,20261.340424064758,4158.7475902214901
|
||||
-1200955,-346245.11238489667,185898.25815803016,30163.905893696552
|
||||
-1054855,171029.90424524632,-366296.60906562442,26231.502494924491
|
||||
-908755,98585.018472548079,376301.83738717507,40804.120474682692
|
||||
-762655,-253673.35964539216,-272828.69776402361,19905.706493336733
|
||||
-616555,335481.25258198346,146459.68939852307,14858.106694331284
|
||||
-470455,-383647.05975822004,66013.008896738858,-829.53397079068804
|
||||
-324355,276926.62049089983,-295829.60577838111,-30320.482347503283
|
||||
-178255,-26445.54739251201,386080.83489992673,-28013.881581822534
|
||||
-32155,-173564.14468521808,-327344.00872558804,-37468.46077107366
|
||||
113945,284090.35252503335,239265.58618684916,-25343.650846110519
|
||||
260045,-385159.51631046517,-49348.816915762458,-15277.391684530896
|
||||
406145,345165.46504725312,-214639.96530993938,3409.4271382105871
|
||||
552245,-134100.72888107621,361985.48449367564,29978.850367708317
|
||||
698345,-83982.342926013938,-355811.46501246258,28808.215852912752
|
||||
844445,223235.08660139333,299351.08166803332,36915.189665281585
|
||||
990545,-362602.7882248871,-147511.51703076772,28082.07356267815
|
||||
1136645,383604.91929247783,-132547.95810837013,8838.4387429826493
|
||||
1282745,-216364.57090490102,321764.38059364131,-8961.5757619206961
|
||||
1428845,7411.8790805348717,-359585.32555476134,-27293.192427568905
|
||||
1574945,157039.98810843128,338969.64958912093,-30843.917066599242
|
||||
1721045,-328370.6008870838,-221528.04782516821,-36744.16639076647
|
||||
1867145,400234.16277750907,-57976.122899650822,-23521.520092170747
|
||||
2013245,-274016.4330496171,273934.67297238286,-2129.3720209763578
|
||||
2159345,92794.113822126208,-345233.42375816189,12336.461166121979
|
||||
2305445,88335.023380869083,363166.75449031434,26461.650624866372
|
||||
2451545,-291608.37627978966,-274979.69324404577,36271.128877822775
|
||||
2597645,401391.20135964605,3263.1459841862388,31451.825037924391
|
||||
2743745,-312251.31017224718,223260.77557797215,14444.457253400202
|
||||
2889845,167249.48724887962,-317556.36523747904,861.57897404768664
|
||||
3035945,22695.663788870559,377539.04920572782,-18246.340165569305
|
||||
3182045,-258575.29689052334,-310947.48117673211,-31511.022415741383
|
||||
3328145,391596.12233842851,50493.68541584225,-35666.970403817082
|
||||
3474245,-336640.9610806547,169391.52518719292,-24128.503619066782
|
||||
3620345,229900.52348247898,-283241.08259236073,-10118.936107852864
|
||||
3766445,-30703.555025148959,386554.4143357625,4624.3092540068592
|
||||
3912545,-232340.39127966101,-331532.73305540072,31692.97586258821
|
||||
4058645,373158.78561370028,88690.555206671619,33189.3675445954
|
||||
4204745,-352676.95412928553,110654.5265917302,29168.430885959289
|
||||
4350845,279850.74386479775,-250759.61582796593,22465.457479309545
|
||||
4496945,-63304.248176228684,393828.06630098209,-4545.7108401102751
|
||||
4643045,-211635.86742416851,-339798.65198398969,-18217.631416908665
|
||||
4789145,345881.5840039468,125117.11976231023,-35854.923632790691
|
||||
4935245,-365925.61155116902,47053.327116662622,-31140.397995070532
|
||||
5081345,313238.78055125696,-233551.4275320005,-25002.984985012321
|
||||
5227445,-71571.816487501972,398827.95987496921,-9638.4140099458273
|
||||
5373545,-186177.32558991387,-338413.93355647428,24655.523094058852
|
||||
|
Reference in New Issue
Block a user