2026-09-17 12:27:40 +08:00
|
|
|
package basic
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"math"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// occultationStationBoundarySample is the result of correcting one geocentric
|
|
|
|
|
// contact seed with the station-centred contact equation. Residuals follow
|
|
|
|
|
// occultationRiseSetContext.stateAt, which returns degrees for both the contact
|
|
|
|
|
// gap and the lunar altitude; the two residual fields are therefore degrees,
|
|
|
|
|
// not arcseconds.
|
|
|
|
|
type occultationStationBoundarySample struct {
|
|
|
|
|
point OccultationPathPoint
|
|
|
|
|
contactResidualDeg float64
|
|
|
|
|
horizonResidualDeg float64
|
|
|
|
|
offsetKM float64
|
|
|
|
|
seedResidualDeg float64
|
|
|
|
|
valid bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
occultationStationOracleInitialStepKM = 25.0
|
|
|
|
|
occultationStationOracleMaximumOffsetKM = 2000.0
|
|
|
|
|
occultationStationOracleRootToleranceKM = 0.001
|
|
|
|
|
// 站心接触残差与 stateAt 同单位(度);1e-7 度 = 3.6e-4 角秒。
|
|
|
|
|
// The station contact residual uses the stateAt unit (degrees); 1e-7 deg = 3.6e-4 arcsec.
|
|
|
|
|
occultationStationOracleResidualToleranceDeg = 1e-7
|
|
|
|
|
// 地面偏移每公里最多改变约 1/384400 弧度的月球视差方向,取 3e-4 度/公里作为
|
|
|
|
|
// 接触残差的斜率上限,用来复核“括号已塌缩但残差没到容差”的解。
|
|
|
|
|
occultationStationOracleResidualSlopeDegPerKM = 3e-4
|
|
|
|
|
occultationStationEnvelopeMaximumOffsetKM = 2500.0
|
|
|
|
|
occultationStationHorizonMaximumOffsetKM = 250.0
|
|
|
|
|
occultationStationHorizonResidualToleranceDeg = 1e-7
|
|
|
|
|
// 地平线接触求解的最终验收带;带内的负残差是数值噪声。
|
|
|
|
|
occultationStationHorizonAcceptanceDeg = 1e-5
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// occultationStationCorrectBoundaryPoint refines a geocentric contact point
|
|
|
|
|
// along the local ground cross-track direction. The contact equation is
|
|
|
|
|
// evaluated by the existing station-centred vector context, so moon parallax,
|
|
|
|
|
// target parallax, apparent radii and the horizon all share one observer model.
|
|
|
|
|
// The function deliberately does not choose a polygon or join branches.
|
|
|
|
|
func occultationStationCorrectBoundaryPoint(
|
|
|
|
|
tt float64,
|
|
|
|
|
seed OccultationPathPoint,
|
|
|
|
|
frameAt occultationPathFrameFunc,
|
|
|
|
|
contextAt occultationRiseSetContextFunc,
|
|
|
|
|
total bool,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) (occultationStationBoundarySample, bool) {
|
|
|
|
|
frame, ok := frameAt(tt)
|
|
|
|
|
if !ok {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
if contextAt == nil {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
contextFactory := contextAt
|
|
|
|
|
if total {
|
|
|
|
|
contextFactory = func(value float64) occultationRiseSetContext {
|
|
|
|
|
return contextAt(value).withInternalContact()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
context := contextFactory(tt)
|
|
|
|
|
if !context.valid {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
seedFixed := occultationStationSurfaceVector(seed.Longitude, seed.Latitude)
|
|
|
|
|
if occultationPathNorm(seedFixed) <= 0 {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
cross, directionOK := occultationStationCrossTrackAt(tt, frame, frameAt, seedFixed)
|
|
|
|
|
if !directionOK {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
evaluate := func(offsetKM float64) (float64, float64, float64, bool) {
|
|
|
|
|
fixed := occultationStationOffsetSurfaceVector(seedFixed, cross, offsetKM)
|
|
|
|
|
longitude, latitude := occultationStationGeodetic(fixed)
|
|
|
|
|
state := context.stateAt(longitude, latitude)
|
|
|
|
|
if !state.valid || !finite(state.contactMetric) || !finite(state.moonAltitude) {
|
|
|
|
|
return 0, 0, 0, false
|
|
|
|
|
}
|
|
|
|
|
return state.contactMetric, state.moonAltitude, longitude, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seedResidual, seedAltitude, _, seedOK := evaluate(0)
|
|
|
|
|
if !seedOK {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
if math.Abs(seedResidual) <= occultationStationOracleResidualToleranceDeg {
|
|
|
|
|
return occultationStationOracleSampleAt(
|
|
|
|
|
tt, seedFixed, 0, seedResidual, seedResidual, seedAltitude, frame, location,
|
|
|
|
|
), true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
leftOffset, rightOffset, bracketOK := occultationStationFindBracket(seedResidual, evaluate)
|
|
|
|
|
if !bracketOK {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
leftResidual, _, _, leftOK := evaluate(leftOffset)
|
|
|
|
|
rightResidual, _, _, rightOK := evaluate(rightOffset)
|
|
|
|
|
if !leftOK || !rightOK || leftResidual*rightResidual > 0 {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
for iteration := 0; iteration < 64; iteration++ {
|
|
|
|
|
middleOffset := (leftOffset + rightOffset) / 2
|
|
|
|
|
middleResidual, _, _, middleOK := evaluate(middleOffset)
|
|
|
|
|
if !middleOK {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
if math.Abs(middleResidual) <= occultationStationOracleResidualToleranceDeg ||
|
|
|
|
|
math.Abs(rightOffset-leftOffset) <= occultationStationOracleRootToleranceKM {
|
|
|
|
|
leftOffset, rightOffset = middleOffset, middleOffset
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if leftResidual*middleResidual <= 0 {
|
|
|
|
|
rightOffset, rightResidual = middleOffset, middleResidual
|
|
|
|
|
} else {
|
|
|
|
|
leftOffset, leftResidual = middleOffset, middleResidual
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
offset := (leftOffset + rightOffset) / 2
|
|
|
|
|
residual, altitude, _, solved := evaluate(offset)
|
|
|
|
|
// 括号塌缩不等于解存在:偏离接触方程超过该宽度可解释范围的“收敛”点只是无效
|
|
|
|
|
// 区间里的一个位置,不能当成已求解。
|
|
|
|
|
if !solved || !finite(residual) ||
|
|
|
|
|
math.Abs(residual) > occultationStationOracleResidualBound(rightOffset-leftOffset) {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
pointFixed := occultationStationOffsetSurfaceVector(seedFixed, cross, offset)
|
|
|
|
|
return occultationStationOracleSampleAt(
|
|
|
|
|
tt, pointFixed, offset, seedResidual, residual, altitude, frame, location,
|
|
|
|
|
), true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// occultationStationCorrectContours maps a geocentric contact-envelope seed to
|
|
|
|
|
// the station-centred temporal envelope without changing its sampling order.
|
|
|
|
|
// A static time union is bounded by contact=0 and d(contact)/dt=0; correcting
|
|
|
|
|
// contact alone moves a seed onto an instantaneous footprint but does not keep
|
|
|
|
|
// it on the outer envelope.
|
|
|
|
|
func occultationStationCorrectContours(
|
|
|
|
|
contours [][]OccultationPathPoint,
|
|
|
|
|
curves []OccultationRiseSetCurve,
|
|
|
|
|
cache *occultationRiseSetEvaluationCache,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) [][]OccultationPathPoint {
|
|
|
|
|
if len(contours) == 0 || cache == nil {
|
|
|
|
|
return contours
|
|
|
|
|
}
|
|
|
|
|
contextAt := cache.context
|
|
|
|
|
corrected := make([][]OccultationPathPoint, 0, len(contours))
|
|
|
|
|
for _, contour := range contours {
|
|
|
|
|
if len(contour) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
samples, solved := occultationStationEnvelopeSamples(contour, contextAt, false, location)
|
|
|
|
|
for start := 0; start < len(contour); {
|
|
|
|
|
for start < len(contour) && !solved[start] {
|
|
|
|
|
start++
|
|
|
|
|
}
|
|
|
|
|
if start == len(contour) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
end := start
|
|
|
|
|
for end < len(contour) && solved[end] {
|
|
|
|
|
end++
|
|
|
|
|
}
|
|
|
|
|
if end-start >= 2 {
|
|
|
|
|
segment := make([]OccultationPathPoint, end-start)
|
|
|
|
|
for index := start; index < end; index++ {
|
|
|
|
|
point := samples[index].point
|
|
|
|
|
point.Time = contour[index].Time
|
|
|
|
|
point.WidthKM = contour[index].WidthKM
|
|
|
|
|
segment[index-start] = point
|
|
|
|
|
}
|
|
|
|
|
for _, sampleRange := range occultationContinuousBoundaryRanges(segment) {
|
|
|
|
|
if sampleRange.end-sampleRange.start >= 2 {
|
|
|
|
|
corrected = append(corrected, append(
|
|
|
|
|
[]OccultationPathPoint(nil), segment[sampleRange.start:sampleRange.end]...,
|
|
|
|
|
))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
start = end
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(curves) == 0 {
|
|
|
|
|
return occultationStationDensifyContours(corrected, cache, location)
|
|
|
|
|
}
|
|
|
|
|
visible := occultationStationVisibleEnvelopeContours(corrected, curves, cache, location)
|
|
|
|
|
// Local corrected fragments do not establish a complete visible envelope.
|
|
|
|
|
return occultationStationDensifyContours(visible, cache, location)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// occultationStationCorrectLimitSeries keeps the public north/south limit
|
|
|
|
|
// samples on the station contact curve. Static temporal envelopes are exposed
|
|
|
|
|
// separately through BandContours.
|
|
|
|
|
func occultationStationCorrectLimitSeries(
|
|
|
|
|
points []OccultationPathPoint,
|
|
|
|
|
frameAt occultationPathFrameFunc,
|
|
|
|
|
contextAt occultationRiseSetContextFunc,
|
|
|
|
|
total bool,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) []OccultationPathPoint {
|
|
|
|
|
if len(points) == 0 || frameAt == nil || contextAt == nil {
|
|
|
|
|
return points
|
|
|
|
|
}
|
|
|
|
|
corrected := append([]OccultationPathPoint(nil), points...)
|
|
|
|
|
for index, seed := range points {
|
|
|
|
|
sample, ok := occultationStationCorrectBoundaryPoint(
|
|
|
|
|
centerTimeTT(seed.Time), seed, frameAt, contextAt, total, location,
|
|
|
|
|
)
|
|
|
|
|
if !ok || !sample.valid {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
point := sample.point
|
|
|
|
|
// Public limit tracks retain their event sample times exactly; the
|
|
|
|
|
// station solver may otherwise round-trip through civil time.
|
|
|
|
|
point.Time = seed.Time
|
|
|
|
|
point.WidthKM = seed.WidthKM
|
|
|
|
|
corrected[index] = point
|
|
|
|
|
}
|
|
|
|
|
return corrected
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationEnvelopeSamples(
|
|
|
|
|
points []OccultationPathPoint,
|
|
|
|
|
contextAt occultationRiseSetContextFunc,
|
|
|
|
|
total bool,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) ([]occultationStationBoundarySample, []bool) {
|
|
|
|
|
samples := make([]occultationStationBoundarySample, len(points))
|
|
|
|
|
solved := make([]bool, len(points))
|
|
|
|
|
try := func(index int, seed OccultationPathPoint) bool {
|
|
|
|
|
sample, ok := occultationStationCorrectEnvelopePoint(
|
|
|
|
|
centerTimeTT(points[index].Time), seed, contextAt, total, location,
|
|
|
|
|
)
|
|
|
|
|
if !ok || !sample.valid || occultationPathDistanceKM(points[index], sample.point) >
|
|
|
|
|
occultationStationEnvelopeMaximumOffsetKM {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
sample.offsetKM = occultationPathDistanceKM(points[index], sample.point)
|
|
|
|
|
samples[index], solved[index] = sample, true
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
for index, seed := range points {
|
|
|
|
|
// 延拓初值:轮廓上相邻点的解彼此接近,用上一个已收敛解做 Newton 初值通常能把
|
|
|
|
|
// 迭代次数从十余次降到几次;若它落在原始点的容差之外或求解失败,再退回几何底点。
|
|
|
|
|
// Continuation seed: neighbouring contour points solve to nearby stations, so the
|
|
|
|
|
// previous converged solution is a much better Newton seed than the geometric point.
|
|
|
|
|
// When it fails or lands outside the original point's tolerance, fall back to the
|
|
|
|
|
// geometric seed.
|
|
|
|
|
if index > 0 && solved[index-1] {
|
|
|
|
|
neighbor := seed
|
|
|
|
|
neighbor.Longitude = samples[index-1].point.Longitude
|
|
|
|
|
neighbor.Latitude = samples[index-1].point.Latitude
|
|
|
|
|
if try(index, neighbor) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
try(index, seed)
|
|
|
|
|
}
|
|
|
|
|
for pass := 0; pass < 2; pass++ {
|
|
|
|
|
for index := 1; index < len(points); index++ {
|
|
|
|
|
if solved[index] || !solved[index-1] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seed := points[index]
|
|
|
|
|
seed.Longitude = samples[index-1].point.Longitude
|
|
|
|
|
seed.Latitude = samples[index-1].point.Latitude
|
|
|
|
|
try(index, seed)
|
|
|
|
|
}
|
|
|
|
|
for index := len(points) - 2; index >= 0; index-- {
|
|
|
|
|
if solved[index] || !solved[index+1] {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seed := points[index]
|
|
|
|
|
seed.Longitude = samples[index+1].point.Longitude
|
|
|
|
|
seed.Latitude = samples[index+1].point.Latitude
|
|
|
|
|
try(index, seed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return samples, solved
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// occultationStationCorrectContactPoint refines an arbitrary contact-arc seed
|
|
|
|
|
// with a local two-dimensional Newton step. The one-dimensional cross-track
|
|
|
|
|
// oracle is preferable for time-contour sides; this variant is for the two
|
|
|
|
|
// instantaneous contact arcs that close a direct static ring.
|
|
|
|
|
func occultationStationCorrectContactPoint(
|
|
|
|
|
tt float64,
|
|
|
|
|
seed OccultationPathPoint,
|
|
|
|
|
contextAt occultationRiseSetContextFunc,
|
|
|
|
|
total bool,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) (occultationStationBoundarySample, bool) {
|
|
|
|
|
if contextAt == nil {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
context := contextAt(tt)
|
|
|
|
|
if total {
|
|
|
|
|
context = context.withInternalContact()
|
|
|
|
|
}
|
|
|
|
|
if !context.valid {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
longitude, latitude := seed.Longitude, seed.Latitude
|
|
|
|
|
seedResidual := math.NaN()
|
|
|
|
|
for iteration := 0; iteration < 16; iteration++ {
|
|
|
|
|
state := context.stateAt(longitude, latitude)
|
|
|
|
|
if !state.valid || !finite(state.contactMetric) {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
if iteration == 0 {
|
|
|
|
|
seedResidual = state.contactMetric
|
|
|
|
|
}
|
|
|
|
|
if math.Abs(state.contactMetric) <= occultationStationOracleResidualToleranceDeg {
|
|
|
|
|
return occultationStationSampleFromCoordinates(
|
|
|
|
|
tt, seed, longitude, latitude, state.contactMetric, state.moonAltitude, seedResidual, location,
|
|
|
|
|
), true
|
|
|
|
|
}
|
|
|
|
|
const coordinateStepDeg = 0.005
|
|
|
|
|
plusLongitude := context.stateAt(normalizeLongitude(longitude+coordinateStepDeg), latitude)
|
|
|
|
|
minusLongitude := context.stateAt(normalizeLongitude(longitude-coordinateStepDeg), latitude)
|
|
|
|
|
plusLatitude := context.stateAt(longitude, math.Min(89.999999, latitude+coordinateStepDeg))
|
|
|
|
|
minusLatitude := context.stateAt(longitude, math.Max(-89.999999, latitude-coordinateStepDeg))
|
|
|
|
|
if !plusLongitude.valid || !minusLongitude.valid || !plusLatitude.valid || !minusLatitude.valid {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
cosLatitude := math.Max(0.05, math.Cos(latitude*rad))
|
|
|
|
|
gradientX := (plusLongitude.contactMetric - minusLongitude.contactMetric) / (2 * coordinateStepDeg * cosLatitude)
|
|
|
|
|
gradientY := (plusLatitude.contactMetric - minusLatitude.contactMetric) / (2 * coordinateStepDeg)
|
|
|
|
|
gradientSquared := gradientX*gradientX + gradientY*gradientY
|
|
|
|
|
if !finite(gradientSquared) || gradientSquared <= 1e-18 {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
deltaX := -state.contactMetric * gradientX / gradientSquared
|
|
|
|
|
deltaY := -state.contactMetric * gradientY / gradientSquared
|
|
|
|
|
length := math.Hypot(deltaX, deltaY)
|
|
|
|
|
if length > 1.0 {
|
|
|
|
|
scale := 1.0 / length
|
|
|
|
|
deltaX *= scale
|
|
|
|
|
deltaY *= scale
|
|
|
|
|
}
|
|
|
|
|
longitude = normalizeLongitude(longitude + deltaX/cosLatitude)
|
|
|
|
|
latitude = math.Max(-89.999999, math.Min(89.999999, latitude+deltaY))
|
|
|
|
|
}
|
|
|
|
|
state := context.stateAt(longitude, latitude)
|
|
|
|
|
if !state.valid || math.Abs(state.contactMetric) > 1e-5 {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
return occultationStationSampleFromCoordinates(
|
|
|
|
|
tt, seed, longitude, latitude, state.contactMetric, state.moonAltitude, seedResidual, location,
|
|
|
|
|
), true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type occultationStationEnvelopeState struct {
|
|
|
|
|
contactMetric float64
|
|
|
|
|
contactDerivative float64
|
|
|
|
|
moonAltitude float64
|
|
|
|
|
valid bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// occultationStationCorrectEnvelopePoint solves the two necessary conditions
|
|
|
|
|
// for an interior boundary of the station-visible time union at a fixed time:
|
|
|
|
|
// the station contact gap is zero and stationary in time. The geocentric
|
|
|
|
|
// north/south limit is only a seed; both equations are evaluated with the same
|
|
|
|
|
// topocentric Moon/target vectors used by local occultation calculations.
|
|
|
|
|
func occultationStationCorrectEnvelopePoint(
|
|
|
|
|
tt float64,
|
|
|
|
|
seed OccultationPathPoint,
|
|
|
|
|
contextAt occultationRiseSetContextFunc,
|
|
|
|
|
total bool,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) (occultationStationBoundarySample, bool) {
|
|
|
|
|
if contextAt == nil {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
contextFactory := contextAt
|
|
|
|
|
if total {
|
|
|
|
|
contextFactory = func(value float64) occultationRiseSetContext {
|
|
|
|
|
return contextAt(value).withInternalContact()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
evaluation := occultationRiseSetEvaluation{
|
|
|
|
|
tt: tt,
|
|
|
|
|
center: contextFactory(tt),
|
|
|
|
|
before: contextFactory(tt - occultationRiseSetDerivativeStepDays),
|
|
|
|
|
after: contextFactory(tt + occultationRiseSetDerivativeStepDays),
|
|
|
|
|
}
|
|
|
|
|
if !evaluation.center.valid || !evaluation.before.valid || !evaluation.after.valid {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
evaluate := func(longitude, latitude float64) occultationStationEnvelopeState {
|
|
|
|
|
center := evaluation.center.stateAt(longitude, latitude)
|
|
|
|
|
before := evaluation.before.stateAt(longitude, latitude)
|
|
|
|
|
after := evaluation.after.stateAt(longitude, latitude)
|
|
|
|
|
if !center.valid || !before.valid || !after.valid {
|
|
|
|
|
return occultationStationEnvelopeState{}
|
|
|
|
|
}
|
|
|
|
|
derivative := (after.contactMetric - before.contactMetric) /
|
|
|
|
|
(2 * occultationRiseSetDerivativeStepDays)
|
|
|
|
|
return occultationStationEnvelopeState{
|
|
|
|
|
contactMetric: center.contactMetric,
|
|
|
|
|
contactDerivative: derivative,
|
|
|
|
|
moonAltitude: center.moonAltitude,
|
|
|
|
|
valid: finite(center.contactMetric) && finite(derivative) && finite(center.moonAltitude),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
residualNorm := func(state occultationStationEnvelopeState) float64 {
|
|
|
|
|
return math.Hypot(
|
|
|
|
|
state.contactMetric,
|
|
|
|
|
state.contactDerivative*occultationRiseSetDerivativeStepDays,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
longitude, latitude := seed.Longitude, seed.Latitude
|
|
|
|
|
seedState := evaluate(longitude, latitude)
|
|
|
|
|
if !seedState.valid {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
for iteration := 0; iteration < 24; iteration++ {
|
|
|
|
|
state := evaluate(longitude, latitude)
|
|
|
|
|
if !state.valid {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
if math.Abs(state.contactMetric) <= occultationStationOracleResidualToleranceDeg &&
|
|
|
|
|
math.Abs(state.contactDerivative) <= occultationRiseSetJunctionDerivativeTolerance {
|
|
|
|
|
return occultationStationSampleFromCoordinates(
|
|
|
|
|
tt, seed, longitude, latitude, state.contactMetric, state.moonAltitude,
|
|
|
|
|
seedState.contactMetric, location,
|
|
|
|
|
), true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const coordinateStepDeg = 0.002
|
|
|
|
|
cosLatitude := math.Max(0.05, math.Cos(latitude*rad))
|
|
|
|
|
plusLongitude := evaluate(normalizeLongitude(longitude+coordinateStepDeg/cosLatitude), latitude)
|
|
|
|
|
minusLongitude := evaluate(normalizeLongitude(longitude-coordinateStepDeg/cosLatitude), latitude)
|
|
|
|
|
plusLatitude := evaluate(longitude, math.Min(89.999999, latitude+coordinateStepDeg))
|
|
|
|
|
minusLatitude := evaluate(longitude, math.Max(-89.999999, latitude-coordinateStepDeg))
|
|
|
|
|
if !plusLongitude.valid || !minusLongitude.valid || !plusLatitude.valid || !minusLatitude.valid {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
contactX := (plusLongitude.contactMetric - minusLongitude.contactMetric) / (2 * coordinateStepDeg)
|
|
|
|
|
contactY := (plusLatitude.contactMetric - minusLatitude.contactMetric) / (2 * coordinateStepDeg)
|
|
|
|
|
derivativeX := (plusLongitude.contactDerivative - minusLongitude.contactDerivative) / (2 * coordinateStepDeg)
|
|
|
|
|
derivativeY := (plusLatitude.contactDerivative - minusLatitude.contactDerivative) / (2 * coordinateStepDeg)
|
|
|
|
|
determinant := contactX*derivativeY - contactY*derivativeX
|
|
|
|
|
if !finite(determinant) || math.Abs(determinant) <= 1e-12 {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
deltaX := (-state.contactMetric*derivativeY + contactY*state.contactDerivative) / determinant
|
|
|
|
|
deltaY := (-contactX*state.contactDerivative + derivativeX*state.contactMetric) / determinant
|
|
|
|
|
if !finite(deltaX) || !finite(deltaY) {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
if length := math.Hypot(deltaX, deltaY); length > 6 {
|
|
|
|
|
scale := 6 / length
|
|
|
|
|
deltaX *= scale
|
|
|
|
|
deltaY *= scale
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentNorm := residualNorm(state)
|
|
|
|
|
accepted := false
|
|
|
|
|
for damping := 1.0; damping >= 1.0/128; damping /= 2 {
|
|
|
|
|
candidateLongitude := normalizeLongitude(longitude + damping*deltaX/cosLatitude)
|
|
|
|
|
candidateLatitude := math.Max(-89.999999, math.Min(89.999999, latitude+damping*deltaY))
|
|
|
|
|
if occultationPathDistanceKMValues(
|
|
|
|
|
seed.Longitude, seed.Latitude, candidateLongitude, candidateLatitude,
|
|
|
|
|
) > occultationStationEnvelopeMaximumOffsetKM {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
candidate := evaluate(candidateLongitude, candidateLatitude)
|
|
|
|
|
if !candidate.valid || residualNorm(candidate) >= currentNorm {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
longitude, latitude = candidateLongitude, candidateLatitude
|
|
|
|
|
accepted = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if !accepted {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
state := evaluate(longitude, latitude)
|
|
|
|
|
if !state.valid || math.Abs(state.contactMetric) > 1e-5 ||
|
|
|
|
|
math.Abs(state.contactDerivative) > 1e-4 {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
return occultationStationSampleFromCoordinates(
|
|
|
|
|
tt, seed, longitude, latitude, state.contactMetric, state.moonAltitude,
|
|
|
|
|
seedState.contactMetric, location,
|
|
|
|
|
), true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
occultationStationEnvelopeTimeScale = 360.0
|
|
|
|
|
occultationStationEnvelopeArcStepDegrees = 0.2
|
|
|
|
|
occultationStationEnvelopeMinArcStepDegrees = 0.001
|
|
|
|
|
occultationStationEnvelopeTargetSpacingKM = 30.0
|
|
|
|
|
occultationStationEnvelopeMaxArcSteps = 8192
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type occultationStationEnvelopeArcState struct {
|
|
|
|
|
coordinates [3]float64
|
|
|
|
|
tangent [3]float64
|
|
|
|
|
point OccultationPathPoint
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type occultationStationEnvelopeTrace struct {
|
|
|
|
|
points []OccultationPathPoint
|
|
|
|
|
boundary bool
|
|
|
|
|
closed bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type occultationStationEnvelopeKind uint8
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
occultationStationContactEnvelope occultationStationEnvelopeKind = iota
|
|
|
|
|
occultationStationVisibilityEnvelope
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type occultationStationEnvelopeModel struct {
|
|
|
|
|
kind occultationStationEnvelopeKind
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// occultationStationVisibleEnvelopeContours traces the station-centred static
|
|
|
|
|
// contact envelope as an implicit curve in longitude, latitude and time. The
|
|
|
|
|
// pseudo-arclength parameter remains regular where fixed-time north/south roots
|
|
|
|
|
// meet, so a real temporal fold is preserved instead of becoming a branch jump.
|
|
|
|
|
func occultationStationVisibleEnvelopeContours(
|
|
|
|
|
seeds [][]OccultationPathPoint,
|
|
|
|
|
curves []OccultationRiseSetCurve,
|
|
|
|
|
cache *occultationRiseSetEvaluationCache,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) [][]OccultationPathPoint {
|
|
|
|
|
model := occultationStationEnvelopeModel{kind: occultationStationContactEnvelope}
|
|
|
|
|
minimumTT, maximumTT := math.Inf(1), math.Inf(-1)
|
|
|
|
|
candidates := make([]OccultationPathPoint, 0, len(seeds))
|
|
|
|
|
for _, segment := range seeds {
|
|
|
|
|
if len(segment) < 2 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
best := OccultationPathPoint{MoonAltitude: math.Inf(-1)}
|
|
|
|
|
for _, point := range segment {
|
|
|
|
|
tt := centerTimeTT(point.Time)
|
|
|
|
|
minimumTT = math.Min(minimumTT, tt)
|
|
|
|
|
maximumTT = math.Max(maximumTT, tt)
|
|
|
|
|
if point.MoonAltitude > best.MoonAltitude {
|
|
|
|
|
best = point
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if best.MoonAltitude > occultationStationHorizonResidualToleranceDeg {
|
|
|
|
|
candidates = append(candidates, best)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// A geocentric north/south seed can locate only one component after station
|
|
|
|
|
// parallax changes the topology. Every start/end phase junction is an exact
|
|
|
|
|
// endpoint of a visible contact-envelope component, so use those junctions
|
|
|
|
|
// to discover any remaining component without inventing a connector.
|
|
|
|
|
for _, curve := range curves {
|
|
|
|
|
if curve.Phase != RiseSetPhaseStart && curve.Phase != RiseSetPhaseEnd {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, segment := range curve.Segments {
|
|
|
|
|
for _, point := range segment {
|
|
|
|
|
tt := centerTimeTT(point.Time)
|
|
|
|
|
minimumTT = math.Min(minimumTT, tt)
|
|
|
|
|
maximumTT = math.Max(maximumTT, tt)
|
|
|
|
|
}
|
|
|
|
|
if len(segment) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, endpoint := range []OccultationPathPoint{segment[0], segment[len(segment)-1]} {
|
|
|
|
|
junction, ok := refineOccultationRiseSetPhaseJunction(endpoint, location, cache)
|
|
|
|
|
if ok && !occultationStationEnvelopePointCovered(
|
|
|
|
|
[][]OccultationPathPoint{candidates}, junction, 1,
|
|
|
|
|
) {
|
|
|
|
|
candidates = append(candidates, junction)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(candidates) == 0 || !finite(minimumTT) || !finite(maximumTT) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
// The geocentric seeds only locate the component. Station parallax can move a
|
|
|
|
|
// temporal fold just beyond their fixed-time range, so leave a bounded margin.
|
|
|
|
|
minimumTT -= 0.05
|
|
|
|
|
maximumTT += 0.05
|
|
|
|
|
result := make([][]OccultationPathPoint, 0, len(candidates))
|
|
|
|
|
for _, candidate := range candidates {
|
|
|
|
|
if occultationStationEnvelopePointCovered(result, candidate, occultationStationEnvelopeTargetSpacingKM) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
referenceTT := centerTimeTT(candidate.Time)
|
|
|
|
|
if candidate.MoonAltitude <= occultationStationHorizonResidualToleranceDeg {
|
|
|
|
|
for _, direction := range []int{-1, 1} {
|
|
|
|
|
trace := occultationStationTraceEnvelope(
|
|
|
|
|
candidate, direction, referenceTT, minimumTT, maximumTT, cache, model, location,
|
|
|
|
|
)
|
|
|
|
|
if !trace.boundary || len(trace.points) < 3 ||
|
|
|
|
|
occultationPathDistanceKM(trace.points[0], trace.points[len(trace.points)-1]) <= 1 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
trace.points = occultationStationDeduplicateEnvelopePoints(trace.points)
|
|
|
|
|
result = append(result, occultationStationSplitEnvelopeAtTimeFolds(trace.points)...)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
backward := occultationStationTraceEnvelope(
|
|
|
|
|
candidate, -1, referenceTT, minimumTT, maximumTT, cache, model, location,
|
|
|
|
|
)
|
|
|
|
|
if backward.closed {
|
|
|
|
|
closed := append([]OccultationPathPoint(nil), backward.points...)
|
|
|
|
|
if len(closed) >= 4 {
|
|
|
|
|
closed[len(closed)-1] = closed[0]
|
|
|
|
|
result = append(result, occultationStationSplitEnvelopeAtTimeFolds(closed)...)
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
forward := occultationStationTraceEnvelope(
|
|
|
|
|
candidate, 1, referenceTT, minimumTT, maximumTT, cache, model, location,
|
|
|
|
|
)
|
|
|
|
|
if !backward.boundary || !forward.boundary {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
segment := make([]OccultationPathPoint, 0, len(backward.points)+len(forward.points)-1)
|
|
|
|
|
for index := len(backward.points) - 1; index >= 0; index-- {
|
|
|
|
|
segment = append(segment, backward.points[index])
|
|
|
|
|
}
|
|
|
|
|
segment = append(segment, forward.points[1:]...)
|
|
|
|
|
segment = occultationStationDeduplicateEnvelopePoints(segment)
|
|
|
|
|
if len(segment) >= 3 {
|
|
|
|
|
result = append(result, occultationStationSplitEnvelopeAtTimeFolds(segment)...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// occultationStationVisibilityEnvelopeContours traces the boundary of the
|
|
|
|
|
// time-union of Moon-above-horizon states. It is the implicit curve H=0,
|
|
|
|
|
// dH/dt=0 restricted to sites where the requested contact metric is negative
|
|
|
|
|
// and H has a temporal maximum. Endpoints are the exact [F,H,dH/dt]=0
|
|
|
|
|
// direction junctions shared by start/end moonrise and moonset phase curves.
|
|
|
|
|
func occultationStationVisibilityEnvelopeContours(
|
|
|
|
|
curves []OccultationRiseSetCurve,
|
|
|
|
|
cache *occultationRiseSetEvaluationCache,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) [][]OccultationPathPoint {
|
|
|
|
|
if len(curves) == 0 || cache == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
minimumTT, maximumTT := math.Inf(1), math.Inf(-1)
|
|
|
|
|
seeds := make([]OccultationPathPoint, 0, 8)
|
|
|
|
|
for _, curve := range curves {
|
|
|
|
|
if curve.Phase != RiseSetPhaseStart && curve.Phase != RiseSetPhaseEnd {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, segment := range curve.Segments {
|
|
|
|
|
for _, point := range segment {
|
|
|
|
|
tt := centerTimeTT(point.Time)
|
|
|
|
|
minimumTT = math.Min(minimumTT, tt)
|
|
|
|
|
maximumTT = math.Max(maximumTT, tt)
|
|
|
|
|
}
|
|
|
|
|
if len(segment) == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, candidate := range []OccultationPathPoint{segment[0], segment[len(segment)-1]} {
|
|
|
|
|
junction, ok := refineOccultationRiseSetDirectionJunction(
|
|
|
|
|
centerTimeTT(candidate.Time), candidate.Longitude, candidate.Latitude,
|
|
|
|
|
false, location, cache,
|
|
|
|
|
)
|
|
|
|
|
if !ok {
|
|
|
|
|
junction, ok = refineOccultationRiseSetDirectionJunctionOnHorizon(candidate, candidate, false, location, cache)
|
|
|
|
|
}
|
|
|
|
|
if ok && !occultationStationEnvelopePointCovered(
|
|
|
|
|
[][]OccultationPathPoint{seeds}, junction, 1,
|
|
|
|
|
) {
|
|
|
|
|
seeds = append(seeds, junction)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(seeds) == 0 || !finite(minimumTT) || !finite(maximumTT) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
minimumTT -= 0.05
|
|
|
|
|
maximumTT += 0.05
|
|
|
|
|
model := occultationStationEnvelopeModel{kind: occultationStationVisibilityEnvelope}
|
|
|
|
|
result := make([][]OccultationPathPoint, 0, len(seeds)/2+1)
|
|
|
|
|
for _, seed := range seeds {
|
|
|
|
|
if !model.isRequiredExtremum(cache.evaluation(centerTimeTT(seed.Time)), seed.Longitude, seed.Latitude) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if occultationStationEnvelopePointCovered(result, seed, occultationStationEnvelopeTargetSpacingKM) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
referenceTT := centerTimeTT(seed.Time)
|
|
|
|
|
for _, direction := range []int{-1, 1} {
|
|
|
|
|
trace := occultationStationTraceEnvelope(
|
|
|
|
|
seed, direction, referenceTT, minimumTT, maximumTT, cache, model, location,
|
|
|
|
|
)
|
|
|
|
|
if trace.closed && len(trace.points) >= 4 {
|
|
|
|
|
closed := append([]OccultationPathPoint(nil), trace.points...)
|
|
|
|
|
closed[len(closed)-1] = closed[0]
|
|
|
|
|
result = append(result, occultationStationSplitEnvelopeAtTimeFolds(closed)...)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if !trace.boundary || len(trace.points) < 3 ||
|
|
|
|
|
occultationPathDistanceKM(trace.points[0], trace.points[len(trace.points)-1]) <= 1 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
trace.points = occultationStationDeduplicateEnvelopePoints(trace.points)
|
|
|
|
|
result = append(result, occultationStationSplitEnvelopeAtTimeFolds(trace.points)...)
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationTraceEnvelope(
|
|
|
|
|
seed OccultationPathPoint,
|
|
|
|
|
direction int,
|
|
|
|
|
referenceTT, minimumTT, maximumTT float64,
|
|
|
|
|
cache *occultationRiseSetEvaluationCache,
|
|
|
|
|
model occultationStationEnvelopeModel,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) occultationStationEnvelopeTrace {
|
|
|
|
|
state, ok := occultationStationEnvelopeArcStateAt(seed, referenceTT, cache, model)
|
|
|
|
|
if !ok {
|
|
|
|
|
return occultationStationEnvelopeTrace{points: []OccultationPathPoint{seed}}
|
|
|
|
|
}
|
|
|
|
|
for index := range state.tangent {
|
|
|
|
|
state.tangent[index] *= float64(direction)
|
|
|
|
|
}
|
|
|
|
|
result := occultationStationEnvelopeTrace{points: []OccultationPathPoint{state.point}}
|
|
|
|
|
step := occultationStationEnvelopeArcStepDegrees
|
|
|
|
|
travelKM := 0.0
|
|
|
|
|
for count := 0; count < occultationStationEnvelopeMaxArcSteps; count++ {
|
|
|
|
|
predictor := state.coordinates
|
|
|
|
|
for index := range predictor {
|
|
|
|
|
predictor[index] += step * state.tangent[index]
|
|
|
|
|
}
|
|
|
|
|
next, iterations, solved := occultationStationCorrectEnvelopeArc(
|
|
|
|
|
predictor, state.tangent, referenceTT, cache, model, state.point.WidthKM, location,
|
|
|
|
|
)
|
|
|
|
|
if !solved {
|
|
|
|
|
step /= 2
|
|
|
|
|
if step < occultationStationEnvelopeMinArcStepDegrees {
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if dotSolarEclipse3(next.tangent, state.tangent) < 0 {
|
|
|
|
|
for index := range next.tangent {
|
|
|
|
|
next.tangent[index] = -next.tangent[index]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
distanceKM := occultationPathDistanceKM(state.point, next.point)
|
|
|
|
|
if !finite(distanceKM) || distanceKM > occultationStationEnvelopeTargetSpacingKM {
|
|
|
|
|
step /= 2
|
|
|
|
|
if step < occultationStationEnvelopeMinArcStepDegrees {
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
nextTT := centerTimeTT(next.point.Time)
|
|
|
|
|
if nextTT < minimumTT || nextTT > maximumTT {
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
if model.activeMargin(cache.evaluation(nextTT), next.point.Longitude, next.point.Latitude) < 0 {
|
|
|
|
|
junction, junctionOK := occultationStationEnvelopeActivityJunction(
|
|
|
|
|
state.point, next.point, location, cache, model,
|
|
|
|
|
)
|
|
|
|
|
if !junctionOK {
|
|
|
|
|
step /= 2
|
|
|
|
|
if step < occultationStationEnvelopeMinArcStepDegrees {
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
result.points = append(result.points, junction)
|
|
|
|
|
result.boundary = true
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
travelKM += distanceKM
|
|
|
|
|
if travelKM > 4*occultationStationEnvelopeTargetSpacingKM &&
|
|
|
|
|
occultationPathDistanceKM(seed, next.point) <= occultationStationEnvelopeTargetSpacingKM/2 {
|
|
|
|
|
result.points = append(result.points, seed)
|
|
|
|
|
result.closed = true
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
result.points = append(result.points, next.point)
|
|
|
|
|
state = next
|
|
|
|
|
if model.activeMargin(cache.evaluation(nextTT), next.point.Longitude, next.point.Latitude) <=
|
|
|
|
|
occultationStationHorizonResidualToleranceDeg {
|
|
|
|
|
result.boundary = true
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
if distanceKM < occultationStationEnvelopeTargetSpacingKM/2 && iterations <= 4 {
|
|
|
|
|
step = math.Min(occultationStationEnvelopeArcStepDegrees, step*1.5)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationEnvelopeArcStateAt(
|
|
|
|
|
point OccultationPathPoint,
|
|
|
|
|
referenceTT float64,
|
|
|
|
|
cache *occultationRiseSetEvaluationCache,
|
|
|
|
|
model occultationStationEnvelopeModel,
|
|
|
|
|
) (occultationStationEnvelopeArcState, bool) {
|
|
|
|
|
coordinates := [3]float64{
|
|
|
|
|
point.Longitude,
|
|
|
|
|
point.Latitude,
|
|
|
|
|
(centerTimeTT(point.Time) - referenceTT) * occultationStationEnvelopeTimeScale,
|
|
|
|
|
}
|
|
|
|
|
_, jacobian, ok := occultationStationEnvelopeJacobian(coordinates, referenceTT, cache, model)
|
|
|
|
|
if !ok {
|
|
|
|
|
return occultationStationEnvelopeArcState{}, false
|
|
|
|
|
}
|
|
|
|
|
tangent, ok := occultationStationEnvelopeTangent(jacobian)
|
|
|
|
|
if !ok {
|
|
|
|
|
return occultationStationEnvelopeArcState{}, false
|
|
|
|
|
}
|
|
|
|
|
return occultationStationEnvelopeArcState{coordinates: coordinates, tangent: tangent, point: point}, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationCorrectEnvelopeArc(
|
|
|
|
|
predictor, tangent [3]float64,
|
|
|
|
|
referenceTT float64,
|
|
|
|
|
cache *occultationRiseSetEvaluationCache,
|
|
|
|
|
model occultationStationEnvelopeModel,
|
|
|
|
|
widthKM float64,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) (occultationStationEnvelopeArcState, int, bool) {
|
|
|
|
|
coordinates := predictor
|
|
|
|
|
iterationCache := cache.candidateOnly()
|
|
|
|
|
jacobianCache := iterationCache
|
|
|
|
|
for iteration := 0; iteration < 20; iteration++ {
|
|
|
|
|
residual, jacobian, ok := occultationStationEnvelopeJacobian(coordinates, referenceTT, jacobianCache, model)
|
|
|
|
|
if iterationCache == cache && ok {
|
|
|
|
|
tt := referenceTT + coordinates[2]/occultationStationEnvelopeTimeScale
|
|
|
|
|
residual, ok = model.residual(cache.evaluation(tt), normalizeLongitude(coordinates[0]), coordinates[1])
|
|
|
|
|
}
|
|
|
|
|
if !ok {
|
|
|
|
|
if jacobianCache != cache {
|
|
|
|
|
iterationCache = cache
|
|
|
|
|
jacobianCache = cache
|
|
|
|
|
coordinates = predictor
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
return occultationStationEnvelopeArcState{}, iteration, false
|
|
|
|
|
}
|
|
|
|
|
planeResidual := dotSolarEclipse3(subtractSolarEclipse3(coordinates, predictor), tangent)
|
|
|
|
|
// Interpolation predicts the Newton root; accepted vertices must pass
|
|
|
|
|
// the original exact residual and extremum checks. The candidate
|
|
|
|
|
// Jacobian only predicts corrections and the next tracing direction.
|
|
|
|
|
if iterationCache != cache && (iteration >= 5 ||
|
|
|
|
|
math.Abs(residual[0]) <= model.valueTolerance() && math.Abs(residual[1]) <= model.derivativeTolerance()) {
|
|
|
|
|
iterationCache = cache
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// Retain a converged exact root before finite-difference noise can
|
|
|
|
|
// move the derivative back outside the final acceptance tolerance.
|
|
|
|
|
if math.Abs(residual[0]) <= 1e-9 && math.Abs(residual[1]) <= model.derivativeTolerance() && math.Abs(planeResidual) <= 1e-9 {
|
|
|
|
|
return occultationStationValidEnvelopeArcState(
|
|
|
|
|
coordinates, jacobian, referenceTT, cache, model, widthKM, location, 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 occultationStationEnvelopeArcState{}, iteration, false
|
|
|
|
|
}
|
|
|
|
|
if norm := math.Sqrt(dotSolarEclipse3(delta, delta)); norm > 1 {
|
|
|
|
|
for index := range delta {
|
|
|
|
|
delta[index] /= norm
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for index := range coordinates {
|
|
|
|
|
coordinates[index] += delta[index]
|
|
|
|
|
}
|
|
|
|
|
coordinates[0] = predictor[0] + math.Remainder(coordinates[0]-predictor[0], 360)
|
|
|
|
|
if coordinates[1] <= -89.999999 || coordinates[1] >= 89.999999 {
|
|
|
|
|
return occultationStationEnvelopeArcState{}, iteration, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
residual, jacobian, ok := occultationStationEnvelopeJacobian(coordinates, referenceTT, cache, model)
|
|
|
|
|
planeResidual := dotSolarEclipse3(subtractSolarEclipse3(coordinates, predictor), tangent)
|
|
|
|
|
if !ok || math.Abs(residual[0]) > model.valueTolerance() ||
|
|
|
|
|
math.Abs(residual[1]) > model.derivativeTolerance() ||
|
|
|
|
|
math.Abs(planeResidual) > 1e-7 {
|
|
|
|
|
return occultationStationRefineEnvelopeAtFixedTime(
|
|
|
|
|
coordinates, predictor, tangent, referenceTT, cache, model, widthKM, location,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
return occultationStationValidEnvelopeArcState(
|
|
|
|
|
coordinates, jacobian, referenceTT, cache, model, widthKM, location, 20,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// At the Julian-day rounding floor, changing time can oscillate between two
|
|
|
|
|
// derivative values. A bounded spatial correction keeps the physical equations
|
|
|
|
|
// exact; only the auxiliary arclength plane may move by one minimum trace step.
|
|
|
|
|
func occultationStationRefineEnvelopeAtFixedTime(
|
|
|
|
|
coordinates, predictor, tangent [3]float64,
|
|
|
|
|
referenceTT float64,
|
|
|
|
|
cache *occultationRiseSetEvaluationCache,
|
|
|
|
|
model occultationStationEnvelopeModel,
|
|
|
|
|
widthKM float64,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) (occultationStationEnvelopeArcState, int, bool) {
|
|
|
|
|
origin := coordinates
|
|
|
|
|
for iteration := 0; iteration < 4; iteration++ {
|
|
|
|
|
residual, jacobian, ok := occultationStationEnvelopeJacobian(coordinates, referenceTT, cache, model)
|
|
|
|
|
if !ok || coordinates[1] <= -89.999999 || coordinates[1] >= 89.999999 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
planeResidual := dotSolarEclipse3(subtractSolarEclipse3(coordinates, predictor), tangent)
|
|
|
|
|
if math.Abs(planeResidual) > occultationStationEnvelopeMinArcStepDegrees {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if math.Abs(residual[0]) <= model.valueTolerance() && math.Abs(residual[1]) <= model.derivativeTolerance() {
|
|
|
|
|
return occultationStationValidEnvelopeArcState(
|
|
|
|
|
coordinates, jacobian, referenceTT, cache, model, widthKM, location, 20+iteration,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
determinant := jacobian[0][0]*jacobian[1][1] - jacobian[0][1]*jacobian[1][0]
|
|
|
|
|
if !finite(determinant) || math.Abs(determinant) < 1e-12 {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
longitudeStep := (-residual[0]*jacobian[1][1] + residual[1]*jacobian[0][1]) / determinant
|
|
|
|
|
latitudeStep := (-residual[1]*jacobian[0][0] + residual[0]*jacobian[1][0]) / determinant
|
|
|
|
|
coordinates[0] += longitudeStep
|
|
|
|
|
coordinates[1] += latitudeStep
|
|
|
|
|
if !finite(coordinates[0]) || !finite(coordinates[1]) ||
|
|
|
|
|
math.Hypot(coordinates[0]-origin[0], coordinates[1]-origin[1]) > occultationStationEnvelopeMinArcStepDegrees {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return occultationStationEnvelopeArcState{}, 24, false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationValidEnvelopeArcState(
|
|
|
|
|
coordinates [3]float64,
|
|
|
|
|
jacobian [2][3]float64,
|
|
|
|
|
referenceTT float64,
|
|
|
|
|
cache *occultationRiseSetEvaluationCache,
|
|
|
|
|
model occultationStationEnvelopeModel,
|
|
|
|
|
widthKM float64,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
iterations int,
|
|
|
|
|
) (occultationStationEnvelopeArcState, int, bool) {
|
|
|
|
|
tt := referenceTT + coordinates[2]/occultationStationEnvelopeTimeScale
|
|
|
|
|
longitude, latitude := normalizeLongitude(coordinates[0]), coordinates[1]
|
|
|
|
|
evaluation := cache.evaluation(tt)
|
|
|
|
|
state := evaluation.center.stateAt(longitude, latitude)
|
|
|
|
|
residual, ok := model.residual(evaluation, longitude, latitude)
|
|
|
|
|
if !ok || math.Abs(residual[0]) > model.valueTolerance() ||
|
|
|
|
|
math.Abs(residual[1]) > model.derivativeTolerance() ||
|
|
|
|
|
!model.isRequiredExtremum(evaluation, longitude, latitude) {
|
|
|
|
|
return occultationStationEnvelopeArcState{}, iterations, false
|
|
|
|
|
}
|
|
|
|
|
tangent, ok := occultationStationEnvelopeTangent(jacobian)
|
|
|
|
|
if !ok {
|
|
|
|
|
return occultationStationEnvelopeArcState{}, iterations, false
|
|
|
|
|
}
|
|
|
|
|
return occultationStationEnvelopeArcState{
|
|
|
|
|
coordinates: coordinates,
|
|
|
|
|
tangent: tangent,
|
|
|
|
|
point: OccultationPathPoint{
|
|
|
|
|
Time: occultationTTToLocation(tt, location), Longitude: longitude,
|
|
|
|
|
Latitude: latitude, MoonAltitude: state.moonAltitude, WidthKM: widthKM,
|
|
|
|
|
},
|
|
|
|
|
}, iterations, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationEnvelopeJacobian(
|
|
|
|
|
coordinates [3]float64,
|
|
|
|
|
referenceTT float64,
|
|
|
|
|
cache *occultationRiseSetEvaluationCache,
|
|
|
|
|
model occultationStationEnvelopeModel,
|
|
|
|
|
) ([2]float64, [2][3]float64, bool) {
|
|
|
|
|
tt := referenceTT + coordinates[2]/occultationStationEnvelopeTimeScale
|
|
|
|
|
longitude, latitude := normalizeLongitude(coordinates[0]), coordinates[1]
|
|
|
|
|
evaluation := cache.evaluation(tt)
|
2026-09-23 18:55:12 +08:00
|
|
|
// 点源目标(恒星)没有盘面半径项,接触度量可用解析速率;有限盘面行星保持中心差分。
|
|
|
|
|
residualAt := model.residual
|
|
|
|
|
if evaluation.center.targetRadiusKM <= 0 {
|
|
|
|
|
residualAt = model.residualWithRate
|
|
|
|
|
}
|
|
|
|
|
residual, ok := residualAt(evaluation, longitude, latitude)
|
2026-09-17 12:27:40 +08:00
|
|
|
if !ok {
|
|
|
|
|
return [2]float64{}, [2][3]float64{}, false
|
|
|
|
|
}
|
|
|
|
|
steps := [3]float64{1e-4, 1e-4, 5 * occultationStationEnvelopeTimeScale / 86400}
|
|
|
|
|
jacobian := [2][3]float64{}
|
|
|
|
|
for column := range steps {
|
|
|
|
|
shifted := coordinates
|
|
|
|
|
shifted[column] += steps[column]
|
|
|
|
|
shiftedTT := referenceTT + shifted[2]/occultationStationEnvelopeTimeScale
|
|
|
|
|
shiftedEvaluation := cache.evaluation(shiftedTT)
|
2026-09-23 18:55:12 +08:00
|
|
|
shiftedResidual, shiftedOK := residualAt(
|
2026-09-17 12:27:40 +08:00
|
|
|
shiftedEvaluation, normalizeLongitude(shifted[0]), shifted[1],
|
|
|
|
|
)
|
|
|
|
|
if !shiftedOK {
|
|
|
|
|
return [2]float64{}, [2][3]float64{}, false
|
|
|
|
|
}
|
|
|
|
|
for row := range residual {
|
|
|
|
|
jacobian[row][column] = (shiftedResidual[row] - residual[row]) / steps[column]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return residual, jacobian, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (model occultationStationEnvelopeModel) residual(
|
|
|
|
|
evaluation occultationRiseSetEvaluation,
|
|
|
|
|
longitude, latitude float64,
|
|
|
|
|
) ([2]float64, bool) {
|
|
|
|
|
state := evaluation.center.stateAt(longitude, latitude)
|
|
|
|
|
if model.kind == occultationStationVisibilityEnvelope {
|
|
|
|
|
derivative := evaluation.moonAltitudeDerivative(longitude, latitude)
|
|
|
|
|
return [2]float64{state.moonAltitude, derivative}, state.valid && finite(derivative)
|
|
|
|
|
}
|
|
|
|
|
derivative := evaluation.contactDerivative(longitude, latitude)
|
|
|
|
|
return [2]float64{state.contactMetric, derivative}, state.valid && finite(derivative)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-23 18:55:12 +08:00
|
|
|
// residualWithRate 与 residual 取值完全相同,但把时间导数换成解析速率:每列只需一次站心几何求值,
|
|
|
|
|
// 不再为前后时刻各求一次状态。接受判据仍走 residual 的精确中心差分。
|
|
|
|
|
func (model occultationStationEnvelopeModel) residualWithRate(
|
|
|
|
|
evaluation occultationRiseSetEvaluation,
|
|
|
|
|
longitude, latitude float64,
|
|
|
|
|
) ([2]float64, bool) {
|
|
|
|
|
state := evaluation.center.stateAt(longitude, latitude)
|
|
|
|
|
if !state.valid {
|
|
|
|
|
return [2]float64{}, false
|
|
|
|
|
}
|
|
|
|
|
if model.kind == occultationStationVisibilityEnvelope {
|
|
|
|
|
rate := evaluation.moonAltitudeRateAt(longitude, latitude)
|
|
|
|
|
if !finite(rate) {
|
|
|
|
|
return [2]float64{}, false
|
|
|
|
|
}
|
|
|
|
|
return [2]float64{state.moonAltitude, rate}, true
|
|
|
|
|
}
|
|
|
|
|
rate := evaluation.contactRateAt(longitude, latitude)
|
|
|
|
|
if !finite(rate) {
|
|
|
|
|
return [2]float64{}, false
|
|
|
|
|
}
|
|
|
|
|
return [2]float64{state.contactMetric, rate}, true
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 12:27:40 +08:00
|
|
|
func (model occultationStationEnvelopeModel) valueTolerance() float64 {
|
|
|
|
|
if model.kind == occultationStationVisibilityEnvelope {
|
|
|
|
|
return occultationStationHorizonResidualToleranceDeg
|
|
|
|
|
}
|
|
|
|
|
return 1e-5
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (model occultationStationEnvelopeModel) derivativeTolerance() float64 {
|
|
|
|
|
return occultationRiseSetJunctionDerivativeTolerance
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (model occultationStationEnvelopeModel) isRequiredExtremum(
|
|
|
|
|
evaluation occultationRiseSetEvaluation,
|
|
|
|
|
longitude, latitude float64,
|
|
|
|
|
) bool {
|
|
|
|
|
if model.kind == occultationStationVisibilityEnvelope {
|
|
|
|
|
return evaluation.moonAltitudeSecondDerivative(longitude, latitude) < 0
|
|
|
|
|
}
|
|
|
|
|
return evaluation.contactSecondDerivative(longitude, latitude) > 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (model occultationStationEnvelopeModel) activeMargin(
|
|
|
|
|
evaluation occultationRiseSetEvaluation,
|
|
|
|
|
longitude, latitude float64,
|
|
|
|
|
) float64 {
|
|
|
|
|
state := evaluation.center.stateAt(longitude, latitude)
|
|
|
|
|
if !state.valid {
|
|
|
|
|
return math.Inf(-1)
|
|
|
|
|
}
|
|
|
|
|
if model.kind == occultationStationVisibilityEnvelope {
|
|
|
|
|
return -state.contactMetric
|
|
|
|
|
}
|
|
|
|
|
return state.moonAltitude
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationEnvelopeTangent(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 occultationStationEnvelopeActivityJunction(
|
|
|
|
|
inside, outside OccultationPathPoint,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
cache *occultationRiseSetEvaluationCache,
|
|
|
|
|
model occultationStationEnvelopeModel,
|
|
|
|
|
) (OccultationPathPoint, bool) {
|
|
|
|
|
seed := occultationRiseSetMidpoint(inside, outside)
|
|
|
|
|
var junction OccultationPathPoint
|
|
|
|
|
var ok bool
|
|
|
|
|
if model.kind == occultationStationVisibilityEnvelope {
|
|
|
|
|
junction, ok = refineOccultationRiseSetDirectionJunction(
|
|
|
|
|
centerTimeTT(seed.Time), seed.Longitude, seed.Latitude, false, location, cache,
|
|
|
|
|
)
|
|
|
|
|
if !ok {
|
|
|
|
|
junction, ok = refineOccultationRiseSetDirectionJunctionOnHorizon(inside, outside, false, location, cache)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
junction, ok = refineOccultationRiseSetPhaseJunction(seed, location, cache)
|
|
|
|
|
if !ok {
|
|
|
|
|
junction, ok = refineOccultationRiseSetPhaseJunctionOnHorizon(seed, location, cache)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !ok {
|
|
|
|
|
return OccultationPathPoint{}, false
|
|
|
|
|
}
|
|
|
|
|
minimumTT := math.Min(centerTimeTT(inside.Time), centerTimeTT(outside.Time)) - 1.0/86400
|
|
|
|
|
maximumTT := math.Max(centerTimeTT(inside.Time), centerTimeTT(outside.Time)) + 1.0/86400
|
|
|
|
|
if tt := centerTimeTT(junction.Time); tt < minimumTT || tt > maximumTT {
|
|
|
|
|
return OccultationPathPoint{}, false
|
|
|
|
|
}
|
|
|
|
|
maximumDistance := math.Max(10, 3*occultationPathDistanceKM(inside, outside))
|
|
|
|
|
if occultationPathDistanceKM(junction, inside) > maximumDistance ||
|
|
|
|
|
occultationPathDistanceKM(junction, outside) > maximumDistance {
|
|
|
|
|
return OccultationPathPoint{}, false
|
|
|
|
|
}
|
|
|
|
|
return junction, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationEnvelopePointCovered(
|
|
|
|
|
segments [][]OccultationPathPoint,
|
|
|
|
|
point OccultationPathPoint,
|
|
|
|
|
toleranceKM float64,
|
|
|
|
|
) bool {
|
|
|
|
|
for _, segment := range segments {
|
|
|
|
|
for _, existing := range segment {
|
|
|
|
|
if occultationPathDistanceKM(existing, point) <= toleranceKM {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationDeduplicateEnvelopePoints(points []OccultationPathPoint) []OccultationPathPoint {
|
|
|
|
|
if len(points) < 2 {
|
|
|
|
|
return points
|
|
|
|
|
}
|
|
|
|
|
result := make([]OccultationPathPoint, 0, len(points))
|
|
|
|
|
for _, point := range points {
|
|
|
|
|
if len(result) == 0 || occultationPathDistanceKM(result[len(result)-1], point) > 0.001 {
|
|
|
|
|
result = append(result, point)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationSplitEnvelopeAtTimeFolds(
|
|
|
|
|
points []OccultationPathPoint,
|
|
|
|
|
) [][]OccultationPathPoint {
|
|
|
|
|
if len(points) < 2 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
// Station correction can produce the same civil timestamp twice at a
|
|
|
|
|
// temporal-envelope junction. Collapse coincident samples before splitting
|
|
|
|
|
// folds; retaining both would violate the public strictly-increasing-time
|
|
|
|
|
// contour contract while adding no geometry.
|
|
|
|
|
normalized := make([]OccultationPathPoint, 0, len(points))
|
|
|
|
|
for _, point := range points {
|
|
|
|
|
if len(normalized) > 0 && point.Time.Equal(normalized[len(normalized)-1].Time) &&
|
|
|
|
|
occultationPathDistanceKM(point, normalized[len(normalized)-1]) <= 0.01 {
|
|
|
|
|
normalized[len(normalized)-1] = point
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
normalized = append(normalized, point)
|
|
|
|
|
}
|
|
|
|
|
points = normalized
|
|
|
|
|
if len(points) < 2 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
segments := make([][]OccultationPathPoint, 0, 4)
|
|
|
|
|
start := 0
|
|
|
|
|
direction := 0
|
|
|
|
|
appendSegment := func(first, end, sign int) {
|
|
|
|
|
if end-first < 2 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
segment := append([]OccultationPathPoint(nil), points[first:end]...)
|
|
|
|
|
if sign < 0 {
|
|
|
|
|
for left, right := 0, len(segment)-1; left < right; left, right = left+1, right-1 {
|
|
|
|
|
segment[left], segment[right] = segment[right], segment[left]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
segments = append(segments, segment)
|
|
|
|
|
}
|
|
|
|
|
// Public contour segments must be strictly monotonic. A sub-millisecond
|
|
|
|
|
// reversal is still a real time fold when the traced points are spatially
|
|
|
|
|
// distinct, so split it without the coarser rise/set junction tolerance.
|
|
|
|
|
for index := 1; index < len(points); index++ {
|
|
|
|
|
sign := 0
|
|
|
|
|
if points[index].Time.After(points[index-1].Time) {
|
|
|
|
|
sign = 1
|
|
|
|
|
} else if points[index].Time.Before(points[index-1].Time) {
|
|
|
|
|
sign = -1
|
|
|
|
|
}
|
|
|
|
|
if sign == 0 {
|
|
|
|
|
// Distinct points at one timestamp are a genuine temporal fold. End
|
|
|
|
|
// the preceding monotone segment and restart at the second branch.
|
|
|
|
|
if direction != 0 {
|
|
|
|
|
appendSegment(start, index, direction)
|
|
|
|
|
}
|
|
|
|
|
start = index
|
|
|
|
|
direction = 0
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if direction == 0 {
|
|
|
|
|
direction = sign
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if sign == direction {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
appendSegment(start, index, direction)
|
|
|
|
|
start = index - 1
|
|
|
|
|
direction = sign
|
|
|
|
|
}
|
|
|
|
|
appendSegment(start, len(points), direction)
|
|
|
|
|
return segments
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// occultationStationCorrectHorizonContactPoint refines an open footprint
|
|
|
|
|
// endpoint onto the intersection of the fixed-time contact curve and the lunar
|
|
|
|
|
// horizon. Solving only the contact equation leaves one unconstrained surface
|
|
|
|
|
// direction and lets independently sampled endpoints drift along the contact
|
|
|
|
|
// curve, which turns their temporal outline into a scalloped static boundary.
|
|
|
|
|
func occultationStationCorrectHorizonContactPoint(
|
|
|
|
|
tt float64,
|
|
|
|
|
seed OccultationPathPoint,
|
|
|
|
|
contextAt occultationRiseSetContextFunc,
|
|
|
|
|
total bool,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) (occultationStationBoundarySample, bool) {
|
|
|
|
|
if contextAt == nil {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
context := contextAt(tt)
|
|
|
|
|
if total {
|
|
|
|
|
context = context.withInternalContact()
|
|
|
|
|
}
|
|
|
|
|
if !context.valid {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
longitude, latitude := seed.Longitude, seed.Latitude
|
|
|
|
|
seedState := context.stateAt(longitude, latitude)
|
|
|
|
|
if !seedState.valid {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
seedResidual := seedState.contactMetric
|
|
|
|
|
residualNorm := func(state occultationRiseSetState) float64 {
|
|
|
|
|
return math.Hypot(state.contactMetric, state.moonAltitude)
|
|
|
|
|
}
|
|
|
|
|
for iteration := 0; iteration < 20; iteration++ {
|
|
|
|
|
state := context.stateAt(longitude, latitude)
|
|
|
|
|
if !state.valid || !finite(state.contactMetric) || !finite(state.moonAltitude) {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
if math.Abs(state.contactMetric) <= occultationStationOracleResidualToleranceDeg &&
|
|
|
|
|
math.Abs(state.moonAltitude) <= occultationStationHorizonResidualToleranceDeg {
|
|
|
|
|
return occultationStationSampleFromCoordinates(
|
|
|
|
|
tt, seed, longitude, latitude, state.contactMetric, state.moonAltitude, seedResidual, location,
|
|
|
|
|
), true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const coordinateStepDeg = 0.002
|
|
|
|
|
plusLongitude := context.stateAt(normalizeLongitude(longitude+coordinateStepDeg), latitude)
|
|
|
|
|
minusLongitude := context.stateAt(normalizeLongitude(longitude-coordinateStepDeg), latitude)
|
|
|
|
|
plusLatitude := context.stateAt(longitude, math.Min(89.999999, latitude+coordinateStepDeg))
|
|
|
|
|
minusLatitude := context.stateAt(longitude, math.Max(-89.999999, latitude-coordinateStepDeg))
|
|
|
|
|
if !plusLongitude.valid || !minusLongitude.valid || !plusLatitude.valid || !minusLatitude.valid {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
cosLatitude := math.Max(0.05, math.Cos(latitude*rad))
|
|
|
|
|
contactX := (plusLongitude.contactMetric - minusLongitude.contactMetric) / (2 * coordinateStepDeg * cosLatitude)
|
|
|
|
|
contactY := (plusLatitude.contactMetric - minusLatitude.contactMetric) / (2 * coordinateStepDeg)
|
|
|
|
|
altitudeX := (plusLongitude.moonAltitude - minusLongitude.moonAltitude) / (2 * coordinateStepDeg * cosLatitude)
|
|
|
|
|
altitudeY := (plusLatitude.moonAltitude - minusLatitude.moonAltitude) / (2 * coordinateStepDeg)
|
|
|
|
|
determinant := contactX*altitudeY - contactY*altitudeX
|
|
|
|
|
if !finite(determinant) || math.Abs(determinant) <= 1e-12 {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
deltaX := (-state.contactMetric*altitudeY + contactY*state.moonAltitude) / determinant
|
|
|
|
|
deltaY := (-contactX*state.moonAltitude + altitudeX*state.contactMetric) / determinant
|
|
|
|
|
if !finite(deltaX) || !finite(deltaY) {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
if length := math.Hypot(deltaX, deltaY); length > 0.5 {
|
|
|
|
|
scale := 0.5 / length
|
|
|
|
|
deltaX *= scale
|
|
|
|
|
deltaY *= scale
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentNorm := residualNorm(state)
|
|
|
|
|
accepted := false
|
|
|
|
|
for damping := 1.0; damping >= 1.0/64; damping /= 2 {
|
|
|
|
|
candidateLongitude := normalizeLongitude(longitude + damping*deltaX/cosLatitude)
|
|
|
|
|
candidateLatitude := math.Max(-89.999999, math.Min(89.999999, latitude+damping*deltaY))
|
|
|
|
|
if occultationPathDistanceKMValues(
|
|
|
|
|
seed.Longitude, seed.Latitude, candidateLongitude, candidateLatitude,
|
|
|
|
|
) > occultationStationHorizonMaximumOffsetKM {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
candidate := context.stateAt(candidateLongitude, candidateLatitude)
|
|
|
|
|
if !candidate.valid || residualNorm(candidate) >= currentNorm {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
longitude, latitude = candidateLongitude, candidateLatitude
|
|
|
|
|
accepted = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if !accepted {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
state := context.stateAt(longitude, latitude)
|
|
|
|
|
if !state.valid || math.Abs(state.contactMetric) > occultationStationHorizonAcceptanceDeg ||
|
|
|
|
|
math.Abs(state.moonAltitude) > occultationStationHorizonAcceptanceDeg {
|
|
|
|
|
return occultationStationBoundarySample{}, false
|
|
|
|
|
}
|
|
|
|
|
return occultationStationSampleFromCoordinates(
|
|
|
|
|
tt, seed, longitude, latitude, state.contactMetric, state.moonAltitude, seedResidual, location,
|
|
|
|
|
), true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationSampleFromCoordinates(
|
|
|
|
|
tt float64,
|
|
|
|
|
seed OccultationPathPoint,
|
|
|
|
|
longitude, latitude, residual, altitude, seedResidual float64,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) occultationStationBoundarySample {
|
|
|
|
|
offset := occultationPathDistanceKMValues(seed.Longitude, seed.Latitude, longitude, latitude)
|
|
|
|
|
point := seed
|
|
|
|
|
point.Time = occultationTTToLocation(tt, location)
|
|
|
|
|
point.Longitude = longitude
|
|
|
|
|
point.Latitude = latitude
|
|
|
|
|
point.MoonAltitude = altitude
|
|
|
|
|
return occultationStationBoundarySample{
|
|
|
|
|
point: point,
|
|
|
|
|
contactResidualDeg: residual,
|
|
|
|
|
horizonResidualDeg: altitude,
|
|
|
|
|
offsetKM: offset,
|
|
|
|
|
seedResidualDeg: seedResidual,
|
|
|
|
|
valid: finite(residual) && finite(altitude),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// occultationStationCorrectFootprintEdges corrects only the two instantaneous
|
|
|
|
|
// contact arcs used by a direct ring and the endpoints that form their time
|
|
|
|
|
// tracks. Interior horizon samples remain untouched and retain their original
|
|
|
|
|
// footprint closure contract.
|
|
|
|
|
func occultationStationCorrectFootprintEdges(
|
|
|
|
|
footprints []PlanetOccultationFootprint,
|
|
|
|
|
frameAt occultationPathFrameFunc,
|
|
|
|
|
contextAt occultationRiseSetContextFunc,
|
|
|
|
|
total bool,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) []PlanetOccultationFootprint {
|
|
|
|
|
if len(footprints) == 0 || frameAt == nil || contextAt == nil {
|
|
|
|
|
return footprints
|
|
|
|
|
}
|
|
|
|
|
result := make([]PlanetOccultationFootprint, len(footprints))
|
|
|
|
|
for index, footprint := range footprints {
|
|
|
|
|
result[index] = footprint
|
|
|
|
|
result[index].Boundaries = make([][]OccultationPathPoint, len(footprint.Boundaries))
|
|
|
|
|
for boundaryIndex, boundary := range footprint.Boundaries {
|
|
|
|
|
result[index].Boundaries[boundaryIndex] = append([]OccultationPathPoint(nil), boundary...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for footprintIndex := range result {
|
|
|
|
|
for boundaryIndex := range result[footprintIndex].Boundaries {
|
|
|
|
|
boundary := result[footprintIndex].Boundaries[boundaryIndex]
|
|
|
|
|
for pointIndex := range boundary {
|
|
|
|
|
if pointIndex != 0 && pointIndex+1 != len(boundary) && footprintIndex != 0 && footprintIndex+1 != len(result) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seed := boundary[pointIndex]
|
|
|
|
|
correct := occultationStationCorrectContactPoint
|
|
|
|
|
if !result[footprintIndex].Closed && (pointIndex == 0 || pointIndex+1 == len(boundary)) {
|
|
|
|
|
correct = occultationStationCorrectHorizonContactPoint
|
|
|
|
|
}
|
|
|
|
|
sample, ok := correct(centerTimeTT(seed.Time), seed, contextAt, total, location)
|
|
|
|
|
if ok && sample.valid {
|
|
|
|
|
corrected := sample.point
|
|
|
|
|
// Station solving may round-trip TT through a civil-time
|
|
|
|
|
// conversion with sub-millisecond drift. Footprint contracts
|
|
|
|
|
// require every boundary point to retain its parent sample time.
|
|
|
|
|
corrected.Time = seed.Time
|
|
|
|
|
// 验收带内的负高度是求解器噪声,导出契约要求边界切点高度非负。
|
|
|
|
|
if corrected.MoonAltitude < 0 && corrected.MoonAltitude >= -occultationStationHorizonAcceptanceDeg {
|
|
|
|
|
corrected.MoonAltitude = 0
|
|
|
|
|
}
|
|
|
|
|
boundary[pointIndex] = corrected
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
result[footprintIndex].Boundaries[boundaryIndex] = boundary
|
|
|
|
|
}
|
|
|
|
|
result[footprintIndex].Polygons = occultationStationFootprintPolygons(
|
|
|
|
|
result[footprintIndex], frameAt, location,
|
|
|
|
|
)
|
|
|
|
|
// Horizon-arc points are recomputed from TT and can differ from the
|
|
|
|
|
// parent civil timestamp by a few microseconds after the station solve.
|
|
|
|
|
// Polygon samples are instantaneous geometry, so normalize their time
|
|
|
|
|
// metadata to the owning footprint before public validation/serialization.
|
|
|
|
|
for polygonIndex := range result[footprintIndex].Polygons {
|
|
|
|
|
for pointIndex := range result[footprintIndex].Polygons[polygonIndex] {
|
|
|
|
|
result[footprintIndex].Polygons[polygonIndex][pointIndex].Time = result[footprintIndex].Time
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationFootprintPolygons(
|
|
|
|
|
footprint PlanetOccultationFootprint,
|
|
|
|
|
frameAt occultationPathFrameFunc,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) [][]OccultationPathPoint {
|
|
|
|
|
if len(footprint.Boundaries) == 0 {
|
|
|
|
|
return footprint.Polygons
|
|
|
|
|
}
|
|
|
|
|
if footprint.Closed {
|
|
|
|
|
polygons := make([][]OccultationPathPoint, 0, len(footprint.Boundaries)+len(footprint.InteriorPolygons))
|
|
|
|
|
for _, boundary := range footprint.Boundaries {
|
|
|
|
|
if len(boundary) < 3 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
polygon := append([]OccultationPathPoint(nil), boundary...)
|
|
|
|
|
if occultationPathDistanceKM(polygon[0], polygon[len(polygon)-1]) > 0.001 {
|
|
|
|
|
polygon = append(polygon, polygon[0])
|
|
|
|
|
}
|
|
|
|
|
polygons = append(polygons, polygon)
|
|
|
|
|
}
|
|
|
|
|
for _, interior := range footprint.InteriorPolygons {
|
|
|
|
|
polygons = append(polygons, append([]OccultationPathPoint(nil), interior...))
|
|
|
|
|
}
|
|
|
|
|
if len(polygons) > 0 {
|
|
|
|
|
return polygons
|
|
|
|
|
}
|
|
|
|
|
return footprint.Polygons
|
|
|
|
|
}
|
|
|
|
|
tt := centerTimeTT(footprint.Time)
|
|
|
|
|
frame, ok := frameAt(tt)
|
|
|
|
|
if !ok {
|
|
|
|
|
return footprint.Polygons
|
|
|
|
|
}
|
|
|
|
|
polygons := make([][]OccultationPathPoint, 0, len(footprint.Boundaries)+len(footprint.InteriorPolygons))
|
|
|
|
|
for _, boundary := range footprint.Boundaries {
|
|
|
|
|
if len(boundary) < 2 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
polygon := append([]OccultationPathPoint(nil), boundary...)
|
|
|
|
|
polygon = append(polygon, planetOccultationHorizonArc(
|
|
|
|
|
tt, frame, boundary, location, planetOccultationBandHorizonPoints,
|
|
|
|
|
)...)
|
|
|
|
|
if len(polygon) >= 4 {
|
|
|
|
|
polygons = append(polygons, polygon)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, interior := range footprint.InteriorPolygons {
|
|
|
|
|
polygons = append(polygons, append([]OccultationPathPoint(nil), interior...))
|
|
|
|
|
}
|
|
|
|
|
if len(polygons) == 0 {
|
|
|
|
|
return footprint.Polygons
|
|
|
|
|
}
|
|
|
|
|
return polygons
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// occultationStationOracleProbeOffsets 返回由初始步长倍增得到的探测偏移序列,
|
|
|
|
|
// 末项恰好等于搜索半径上限;等比步长本身不会落在上限上。
|
|
|
|
|
func occultationStationOracleProbeOffsets() []float64 {
|
|
|
|
|
offsets := make([]float64, 0, 8)
|
|
|
|
|
for step := occultationStationOracleInitialStepKM; step < occultationStationOracleMaximumOffsetKM; step *= 2 {
|
|
|
|
|
offsets = append(offsets, step)
|
|
|
|
|
}
|
|
|
|
|
return append(offsets, occultationStationOracleMaximumOffsetKM)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// occultationStationOracleResidualBound 返回某括号宽度下仍可接受的接触残差上限:
|
|
|
|
|
// 容差的 10 倍与斜率上限乘括号宽度取较大者,避免浮点噪声否定已收敛的解。
|
|
|
|
|
func occultationStationOracleResidualBound(bracketWidthKM float64) float64 {
|
|
|
|
|
return math.Max(
|
|
|
|
|
10*occultationStationOracleResidualToleranceDeg,
|
|
|
|
|
occultationStationOracleResidualSlopeDegPerKM*math.Abs(bracketWidthKM),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationFindBracket(
|
|
|
|
|
seedResidual float64,
|
|
|
|
|
evaluate func(float64) (float64, float64, float64, bool),
|
|
|
|
|
) (float64, float64, bool) {
|
|
|
|
|
if !finite(seedResidual) {
|
|
|
|
|
return 0, 0, false
|
|
|
|
|
}
|
|
|
|
|
var brackets [][2]float64
|
|
|
|
|
for _, sign := range []float64{1, -1} {
|
|
|
|
|
previousOffset := 0.0
|
|
|
|
|
previousResidual := seedResidual
|
|
|
|
|
for _, step := range occultationStationOracleProbeOffsets() {
|
|
|
|
|
currentOffset := sign * step
|
|
|
|
|
currentResidual, _, _, ok := evaluate(currentOffset)
|
|
|
|
|
if !ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if previousResidual*currentResidual <= 0 {
|
|
|
|
|
brackets = append(brackets, [2]float64{previousOffset, currentOffset})
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
previousOffset, previousResidual = currentOffset, currentResidual
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(brackets) == 0 {
|
|
|
|
|
return 0, 0, false
|
|
|
|
|
}
|
|
|
|
|
best := brackets[0]
|
|
|
|
|
for _, candidate := range brackets[1:] {
|
|
|
|
|
bestDistance := math.Abs(best[0] + best[1])
|
|
|
|
|
candidateDistance := math.Abs(candidate[0] + candidate[1])
|
|
|
|
|
if candidateDistance < bestDistance {
|
|
|
|
|
best = candidate
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return best[0], best[1], true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationOracleSampleAt(
|
|
|
|
|
tt float64,
|
|
|
|
|
fixed occultationPathVector,
|
|
|
|
|
offsetKM, seedResidual, residual, altitude float64,
|
|
|
|
|
frame occultationPathFrame,
|
|
|
|
|
location *time.Location,
|
|
|
|
|
) occultationStationBoundarySample {
|
|
|
|
|
rotation := occultationPathEarthRotationAt(tt)
|
|
|
|
|
inertial := occultationStationInverseEarthRotation(fixed, rotation)
|
2026-09-23 18:55:12 +08:00
|
|
|
longitude, latitude := occultationPathGeodeticWithSidereal(inertial, ApparentSiderealTime(TT2UT1(tt))*15)
|
2026-09-17 12:27:40 +08:00
|
|
|
return occultationStationBoundarySample{
|
|
|
|
|
point: OccultationPathPoint{
|
|
|
|
|
Time: occultationTTToLocation(tt, location),
|
|
|
|
|
Longitude: longitude,
|
|
|
|
|
Latitude: latitude,
|
|
|
|
|
MoonAltitude: altitude,
|
|
|
|
|
},
|
|
|
|
|
contactResidualDeg: residual,
|
|
|
|
|
horizonResidualDeg: altitude,
|
|
|
|
|
offsetKM: offsetKM,
|
|
|
|
|
seedResidualDeg: seedResidual,
|
|
|
|
|
valid: finite(residual) && finite(altitude) && occultationPathNorm(frame.moon) > 0,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationCrossTrackAt(
|
|
|
|
|
tt float64,
|
|
|
|
|
frame occultationPathFrame,
|
|
|
|
|
frameAt occultationPathFrameFunc,
|
|
|
|
|
seedFixed occultationPathVector,
|
|
|
|
|
) (occultationPathVector, bool) {
|
|
|
|
|
before, beforeOK := frameAt(tt - occultationPathVelocityStepDays)
|
|
|
|
|
after, afterOK := frameAt(tt + occultationPathVelocityStepDays)
|
|
|
|
|
if !beforeOK || !afterOK {
|
|
|
|
|
return occultationPathVector{}, false
|
|
|
|
|
}
|
|
|
|
|
vx := after.moonProjectionX() - before.moonProjectionX()
|
|
|
|
|
vy := after.moonProjectionY() - before.moonProjectionY()
|
|
|
|
|
if math.Hypot(vx, vy) <= 1e-12 {
|
|
|
|
|
return occultationPathVector{}, false
|
|
|
|
|
}
|
|
|
|
|
planeCross := occultationPathUnit(occultationPathAdd(
|
|
|
|
|
occultationPathScale(frame.first, -vy/math.Hypot(vx, vy)),
|
|
|
|
|
occultationPathScale(frame.second, vx/math.Hypot(vx, vy)),
|
|
|
|
|
))
|
|
|
|
|
trackReference, trackOK := occultationPathTrackReference(frame)
|
|
|
|
|
if !trackOK {
|
|
|
|
|
return occultationPathVector{}, false
|
|
|
|
|
}
|
|
|
|
|
trackFixed := occultationPathSub(
|
|
|
|
|
occultationPathEarthFixedVector(tt+occultationPathVelocityStepDays, trackReference),
|
|
|
|
|
occultationPathEarthFixedVector(tt-occultationPathVelocityStepDays, trackReference),
|
|
|
|
|
)
|
|
|
|
|
polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio
|
|
|
|
|
normal := occultationPathUnit(occultationPathVector{
|
|
|
|
|
x: seedFixed.x,
|
|
|
|
|
y: seedFixed.y,
|
|
|
|
|
z: seedFixed.z / polarRatioSquared,
|
|
|
|
|
})
|
|
|
|
|
trackFixed = occultationPathSub(trackFixed, occultationPathScale(normal, occultationPathDot(trackFixed, normal)))
|
|
|
|
|
if occultationPathNorm(trackFixed) <= 1e-12 {
|
|
|
|
|
trackFixed = occultationPathEarthFixedVectorWithRotation(planeCross, occultationPathEarthRotationAt(tt))
|
|
|
|
|
trackFixed = occultationPathSub(trackFixed, occultationPathScale(normal, occultationPathDot(trackFixed, normal)))
|
|
|
|
|
}
|
|
|
|
|
trackFixed = occultationPathUnit(trackFixed)
|
|
|
|
|
cross := occultationPathUnit(occultationPathCross(normal, trackFixed))
|
|
|
|
|
if occultationPathNorm(cross) <= 1e-12 {
|
|
|
|
|
return occultationPathVector{}, false
|
|
|
|
|
}
|
|
|
|
|
return cross, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationSurfaceVector(longitude, latitude float64) occultationPathVector {
|
|
|
|
|
latitudeRad := latitude * rad
|
|
|
|
|
longitudeRad := longitude * rad
|
|
|
|
|
polarRatio := occultationPathEarthPolarRatio
|
|
|
|
|
u := math.Atan(polarRatio * math.Tan(latitudeRad))
|
|
|
|
|
sinU, cosU := math.Sincos(u)
|
|
|
|
|
sinLongitude, cosLongitude := math.Sincos(longitudeRad)
|
|
|
|
|
return occultationPathScale(occultationPathVector{
|
|
|
|
|
x: cosU * cosLongitude,
|
|
|
|
|
y: cosU * sinLongitude,
|
|
|
|
|
z: polarRatio * sinU,
|
|
|
|
|
}, occultationPathEarthEquatorialRadiusKM)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationOffsetSurfaceVector(
|
|
|
|
|
seedFixed, tangent occultationPathVector, offsetKM float64,
|
|
|
|
|
) occultationPathVector {
|
|
|
|
|
point := occultationPathAdd(seedFixed, occultationPathScale(tangent, offsetKM))
|
|
|
|
|
polarRatioSquared := occultationPathEarthPolarRatio * occultationPathEarthPolarRatio
|
|
|
|
|
metric := math.Sqrt(point.x*point.x + point.y*point.y + point.z*point.z/polarRatioSquared)
|
|
|
|
|
if metric <= 0 || !finite(metric) {
|
|
|
|
|
return seedFixed
|
|
|
|
|
}
|
|
|
|
|
return occultationPathScale(point, occultationPathEarthEquatorialRadiusKM/metric)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationGeodetic(fixed occultationPathVector) (float64, float64) {
|
|
|
|
|
return normalizeLongitude(math.Atan2(fixed.y, fixed.x) / rad), occultationPathGeodeticLatitude(fixed)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func occultationStationInverseEarthRotation(
|
|
|
|
|
fixed occultationPathVector,
|
|
|
|
|
rotation occultationPathEarthRotation,
|
|
|
|
|
) occultationPathVector {
|
|
|
|
|
return occultationPathVector{
|
|
|
|
|
x: rotation.cosine*fixed.x - rotation.sine*fixed.y,
|
|
|
|
|
y: rotation.sine*fixed.x + rotation.cosine*fixed.y,
|
|
|
|
|
z: fixed.z,
|
|
|
|
|
}
|
|
|
|
|
}
|