Files
astro/basic/solar_eclipse_band_closure_scan.go
T

284 lines
11 KiB
Go
Raw Normal View History

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
}