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
+191
View File
@@ -0,0 +1,191 @@
package basic
import "math"
// OrbitalEventKind 轨道事件类型 / orbital event kind.
type OrbitalEventKind int
const (
OrbitalPerihelion OrbitalEventKind = iota // 近日点 / perihelion
OrbitalAphelion // 远日点 / aphelion
OrbitalAscendingNode // 升交点 / ascending node
OrbitalDescendingNode // 降交点 / descending node
)
// OrbitalEvent 轨道事件:TT 儒略日与日心距(AU)/ orbital event at TT JDE with heliocentric distance in AU.
type OrbitalEvent struct {
JDE float64
Kind OrbitalEventKind
DistanceAU float64
}
// 恒星周期(日),只用于定采样步长和支配窗口。
var orbitalPeriodDays = map[SkyBody]float64{
SkyMercury: 87.969, SkyVenus: 224.701, SkyMars: 686.98, SkyJupiter: 4332.59,
SkySaturn: 10759.22, SkyUranus: 30688.5, SkyNeptune: 60182,
}
// SkyEarth 地球,只用于近日点、远日点查询 / Earth, valid only for apsis queries.
const SkyEarth SkyBody = -1
func orbitalIndex(body SkyBody) (int, float64, bool) {
if body == SkyEarth {
return -1, 365.256, true
}
period, ok := orbitalPeriodDays[body]
if !ok {
return 0, 0, false
}
return int(body-SkyMercury) + 1, period, true
}
// PlanetApsidesWithInstance 行星或地球(SkyEarth)在 [startJDE, endJDE] 内的近日点与远日点(TT)/ perihelia and aphelia in the TT window.
//
// 极值取日心距本身;太阳绕质心的反射运动会让天王星、海王星的日心距在近日点附近出现多个小极值,
// 这里只保留前后 0.3 个周期内最深的一个,与吻切轨道近日点可相差数年。
func PlanetApsidesWithInstance(body SkyBody, startJDE, endJDE float64, inst *Instance) []OrbitalEvent {
index, period, ok := orbitalIndex(body)
if !ok || !finite(startJDE) || !finite(endJDE) || endJDE < startJDE {
return nil
}
radius := func(t float64) float64 { return PlanetCoordinateNWithInstance(index, 2, t, -1, inst) }
step := period / 200
from, to := startJDE-period/2, endJDE+period/2
// 与 skyGrid 同一上限,同时挡住 to-from 溢出为 Inf。
ratio := math.Ceil((to - from) / step)
if !(ratio < skyGridMaxIntervals) {
return nil
}
n := int(ratio) + 1
window := int(math.Ceil(0.3 * period / step))
ring := make([]float64, 2*window+1)
sample := func(j int) float64 { return ring[j%len(ring)] }
var out []OrbitalEvent
check := func(i int) {
for _, aphelion := range []bool{false, true} {
lo, hi := i-window, i+window
if lo < 0 {
lo = 0
}
if hi > n-1 {
hi = n - 1
}
if !orbitalDominant(sample, i, lo, hi, aphelion) {
continue
}
center := from + float64(i)*step
t, r := orbitalGoldenExtremum(radius, center-step, center+step, aphelion)
if t < startJDE || t > endJDE {
continue
}
kind := OrbitalPerihelion
if aphelion {
kind = OrbitalAphelion
}
out = append(out, OrbitalEvent{JDE: t, Kind: kind, DistanceAU: r})
}
}
for k := 0; k < n; k++ {
ring[k%len(ring)] = radius(from + float64(k)*step)
if i := k - window; i >= 1 && i <= n-2 {
check(i)
}
}
tail := n - window
if tail < 1 {
tail = 1
}
for i := tail; i <= n-2; i++ {
check(i)
}
return out
}
// orbitalDominant 报告第 i 个样本是否为 [lo, hi] 内的唯一最值(并列时只取最左侧)。
func orbitalDominant(value func(int) float64, i, lo, hi int, maximize bool) bool {
better := func(a, b float64) bool {
if maximize {
return a > b
}
return a < b
}
vi := value(i)
for j := lo; j <= hi; j++ {
if j == i {
continue
}
if vj := value(j); better(vj, vi) || (vj == vi && j < i) {
return false
}
}
return !math.IsNaN(vi)
}
func orbitalGoldenExtremum(f func(float64) float64, a, b float64, maximize bool) (float64, float64) {
sign := 1.0
if maximize {
sign = -1
}
const ratio = 0.6180339887498949
c, d := b-ratio*(b-a), a+ratio*(b-a)
fc, fd := sign*f(c), sign*f(d)
for i := 0; i < 100 && b-a > 1e-5; i++ {
if fc < fd {
b, d, fd = d, c, fc
c = b - ratio*(b-a)
fc = sign * f(c)
} else {
a, c, fc = c, d, fd
d = a + ratio*(b-a)
fd = sign * f(d)
}
}
t := (a + b) / 2
return t, f(t)
}
// PlanetNodesWithInstance 行星在 [startJDE, endJDE] 内穿过历元平黄道的升交点与降交点时刻(TT)/ ascending and descending node passages.
func PlanetNodesWithInstance(body SkyBody, startJDE, endJDE float64, inst *Instance) []OrbitalEvent {
index, period, ok := orbitalIndex(body)
if !ok || index < 0 || !finite(startJDE) || !finite(endJDE) || endJDE < startJDE {
return nil
}
latitude := func(t float64) float64 { return PlanetCoordinateNWithInstance(index, 1, t, -1, inst) }
count, at, ok := skyGrid(startJDE, endJDE, period/200, 1)
if !ok {
return nil
}
var out []OrbitalEvent
last := map[OrbitalEventKind]float64{}
prevT, prevB := startJDE, latitude(startJDE)
for step := 1; step <= count; step++ {
t := at(step)
b := latitude(t)
if (prevB < 0) != (b < 0) && !math.IsNaN(b) && !math.IsNaN(prevB) {
kind := OrbitalAscendingNode
if b < 0 {
kind = OrbitalDescendingNode
}
root := orbitalBisect(latitude, prevT, t, prevB)
if seen, ok := last[kind]; !ok || root-seen > 0.3*period {
last[kind] = root
out = append(out, OrbitalEvent{JDE: root, Kind: kind, DistanceAU: PlanetCoordinateNWithInstance(index, 2, root, -1, inst)})
}
}
prevT, prevB = t, b
}
return out
}
func orbitalBisect(f func(float64) float64, a, b, fa float64) float64 {
for i := 0; i < 80 && b-a > 1e-6; i++ {
m := (a + b) / 2
fm := f(m)
if (fm < 0) == (fa < 0) {
a, fa = m, fm
} else {
b = m
}
}
return (a + b) / 2
}