Files
astro/planet/internal/semianalytic/lunar/lunar.go
T
b612 044f2f4381 feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算
- 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询
- 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出
- 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解
- 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤
- 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持
- 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理
- 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
2026-10-15 23:54:15 +08:00

514 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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
}