feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
@@ -3,6 +3,8 @@ package basic
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/internal/geodata"
|
||||
)
|
||||
|
||||
// occultationStationBoundarySample is the result of correcting one geocentric
|
||||
@@ -322,8 +324,8 @@ func occultationStationCorrectContactPoint(
|
||||
), true
|
||||
}
|
||||
const coordinateStepDeg = 0.005
|
||||
plusLongitude := context.stateAt(normalizeLongitude(longitude+coordinateStepDeg), latitude)
|
||||
minusLongitude := context.stateAt(normalizeLongitude(longitude-coordinateStepDeg), latitude)
|
||||
plusLongitude := context.stateAt(geodata.NormalizeLongitude(longitude+coordinateStepDeg), latitude)
|
||||
minusLongitude := context.stateAt(geodata.NormalizeLongitude(longitude-coordinateStepDeg), latitude)
|
||||
plusLatitude := context.stateAt(longitude, math.Min(89.999999, latitude+coordinateStepDeg))
|
||||
minusLatitude := context.stateAt(longitude, math.Max(-89.999999, latitude-coordinateStepDeg))
|
||||
if !plusLongitude.valid || !minusLongitude.valid || !plusLatitude.valid || !minusLatitude.valid {
|
||||
@@ -344,7 +346,7 @@ func occultationStationCorrectContactPoint(
|
||||
deltaX *= scale
|
||||
deltaY *= scale
|
||||
}
|
||||
longitude = normalizeLongitude(longitude + deltaX/cosLatitude)
|
||||
longitude = geodata.NormalizeLongitude(longitude + deltaX/cosLatitude)
|
||||
latitude = math.Max(-89.999999, math.Min(89.999999, latitude+deltaY))
|
||||
}
|
||||
state := context.stateAt(longitude, latitude)
|
||||
@@ -436,8 +438,8 @@ func occultationStationCorrectEnvelopePoint(
|
||||
|
||||
const coordinateStepDeg = 0.002
|
||||
cosLatitude := math.Max(0.05, math.Cos(latitude*rad))
|
||||
plusLongitude := evaluate(normalizeLongitude(longitude+coordinateStepDeg/cosLatitude), latitude)
|
||||
minusLongitude := evaluate(normalizeLongitude(longitude-coordinateStepDeg/cosLatitude), latitude)
|
||||
plusLongitude := evaluate(geodata.NormalizeLongitude(longitude+coordinateStepDeg/cosLatitude), latitude)
|
||||
minusLongitude := evaluate(geodata.NormalizeLongitude(longitude-coordinateStepDeg/cosLatitude), latitude)
|
||||
plusLatitude := evaluate(longitude, math.Min(89.999999, latitude+coordinateStepDeg))
|
||||
minusLatitude := evaluate(longitude, math.Max(-89.999999, latitude-coordinateStepDeg))
|
||||
if !plusLongitude.valid || !minusLongitude.valid || !plusLatitude.valid || !minusLatitude.valid {
|
||||
@@ -465,7 +467,7 @@ func occultationStationCorrectEnvelopePoint(
|
||||
currentNorm := residualNorm(state)
|
||||
accepted := false
|
||||
for damping := 1.0; damping >= 1.0/128; damping /= 2 {
|
||||
candidateLongitude := normalizeLongitude(longitude + damping*deltaX/cosLatitude)
|
||||
candidateLongitude := geodata.NormalizeLongitude(longitude + damping*deltaX/cosLatitude)
|
||||
candidateLatitude := math.Max(-89.999999, math.Min(89.999999, latitude+damping*deltaY))
|
||||
if occultationPathDistanceKMValues(
|
||||
seed.Longitude, seed.Latitude, candidateLongitude, candidateLatitude,
|
||||
@@ -845,7 +847,7 @@ func occultationStationCorrectEnvelopeArc(
|
||||
residual, jacobian, ok := occultationStationEnvelopeJacobian(coordinates, referenceTT, jacobianCache, model)
|
||||
if iterationCache == cache && ok {
|
||||
tt := referenceTT + coordinates[2]/occultationStationEnvelopeTimeScale
|
||||
residual, ok = model.residual(cache.evaluation(tt), normalizeLongitude(coordinates[0]), coordinates[1])
|
||||
residual, ok = model.residual(cache.evaluation(tt), geodata.NormalizeLongitude(coordinates[0]), coordinates[1])
|
||||
}
|
||||
if !ok {
|
||||
if jacobianCache != cache {
|
||||
@@ -959,7 +961,7 @@ func occultationStationValidEnvelopeArcState(
|
||||
iterations int,
|
||||
) (occultationStationEnvelopeArcState, int, bool) {
|
||||
tt := referenceTT + coordinates[2]/occultationStationEnvelopeTimeScale
|
||||
longitude, latitude := normalizeLongitude(coordinates[0]), coordinates[1]
|
||||
longitude, latitude := geodata.NormalizeLongitude(coordinates[0]), coordinates[1]
|
||||
evaluation := cache.evaluation(tt)
|
||||
state := evaluation.center.stateAt(longitude, latitude)
|
||||
residual, ok := model.residual(evaluation, longitude, latitude)
|
||||
@@ -989,7 +991,7 @@ func occultationStationEnvelopeJacobian(
|
||||
model occultationStationEnvelopeModel,
|
||||
) ([2]float64, [2][3]float64, bool) {
|
||||
tt := referenceTT + coordinates[2]/occultationStationEnvelopeTimeScale
|
||||
longitude, latitude := normalizeLongitude(coordinates[0]), coordinates[1]
|
||||
longitude, latitude := geodata.NormalizeLongitude(coordinates[0]), coordinates[1]
|
||||
evaluation := cache.evaluation(tt)
|
||||
// 点源目标(恒星)没有盘面半径项,接触度量可用解析速率;有限盘面行星保持中心差分。
|
||||
residualAt := model.residual
|
||||
@@ -1008,7 +1010,7 @@ func occultationStationEnvelopeJacobian(
|
||||
shiftedTT := referenceTT + shifted[2]/occultationStationEnvelopeTimeScale
|
||||
shiftedEvaluation := cache.evaluation(shiftedTT)
|
||||
shiftedResidual, shiftedOK := residualAt(
|
||||
shiftedEvaluation, normalizeLongitude(shifted[0]), shifted[1],
|
||||
shiftedEvaluation, geodata.NormalizeLongitude(shifted[0]), shifted[1],
|
||||
)
|
||||
if !shiftedOK {
|
||||
return [2]float64{}, [2][3]float64{}, false
|
||||
@@ -1174,6 +1176,74 @@ func occultationStationDeduplicateEnvelopePoints(points []OccultationPathPoint)
|
||||
return result
|
||||
}
|
||||
|
||||
// occultationStationEnvelopeTieBreak 是同时间戳平台内的最大时间步长。追踪点时间来自 JD 的
|
||||
// float64,现代历元分辨率约 40 µs;1 µs 远低于这一量化,只用于恢复真实的先后次序。
|
||||
const occultationStationEnvelopeTieBreak = time.Microsecond
|
||||
|
||||
// occultationStationCollapseTimePlateaus 消除包络追踪中"不同位置、同一时间戳"的平台。
|
||||
// 平台是 JD 量化的产物:时间导数在折返顶点或结点附近趋零时,相邻追踪点落进同一量化格。
|
||||
// 若把平台当成真折返切开,平台内的边不属于任何严格单调段,轮廓在此断开(Saturn
|
||||
// 2025-01-05 顶点处断开 2.8 km,结点处断开时端点被丢弃)。这里沿进入平台的时间方向
|
||||
// (平台在首部时取离开方向)为平台各点补上亚量化的递增偏移:非折返平台连成一段,
|
||||
// 折返平台的末点成为两段共享的顶点,所有点与边都被保留。整条曲线同一时间戳时没有方向,原样返回。
|
||||
func occultationStationCollapseTimePlateaus(points []OccultationPathPoint) []OccultationPathPoint {
|
||||
sign := func(first, second time.Time) int {
|
||||
switch {
|
||||
case second.After(first):
|
||||
return 1
|
||||
case second.Before(first):
|
||||
return -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
var result []OccultationPathPoint
|
||||
for start := 0; start < len(points); {
|
||||
end := start + 1
|
||||
for end < len(points) && points[end].Time.Equal(points[start].Time) {
|
||||
end++
|
||||
}
|
||||
if end-start == 1 {
|
||||
start = end
|
||||
continue
|
||||
}
|
||||
direction := 0
|
||||
if start > 0 {
|
||||
direction = sign(points[start-1].Time, points[start].Time)
|
||||
}
|
||||
if direction == 0 && end < len(points) {
|
||||
direction = sign(points[end-1].Time, points[end].Time)
|
||||
}
|
||||
if direction == 0 {
|
||||
return points
|
||||
}
|
||||
step := occultationStationEnvelopeTieBreak
|
||||
// 不折返时偏移不能越过下一点,否则平台出口会变成假折返。
|
||||
if end < len(points) && sign(points[end-1].Time, points[end].Time) == direction {
|
||||
if gap := points[end].Time.Sub(points[start].Time) * time.Duration(direction) / time.Duration(end-start); gap < step {
|
||||
step = gap
|
||||
}
|
||||
}
|
||||
if result == nil {
|
||||
result = append([]OccultationPathPoint(nil), points...)
|
||||
}
|
||||
if step <= 0 {
|
||||
// 相邻点只差纳秒级时无法再细分,保留平台首点即可,丢弃的点与首点同一时刻。
|
||||
result = append(result[:start+1], result[end:]...)
|
||||
points = result
|
||||
start++
|
||||
continue
|
||||
}
|
||||
for index := start + 1; index < end; index++ {
|
||||
result[index].Time = result[start].Time.Add(time.Duration(direction*(index-start)) * step)
|
||||
}
|
||||
start = end
|
||||
}
|
||||
if result == nil {
|
||||
return points
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func occultationStationSplitEnvelopeAtTimeFolds(
|
||||
points []OccultationPathPoint,
|
||||
) [][]OccultationPathPoint {
|
||||
@@ -1193,7 +1263,7 @@ func occultationStationSplitEnvelopeAtTimeFolds(
|
||||
}
|
||||
normalized = append(normalized, point)
|
||||
}
|
||||
points = normalized
|
||||
points = occultationStationCollapseTimePlateaus(normalized)
|
||||
if len(points) < 2 {
|
||||
return nil
|
||||
}
|
||||
@@ -1291,8 +1361,8 @@ func occultationStationCorrectHorizonContactPoint(
|
||||
}
|
||||
|
||||
const coordinateStepDeg = 0.002
|
||||
plusLongitude := context.stateAt(normalizeLongitude(longitude+coordinateStepDeg), latitude)
|
||||
minusLongitude := context.stateAt(normalizeLongitude(longitude-coordinateStepDeg), latitude)
|
||||
plusLongitude := context.stateAt(geodata.NormalizeLongitude(longitude+coordinateStepDeg), latitude)
|
||||
minusLongitude := context.stateAt(geodata.NormalizeLongitude(longitude-coordinateStepDeg), latitude)
|
||||
plusLatitude := context.stateAt(longitude, math.Min(89.999999, latitude+coordinateStepDeg))
|
||||
minusLatitude := context.stateAt(longitude, math.Max(-89.999999, latitude-coordinateStepDeg))
|
||||
if !plusLongitude.valid || !minusLongitude.valid || !plusLatitude.valid || !minusLatitude.valid {
|
||||
@@ -1321,7 +1391,7 @@ func occultationStationCorrectHorizonContactPoint(
|
||||
currentNorm := residualNorm(state)
|
||||
accepted := false
|
||||
for damping := 1.0; damping >= 1.0/64; damping /= 2 {
|
||||
candidateLongitude := normalizeLongitude(longitude + damping*deltaX/cosLatitude)
|
||||
candidateLongitude := geodata.NormalizeLongitude(longitude + damping*deltaX/cosLatitude)
|
||||
candidateLatitude := math.Max(-89.999999, math.Min(89.999999, latitude+damping*deltaY))
|
||||
if occultationPathDistanceKMValues(
|
||||
seed.Longitude, seed.Latitude, candidateLongitude, candidateLatitude,
|
||||
@@ -1633,7 +1703,7 @@ func occultationStationSurfaceVector(longitude, latitude float64) occultationPat
|
||||
x: cosU * cosLongitude,
|
||||
y: cosU * sinLongitude,
|
||||
z: polarRatio * sinU,
|
||||
}, occultationPathEarthEquatorialRadiusKM)
|
||||
}, geodata.EarthEquatorialRadiusKM)
|
||||
}
|
||||
|
||||
func occultationStationOffsetSurfaceVector(
|
||||
@@ -1645,11 +1715,11 @@ func occultationStationOffsetSurfaceVector(
|
||||
if metric <= 0 || !finite(metric) {
|
||||
return seedFixed
|
||||
}
|
||||
return occultationPathScale(point, occultationPathEarthEquatorialRadiusKM/metric)
|
||||
return occultationPathScale(point, geodata.EarthEquatorialRadiusKM/metric)
|
||||
}
|
||||
|
||||
func occultationStationGeodetic(fixed occultationPathVector) (float64, float64) {
|
||||
return normalizeLongitude(math.Atan2(fixed.y, fixed.x) / rad), occultationPathGeodeticLatitude(fixed)
|
||||
return geodata.NormalizeLongitude(math.Atan2(fixed.y, fixed.x) / rad), occultationPathGeodeticLatitude(fixed)
|
||||
}
|
||||
|
||||
func occultationStationInverseEarthRotation(
|
||||
|
||||
Reference in New Issue
Block a user