2bf8478639
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
88 lines
3.4 KiB
Go
88 lines
3.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|