Files
astro/basic/orbital_events.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

192 lines
5.5 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 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
}