1182 lines
41 KiB
Go
1182 lines
41 KiB
Go
|
|
package basic
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"sort"
|
||
|
|
)
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) magnitudeContourSegments(
|
||
|
|
startJDE, endJDE, centralStartJDE, centralEndJDE,
|
||
|
|
greatestJDE, magnitude, fallbackStepDays float64, hybrid bool,
|
||
|
|
) [][]SolarEclipsePathPoint {
|
||
|
|
return solver.magnitudeContourSegmentsWithSpacing(
|
||
|
|
startJDE, endJDE, centralStartJDE, centralEndJDE,
|
||
|
|
greatestJDE, magnitude, fallbackStepDays, hybrid,
|
||
|
|
solarEclipseMagnitudeContourTargetSpacingKM,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) magnitudeContourSegmentsWithSpacing(
|
||
|
|
startJDE, endJDE, centralStartJDE, centralEndJDE,
|
||
|
|
greatestJDE, magnitude, fallbackStepDays float64, hybrid bool,
|
||
|
|
targetSpacingKM float64,
|
||
|
|
) [][]SolarEclipsePathPoint {
|
||
|
|
if targetSpacingKM <= 0 || math.IsNaN(targetSpacingKM) || math.IsInf(targetSpacingKM, 0) {
|
||
|
|
targetSpacingKM = solarEclipseMagnitudeContourTargetSpacingKM
|
||
|
|
}
|
||
|
|
var transitions []SolarEclipsePathPoint
|
||
|
|
if hybrid && math.Abs(magnitude-1) <= 1e-12 {
|
||
|
|
transitions = solver.centralMagnitudeOneTransitionsInInterval(
|
||
|
|
centralStartJDE, centralEndJDE, greatestJDE, fallbackStepDays,
|
||
|
|
)
|
||
|
|
if len(transitions) == 2 {
|
||
|
|
if segments := solver.hybridMagnitudeOneLimitSegments(transitions, fallbackStepDays); len(segments) == 2 {
|
||
|
|
return segments
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
seeds := solver.magnitudeContourPointsAt(greatestJDE, magnitude)
|
||
|
|
if len(seeds) == 0 {
|
||
|
|
times, _ := solarEclipsePathSampleTimes(startJDE, endJDE, greatestJDE, fallbackStepDays)
|
||
|
|
seedIndex := sort.SearchFloat64s(times, greatestJDE)
|
||
|
|
for offset := 1; offset < len(times); offset++ {
|
||
|
|
for _, index := range []int{seedIndex - offset, seedIndex + offset} {
|
||
|
|
if index < 0 || index >= len(times) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
seeds = solver.magnitudeContourPointsAt(times[index], magnitude)
|
||
|
|
if len(seeds) > 0 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(seeds) > 0 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
segments := make([][]SolarEclipsePathPoint, 0, len(seeds))
|
||
|
|
constrainBranch := hybrid && math.Abs(magnitude-1) <= 1e-12
|
||
|
|
for _, seed := range seeds {
|
||
|
|
backward := solver.traceMagnitudeContourArc(
|
||
|
|
seed, magnitude, -1, greatestJDE, startJDE, endJDE, targetSpacingKM, constrainBranch,
|
||
|
|
)
|
||
|
|
forward := solver.traceMagnitudeContourArc(
|
||
|
|
seed, magnitude, 1, greatestJDE, startJDE, endJDE, targetSpacingKM, constrainBranch,
|
||
|
|
)
|
||
|
|
segment := make([]SolarEclipsePathPoint, 0, len(backward)+len(forward)-1)
|
||
|
|
for index := len(backward) - 1; index >= 0; index-- {
|
||
|
|
segment = append(segment, backward[index])
|
||
|
|
}
|
||
|
|
segment = append(segment, forward[1:]...)
|
||
|
|
if len(segment) >= 2 {
|
||
|
|
segments = append(segments, segment)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(transitions) > 0 {
|
||
|
|
segments = solver.completeMagnitudeOneContourTransitions(segments, transitions)
|
||
|
|
}
|
||
|
|
// A grazing zero-magnitude solve can converge to a degenerate branch that
|
||
|
|
// only touches the horizon at its endpoints and otherwise lies below it.
|
||
|
|
// That branch is not part of the visible envelope; retaining it creates a
|
||
|
|
// short backward spur when the phase curves are polygonized. Keep branches
|
||
|
|
// with a measurable above-horizon portion and let rise/set curves represent
|
||
|
|
// the exact tangent contact itself.
|
||
|
|
visible := segments[:0]
|
||
|
|
for _, segment := range segments {
|
||
|
|
maximumAltitude := math.Inf(-1)
|
||
|
|
for _, point := range segment {
|
||
|
|
maximumAltitude = math.Max(maximumAltitude, point.SunAltitude)
|
||
|
|
}
|
||
|
|
if maximumAltitude > 1e-6 {
|
||
|
|
visible = append(visible, segment)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
segments = visible
|
||
|
|
return segments
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) hybridMagnitudeOneLimitSegments(
|
||
|
|
transitions []SolarEclipsePathPoint,
|
||
|
|
fallbackStepDays float64,
|
||
|
|
) [][]SolarEclipsePathPoint {
|
||
|
|
if len(transitions) != 2 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
start, end := transitions[0], transitions[1]
|
||
|
|
if start.JDE > end.JDE {
|
||
|
|
start, end = end, start
|
||
|
|
}
|
||
|
|
stepDays := math.Min(fallbackStepDays, (end.JDE-start.JDE)/64)
|
||
|
|
centerLine, _ := solver.centralPathPoints(
|
||
|
|
start.JDE,
|
||
|
|
end.JDE,
|
||
|
|
(start.JDE+end.JDE)/2,
|
||
|
|
SolarEclipsePathOptions{
|
||
|
|
StepDays: stepDays,
|
||
|
|
TargetSpacingKM: solarEclipseMagnitudeContourTargetSpacingKM,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
northern, southern := solver.centralPathLimits(centerLine)
|
||
|
|
if len(northern) < 2 || len(northern) != len(southern) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
for _, segment := range [][]SolarEclipsePathPoint{northern, southern} {
|
||
|
|
segment[0] = start
|
||
|
|
segment[0].WidthKM = 0
|
||
|
|
segment[len(segment)-1] = end
|
||
|
|
segment[len(segment)-1].WidthKM = 0
|
||
|
|
}
|
||
|
|
return [][]SolarEclipsePathPoint{northern, southern}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) centralMagnitudeOneTransitionsInInterval(
|
||
|
|
startJDE, endJDE, greatestJDE, stepDays float64,
|
||
|
|
) []SolarEclipsePathPoint {
|
||
|
|
if startJDE == 0 || endJDE == 0 || endJDE <= startJDE {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
transitionStepDays := math.Min(stepDays, (endJDE-startJDE)/64)
|
||
|
|
times, _ := solarEclipsePathSampleTimes(startJDE, endJDE, greatestJDE, transitionStepDays)
|
||
|
|
edgeOffset := math.Min(1.0/86400.0, (endJDE-startJDE)/1000)
|
||
|
|
if edgeOffset > 0 {
|
||
|
|
times = append(times, startJDE+edgeOffset, endJDE-edgeOffset)
|
||
|
|
sort.Float64s(times)
|
||
|
|
times = uniqueSolarEclipsePathTimes(times)
|
||
|
|
}
|
||
|
|
points := make([]SolarEclipsePathPoint, 0, len(times))
|
||
|
|
for _, jd := range times {
|
||
|
|
if point, ok := solver.centralPathPointAt(jd); ok {
|
||
|
|
points = append(points, point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return solver.centralMagnitudeOneTransitions(points)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) centralMagnitudeOneTransitions(
|
||
|
|
points []SolarEclipsePathPoint,
|
||
|
|
) []SolarEclipsePathPoint {
|
||
|
|
if len(points) < 2 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
transitions := make([]SolarEclipsePathPoint, 0, 2)
|
||
|
|
previousPoint := points[0]
|
||
|
|
previousGap, previousOK := solver.centralMagnitudeOneGap(previousPoint)
|
||
|
|
for _, point := range points[1:] {
|
||
|
|
gap, ok := solver.centralMagnitudeOneGap(point)
|
||
|
|
if previousOK && ok && previousGap*gap <= 0 {
|
||
|
|
transition, transitionOK := solver.refineCentralMagnitudeOneTransition(
|
||
|
|
previousPoint, point, previousGap, gap,
|
||
|
|
)
|
||
|
|
if transitionOK && !solarEclipseRiseSetPointExists(transitions, transition) {
|
||
|
|
transitions = append(transitions, transition)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
previousPoint, previousGap, previousOK = point, gap, ok
|
||
|
|
}
|
||
|
|
return transitions
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) centralMagnitudeOneGap(point SolarEclipsePathPoint) (float64, bool) {
|
||
|
|
context := solver.localStateContextAt(point.JDE)
|
||
|
|
state := context.stateAt(point.Longitude*rad, point.Latitude*rad, 0)
|
||
|
|
gap := state.moonInnerRadiusRad/state.sunRadiusRad - 1
|
||
|
|
return gap, finite(gap)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) refineCentralMagnitudeOneTransition(
|
||
|
|
left, right SolarEclipsePathPoint,
|
||
|
|
leftGap, rightGap float64,
|
||
|
|
) (SolarEclipsePathPoint, bool) {
|
||
|
|
if left.JDE > right.JDE {
|
||
|
|
left, right = right, left
|
||
|
|
leftGap, rightGap = rightGap, leftGap
|
||
|
|
}
|
||
|
|
if leftGap*rightGap > 0 {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
if math.Abs(leftGap) <= 1e-12 {
|
||
|
|
return left, true
|
||
|
|
}
|
||
|
|
if math.Abs(rightGap) <= 1e-12 {
|
||
|
|
return right, true
|
||
|
|
}
|
||
|
|
for iteration := 0; iteration < 64 && right.JDE-left.JDE > solarEclipsePathDuplicateTimeDays; iteration++ {
|
||
|
|
middle, ok := solver.centralPathPointAt((left.JDE + right.JDE) / 2)
|
||
|
|
if !ok {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
middleGap, ok := solver.centralMagnitudeOneGap(middle)
|
||
|
|
if !ok {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
if math.Abs(middleGap) <= 1e-12 {
|
||
|
|
return middle, true
|
||
|
|
}
|
||
|
|
if leftGap*middleGap <= 0 {
|
||
|
|
right, rightGap = middle, middleGap
|
||
|
|
} else {
|
||
|
|
left, leftGap = middle, middleGap
|
||
|
|
}
|
||
|
|
}
|
||
|
|
transition, ok := solver.centralPathPointAt((left.JDE + right.JDE) / 2)
|
||
|
|
if !ok {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
gap, ok := solver.centralMagnitudeOneGap(transition)
|
||
|
|
return transition, ok && math.Abs(gap) <= 1e-8
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) completeMagnitudeOneContourTransitions(
|
||
|
|
segments [][]SolarEclipsePathPoint,
|
||
|
|
transitions []SolarEclipsePathPoint,
|
||
|
|
) [][]SolarEclipsePathPoint {
|
||
|
|
if len(transitions) == 0 {
|
||
|
|
return segments
|
||
|
|
}
|
||
|
|
const maximumTimeGapDays = 5.0 / 1440.0
|
||
|
|
for segmentIndex, segment := range segments {
|
||
|
|
if len(segment) < 2 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
for _, atStart := range []bool{true, false} {
|
||
|
|
endpointIndex := len(segment) - 1
|
||
|
|
if atStart {
|
||
|
|
endpointIndex = 0
|
||
|
|
}
|
||
|
|
endpoint := segment[endpointIndex]
|
||
|
|
bestIndex := -1
|
||
|
|
bestDistance := math.Inf(1)
|
||
|
|
for transitionIndex, transition := range transitions {
|
||
|
|
distance := solarEclipsePathDistanceKM(endpoint, transition)
|
||
|
|
if distance < bestDistance {
|
||
|
|
bestIndex, bestDistance = transitionIndex, distance
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if bestIndex < 0 || bestDistance > 3000 ||
|
||
|
|
math.Abs(endpoint.JDE-transitions[bestIndex].JDE) > maximumTimeGapDays {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
transition := transitions[bestIndex]
|
||
|
|
transition.WidthKM = 0
|
||
|
|
if bestDistance < 0.01 {
|
||
|
|
segment[endpointIndex] = transition
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if bestDistance > solarEclipseMagnitudeContourTargetSpacingKM {
|
||
|
|
if bridge := solver.magnitudeOneTransitionBridge(transition, endpoint); len(bridge) >= 2 {
|
||
|
|
if atStart {
|
||
|
|
segment = append(bridge[:len(bridge)-1], segment...)
|
||
|
|
} else {
|
||
|
|
for index := len(bridge) - 2; index >= 0; index-- {
|
||
|
|
segment = append(segment, bridge[index])
|
||
|
|
}
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if atStart {
|
||
|
|
segment = append([]SolarEclipsePathPoint{transition}, segment...)
|
||
|
|
} else {
|
||
|
|
segment = append(segment, transition)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
segments[segmentIndex] = segment
|
||
|
|
}
|
||
|
|
return segments
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) magnitudeOneTransitionBridge(
|
||
|
|
transition, endpoint SolarEclipsePathPoint,
|
||
|
|
) []SolarEclipsePathPoint {
|
||
|
|
start, end := transition, endpoint
|
||
|
|
reverse := false
|
||
|
|
if start.JDE > end.JDE {
|
||
|
|
start, end = end, start
|
||
|
|
reverse = true
|
||
|
|
}
|
||
|
|
spanDays := end.JDE - start.JDE
|
||
|
|
if spanDays <= 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
centerLine, _ := solver.centralPathPoints(
|
||
|
|
start.JDE,
|
||
|
|
end.JDE,
|
||
|
|
(start.JDE+end.JDE)/2,
|
||
|
|
SolarEclipsePathOptions{
|
||
|
|
StepDays: math.Min(1.0/1440.0, spanDays/16),
|
||
|
|
TargetSpacingKM: solarEclipseMagnitudeContourTargetSpacingKM,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
first, second := solver.centralPathLimits(centerLine)
|
||
|
|
if len(first) < 2 || len(first) != len(second) {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
bridge := first
|
||
|
|
if reverse {
|
||
|
|
if solarEclipsePathDistanceKM(second[0], endpoint) < solarEclipsePathDistanceKM(first[0], endpoint) {
|
||
|
|
bridge = second
|
||
|
|
}
|
||
|
|
} else if solarEclipsePathDistanceKM(second[len(second)-1], endpoint) <
|
||
|
|
solarEclipsePathDistanceKM(first[len(first)-1], endpoint) {
|
||
|
|
bridge = second
|
||
|
|
}
|
||
|
|
bridge[0], bridge[len(bridge)-1] = start, end
|
||
|
|
for index := range bridge {
|
||
|
|
bridge[index].WidthKM = 0
|
||
|
|
}
|
||
|
|
if reverse {
|
||
|
|
for left, right := 0, len(bridge)-1; left < right; left, right = left+1, right-1 {
|
||
|
|
bridge[left], bridge[right] = bridge[right], bridge[left]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return bridge
|
||
|
|
}
|
||
|
|
|
||
|
|
type solarEclipseMagnitudeArcState struct {
|
||
|
|
coordinates [3]float64
|
||
|
|
tangent [3]float64
|
||
|
|
point SolarEclipsePathPoint
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) traceMagnitudeContourArc(
|
||
|
|
seed SolarEclipsePathPoint,
|
||
|
|
magnitude float64,
|
||
|
|
direction int,
|
||
|
|
referenceJDE, startJDE, endJDE float64,
|
||
|
|
targetSpacingKM float64,
|
||
|
|
constrainBranch bool,
|
||
|
|
) []SolarEclipsePathPoint {
|
||
|
|
state, ok := solver.magnitudeArcStateAt(seed, magnitude, referenceJDE)
|
||
|
|
if !ok {
|
||
|
|
return []SolarEclipsePathPoint{seed}
|
||
|
|
}
|
||
|
|
branchSign, branchConstrained := 0.0, false
|
||
|
|
if constrainBranch {
|
||
|
|
branchSign, branchConstrained = solver.magnitudeContourBranchSign(seed)
|
||
|
|
}
|
||
|
|
for index := range state.tangent {
|
||
|
|
state.tangent[index] *= float64(direction)
|
||
|
|
}
|
||
|
|
points := []SolarEclipsePathPoint{seed}
|
||
|
|
step := solarEclipseMagnitudeContourArcStepDegrees
|
||
|
|
// Continue the F(magnitude, greatest-time)=0 curve in longitude, latitude, and scaled time.
|
||
|
|
for count := 0; count < solarEclipseMagnitudeContourMaxArcSteps; count++ {
|
||
|
|
predictor := state.coordinates
|
||
|
|
for index := range predictor {
|
||
|
|
predictor[index] += step * state.tangent[index]
|
||
|
|
}
|
||
|
|
next, iterations, nextOK := solver.correctMagnitudeContourArc(
|
||
|
|
predictor, state.tangent, magnitude, referenceJDE,
|
||
|
|
)
|
||
|
|
if !nextOK {
|
||
|
|
step /= 2
|
||
|
|
if step < solarEclipseMagnitudeContourMinArcStepDegrees {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if dotSolarEclipse3(next.tangent, state.tangent) < 0 {
|
||
|
|
for index := range next.tangent {
|
||
|
|
next.tangent[index] = -next.tangent[index]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if nextSign, nextConstrained := solver.magnitudeContourBranchSign(next.point); constrainBranch && nextConstrained {
|
||
|
|
if branchConstrained && branchSign*nextSign < 0 {
|
||
|
|
step /= 2
|
||
|
|
if step < solarEclipseMagnitudeContourMinArcStepDegrees {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
branchSign, branchConstrained = nextSign, true
|
||
|
|
}
|
||
|
|
distance := solarEclipsePathDistanceKM(state.point, next.point)
|
||
|
|
if distance > targetSpacingKM || solarEclipseMagnitudeChordErrorKM(state, next, distance) > 2 {
|
||
|
|
step /= 2
|
||
|
|
if step < solarEclipseMagnitudeContourMinArcStepDegrees {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if next.point.JDE < startJDE-0.05 || next.point.JDE > endJDE+0.05 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
if next.point.SunAltitude < 0 {
|
||
|
|
if endpoint, endpointOK := solver.refineMagnitudeHorizonCrossing(state.point, next.point, magnitude); endpointOK {
|
||
|
|
points = append(points, endpoint)
|
||
|
|
break
|
||
|
|
}
|
||
|
|
step /= 2
|
||
|
|
if step < solarEclipseMagnitudeContourMinArcStepDegrees {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
points = append(points, next.point)
|
||
|
|
state = next
|
||
|
|
if next.point.SunAltitude <= 1e-7 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
if distance < targetSpacingKM/2 && iterations <= 4 {
|
||
|
|
step = math.Min(solarEclipseMagnitudeContourArcStepDegrees, step*1.5)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return points
|
||
|
|
}
|
||
|
|
|
||
|
|
// Bound the chord error as well as the spacing near a curved grazing limit.
|
||
|
|
func solarEclipseMagnitudeChordErrorKM(first, second solarEclipseMagnitudeArcState, distance float64) float64 {
|
||
|
|
latitude := (first.point.Latitude + second.point.Latitude) * rad / 2
|
||
|
|
ax, ay := first.tangent[0]*math.Cos(latitude), first.tangent[1]
|
||
|
|
bx, by := second.tangent[0]*math.Cos(latitude), second.tangent[1]
|
||
|
|
norm := math.Hypot(ax, ay) * math.Hypot(bx, by)
|
||
|
|
if norm == 0 {
|
||
|
|
return math.Inf(1)
|
||
|
|
}
|
||
|
|
cosine := math.Max(-1, math.Min(1, (ax*bx+ay*by)/norm))
|
||
|
|
return distance * math.Sqrt(2*(1-cosine)) / 8
|
||
|
|
}
|
||
|
|
|
||
|
|
// magnitudeContourBranchSign identifies which side of the simultaneous
|
||
|
|
// central path a contour point occupies. A magnitude contour can have two
|
||
|
|
// nearby roots near a hybrid transition; keeping this sign prevents Newton
|
||
|
|
// correction from silently switching to the opposite root.
|
||
|
|
func (solver solarEclipseSolver) magnitudeContourBranchSign(point SolarEclipsePathPoint) (float64, bool) {
|
||
|
|
center, centerOK := solver.centralPathPointAt(point.JDE)
|
||
|
|
before, beforeOK := solver.centralPathPointAt(point.JDE - solarEclipsePathVelocityStepDays)
|
||
|
|
after, afterOK := solver.centralPathPointAt(point.JDE + solarEclipsePathVelocityStepDays)
|
||
|
|
if !centerOK || !beforeOK || !afterOK {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
cosLatitude := math.Cos(center.Latitude * rad)
|
||
|
|
pathX := math.Remainder(after.Longitude-before.Longitude, 360) * cosLatitude
|
||
|
|
pathY := after.Latitude - before.Latitude
|
||
|
|
offsetX := math.Remainder(point.Longitude-center.Longitude, 360) * cosLatitude
|
||
|
|
offsetY := point.Latitude - center.Latitude
|
||
|
|
sign := pathX*offsetY - pathY*offsetX
|
||
|
|
if !finite(sign) || math.Abs(sign) <= 1e-10 {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
return sign, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) magnitudeArcStateAt(
|
||
|
|
point SolarEclipsePathPoint,
|
||
|
|
magnitude, referenceJDE float64,
|
||
|
|
) (solarEclipseMagnitudeArcState, bool) {
|
||
|
|
coordinates := [3]float64{
|
||
|
|
point.Longitude,
|
||
|
|
point.Latitude,
|
||
|
|
(point.JDE - referenceJDE) * solarEclipseMagnitudeContourTimeScale,
|
||
|
|
}
|
||
|
|
_, jacobian, ok := solver.magnitudeEnvelopeJacobian(coordinates, magnitude, referenceJDE)
|
||
|
|
if !ok {
|
||
|
|
return solarEclipseMagnitudeArcState{}, false
|
||
|
|
}
|
||
|
|
tangent, ok := solarEclipseMagnitudeArcTangent(jacobian)
|
||
|
|
return solarEclipseMagnitudeArcState{coordinates: coordinates, tangent: tangent, point: point}, ok
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) correctMagnitudeContourArc(
|
||
|
|
predictor, tangent [3]float64,
|
||
|
|
magnitude, referenceJDE float64,
|
||
|
|
) (solarEclipseMagnitudeArcState, int, bool) {
|
||
|
|
coordinates := predictor
|
||
|
|
for iteration := 0; iteration < 16; iteration++ {
|
||
|
|
if !finite(coordinates[0]) || !finite(coordinates[1]) || !finite(coordinates[2]) {
|
||
|
|
return solarEclipseMagnitudeArcState{}, iteration, false
|
||
|
|
}
|
||
|
|
residual, jacobian, ok := solver.magnitudeEnvelopeJacobian(coordinates, magnitude, referenceJDE)
|
||
|
|
if !ok {
|
||
|
|
return solarEclipseMagnitudeArcState{}, iteration, false
|
||
|
|
}
|
||
|
|
planeResidual := dotSolarEclipse3(subtractSolarEclipse3(coordinates, predictor), tangent)
|
||
|
|
if math.Abs(residual[0]) <= 1e-10 && math.Abs(residual[1]) <= 1e-10 && math.Abs(planeResidual) <= 1e-9 {
|
||
|
|
return solver.validMagnitudeArcState(coordinates, jacobian, magnitude, referenceJDE, iteration+1)
|
||
|
|
}
|
||
|
|
matrix := [3][3]float64{jacobian[0], jacobian[1], tangent}
|
||
|
|
delta, ok := solveSolarEclipse3x3(matrix, [3]float64{-residual[0], -residual[1], -planeResidual})
|
||
|
|
if !ok {
|
||
|
|
return solarEclipseMagnitudeArcState{}, iteration, false
|
||
|
|
}
|
||
|
|
norm := math.Sqrt(dotSolarEclipse3(delta, delta))
|
||
|
|
if norm > 2 {
|
||
|
|
for index := range delta {
|
||
|
|
delta[index] *= 2 / norm
|
||
|
|
}
|
||
|
|
}
|
||
|
|
for index := range coordinates {
|
||
|
|
coordinates[index] += delta[index]
|
||
|
|
}
|
||
|
|
if coordinates[1] <= -89.999999 || coordinates[1] >= 89.999999 {
|
||
|
|
return solarEclipseMagnitudeArcState{}, iteration, false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
residual, jacobian, ok := solver.magnitudeEnvelopeJacobian(coordinates, magnitude, referenceJDE)
|
||
|
|
planeResidual := dotSolarEclipse3(subtractSolarEclipse3(coordinates, predictor), tangent)
|
||
|
|
if !ok || math.Abs(residual[0]) > 1e-7 || math.Abs(residual[1]) > 1e-8 || math.Abs(planeResidual) > 1e-7 {
|
||
|
|
return solarEclipseMagnitudeArcState{}, 16, false
|
||
|
|
}
|
||
|
|
return solver.validMagnitudeArcState(coordinates, jacobian, magnitude, referenceJDE, 16)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) validMagnitudeArcState(
|
||
|
|
coordinates [3]float64,
|
||
|
|
jacobian [2][3]float64,
|
||
|
|
magnitude, referenceJDE float64,
|
||
|
|
iterations int,
|
||
|
|
) (solarEclipseMagnitudeArcState, int, bool) {
|
||
|
|
jd := referenceJDE + coordinates[2]/solarEclipseMagnitudeContourTimeScale
|
||
|
|
longitude := normalizeLongitude(coordinates[0])
|
||
|
|
latitude := coordinates[1]
|
||
|
|
evaluation := solver.magnitudeEvaluationAt(jd)
|
||
|
|
state := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
|
||
|
|
if math.Abs(solarEclipseMagnitudeAtTarget(state, magnitude)-magnitude) > 1e-7 ||
|
||
|
|
evaluation.separationSecondDerivative(longitude, latitude) <= 0 {
|
||
|
|
return solarEclipseMagnitudeArcState{}, iterations, false
|
||
|
|
}
|
||
|
|
tangent, ok := solarEclipseMagnitudeArcTangent(jacobian)
|
||
|
|
if !ok {
|
||
|
|
return solarEclipseMagnitudeArcState{}, iterations, false
|
||
|
|
}
|
||
|
|
return solarEclipseMagnitudeArcState{
|
||
|
|
coordinates: coordinates,
|
||
|
|
tangent: tangent,
|
||
|
|
point: SolarEclipsePathPoint{
|
||
|
|
JDE: jd, Longitude: longitude, Latitude: latitude, SunAltitude: state.sunAltitudeRad / rad,
|
||
|
|
},
|
||
|
|
}, iterations, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) magnitudeEnvelopeJacobian(
|
||
|
|
coordinates [3]float64,
|
||
|
|
magnitude, referenceJDE float64,
|
||
|
|
) ([2]float64, [2][3]float64, bool) {
|
||
|
|
jd := referenceJDE + coordinates[2]/solarEclipseMagnitudeContourTimeScale
|
||
|
|
longitude := normalizeLongitude(coordinates[0])
|
||
|
|
latitude := coordinates[1]
|
||
|
|
evaluation := solver.magnitudeEvaluationAt(jd)
|
||
|
|
residual, ok := solarEclipseMagnitudeEnvelopeResidualAt(evaluation, longitude, latitude, magnitude)
|
||
|
|
if !ok {
|
||
|
|
return [2]float64{}, [2][3]float64{}, false
|
||
|
|
}
|
||
|
|
steps := [3]float64{1e-4, 1e-4, 5.0 * solarEclipseMagnitudeContourTimeScale / 86400.0}
|
||
|
|
jacobian := [2][3]float64{}
|
||
|
|
spatialCoordinates := [][2]float64{{longitude + steps[0], latitude}, {longitude, latitude + steps[1]}}
|
||
|
|
for column, shifted := range spatialCoordinates {
|
||
|
|
shiftedResidual, shiftedOK := solarEclipseMagnitudeEnvelopeResidualAt(
|
||
|
|
evaluation, shifted[0], shifted[1], magnitude,
|
||
|
|
)
|
||
|
|
if !shiftedOK {
|
||
|
|
return [2]float64{}, [2][3]float64{}, false
|
||
|
|
}
|
||
|
|
for row := 0; row < 2; row++ {
|
||
|
|
jacobian[row][column] = (shiftedResidual[row] - residual[row]) / steps[column]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
timeEvaluation := solver.magnitudeEvaluationAt(jd + steps[2]/solarEclipseMagnitudeContourTimeScale)
|
||
|
|
timeResidual, timeOK := solarEclipseMagnitudeEnvelopeResidualAt(
|
||
|
|
timeEvaluation, longitude, latitude, magnitude,
|
||
|
|
)
|
||
|
|
if !timeOK {
|
||
|
|
return [2]float64{}, [2][3]float64{}, false
|
||
|
|
}
|
||
|
|
beforeEvaluation := solver.magnitudeEvaluationAt(jd - steps[2]/solarEclipseMagnitudeContourTimeScale)
|
||
|
|
beforeResidual, beforeOK := solarEclipseMagnitudeEnvelopeResidualAt(
|
||
|
|
beforeEvaluation, longitude, latitude, magnitude,
|
||
|
|
)
|
||
|
|
if !beforeOK {
|
||
|
|
return [2]float64{}, [2][3]float64{}, false
|
||
|
|
}
|
||
|
|
for row := 0; row < 2; row++ {
|
||
|
|
jacobian[row][2] = (timeResidual[row] - beforeResidual[row]) / (2 * steps[2])
|
||
|
|
}
|
||
|
|
return residual, jacobian, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipseMagnitudeEnvelopeResidualAt(
|
||
|
|
evaluation solarEclipseRiseSetEvaluation,
|
||
|
|
longitude, latitude, magnitude float64,
|
||
|
|
) ([2]float64, bool) {
|
||
|
|
state := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
|
||
|
|
residual := [2]float64{
|
||
|
|
solarEclipseMagnitudeAtTarget(state, magnitude) - magnitude,
|
||
|
|
evaluation.separationDerivative(longitude, latitude),
|
||
|
|
}
|
||
|
|
return residual, finite(residual[0]) && finite(residual[1])
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipseMagnitudeArcTangent(jacobian [2][3]float64) ([3]float64, bool) {
|
||
|
|
tangent := [3]float64{
|
||
|
|
jacobian[0][1]*jacobian[1][2] - jacobian[0][2]*jacobian[1][1],
|
||
|
|
jacobian[0][2]*jacobian[1][0] - jacobian[0][0]*jacobian[1][2],
|
||
|
|
jacobian[0][0]*jacobian[1][1] - jacobian[0][1]*jacobian[1][0],
|
||
|
|
}
|
||
|
|
norm := math.Sqrt(dotSolarEclipse3(tangent, tangent))
|
||
|
|
if !finite(norm) || norm < 1e-14 {
|
||
|
|
return [3]float64{}, false
|
||
|
|
}
|
||
|
|
for index := range tangent {
|
||
|
|
tangent[index] /= norm
|
||
|
|
}
|
||
|
|
return tangent, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func dotSolarEclipse3(first, second [3]float64) float64 {
|
||
|
|
return first[0]*second[0] + first[1]*second[1] + first[2]*second[2]
|
||
|
|
}
|
||
|
|
|
||
|
|
func subtractSolarEclipse3(first, second [3]float64) [3]float64 {
|
||
|
|
return [3]float64{first[0] - second[0], first[1] - second[1], first[2] - second[2]}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) refineMagnitudeHorizonCrossing(
|
||
|
|
visible, hidden SolarEclipsePathPoint,
|
||
|
|
magnitude float64,
|
||
|
|
) (SolarEclipsePathPoint, bool) {
|
||
|
|
fraction := visible.SunAltitude / (visible.SunAltitude - hidden.SunAltitude)
|
||
|
|
deltaLongitude := math.Remainder(hidden.Longitude-visible.Longitude, 360)
|
||
|
|
return solver.refineMagnitudeHorizonPoint(
|
||
|
|
visible.JDE+fraction*(hidden.JDE-visible.JDE),
|
||
|
|
normalizeLongitude(visible.Longitude+fraction*deltaLongitude),
|
||
|
|
visible.Latitude+fraction*(hidden.Latitude-visible.Latitude),
|
||
|
|
magnitude,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) refineMagnitudeHorizonPoint(
|
||
|
|
jd, longitude, latitude, magnitude float64,
|
||
|
|
) (SolarEclipsePathPoint, bool) {
|
||
|
|
const (
|
||
|
|
geographicStep = 1e-4
|
||
|
|
timeStep = 5.0 / 86400.0
|
||
|
|
)
|
||
|
|
for iteration := 0; iteration < 24; iteration++ {
|
||
|
|
evaluation := solver.magnitudeEvaluationAt(jd)
|
||
|
|
residual, ok := solarEclipseMagnitudeHorizonResidualAt(evaluation, longitude, latitude, magnitude)
|
||
|
|
if !ok {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
if math.Abs(residual[0]) <= 1e-10 && math.Abs(residual[1]) <= 1e-10 && math.Abs(residual[2]) <= 1e-10 {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
longitudeResidual, lonOK := solarEclipseMagnitudeHorizonResidualAt(
|
||
|
|
evaluation, longitude+geographicStep, latitude, magnitude,
|
||
|
|
)
|
||
|
|
latitudeResidual, latOK := solarEclipseMagnitudeHorizonResidualAt(
|
||
|
|
evaluation, longitude, latitude+geographicStep, magnitude,
|
||
|
|
)
|
||
|
|
timeResidual, timeOK := solver.magnitudeHorizonResidual(jd+timeStep, longitude, latitude, magnitude)
|
||
|
|
if !lonOK || !latOK || !timeOK {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
matrix := [3][3]float64{}
|
||
|
|
for row := 0; row < 3; row++ {
|
||
|
|
matrix[row][0] = (longitudeResidual[row] - residual[row]) / geographicStep
|
||
|
|
matrix[row][1] = (latitudeResidual[row] - residual[row]) / geographicStep
|
||
|
|
matrix[row][2] = (timeResidual[row] - residual[row]) / timeStep
|
||
|
|
}
|
||
|
|
delta, ok := solveSolarEclipse3x3(matrix, [3]float64{-residual[0], -residual[1], -residual[2]})
|
||
|
|
if !ok {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
geographicScale := math.Max(math.Abs(delta[0]), math.Abs(delta[1]))
|
||
|
|
if geographicScale > 2 {
|
||
|
|
delta[0] *= 2 / geographicScale
|
||
|
|
delta[1] *= 2 / geographicScale
|
||
|
|
}
|
||
|
|
if math.Abs(delta[2]) > 10.0/1440.0 {
|
||
|
|
delta[2] = math.Copysign(10.0/1440.0, delta[2])
|
||
|
|
}
|
||
|
|
longitude = normalizeLongitude(longitude + delta[0])
|
||
|
|
latitude += delta[1]
|
||
|
|
jd += delta[2]
|
||
|
|
}
|
||
|
|
residual, ok := solver.magnitudeHorizonResidual(jd, longitude, latitude, magnitude)
|
||
|
|
if !ok || math.Abs(residual[0]) > 1e-7 || math.Abs(residual[1]) > 1e-8 || math.Abs(residual[2]) > 1e-7 {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
evaluation := solver.magnitudeEvaluationAt(jd)
|
||
|
|
if evaluation.separationSecondDerivative(longitude, latitude) <= 0 {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
return SolarEclipsePathPoint{
|
||
|
|
JDE: jd, Longitude: longitude, Latitude: latitude, SunAltitude: residual[2] / rad,
|
||
|
|
}, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) magnitudeEvaluationAt(jd float64) solarEclipseRiseSetEvaluation {
|
||
|
|
return solarEclipseRiseSetEvaluation{
|
||
|
|
jd: jd,
|
||
|
|
center: solver.localStateContextAt(jd),
|
||
|
|
before: solver.localStateContextAt(jd - solarEclipseRiseSetDerivativeStepDays),
|
||
|
|
after: solver.localStateContextAt(jd + solarEclipseRiseSetDerivativeStepDays),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) magnitudeCandidateEvaluationAt(jd float64) solarEclipseRiseSetEvaluation {
|
||
|
|
return solarEclipseRiseSetEvaluation{
|
||
|
|
jd: jd,
|
||
|
|
center: solver.localStateContextCandidateAt(jd),
|
||
|
|
before: solver.localStateContextCandidateAt(jd - solarEclipseRiseSetDerivativeStepDays),
|
||
|
|
after: solver.localStateContextCandidateAt(jd + solarEclipseRiseSetDerivativeStepDays),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) localStateContextAt(jd float64) localSolarEclipseStateContext {
|
||
|
|
if solver.localStateContextCache == nil {
|
||
|
|
return newLocalSolarEclipseStateContextWithOverride(jd, solver.deltaTSeconds, solver.params)
|
||
|
|
}
|
||
|
|
key := math.Float64bits(jd)
|
||
|
|
if context, ok := solver.localStateContextCache[key]; ok &&
|
||
|
|
context.generation == deltaTGenerationValue() {
|
||
|
|
return context
|
||
|
|
}
|
||
|
|
context := newLocalSolarEclipseStateContextWithOverride(jd, solver.deltaTSeconds, solver.params)
|
||
|
|
return storeLocalSolarEclipseStateContext(solver.localStateContextCache, key, context)
|
||
|
|
}
|
||
|
|
|
||
|
|
func storeLocalSolarEclipseStateContext(
|
||
|
|
cache map[uint64]localSolarEclipseStateContext,
|
||
|
|
key uint64,
|
||
|
|
context localSolarEclipseStateContext,
|
||
|
|
) localSolarEclipseStateContext {
|
||
|
|
if cache == nil {
|
||
|
|
return context
|
||
|
|
}
|
||
|
|
// 与贝塞尔几何缓存同口径:事件级缓存不维护淘汰表,超过上限整体作废更可预测。
|
||
|
|
if _, exists := cache[key]; !exists && len(cache) >= solarEclipseBesselGeometryCacheMaximumEntries {
|
||
|
|
for cachedKey := range cache {
|
||
|
|
delete(cache, cachedKey)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
context.generation = deltaTGenerationValue()
|
||
|
|
cache[key] = context
|
||
|
|
return context
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) localStateContextCandidateAt(jd float64) localSolarEclipseStateContext {
|
||
|
|
if solver.localEphemeris == nil {
|
||
|
|
return solver.localStateContextAt(jd)
|
||
|
|
}
|
||
|
|
sun, moon, ok := solver.localEphemeris.equatorialAt(jd)
|
||
|
|
if !ok {
|
||
|
|
return solver.localStateContextAt(jd)
|
||
|
|
}
|
||
|
|
return localSolarEclipseStateContext{
|
||
|
|
sunXYZ: solarEclipseLLRToXYZ(sun[0], sun[1], sun[2]),
|
||
|
|
moonXYZ: solarEclipseLLRToXYZ(moon[0], moon[1], moon[2]),
|
||
|
|
gst: solver.siderealTimeAt(jd),
|
||
|
|
params: solver.params,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) magnitudeHorizonResidual(
|
||
|
|
jd, longitude, latitude, magnitude float64,
|
||
|
|
) ([3]float64, bool) {
|
||
|
|
evaluation := solver.magnitudeEvaluationAt(jd)
|
||
|
|
return solarEclipseMagnitudeHorizonResidualAt(evaluation, longitude, latitude, magnitude)
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipseMagnitudeHorizonResidualAt(
|
||
|
|
evaluation solarEclipseRiseSetEvaluation,
|
||
|
|
longitude, latitude, magnitude float64,
|
||
|
|
) ([3]float64, bool) {
|
||
|
|
state := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
|
||
|
|
value := [3]float64{
|
||
|
|
solarEclipseMagnitudeAtTarget(state, magnitude) - magnitude,
|
||
|
|
evaluation.separationDerivative(longitude, latitude),
|
||
|
|
state.sunAltitudeRad,
|
||
|
|
}
|
||
|
|
return value, finite(value[0]) && finite(value[1]) && finite(value[2])
|
||
|
|
}
|
||
|
|
|
||
|
|
func solveSolarEclipse3x3(matrix [3][3]float64, right [3]float64) ([3]float64, bool) {
|
||
|
|
augmented := [3][4]float64{}
|
||
|
|
for row := 0; row < 3; row++ {
|
||
|
|
copy(augmented[row][:3], matrix[row][:])
|
||
|
|
augmented[row][3] = right[row]
|
||
|
|
}
|
||
|
|
for column := 0; column < 3; column++ {
|
||
|
|
pivot := column
|
||
|
|
for row := column + 1; row < 3; row++ {
|
||
|
|
if math.Abs(augmented[row][column]) > math.Abs(augmented[pivot][column]) {
|
||
|
|
pivot = row
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !finite(augmented[pivot][column]) || math.Abs(augmented[pivot][column]) < 1e-18 {
|
||
|
|
return [3]float64{}, false
|
||
|
|
}
|
||
|
|
augmented[column], augmented[pivot] = augmented[pivot], augmented[column]
|
||
|
|
for row := column + 1; row < 3; row++ {
|
||
|
|
factor := augmented[row][column] / augmented[column][column]
|
||
|
|
for index := column; index < 4; index++ {
|
||
|
|
augmented[row][index] -= factor * augmented[column][index]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
result := [3]float64{}
|
||
|
|
for row := 2; row >= 0; row-- {
|
||
|
|
value := augmented[row][3]
|
||
|
|
for column := row + 1; column < 3; column++ {
|
||
|
|
value -= augmented[row][column] * result[column]
|
||
|
|
}
|
||
|
|
result[row] = value / augmented[row][row]
|
||
|
|
if !finite(result[row]) {
|
||
|
|
return [3]float64{}, false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return result, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) magnitudeContourPointsAt(jd, magnitude float64) []SolarEclipsePathPoint {
|
||
|
|
moon := solver.besselMoonAt(jd)
|
||
|
|
axis := solver.besselAxisAt(jd)
|
||
|
|
evaluation := solver.magnitudeEvaluationAt(jd)
|
||
|
|
valueAt := func(angle float64) (float64, bool) {
|
||
|
|
point, ok := solver.magnitudeContourPointAt(
|
||
|
|
jd, moon, axis, math.Cos(angle), math.Sin(angle), magnitude,
|
||
|
|
)
|
||
|
|
if !ok {
|
||
|
|
return 0, false
|
||
|
|
}
|
||
|
|
return evaluation.separationDerivative(point.Longitude, point.Latitude), true
|
||
|
|
}
|
||
|
|
points := make([]SolarEclipsePathPoint, 0, 2)
|
||
|
|
for _, angle := range riseSetCyclicRoots(solarEclipseMagnitudeContourBoundaryPoints, valueAt) {
|
||
|
|
seed, ok := solver.magnitudeContourPointAt(
|
||
|
|
jd, moon, axis, math.Cos(angle), math.Sin(angle), magnitude,
|
||
|
|
)
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
longitude, latitude, ok := solver.refineMagnitudeEnvelopePoint(
|
||
|
|
magnitude, seed.Longitude, seed.Latitude, evaluation,
|
||
|
|
)
|
||
|
|
if !ok {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
state := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
|
||
|
|
if state.sunAltitudeRad < -1e-7 || evaluation.separationSecondDerivative(longitude, latitude) <= 0 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
point := SolarEclipsePathPoint{
|
||
|
|
JDE: jd, Longitude: longitude, Latitude: latitude, SunAltitude: state.sunAltitudeRad / rad,
|
||
|
|
}
|
||
|
|
if !solarEclipseRiseSetPointExists(points, point) {
|
||
|
|
points = append(points, point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if magnitude == 1 && len(points) < 2 {
|
||
|
|
points = solver.appendMagnitudeOneLimitSeeds(points, jd, evaluation)
|
||
|
|
}
|
||
|
|
if len(points) == 0 {
|
||
|
|
points = solver.magnitudeContourGeographicSeedsAt(jd, magnitude)
|
||
|
|
}
|
||
|
|
return points
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) appendMagnitudeOneLimitSeeds(
|
||
|
|
points []SolarEclipsePathPoint,
|
||
|
|
jd float64,
|
||
|
|
evaluation solarEclipseRiseSetEvaluation,
|
||
|
|
) []SolarEclipsePathPoint {
|
||
|
|
center, ok := solver.centralPathPointAt(jd)
|
||
|
|
if !ok {
|
||
|
|
return points
|
||
|
|
}
|
||
|
|
first, second, ok := solver.centralPathLimitsAt(center)
|
||
|
|
if !ok {
|
||
|
|
return points
|
||
|
|
}
|
||
|
|
for _, seed := range []SolarEclipsePathPoint{first, second} {
|
||
|
|
longitude, latitude, refined := solver.refineMagnitudeEnvelopePoint(
|
||
|
|
1, seed.Longitude, seed.Latitude, evaluation,
|
||
|
|
)
|
||
|
|
if !refined {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
state := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
|
||
|
|
if state.sunAltitudeRad < -1e-7 || evaluation.separationSecondDerivative(longitude, latitude) <= 0 {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
candidate := SolarEclipsePathPoint{
|
||
|
|
JDE: jd, Longitude: longitude, Latitude: latitude, SunAltitude: state.sunAltitudeRad / rad,
|
||
|
|
}
|
||
|
|
if !solarEclipseRiseSetPointExists(points, candidate) {
|
||
|
|
points = append(points, candidate)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return points
|
||
|
|
}
|
||
|
|
|
||
|
|
// magnitudeContourGeographicSeedsAt supplies seeds for contours inside the
|
||
|
|
// signed Bessel umbra. The cone-radius interpolation is well-conditioned for
|
||
|
|
// m<=1, but it can become negative for a legitimate m>1 contour near a
|
||
|
|
// total/annular transition. Solving the local-magnitude envelope from the
|
||
|
|
// greatest point keeps those contours available without changing the normal
|
||
|
|
// Bessel path.
|
||
|
|
func (solver solarEclipseSolver) magnitudeContourGeographicSeedsAt(jd, magnitude float64) []SolarEclipsePathPoint {
|
||
|
|
center, ok := solver.centralPathPointAt(jd)
|
||
|
|
if !ok {
|
||
|
|
// A non-central eclipse has no Earth-intersecting shadow axis, but its
|
||
|
|
// local maximum still has a well-defined geographic stationary point.
|
||
|
|
// Reuse the global greatest-eclipse coordinates as the bounded seed for
|
||
|
|
// the local envelope solver instead of treating the missing central axis
|
||
|
|
// as evidence that every magnitude contour is absent.
|
||
|
|
result := solarEclipse(solver.newMoonJDE, solver.model)
|
||
|
|
if !result.HasPartial || result.GreatestEclipse == 0 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
center = SolarEclipsePathPoint{
|
||
|
|
JDE: jd,
|
||
|
|
Longitude: result.GreatestLongitude,
|
||
|
|
Latitude: result.GreatestLatitude,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
evaluation := solver.magnitudeEvaluationAt(jd)
|
||
|
|
centerState := evaluation.center.stateAt(center.Longitude*rad, center.Latitude*rad, 0)
|
||
|
|
maximum := solarEclipseMagnitudeAtTarget(centerState, magnitude)
|
||
|
|
if !finite(maximum) || maximum <= magnitude+1e-9 {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
centerLon, centerLat := center.Longitude*rad, center.Latitude*rad
|
||
|
|
centerVector := [3]float64{
|
||
|
|
math.Cos(centerLat) * math.Cos(centerLon),
|
||
|
|
math.Cos(centerLat) * math.Sin(centerLon),
|
||
|
|
math.Sin(centerLat),
|
||
|
|
}
|
||
|
|
east := [3]float64{-math.Sin(centerLon), math.Cos(centerLon), 0}
|
||
|
|
north := [3]float64{
|
||
|
|
-math.Sin(centerLat) * math.Cos(centerLon),
|
||
|
|
-math.Sin(centerLat) * math.Sin(centerLon),
|
||
|
|
math.Cos(centerLat),
|
||
|
|
}
|
||
|
|
distanceDegrees := []float64{0.02, 0.05, 0.1, 0.2, 0.4, 0.8, 1.6, 3.2, 6.4, 12.8, 25.6, 51.2}
|
||
|
|
seeds := make([]SolarEclipsePathPoint, 0, 2)
|
||
|
|
for bearingIndex := 0; bearingIndex < solarEclipseMagnitudeContourFallbackBearings; bearingIndex++ {
|
||
|
|
bearing := 2 * math.Pi * float64(bearingIndex) / float64(solarEclipseMagnitudeContourFallbackBearings)
|
||
|
|
direction := [3]float64{
|
||
|
|
math.Cos(bearing)*north[0] + math.Sin(bearing)*east[0],
|
||
|
|
math.Cos(bearing)*north[1] + math.Sin(bearing)*east[1],
|
||
|
|
math.Cos(bearing)*north[2] + math.Sin(bearing)*east[2],
|
||
|
|
}
|
||
|
|
previousDistance := 0.0
|
||
|
|
previousValue := maximum - magnitude
|
||
|
|
for distanceIndex := 1; distanceIndex < solarEclipseMagnitudeContourFallbackDistances; distanceIndex++ {
|
||
|
|
distance := distanceDegrees[distanceIndex]
|
||
|
|
angle := distance * rad
|
||
|
|
pointVector := [3]float64{
|
||
|
|
centerVector[0]*math.Cos(angle) + direction[0]*math.Sin(angle),
|
||
|
|
centerVector[1]*math.Cos(angle) + direction[1]*math.Sin(angle),
|
||
|
|
centerVector[2]*math.Cos(angle) + direction[2]*math.Sin(angle),
|
||
|
|
}
|
||
|
|
longitude := normalizeLongitude(math.Atan2(pointVector[1], pointVector[0]) / rad)
|
||
|
|
latitude := math.Asin(math.Max(-1, math.Min(1, pointVector[2]))) / rad
|
||
|
|
state := evaluation.center.stateAt(longitude*rad, latitude*rad, 0)
|
||
|
|
value := solarEclipseMagnitudeAtTarget(state, magnitude) - magnitude
|
||
|
|
if !finite(value) {
|
||
|
|
previousDistance = distance
|
||
|
|
previousValue = math.NaN()
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if finite(previousValue) && previousValue >= 0 && value <= 0 {
|
||
|
|
left, right := previousDistance, distance
|
||
|
|
leftValue := previousValue
|
||
|
|
for iteration := 0; iteration < 48 && right-left > 1e-10; iteration++ {
|
||
|
|
middle := (left + right) / 2
|
||
|
|
middleAngle := middle * rad
|
||
|
|
middleVector := [3]float64{
|
||
|
|
centerVector[0]*math.Cos(middleAngle) + direction[0]*math.Sin(middleAngle),
|
||
|
|
centerVector[1]*math.Cos(middleAngle) + direction[1]*math.Sin(middleAngle),
|
||
|
|
centerVector[2]*math.Cos(middleAngle) + direction[2]*math.Sin(middleAngle),
|
||
|
|
}
|
||
|
|
middleLongitude := normalizeLongitude(math.Atan2(middleVector[1], middleVector[0]) / rad)
|
||
|
|
middleLatitude := math.Asin(math.Max(-1, math.Min(1, middleVector[2]))) / rad
|
||
|
|
middleState := evaluation.center.stateAt(middleLongitude*rad, middleLatitude*rad, 0)
|
||
|
|
middleValue := solarEclipseMagnitudeAtTarget(middleState, magnitude) - magnitude
|
||
|
|
if !finite(middleValue) {
|
||
|
|
break
|
||
|
|
}
|
||
|
|
if leftValue*middleValue <= 0 {
|
||
|
|
right = middle
|
||
|
|
} else {
|
||
|
|
left, leftValue = middle, middleValue
|
||
|
|
}
|
||
|
|
}
|
||
|
|
seedDistance := (left + right) / 2
|
||
|
|
seedAngle := seedDistance * rad
|
||
|
|
seedVector := [3]float64{
|
||
|
|
centerVector[0]*math.Cos(seedAngle) + direction[0]*math.Sin(seedAngle),
|
||
|
|
centerVector[1]*math.Cos(seedAngle) + direction[1]*math.Sin(seedAngle),
|
||
|
|
centerVector[2]*math.Cos(seedAngle) + direction[2]*math.Sin(seedAngle),
|
||
|
|
}
|
||
|
|
seedLongitude := normalizeLongitude(math.Atan2(seedVector[1], seedVector[0]) / rad)
|
||
|
|
seedLatitude := math.Asin(math.Max(-1, math.Min(1, seedVector[2]))) / rad
|
||
|
|
seedLongitude, seedLatitude, refined := solver.refineMagnitudeEnvelopePoint(
|
||
|
|
magnitude, seedLongitude, seedLatitude, evaluation,
|
||
|
|
)
|
||
|
|
if refined {
|
||
|
|
state := evaluation.center.stateAt(seedLongitude*rad, seedLatitude*rad, 0)
|
||
|
|
candidate := SolarEclipsePathPoint{
|
||
|
|
JDE: jd, Longitude: seedLongitude, Latitude: seedLatitude,
|
||
|
|
SunAltitude: state.sunAltitudeRad / rad,
|
||
|
|
}
|
||
|
|
if !solarEclipseRiseSetPointExists(seeds, candidate) {
|
||
|
|
seeds = append(seeds, candidate)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
break
|
||
|
|
}
|
||
|
|
previousDistance, previousValue = distance, value
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return seeds
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) refineMagnitudeEnvelopePoint(
|
||
|
|
magnitude, longitude, latitude float64,
|
||
|
|
evaluation solarEclipseRiseSetEvaluation,
|
||
|
|
) (float64, float64, bool) {
|
||
|
|
return riseSetRefineGeographicRoot(longitude, latitude, func(lon, lat float64) (float64, float64, bool) {
|
||
|
|
state := evaluation.center.stateAt(lon*rad, lat*rad, 0)
|
||
|
|
instantaneousMagnitude := solarEclipseMagnitudeAtTarget(state, magnitude)
|
||
|
|
return instantaneousMagnitude - magnitude, evaluation.separationDerivative(lon, lat),
|
||
|
|
finite(instantaneousMagnitude)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipseMagnitudeContourCompatibilitySides(
|
||
|
|
segments [][]SolarEclipsePathPoint,
|
||
|
|
) ([]SolarEclipsePathPoint, []SolarEclipsePathPoint) {
|
||
|
|
if len(segments) < 2 {
|
||
|
|
return nil, nil
|
||
|
|
}
|
||
|
|
indices := []int{0, 1}
|
||
|
|
for index := 2; index < len(segments); index++ {
|
||
|
|
if len(segments[index]) <= len(segments[indices[1]]) {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
indices[1] = index
|
||
|
|
if len(segments[indices[1]]) > len(segments[indices[0]]) {
|
||
|
|
indices[0], indices[1] = indices[1], indices[0]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
first, second := segments[indices[0]], segments[indices[1]]
|
||
|
|
firstLatitude := first[len(first)/2].Latitude
|
||
|
|
secondLatitude := second[len(second)/2].Latitude
|
||
|
|
if secondLatitude > firstLatitude {
|
||
|
|
first, second = second, first
|
||
|
|
}
|
||
|
|
return first, second
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipseLocalMagnitude(state localSolarEclipseState) float64 {
|
||
|
|
if state.sunRadiusRad <= 0 {
|
||
|
|
return math.NaN()
|
||
|
|
}
|
||
|
|
return (state.moonOuterRadiusRad + state.sunRadiusRad - state.separationRad) / (2 * state.sunRadiusRad)
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipseMagnitudeAtTarget(state localSolarEclipseState, target float64) float64 {
|
||
|
|
if state.sunRadiusRad <= 0 {
|
||
|
|
return math.NaN()
|
||
|
|
}
|
||
|
|
if target > 1 {
|
||
|
|
centralGap := state.moonInnerRadiusRad - state.sunRadiusRad
|
||
|
|
if centralGap <= 0 {
|
||
|
|
return math.NaN()
|
||
|
|
}
|
||
|
|
// Inside totality, normalize the magnitude from 1 at the inner
|
||
|
|
// contact to the apparent-diameter ratio at zero separation. This
|
||
|
|
// preserves the usual m=1 boundary while retaining legal values
|
||
|
|
// above one for deep total eclipses.
|
||
|
|
return 1 + (state.moonInnerRadiusRad/state.sunRadiusRad-1)*
|
||
|
|
(1-state.separationRad/centralGap)
|
||
|
|
}
|
||
|
|
moonRadius := state.moonOuterRadiusRad
|
||
|
|
if target == 1 {
|
||
|
|
moonRadius = state.moonInnerRadiusRad
|
||
|
|
}
|
||
|
|
return (moonRadius + state.sunRadiusRad - state.separationRad) / (2 * state.sunRadiusRad)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (solver solarEclipseSolver) magnitudeContourPointAt(
|
||
|
|
jd float64,
|
||
|
|
moon [3]float64,
|
||
|
|
axis solarEclipseAxis,
|
||
|
|
directionX, directionY, magnitude float64,
|
||
|
|
) (SolarEclipsePathPoint, bool) {
|
||
|
|
if magnitude == 0 {
|
||
|
|
_, _, sun := solver.besselGeometryAt(jd)
|
||
|
|
return solver.shadowFootprintPointAt(jd, moon, axis, sun, math.Atan2(directionY, directionX), solarEclipsePenumbralShadow)
|
||
|
|
}
|
||
|
|
radii := solver.shadowRadiiAt(moon[2])
|
||
|
|
radius := solarEclipseMagnitudeContourRadius(radii, magnitude)
|
||
|
|
if magnitude > 1 && radius <= 0 {
|
||
|
|
// The initial Bessel plane can be on the antumbral side even though
|
||
|
|
// the Earth intersection has a valid totality contour. Start from
|
||
|
|
// the absolute umbral edge and let the surface iteration converge.
|
||
|
|
radius = radii.absUmbraRadius
|
||
|
|
}
|
||
|
|
if radius <= 0 {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
var intersection solarEclipseLineIntersection
|
||
|
|
for iteration := 0; iteration < solarEclipsePartialFootprintIterationLimit; iteration++ {
|
||
|
|
x := moon[0] + radius*directionX
|
||
|
|
y := moon[1] + radius*directionY
|
||
|
|
intersection = solarEclipseLineEar2(
|
||
|
|
x, y, 2,
|
||
|
|
x, y, 0,
|
||
|
|
solarEclipseEarthPolarRatio, 1, axis,
|
||
|
|
)
|
||
|
|
if !intersection.valid {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
nextRadii := solver.shadowRadiiAt(moon[2] - intersection.r2)
|
||
|
|
nextRadius := solarEclipseMagnitudeContourRadius(nextRadii, magnitude)
|
||
|
|
if magnitude > 1 && nextRadius <= 0 {
|
||
|
|
nextRadius = nextRadii.absUmbraRadius
|
||
|
|
}
|
||
|
|
if nextRadius <= 0 {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
if math.Abs(nextRadius-radius) <= solarEclipsePartialFootprintPointTolerance {
|
||
|
|
radius = nextRadius
|
||
|
|
break
|
||
|
|
}
|
||
|
|
radius = nextRadius
|
||
|
|
}
|
||
|
|
x := moon[0] + radius*directionX
|
||
|
|
y := moon[1] + radius*directionY
|
||
|
|
intersection = solarEclipseLineEar2(
|
||
|
|
x, y, 2,
|
||
|
|
x, y, 0,
|
||
|
|
solarEclipseEarthPolarRatio, 1, axis,
|
||
|
|
)
|
||
|
|
if !intersection.valid {
|
||
|
|
return SolarEclipsePathPoint{}, false
|
||
|
|
}
|
||
|
|
longitude, latitude := solarEclipseIntersectionGeodetic(intersection, axis)
|
||
|
|
sunAltitudeRad := solarEclipseSunAltitudeAtGreatest(jd, longitude, latitude, axis.gst)
|
||
|
|
return SolarEclipsePathPoint{
|
||
|
|
JDE: jd,
|
||
|
|
Longitude: longitude,
|
||
|
|
Latitude: latitude,
|
||
|
|
SunAltitude: sunAltitudeRad / rad,
|
||
|
|
}, true
|
||
|
|
}
|
||
|
|
|
||
|
|
func solarEclipseMagnitudeContourRadius(radii solarEclipseShadowRadii, magnitude float64) float64 {
|
||
|
|
if magnitude >= 1 && radii.magnitude > 1 {
|
||
|
|
// Above totality, interpolate from the umbral edge (m=1) to the
|
||
|
|
// Bessel-axis maximum (m=radii.magnitude). Using the penumbra-to-
|
||
|
|
// umbra slope here can turn a valid high-magnitude contour negative
|
||
|
|
// near hybrid and shallow total eclipses.
|
||
|
|
return radii.absUmbraRadius * (radii.magnitude - magnitude) / (radii.magnitude - 1)
|
||
|
|
}
|
||
|
|
return radii.penumbraRadius - magnitude*(radii.penumbraRadius-radii.absUmbraRadius)
|
||
|
|
}
|