// Package planets 行星半解析级数运行时(PLN1/PLN2/PLN3 表,日心,J2000 平黄道)。 package planets import ( "encoding/binary" "errors" "math" "sync" ) const ( j2000 = 2451545.0 millennium = 365250.0 blendDays = 36525.0 auKM = 149597870.691 tMid = -2.0 tHalf = 10.0 emrat = 81.3005682214972 gmSol = 2.9591220836841438269e-04 maxDeg = 24 ) // Body 编号 / body id;3 为地月质心。 const ( Mercury = 1 Venus = 2 EMB = 3 Mars = 4 Jupiter = 5 Saturn = 6 Uranus = 7 Neptune = 8 ) var ci0 = [17]float64{4.402608631669, 3.176134461576, 1.753470369433, 6.203500014141, 4.091360003050, 1.713740719173, 5.598641292287, 2.805136360408, 2.326989734620, 0.5995461070350, 0.8740185101070, 5.481225395663, 5.311897933164, 0.0, 5.19846640063, 1.62790513602, 2.35555563875} var ci1 = [17]float64{26087.90314068555, 10213.28554743445, 6283.075850353215, 3340.612434145457, 1731.170452721855, 1704.450855027201, 1428.948917844273, 1364.756513629990, 1361.923207632842, 529.6909615623250, 213.2990861084880, 74.78165903077800, 38.13297222612500, 0.3595362285049309, 77713.7714481804, 84334.6615717837, 83286.9142477147} var gmp = [8]float64{4.9125474514508118699e-11, 7.2434524861627027000e-10, 8.9970116036316091182e-10, 9.5495351057792580598e-11, 2.8253458420837780000e-07, 8.4597151856806587398e-08, 1.2920249167819693900e-08, 1.5243589007842762800e-08} // topDMu 必须逐步按 float64 舍入(与 Python 参考求值一致),不能写成常量表达式。 var topDMu = func() float64 { f0, f1, d := 529.6909622785881, 213.2990811942489, 880.0 return (f0 - f1) / d }() // skyToVSOP skyfield ecliptic_J2000 → VSOP2013 动力学黄道;求值时用其转置。 var skyToVSOP = func() [3][3]float64 { as := 180 / math.Pi * 3600 return mul(transpose(mul(rz(-0.05188/as), rx(84381.41136/as))), rx(84381.448/as)) }() // element 的项按包络阶降序排列,第 d 阶只覆盖前 cnt[d] 项。 type element struct { secular []float64 arg []int32 cnt []int c, s [][]float64 } const ( maxKeys = 8192 maxTab = 512 topBits = 6 // 超出此值的幅角乘数必然撑破幂表,先在 int64 下拒绝,保证转 int 不溢出。 maxKeyAbs = maxTab << (2 * topBits) ) var errBadKey = errors.New("planets: argument multiplier out of range") // factor 幂表槽位;neg 表示取共轭。 type factor struct { slot int32 neg bool } type body struct { top bool f, l0 float64 rgm float64 // 幂表:VSOP 每个基本幅角一段,TOP 每个 6 位数字层一段 tabMax [17]int tabOff [17]int keyOff []int32 factors []factor keyNeg []bool el [6]element // legDeg 长期项与包络用到的最高 Legendre 阶。 legDeg int } // Model 已解析的 PLN1 表,只读、可并发求值 / parsed PLN1 table, safe for concurrent use. type Model struct { bodies [9]*body modernBodies [9]*body modernStart float64 modernEnd float64 } type reader struct { b []byte p int err error } func (r *reader) take(n int) []byte { if r.err != nil || r.p+n > len(r.b) { r.err = errors.New("planets: truncated table") return make([]byte, n) } s := r.b[r.p : r.p+n] r.p += n return s } func (r *reader) u8() uint8 { return r.take(1)[0] } 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))) } func (r *reader) uvar() uint64 { n, shift := uint64(0), uint(0) for { c := r.u8() n |= uint64(c&0x7F) << shift shift += 7 if c < 0x80 || r.err != nil || shift > 56 { return n } } } func (r *reader) zvar() int64 { z := r.uvar() if z%2 == 0 { return int64(z >> 1) } return -int64(z>>1) - 1 } // Parse 解析 PLN1 表 / parse a PLN1 table. func Parse(b []byte) (*Model, error) { bodies, err := parseBodies(b) if err != nil { return nil, err } return &Model{bodies: bodies}, nil } // WithModernSegment 为长时段表叠加现代段表,[jdStart, jdEnd] 内优先使用 / add a modern-era segment used inside [jdStart, jdEnd]. func (m *Model) WithModernSegment(b []byte, jdStart, jdEnd float64) error { bodies, err := parseBodies(b) if err != nil { return err } m.modernBodies, m.modernStart, m.modernEnd = bodies, jdStart, jdEnd return nil } func parseBodies(b []byte) (bodies [9]*body, err error) { r := &reader{b: b} magic := string(r.take(4)) if magic != "PLN1" && magic != "PLN2" && magic != "PLN3" { return bodies, errors.New("planets: bad magic") } quant, packed := magic == "PLN2", magic == "PLN3" for n := int(r.u8()); n > 0 && r.err == nil; n-- { id := int(r.u8()) if id < 1 || id > 8 { return bodies, errors.New("planets: bad body id") } bd := &body{top: r.u8() != 0} secDeg := int(r.u8()) bd.f, bd.l0 = r.f64(), r.f64() bd.rgm = math.Sqrt(gmp[id-1] + gmSol) nk := r.uvar() if nk > maxKeys { return bodies, errors.New("planets: too many arguments") } if err := readKeys(r, bd, int(nk)); err != nil { return bodies, err } bd.legDeg = secDeg sortedArgs := packed && r.u8() != 0 for v := range bd.el { var err error if packed { err = readElement3(r, &bd.el[v], secDeg, int(nk), sortedArgs) } else { err = readElement(r, &bd.el[v], secDeg, int(nk), quant) } if err != nil { return bodies, err } if d := len(bd.el[v].cnt) - 1; d > bd.legDeg { bd.legDeg = d } } bodies[id] = bd } if r.err != nil { return bodies, r.err } if r.p != len(b) { return bodies, errors.New("planets: trailing bytes") } return bodies, nil } // readKeys 把整数幅角拆成幂表槽位:VSOP 按基本幅角升序,TOP 按 6 位数字层由低到高。 func readKeys(r *reader, bd *body, nk int) error { type raw struct{ fund, k int } keys := make([][]raw, nk) bd.keyNeg = make([]bool, nk) for j := 0; j < nk && r.err == nil; j++ { if bd.top { k := r.zvar() if k > maxKeyAbs || k < -maxKeyAbs { return errBadKey } a := int(k) if a < 0 { a, bd.keyNeg[j] = -a, true } for lvl := 0; lvl < 3; lvl++ { d := a >> uint(topBits*lvl) if lvl < 2 { d &= 1<>uint(i)&1 != 0 { k := r.zvar() if k > maxKeyAbs || k < -maxKeyAbs { return errBadKey } keys[j] = append(keys[j], raw{i, int(k)}) } } } if r.err != nil { return r.err } for _, ks := range keys { for _, f := range ks { a := f.k if a < 0 { a = -a } if a > bd.tabMax[f.fund] { bd.tabMax[f.fund] = a } } } n := 0 for i := range bd.tabMax { bd.tabOff[i] = n if bd.tabMax[i] > 0 || (bd.top && i < 3) { n += bd.tabMax[i] + 1 } } if n > maxTab { return errors.New("planets: power table too large") } bd.keyOff = make([]int32, nk+1) for j, ks := range keys { for _, f := range ks { a, neg := f.k, false if a < 0 { a, neg = -a, true } if !bd.top && a == 0 { continue } bd.factors = append(bd.factors, factor{int32(bd.tabOff[f.fund] + a), neg}) } bd.keyOff[j+1] = int32(len(bd.factors)) } return nil } // readElement 把文件里按包络阶分块的列序重排为逐项连续;quant 为 PLN2 整数量化系数。 func readElement(r *reader, e *element, secDeg, nk int, quant bool) error { if secDeg > maxDeg { return errors.New("planets: secular degree too high") } n64 := r.uvar() if n64 > uint64(nk) { return errors.New("planets: too many terms") } nint := int(n64) e.secular = make([]float64, secDeg+1) for i := range e.secular { e.secular[i] = r.f64() } args, env := make([]int32, nint), make([]int, nint) top := 0 for j := 0; j < nint; j++ { a := r.uvar() if a >= uint64(nk) { return errors.New("planets: argument index out of range") } args[j], env[j] = int32(a), int(r.u8()) if env[j] > maxDeg { return errors.New("planets: envelope degree too high") } if env[j] > top { top = env[j] } } // 按包络阶降序的稳定排序:pos[j] 为文件第 j 项的新位置 pos := make([]int, nint) e.arg = make([]int32, nint) k := 0 for d := top; d >= 0; d-- { for j := 0; j < nint; j++ { if env[j] == d { pos[j], e.arg[k] = k, args[j] k++ } } } coef := func() float64 { return float64(r.f32()) } if quant { step := r.f64() coef = func() float64 { return float64(r.zvar()) * step } } if nint == 0 { return r.err } e.cnt = make([]int, top+1) e.c, e.s = make([][]float64, top+1), make([][]float64, top+1) for d := 0; d <= top; d++ { for j := 0; j < nint; j++ { if env[j] >= d { e.cnt[d]++ } } e.c[d], e.s[d] = make([]float64, e.cnt[d]), make([]float64, e.cnt[d]) for _, dst := range [][]float64{e.c[d], e.s[d]} { for j := 0; j < nint; j++ { if env[j] >= d { dst[pos[j]] = coef() } } } } return r.err } // Heliocentric 日心位置,km,J2000 平黄道 / heliocentric position, km, mean ecliptic J2000. func (m *Model) Heliocentric(id int, jdTDB float64) ([3]float64, error) { pos, _, err := m.state(id, jdTDB, false) return pos, err } // HeliocentricState 日心位置(km)与密切速度(km/日),J2000 平黄道 / heliocentric position and osculating velocity. func (m *Model) HeliocentricState(id int, jdTDB float64) (pos, vel [3]float64, err error) { return m.state(id, jdTDB, true) } // state wantVel 为 false 时 vel 不计算、恒为零。 func (m *Model) state(id int, jdTDB float64, wantVel bool) (pos, vel [3]float64, err error) { if id < 1 || id > 8 { return pos, vel, errors.New("planets: body not in table") } bd := m.bodies[id] if bd == nil { return pos, vel, errors.New("planets: body not in table") } t := (jdTDB - j2000) / millennium md := m.modernBodies[id] sky := func(p, v [3]float64) ([3]float64, [3]float64, error) { if !wantVel { return toSky(p), v, nil } return toSky(p), toSky(v), nil } if md == nil || jdTDB <= m.modernStart || jdTDB >= m.modernEnd { p, v := bd.ellxyz(bd.elements(t), wantVel) return sky(p, v) } w, dw := m.modernWeight(jdTDB) pm, vm := md.ellxyz(md.elements(t), wantVel) if w == 1 { return sky(pm, vm) } pl, vl := bd.ellxyz(bd.elements(t), wantVel) for i := range pos { pos[i] = pl[i] + w*(pm[i]-pl[i]) if wantVel { vel[i] = vl[i] + w*(vm[i]-vl[i]) + dw*(pm[i]-pl[i]) } } return sky(pos, vel) } // modernWeight 现代段权重及其对 JD 的导数;两端各 blendDays 内用 smoothstep 过渡,位置与速度均连续。 func (m *Model) modernWeight(jd float64) (w, dw float64) { u, sign := (jd-m.modernStart)/blendDays, 1.0 if e := (m.modernEnd - jd) / blendDays; e < u { u, sign = e, -1 } if u >= 1 { return 1, 0 } return u * u * (3 - 2*u), sign * 6 * u * (1 - u) / blendDays } // toSky 动力学黄道 au → skyfield ecliptic_J2000 km。 func toSky(p [3]float64) (out [3]float64) { for i := range out { out[i] = (skyToVSOP[0][i]*p[0] + skyToVSOP[1][i]*p[1] + skyToVSOP[2][i]*p[2]) * auKM } return out } // cmul 显式转换阻止编译器融合乘加,保证与 Python 参考求值逐位一致。 func cmul(ar, ai, br, bi float64) (float64, float64) { return float64(ar*br) - float64(ai*bi), float64(ar*bi) + float64(ai*br) } // phasors 填充每个幅角的 (cos, sin)。 // tc、ts 为倍角表工作区,只读取本次写入的槽位,复用时不必清零。 func (bd *body) phasors(t float64, pc, ps, tc, ts []float64) { for i := range bd.tabMax { if bd.tabMax[i] == 0 && !(bd.top && i < 3) { continue } var s, c float64 if bd.top { s, c = math.Sincos(topDMu * t * float64(int(1)< 0 { r, im = tc[fs[0].slot], ts[fs[0].slot] if fs[0].neg { im = -im } fs = fs[1:] } for _, f := range fs { br, bi := tc[f.slot], ts[f.slot] if f.neg { bi = -bi } r, im = cmul(r, im, br, bi) } if bd.keyNeg[j] { im = -im } pc[j], ps[j] = r, im } } type scratch struct{ buf []float64 } var scratchPool = sync.Pool{New: func() interface{} { return new(scratch) }} // elements t 为 TDB 千年(J2000 起算)。 func (bd *body) elements(t float64) [6]float64 { x := (t - tMid) / tHalf var leg [maxDeg + 1]float64 leg[0], leg[1] = 1, x for k := 2; k <= bd.legDeg; k++ { leg[k] = (leg[k-1]*x*float64(2*k-1) - leg[k-2]*float64(k-1)) / float64(k) } nk := len(bd.keyNeg) sc := scratchPool.Get().(*scratch) defer scratchPool.Put(sc) if cap(sc.buf) < 2*nk+2*maxTab { sc.buf = make([]float64, 2*nk+2*maxTab) } // 各段在读取前都会被完整写入,复用时不必清零。 buf := sc.buf[:2*nk+2*maxTab] pc, ps := buf[:nk], buf[nk:2*nk] bd.phasors(t, pc, ps, buf[2*nk:2*nk+maxTab], buf[2*nk+maxTab:]) var el [6]float64 for v := range bd.el { e := &bd.el[v] sum := 0.0 for i, c := range e.secular { sum += c * leg[i] } // 逐阶做点积再乘 Legendre 值,避免逐项累加包络系数的读改写。 for d := range e.cnt { cd, sd := e.c[d], e.s[d] arg := e.arg[:len(cd)] s0, s1 := 0.0, 0.0 for i, a := range arg { s0 += cd[i] * pc[a] s1 += sd[i] * ps[a] } sum += leg[d] * (s0 + s1) } el[v] = sum } // 先归约平经度再加周期和,与 Python 参考求值一致。 lm := math.Mod(bd.l0+bd.f*t, 2*math.Pi) if lm < 0 { lm += 2 * math.Pi } el[1] += lm return el } // EarthFromEMB 由地月质心与地心月球合成物理地心 / Earth centre from EMB and geocentric Moon. func EarthFromEMB(emb, moon [3]float64) [3]float64 { k := 1 / (1 + emrat) return [3]float64{emb[0] - moon[0]*k, emb[1] - moon[1]*k, emb[2] - moon[2]*k} } // ellxyz VSOP2013 椭圆元件 (a,l,k,h,q,p) → 动力学黄道位置(au)与速度(au/日)。 func (bd *body) ellxyz(el [6]float64, wantVel bool) (pos, vel [3]float64) { a, l, k, h, q, p := el[0], el[1], el[2], el[3], el[4], el[5] fi := math.Sqrt(1 - k*k - h*h) ki := math.Sqrt(1 - q*q - p*p) u := 1 / (1 + fi) ex := math.Hypot(k, h) gl := math.Mod(l, 2*math.Pi) if gl < 0 { gl += 2 * math.Pi } gm := gl - math.Atan2(h, k) e := gl + (ex-0.125*ex*ex*ex)*math.Sin(gm) + 0.5*ex*ex*math.Sin(2*gm) + 0.375*ex*ex*ex*math.Sin(3*gm) var zs, zc float64 for it := 0; it < 20; it++ { zs, zc = math.Sincos(e) z3r, z3i := k*zc+h*zs, k*zs-h*zc dl := gl - e + z3i e += dl / (1 - z3r) if math.Abs(dl) < 1e-15 { break } } zs, zc = math.Sincos(e) z3r, z3i := k*zc+h*zs, k*zs-h*zc rsa := 1 - z3r z1r, z1i := u*k*z3i, u*h*z3i z2r, z2i := z1i, -z1r xcw := (-k + zc + z2r) / rsa xsw := (-h + zs + z2i) / rsa xm := p*xcw - q*xsw xr := a * rsa pos = [3]float64{xr * (xcw - 2*p*xm), xr * (xsw + 2*q*xm), -2 * xr * ki * xm} if !wantVel { return pos, vel } xms := a * (h + xsw) / fi xmc := a * (k + xcw) / fi xn := bd.rgm / math.Pow(a, 1.5) vel = [3]float64{xn * ((2*p*p-1)*xms + 2*p*q*xmc), xn * ((1-2*q*q)*xmc - 2*p*q*xms), 2 * xn * ki * (p*xms + q*xmc)} return pos, vel } func rx(a float64) [3][3]float64 { s, c := math.Sincos(a) return [3][3]float64{{1, 0, 0}, {0, c, -s}, {0, s, c}} } func rz(a float64) [3][3]float64 { s, c := math.Sincos(a) return [3][3]float64{{c, -s, 0}, {s, c, 0}, {0, 0, 1}} } func mul(a, b [3][3]float64) (out [3][3]float64) { for i := 0; i < 3; i++ { for j := 0; j < 3; j++ { for k := 0; k < 3; k++ { out[i][j] += a[i][k] * b[k][j] } } } return out } func transpose(a [3][3]float64) (out [3][3]float64) { for i := 0; i < 3; i++ { for j := 0; j < 3; j++ { out[i][j] = a[j][i] } } return out }