feat: 引入可切换星历模型并扩展天象观测链路
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
// 被测契约:通用天象搜索与各行星专用事件一致,并能找到大合与行星近星。
|
||||
package basic
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"b612.me/astro/internal/geodata"
|
||||
)
|
||||
|
||||
func TestSkyAspectMatchesMarsOpposition(t *testing.T) {
|
||||
start := 2460000.5
|
||||
// 专用函数返回 UT。
|
||||
want := UTC2TT(NextMarsOppositionWithInstance(start, nil))
|
||||
got := SkyAspectCrossingsWithInstance(SkyMars, SkySun, 180, start, start+800, nil)
|
||||
if len(got) == 0 || math.Abs(got[0].JDE-want)*86400 > 1 {
|
||||
t.Fatalf("opposition %v, want %.6f", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// 专用函数按赤经留且返回 UT,通用搜索按黄经留,二者相差数小时属口径不同;这里只核对黄经速度确实变号。
|
||||
func TestSkyStationsFlipLongitudeRate(t *testing.T) {
|
||||
got := SkyStationsWithInstance(SkyMars, 2460000.5, 2460800.5, nil)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("stations %v", got)
|
||||
}
|
||||
for _, e := range got {
|
||||
before, _ := SkyBodyApparentLoBoWithInstance(SkyMars, e.JDE-1, nil)
|
||||
at, _ := SkyBodyApparentLoBoWithInstance(SkyMars, e.JDE, nil)
|
||||
after, _ := SkyBodyApparentLoBoWithInstance(SkyMars, e.JDE+1, nil)
|
||||
if geodata.NormalizeLongitude(at-before)*geodata.NormalizeLongitude(after-at) >= 0 {
|
||||
t.Errorf("no rate flip at %.5f", e.JDE)
|
||||
}
|
||||
ut := UTC2TT(NextMarsProgradeToRetrogradeWithInstance(e.JDE-30, nil))
|
||||
if math.Abs(ut-e.JDE) > 5 && math.Abs(UTC2TT(NextMarsRetrogradeToProgradeWithInstance(e.JDE-30, nil))-e.JDE) > 5 {
|
||||
t.Errorf("station %.5f far from RA stations", e.JDE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkyGreatConjunction2020(t *testing.T) {
|
||||
got := SkySeparationMinimaWithInstance(SkyJupiter, SkySaturn, 1, 2459100.5, 2459300.5, nil)
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("minima %v", got)
|
||||
}
|
||||
when := JD2Date(TT2UTC(got[0].JDE))
|
||||
if when.Year() != 2020 || when.Month() != time.December || when.Day() < 21 || when.Day() > 22 || got[0].Value > 0.11 || got[0].Value < 0.09 {
|
||||
t.Errorf("great conjunction at %v separation %.4f°", when, got[0].Value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkyStarApproachRegulus(t *testing.T) {
|
||||
regulus := StarCoordinate{ID: "Regulus", RA: 152.09296, Dec: 11.96721, Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), Frame: CoordinateFrameJ2000}
|
||||
got := SkyStarSeparationMinimaWithInstance(SkyVenus, regulus, 1.5, 2460400.5, 2460600.5, nil)
|
||||
if len(got) == 0 {
|
||||
t.Fatal("no Venus–Regulus approach")
|
||||
}
|
||||
for _, e := range got {
|
||||
if e.Value > 1.5 || e.Value < 0 {
|
||||
t.Errorf("approach %.4f° at %.5f", e.Value, e.JDE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkySeparationMinimaFindsNarrowWindow(t *testing.T) {
|
||||
wide := SkySeparationMinimaWithInstance(SkyJupiter, SkySaturn, 1, 2459190, 2459220, nil)
|
||||
if len(wide) == 0 {
|
||||
t.Fatal("wide window returned no minimum")
|
||||
}
|
||||
center := wide[0].JDE
|
||||
for _, halfWidth := range []float64{0.1, 0.25, 0.49, 0.99, 1.5, 3} {
|
||||
got := SkySeparationMinimaWithInstance(SkyJupiter, SkySaturn, 1, center-halfWidth, center+halfWidth, nil)
|
||||
if len(got) == 0 {
|
||||
t.Errorf("halfWidth %v: no minimum", halfWidth)
|
||||
continue
|
||||
}
|
||||
if math.Abs(got[0].JDE-center) > 1e-4 {
|
||||
t.Errorf("halfWidth %v: JDE = %.6f, want %.6f", halfWidth, got[0].JDE, center)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkySeparationMinimaFindsEdgeOfWindow(t *testing.T) {
|
||||
wide := SkySeparationMinimaWithInstance(SkyJupiter, SkySaturn, 1, 2459190, 2459220, nil)
|
||||
if len(wide) == 0 {
|
||||
t.Fatal("wide window returned no minimum")
|
||||
}
|
||||
center := wide[0].JDE
|
||||
windows := [][2]float64{
|
||||
{center - 1.0/1440, center + 3.0/24},
|
||||
{center - 3.0/24, center + 1.0/1440},
|
||||
{center - 1.0/24, center + 1.0/24},
|
||||
}
|
||||
for _, window := range windows {
|
||||
got := SkySeparationMinimaWithInstance(SkyJupiter, SkySaturn, 1, window[0], window[1], nil)
|
||||
if len(got) != 1 || math.Abs(got[0].JDE-center) > 1e-4 {
|
||||
t.Errorf("window [%+.4f, %+.4f]: got %+v, want one minimum at %.6f", window[0]-center, window[1]-center, got, center)
|
||||
}
|
||||
}
|
||||
if got := SkySeparationMinimaWithInstance(SkyJupiter, SkySaturn, 1, center+0.5, center+3, nil); len(got) != 0 {
|
||||
t.Errorf("window after the minimum: got %+v, want none", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkyScansTerminateOnDegenerateWindows(t *testing.T) {
|
||||
start := 2459205.26539
|
||||
windows := [][2]float64{
|
||||
{start, math.Nextafter(start, math.Inf(1))},
|
||||
{start, start + 26e-6/86400},
|
||||
{start, math.Inf(1)},
|
||||
{math.Inf(-1), start},
|
||||
{math.NaN(), start},
|
||||
}
|
||||
for _, window := range windows {
|
||||
SkySeparationMinimaWithInstance(SkyJupiter, SkySaturn, 1, window[0], window[1], nil)
|
||||
SkyLongitudeCrossingsWithInstance(SkySun, 280, window[0], window[1], nil)
|
||||
SkyAspectCrossingsWithInstance(SkyJupiter, SkySaturn, 0, window[0], window[1], nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkyGridRejectsLimitInclusive(t *testing.T) {
|
||||
if _, _, ok := skyGrid(0, float64(skyGridMaxIntervals), 1, 1); ok {
|
||||
t.Error("grid at limit accepted")
|
||||
}
|
||||
if count, _, ok := skyGrid(0, float64(skyGridMaxIntervals-1), 1, 1); !ok || count != skyGridMaxIntervals-1 {
|
||||
t.Errorf("grid below limit: count=%d ok=%v", count, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkyLongitudeCrossingsIncludesClosedEndpoints(t *testing.T) {
|
||||
const jde = 2451545.0
|
||||
lo, _ := SkyBodyApparentLoBoWithInstance(SkySun, jde, nil)
|
||||
for _, window := range [][2]float64{{jde - 2, jde}, {jde, jde + 2}} {
|
||||
got := SkyLongitudeCrossingsWithInstance(SkySun, lo, window[0], window[1], nil)
|
||||
if len(got) != 1 || math.Abs(got[0].JDE-jde) > 1e-5 {
|
||||
t.Errorf("window [%v, %v]: got %+v, want one crossing at %v", window[0], window[1], got, jde)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user