feat: 引入可切换星历模型并扩展天象观测链路

- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算
- 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询
- 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出
- 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解
- 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤
- 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持
- 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理
- 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
This commit is contained in:
2026-10-15 23:54:15 +08:00
parent 16c62a97d5
commit 044f2f4381
674 changed files with 62983 additions and 8396 deletions
+41 -13
View File
@@ -11,6 +11,7 @@ import (
"time"
"b612.me/astro/basic"
"b612.me/astro/internal/ephem"
)
// 默认只校验发布表的新鲜度与金标准;重新生成必须同时给出 -saros-generate 与环境变量。
@@ -41,7 +42,7 @@ type sarosTableGolden struct {
span sarosSpan
}
var sarosTableGoldens = []sarosTableGolden{
var sarosTableLegacyGoldens = []sarosTableGolden{
{name: "solar/head", span: sarosSpan{Series: -47, First: -79891, Last: -61605, Member: 1, Count: 83}},
{name: "solar/202", span: sarosSpan{Series: 202, First: 15941, Last: 31551, Member: 1, Count: 71}},
{name: "solar/tail", span: sarosSpan{Series: 293, First: 48742, Last: 63906, Member: 1, Count: 69}},
@@ -51,6 +52,21 @@ var sarosTableGoldens = []sarosTableGolden{
{name: "lunar/tail", span: sarosSpan{Series: 287, First: 49211, Last: 64821, Member: 1, Count: 71}},
}
func sarosTableGoldens() []sarosTableGolden {
if ephem.Active() != ephem.Semianalytic {
return sarosTableLegacyGoldens
}
return []sarosTableGolden{
{name: "solar/head", span: sarosSpan{Series: -47, First: -80114, Last: -61605, Member: 1, Count: 84}},
{name: "solar/202", span: sarosSpan{Series: 202, First: 16164, Last: 31551, Member: 1, Count: 70}},
{name: "solar/tail", span: sarosSpan{Series: 293, First: 48742, Last: 63906, Member: 1, Count: 69}},
{name: "lunar/head", span: sarosSpan{Series: -54, First: -81564, Last: -61717, Member: 1, Count: 90}},
{name: "lunar/4-head", span: sarosSpan{Series: 4, First: -57901, Last: -56340, Member: 1, Count: 86}},
{name: "lunar/4-body", span: sarosSpan{Series: 4, First: -55894, Last: -38723, Member: 9, Count: 86}},
{name: "lunar/tail", span: sarosSpan{Series: 287, First: 49211, Last: 64821, Member: 1, Count: 71}},
}
}
// 发布表必须与文档化的窗口、生成时的序列总数和若干金标准行一致。
func TestSarosExtensionTableIsFresh(t *testing.T) {
pinSarosWindowPolicy(t)
@@ -64,17 +80,25 @@ func TestSarosExtensionTableIsFresh(t *testing.T) {
rows int
span []sarosSpan
}{
{name: "solar", phase: 0, rows: 159, span: solarSarosExtended[:]},
{name: "lunar", phase: 1, rows: 249, span: lunarSarosExtended[:]},
{name: "solar", phase: 0, rows: 159, span: sarosExtensionSpans(0)},
{name: "lunar", phase: 1, rows: 249, span: sarosExtensionSpans(1)},
} {
if ephem.Active() == ephem.Semianalytic {
switch table.phase {
case 0:
table.rows = 163
case 1:
table.rows = 252
}
}
if len(table.span) != table.rows {
t.Fatalf("%s table has %d rows, golden table has %d", table.name, len(table.span), table.rows)
}
validateSarosExtensionSpans(t, table.name, table.phase, table.span)
}
for _, golden := range sarosTableGoldens {
for _, golden := range sarosTableGoldens() {
found := false
for _, span := range append(solarSarosExtended[:], lunarSarosExtended[:]...) {
for _, span := range append(append([]sarosSpan{}, sarosExtensionSpans(0)...), sarosExtensionSpans(1)...) {
if span == golden.span {
found = true
break
@@ -121,7 +145,7 @@ type sarosGenerationEvent struct {
func TestGenerateSarosExtensionTables(t *testing.T) {
if !sarosTableGenerationEnabled() {
t.Skipf("set %s=1 and pass -saros-generate to rewrite saros_table_extended.go", sarosGenerateEnv)
t.Skipf("set %s=1 and pass -saros-generate to rewrite the active model's Saros table", sarosGenerateEnv)
}
pinSarosWindowPolicy(t)
startTT := timeToTTJDE(time.Date(-3000, 1, 1, 0, 0, 0, 0, time.UTC))
@@ -129,17 +153,17 @@ func TestGenerateSarosExtensionTables(t *testing.T) {
var buf bytes.Buffer
fmt.Fprintln(&buf, "// Code generated by go generate ./eclipse; DO NOT EDIT.")
fmt.Fprintln(&buf, "\npackage eclipse")
fmt.Fprintln(&buf, "\n// UTC astronomical years -3000 through +6000, including both end years.")
fmt.Fprintln(&buf, "// Series numbers follow NASA's Saros-Inex rules. Members are computed with")
fmt.Fprintln(&buf, "// Split-K solar geometry and the union of Danjon/Chauvenet lunar detections.")
fmt.Fprintf(&buf, "const sarosExtendedStartTT = %.12f\nconst sarosExtendedEndTT = %.12f\n", startTT, endTT)
suffix := "Legacy"
if ephem.Active() == ephem.Semianalytic {
suffix = "Semianalytic"
}
for phase, name := range []string{"solar", "lunar"} {
events := scanSarosGenerationEvents(t, phase)
assignSarosGenerationNumbers(t, events, phase)
spans := buildSarosGenerationSpans(t, events, phase, startTT, endTT)
// 写盘前先按发布口径校验,坏表不得进入仓库。
validateSarosExtensionSpans(t, name, phase, spans)
fmt.Fprintf(&buf, "\nvar %sSarosExtended = [...]sarosSpan{\n", name)
fmt.Fprintf(&buf, "\nvar %sSarosExtended%s = [...]sarosSpan{\n", name, suffix)
for _, span := range spans {
fmt.Fprintf(&buf, "{Series: %d, First: %d, Last: %d, Member: %d, Count: %d},\n", span.Series, span.First, span.Last, span.Member, span.Count)
}
@@ -150,10 +174,14 @@ func TestGenerateSarosExtensionTables(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile("saros_table_extended.go", source, 0644); err != nil {
filename := "saros_table_extended_legacy.go"
if ephem.Active() == ephem.Semianalytic {
filename = "saros_table_extended_semianalytic.go"
}
if err := os.WriteFile(filename, source, 0644); err != nil {
t.Fatal(err)
}
t.Logf("rewrote saros_table_extended.go; rerun TestSarosExtensionTableIsFresh and update the goldens")
t.Logf("rewrote %s; rerun TestSarosExtensionTableIsFresh and update the goldens", filename)
}
func scanSarosGenerationEvents(t *testing.T, phase int) []sarosGenerationEvent {