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