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

166 lines
6.1 KiB
Go

package basic
import "math"
// solarEclipsePartialLimbHorizonDeg 以下的零食分包络点视为地平线端点,改用日出日落约束求解,与升落曲线在同一点相接。
const solarEclipsePartialLimbHorizonDeg = 0.05
// limbCorrectPartialBoundary 把偏食可见域外边界(分包络与初亏、复圆的升落曲线)平移到月缘外切界线,食甚曲线保持原样。
func (solver solarEclipseSolver) limbCorrectPartialBoundary(
result *SolarEclipsePartialFootprintsResult,
limb LunarLimb,
) bool {
if limb == nil {
return false
}
applied := false
contours := make([][]SolarEclipsePathPoint, len(result.PartialBandContours))
for index, contour := range result.PartialBandContours {
corrected, changed := solver.limbCorrectPartialPoints(contour, limb, false)
contours[index] = corrected
applied = applied || changed
}
result.PartialBandContours = contours
curves := make([]SolarEclipseRiseSetCurve, len(result.RiseSetCurves))
for index, curve := range result.RiseSetCurves {
curves[index] = curve
if curve.Phase == RiseSetPhaseGreatest {
continue
}
curves[index].Segments = make([][]SolarEclipsePathPoint, len(curve.Segments))
for segmentIndex, segment := range curve.Segments {
corrected, changed := solver.limbCorrectPartialPoints(segment, limb, true)
curves[index].Segments[segmentIndex] = corrected
applied = applied || changed
}
}
result.RiseSetCurves = curves
return applied
}
// limbCorrectPartialPoints 逐点平移;求解失败的点保留圆月面位置。horizon 为真表示整条线都在地平线上。
func (solver solarEclipseSolver) limbCorrectPartialPoints(
points []SolarEclipsePathPoint,
limb LunarLimb,
horizon bool,
) ([]SolarEclipsePathPoint, bool) {
corrected := append([]SolarEclipsePathPoint(nil), points...)
applied := false
for index, point := range points {
if !finite(point.JDE) || point.JDE <= 0 {
continue
}
onHorizon := horizon || math.Abs(point.SunAltitude) < solarEclipsePartialLimbHorizonDeg
// 介于端点与 0.5° 之间的低空点地面梯度过小,平移不可信,保留圆月面点。
if !onHorizon && math.Abs(point.SunAltitude) < limbLimitHorizonDeg {
continue
}
context := solver.localStateContextAt(point.JDE)
evaluate, ok := solver.partialLimbGap(context, point.JDE, limb)
if !ok {
continue
}
var longitude, latitude float64
if onHorizon {
longitude, latitude, ok = partialLimbHorizonSolve(point.Longitude, point.Latitude, evaluate, context)
} else {
longitude, latitude, ok = limbLimitSolve(point.Longitude, point.Latitude, evaluate)
}
if !ok {
continue
}
if !limbGroundMoved(point.Longitude, point.Latitude, longitude, latitude) {
continue
}
corrected[index].Longitude, corrected[index].Latitude = longitude, latitude
corrected[index].SunAltitude = context.stateAt(longitude*rad, latitude*rad, 0).sunAltitudeRad / rad
applied = true
}
return corrected, applied
}
// partialLimbGap 返回某时刻站点处的圆月面外切间隙与月缘要求的间隙(弧度),月缘间隙与站心同一判据。
func (solver solarEclipseSolver) partialLimbGap(
context localSolarEclipseStateContext,
jde float64,
limb LunarLimb,
) (limbLimitGap, bool) {
limbContext := solver.withLimbParams(limb).localStateContextAt(jde)
if limbContext.limbFrame == nil {
return nil, false
}
return func(longitude, latitude float64) (float64, float64, bool) {
lonRad, latRad := longitude*rad, latitude*rad
state := context.stateAt(lonRad, latRad, 0)
limbState := limbContext.stateAt(lonRad, latRad, 0)
if !limbState.outerLimbApplied {
return 0, 0, false
}
gap := state.movingDiskContactState().externalContactGap()
return gap, gap - limbState.movingDiskContactState().externalContactGap(), true
}, true
}
// partialLimbHorizonSolve 在固定时刻同时满足月缘外切与太阳高度为零:沿该时刻的晨昏线移动地平线上的界线点。
func partialLimbHorizonSolve(
longitude, latitude float64,
evaluate limbLimitGap,
context localSolarEclipseStateContext,
) (float64, float64, bool) {
kmPerDegLat := math.Pi / 180 * topocentricEarthRadiusKM
altitudeAt := func(lon, lat float64) float64 {
return context.stateAt(lon*rad, lat*rad, 0).sunAltitudeRad
}
gap0, _, ok := evaluate(longitude, latitude)
if !ok {
return longitude, latitude, false
}
altitude0 := altitudeAt(longitude, latitude)
lon, lat := longitude, latitude
converged := false
for iteration := 0; iteration < limbLimitIterations+2; iteration++ {
kmPerDegLon := kmPerDegLat * math.Cos(lat*rad)
if kmPerDegLon < 1 {
return longitude, latitude, false
}
dLon := limbLimitProbeKM / kmPerDegLon
dLat := limbLimitProbeKM / kmPerDegLat
gap, target, ok := evaluate(lon, lat)
east, _, okEast := evaluate(lon+dLon, lat)
north, _, okNorth := evaluate(lon, lat+dLat)
if !ok || !okEast || !okNorth {
return longitude, latitude, false
}
// 高度残差以原点为零点,与 gap0 同理吸收圆月面采样点本身的地平线求解误差。
altitude := altitudeAt(lon, lat) - altitude0
altitudeEast := altitudeAt(lon+dLon, lat) - altitude0
altitudeNorth := altitudeAt(lon, lat+dLat) - altitude0
gx, gy := (east-gap)/limbLimitProbeKM, (north-gap)/limbLimitProbeKM
hx, hy := (altitudeEast-altitude)/limbLimitProbeKM, (altitudeNorth-altitude)/limbLimitProbeKM
determinant := gx*hy - gy*hx
if !finite(determinant) || determinant == 0 {
return longitude, latitude, false
}
residualGap, residualAltitude := -(gap - gap0 - target), -altitude
stepX := (residualGap*hy - gy*residualAltitude) / determinant
stepY := (gx*residualAltitude - residualGap*hx) / determinant
lon += stepX / kmPerDegLon
lat += stepY / kmPerDegLat
if math.Abs(stepX) < 1e-4 && math.Abs(stepY) < 1e-4 {
converged = true
break
}
}
shiftKM := math.Hypot(signedAngleDifference(lon, longitude)*kmPerDegLat*math.Cos(latitude*rad), (lat-latitude)*kmPerDegLat)
if !converged || !finite(shiftKM) || shiftKM > limbLimitMaxShiftKM || lat > 90 || lat < -90 {
return longitude, latitude, false
}
if lon > 180 {
lon -= 360
} else if lon < -180 {
lon += 360
}
return lon, lat, true
}