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,269 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"b612.me/astro/internal/geodata"
|
||||
)
|
||||
|
||||
// SkyBody 通用天象搜索支持的天体 / body for generic sky event searches.
|
||||
type SkyBody int
|
||||
|
||||
const (
|
||||
// SkySun 太阳 / the Sun.
|
||||
SkySun SkyBody = iota
|
||||
// SkyMoon 月球 / the Moon.
|
||||
SkyMoon
|
||||
// SkyMercury 水星 / Mercury.
|
||||
SkyMercury
|
||||
// SkyVenus 金星 / Venus.
|
||||
SkyVenus
|
||||
// SkyMars 火星 / Mars.
|
||||
SkyMars
|
||||
// SkyJupiter 木星 / Jupiter.
|
||||
SkyJupiter
|
||||
// SkySaturn 土星 / Saturn.
|
||||
SkySaturn
|
||||
// SkyUranus 天王星 / Uranus.
|
||||
SkyUranus
|
||||
// SkyNeptune 海王星 / Neptune.
|
||||
SkyNeptune
|
||||
)
|
||||
|
||||
// SkyEvent 通用天象事件:TT 儒略日与事件时的角量(度)/ generic sky event at TT JDE with its angle in degrees.
|
||||
type SkyEvent struct {
|
||||
JDE float64
|
||||
Value float64
|
||||
}
|
||||
|
||||
func (body SkyBody) valid() bool { return body >= SkySun && body <= SkyNeptune }
|
||||
|
||||
// skyScanStep 保证相邻采样间视黄经变化远小于 180°,逆行回转也不会漏根。
|
||||
func skyScanStep(bodies ...SkyBody) float64 {
|
||||
step := 2.0
|
||||
for _, b := range bodies {
|
||||
switch b {
|
||||
case SkyMoon:
|
||||
step = math.Min(step, 0.25)
|
||||
case SkyMercury, SkyVenus, SkySun:
|
||||
step = math.Min(step, 1.0)
|
||||
}
|
||||
}
|
||||
return step
|
||||
}
|
||||
|
||||
// SkyBodyApparentLoBoWithInstance 地心视黄经、视黄纬(度)/ geocentric apparent ecliptic longitude and latitude.
|
||||
func SkyBodyApparentLoBoWithInstance(body SkyBody, jde float64, inst *Instance) (float64, float64) {
|
||||
switch body {
|
||||
case SkySun:
|
||||
return sunApparentLoBoN(jde, -1, inst)
|
||||
case SkyMoon:
|
||||
return moonApparentLoBoN(jde, -1, inst)
|
||||
case SkyMercury, SkyVenus, SkyMars, SkyJupiter, SkySaturn, SkyUranus, SkyNeptune:
|
||||
return planetApparentLoBoWithInstance(int(body-SkyMercury)+1, jde, inst)
|
||||
}
|
||||
return math.NaN(), math.NaN()
|
||||
}
|
||||
|
||||
// skyGridMaxIntervals 网格段数上限;须能放进 32 位 int,超出时扫描本身也不可能完成。
|
||||
const skyGridMaxIntervals = 1 << 30
|
||||
|
||||
// skyGrid 把 [start, end] 均分为至少 minIntervals 段、每段不超过 step;按下标取点,浮点下必然前进且末点恰为 end。
|
||||
func skyGrid(start, end, step float64, minIntervals int) (count int, at func(int) float64, ok bool) {
|
||||
span := end - start
|
||||
if !finite(start) || !finite(end) || !(span > 0) || !(step > 0) {
|
||||
return 0, nil, false
|
||||
}
|
||||
ratio := math.Ceil(span / step)
|
||||
if !(ratio < skyGridMaxIntervals) {
|
||||
return 0, nil, false
|
||||
}
|
||||
count = minIntervals
|
||||
if ratio > float64(count) {
|
||||
count = int(ratio)
|
||||
}
|
||||
return count, func(index int) float64 {
|
||||
if index == count {
|
||||
return end
|
||||
}
|
||||
return start + span*float64(index)/float64(count)
|
||||
}, true
|
||||
}
|
||||
|
||||
// skyRoots 在闭区间 [start, end] 内按 step 扫描 f 的零点并二分到约 0.1 秒;f 须连续(已处理 360° 回绕)。
|
||||
func skyRoots(start, end, step float64, f func(float64) float64) []float64 {
|
||||
count, at, ok := skyGrid(start, end, step, 1)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var roots []float64
|
||||
prevT, prev := start, f(start)
|
||||
if prev == 0 {
|
||||
roots = append(roots, start)
|
||||
}
|
||||
for index := 1; index <= count; index++ {
|
||||
t := at(index)
|
||||
cur := f(t)
|
||||
switch {
|
||||
case cur == 0:
|
||||
roots = append(roots, t)
|
||||
// 回绕跳变时 |差| 接近 360,不是真根;prev 为 0 的根已在上一点记过。
|
||||
case prev != 0 && !math.IsNaN(prev) && !math.IsNaN(cur) && (prev < 0) != (cur < 0) && math.Abs(cur-prev) < 180:
|
||||
lo, hi, flo := prevT, t, prev
|
||||
// 迭代上限防止大 JDE 下浮点间隔大于容差时不收敛。
|
||||
for i := 0; i < 100 && hi-lo > 1e-6; i++ {
|
||||
mid := (lo + hi) / 2
|
||||
fm := f(mid)
|
||||
if fm == 0 {
|
||||
lo, hi = mid, mid
|
||||
break
|
||||
}
|
||||
if (fm < 0) == (flo < 0) {
|
||||
lo, flo = mid, fm
|
||||
} else {
|
||||
hi = mid
|
||||
}
|
||||
}
|
||||
roots = append(roots, (lo+hi)/2)
|
||||
}
|
||||
prevT, prev = t, cur
|
||||
}
|
||||
return roots
|
||||
}
|
||||
|
||||
// SkyLongitudeCrossingsWithInstance body 视黄经经过 longitude 的全部时刻 / times when the apparent longitude equals longitude.
|
||||
func SkyLongitudeCrossingsWithInstance(body SkyBody, longitude, startJDE, endJDE float64, inst *Instance) []SkyEvent {
|
||||
if !body.valid() || !(endJDE > startJDE) {
|
||||
return nil
|
||||
}
|
||||
f := func(t float64) float64 {
|
||||
lo, _ := SkyBodyApparentLoBoWithInstance(body, t, inst)
|
||||
return geodata.NormalizeLongitude(lo - longitude)
|
||||
}
|
||||
var out []SkyEvent
|
||||
for _, t := range skyRoots(startJDE, endJDE, skyScanStep(body), f) {
|
||||
out = append(out, SkyEvent{JDE: t, Value: longitude})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SkyStationsWithInstance 视黄经留(速度过零)的时刻,Value 为留时视黄经 / longitude stations; Value is the longitude.
|
||||
func SkyStationsWithInstance(body SkyBody, startJDE, endJDE float64, inst *Instance) []SkyEvent {
|
||||
if !body.valid() || body == SkySun || body == SkyMoon || !(endJDE > startJDE) {
|
||||
return nil
|
||||
}
|
||||
const h = 0.05
|
||||
rate := func(t float64) float64 {
|
||||
a, _ := SkyBodyApparentLoBoWithInstance(body, t-h, inst)
|
||||
b, _ := SkyBodyApparentLoBoWithInstance(body, t+h, inst)
|
||||
return geodata.NormalizeLongitude(b - a)
|
||||
}
|
||||
var out []SkyEvent
|
||||
for _, t := range skyRoots(startJDE, endJDE, skyScanStep(body), rate) {
|
||||
lo, _ := SkyBodyApparentLoBoWithInstance(body, t, inst)
|
||||
out = append(out, SkyEvent{JDE: t, Value: lo})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SkyAspectCrossingsWithInstance a 与 b 视黄经差(a − b)等于 angle 的时刻;angle 0 为黄经合 / times when lon(a) − lon(b) equals angle.
|
||||
func SkyAspectCrossingsWithInstance(a, b SkyBody, angle, startJDE, endJDE float64, inst *Instance) []SkyEvent {
|
||||
if !a.valid() || !b.valid() || a == b || !(endJDE > startJDE) {
|
||||
return nil
|
||||
}
|
||||
f := func(t float64) float64 {
|
||||
la, _ := SkyBodyApparentLoBoWithInstance(a, t, inst)
|
||||
lb, _ := SkyBodyApparentLoBoWithInstance(b, t, inst)
|
||||
return geodata.NormalizeLongitude(la - lb - angle)
|
||||
}
|
||||
var out []SkyEvent
|
||||
for _, t := range skyRoots(startJDE, endJDE, skyScanStep(a, b), f) {
|
||||
out = append(out, SkyEvent{JDE: t, Value: angle})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func skyAngularDistance(lon1, lat1, lon2, lat2 float64) float64 {
|
||||
v1, v2 := sphericalUnit(lon1, lat1), sphericalUnit(lon2, lat2)
|
||||
cross := [3]float64{v1[1]*v2[2] - v1[2]*v2[1], v1[2]*v2[0] - v1[0]*v2[2], v1[0]*v2[1] - v1[1]*v2[0]}
|
||||
return math.Atan2(math.Sqrt(cross[0]*cross[0]+cross[1]*cross[1]+cross[2]*cross[2]), v1[0]*v2[0]+v1[1]*v2[1]+v1[2]*v2[2]) / rad
|
||||
}
|
||||
|
||||
// skyGoldenMin 黄金分割求 [lo, hi] 内单峰 f 的极小点。
|
||||
func skyGoldenMin(lo, hi float64, f func(float64) float64) (float64, float64) {
|
||||
const g = 0.6180339887498949
|
||||
x1, x2 := hi-g*(hi-lo), lo+g*(hi-lo)
|
||||
fx1, fx2 := f(x1), f(x2)
|
||||
for i := 0; i < 200 && hi-lo > 1e-6; i++ {
|
||||
if fx1 < fx2 {
|
||||
hi, x2, fx2 = x2, x1, fx1
|
||||
x1 = hi - g*(hi-lo)
|
||||
fx1 = f(x1)
|
||||
} else {
|
||||
lo, x1, fx1 = x1, x2, fx2
|
||||
x2 = lo + g*(hi-lo)
|
||||
fx2 = f(x2)
|
||||
}
|
||||
}
|
||||
t := (lo + hi) / 2
|
||||
return t, f(t)
|
||||
}
|
||||
|
||||
// skyMinima 扫描 f 在 [start, end] 内的局部极小并用黄金分割细化;只保留 ≤ maxValue 的极小。
|
||||
func skyMinima(start, end, step, maxValue float64, f func(float64) float64) []SkyEvent {
|
||||
count, at, ok := skyGrid(start, end, step, 2)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
var out []SkyEvent
|
||||
add := func(lo, hi, boundary float64) {
|
||||
// 端点段细化值须低于边界采样值,否则极小在窗口外,不是窗口内的局部极小。
|
||||
if t, v := skyGoldenMin(lo, hi, f); v <= maxValue && v < boundary {
|
||||
out = append(out, SkyEvent{JDE: t, Value: v})
|
||||
}
|
||||
}
|
||||
t0, t1 := at(0), at(1)
|
||||
f0, f1 := f(t0), f(t1)
|
||||
if f0 < f1 {
|
||||
add(t0, t1, f0)
|
||||
}
|
||||
for index := 2; index <= count; index++ {
|
||||
t2 := at(index)
|
||||
f2 := f(t2)
|
||||
if f1 <= f0 && f1 < f2 {
|
||||
add(t0, t2, math.Inf(1))
|
||||
}
|
||||
t0, t1, f0, f1 = t1, t2, f1, f2
|
||||
}
|
||||
if f1 < f0 {
|
||||
add(t0, t1, f1)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SkySeparationMinimaWithInstance a、b 地心视角距的局部极小(度),只返回 ≤ maxSeparation 的 / closest approaches within maxSeparation degrees.
|
||||
func SkySeparationMinimaWithInstance(a, b SkyBody, maxSeparation, startJDE, endJDE float64, inst *Instance) []SkyEvent {
|
||||
if !a.valid() || !b.valid() || a == b || !(endJDE > startJDE) {
|
||||
return nil
|
||||
}
|
||||
f := func(t float64) float64 {
|
||||
la, ba := SkyBodyApparentLoBoWithInstance(a, t, inst)
|
||||
lb, bb := SkyBodyApparentLoBoWithInstance(b, t, inst)
|
||||
return skyAngularDistance(la, ba, lb, bb)
|
||||
}
|
||||
return skyMinima(startJDE, endJDE, skyScanStep(a, b)/2, maxSeparation, f)
|
||||
}
|
||||
|
||||
// SkyStarSeparationMinimaWithInstance 天体与恒星地心视角距的局部极小(度)/ closest approaches of a body to a star within maxSeparation degrees.
|
||||
func SkyStarSeparationMinimaWithInstance(body SkyBody, star StarCoordinate, maxSeparation, startJDE, endJDE float64, inst *Instance) []SkyEvent {
|
||||
if !body.valid() || !(endJDE > startJDE) {
|
||||
return nil
|
||||
}
|
||||
f := func(t float64) float64 {
|
||||
ra, dec := starApparentRaDecGeocentric(t, star, inst)
|
||||
sl, sb := RaDecToLoBo(t, ra, dec)
|
||||
lo, bo := SkyBodyApparentLoBoWithInstance(body, t, inst)
|
||||
return skyAngularDistance(lo, bo, sl, sb)
|
||||
}
|
||||
return skyMinima(startJDE, endJDE, skyScanStep(body)/2, maxSeparation, f)
|
||||
}
|
||||
Reference in New Issue
Block a user