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

94 lines
3.8 KiB
Go

package basic
import (
"reflect"
"testing"
"time"
)
// 折点补根的判据只能统计查询窗口内部的未成对端点:窗口边界上的裁剪端点没有任何折点
// 可以配对,把它们计入会让“未成对端点数严格下降”永远不成立。
func TestOccultationRiseSetFoldRecoveryCountsInteriorEndpoints(t *testing.T) {
zone := time.FixedZone("UTC+8", 8*3600)
start := time.Date(2025, time.July, 29, 0, 0, 0, 0, zone)
options := OccultationPathOptions{
Step: 20 * time.Minute, TargetSpacingKM: 900, RiseSetStep: time.Minute,
DisableFootprints: true,
}
paths, err := FindPlanetOccultationPaths(start, start.Add(24*time.Hour), OccultationMars, options)
if err != nil || len(paths) != 1 || !paths[0].HasTotalBand {
t.Fatalf("paths=%d err=%v, want one total-band event", len(paths), err)
}
path := paths[0]
config, _ := planetOccultationConfigFor(OccultationMars)
cache := newPlanetOccultationEventCache(config)
greatestTT := occultationTimeToTT(path.Greatest.Time)
cache.preparePathEphemeris(greatestTT, OccultationPathAlgorithmOptimized)
firstTT := occultationTimeToTT(path.TotalStart.Time)
lastTT := occultationTimeToTT(path.TotalEnd.Time)
curves, base, recovered := occultationRiseSetCurvesWithRecoveryReport(
firstTT, lastTT, greatestTT, options, start.Location(), cache.totalRiseSetCache,
)
if recovered == nil {
t.Fatal("fold recovery was never attempted on a graph with interior unpaired endpoints")
}
baseInterior := occultationRiseSetUnclosedEndpointCount(base, true, firstTT, lastTT)
recoveredInterior := occultationRiseSetUnclosedEndpointCount(recovered, true, firstTT, lastTT)
if baseInterior == 0 || recoveredInterior >= baseInterior {
t.Fatalf("interior unpaired endpoints base=%d recovered=%d, want a strict decrease",
baseInterior, recoveredInterior)
}
// 全局计数被窗口裁剪端点抬平:按它判定会丢弃这次重建。
baseGlobal := occultationRiseSetUnclosedEndpointCount(base, false, firstTT, lastTT)
recoveredGlobal := occultationRiseSetUnclosedEndpointCount(recovered, false, firstTT, lastTT)
if recoveredGlobal < baseGlobal {
t.Fatalf("global unpaired endpoints base=%d recovered=%d, sample no longer shows a window-truncated endpoint",
baseGlobal, recoveredGlobal)
}
if !reflect.DeepEqual(curves, recovered) {
t.Fatal("interior-endpoint test did not select the rebuilt phase graph")
}
}
func TestOccultationRiseSetWindowClippedEndpointIgnoredByInteriorCount(t *testing.T) {
firstTT := 2460000.0
lastTT := firstTT + 0.25
point := func(tt float64) OccultationPathPoint {
return OccultationPathPoint{Time: occultationTTToLocation(tt, time.UTC)}
}
curves := []OccultationRiseSetCurve{
{
Phase: RiseSetPhaseStart, Direction: RiseSetDirectionRise,
Segments: [][]OccultationPathPoint{
{point(lastTT), point(lastTT + 0.01)},
{point(firstTT + 0.02), point(firstTT + 0.03)},
},
},
}
if got := occultationRiseSetUnclosedEndpointCount(curves, false, firstTT, lastTT); got != 4 {
t.Fatalf("global unpaired endpoints=%d, want 4", got)
}
if got := occultationRiseSetUnclosedEndpointCount(curves, true, firstTT, lastTT); got != 2 {
t.Fatalf("interior unpaired endpoints=%d, want 2", got)
}
}
// stateAt 的分配预算:同一上下文内换站点只复用已分配的站点表,不再额外分配。
func BenchmarkOccultationRiseSetStateAt(b *testing.B) {
context := newOccultationRiseSetContext(
occultationTimeToTT(time.Date(2025, time.July, 29, 12, 0, 0, 0, time.UTC)),
100, 10, 384400, 200, -15, 149597870.7*2, 3389.5,
)
coordinates := make([][2]float64, 0, 64)
for index := 0; index < 64; index++ {
coordinates = append(coordinates, [2]float64{float64(index) * 1.7, float64(index%30) - 15})
}
b.ReportAllocs()
b.ResetTimer()
for index := 0; index < b.N; index++ {
for _, coordinate := range coordinates {
_ = context.stateAt(coordinate[0], coordinate[1])
}
}
}