Files
astro/eclipse/lunar_ephemeris_instance_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

154 lines
6.0 KiB
Go

package eclipse
import (
"math"
"reflect"
"testing"
"time"
"b612.me/astro/basic"
"b612.me/astro/internal/ephem"
)
type p3LunarShiftEngine struct {
base *ephem.Instance
offset float64
}
func (engine p3LunarShiftEngine) State(target, center ephem.Body, jde float64) ([3]float64, [3]float64, error) {
return engine.base.State(target, center, jde+engine.offset)
}
func (engine p3LunarShiftEngine) Apparent(target ephem.Body, jde float64) ([3]float64, float64, float64, error) {
return engine.base.Apparent(target, jde+engine.offset)
}
func (engine p3LunarShiftEngine) Coverage(target ephem.Body) (float64, float64, bool) {
start, end, ok := engine.base.Coverage(target)
return start - engine.offset, end - engine.offset, ok
}
func TestP3LunarQueriesPreserveNilAndLegacy(t *testing.T) {
previous := ephem.Active()
t.Cleanup(func() { ephem.SetActive(previous) })
date := time.Date(2022, 11, 8, 12, 0, 0, 0, time.UTC)
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
ephem.SetActive(model)
inst := ephem.NewModelInstance(model)
for _, query := range []struct {
old func(time.Time) (LunarEclipseInfo, bool)
new func(time.Time, *ephem.Instance) (LunarEclipseInfo, bool)
}{
{LunarEclipseOnDate, LunarEclipseOnDateWithInstance},
{LunarEclipseOnDateDanjon, LunarEclipseOnDateDanjonWithInstance},
{LunarEclipseOnDateChauvenet, LunarEclipseOnDateChauvenetWithInstance},
} {
want, wantOK := query.old(date)
got, gotOK := query.new(date, nil)
if gotOK != wantOK || !reflect.DeepEqual(got, want) {
t.Error("nil changed local-date query")
}
if model == ephem.Legacy {
got, gotOK = query.new(date, inst)
if gotOK != wantOK || !reflect.DeepEqual(got, want) {
t.Error("explicit Legacy changed local-date query")
}
}
}
for _, query := range []struct {
old func(time.Time) LunarEclipseInfo
new func(time.Time, *ephem.Instance) LunarEclipseInfo
}{
{LastLunarEclipse, LastLunarEclipseWithInstance},
{LastLunarEclipseDanjon, LastLunarEclipseDanjonWithInstance},
{LastLunarEclipseChauvenet, LastLunarEclipseChauvenetWithInstance},
{NextLunarEclipse, NextLunarEclipseWithInstance},
{NextLunarEclipseDanjon, NextLunarEclipseDanjonWithInstance},
{NextLunarEclipseChauvenet, NextLunarEclipseChauvenetWithInstance},
{ClosestLunarEclipse, ClosestLunarEclipseWithInstance},
{ClosestLunarEclipseDanjon, ClosestLunarEclipseDanjonWithInstance},
{ClosestLunarEclipseChauvenet, ClosestLunarEclipseChauvenetWithInstance},
} {
want := query.old(date)
if got := query.new(date, nil); !reflect.DeepEqual(got, want) {
t.Error("nil changed directional search")
}
if model == ephem.Legacy {
if got := query.new(date, inst); !reflect.DeepEqual(got, want) {
t.Error("explicit Legacy changed directional search")
}
}
}
}
}
func TestP3LunarQueriesUseInstanceThroughout(t *testing.T) {
previous := ephem.Active()
t.Cleanup(func() { ephem.SetActive(previous) })
base := ephem.NewModelInstance(ephem.Semianalytic)
first := ephem.NewInstance(p3LunarShiftEngine{base: base, offset: 0.1})
second := ephem.NewInstance(p3LunarShiftEngine{base: base, offset: 0.2})
date := time.Date(2022, 11, 8, 12, 0, 0, 0, time.FixedZone("UTC+8", 8*3600))
global, model, generation := ephem.Global(), ephem.Active(), ephem.Generation()
firstInfo, firstOK := LunarEclipseOnDateWithInstance(date, first)
secondInfo, secondOK := LunarEclipseOnDateWithInstance(date, second)
if !firstOK || !secondOK || !firstInfo.HasTotal || !secondInfo.HasTotal {
t.Fatalf("explicit query lost total eclipse: %+v / %+v", firstInfo, secondInfo)
}
if firstInfo.HasSaros || secondInfo.HasSaros {
t.Error("custom engines must not inherit built-in Saros metadata")
}
if math.Abs(firstInfo.GreatestJDE-secondInfo.GreatestJDE-0.1) > 0.001 {
t.Errorf("event time did not follow instances: %v / %v", firstInfo.GreatestJDE, secondInfo.GreatestJDE)
}
if firstInfo.Maximum.Location() != date.Location() || len(firstInfo.ContactPoints) != 6 {
t.Error("local timezone or contacts were lost")
}
if ephem.Global() != global || ephem.Active() != model || ephem.Generation() != generation {
t.Error("query changed global ephemeris state")
}
for _, active := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
ephem.SetActive(active)
got, ok := LunarEclipseOnDateWithInstance(date, first)
if !ok || !reflect.DeepEqual(got, firstInfo) {
t.Error("result, contact orientation or shadow geometry read the global model")
}
for _, query := range []func(time.Time, *ephem.Instance) LunarEclipseInfo{LastLunarEclipseWithInstance, NextLunarEclipseWithInstance, ClosestLunarEclipseWithInstance} {
got := query(firstInfo.Maximum.Add(-time.Minute), first)
if got.Type == LunarEclipseNone || got.Maximum.IsZero() {
t.Error("directional search lost instance")
}
}
}
}
func TestP3LunarSarosSelectsInstanceModel(t *testing.T) {
previous := ephem.Active()
t.Cleanup(func() { ephem.SetActive(previous) })
for _, model := range []ephem.Model{ephem.Legacy, ephem.Semianalytic} {
inst := ephem.NewModelInstance(model)
ephem.SetActive(model)
for _, seed := range []float64{basic.JDCalc(2022, 11, 8), basic.JDCalc(7000, 1, 1), basic.JDCalc(-5000, 1, 1)} {
want, wantOK := lunarSarosInfoWithInstance(seed, inst)
ephem.SetActive(ephem.Model(1 - int(model)))
got, gotOK := lunarSarosInfoWithInstance(seed, inst)
if gotOK != wantOK || got != want {
t.Errorf("Saros read active model instead of instance: %+v / %+v", got, want)
}
}
}
}
func TestP3LunarQueriesStopOnEngineFailure(t *testing.T) {
inst := ephem.NewInstance(nil)
date := time.Date(2022, 11, 8, 12, 0, 0, 0, time.UTC)
if info, ok := LunarEclipseOnDateWithInstance(date, inst); ok || info.Type != "" {
t.Error("failed engine produced a local-date eclipse")
}
for _, query := range []func(time.Time, *ephem.Instance) LunarEclipseInfo{LastLunarEclipseWithInstance, NextLunarEclipseWithInstance, ClosestLunarEclipseWithInstance} {
if info := query(date, inst); !info.Maximum.IsZero() {
t.Error("failed engine fell back during directional search")
}
}
}