feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func occultationIsochroneTestStar() StarCoordinate {
|
||||
return StarCoordinate{
|
||||
ID: "Regulus",
|
||||
RA: 152.09292,
|
||||
Dec: 11.96719,
|
||||
Epoch: time.Date(2000, time.January, 1, 12, 0, 0, 0, time.UTC),
|
||||
Frame: CoordinateFrameJ2000,
|
||||
}
|
||||
}
|
||||
|
||||
// 等时线必须与站心掩甚定义自洽:支路上任意点由站心事件搜索独立求出的掩甚时刻等于该支路电平。
|
||||
func TestOccultationGreatestTimeContoursMatchLocalGreatest(t *testing.T) {
|
||||
star := occultationIsochroneTestStar()
|
||||
start := time.Date(2025, time.August, 22, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2025, time.August, 24, 0, 0, 0, 0, time.UTC)
|
||||
options := OccultationPathOptions{
|
||||
Step: 2 * time.Minute, DisableFootprints: true, GreatestTimeStep: 15 * time.Minute,
|
||||
}
|
||||
paths, err := FindStarOccultationPaths(start, end, star, options)
|
||||
if err != nil || len(paths) == 0 {
|
||||
t.Fatalf("no path with isochrones: %v", err)
|
||||
}
|
||||
contours := paths[0].GreatestTimeContours
|
||||
if len(contours) < 4 {
|
||||
t.Fatalf("expected a full isochrone fan, got %d contours", len(contours))
|
||||
}
|
||||
for _, contour := range contours {
|
||||
if contour.Time.Second() != 0 || contour.Time.Nanosecond() != 0 || contour.Time.Minute()%15 != 0 {
|
||||
t.Fatalf("contour time %s is not aligned to the requested step",
|
||||
contour.Time.Format("15:04:05.000"))
|
||||
}
|
||||
}
|
||||
|
||||
verified := 0
|
||||
worst := 0.0
|
||||
for _, contour := range contours {
|
||||
branchPoints := 0
|
||||
for _, segment := range contour.Segments {
|
||||
if len(segment) < 2 {
|
||||
t.Fatalf("contour %.6f has a degenerate branch", contour.JDE)
|
||||
}
|
||||
branchPoints += len(segment)
|
||||
}
|
||||
// 弧长步长上限决定了单条等时线的点数上界;点数爆表意味着延拓没有真正前进。
|
||||
if branchPoints > 4000 {
|
||||
t.Fatalf("contour %.6f has %d points, continuation is not advancing", contour.JDE, branchPoints)
|
||||
}
|
||||
level := occultationTTToLocation(contour.JDE, time.UTC)
|
||||
for _, segment := range contour.Segments {
|
||||
for index := 0; index < len(segment) && verified < 80; index += 1 + len(segment)/8 {
|
||||
point := segment[index]
|
||||
infos, searchErr := FindStarOccultations(
|
||||
level.Add(-2*time.Hour), level.Add(2*time.Hour), star,
|
||||
point.Longitude, point.Latitude, 0, OccultationSearchOptions{},
|
||||
)
|
||||
if searchErr != nil || len(infos) == 0 {
|
||||
continue
|
||||
}
|
||||
delta := math.Abs(infos[0].Greatest.Sub(point.Time).Seconds())
|
||||
if delta > worst {
|
||||
worst = delta
|
||||
}
|
||||
verified++
|
||||
}
|
||||
}
|
||||
}
|
||||
if verified < 10 {
|
||||
t.Fatalf("verified only %d points", verified)
|
||||
}
|
||||
t.Logf("%d contours, %d points verified, worst %.2f s", len(contours), verified, worst)
|
||||
if worst > 1 {
|
||||
t.Fatalf("isochrone disagrees with local greatest occultation by %.2f s", worst)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOccultationGreatestTimeContoursDisabledByDefault(t *testing.T) {
|
||||
star := occultationIsochroneTestStar()
|
||||
start := time.Date(2025, time.August, 22, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2025, time.August, 24, 0, 0, 0, 0, time.UTC)
|
||||
paths, err := FindStarOccultationPaths(start, end, star, OccultationPathOptions{
|
||||
Step: 2 * time.Minute, DisableFootprints: true,
|
||||
})
|
||||
if err != nil || len(paths) == 0 {
|
||||
t.Fatalf("no occultation path: %v", err)
|
||||
}
|
||||
if len(paths[0].GreatestTimeContours) != 0 {
|
||||
t.Fatalf("contours computed without a request: %d", len(paths[0].GreatestTimeContours))
|
||||
}
|
||||
}
|
||||
|
||||
// 行星是有限盘面,等时线按外接触锥门控,必须与站心行星掩甚一致。
|
||||
func TestPlanetOccultationGreatestTimeContoursMatchLocalGreatest(t *testing.T) {
|
||||
start := time.Date(2025, time.February, 1, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2025, time.February, 2, 0, 0, 0, 0, time.UTC)
|
||||
options := OccultationPathOptions{
|
||||
Step: 2 * time.Minute, DisableFootprints: true, GreatestTimeStep: 30 * time.Minute,
|
||||
}
|
||||
paths, err := FindPlanetOccultationPaths(start, end, OccultationSaturn, options)
|
||||
if err != nil || len(paths) == 0 {
|
||||
t.Fatalf("no planetary path with isochrones: %v", err)
|
||||
}
|
||||
contours := paths[0].GreatestTimeContours
|
||||
if len(contours) == 0 {
|
||||
t.Fatal("no greatest-time contours")
|
||||
}
|
||||
verified := 0
|
||||
worst := 0.0
|
||||
for _, contour := range contours {
|
||||
level := occultationTTToLocation(contour.JDE, time.UTC)
|
||||
for _, segment := range contour.Segments {
|
||||
if len(segment) < 2 {
|
||||
t.Fatalf("contour %.6f has a degenerate branch", contour.JDE)
|
||||
}
|
||||
for index := 0; index < len(segment) && verified < 60; index += 1 + len(segment)/8 {
|
||||
point := segment[index]
|
||||
infos, searchErr := FindPlanetOccultations(
|
||||
level.Add(-3*time.Hour), level.Add(3*time.Hour), OccultationSaturn,
|
||||
point.Longitude, point.Latitude, 0, OccultationSearchOptions{},
|
||||
)
|
||||
if searchErr != nil || len(infos) == 0 {
|
||||
continue
|
||||
}
|
||||
delta := math.Abs(infos[0].Greatest.Sub(point.Time).Seconds())
|
||||
if delta > worst {
|
||||
worst = delta
|
||||
}
|
||||
verified++
|
||||
}
|
||||
}
|
||||
}
|
||||
if verified < 10 {
|
||||
t.Fatalf("verified only %d points", verified)
|
||||
}
|
||||
t.Logf("%d contours, %d points verified, worst %.2f s", len(contours), verified, worst)
|
||||
if worst > 1 {
|
||||
t.Fatalf("isochrone disagrees with local greatest occultation by %.2f s", worst)
|
||||
}
|
||||
}
|
||||
|
||||
// 同一时刻取值的等时线必须是单条连通曲线:曾因"覆盖判据用点到顶点距离"而被重复延拓
|
||||
// (行星掩星每个取值 3 条重叠支路)。
|
||||
func TestOccultationGreatestTimeContoursHaveSingleBranchPerLevel(t *testing.T) {
|
||||
star := occultationIsochroneTestStar()
|
||||
starPaths, err := FindStarOccultationPaths(
|
||||
time.Date(2025, time.August, 22, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2025, time.August, 24, 0, 0, 0, 0, time.UTC), star,
|
||||
OccultationPathOptions{Step: 2 * time.Minute, DisableFootprints: true, GreatestTimeStep: 15 * time.Minute},
|
||||
)
|
||||
if err != nil || len(starPaths) == 0 {
|
||||
t.Fatalf("star: %v", err)
|
||||
}
|
||||
planetPaths, err := FindPlanetOccultationPaths(
|
||||
time.Date(2025, time.February, 1, 0, 0, 0, 0, time.UTC),
|
||||
time.Date(2025, time.February, 2, 0, 0, 0, 0, time.UTC), OccultationSaturn,
|
||||
OccultationPathOptions{Step: 2 * time.Minute, DisableFootprints: true, GreatestTimeStep: 30 * time.Minute},
|
||||
)
|
||||
if err != nil || len(planetPaths) == 0 {
|
||||
t.Fatalf("planet: %v", err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
contours []OccultationGreatestTimeContour
|
||||
}{
|
||||
{"star", starPaths[0].GreatestTimeContours},
|
||||
{"planet", planetPaths[0].GreatestTimeContours},
|
||||
} {
|
||||
if len(tc.contours) == 0 {
|
||||
t.Fatalf("%s: no contours", tc.name)
|
||||
}
|
||||
for _, contour := range tc.contours {
|
||||
if len(contour.Segments) != 1 {
|
||||
t.Fatalf("%s contour %s has %d branches, want a single connected curve",
|
||||
tc.name, contour.Time.Format("15:04"), len(contour.Segments))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 显式时刻取值:按 TT 儒略日给出,回显时刻取整到毫秒,窗口外取值被丢弃,且同样受上限保护。
|
||||
func TestOccultationGreatestTimeContoursAcceptExplicitValues(t *testing.T) {
|
||||
star := occultationIsochroneTestStar()
|
||||
start := time.Date(2025, time.August, 22, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2025, time.August, 24, 0, 0, 0, 0, time.UTC)
|
||||
base := OccultationPathOptions{Step: 2 * time.Minute, DisableFootprints: true, GreatestTimeStep: 15 * time.Minute}
|
||||
stepPaths, err := FindStarOccultationPaths(start, end, star, base)
|
||||
if err != nil || len(stepPaths) == 0 || len(stepPaths[0].GreatestTimeContours) == 0 {
|
||||
t.Fatalf("step request failed: %v", err)
|
||||
}
|
||||
stepContours := stepPaths[0].GreatestTimeContours
|
||||
// 用步长路径得到的取值当显式取值,外加 8 个窗口外取值,检验回显与过滤。
|
||||
values := make([]float64, 0, len(stepContours)+8)
|
||||
for _, contour := range stepContours {
|
||||
values = append(values, contour.JDE)
|
||||
}
|
||||
for index := 0; index < 8; index++ {
|
||||
values = append(values, occultationTimeToTT(start.Add(-time.Duration(index+1)*time.Hour)))
|
||||
}
|
||||
explicitPaths, err := FindStarOccultationPaths(start, end, star, OccultationPathOptions{
|
||||
Step: 2 * time.Minute, DisableFootprints: true, GreatestTimeValues: values,
|
||||
})
|
||||
if err != nil || len(explicitPaths) == 0 {
|
||||
t.Fatalf("explicit request failed: %v", err)
|
||||
}
|
||||
contours := explicitPaths[0].GreatestTimeContours
|
||||
if len(contours) != len(stepContours) {
|
||||
t.Fatalf("explicit values produced %d contours, step request produced %d", len(contours), len(stepContours))
|
||||
}
|
||||
for index, contour := range contours {
|
||||
if contour.JDE != stepContours[index].JDE {
|
||||
t.Fatalf("contour %d JDE %.9f does not match the requested value %.9f", index, contour.JDE, stepContours[index].JDE)
|
||||
}
|
||||
if contour.Time.Nanosecond() != 0 {
|
||||
t.Fatalf("contour time %s is not rounded to the millisecond", contour.Time.Format("15:04:05.000000000"))
|
||||
}
|
||||
}
|
||||
|
||||
// 上限:72 个互不相同、都落在窗口内的取值,返回条数不得超过共享上限。
|
||||
dense := make([]float64, 0, greatestTimeContourMaxLevels+8)
|
||||
for index := 0; index < greatestTimeContourMaxLevels+8; index++ {
|
||||
dense = append(dense, stepContours[0].JDE+float64(index)*1e-6)
|
||||
}
|
||||
cappedPaths, err := FindStarOccultationPaths(start, end, star, OccultationPathOptions{
|
||||
Step: 2 * time.Minute, DisableFootprints: true, GreatestTimeValues: dense,
|
||||
})
|
||||
if err != nil || len(cappedPaths) == 0 {
|
||||
t.Fatalf("capped request failed: %v", err)
|
||||
}
|
||||
if count := len(cappedPaths[0].GreatestTimeContours); count > greatestTimeContourMaxLevels {
|
||||
t.Fatalf("level cap not applied: %d contours", count)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOccultationGreatestTimeContours(b *testing.B) {
|
||||
star := occultationIsochroneTestStar()
|
||||
start := time.Date(2025, time.August, 22, 0, 0, 0, 0, time.UTC)
|
||||
end := time.Date(2025, time.August, 24, 0, 0, 0, 0, time.UTC)
|
||||
options := OccultationPathOptions{
|
||||
Step: 2 * time.Minute, DisableFootprints: true, GreatestTimeStep: 15 * time.Minute,
|
||||
}
|
||||
for index := 0; index < b.N; index++ {
|
||||
FindStarOccultationPaths(start, end, star, options)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user