16c62a97d5
- 新增时标、ΔT 模型、质心时间与 UT1 支持 - 改进日月食、月掩、行星事件及路径边界计算 - 完善恒星三维自行与动态距离传播 - 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具 - 整理中英文手册、示例资源及回归测试
284 lines
11 KiB
Go
284 lines
11 KiB
Go
package basic
|
|
|
|
import (
|
|
"math"
|
|
"sort"
|
|
)
|
|
|
|
// 地平闭包根的扫描式枚举。闭包根的定义是三个条件同时成立:站点落在 central limit 上
|
|
// (gap=0)、该站点的间隙在时间上取极值(∂gap/∂t=0)、站点落在地平线上(alt=0)。
|
|
// 于是可以先把前两个条件在固定时刻化成一维周期求根(「地平线上的食甚点」),再让这些
|
|
// 点上的 gap 随时刻穿越零——闭包根就是那次穿越。这样既不需要种子,也不需要三维牛顿的
|
|
// 有限差分雅可比;采样分支恰好终止在根上时不会漏根。
|
|
|
|
const (
|
|
// 地平圈整圈求根的采样数,与升落曲线同口径。
|
|
solarEclipseClosureScanBoundaryPoints = solarEclipseRiseSetBoundaryPoints
|
|
// 时间导数沿地平圈找根时的折点容差,单位与 ∂gap/∂t(每天)一致。
|
|
solarEclipseClosureScanRateFoldTolerance = 1e-3
|
|
// 行数下限与上限;窗口很短时按比例加密,很长时按比例放稀。
|
|
solarEclipseClosureScanMinimumRows = 24
|
|
solarEclipseClosureScanMaximumRows = 720
|
|
// 默认行距 1 min:闭包根之间的间隙可达数十分钟,够给每个符号变化留出样本。
|
|
solarEclipseClosureScanStepDays = 60.0 / 86400.0
|
|
// 相邻行配对的距离上限:同一条闭合弧上的点一行之内不会超过该距离。
|
|
solarEclipseClosureScanMatchKM = 900.0
|
|
// 时间二分上限,1e-9 天约 1e-4 s。
|
|
solarEclipseClosureScanBisectionSteps = 40
|
|
// 牛顿抛光后允许偏离扫描根的上限,超过说明落到了别的分支。
|
|
solarEclipseClosureScanPolishKM = 2.0
|
|
)
|
|
|
|
type solarEclipseClosureScanCandidate struct {
|
|
jde float64
|
|
longitude float64
|
|
latitude float64
|
|
gap float64
|
|
}
|
|
|
|
type solarEclipseClosureScanRow struct {
|
|
jde float64
|
|
candidates []solarEclipseClosureScanCandidate
|
|
}
|
|
|
|
// centralLimitHorizonRootsByScan 返回一侧窗口内全部地平闭包根,按时间排序。
|
|
func (solver solarEclipseSolver) centralLimitHorizonRootsByScan(
|
|
shadowContactJDE, innerContactJDE float64,
|
|
) []SolarEclipsePathPoint {
|
|
low, high := math.Min(shadowContactJDE, innerContactJDE), math.Max(shadowContactJDE, innerContactJDE)
|
|
if low <= 0 || high <= low {
|
|
return nil
|
|
}
|
|
// 闭包弧可以把一个根放在接触窗口之外,窗口按既有口径外扩。
|
|
margin := solarEclipseCentralLimitHorizonContactMarginDays
|
|
if span := high - low; span > 0 {
|
|
margin = math.Max(margin, span*0.15)
|
|
}
|
|
start, end := low-margin, high+margin
|
|
rows := solarEclipseClosureScanRowCount(end - start)
|
|
step := (end - start) / float64(rows)
|
|
scan := make([]solarEclipseClosureScanRow, rows+1)
|
|
for index := range scan {
|
|
scan[index] = solarEclipseClosureScanRow{
|
|
jde: start + float64(index)*step,
|
|
candidates: solver.closureScanCandidatesAt(start + float64(index)*step),
|
|
}
|
|
}
|
|
roots := make([]SolarEclipsePathPoint, 0, 2)
|
|
for index := 1; index < len(scan); index++ {
|
|
for _, bracket := range solarEclipseClosureScanBrackets(scan[index-1], scan[index]) {
|
|
root, ok := solver.refineSolarEclipseClosureScanRoot(bracket)
|
|
if !ok || root.JDE < low-margin || root.JDE > high+margin ||
|
|
solarEclipseRiseSetPointExists(roots, root) {
|
|
continue
|
|
}
|
|
roots = append(roots, root)
|
|
}
|
|
}
|
|
sort.Slice(roots, func(first, second int) bool { return roots[first].JDE < roots[second].JDE })
|
|
return roots
|
|
}
|
|
|
|
func solarEclipseClosureScanRowCount(spanDays float64) int {
|
|
rows := int(spanDays/solarEclipseClosureScanStepDays + 0.5)
|
|
if rows < solarEclipseClosureScanMinimumRows {
|
|
rows = solarEclipseClosureScanMinimumRows
|
|
}
|
|
if rows > solarEclipseClosureScanMaximumRows {
|
|
rows = solarEclipseClosureScanMaximumRows
|
|
}
|
|
return rows
|
|
}
|
|
|
|
// closureScanCandidatesAt 枚举该时刻地平线上全部 ∂gap/∂t=0 的点,并给出各点的 gap。
|
|
func (solver solarEclipseSolver) closureScanCandidatesAt(jde float64) []solarEclipseClosureScanCandidate {
|
|
evaluation := solver.closureScanEvaluationAt(jde)
|
|
sun := solarEclipseXYZToLLR(
|
|
evaluation.center.sunXYZ[0], evaluation.center.sunXYZ[1], evaluation.center.sunXYZ[2],
|
|
)
|
|
centerLongitude := normalizeLongitude((sun[0] - evaluation.center.gst) / rad)
|
|
centerLatitude := sun[1] / rad
|
|
valueAt := func(angle float64) (float64, bool) {
|
|
longitude, latitude := riseSetHorizonPoint(centerLongitude, centerLatitude, angle)
|
|
rate := evaluation.centralContactDerivative(longitude, latitude)
|
|
return rate, finite(rate)
|
|
}
|
|
angles := riseSetCyclicRootsWithFoldTolerance(
|
|
solarEclipseClosureScanBoundaryPoints, solarEclipseClosureScanRateFoldTolerance, valueAt,
|
|
)
|
|
candidates := make([]solarEclipseClosureScanCandidate, 0, len(angles))
|
|
for _, angle := range angles {
|
|
longitude, latitude := riseSetHorizonPoint(centerLongitude, centerLatitude, angle)
|
|
candidate, ok := solver.closureScanCandidateAt(evaluation, longitude, latitude)
|
|
if !ok {
|
|
continue
|
|
}
|
|
candidates = append(candidates, candidate)
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
// closureScanCandidateAt 把固定时刻的地理起点修正到 {∂gap/∂t=0, alt=0} 上。
|
|
func (solver solarEclipseSolver) closureScanCandidateAt(
|
|
evaluation solarEclipseRiseSetEvaluation,
|
|
longitude, latitude float64,
|
|
) (solarEclipseClosureScanCandidate, bool) {
|
|
// 不能沿用 riseSetRefineGeographicRoot:∂gap/∂t 是 5 s 有限差分,残差噪声约 1e-7,
|
|
// 该函数的收尾判据 1e-8 永远达不到;这里的容差与三维牛顿的收敛判据保持一致。
|
|
const stepDegrees = 1e-4
|
|
residualAt := func(longitude, latitude float64) (float64, float64) {
|
|
state := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
|
|
return evaluation.centralContactDerivative(longitude, latitude), state.sunAltitudeRad
|
|
}
|
|
for iteration := 0; iteration < 8; iteration++ {
|
|
rate, altitude := residualAt(longitude, latitude)
|
|
if !finite(rate) || !finite(altitude) {
|
|
return solarEclipseClosureScanCandidate{}, false
|
|
}
|
|
if math.Abs(rate) <= 1e-7 && math.Abs(altitude) <= 1e-9 {
|
|
break
|
|
}
|
|
shiftedLongitudeRate, shiftedLongitudeAltitude := residualAt(longitude+stepDegrees, latitude)
|
|
shiftedLatitudeRate, shiftedLatitudeAltitude := residualAt(longitude, latitude+stepDegrees)
|
|
a := (shiftedLongitudeRate - rate) / stepDegrees
|
|
b := (shiftedLatitudeRate - rate) / stepDegrees
|
|
c := (shiftedLongitudeAltitude - altitude) / stepDegrees
|
|
d := (shiftedLatitudeAltitude - altitude) / stepDegrees
|
|
determinant := a*d - b*c
|
|
if !finite(determinant) || math.Abs(determinant) < 1e-18 {
|
|
return solarEclipseClosureScanCandidate{}, false
|
|
}
|
|
deltaLongitude := (-rate*d + b*altitude) / determinant
|
|
deltaLatitude := (c*rate - a*altitude) / determinant
|
|
if scale := math.Max(math.Abs(deltaLongitude), math.Abs(deltaLatitude)); scale > 5 {
|
|
deltaLongitude *= 5 / scale
|
|
deltaLatitude *= 5 / scale
|
|
}
|
|
longitude += deltaLongitude
|
|
latitude += deltaLatitude
|
|
if !finite(longitude) || !finite(latitude) || latitude <= -89.999 || latitude >= 89.999 {
|
|
return solarEclipseClosureScanCandidate{}, false
|
|
}
|
|
}
|
|
rate, altitude := residualAt(longitude, latitude)
|
|
if !finite(rate) || !finite(altitude) || math.Abs(rate) > 1e-6 || math.Abs(altitude) > 1e-8 {
|
|
return solarEclipseClosureScanCandidate{}, false
|
|
}
|
|
state := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
|
|
gap := solarEclipseCentralContactGap(state)
|
|
if !finite(gap) {
|
|
return solarEclipseClosureScanCandidate{}, false
|
|
}
|
|
return solarEclipseClosureScanCandidate{
|
|
jde: evaluation.jd, longitude: longitude, latitude: latitude, gap: gap,
|
|
}, true
|
|
}
|
|
|
|
// closureScanEvaluationAt 同时给出中心与前后时刻的星历态:时间导数需要前后两点。
|
|
func (solver solarEclipseSolver) closureScanEvaluationAt(jde float64) solarEclipseRiseSetEvaluation {
|
|
return solarEclipseRiseSetEvaluation{
|
|
jd: jde,
|
|
center: solver.localStateContextAt(jde),
|
|
before: solver.localStateContextAt(jde - solarEclipseRiseSetDerivativeStepDays),
|
|
after: solver.localStateContextAt(jde + solarEclipseRiseSetDerivativeStepDays),
|
|
}
|
|
}
|
|
|
|
// solarEclipseClosureScanBrackets 在相邻两行之间按最近距离配对,返回 gap 变号的分支区间。
|
|
func solarEclipseClosureScanBrackets(
|
|
previous, current solarEclipseClosureScanRow,
|
|
) [][2]solarEclipseClosureScanCandidate {
|
|
if len(previous.candidates) == 0 || len(current.candidates) == 0 {
|
|
return nil
|
|
}
|
|
type pair struct {
|
|
previous int
|
|
current int
|
|
distance float64
|
|
}
|
|
pairs := make([]pair, 0, len(previous.candidates)*len(current.candidates))
|
|
for first := range previous.candidates {
|
|
for second := range current.candidates {
|
|
distance := solarEclipsePathDistanceKM(
|
|
SolarEclipsePathPoint{
|
|
Longitude: previous.candidates[first].longitude,
|
|
Latitude: previous.candidates[first].latitude,
|
|
},
|
|
SolarEclipsePathPoint{
|
|
Longitude: current.candidates[second].longitude,
|
|
Latitude: current.candidates[second].latitude,
|
|
},
|
|
)
|
|
if distance > solarEclipseClosureScanMatchKM {
|
|
continue
|
|
}
|
|
pairs = append(pairs, pair{first, second, distance})
|
|
}
|
|
}
|
|
sort.Slice(pairs, func(first, second int) bool { return pairs[first].distance < pairs[second].distance })
|
|
usedPrevious := make([]bool, len(previous.candidates))
|
|
usedCurrent := make([]bool, len(current.candidates))
|
|
brackets := make([][2]solarEclipseClosureScanCandidate, 0, 2)
|
|
for _, candidate := range pairs {
|
|
if usedPrevious[candidate.previous] || usedCurrent[candidate.current] {
|
|
continue
|
|
}
|
|
usedPrevious[candidate.previous] = true
|
|
usedCurrent[candidate.current] = true
|
|
left, right := previous.candidates[candidate.previous], current.candidates[candidate.current]
|
|
if (left.gap <= 0) != (right.gap <= 0) {
|
|
brackets = append(brackets, [2]solarEclipseClosureScanCandidate{left, right})
|
|
}
|
|
}
|
|
return brackets
|
|
}
|
|
|
|
// refineSolarEclipseClosureScanRoot 在时间上二分 gap 的零点,再用既有三维牛顿抛光到同一容差。
|
|
func (solver solarEclipseSolver) refineSolarEclipseClosureScanRoot(
|
|
bracket [2]solarEclipseClosureScanCandidate,
|
|
) (SolarEclipsePathPoint, bool) {
|
|
start, end := bracket[0], bracket[1]
|
|
if start.gap == 0 {
|
|
return solver.closureScanPointAt(start)
|
|
}
|
|
for iteration := 0; iteration < solarEclipseClosureScanBisectionSteps; iteration++ {
|
|
if end.jde-start.jde <= 1e-9 {
|
|
break
|
|
}
|
|
middle := 0.5 * (start.jde + end.jde)
|
|
longitude := 0.5 * (start.longitude + end.longitude)
|
|
latitude := 0.5 * (start.latitude + end.latitude)
|
|
candidate, ok := solver.closureScanCandidateAt(
|
|
solver.closureScanEvaluationAt(middle), longitude, latitude,
|
|
)
|
|
if !ok {
|
|
return SolarEclipsePathPoint{}, false
|
|
}
|
|
if (start.gap <= 0) == (candidate.gap <= 0) {
|
|
start = candidate
|
|
} else {
|
|
end = candidate
|
|
}
|
|
}
|
|
return solver.closureScanPointAt(start)
|
|
}
|
|
|
|
// closureScanPointAt 把扫描候选抛光到三维系统的同一容差;扫描只负责给出种子。
|
|
func (solver solarEclipseSolver) closureScanPointAt(
|
|
candidate solarEclipseClosureScanCandidate,
|
|
) (SolarEclipsePathPoint, bool) {
|
|
polished, ok := solveSolarEclipseCentralLimitHorizonRoot(
|
|
solver, [3]float64{candidate.longitude, candidate.latitude, candidate.jde},
|
|
)
|
|
if !ok {
|
|
return SolarEclipsePathPoint{}, false
|
|
}
|
|
point := SolarEclipsePathPoint{
|
|
JDE: candidate.jde, Longitude: candidate.longitude, Latitude: candidate.latitude,
|
|
}
|
|
if solarEclipsePathDistanceKM(polished, point) > solarEclipseClosureScanPolishKM {
|
|
return SolarEclipsePathPoint{}, false
|
|
}
|
|
return polished, true
|
|
}
|