2bf8478639
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
185 lines
5.4 KiB
Go
185 lines
5.4 KiB
Go
package basic
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
func TestMovingDiskEventEngineSampleTimesKeepAnchorsAndBudget(t *testing.T) {
|
|
engine := movingDiskEventEngine{maxSampleCount: 7, reserveAnchorSlot: true, uniformOverflow: true}
|
|
times, step := engine.sampleTimes(0, 1, 0.37, 0.01)
|
|
if len(times) != 7 {
|
|
t.Fatalf("sample count = %d, want 7", len(times))
|
|
}
|
|
if times[0] != 0 || times[len(times)-1] != 1 {
|
|
t.Fatalf("sample endpoints = %.6f, %.6f, want 0 and 1", times[0], times[len(times)-1])
|
|
}
|
|
foundGreatest := false
|
|
for index, value := range times {
|
|
if index > 0 && value <= times[index-1] {
|
|
t.Fatalf("sample times are not strictly increasing: %v", times)
|
|
}
|
|
if math.Abs(value-0.37) <= 1e-12 {
|
|
foundGreatest = true
|
|
}
|
|
}
|
|
if !foundGreatest {
|
|
t.Fatalf("sample times omitted greatest: %v", times)
|
|
}
|
|
if step <= 0 {
|
|
t.Fatalf("effective step = %v, want positive", step)
|
|
}
|
|
}
|
|
|
|
func TestMovingDiskEventEngineWindowUsesCandidateAndExactPredicates(t *testing.T) {
|
|
engine := movingDiskEventEngine{
|
|
searchSpanDays: 1,
|
|
rangeStepDays: 0.1,
|
|
rootToleranceDays: 1e-9,
|
|
}
|
|
candidateCalls, exactCalls := 0, 0
|
|
candidate := func(tt float64) bool {
|
|
candidateCalls++
|
|
return tt >= -0.35 && tt <= 0.42
|
|
}
|
|
exact := func(tt float64) bool {
|
|
exactCalls++
|
|
return tt >= -0.3 && tt <= 0.4
|
|
}
|
|
start, end, ok := engine.window(0, -1, 1, candidate, exact)
|
|
if !ok {
|
|
t.Fatal("window returned not found")
|
|
}
|
|
if math.Abs(start+0.3) > 2e-8 || math.Abs(end-0.4) > 2e-8 {
|
|
t.Fatalf("window = %.12f..%.12f, want -0.3..0.4", start, end)
|
|
}
|
|
if candidateCalls == 0 || exactCalls == 0 {
|
|
t.Fatalf("candidate/exact calls = %d/%d, want both", candidateCalls, exactCalls)
|
|
}
|
|
}
|
|
|
|
func TestMovingDiskEventEngineGreatestFindsMinimumImpact(t *testing.T) {
|
|
engine := movingDiskEventEngine{greatestSpanDays: 0.75}
|
|
got := engine.greatest(0.1, -1, 1, func(tt float64) (float64, bool) {
|
|
return (tt - 0.23) * (tt - 0.23), true
|
|
}, 56)
|
|
if math.Abs(got-0.23) > 1e-8 {
|
|
t.Fatalf("greatest = %.12f, want 0.23", got)
|
|
}
|
|
}
|
|
|
|
func TestMovingDiskContactStateUsesSolarOuterAndInnerRadii(t *testing.T) {
|
|
state := movingDiskContactState{
|
|
separation: 1.0,
|
|
occultingOuterRadius: 0.6,
|
|
occultingInnerRadius: 0.4,
|
|
targetRadius: 0.5,
|
|
valid: true,
|
|
}
|
|
if got := state.externalContactGap(); math.Abs(got+0.1) > 1e-12 {
|
|
t.Fatalf("external gap = %.12f, want -0.1", got)
|
|
}
|
|
if got := state.internalContactGap(); math.Abs(got-0.9) > 1e-12 {
|
|
t.Fatalf("internal gap = %.12f, want 0.9", got)
|
|
}
|
|
if !movingDiskContactStateValid(1, 0.6, 0.4, 0.5) {
|
|
t.Fatal("finite disk state was rejected")
|
|
}
|
|
if movingDiskContactStateValid(math.NaN(), 0.6, 0.4, 0.5) {
|
|
t.Fatal("NaN disk state was accepted")
|
|
}
|
|
}
|
|
|
|
func TestMovingDiskEventEngineContactRootWalksBothDirections(t *testing.T) {
|
|
engine := movingDiskEventEngine{}
|
|
metric := func(tt float64) (float64, bool) {
|
|
return (tt + 0.3) * (tt - 0.4), true
|
|
}
|
|
for _, test := range []struct {
|
|
name string
|
|
direction float64
|
|
want float64
|
|
}{
|
|
{name: "forward", direction: 1, want: 0.4},
|
|
{name: "backward", direction: -1, want: -0.3},
|
|
} {
|
|
got, ok := engine.contactRoot(0, test.direction, 0.05, 1, 1e-9, metric, 64)
|
|
if !ok || math.Abs(got-test.want) > 2e-8 {
|
|
t.Errorf("%s contact root = %.12f, ok=%v, want %.3f", test.name, got, ok, test.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMovingDiskContactEvaluatorCachesValidAndInvalidStates(t *testing.T) {
|
|
calls := 0
|
|
evaluator := newMovingDiskContactEvaluator(func(tt float64) (movingDiskContactState, bool) {
|
|
calls++
|
|
if tt < 0 {
|
|
return movingDiskContactState{}, false
|
|
}
|
|
state := movingDiskContactState{
|
|
separation: 1,
|
|
occultingOuterRadius: 0.6,
|
|
occultingInnerRadius: 0.6,
|
|
targetRadius: 0.2,
|
|
valid: true,
|
|
}
|
|
return state, true
|
|
})
|
|
if _, ok := evaluator.gap(0.25, false); !ok {
|
|
t.Fatal("first valid evaluation failed")
|
|
}
|
|
if _, ok := evaluator.gap(0.25, true); !ok {
|
|
t.Fatal("cached valid evaluation failed")
|
|
}
|
|
if _, ok := evaluator.gap(-0.25, false); ok {
|
|
t.Fatal("invalid evaluation was accepted")
|
|
}
|
|
if _, ok := evaluator.gap(-0.25, true); ok {
|
|
t.Fatal("cached invalid evaluation was accepted")
|
|
}
|
|
if calls != 2 {
|
|
t.Fatalf("evaluation calls = %d, want 2 after valid/invalid cache hits", calls)
|
|
}
|
|
}
|
|
|
|
func TestMovingDiskContactEvaluatorPrimeAvoidsCallback(t *testing.T) {
|
|
calls := 0
|
|
evaluator := newMovingDiskContactEvaluator(func(tt float64) (movingDiskContactState, bool) {
|
|
calls++
|
|
return movingDiskContactState{}, false
|
|
})
|
|
state := movingDiskContactState{
|
|
separation: 1,
|
|
occultingOuterRadius: 0.6,
|
|
occultingInnerRadius: 0.6,
|
|
targetRadius: 0.2,
|
|
valid: true,
|
|
}
|
|
evaluator.prime(0.5, state, true)
|
|
value, ok := evaluator.gap(0.5, false)
|
|
if !ok || math.Abs(value-0.2) > 1e-12 {
|
|
t.Fatalf("primed external gap = %.12f, ok=%v", value, ok)
|
|
}
|
|
if calls != 0 {
|
|
t.Fatalf("callback calls = %d, want 0 for primed TT", calls)
|
|
}
|
|
}
|
|
|
|
func TestMovingDiskContactCacheClearsAtBoundedCapacity(t *testing.T) {
|
|
cache := &movingDiskContactCache{maxEntries: 2}
|
|
state := movingDiskContactState{valid: true}
|
|
cache.store(1, state, true)
|
|
cache.store(2, state, true)
|
|
cache.store(3, state, true)
|
|
if len(cache.entries) != 1 {
|
|
t.Fatalf("cache size = %d, want 1 after bounded clear", len(cache.entries))
|
|
}
|
|
if _, _, hit := cache.lookup(1); hit {
|
|
t.Fatal("old entry survived bounded cache clear")
|
|
}
|
|
if _, _, hit := cache.lookup(3); !hit {
|
|
t.Fatal("new entry missing after bounded cache clear")
|
|
}
|
|
}
|