Files
astro/basic/solar_eclipse_magnitude_interp_test.go
T
b612 044f2f4381 feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算
- 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询
- 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出
- 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解
- 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤
- 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持
- 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理
- 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
2026-10-15 23:54:15 +08:00

85 lines
3.4 KiB
Go

package basic
import (
"math"
"testing"
)
// 等值线追踪走插值星历,但每个输出点都必须满足精确星历下的食分 / contour tracing uses the
// interpolator, yet every output point must satisfy the target magnitude under exact ephemerides.
func TestSolarEclipseMagnitudeContoursExactAtOutputPoints(t *testing.T) {
cases := []struct {
name string
year, month int
day float64
deltaTSeconds float64
}{
{"2023-04-20 hybrid", 2023, 4, 20, 0},
{"2024-04-08 total", 2024, 4, 8, 0},
{"2022-10-25 partial", 2022, 10, 25, 0},
{"2027-08-02 total explicit ΔT", 2027, 8, 2, 75},
}
magnitudes := []float64{0.2, 0.5, 0.8}
for _, tc := range cases {
seed := JDCalc(tc.year, tc.month, tc.day)
result := SolarEclipsePartialFootprints(seed, SolarEclipsePartialFootprintOptions{
StepDays: 5.0 / 1440, BoundaryPoints: 48, MagnitudeValues: magnitudes,
DeltaTSeconds: tc.deltaTSeconds,
})
solver := newSolarEclipseSolverWithOptions(solarEclipseAnchorNewMoon(seed), SolarEclipseOptions{
RadiusModel: SolarEclipseModelNASABulletinSplitK,
}).withDeltaTSeconds(tc.deltaTSeconds)
if len(result.MagnitudeContours) != len(magnitudes) {
t.Fatalf("%s: %d contours, want %d", tc.name, len(result.MagnitudeContours), len(magnitudes))
}
for _, contour := range result.MagnitudeContours {
points := 0
for _, segment := range contour.Segments {
for _, point := range segment {
// 端点可能由地平线或混合食分支精修得到,只检查追踪内点 / endpoints may come
// from horizon or hybrid refinement; only traced interior points are checked.
if point.SunAltitude < 0.05 {
continue
}
state := solver.localStateContextAt(point.JDE).stateAt(point.Longitude*rad, point.Latitude*rad, 0)
got := solarEclipseMagnitudeAtTarget(state, contour.Magnitude)
if math.IsNaN(got) || math.Abs(got-contour.Magnitude) > 1e-6 {
t.Fatalf("%s m=%.1f point (%.6f, %.6f) at %.8f: exact magnitude %.9f",
tc.name, contour.Magnitude, point.Longitude, point.Latitude, point.JDE, got)
}
points++
}
}
if points < 20 {
t.Fatalf("%s m=%.1f: only %d interior points checked", tc.name, contour.Magnitude, points)
}
}
}
}
// 追踪依赖的插值误差在事件 ±4 小时内远低于等值线容差 / interpolation error over ±4 h stays far below contour tolerance.
func TestSolarEclipseLocalEphemerisAngularErrorForContours(t *testing.T) {
center := CalcMoonSHByJDE(JDCalc(2024, 4, 8), 0)
ephemeris := newSolarEclipseLocalEphemeris(center)
maxArcsec := 0.0
for step := -1440; step <= 1440; step++ {
jde := center + float64(step)*10.0/86400
sun, moon, ok := ephemeris.equatorialAt(jde)
if !ok {
t.Fatalf("interpolator rejected %+.4f d", jde-center)
}
wantSun, wantMoon := solarEclipseSunMoonEquatorial(jde)
for _, pair := range [][2][3]float64{{sun, wantSun}, {moon, wantMoon}} {
got := solarEclipseLLRToXYZ(pair[0][0], pair[0][1], pair[0][2])
want := solarEclipseLLRToXYZ(pair[1][0], pair[1][1], pair[1][2])
cross := [3]float64{got[1]*want[2] - got[2]*want[1], got[2]*want[0] - got[0]*want[2], got[0]*want[1] - got[1]*want[0]}
dot := got[0]*want[0] + got[1]*want[1] + got[2]*want[2]
arcsec := math.Atan2(math.Sqrt(cross[0]*cross[0]+cross[1]*cross[1]+cross[2]*cross[2]), dot) / rad * 3600
maxArcsec = math.Max(maxArcsec, arcsec)
}
}
if maxArcsec > 1e-3 {
t.Fatalf("max interpolation error %.3e arcsec, want <= 1e-3", maxArcsec)
}
}