feat: 完善日月食与月掩几何链路并扩展历法接口

- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑
- 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口
- 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界
- 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名
- 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验
- 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口
- 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
2026-09-17 12:27:40 +08:00
parent 9ee2163cc7
commit 2bf8478639
428 changed files with 85981 additions and 7998 deletions
+56
View File
@@ -0,0 +1,56 @@
package basic
import (
"math"
"testing"
)
// 记忆表必须逐位精确:命中返回值与重新展开 77 项级数完全一致,且相邻 1 ULP 的输入互不干扰。
// The memo must be bit-exact: a hit returns exactly what the 77-term series would recompute, and
// inputs one ULP apart keep independent entries.
func TestNutationMemoIsBitExact(t *testing.T) {
values := []float64{
2451545.0, 2460310.5, 0, -1000000.5, -4713.5, 5373484.5,
math.NaN(), math.Inf(1), math.Inf(-1), math.SmallestNonzeroFloat64,
}
for index := 0; index < 5000; index++ {
values = append(values, 1000000.0+float64(index)*0.37)
}
for _, jd := range values {
memoPsi, memoEps := Nutation2000B(jd)
directPsi, directEps := nutation2000BCompute(jd)
if math.Float64bits(memoPsi) != math.Float64bits(directPsi) ||
math.Float64bits(memoEps) != math.Float64bits(directEps) {
t.Fatalf("jd=%v: memo (%v,%v) != direct (%v,%v)", jd, memoPsi, memoEps, directPsi, directEps)
}
againPsi, againEps := Nutation2000B(jd)
if math.Float64bits(againPsi) != math.Float64bits(memoPsi) ||
math.Float64bits(againEps) != math.Float64bits(memoEps) {
t.Fatalf("jd=%v: repeated call changed the value", jd)
}
// 相邻 1 ULP 的输入必须各自独立(不能被同一槽位合并)。
neighbour := math.Nextafter(jd, math.Inf(1))
if math.IsNaN(jd) || math.IsInf(jd, 0) {
continue
}
nearPsi, _ := Nutation2000B(neighbour)
nearDirect, _ := nutation2000BCompute(neighbour)
if math.Float64bits(nearPsi) != math.Float64bits(nearDirect) {
t.Fatalf("jd=%v neighbour: memo (%v) != direct (%v)", jd, nearPsi, nearDirect)
}
}
}
// 章动只依赖传入的 jd:ΔT 覆盖不改变它,因此记忆表不需要世代戳(与视恒星时不同)。
func TestNutationMemoIgnoresDeltaTGeneration(t *testing.T) {
original := GetDeltaTFn()
defer SetDeltaTFn(original)
jd := 2460310.5
beforePsi, beforeEps := Nutation2000B(jd)
SetDeltaTFn(func(date float64, isJd bool) float64 { return 6000 })
afterPsi, afterEps := Nutation2000B(jd)
if math.Float64bits(beforePsi) != math.Float64bits(afterPsi) ||
math.Float64bits(beforeEps) != math.Float64bits(afterEps) {
t.Fatalf("nutation changed with the ΔT generation: (%v,%v) vs (%v,%v)", beforePsi, beforeEps, afterPsi, afterEps)
}
}