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,138 @@
|
||||
// 被测契约:天狼星在开罗约 8 月初晨始见;金星下合前后依次出现昏末见与晨始见;极限星等随天光单调变化;偕日事件不得越出请求区间。
|
||||
package basic
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSiriusHeliacalRisingCairo(t *testing.T) {
|
||||
sirius := StarCoordinate{ID: "Sirius", RA: 101.28715533, Dec: -16.71611586, Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), Frame: CoordinateFrameJ2000,
|
||||
ProperMotionRACosDecMasPerYear: -546.01, ProperMotionDecMasPerYear: -1223.07}
|
||||
start := Date2JD(time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC))
|
||||
end := Date2JD(time.Date(2025, 9, 30, 0, 0, 0, 0, time.UTC))
|
||||
events := FindStarHeliacalEventsWithInstance(sirius, -1.46, start, end, 31.23, 30.04, HeliacalOptions{}, nil)
|
||||
var rising *HeliacalEvent
|
||||
for i := range events {
|
||||
if events[i].Kind == HeliacalMorningFirst {
|
||||
rising = &events[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if rising == nil {
|
||||
t.Fatalf("no morning first visibility: %+v", events)
|
||||
}
|
||||
when := JD2Date(rising.JD)
|
||||
t.Logf("Sirius morning first %v margin %.2f alt %.2f sun %.2f", when, rising.Margin, rising.TargetAltitude, rising.SunAltitude)
|
||||
if when.Month() != time.July && when.Month() != time.August {
|
||||
t.Errorf("Sirius heliacal rising %v, expected late July to mid August", when)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVenusHeliacalAroundInferiorConjunction(t *testing.T) {
|
||||
// 2025-03-23 金星下合。
|
||||
start := Date2JD(time.Date(2025, 2, 15, 0, 0, 0, 0, time.UTC))
|
||||
end := Date2JD(time.Date(2025, 4, 30, 0, 0, 0, 0, time.UTC))
|
||||
events := FindPlanetHeliacalEventsWithInstance(SkyVenus, start, end, 116.4, 39.9, HeliacalOptions{}, nil)
|
||||
var eveningLast, morningFirst float64
|
||||
for _, e := range events {
|
||||
t.Logf("kind %d %v margin %.2f", e.Kind, JD2Date(e.JD), e.Margin)
|
||||
if e.Kind == HeliacalEveningLast && eveningLast == 0 {
|
||||
eveningLast = e.JD
|
||||
}
|
||||
if e.Kind == HeliacalMorningFirst && morningFirst == 0 {
|
||||
morningFirst = e.JD
|
||||
}
|
||||
}
|
||||
conj := Date2JD(time.Date(2025, 3, 23, 0, 0, 0, 0, time.UTC))
|
||||
if eveningLast == 0 || morningFirst == 0 || !(eveningLast < conj && morningFirst > conj) {
|
||||
t.Errorf("evening last %.1f, morning first %.1f around conjunction %.1f", eveningLast, morningFirst, conj)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitingMagnitudeDarkensWithTwilight(t *testing.T) {
|
||||
prev := LimitingMagnitude(20, -2, 30, 0.25)
|
||||
for _, sun := range []float64{-4, -6, -9, -12, -15, -18} {
|
||||
cur := LimitingMagnitude(20, sun, 30, 0.25)
|
||||
if !(cur > prev) {
|
||||
t.Errorf("sun %.0f: limiting magnitude %.2f not fainter than %.2f", sun, cur, prev)
|
||||
}
|
||||
prev = cur
|
||||
}
|
||||
if prev < 5 || prev > 7.5 {
|
||||
t.Errorf("dark-sky limiting magnitude at 20° altitude %.2f", prev)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoonlightBrightensSky(t *testing.T) {
|
||||
if MoonlightNanolambert(30, -5, 20, 10, 0.25) != 0 {
|
||||
t.Error("moon below horizon must contribute nothing")
|
||||
}
|
||||
full := MoonlightNanolambert(30, 40, 20, 5, 0.25)
|
||||
crescent := MoonlightNanolambert(30, 40, 20, 120, 0.25)
|
||||
if !(full > crescent && crescent > 0 && full > 1000) {
|
||||
t.Errorf("full %.1f crescent %.1f nL", full, crescent)
|
||||
}
|
||||
start := 2460676.5
|
||||
plain := FindPlanetHeliacalEventsWithInstance(SkyVenus, start, start+400, 116.4, 39.9, HeliacalOptions{}, nil)
|
||||
lit := FindPlanetHeliacalEventsWithInstance(SkyVenus, start, start+400, 116.4, 39.9, HeliacalOptions{Moonlight: true}, nil)
|
||||
if len(plain) == 0 || len(lit) == 0 {
|
||||
t.Fatalf("events %d %d", len(plain), len(lit))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeliacalRejectsUnscannableRange(t *testing.T) {
|
||||
sirius := StarCoordinate{RA: 101.287, Dec: -16.716}
|
||||
for _, r := range [][2]float64{
|
||||
{2451545, math.Inf(1)}, {math.Inf(-1), 2451545}, {math.NaN(), 2451545},
|
||||
{2451545, math.NaN()}, {2451545, 2451545 + 1e10}, {1e300, math.MaxFloat64},
|
||||
} {
|
||||
if got := FindStarHeliacalEventsWithInstance(sirius, -1.46, r[0], r[1], 31.23, 30.04, HeliacalOptions{}, nil); got != nil {
|
||||
t.Errorf("star %v: %d events", r, len(got))
|
||||
}
|
||||
if got := FindPlanetHeliacalEventsWithInstance(SkyVenus, r[0], r[1], 116.4, 39.9, HeliacalOptions{}, nil); got != nil {
|
||||
t.Errorf("planet %v: %d events", r, len(got))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkyModelRejectsNonPositiveExtinction(t *testing.T) {
|
||||
for _, k := range []float64{0, -1, math.NaN(), math.Inf(1)} {
|
||||
if v := SkyBrightnessNanolambert(20, -10, 30, k); !math.IsNaN(v) {
|
||||
t.Errorf("sky k=%g: %g", k, v)
|
||||
}
|
||||
if v := LimitingMagnitude(20, -10, 30, k); !math.IsNaN(v) {
|
||||
t.Errorf("limit k=%g: %g", k, v)
|
||||
}
|
||||
if v := MoonlightNanolambert(30, 40, 20, 5, k); !math.IsNaN(v) {
|
||||
t.Errorf("moonlight k=%g: %g", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeliacalEventsStayWithinRequestedRange(t *testing.T) {
|
||||
lon, lat := 116.4, 39.9
|
||||
narrow := 0
|
||||
for _, body := range []SkyBody{SkyMercury, SkyVenus, SkyMars, SkyJupiter, SkySaturn} {
|
||||
wide := FindPlanetHeliacalEventsWithInstance(body, 2459000.5, 2460000.5, lon, lat, HeliacalOptions{}, nil)
|
||||
for _, event := range wide {
|
||||
// 事件时刻是采样日内的细化值;把 endJD 卡在两者之间,正是越界最容易出现的窗口。
|
||||
day := math.Floor(event.JD-0.5) + 0.5
|
||||
offset := event.JD - day
|
||||
if offset <= 0.1 {
|
||||
continue
|
||||
}
|
||||
start, end := day, day+offset-0.05
|
||||
narrow++
|
||||
for _, got := range FindPlanetHeliacalEventsWithInstance(body, start, end, lon, lat, HeliacalOptions{}, nil) {
|
||||
if got.JD < start || got.JD > end {
|
||||
t.Fatalf("body %v: event JD %.6f outside [%.6f, %.6f]", body, got.JD, start, end)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if narrow == 0 {
|
||||
t.Fatal("no narrow window was exercised")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user