astro/basic/solar_eclipse_path_test.go
starainrt 3ffdbe0034
feat: 扩展天文计算能力
- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息
- 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算
- 新增木星伽利略卫星位置、现象与接触事件计算
- 新增恒星星表、星座判定、自行修正与观测辅助能力
- 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包
- 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算
- 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试
- 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试
- 更新中文和英文 README,补充示例、精度说明、SVG 配图
2026-05-01 22:38:44 +08:00

196 lines
7.0 KiB
Go

package basic
import (
"math"
"testing"
)
func TestSolarEclipseCentralPathMatchesGlobalGreatest(t *testing.T) {
seedJDE := JDECalc(2024, 4, 8)
global := SolarEclipse(seedJDE)
path := SolarEclipseCentralPath(seedJDE, SolarEclipsePathOptions{StepDays: 5.0 / 1440.0})
if path.Eclipse.Type != global.Type {
t.Fatalf("type mismatch: got %s want %s", path.Eclipse.Type, global.Type)
}
if !path.Eclipse.HasCentral {
t.Fatalf("expected central eclipse path")
}
if len(path.CenterLine) == 0 {
t.Fatalf("expected center line points")
}
if len(path.NorthernLimit) == 0 || len(path.SouthernLimit) == 0 {
t.Fatalf("expected central path limits: north=%d south=%d", len(path.NorthernLimit), len(path.SouthernLimit))
}
assertSolarEclipseJDEClose(t, "Greatest.JDE", path.Greatest.JDE, global.GreatestEclipse, 1e-8)
assertSolarEclipseFloatClose(t, "Greatest.Longitude", path.Greatest.Longitude, global.GreatestLongitude, 1e-9)
assertSolarEclipseFloatClose(t, "Greatest.Latitude", path.Greatest.Latitude, global.GreatestLatitude, 1e-9)
assertSolarEclipseFloatClose(t, "Greatest.WidthKM", path.Greatest.WidthKM, global.PathWidthKM, 1e-9)
foundGreatest := false
for _, point := range path.CenterLine {
if math.Abs(point.JDE-global.GreatestEclipse) <= solarEclipsePathDuplicateTimeDays {
foundGreatest = true
break
}
}
if !foundGreatest {
t.Fatalf("center line should include greatest eclipse JDE %.12f", global.GreatestEclipse)
}
}
func TestSolarEclipseCentralPathTargetSpacingRefinesSamples(t *testing.T) {
seedJDE := JDECalc(2024, 4, 8)
coarse := SolarEclipseCentralPath(seedJDE, SolarEclipsePathOptions{StepDays: 20.0 / 1440.0})
refined := SolarEclipseCentralPath(seedJDE, SolarEclipsePathOptions{
StepDays: 20.0 / 1440.0,
TargetSpacingKM: 120,
})
if len(coarse.CenterLine) == 0 || len(refined.CenterLine) == 0 {
t.Fatalf("expected path points: coarse=%d refined=%d", len(coarse.CenterLine), len(refined.CenterLine))
}
if len(refined.CenterLine) <= len(coarse.CenterLine) {
t.Fatalf("target spacing should refine samples: coarse=%d refined=%d", len(coarse.CenterLine), len(refined.CenterLine))
}
for i := 1; i < len(refined.CenterLine); i++ {
distanceKM := solarEclipsePathDistanceKM(refined.CenterLine[i-1], refined.CenterLine[i])
if distanceKM > 120.1 {
t.Fatalf("segment %d too long: got %.6f km want <= 120.1 km", i, distanceKM)
}
}
}
func TestSolarEclipseCentralPathPartialHasNoCenterLine(t *testing.T) {
path := SolarEclipseCentralPath(JDECalc(2025, 3, 29), SolarEclipsePathOptions{})
if path.Eclipse.Type != SolarEclipsePartial {
t.Fatalf("unexpected eclipse type: got %s want %s", path.Eclipse.Type, SolarEclipsePartial)
}
if path.Eclipse.HasCentral {
t.Fatalf("partial eclipse should not have central path")
}
if len(path.CenterLine) != 0 || len(path.NorthernLimit) != 0 || len(path.SouthernLimit) != 0 {
t.Fatalf(
"partial eclipse should not return central path points: center=%d north=%d south=%d",
len(path.CenterLine),
len(path.NorthernLimit),
len(path.SouthernLimit),
)
}
}
func TestSolarEclipsePartialFootprintsIncludeGreatest(t *testing.T) {
seedJDE := JDECalc(2024, 4, 8)
global := SolarEclipse(seedJDE)
footprints := SolarEclipsePartialFootprints(seedJDE, SolarEclipsePartialFootprintOptions{
StepDays: 30.0 / 1440.0,
BoundaryPoints: 72,
})
if footprints.Eclipse.Type != SolarEclipseTotal {
t.Fatalf("unexpected eclipse type: got %s want %s", footprints.Eclipse.Type, SolarEclipseTotal)
}
if footprints.BoundaryPoints != 72 {
t.Fatalf("boundary points mismatch: got %d want 72", footprints.BoundaryPoints)
}
if len(footprints.Footprints) == 0 {
t.Fatalf("expected partial footprints")
}
foundGreatest := false
for _, footprint := range footprints.Footprints {
if math.Abs(footprint.JDE-global.GreatestEclipse) <= solarEclipsePathDuplicateTimeDays {
foundGreatest = true
}
if len(footprint.Boundaries) == 0 {
t.Fatalf("footprint at %.12f has no boundaries", footprint.JDE)
}
for _, boundary := range footprint.Boundaries {
if len(boundary) == 0 {
t.Fatalf("footprint at %.12f has an empty boundary segment", footprint.JDE)
}
for _, point := range boundary {
if math.Abs(point.JDE-footprint.JDE) > 1e-12 {
t.Fatalf("point JDE mismatch: got %.12f want %.12f", point.JDE, footprint.JDE)
}
if point.Longitude < -180 || point.Longitude > 180 {
t.Fatalf("longitude out of range: %.9f", point.Longitude)
}
if point.Latitude < -90 || point.Latitude > 90 {
t.Fatalf("latitude out of range: %.9f", point.Latitude)
}
}
}
assertSolarEclipseFootprintClosedFlag(t, footprint)
}
if !foundGreatest {
t.Fatalf("partial footprints should include greatest eclipse JDE %.12f", global.GreatestEclipse)
}
}
func TestSolarEclipsePartialFootprintsWorkForPartialOnlyEclipse(t *testing.T) {
footprints := SolarEclipsePartialFootprints(JDECalc(2025, 3, 29), SolarEclipsePartialFootprintOptions{
StepDays: 30.0 / 1440.0,
BoundaryPoints: 72,
})
if footprints.Eclipse.Type != SolarEclipsePartial {
t.Fatalf("unexpected eclipse type: got %s want %s", footprints.Eclipse.Type, SolarEclipsePartial)
}
if footprints.Eclipse.HasCentral {
t.Fatalf("partial-only eclipse should not have central path")
}
if len(footprints.Footprints) == 0 {
t.Fatalf("expected partial footprints for partial-only eclipse")
}
}
func TestSolarEclipsePartialFootprintsNoEvent(t *testing.T) {
footprints := SolarEclipsePartialFootprints(JDECalc(2023, 5, 15), SolarEclipsePartialFootprintOptions{})
if footprints.Eclipse.Type != SolarEclipseNone {
t.Fatalf("unexpected eclipse type: got %s want %s", footprints.Eclipse.Type, SolarEclipseNone)
}
if len(footprints.Footprints) != 0 {
t.Fatalf("no eclipse should not return footprints: got %d", len(footprints.Footprints))
}
}
func TestSolarEclipsePartialAreaCompatibilityWrapper(t *testing.T) {
seedJDE := JDECalc(2024, 4, 8)
options := SolarEclipsePartialAreaOptions{
StepDays: 30.0 / 1440.0,
BoundaryPoints: 72,
}
compat := SolarEclipsePartialArea(seedJDE, options)
primary := SolarEclipsePartialFootprints(seedJDE, options)
if compat.Eclipse.Type != primary.Eclipse.Type {
t.Fatalf("compat type mismatch: got %s want %s", compat.Eclipse.Type, primary.Eclipse.Type)
}
if len(compat.Footprints) != len(primary.Footprints) {
t.Fatalf("compat footprint count mismatch: got %d want %d", len(compat.Footprints), len(primary.Footprints))
}
}
func assertSolarEclipseFootprintClosedFlag(t *testing.T, footprint SolarEclipsePartialFootprint) {
t.Helper()
if !footprint.Closed {
return
}
if len(footprint.Boundaries) != 1 {
t.Fatalf("closed footprint should have one boundary: got %d", len(footprint.Boundaries))
}
boundary := footprint.Boundaries[0]
if len(boundary) < 2 {
t.Fatalf("closed footprint boundary too short: got %d", len(boundary))
}
first := boundary[0]
last := boundary[len(boundary)-1]
if first.Longitude != last.Longitude || first.Latitude != last.Latitude {
t.Fatalf("closed footprint boundary should repeat first point at end")
}
}