Files
astro/basic/occultation_optimization_test.go
T

88 lines
3.4 KiB
Go
Raw Normal View History

package basic
import (
"math"
"testing"
"time"
)
func TestOccultationStationCacheRecyclingPreservesContactModel(t *testing.T) {
context := newOccultationRiseSetContext(2451545, 10, 5, 384000, 10.5, 5.25, 1e9, 60000)
uncached := context
uncached.states = nil
for pass := 0; pass < 2; pass++ {
for index := 0; index < 4*occultationRiseSetStateCacheMaximumEntries; index++ {
lon, lat := float64(index*7-110), float64(index-40)
for _, total := range []bool{false, true} {
actual, reference := context, uncached
if total {
actual, reference = actual.withInternalContact(), reference.withInternalContact()
}
if got, want := actual.stateAt(lon, lat), reference.stateAt(lon, lat); got != want {
t.Fatalf("cached station differs: got=%+v want=%+v", got, want)
}
}
if len(context.states) > occultationRiseSetStateCacheMaximumEntries {
t.Fatal("station cache exceeded bound")
}
}
}
}
func TestOccultationEnvelopePredictionRequiresExactCorrection(t *testing.T) {
start := time.Date(2025, 1, 4, 0, 0, 0, 0, time.UTC)
events, err := FindBestPlanetOccultations(start, start.Add(48*time.Hour), OccultationSaturn, OccultationSearchOptions{})
if err != nil || len(events) != 1 {
t.Fatalf("events=%d err=%v", len(events), err)
}
tt := occultationTimeToTT(events[0].Greatest)
config, _ := planetOccultationConfigFor(OccultationSaturn)
event := newPlanetOccultationEventCache(config)
event.prepareLocalEphemeris(tt)
north, _, width, ok := occultationPathLimitsAndWidthForFrame(tt, event.outerFrameAt)
if !ok {
t.Fatal("missing limit seed")
}
seed := occultationPathPointFromVector(tt, north, width, time.UTC)
sample, ok := occultationStationCorrectEnvelopePoint(tt, seed, event.riseSetContextAt, false, time.UTC)
if !ok {
t.Fatal("missing exact envelope seed")
}
model := occultationStationEnvelopeModel{kind: occultationStationContactEnvelope}
exact := newOccultationRiseSetEvaluationCache(event.riseSetContextAt)
state, ok := occultationStationEnvelopeArcStateAt(sample.point, tt, exact, model)
if !ok {
t.Fatal("missing envelope tangent")
}
predictor := state.coordinates
for index := range predictor {
predictor[index] += 0.2 * state.tangent[index]
}
want, _, ok := occultationStationCorrectEnvelopeArc(predictor, state.tangent, tt, exact, model, width, time.UTC)
if !ok {
t.Fatal("exact correction failed")
}
for _, invalid := range []bool{false, true} {
candidateCalls := 0
cache := newOccultationRiseSetEvaluationCacheWithCandidate(event.riseSetContextAt, func(at float64) occultationRiseSetContext {
candidateCalls++
if invalid {
return occultationRiseSetContext{}
}
return event.candidateRiseSetContextAt(at)
})
got, _, ok := occultationStationCorrectEnvelopeArc(predictor, state.tangent, tt, cache, model, width, time.UTC)
if !ok || candidateCalls == 0 {
t.Fatalf("candidate correction ok=%v calls=%d invalid=%v", ok, candidateCalls, invalid)
}
if distance := occultationPathDistanceKM(want.point, got.point); distance > 0.005 {
t.Fatalf("candidate displaced exact root by %v km", distance)
}
evaluation := exact.evaluation(centerTimeTT(got.point.Time))
residual, ok := model.residual(evaluation, got.point.Longitude, got.point.Latitude)
if !ok || math.Abs(residual[0]) > model.valueTolerance() || math.Abs(residual[1]) > model.derivativeTolerance() {
t.Fatalf("accepted inexact root %v", residual)
}
}
}