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

87 lines
4.3 KiB
Go

package basic
import (
"fmt"
"math"
"testing"
"b612.me/astro/internal/geodata"
)
func TestSolarEclipseHybridEnvelope21640323(t *testing.T) {
seed := JDECalc(2164, 3, 23)
result := SolarEclipsePartialFootprints(seed, SolarEclipsePartialFootprintOptions{StepDays: 2.0 / 1440, BoundaryPoints: 96})
if len(result.CentralBandHorizonClosures) != 2 {
t.Fatal("missing hybrid horizon closures")
}
if len(result.CentralBandSegments) != 3 {
t.Fatalf("hybrid envelope segments=%d, want annular/total/annular", len(result.CentralBandSegments))
}
solver := newSolarEclipseSolver(CalcMoonSHByJDE(seed, 0), SolarEclipseModelNASABulletinSplitK)
for _, closure := range result.CentralBandHorizonClosures {
for _, point := range []SolarEclipsePathPoint{closure[0], closure[len(closure)-1]} {
evaluation := solver.magnitudeEvaluationAt(point.JDE)
state := evaluation.center.stateAt(point.Longitude*rad, point.Latitude*rad, 0)
first, _ := solarCentralBandVectorResidual(evaluation, point.Longitude, point.Latitude, 1)
second, _ := solarCentralBandVectorResidual(evaluation, point.Longitude, point.Latitude, -1)
if math.Abs(solarEclipseCentralContactGap(state)) > 1e-7 || math.Abs(state.sunAltitudeRad) > 1e-7 ||
math.Min(math.Hypot(first[0], first[1]), math.Hypot(second[0], second[1])) > 1.01*solarEclipseCentralVectorTolerance {
t.Fatalf("hybrid root is not a central-envelope horizon intersection: %+v", point)
}
}
}
}
func TestSolarEclipseHybridSignedEnvelopeEvents(t *testing.T) {
for _, date := range [][3]int{{1144, 7, 3}, {1827, 10, 20}, {1854, 11, 20}, {1986, 10, 3}, {2013, 11, 3}, {2023, 4, 20}, {2164, 3, 23}, {2172, 10, 17}} {
t.Run(fmt.Sprintf("%04d-%02d-%02d", date[0], date[1], date[2]), func(t *testing.T) {
seed := JDECalc(date[0], date[1], float64(date[2]))
result := SolarEclipsePartialFootprints(seed, SolarEclipsePartialFootprintOptions{StepDays: 2.0 / 1440, BoundaryPoints: 96, CentralShadowStepDays: 2.0 / 1440})
if result.Eclipse.Type != SolarEclipseHybrid || len(result.CentralBandHorizonClosures) != 2 || len(result.CentralBandSegments) < 2 {
t.Fatalf("type=%s closures=%d segments=%d", result.Eclipse.Type, len(result.CentralBandHorizonClosures), len(result.CentralBandSegments))
}
solver := newSolarEclipseSolver(CalcMoonSHByJDE(seed, 0), SolarEclipseModelNASABulletinSplitK)
polygons := make([][]geodata.GeoPoint, len(result.CentralBandSegments))
for i, ring := range result.CentralBandSegments {
if len(ring) < 4 || solarEclipsePathDistanceKM(ring[0], ring[len(ring)-1]) > 0.001 {
t.Fatal("open hybrid component")
}
for _, point := range ring {
polygons[i] = append(polygons[i], geodata.GeoPoint{Longitude: point.Longitude, Latitude: point.Latitude})
if solarEclipseCentralEnvelopePointOnHorizonClosure(point, result.CentralBandHorizonClosures) {
continue
}
evaluation := solver.magnitudeEvaluationAt(point.JDE)
first, firstOK := solarCentralBandVectorResidual(evaluation, point.Longitude, point.Latitude, 1)
second, secondOK := solarCentralBandVectorResidual(evaluation, point.Longitude, point.Latitude, -1)
if !firstOK || !secondOK || math.Min(math.Hypot(first[0], first[1]), math.Hypot(second[0], second[1])) > 1.01*solarEclipseCentralVectorTolerance {
t.Fatalf("non-critical point: %+v residuals=%v,%v", point, first, second)
}
}
}
var visible []geodata.GeoPoint
for _, footprint := range result.CentralShadowFootprints {
context := solver.localStateContextAt(footprint.JDE)
for _, boundary := range footprint.Boundaries {
for i, point := range boundary {
if i%4 != 0 {
continue
}
state := context.stateAt(point.Longitude*rad, point.Latitude*rad, 0)
if state.sunAltitudeRad > 0 && solarEclipseCentralContactGap(state) < -1e-8 {
visible = append(visible, geodata.GeoPoint{Longitude: point.Longitude, Latitude: point.Latitude})
}
}
}
}
contained := geodata.SphericalPolygonsContainPoints(polygons, visible)
for i, inside := range contained {
if !inside && !geodata.SphericalPolygonsContainPathsWithinKM(polygons, [][]geodata.GeoPoint{{visible[i], visible[i]}}, false, 0.05) {
t.Errorf("visible central-shadow point outside envelope: %+v", visible[i])
}
}
t.Logf("segments=%d visible shadow samples=%d", len(polygons), len(visible))
})
}
}