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
+66
View File
@@ -94,3 +94,69 @@ func assertGalileanContactMatchesBasic(
t.Fatalf("%s phase mismatch: got %q want %q", name, got.Phase, want.Phase)
}
}
// IMCCE 年表每个事件给 4 个时刻:D1、D2 = D1+时长、F1、F2 = F1+时长。
func TestGalileanPhenomenonContactEventMatchesIMCCEBaseline(t *testing.T) {
records := loadGalileanPublicEventBaseline(t)
const (
contactTolerance = 120 * time.Second
durationTolerance = 25 * time.Second
)
var maxContact, maxDuration time.Duration
for _, record := range records {
startUTC := mustParseGalileanEventTime(t, record.StartUTC)
endUTC := mustParseGalileanEventTime(t, record.EndUTC)
startDuration := time.Duration(record.StartDurationMinutes * float64(time.Minute))
endDuration := time.Duration(record.EndDurationMinutes * float64(time.Minute))
endOfEvent := endUTC.Add(endDuration)
midpoint := startUTC.Add(endOfEvent.Sub(startUTC) / 2)
got := ClosestGalileanPhenomenonContactEvent(midpoint, record.Satellite, GalileanPhenomenonType(record.Type))
if !got.Valid {
t.Fatalf("%s returned no contact event", record.Label)
}
contacts := []struct {
name string
got time.Time
want time.Time
}{
{"D1", got.Disappearance.Start, startUTC},
{"D2", got.Disappearance.End, startUTC.Add(startDuration)},
{"F1", got.Reappearance.Start, endUTC},
{"F2", got.Reappearance.End, endOfEvent},
}
for _, contact := range contacts {
delta := contact.got.Sub(contact.want)
if delta < 0 {
delta = -delta
}
if delta > maxContact {
maxContact = delta
}
if delta > contactTolerance {
t.Fatalf("%s %s offset %v exceeds %v", record.Label, contact.name, contact.got.Sub(contact.want), contactTolerance)
}
}
durations := []struct {
name string
got time.Duration
want time.Duration
}{
{"D", got.Disappearance.Duration, startDuration},
{"F", got.Reappearance.Duration, endDuration},
}
for _, duration := range durations {
delta := duration.got - duration.want
if delta < 0 {
delta = -delta
}
if delta > maxDuration {
maxDuration = delta
}
if delta > durationTolerance {
t.Fatalf("%s %s-phase duration offset %v exceeds %v", record.Label, duration.name, duration.got-duration.want, durationTolerance)
}
}
}
t.Logf("contact offset max %v, duration offset max %v", maxContact, maxDuration)
}