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
+74 -68
View File
@@ -1,6 +1,8 @@
package basic
import "math"
import (
"math"
)
const (
jupiterGalileanEventSearchSpanDays = 8 * 365.25
@@ -56,28 +58,22 @@ type jupiterGalileanShadowPoint struct {
yJupiterRadii float64
}
// LastJupiterGalileanPhenomenonEvent 上一次伽利略卫星现象 / previous Galilean-satellite event.
//
// jd 与返回时刻都是民用儒略日;需要瞬时状态时先用 UTC2TT(jd) 换成 TT。
func LastJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true)
// LastJupiterGalileanPhenomenonEventWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func LastJupiterGalileanPhenomenonEventWithInstance(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType, inst *Instance) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true, inst)
return event
}
// NextJupiterGalileanPhenomenonEvent 下一次伽利略卫星现象 / next Galilean-satellite event.
//
// jd 与返回时刻都是民用儒略日;需要瞬时状态时先用 UTC2TT(jd) 换成 TT。
func NextJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false)
// NextJupiterGalileanPhenomenonEventWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func NextJupiterGalileanPhenomenonEventWithInstance(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType, inst *Instance) JupiterGalileanPhenomenonEvent {
event, _ := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false, inst)
return event
}
// ClosestJupiterGalileanPhenomenonEvent 最近一次伽利略卫星现象 / closest Galilean-satellite event.
//
// jd 与返回时刻都是民用儒略日;需要瞬时状态时先用 UTC2TT(jd) 换成 TT。
func ClosestJupiterGalileanPhenomenonEvent(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
last, hasLast := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true)
next, hasNext := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false)
// ClosestJupiterGalileanPhenomenonEventWithInstance 用指定星历实例求值;inst 为 nil 时用进程当前模型 / evaluation with an explicit instance; nil uses the process model.
func ClosestJupiterGalileanPhenomenonEventWithInstance(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType, inst *Instance) JupiterGalileanPhenomenonEvent {
last, hasLast := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, -1, true, inst)
next, hasNext := searchJupiterGalileanPhenomenonEvent(jd, satellite, phenomenonType, 1, false, inst)
switch {
case hasLast && !hasNext:
return last
@@ -97,14 +93,15 @@ func searchJupiterGalileanPhenomenonEvent(
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
direction int,
includeCurrent bool,
) (JupiterGalileanPhenomenonEvent, bool) {
includeCurrent bool, instances ...*Instance) (JupiterGalileanPhenomenonEvent, bool) {
inst := instanceValue(instances)
if !isFinite(jd) || direction == 0 || satellite < 1 || satellite > 4 || !isValidJupiterGalileanPhenomenonType(phenomenonType) {
return invalidJupiterGalileanPhenomenonEvent(), false
}
if sample := jupiterGalileanPhenomenonMetricAt(jd, satellite, phenomenonType); sample.active {
current := findJupiterGalileanPhenomenonEventAround(jd, satellite, phenomenonType)
if sample := jupiterGalileanPhenomenonMetricAt(jd, satellite, phenomenonType, inst); sample.active {
current := findJupiterGalileanPhenomenonEventAround(jd, satellite, phenomenonType, inst)
if current.Valid && includeCurrent {
return current, true
}
@@ -122,16 +119,16 @@ func searchJupiterGalileanPhenomenonEvent(
sign := float64(direction)
prevTime := jd
prevSample := jupiterGalileanPhenomenonMetricAt(prevTime, satellite, phenomenonType)
prevSample := jupiterGalileanPhenomenonMetricAt(prevTime, satellite, phenomenonType, inst)
midTime := jd + sign*stepDays
midSample := jupiterGalileanPhenomenonMetricAt(midTime, satellite, phenomenonType)
midSample := jupiterGalileanPhenomenonMetricAt(midTime, satellite, phenomenonType, inst)
for i := 2; i <= maxSteps; i++ {
nextTime := jd + sign*float64(i)*stepDays
nextSample := jupiterGalileanPhenomenonMetricAt(nextTime, satellite, phenomenonType)
nextSample := jupiterGalileanPhenomenonMetricAt(nextTime, satellite, phenomenonType, inst)
if event, ok := jupiterGalileanIntervalCandidate(
prevTime, nextTime, prevSample, midSample, nextSample,
jd, satellite, phenomenonType, direction, includeCurrent,
jd, satellite, phenomenonType, direction, includeCurrent, inst,
); ok {
return event, true
}
@@ -150,8 +147,9 @@ func jupiterGalileanIntervalCandidate(
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
direction int,
includeCurrent bool,
) (JupiterGalileanPhenomenonEvent, bool) {
includeCurrent bool, instances ...*Instance) (JupiterGalileanPhenomenonEvent, bool) {
inst := instanceValue(instances)
var best JupiterGalileanPhenomenonEvent
found := false
consider := func(event JupiterGalileanPhenomenonEvent) {
@@ -169,17 +167,17 @@ func jupiterGalileanIntervalCandidate(
midSample.metric <= prevSample.metric &&
midSample.metric <= nextSample.metric {
consider(findJupiterGalileanPhenomenonEventAround(
refineJupiterGalileanMetricMinimum(prevTime, nextTime, satellite, phenomenonType),
satellite, phenomenonType,
refineJupiterGalileanMetricMinimum(prevTime, nextTime, satellite, phenomenonType, inst),
satellite, phenomenonType, inst,
))
}
if jupiterGalileanSignedCrosses(prevSample.signed, nextSample.signed) &&
jupiterGalileanNearDisk(prevSample, midSample, nextSample) {
crossing := refineJupiterGalileanSignedCrossing(prevTime, nextTime, satellite, phenomenonType)
crossing := refineJupiterGalileanSignedCrossing(prevTime, nextTime, satellite, phenomenonType, inst)
// 过零点只说明"靠近木星",只在它已贴近视面时按事件尺度细扫一次。
if sample := jupiterGalileanPhenomenonMetricAt(crossing, satellite, phenomenonType); isFinite(sample.metric) &&
if sample := jupiterGalileanPhenomenonMetricAt(crossing, satellite, phenomenonType, inst); isFinite(sample.metric) &&
sample.metric <= jupiterGalileanCrossingProbeMetric {
if event, ok := jupiterGalileanFineScanAround(crossing, satellite, phenomenonType); ok {
if event, ok := jupiterGalileanFineScanAround(crossing, satellite, phenomenonType, inst); ok {
consider(event)
}
}
@@ -210,8 +208,9 @@ func jupiterGalileanNearDisk(prevSample, midSample, nextSample jupiterGalileanMe
func jupiterGalileanFineScanAround(
jd float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) (JupiterGalileanPhenomenonEvent, bool) {
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) (JupiterGalileanPhenomenonEvent, bool) {
inst := instanceValue(instances)
// 从过零点向两侧交替外扩,通常几步内就能落进事件窗口。
steps := int(math.Round(jupiterGalileanCrossingProbeDays / jupiterGalileanCrossingProbeStepDays))
for step := 0; step <= steps; step++ {
@@ -220,10 +219,10 @@ func jupiterGalileanFineScanAround(
continue
}
candidate := jd + sign*float64(step)*jupiterGalileanCrossingProbeStepDays
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType).active {
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType, inst).active {
continue
}
if event := findJupiterGalileanPhenomenonEventAround(candidate, satellite, phenomenonType); event.Valid {
if event := findJupiterGalileanPhenomenonEventAround(candidate, satellite, phenomenonType, inst); event.Valid {
return event, true
}
}
@@ -246,16 +245,17 @@ func jupiterGalileanSignedCrosses(first, second float64) bool {
func refineJupiterGalileanSignedCrossing(
left, right float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) float64 {
leftValue := jupiterGalileanPhenomenonMetricAt(left, satellite, phenomenonType).signed
rightValue := jupiterGalileanPhenomenonMetricAt(right, satellite, phenomenonType).signed
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) float64 {
inst := instanceValue(instances)
leftValue := jupiterGalileanPhenomenonMetricAt(left, satellite, phenomenonType, inst).signed
rightValue := jupiterGalileanPhenomenonMetricAt(right, satellite, phenomenonType, inst).signed
if !isFinite(leftValue) || !isFinite(rightValue) || (leftValue < 0) == (rightValue < 0) {
return (left + right) / 2
}
for i := 0; i < 80 && right-left > jupiterGalileanEventEpsilonDays; i++ {
middle := (left + right) / 2
value := jupiterGalileanPhenomenonMetricAt(middle, satellite, phenomenonType).signed
value := jupiterGalileanPhenomenonMetricAt(middle, satellite, phenomenonType, inst).signed
if !isFinite(value) {
return middle
}
@@ -268,8 +268,10 @@ func refineJupiterGalileanSignedCrossing(
return (left + right) / 2
}
func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType) JupiterGalileanPhenomenonEvent {
sample := jupiterGalileanPhenomenonMetricAt(jd, satellite, phenomenonType)
func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) JupiterGalileanPhenomenonEvent {
inst := instanceValue(instances)
sample := jupiterGalileanPhenomenonMetricAt(jd, satellite, phenomenonType, inst)
if !sample.active {
return invalidJupiterGalileanPhenomenonEvent()
}
@@ -281,7 +283,7 @@ func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenome
inactiveStart := math.NaN()
for i := 0; i < maxBoundarySteps; i++ {
candidate := activeStart - stepDays
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType).active {
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType, inst).active {
inactiveStart = candidate
break
}
@@ -295,7 +297,7 @@ func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenome
inactiveEnd := math.NaN()
for i := 0; i < maxBoundarySteps; i++ {
candidate := activeEnd + stepDays
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType).active {
if !jupiterGalileanPhenomenonMetricAt(candidate, satellite, phenomenonType, inst).active {
inactiveEnd = candidate
break
}
@@ -305,14 +307,14 @@ func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenome
return invalidJupiterGalileanPhenomenonEvent()
}
start := refineJupiterGalileanEventStart(inactiveStart, activeStart, satellite, phenomenonType)
end := refineJupiterGalileanEventEnd(activeEnd, inactiveEnd, satellite, phenomenonType)
start := refineJupiterGalileanEventStart(inactiveStart, activeStart, satellite, phenomenonType, inst)
end := refineJupiterGalileanEventEnd(activeEnd, inactiveEnd, satellite, phenomenonType, inst)
if !isFinite(start) || !isFinite(end) || end <= start {
return invalidJupiterGalileanPhenomenonEvent()
}
greatest := refineJupiterGalileanMetricMinimum(start, end, satellite, phenomenonType)
greatestSample := jupiterGalileanPhenomenonMetricAt(greatest, satellite, phenomenonType)
greatest := refineJupiterGalileanMetricMinimum(start, end, satellite, phenomenonType, inst)
greatestSample := jupiterGalileanPhenomenonMetricAt(greatest, satellite, phenomenonType, inst)
if !greatestSample.active {
return invalidJupiterGalileanPhenomenonEvent()
}
@@ -331,22 +333,23 @@ func findJupiterGalileanPhenomenonEventAround(jd float64, satellite int, phenome
func refineJupiterGalileanEventStart(
outsideJD, insideJD float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) float64 {
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) float64 {
inst := instanceValue(instances)
if insideJD < outsideJD {
outsideJD, insideJD = insideJD, outsideJD
}
if jupiterGalileanPhenomenonMetricAt(outsideJD, satellite, phenomenonType).active {
if jupiterGalileanPhenomenonMetricAt(outsideJD, satellite, phenomenonType, inst).active {
return math.NaN()
}
if !jupiterGalileanPhenomenonMetricAt(insideJD, satellite, phenomenonType).active {
if !jupiterGalileanPhenomenonMetricAt(insideJD, satellite, phenomenonType, inst).active {
return math.NaN()
}
left := outsideJD
right := insideJD
for i := 0; i < 80 && right-left > jupiterGalileanEventEpsilonDays; i++ {
mid := (left + right) / 2
if jupiterGalileanPhenomenonMetricAt(mid, satellite, phenomenonType).active {
if jupiterGalileanPhenomenonMetricAt(mid, satellite, phenomenonType, inst).active {
right = mid
} else {
left = mid
@@ -358,22 +361,23 @@ func refineJupiterGalileanEventStart(
func refineJupiterGalileanEventEnd(
insideJD, outsideJD float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) float64 {
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) float64 {
inst := instanceValue(instances)
if outsideJD < insideJD {
insideJD, outsideJD = outsideJD, insideJD
}
if !jupiterGalileanPhenomenonMetricAt(insideJD, satellite, phenomenonType).active {
if !jupiterGalileanPhenomenonMetricAt(insideJD, satellite, phenomenonType, inst).active {
return math.NaN()
}
if jupiterGalileanPhenomenonMetricAt(outsideJD, satellite, phenomenonType).active {
if jupiterGalileanPhenomenonMetricAt(outsideJD, satellite, phenomenonType, inst).active {
return math.NaN()
}
left := insideJD
right := outsideJD
for i := 0; i < 80 && right-left > jupiterGalileanEventEpsilonDays; i++ {
mid := (left + right) / 2
if jupiterGalileanPhenomenonMetricAt(mid, satellite, phenomenonType).active {
if jupiterGalileanPhenomenonMetricAt(mid, satellite, phenomenonType, inst).active {
left = mid
} else {
right = mid
@@ -385,8 +389,9 @@ func refineJupiterGalileanEventEnd(
func refineJupiterGalileanMetricMinimum(
jd1, jd2 float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) float64 {
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) float64 {
inst := instanceValue(instances)
left := math.Min(jd1, jd2)
right := math.Max(jd1, jd2)
if right-left <= jupiterGalileanEventEpsilonDays {
@@ -395,21 +400,21 @@ func refineJupiterGalileanMetricMinimum(
const phi = 0.6180339887498948482
x1 := right - phi*(right-left)
x2 := left + phi*(right-left)
f1 := jupiterGalileanPhenomenonMetricAt(x1, satellite, phenomenonType).metric
f2 := jupiterGalileanPhenomenonMetricAt(x2, satellite, phenomenonType).metric
f1 := jupiterGalileanPhenomenonMetricAt(x1, satellite, phenomenonType, inst).metric
f2 := jupiterGalileanPhenomenonMetricAt(x2, satellite, phenomenonType, inst).metric
for i := 0; i < 80 && right-left > jupiterGalileanEventEpsilonDays; i++ {
if f1 <= f2 {
right = x2
x2 = x1
f2 = f1
x1 = right - phi*(right-left)
f1 = jupiterGalileanPhenomenonMetricAt(x1, satellite, phenomenonType).metric
f1 = jupiterGalileanPhenomenonMetricAt(x1, satellite, phenomenonType, inst).metric
} else {
left = x1
x1 = x2
f1 = f2
x2 = left + phi*(right-left)
f2 = jupiterGalileanPhenomenonMetricAt(x2, satellite, phenomenonType).metric
f2 = jupiterGalileanPhenomenonMetricAt(x2, satellite, phenomenonType, inst).metric
}
}
return (left + right) / 2
@@ -418,8 +423,9 @@ func refineJupiterGalileanMetricMinimum(
func jupiterGalileanPhenomenonMetricAt(
jd float64,
satellite int,
phenomenonType JupiterGalileanPhenomenonType,
) jupiterGalileanMetricSample {
phenomenonType JupiterGalileanPhenomenonType, instances ...*Instance) jupiterGalileanMetricSample {
inst := instanceValue(instances)
if !isFinite(jd) || satellite < 1 || satellite > 4 || !isValidJupiterGalileanPhenomenonType(phenomenonType) {
return jupiterGalileanMetricSample{
metric: math.Inf(1),
@@ -429,7 +435,7 @@ func jupiterGalileanPhenomenonMetricAt(
}
evaluationJDE := UTC2TT(jd)
context := newJupiterGalileanObservationContext(evaluationJDE)
context := newJupiterGalileanObservationContext(evaluationJDE, inst)
if context.jupiterDistance == 0 {
return jupiterGalileanMetricSample{
metric: math.Inf(1),