feat: 完善日月食与月掩几何链路并扩展历法接口
- 新增日月食中心带、偏食带、阴影足迹、等时线、食分线及升落边界计算,支持极区与混合食拓扑 - 新增日食单时刻阴影求解器、站心状态查询、批量采样和 ΔT 覆盖接口 - 重构恒星与行星月掩路径,补充有限盘面接触、站心修正、掩带宽度、极区投影及升落边界 - 扩展 SVG 与 GeoJSON 输出,支持详细面板、全球/极区/地球投影、边界闭合、时间标记和拓扑签名 - 扩展日月食候选搜索、局地搜索、沙罗序列预计算与范围外推,补充系列锚点和成员一致性校验 - 补齐古历纪年、儒略历独有闰日、多公历候选、历法改革跨日及精确日期运算接口 - 优化 ΔT、章动、恒星时、月球地平线、事件根搜索和本地星历缓存,降低重复计算开销并提升边界稳定
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
package basic
|
||||
|
||||
import "testing"
|
||||
|
||||
// TestSolarEclipseSarosPathSeriesRemainFiniteWithinBudget exercises actual
|
||||
// footprint and central-band generation at several points spanning roughly
|
||||
// five centuries. The P0 state test catches bad ephemeris values; this test
|
||||
// also walks the sampled path output and therefore covers the topology input
|
||||
// used by GeoJSON and SVG consumers.
|
||||
func TestSolarEclipseSarosPathSeriesRemainFiniteWithinBudget(t *testing.T) {
|
||||
const sarosDays = 6585.321314
|
||||
seed := JDECalc(2024, 4, 8)
|
||||
for _, familyIndex := range []int{-28, -21, -14, -7, 0, 7, 14, 21, 28} {
|
||||
familyIndex := familyIndex
|
||||
t.Run("saros-"+formatSignedRegressionIndex(familyIndex), func(t *testing.T) {
|
||||
result := SolarEclipsePartialFootprints(seed+float64(familyIndex)*sarosDays,
|
||||
SolarEclipsePartialFootprintOptions{
|
||||
StepDays: 20.0 / 1440.0,
|
||||
BoundaryPoints: 72,
|
||||
CentralShadowStepDays: 5.0 / 1440.0,
|
||||
DisableRiseSetCurves: true,
|
||||
})
|
||||
if !result.Eclipse.HasPartial || len(result.Footprints) == 0 {
|
||||
t.Fatalf("missing partial path: type=%s footprints=%d", result.Eclipse.Type, len(result.Footprints))
|
||||
}
|
||||
assertSolarEclipseP2Footprints(t, "partial", result.Footprints)
|
||||
assertSolarEclipseP2Footprints(t, "central-shadow", result.CentralShadowFootprints)
|
||||
assertSolarEclipseP2Footprints(t, "central-band", result.CentralBandFootprints)
|
||||
if result.Eclipse.Centrality == SolarEclipseNonCentral && result.Eclipse.Type != SolarEclipsePartial &&
|
||||
len(result.CentralBandFootprints) == 0 {
|
||||
t.Fatal("non-central annular/total event has no central-band samples")
|
||||
}
|
||||
if result.Eclipse.HasCentral {
|
||||
path := SolarEclipseCentralPath(seed+float64(familyIndex)*sarosDays,
|
||||
SolarEclipsePathOptions{StepDays: 20.0 / 1440.0, TargetSpacingKM: 500})
|
||||
if len(path.CenterLine) < 2 || len(path.NorthernLimit) < 2 || len(path.SouthernLimit) < 2 {
|
||||
t.Fatalf("central path is incomplete: center=%d north=%d south=%d",
|
||||
len(path.CenterLine), len(path.NorthernLimit), len(path.SouthernLimit))
|
||||
}
|
||||
assertSolarEclipseP2PointSeries(t, "center-line", path.CenterLine)
|
||||
assertSolarEclipseP2PointSeries(t, "north-limit", path.NorthernLimit)
|
||||
assertSolarEclipseP2PointSeries(t, "south-limit", path.SouthernLimit)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSolarEclipseHighResolutionSamplingHonorsPointBudget(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
shadowStep float64
|
||||
}{
|
||||
{name: "partial-only"},
|
||||
{name: "partial-and-central-shadow", shadowStep: 1.0 / 86400.0},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := SolarEclipsePartialFootprints(JDECalc(2024, 4, 8), SolarEclipsePartialFootprintOptions{
|
||||
StepDays: 1.0 / 86400.0,
|
||||
BoundaryPoints: solarEclipsePartialFootprintMaxBoundaryPoints,
|
||||
CentralShadowStepDays: test.shadowStep,
|
||||
DisableRiseSetCurves: true,
|
||||
})
|
||||
if len(result.Footprints) == 0 {
|
||||
t.Fatal("high-resolution request returned no partial footprints")
|
||||
}
|
||||
if result.BoundaryPoints < solarEclipsePartialFootprintMinBoundaryPoints ||
|
||||
result.BoundaryPoints > solarEclipsePartialFootprintMaxBoundaryPoints {
|
||||
t.Fatalf("effective boundary points=%d outside [%d,%d]", result.BoundaryPoints,
|
||||
solarEclipsePartialFootprintMinBoundaryPoints, solarEclipsePartialFootprintMaxBoundaryPoints)
|
||||
}
|
||||
totalPoints := 0
|
||||
for _, series := range [][]SolarEclipsePartialFootprint{result.Footprints, result.CentralShadowFootprints, result.CentralBandFootprints} {
|
||||
for _, footprint := range series {
|
||||
for _, boundary := range footprint.Boundaries {
|
||||
totalPoints += len(boundary)
|
||||
}
|
||||
}
|
||||
}
|
||||
if totalPoints > solarEclipsePartialFootprintMaxPointCount {
|
||||
t.Fatalf("high-resolution output points=%d, want <=%d", totalPoints, solarEclipsePartialFootprintMaxPointCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertSolarEclipseP2Footprints(t *testing.T, name string, footprints []SolarEclipsePartialFootprint) {
|
||||
t.Helper()
|
||||
for footprintIndex, footprint := range footprints {
|
||||
if !finite(footprint.JDE) || len(footprint.Boundaries) == 0 {
|
||||
t.Fatalf("%s footprint %d is incomplete: %+v", name, footprintIndex, footprint)
|
||||
}
|
||||
for boundaryIndex, boundary := range footprint.Boundaries {
|
||||
if len(boundary) < 2 {
|
||||
t.Fatalf("%s footprint %d boundary %d has %d points", name, footprintIndex, boundaryIndex, len(boundary))
|
||||
}
|
||||
for pointIndex, point := range boundary {
|
||||
if !finite(point.JDE) || !finite(point.Longitude) || !finite(point.Latitude) ||
|
||||
!finite(point.SunAltitude) || point.Longitude < -180 || point.Longitude > 180 ||
|
||||
point.Latitude < -90 || point.Latitude > 90 {
|
||||
t.Fatalf("%s footprint %d boundary %d point %d is invalid: %+v",
|
||||
name, footprintIndex, boundaryIndex, pointIndex, point)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertSolarEclipseP2PointSeries(t *testing.T, name string, points []SolarEclipsePathPoint) {
|
||||
t.Helper()
|
||||
for index, point := range points {
|
||||
if !finite(point.JDE) || !finite(point.Longitude) || !finite(point.Latitude) ||
|
||||
!finite(point.SunAltitude) || !finite(point.WidthKM) || point.Longitude < -180 ||
|
||||
point.Longitude > 180 || point.Latitude < -90 || point.Latitude > 90 {
|
||||
t.Fatalf("%s point %d is invalid: %+v", name, index, point)
|
||||
}
|
||||
if index > 0 && point.JDE <= points[index-1].JDE {
|
||||
t.Fatalf("%s times are not strictly increasing at %d", name, index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func formatSignedRegressionIndex(value int) string {
|
||||
if value >= 0 {
|
||||
return "+" + formatRegressionIndexMagnitude(value)
|
||||
}
|
||||
return "-" + formatRegressionIndexMagnitude(-value)
|
||||
}
|
||||
|
||||
func formatRegressionIndexMagnitude(value int) string {
|
||||
if value == 0 {
|
||||
return "0"
|
||||
}
|
||||
return string([]byte{'0' + byte(value/10), '0' + byte(value%10)})
|
||||
}
|
||||
Reference in New Issue
Block a user