Files
astro/basic/occultation_rise_set_topology.go
T

1242 lines
48 KiB
Go
Raw Normal View History

package basic
import (
"math"
"sort"
"time"
)
func appendOccultationRiseSetSamples(
tracks []*occultationRiseSetTrack,
points []OccultationPathPoint,
stepDays float64,
) []*occultationRiseSetTrack {
sort.SliceStable(points, func(first, second int) bool {
return points[first].Longitude < points[second].Longitude
})
type match struct {
point, track int
distance float64
}
var matches []match
for pointIndex, point := range points {
for index, track := range tracks {
if len(track.segments) == 0 || len(track.segments[len(track.segments)-1]) == 0 {
continue
}
last := track.segments[len(track.segments)-1][len(track.segments[len(track.segments)-1])-1]
deltaDays := point.Time.Sub(last.Time).Hours() / 24
if deltaDays <= 0 || deltaDays > 2.5*stepDays {
continue
}
distance := occultationPathDistanceKM(last, point)
if riseSetGeographicBranchChanged(distance, deltaDays) {
continue
}
matches = append(matches, match{point: pointIndex, track: index, distance: distance})
}
}
// Match the closest pairs before creating tracks for newly appearing
// roots. Longitude order must not let a new root claim an existing branch
// ahead of that branch's much closer continuation.
sort.SliceStable(matches, func(i, j int) bool { return matches[i].distance < matches[j].distance })
usedTracks, usedPoints := make([]bool, len(tracks)), make([]bool, len(points))
for _, candidate := range matches {
if usedTracks[candidate.track] || usedPoints[candidate.point] {
continue
}
usedTracks[candidate.track], usedPoints[candidate.point] = true, true
track := tracks[candidate.track]
track.segments[len(track.segments)-1] = append(track.segments[len(track.segments)-1], points[candidate.point])
}
for index, point := range points {
if !usedPoints[index] {
tracks = append(tracks, &occultationRiseSetTrack{segments: [][]OccultationPathPoint{{point}}})
}
}
return tracks
}
func completeOccultationRiseSetCurveEndpoints(
curves []OccultationRiseSetCurve,
stepDays float64,
location *time.Location,
cache *occultationRiseSetEvaluationCache,
) {
for index := range curves {
completeOccultationRiseSetFoldEndpoints(&curves[index], stepDays, location, cache)
}
completeOccultationRiseSetPhaseJunctions(curves, stepDays, location, cache)
completeOccultationRiseSetDirectionJunctions(curves, stepDays, location, cache)
refinementCache := cache.candidateOnly()
for index := range curves {
// Split sampled branch jumps before inserting adaptive midpoints. If a
// midpoint root is singular, refinement must not turn that jump back into
// a long straight segment.
normalizeOccultationRiseSetCurveSegments(&curves[index])
refineOccultationRiseSetCurveSpacing(&curves[index], location, refinementCache)
normalizeOccultationRiseSetCurveSegments(&curves[index])
}
refineOccultationRiseSetPhaseJunctionApproaches(curves, location, refinementCache)
for index := range curves {
normalizeOccultationRiseSetCurveSegments(&curves[index])
}
}
// normalizeOccultationRiseSetCurveSegments keeps each rendered branch
// strictly time-ordered. A completed horizon fold is a branch boundary when
// its time reverses; roots at the same location and time are numerical dupes.
func normalizeOccultationRiseSetCurveSegments(curve *OccultationRiseSetCurve) {
if curve == nil {
return
}
segments := make([][]OccultationPathPoint, 0, len(curve.Segments))
for _, segment := range curve.Segments {
if len(segment) == 0 {
continue
}
current := []OccultationPathPoint{segment[0]}
for _, point := range segment[1:] {
last := current[len(current)-1]
pointTT, lastTT := occultationTimeToTT(point.Time), occultationTimeToTT(last.Time)
if pointTT > lastTT+occultationRiseSetTimeEpsilonDays {
if occultationRiseSetBranchChanged(
occultationPathDistanceKM(last, point), pointTT-lastTT,
) {
if len(current) >= 2 {
segments = append(segments, current)
}
current = []OccultationPathPoint{point}
continue
}
current = append(current, point)
continue
}
if math.Abs(pointTT-lastTT) <= occultationRiseSetTimeEpsilonDays &&
occultationPathDistanceKM(point, last) <= 0.01 {
continue
}
if len(current) >= 2 {
segments = append(segments, current)
}
current = []OccultationPathPoint{point}
}
if len(current) >= 2 {
segments = append(segments, current)
}
}
curve.Segments = segments
}
type occultationRiseSetEndpoint struct {
curveIndex int
segmentIndex int
atStart bool
point OccultationPathPoint
}
type occultationRiseSetPhaseAttachment struct {
endpoint occultationRiseSetEndpoint
fold OccultationPathPoint
hasFold bool
}
func completeOccultationRiseSetFoldEndpoints(
curve *OccultationRiseSetCurve,
stepDays float64,
location *time.Location,
cache *occultationRiseSetEvaluationCache,
) {
if curve == nil || len(curve.Segments) < 2 {
return
}
endpoints := make([]occultationRiseSetEndpoint, 0, 2*len(curve.Segments))
for segmentIndex, segment := range curve.Segments {
if len(segment) == 0 {
continue
}
for _, atStart := range []bool{true, false} {
endpoints = append(endpoints, occultationRiseSetEndpoint{
segmentIndex: segmentIndex, atStart: atStart,
point: occultationRiseSetSegmentEndpoint(segment, atStart),
})
}
}
used := make(map[[2]int]bool, len(endpoints))
for firstIndex := 0; firstIndex < len(endpoints); firstIndex++ {
first := endpoints[firstIndex]
firstKey := [2]int{first.segmentIndex, boolInt(first.atStart)}
if used[firstKey] {
continue
}
for secondIndex := firstIndex + 1; secondIndex < len(endpoints); secondIndex++ {
second := endpoints[secondIndex]
secondKey := [2]int{second.segmentIndex, boolInt(second.atStart)}
if first.segmentIndex == second.segmentIndex || first.atStart != second.atStart || used[secondKey] {
continue
}
// Close roots can cease to be resolved on adjacent time samples.
// Candidate pairing may span one sample; the fold solver and its
// physical residuals still decide whether the branches really meet.
if math.Abs(occultationTimeToTT(first.point.Time)-occultationTimeToTT(second.point.Time)) > math.Max(1e-8, 1.5*stepDays) {
continue
}
distance := occultationPathDistanceKM(first.point, second.point)
if distance <= 0.01 || distance > 3000 {
continue
}
if !occultationRiseSetFoldCandidate(
curve.Segments[first.segmentIndex], first.atStart,
curve.Segments[second.segmentIndex], second.atStart,
) {
continue
}
fold, ok := refineOccultationRiseSetFold(
first.point, second.point, first.atStart,
curve.Phase == RiseSetPhaseGreatest, stepDays, location, cache,
)
if !ok || math.Abs(occultationTimeToTT(fold.Time)-(occultationTimeToTT(first.point.Time)+occultationTimeToTT(second.point.Time))/2) > 2.5*stepDays ||
occultationPathDistanceKM(fold, first.point) > 3000 || occultationPathDistanceKM(fold, second.point) > 3000 {
continue
}
curve.Segments[first.segmentIndex] = occultationRiseSetAddEndpoint(curve.Segments[first.segmentIndex], fold, first.atStart)
curve.Segments[second.segmentIndex] = occultationRiseSetAddEndpoint(curve.Segments[second.segmentIndex], fold, second.atStart)
used[firstKey], used[secondKey] = true, true
break
}
}
}
// occultationRiseSetFoldCandidate reports whether two nearby endpoints are
// actually separating into different sampled branches. Endpoints that merely
// happen to be close are common on smooth curves and do not need a fold root.
func occultationRiseSetFoldCandidate(
first []OccultationPathPoint,
firstAtStart bool,
second []OccultationPathPoint,
secondAtStart bool,
) bool {
firstAdjacent, firstOK := occultationRiseSetAdjacentPoint(first, firstAtStart)
secondAdjacent, secondOK := occultationRiseSetAdjacentPoint(second, secondAtStart)
if !firstOK || !secondOK {
return true
}
endpointDistance := occultationPathDistanceKM(
occultationRiseSetSegmentEndpoint(first, firstAtStart),
occultationRiseSetSegmentEndpoint(second, secondAtStart),
)
adjacentDistance := occultationPathDistanceKM(firstAdjacent, secondAdjacent)
margin := 250.0
firstEndpoint := occultationRiseSetSegmentEndpoint(first, firstAtStart)
secondEndpoint := occultationRiseSetSegmentEndpoint(second, secondAtStart)
if math.Max(math.Abs(firstEndpoint.Latitude), math.Abs(secondEndpoint.Latitude)) >= 70 {
// Longitude convergence makes the first post-fold samples unusually
// close near a pole. Leave numerical room for a 10-second sample to
// land just inside the ordinary 250 km divergence margin.
margin = 200
}
return adjacentDistance > endpointDistance+margin
}
func occultationRiseSetAdjacentPoint(segment []OccultationPathPoint, atStart bool) (OccultationPathPoint, bool) {
if len(segment) < 2 {
return OccultationPathPoint{}, false
}
if atStart {
return segment[1], true
}
return segment[len(segment)-2], true
}
func completeOccultationRiseSetPhaseJunctions(
curves []OccultationRiseSetCurve,
stepDays float64,
location *time.Location,
cache *occultationRiseSetEvaluationCache,
) {
curveIndices := make(map[occultationRiseSetCurveKey]int, len(curves))
for index, curve := range curves {
curveIndices[occultationRiseSetCurveKey{phase: curve.Phase, direction: curve.Direction}] = index
}
for _, direction := range []RiseSetDirection{RiseSetDirectionRise, RiseSetDirectionSet} {
startIndex, haveStart := curveIndices[occultationRiseSetCurveKey{phase: RiseSetPhaseStart, direction: direction}]
greatestIndex, haveGreatest := curveIndices[occultationRiseSetCurveKey{phase: RiseSetPhaseGreatest, direction: direction}]
endIndex, haveEnd := curveIndices[occultationRiseSetCurveKey{phase: RiseSetPhaseEnd, direction: direction}]
if !haveStart || !haveGreatest || !haveEnd {
continue
}
seeds := occultationRiseSetUnsharedEndpoints(startIndex, curves[startIndex].Segments)
for _, seed := range seeds {
candidateSeeds := []OccultationPathPoint{seed.point}
if endEndpoint, endOK := occultationRiseSetClosestEndpoint(seed.point, curves[endIndex].Segments, stepDays); endOK {
candidateSeeds = append([]OccultationPathPoint{
occultationRiseSetMidpoint(seed.point, endEndpoint.point),
endEndpoint.point,
}, candidateSeeds...)
}
junction, ok := OccultationPathPoint{}, false
for _, candidate := range candidateSeeds {
junction, ok = refineOccultationRiseSetPhaseJunctionOnHorizon(candidate, location, cache)
if !ok {
junction, ok = refineOccultationRiseSetPhaseJunction(candidate, location, cache)
}
if ok {
break
}
}
if !ok || math.Abs(occultationTimeToTT(junction.Time)-occultationTimeToTT(seed.point.Time)) > 3*stepDays ||
occultationPathDistanceKM(junction, seed.point) > 3000 {
continue
}
matched := make([]occultationRiseSetPhaseAttachment, 0, 3)
attachmentTargets := make(map[int]OccultationPathPoint, 1)
recovery := occultationRiseSetNarrowPhaseRecovery{}
recovered := false
// Contact extrema include the changing apparent radii; greatest
// separation need not end at exactly the same time. Complete the
// start/end pair even when the diagnostic greatest branch differs.
for _, curveIndex := range []int{startIndex, endIndex, greatestIndex} {
endpoint, endpointOK := occultationRiseSetClosestEndpoint(
junction, curves[curveIndex].Segments, stepDays,
)
if !endpointOK {
if curveIndex == greatestIndex {
continue
}
matched = nil
break
}
endpoint.curveIndex = curveIndex
attachment := occultationRiseSetPhaseAttachment{endpoint: endpoint}
if curveIndex == greatestIndex && occultationPathDistanceKM(junction, endpoint.point) > occultationPathBoundarySpacingKM {
if !occultationRiseSetPhaseSegmentIsContinuous(
junction, endpoint.point, RiseSetPhaseGreatest, direction,
location, cache,
) {
fold, foldOK := refineOccultationRiseSetFold(
junction, endpoint.point, endpoint.atStart, true,
stepDays, location, cache,
)
if !foldOK || !occultationRiseSetFoldBridgesPhaseJunction(
junction, endpoint, fold, direction, cache,
) {
recovery, recovered = occultationRiseSetRecoverNarrowPhaseJunction(
junction, endpoint, direction, stepDays, location, cache,
)
if !recovered {
break
}
attachmentTargets[curveIndex] = recovery.junction
} else {
attachment.fold, attachment.hasFold = fold, true
}
}
}
matched = append(matched, attachment)
}
if len(matched) < 2 {
continue
}
for _, attachment := range matched {
endpoint := attachment.endpoint
target := junction
if recoveredTarget, ok := attachmentTargets[endpoint.curveIndex]; ok {
target = recoveredTarget
}
if attachment.hasFold {
curves[endpoint.curveIndex].Segments[endpoint.segmentIndex] = occultationRiseSetAddEndpoint(
curves[endpoint.curveIndex].Segments[endpoint.segmentIndex], attachment.fold, endpoint.atStart,
)
shortBranch := []OccultationPathPoint{attachment.fold, target}
if !endpoint.atStart {
shortBranch[0], shortBranch[1] = shortBranch[1], shortBranch[0]
}
curves[endpoint.curveIndex].Segments = append(curves[endpoint.curveIndex].Segments, shortBranch)
continue
}
curves[endpoint.curveIndex].Segments[endpoint.segmentIndex] = occultationRiseSetAddEndpoint(
curves[endpoint.curveIndex].Segments[endpoint.segmentIndex], target, endpoint.atStart,
)
}
if recovered {
if segment := recovery.segments[RiseSetPhaseStart]; len(segment) >= 2 {
curves[startIndex].Segments = append(curves[startIndex].Segments, segment)
}
if segment := recovery.segments[RiseSetPhaseEnd]; len(segment) >= 2 {
curves[endIndex].Segments = append(curves[endIndex].Segments, segment)
}
}
}
}
}
func occultationRiseSetPhaseSegmentIsContinuous(
start, end OccultationPathPoint,
phase RiseSetPhase,
direction RiseSetDirection,
location *time.Location,
cache *occultationRiseSetEvaluationCache,
) bool {
startTT := occultationTimeToTT(start.Time)
endTT := occultationTimeToTT(end.Time)
if math.Abs(endTT-startTT)*86400 < 0.1 {
return false
}
totalDistance := occultationPathDistanceKM(start, end)
continuityToleranceKM := math.Max(100, 0.1*totalDistance)
candidate := end
for divisor := 2.0; divisor <= 1024; divisor *= 2 {
tt := startTT + (endTT-startTT)/divisor
seedAngle := occultationRiseSetHorizonAngle(
tt, candidate.Longitude, candidate.Latitude, cache.context,
)
next, ok := occultationRiseSetPhasePointOnHorizon(
tt, seedAngle, phase, direction, location, cache,
)
if !ok {
return occultationPathDistanceKM(start, candidate) <= continuityToleranceKM
}
candidate = next
// At a three-phase junction the root becomes numerically singular.
// Once the traced branch is already inside the spatial tolerance,
// continuing toward the exact junction can jump to its sibling root.
if occultationPathDistanceKM(start, candidate) <= continuityToleranceKM {
return true
}
}
return occultationPathDistanceKM(start, candidate) <= continuityToleranceKM
}
type occultationRiseSetNarrowPhaseRecovery struct {
junction OccultationPathPoint
segments map[RiseSetPhase][]OccultationPathPoint
}
// A pair of phase junctions can be only a few seconds apart. The ordinary
// horizon scan cannot resolve both roots, so continue the already sampled
// greatest-phase branch to its contact crossing and reconstruct the two short
// contact branches only for that local degeneracy.
func occultationRiseSetRecoverNarrowPhaseJunction(
primary OccultationPathPoint,
greatestEndpoint occultationRiseSetEndpoint,
direction RiseSetDirection,
stepDays float64,
location *time.Location,
cache *occultationRiseSetEvaluationCache,
) (occultationRiseSetNarrowPhaseRecovery, bool) {
fold, ok := refineOccultationRiseSetFold(
primary, greatestEndpoint.point, greatestEndpoint.atStart,
true, stepDays, location, cache,
)
if !ok {
return occultationRiseSetNarrowPhaseRecovery{}, false
}
primaryTT := occultationTimeToTT(primary.Time)
endpointTT := occultationTimeToTT(greatestEndpoint.point.Time)
foldTT := occultationTimeToTT(fold.Time)
extensionDirection := primaryTT - endpointTT
if extensionDirection == 0 || (foldTT-primaryTT)*extensionDirection <= 0 {
return occultationRiseSetNarrowPhaseRecovery{}, false
}
seedAngle := occultationRiseSetHorizonAngle(
primaryTT, greatestEndpoint.point.Longitude, greatestEndpoint.point.Latitude, cache.context,
)
branchPoint, branchEvaluation, ok := occultationRiseSetRawPhasePointOnHorizon(
primaryTT, seedAngle, true, location, cache,
)
if !ok || !occultationRiseSetDirectionMatches(
branchEvaluation, branchPoint, direction,
) {
return occultationRiseSetNarrowPhaseRecovery{}, false
}
branchContact := branchEvaluation.center.stateAt(
branchPoint.Longitude, branchPoint.Latitude,
).contactMetric
foldEvaluation := cache.evaluation(foldTT)
foldContact := foldEvaluation.center.stateAt(fold.Longitude, fold.Latitude).contactMetric
if !finite(branchContact) || !finite(foldContact) || branchContact >= 0 || foldContact <= 0 {
return occultationRiseSetNarrowPhaseRecovery{}, false
}
inside, outside := branchPoint, fold
insideContact, outsideContact := branchContact, foldContact
for iteration := 0; iteration < 56; iteration++ {
insideTT := occultationTimeToTT(inside.Time)
outsideTT := occultationTimeToTT(outside.Time)
if math.Abs(outsideTT-insideTT) <= 1e-10 {
break
}
middleTT := (insideTT + outsideTT) / 2
insideAngle := occultationRiseSetHorizonAngle(
middleTT, inside.Longitude, inside.Latitude, cache.context,
)
outsideAngle := occultationRiseSetHorizonAngle(
middleTT, outside.Longitude, outside.Latitude, cache.context,
)
if !finite(insideAngle) || !finite(outsideAngle) {
return occultationRiseSetNarrowPhaseRecovery{}, false
}
middleAngle := riseSetNormalizeRadians(
insideAngle + math.Remainder(outsideAngle-insideAngle, 2*math.Pi)/2,
)
middle, evaluation, middleOK := occultationRiseSetRawPhasePointOnHorizon(
middleTT, middleAngle, true, location, cache,
)
if !middleOK || !occultationRiseSetDirectionMatches(evaluation, middle, direction) {
outside = OccultationPathPoint{
Time: occultationTTToLocation(middleTT, location),
Longitude: normalizeLongitude(
inside.Longitude + math.Remainder(outside.Longitude-inside.Longitude, 360)/2,
),
Latitude: (inside.Latitude + outside.Latitude) / 2,
}
continue
}
contact := evaluation.center.stateAt(middle.Longitude, middle.Latitude).contactMetric
if !finite(contact) {
return occultationRiseSetNarrowPhaseRecovery{}, false
}
if contact <= 0 {
inside, insideContact = middle, contact
} else {
outside, outsideContact = middle, contact
}
}
secondary := inside
if math.Abs(outsideContact) < math.Abs(insideContact) {
secondary = outside
}
secondaryTT := occultationTimeToTT(secondary.Time)
secondaryEvaluation := cache.evaluation(secondaryTT)
secondaryState := secondaryEvaluation.center.stateAt(secondary.Longitude, secondary.Latitude)
if !secondaryState.valid || math.Abs(secondaryState.contactMetric) > 1e-7 ||
math.Abs(secondaryEvaluation.separationDerivative(secondary.Longitude, secondary.Latitude)) > 1e-7 ||
math.Abs(secondaryState.moonAltitude) > 1e-7 ||
!occultationRiseSetDirectionMatches(secondaryEvaluation, secondary, direction) ||
math.Abs(secondaryTT-primaryTT) > stepDays ||
occultationPathDistanceKM(primary, secondary) > 500 {
return occultationRiseSetNarrowPhaseRecovery{}, false
}
phaseRoots, ok := occultationRiseSetNarrowContactRoots(
primary, secondary, direction, location, cache,
)
if !ok {
return occultationRiseSetNarrowPhaseRecovery{}, false
}
first, last := primary, secondary
if last.Time.Before(first.Time) {
first, last = last, first
}
segments := make(map[RiseSetPhase][]OccultationPathPoint, 2)
for phase, root := range phaseRoots {
segments[phase] = []OccultationPathPoint{first, root, last}
}
return occultationRiseSetNarrowPhaseRecovery{junction: secondary, segments: segments}, true
}
func occultationRiseSetDirectionMatches(
evaluation occultationRiseSetEvaluation,
point OccultationPathPoint,
direction RiseSetDirection,
) bool {
derivative := evaluation.moonAltitudeDerivative(point.Longitude, point.Latitude)
return finite(derivative) && ((direction == RiseSetDirectionRise && derivative > 0) ||
(direction == RiseSetDirectionSet && derivative < 0))
}
func occultationRiseSetNarrowContactRoots(
first, second OccultationPathPoint,
direction RiseSetDirection,
location *time.Location,
cache *occultationRiseSetEvaluationCache,
) (map[RiseSetPhase]OccultationPathPoint, bool) {
tt := (occultationTimeToTT(first.Time) + occultationTimeToTT(second.Time)) / 2
firstAngle := occultationRiseSetHorizonAngle(tt, first.Longitude, first.Latitude, cache.context)
secondAngle := occultationRiseSetHorizonAngle(tt, second.Longitude, second.Latitude, cache.context)
if !finite(firstAngle) || !finite(secondAngle) {
return nil, false
}
centerAngle := riseSetNormalizeRadians(
firstAngle + math.Remainder(secondAngle-firstAngle, 2*math.Pi)/2,
)
result := make(map[RiseSetPhase]OccultationPathPoint, 2)
for _, phase := range []RiseSetPhase{RiseSetPhaseStart, RiseSetPhaseEnd} {
found := false
for scale := 0; scale < 22 && !found; scale++ {
delta := 0.0
if scale > 0 {
delta = math.Ldexp(1e-6, scale-1)
}
for _, sign := range []float64{-1, 1} {
if delta == 0 && sign > 0 {
continue
}
point, ok := occultationRiseSetPhasePointOnHorizon(
tt, centerAngle+sign*delta, phase, direction, location, cache,
)
if !ok || math.Min(
occultationPathDistanceKM(first, point),
occultationPathDistanceKM(second, point),
) > 500 {
continue
}
result[phase], found = point, true
break
}
}
if !found {
continue
}
}
return result, len(result) > 0
}
func occultationRiseSetFoldBridgesPhaseJunction(
junction OccultationPathPoint,
endpoint occultationRiseSetEndpoint,
fold OccultationPathPoint,
direction RiseSetDirection,
cache *occultationRiseSetEvaluationCache,
) bool {
junctionTT := occultationTimeToTT(junction.Time)
endpointTT := occultationTimeToTT(endpoint.point.Time)
foldTT := occultationTimeToTT(fold.Time)
const timeToleranceDays = 1e-8
if endpoint.atStart {
if foldTT > math.Min(junctionTT, endpointTT)+timeToleranceDays {
return false
}
} else if foldTT < math.Max(junctionTT, endpointTT)-timeToleranceDays {
return false
}
if occultationPathDistanceKM(fold, junction) > 3000 ||
occultationPathDistanceKM(fold, endpoint.point) > 3000 {
return false
}
evaluation := cache.evaluation(foldTT)
_, key, valid := evaluation.classify(fold.Longitude, fold.Latitude, true, fold.Time.Location())
return valid && key.phase == RiseSetPhaseGreatest && key.direction == direction
}
func occultationRiseSetMidpoint(first, second OccultationPathPoint) OccultationPathPoint {
firstTT := occultationTimeToTT(first.Time)
secondTT := occultationTimeToTT(second.Time)
return OccultationPathPoint{
Time: occultationTTToLocation((firstTT+secondTT)/2, first.Time.Location()),
Longitude: normalizeLongitude(
first.Longitude + math.Remainder(second.Longitude-first.Longitude, 360)/2,
),
Latitude: (first.Latitude + second.Latitude) / 2,
MoonAltitude: (first.MoonAltitude + second.MoonAltitude) / 2,
}
}
func completeOccultationRiseSetDirectionJunctions(
curves []OccultationRiseSetCurve,
stepDays float64,
location *time.Location,
cache *occultationRiseSetEvaluationCache,
) {
type candidate struct {
riseIndex, setIndex int
junction OccultationPathPoint
riseAttachment occultationRiseSetPhaseAttachment
setAttachment occultationRiseSetPhaseAttachment
metric float64
}
curveIndices := make(map[occultationRiseSetCurveKey]int, len(curves))
for index, curve := range curves {
curveIndices[occultationRiseSetCurveKey{phase: curve.Phase, direction: curve.Direction}] = index
}
for _, phase := range []RiseSetPhase{RiseSetPhaseStart, RiseSetPhaseGreatest, RiseSetPhaseEnd} {
riseIndex, haveRise := curveIndices[occultationRiseSetCurveKey{phase: phase, direction: RiseSetDirectionRise}]
setIndex, haveSet := curveIndices[occultationRiseSetCurveKey{phase: phase, direction: RiseSetDirectionSet}]
if !haveRise || !haveSet {
continue
}
riseEndpoints := occultationRiseSetAllEndpoints(curves[riseIndex].Segments)
setEndpoints := occultationRiseSetAllEndpoints(curves[setIndex].Segments)
candidates := make([]candidate, 0, len(riseEndpoints)*len(setEndpoints))
for riseEndpointIndex, riseEndpoint := range riseEndpoints {
for setEndpointIndex, setEndpoint := range setEndpoints {
deltaDays := math.Abs(occultationTimeToTT(riseEndpoint.point.Time) - occultationTimeToTT(setEndpoint.point.Time))
if deltaDays > 3*stepDays {
continue
}
distance := occultationPathDistanceKM(riseEndpoint.point, setEndpoint.point)
if distance > 3000 {
continue
}
junction, ok := refineOccultationRiseSetDirectionJunctionOnHorizon(
riseEndpoint.point, setEndpoint.point, phase == RiseSetPhaseGreatest, location, cache,
)
if !ok {
longitude := normalizeLongitude(riseEndpoint.point.Longitude + math.Remainder(setEndpoint.point.Longitude-riseEndpoint.point.Longitude, 360)/2)
latitude := (riseEndpoint.point.Latitude + setEndpoint.point.Latitude) / 2
junction, ok = refineOccultationRiseSetDirectionJunction(
(occultationTimeToTT(riseEndpoint.point.Time)+occultationTimeToTT(setEndpoint.point.Time))/2,
longitude, latitude, phase == RiseSetPhaseGreatest, location, cache,
)
}
if !ok {
continue
}
junctionDeltaRise := math.Abs(occultationTimeToTT(junction.Time) - occultationTimeToTT(riseEndpoint.point.Time))
junctionDeltaSet := math.Abs(occultationTimeToTT(junction.Time) - occultationTimeToTT(setEndpoint.point.Time))
junctionDistanceRise := occultationPathDistanceKM(junction, riseEndpoint.point)
junctionDistanceSet := occultationPathDistanceKM(junction, setEndpoint.point)
if !occultationRiseSetEndpointAcceptsJunction(riseEndpoint, junction) ||
!occultationRiseSetEndpointAcceptsJunction(setEndpoint, junction) ||
junctionDeltaRise > 3*stepDays || junctionDeltaSet > 3*stepDays ||
junctionDistanceRise > 3000 || junctionDistanceSet > 3000 ||
occultationRiseSetBranchChanged(junctionDistanceRise, junctionDeltaRise) ||
occultationRiseSetBranchChanged(junctionDistanceSet, junctionDeltaSet) {
continue
}
riseAttachment, riseOK := occultationRiseSetDirectionAttachment(
riseEndpoint, junction, phase, RiseSetDirectionRise,
stepDays, location, cache,
)
setAttachment, setOK := occultationRiseSetDirectionAttachment(
setEndpoint, junction, phase, RiseSetDirectionSet,
stepDays, location, cache,
)
if !riseOK || !setOK {
continue
}
metric := occultationPathDistanceKM(junction, riseEndpoint.point) +
occultationPathDistanceKM(junction, setEndpoint.point)
candidates = append(candidates, candidate{
riseIndex: riseEndpointIndex, setIndex: setEndpointIndex,
junction: junction, riseAttachment: riseAttachment,
setAttachment: setAttachment, metric: metric,
})
}
}
sort.SliceStable(candidates, func(first, second int) bool {
return candidates[first].metric < candidates[second].metric
})
usedRise := make([]bool, len(riseEndpoints))
usedSet := make([]bool, len(setEndpoints))
for _, candidate := range candidates {
if usedRise[candidate.riseIndex] || usedSet[candidate.setIndex] {
continue
}
occultationRiseSetAttachDirectionJunction(
&curves[riseIndex], candidate.riseAttachment, candidate.junction,
)
occultationRiseSetAttachDirectionJunction(
&curves[setIndex], candidate.setAttachment, candidate.junction,
)
usedRise[candidate.riseIndex] = true
usedSet[candidate.setIndex] = true
}
}
}
func occultationRiseSetDirectionAttachment(
endpoint occultationRiseSetEndpoint,
junction OccultationPathPoint,
phase RiseSetPhase,
direction RiseSetDirection,
stepDays float64,
location *time.Location,
cache *occultationRiseSetEvaluationCache,
) (occultationRiseSetPhaseAttachment, bool) {
attachment := occultationRiseSetPhaseAttachment{endpoint: endpoint}
if occultationPathDistanceKM(junction, endpoint.point) <= occultationPathBoundarySpacingKM ||
occultationRiseSetPhaseSegmentIsContinuous(
junction, endpoint.point, phase, direction, location, cache,
) {
return attachment, true
}
fold, ok := refineOccultationRiseSetFold(
junction, endpoint.point, endpoint.atStart,
phase == RiseSetPhaseGreatest, stepDays, location, cache,
)
if !ok || !occultationRiseSetFoldBridgesDirectionJunction(
junction, endpoint, fold, phase, direction, cache,
) {
return occultationRiseSetPhaseAttachment{}, false
}
attachment.fold, attachment.hasFold = fold, true
return attachment, true
}
func occultationRiseSetAttachDirectionJunction(
curve *OccultationRiseSetCurve,
attachment occultationRiseSetPhaseAttachment,
junction OccultationPathPoint,
) {
endpoint := attachment.endpoint
if attachment.hasFold {
curve.Segments[endpoint.segmentIndex] = occultationRiseSetAddEndpoint(
curve.Segments[endpoint.segmentIndex], attachment.fold, endpoint.atStart,
)
shortBranch := []OccultationPathPoint{attachment.fold, junction}
if !endpoint.atStart {
shortBranch[0], shortBranch[1] = shortBranch[1], shortBranch[0]
}
curve.Segments = append(curve.Segments, shortBranch)
return
}
curve.Segments[endpoint.segmentIndex] = occultationRiseSetAddEndpoint(
curve.Segments[endpoint.segmentIndex], junction, endpoint.atStart,
)
}
func occultationRiseSetFoldBridgesDirectionJunction(
junction OccultationPathPoint,
endpoint occultationRiseSetEndpoint,
fold OccultationPathPoint,
phase RiseSetPhase,
direction RiseSetDirection,
cache *occultationRiseSetEvaluationCache,
) bool {
junctionTT := occultationTimeToTT(junction.Time)
endpointTT := occultationTimeToTT(endpoint.point.Time)
foldTT := occultationTimeToTT(fold.Time)
if endpoint.atStart {
if foldTT > math.Min(junctionTT, endpointTT)+occultationRiseSetTimeEpsilonDays {
return false
}
} else if foldTT < math.Max(junctionTT, endpointTT)-occultationRiseSetTimeEpsilonDays {
return false
}
if occultationPathDistanceKM(fold, junction) > 3000 ||
occultationPathDistanceKM(fold, endpoint.point) > 3000 {
return false
}
evaluation := cache.evaluation(foldTT)
_, key, valid := evaluation.classify(
fold.Longitude, fold.Latitude, phase == RiseSetPhaseGreatest, fold.Time.Location(),
)
return valid && key.phase == phase && key.direction == direction
}
func occultationRiseSetEndpointAcceptsJunction(
endpoint occultationRiseSetEndpoint,
junction OccultationPathPoint,
) bool {
endpointTT := occultationTimeToTT(endpoint.point.Time)
junctionTT := occultationTimeToTT(junction.Time)
if endpoint.atStart {
return junctionTT <= endpointTT+occultationRiseSetTimeEpsilonDays
}
return junctionTT >= endpointTT-occultationRiseSetTimeEpsilonDays
}
func occultationRiseSetUnsharedEndpoints(curveIndex int, segments [][]OccultationPathPoint) []occultationRiseSetEndpoint {
endpoints := occultationRiseSetAllEndpoints(segments)
result := make([]occultationRiseSetEndpoint, 0, len(endpoints))
for index, endpoint := range endpoints {
shared := false
for otherIndex, other := range endpoints {
if index == otherIndex || endpoint.segmentIndex == other.segmentIndex {
continue
}
if math.Abs(occultationTimeToTT(endpoint.point.Time)-occultationTimeToTT(other.point.Time)) <= 1e-8 &&
occultationPathDistanceKM(endpoint.point, other.point) <= 0.01 {
shared = true
break
}
}
if !shared {
endpoint.curveIndex = curveIndex
result = append(result, endpoint)
}
}
return result
}
func occultationRiseSetAllEndpoints(segments [][]OccultationPathPoint) []occultationRiseSetEndpoint {
endpoints := make([]occultationRiseSetEndpoint, 0, 2*len(segments))
for segmentIndex, segment := range segments {
if len(segment) == 0 {
continue
}
for _, atStart := range []bool{true, false} {
endpoints = append(endpoints, occultationRiseSetEndpoint{
segmentIndex: segmentIndex, atStart: atStart,
point: occultationRiseSetSegmentEndpoint(segment, atStart),
})
}
}
return endpoints
}
func occultationRiseSetClosestEndpoint(
junction OccultationPathPoint,
segments [][]OccultationPathPoint,
stepDays float64,
) (occultationRiseSetEndpoint, bool) {
best := occultationRiseSetEndpoint{}
bestMetric := math.Inf(1)
junctionTT := occultationTimeToTT(junction.Time)
for segmentIndex, segment := range segments {
if len(segment) == 0 {
continue
}
for _, atStart := range []bool{true, false} {
point := occultationRiseSetSegmentEndpoint(segment, atStart)
pointTT := occultationTimeToTT(point.Time)
if atStart && junctionTT > pointTT+1e-8 || !atStart && junctionTT < pointTT-1e-8 ||
math.Abs(junctionTT-pointTT) > 3*stepDays || occultationPathDistanceKM(junction, point) > 3000 {
continue
}
metric := occultationPathDistanceKM(junction, point) + math.Abs(junctionTT-pointTT)*8640
if metric < bestMetric {
best = occultationRiseSetEndpoint{segmentIndex: segmentIndex, atStart: atStart, point: point}
bestMetric = metric
}
}
}
return best, bestMetric < math.Inf(1)
}
func refineOccultationRiseSetPhaseJunction(
seed OccultationPathPoint,
location *time.Location,
cache *occultationRiseSetEvaluationCache,
) (OccultationPathPoint, bool) {
coordinates := [3]float64{seed.Longitude, seed.Latitude, occultationTimeToTT(seed.Time)}
for iteration := 0; iteration < 24; iteration++ {
evaluation := cache.evaluation(coordinates[2])
residual, ok := occultationRiseSetPhaseJunctionResidualAt(evaluation, coordinates[0], coordinates[1])
if !ok {
return OccultationPathPoint{}, false
}
if math.Abs(residual[0]) <= 1e-8 && math.Abs(residual[1]) <= occultationRiseSetJunctionDerivativeTolerance && math.Abs(residual[2]) <= 1e-8 {
break
}
matrix, ok := occultationRiseSetPhaseJunctionJacobian(coordinates, residual, cache)
if !ok {
return OccultationPathPoint{}, false
}
delta, ok := solveSolarEclipse3x3(matrix, [3]float64{-residual[0], -residual[1], -residual[2]})
if !ok {
return OccultationPathPoint{}, false
}
occultationRiseSetLimitJunctionDelta(&delta)
coordinates[0] = normalizeLongitude(coordinates[0] + delta[0])
coordinates[1] += delta[1]
coordinates[2] += delta[2]
if coordinates[1] <= -89.999999 || coordinates[1] >= 89.999999 {
return OccultationPathPoint{}, false
}
}
evaluation := cache.evaluation(coordinates[2])
residual, ok := occultationRiseSetPhaseJunctionResidualAt(evaluation, coordinates[0], coordinates[1])
if !ok || math.Abs(residual[0]) > 1e-7 || math.Abs(residual[1]) > occultationRiseSetJunctionDerivativeTolerance || math.Abs(residual[2]) > 1e-7 {
return OccultationPathPoint{}, false
}
state := evaluation.center.stateAt(coordinates[0], coordinates[1])
return OccultationPathPoint{Time: occultationTTToLocation(coordinates[2], location), Longitude: coordinates[0], Latitude: coordinates[1], MoonAltitude: state.moonAltitude}, true
}
func refineOccultationRiseSetPhaseJunctionOnHorizon(
seed OccultationPathPoint,
location *time.Location,
cache *occultationRiseSetEvaluationCache,
) (OccultationPathPoint, bool) {
seedTT := occultationTimeToTT(seed.Time)
coordinates := [2]float64{
occultationRiseSetHorizonAngle(seedTT, seed.Longitude, seed.Latitude, cache.context),
0,
}
residualAt := func(value [2]float64) ([2]float64, float64, float64, float64, bool) {
tt := seedTT + value[1]/1440
longitude, latitude, horizonOK := occultationRiseSetHorizonPoint(tt, value[0], cache.context)
if !horizonOK {
return [2]float64{}, 0, 0, 0, false
}
evaluation := cache.evaluation(tt)
state := evaluation.center.stateAt(longitude, latitude)
residual := [2]float64{state.contactMetric, evaluation.contactDerivative(longitude, latitude)}
return residual, tt, longitude, latitude,
state.valid && finite(residual[0]) && finite(residual[1])
}
const angleStep = 1e-4
const timeStepMinutes = 1.0 / 60.0
for iteration := 0; iteration < 32; iteration++ {
residual, _, _, _, ok := residualAt(coordinates)
if !ok {
return OccultationPathPoint{}, false
}
if math.Abs(residual[0]) <= 1e-8 && math.Abs(residual[1]) <= occultationRiseSetJunctionDerivativeTolerance {
break
}
plusAngle, _, _, _, plusAngleOK := residualAt([2]float64{coordinates[0] + angleStep, coordinates[1]})
minusAngle, _, _, _, minusAngleOK := residualAt([2]float64{coordinates[0] - angleStep, coordinates[1]})
plusTime, _, _, _, plusTimeOK := residualAt([2]float64{coordinates[0], coordinates[1] + timeStepMinutes})
minusTime, _, _, _, minusTimeOK := residualAt([2]float64{coordinates[0], coordinates[1] - timeStepMinutes})
if !plusAngleOK || !minusAngleOK || !plusTimeOK || !minusTimeOK {
return OccultationPathPoint{}, false
}
matrix := [2][2]float64{
{(plusAngle[0] - minusAngle[0]) / (2 * angleStep), (plusTime[0] - minusTime[0]) / (2 * timeStepMinutes)},
{(plusAngle[1] - minusAngle[1]) / (2 * angleStep), (plusTime[1] - minusTime[1]) / (2 * timeStepMinutes)},
}
determinant := matrix[0][0]*matrix[1][1] - matrix[0][1]*matrix[1][0]
if !finite(determinant) || math.Abs(determinant) < 1e-18 {
return OccultationPathPoint{}, false
}
delta := [2]float64{
(-residual[0]*matrix[1][1] + matrix[0][1]*residual[1]) / determinant,
(-matrix[0][0]*residual[1] + residual[0]*matrix[1][0]) / determinant,
}
if math.Abs(delta[0]) > 0.25 {
delta[0] = math.Copysign(0.25, delta[0])
}
if math.Abs(delta[1]) > 5 {
delta[1] = math.Copysign(5, delta[1])
}
coordinates[0] = riseSetNormalizeRadians(coordinates[0] + delta[0])
coordinates[1] += delta[1]
}
residual, tt, longitude, latitude, ok := residualAt(coordinates)
if !ok || math.Abs(residual[0]) > 1e-7 || math.Abs(residual[1]) > occultationRiseSetJunctionDerivativeTolerance {
return OccultationPathPoint{}, false
}
state := cache.context(tt).stateAt(longitude, latitude)
return OccultationPathPoint{
Time: occultationTTToLocation(tt, location), Longitude: longitude,
Latitude: latitude, MoonAltitude: state.moonAltitude,
}, true
}
func refineOccultationRiseSetDirectionJunction(
tt, longitude, latitude float64,
greatest bool,
location *time.Location,
cache *occultationRiseSetEvaluationCache,
) (OccultationPathPoint, bool) {
coordinates := [3]float64{longitude, latitude, tt}
for iteration := 0; iteration < 24; iteration++ {
evaluation := cache.evaluation(coordinates[2])
residual, ok := occultationRiseSetDirectionJunctionResidualAt(evaluation, coordinates[0], coordinates[1], greatest)
if !ok {
return OccultationPathPoint{}, false
}
if math.Abs(residual[0]) <= 1e-8 && math.Abs(residual[1]) <= 1e-9 && math.Abs(residual[2]) <= 1e-7 {
break
}
matrix, ok := occultationRiseSetDirectionJunctionJacobian(coordinates, residual, greatest, cache)
if !ok {
return OccultationPathPoint{}, false
}
delta, ok := solveSolarEclipse3x3(matrix, [3]float64{-residual[0], -residual[1], -residual[2]})
if !ok {
return OccultationPathPoint{}, false
}
occultationRiseSetLimitJunctionDelta(&delta)
coordinates[0] = normalizeLongitude(coordinates[0] + delta[0])
coordinates[1] += delta[1]
coordinates[2] += delta[2]
if coordinates[1] <= -89.999999 || coordinates[1] >= 89.999999 {
return OccultationPathPoint{}, false
}
}
evaluation := cache.evaluation(coordinates[2])
residual, ok := occultationRiseSetDirectionJunctionResidualAt(evaluation, coordinates[0], coordinates[1], greatest)
if !ok || math.Abs(residual[0]) > 1e-7 || math.Abs(residual[1]) > 1e-8 || math.Abs(residual[2]) > 1e-7 {
return OccultationPathPoint{}, false
}
state := evaluation.center.stateAt(coordinates[0], coordinates[1])
return OccultationPathPoint{Time: occultationTTToLocation(coordinates[2], location), Longitude: coordinates[0], Latitude: coordinates[1], MoonAltitude: state.moonAltitude}, true
}
func refineOccultationRiseSetDirectionJunctionOnHorizon(
first, second OccultationPathPoint,
greatest bool,
location *time.Location,
cache *occultationRiseSetEvaluationCache,
) (OccultationPathPoint, bool) {
seedTT := (occultationTimeToTT(first.Time) + occultationTimeToTT(second.Time)) / 2
firstAngle := occultationRiseSetHorizonAngle(seedTT, first.Longitude, first.Latitude, cache.context)
secondAngle := occultationRiseSetHorizonAngle(seedTT, second.Longitude, second.Latitude, cache.context)
if !finite(firstAngle) || !finite(secondAngle) {
return OccultationPathPoint{}, false
}
coordinates := [2]float64{
riseSetNormalizeRadians(firstAngle + math.Remainder(secondAngle-firstAngle, 2*math.Pi)/2),
0,
}
residualAt := func(value [2]float64) ([2]float64, float64, float64, float64, bool) {
tt := seedTT + value[1]/1440
longitude, latitude, ok := occultationRiseSetHorizonPoint(tt, value[0], cache.context)
if !ok {
return [2]float64{}, 0, 0, 0, false
}
evaluation := cache.evaluation(tt)
phaseResidual, phaseOK := occultationRiseSetPhaseResidual(evaluation, longitude, latitude, greatest)
altitudeDerivative := evaluation.moonAltitudeDerivative(longitude, latitude)
return [2]float64{phaseResidual, altitudeDerivative}, tt, longitude, latitude,
phaseOK && finite(phaseResidual) && finite(altitudeDerivative)
}
const (
angleStep = 1e-4
timeStepMinute = 1.0 / 60
)
for iteration := 0; iteration < 32; iteration++ {
residual, _, _, _, ok := residualAt(coordinates)
if !ok {
return OccultationPathPoint{}, false
}
if math.Abs(residual[0]) <= 1e-8 && math.Abs(residual[1]) <= 1e-7 {
break
}
anglePlus, _, _, _, anglePlusOK := residualAt([2]float64{coordinates[0] + angleStep, coordinates[1]})
angleMinus, _, _, _, angleMinusOK := residualAt([2]float64{coordinates[0] - angleStep, coordinates[1]})
timePlus, _, _, _, timePlusOK := residualAt([2]float64{coordinates[0], coordinates[1] + timeStepMinute})
timeMinus, _, _, _, timeMinusOK := residualAt([2]float64{coordinates[0], coordinates[1] - timeStepMinute})
if !anglePlusOK || !angleMinusOK || !timePlusOK || !timeMinusOK {
return OccultationPathPoint{}, false
}
matrix := [2][2]float64{
{(anglePlus[0] - angleMinus[0]) / (2 * angleStep), (timePlus[0] - timeMinus[0]) / (2 * timeStepMinute)},
{(anglePlus[1] - angleMinus[1]) / (2 * angleStep), (timePlus[1] - timeMinus[1]) / (2 * timeStepMinute)},
}
determinant := matrix[0][0]*matrix[1][1] - matrix[0][1]*matrix[1][0]
if !finite(determinant) || math.Abs(determinant) < 1e-18 {
return OccultationPathPoint{}, false
}
delta := [2]float64{
(-residual[0]*matrix[1][1] + matrix[0][1]*residual[1]) / determinant,
(-matrix[0][0]*residual[1] + residual[0]*matrix[1][0]) / determinant,
}
if math.Abs(delta[0]) > 0.25 {
delta[0] = math.Copysign(0.25, delta[0])
}
if math.Abs(delta[1]) > 5 {
delta[1] = math.Copysign(5, delta[1])
}
coordinates[0] = riseSetNormalizeRadians(coordinates[0] + delta[0])
coordinates[1] += delta[1]
}
residual, tt, longitude, latitude, ok := residualAt(coordinates)
if !ok || math.Abs(residual[0]) > 1e-7 || math.Abs(residual[1]) > 1e-7 {
return OccultationPathPoint{}, false
}
state := cache.context(tt).stateAt(longitude, latitude)
if !state.valid || math.Abs(state.moonAltitude) > 1e-7 {
return OccultationPathPoint{}, false
}
return OccultationPathPoint{
Time: occultationTTToLocation(tt, location), Longitude: longitude,
Latitude: latitude, MoonAltitude: state.moonAltitude,
}, true
}
func occultationRiseSetPhaseJunctionResidualAt(evaluation occultationRiseSetEvaluation, longitude, latitude float64) ([3]float64, bool) {
state := evaluation.center.stateAt(longitude, latitude)
residual := [3]float64{state.contactMetric, evaluation.contactDerivative(longitude, latitude), state.moonAltitude}
return residual, state.valid && finite(residual[1]) && finite(residual[2])
}
func occultationRiseSetDirectionJunctionResidualAt(evaluation occultationRiseSetEvaluation, longitude, latitude float64, greatest bool) ([3]float64, bool) {
phase, phaseOK := occultationRiseSetPhaseResidual(evaluation, longitude, latitude, greatest)
state := evaluation.center.stateAt(longitude, latitude)
residual := [3]float64{phase, state.moonAltitude, evaluation.moonAltitudeDerivative(longitude, latitude)}
return residual, phaseOK && state.valid && finite(residual[1]) && finite(residual[2])
}
func occultationRiseSetPhaseResidual(evaluation occultationRiseSetEvaluation, longitude, latitude float64, greatest bool) (float64, bool) {
if greatest {
value := evaluation.separationDerivative(longitude, latitude)
return value, finite(value)
}
state := evaluation.center.stateAt(longitude, latitude)
return state.contactMetric, state.valid && finite(state.contactMetric)
}
func occultationRiseSetPhaseJunctionJacobian(
coordinates [3]float64,
residual [3]float64,
cache *occultationRiseSetEvaluationCache,
) ([3][3]float64, bool) {
const geographicStep = 1e-4
const timeStep = 1.0 / 86400.0
evaluation := cache.evaluation(coordinates[2])
plusLongitude, longitudeOK := occultationRiseSetPhaseJunctionResidualAt(evaluation, coordinates[0]+geographicStep, coordinates[1])
plusLatitude, latitudeOK := occultationRiseSetPhaseJunctionResidualAt(evaluation, coordinates[0], coordinates[1]+geographicStep)
plusTimeEval := cache.evaluation(coordinates[2] + timeStep)
plusTime, timeOK := occultationRiseSetPhaseJunctionResidualAt(plusTimeEval, coordinates[0], coordinates[1])
if !longitudeOK || !latitudeOK || !timeOK {
return [3][3]float64{}, false
}
var matrix [3][3]float64
for row := range matrix {
matrix[row][0] = (plusLongitude[row] - residual[row]) / geographicStep
matrix[row][1] = (plusLatitude[row] - residual[row]) / geographicStep
matrix[row][2] = (plusTime[row] - residual[row]) / timeStep
}
return matrix, true
}
func occultationRiseSetDirectionJunctionJacobian(
coordinates [3]float64,
residual [3]float64,
greatest bool,
cache *occultationRiseSetEvaluationCache,
) ([3][3]float64, bool) {
const geographicStep = 1e-4
const timeStep = 1.0 / 86400.0
evaluation := cache.evaluation(coordinates[2])
plusLongitude, longitudeOK := occultationRiseSetDirectionJunctionResidualAt(
evaluation, coordinates[0]+geographicStep, coordinates[1], greatest,
)
plusLatitude, latitudeOK := occultationRiseSetDirectionJunctionResidualAt(
evaluation, coordinates[0], coordinates[1]+geographicStep, greatest,
)
plusTimeEval := cache.evaluation(coordinates[2] + timeStep)
plusTime, timeOK := occultationRiseSetDirectionJunctionResidualAt(
plusTimeEval, coordinates[0], coordinates[1], greatest,
)
if !longitudeOK || !latitudeOK || !timeOK {
return [3][3]float64{}, false
}
var matrix [3][3]float64
for row := range matrix {
matrix[row][0] = (plusLongitude[row] - residual[row]) / geographicStep
matrix[row][1] = (plusLatitude[row] - residual[row]) / geographicStep
matrix[row][2] = (plusTime[row] - residual[row]) / timeStep
}
return matrix, true
}
func occultationRiseSetLimitJunctionDelta(delta *[3]float64) {
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]) > 5.0/1440 {
delta[2] = math.Copysign(5.0/1440, delta[2])
}
}
func occultationRiseSetSegmentEndpoint(segment []OccultationPathPoint, atStart bool) OccultationPathPoint {
if atStart {
return segment[0]
}
return segment[len(segment)-1]
}
func occultationRiseSetAddEndpoint(segment []OccultationPathPoint, point OccultationPathPoint, atStart bool) []OccultationPathPoint {
current := occultationRiseSetSegmentEndpoint(segment, atStart)
if math.Abs(occultationTimeToTT(current.Time)-occultationTimeToTT(point.Time)) <= occultationRiseSetTimeEpsilonDays && occultationPathDistanceKM(current, point) <= 0.01 {
if atStart {
segment[0] = point
} else {
segment[len(segment)-1] = point
}
return segment
}
if atStart {
return append([]OccultationPathPoint{point}, segment...)
}
return append(segment, point)
}