Files
astro/star/catalog/cross_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

142 lines
4.9 KiB
Go

package catalog
import (
"math"
"os"
"strings"
"testing"
"time"
"b612.me/astro/basic"
)
// propagateToJ2000 按线性空间运动把记录推到 J2000.0,返回赤经赤纬(度)。
func propagateToJ2000(entry Entry) (float64, float64) {
c := entry.Coordinate
j2000 := basic.JD2DateByZone(2451545.0, time.UTC, false)
years := j2000.Sub(c.Epoch).Hours() / 24 / 365.25
ra := c.RA * math.Pi / 180
dec := c.Dec * math.Pi / 180
p := [3]float64{math.Cos(dec) * math.Cos(ra), math.Cos(dec) * math.Sin(ra), math.Sin(dec)}
east := [3]float64{-math.Sin(ra), math.Cos(ra), 0}
north := [3]float64{-math.Sin(dec) * math.Cos(ra), -math.Sin(dec) * math.Sin(ra), math.Cos(dec)}
masToRad := math.Pi / 180 / 3600 / 1000
var q [3]float64
for i := range q {
q[i] = p[i] + (east[i]*c.ProperMotionRACosDecMasPerYear+north[i]*c.ProperMotionDecMasPerYear)*masToRad*years
}
return math.Mod(math.Atan2(q[1], q[0])*180/math.Pi+360, 360), math.Atan2(q[2], math.Hypot(q[0], q[1])) * 180 / math.Pi
}
func separationMas(ra1, dec1, ra2, dec2 float64) float64 {
dra := (ra1 - ra2) * math.Cos(dec1*math.Pi/180)
if dra > 180 {
dra -= 360
}
if dra < -180 {
dra += 360
}
return math.Hypot(dra, dec1-dec2) * 3600 * 1000
}
func readFixtureCatalog(t *testing.T, name string, read func(*os.File) (*Catalog, error)) *Catalog {
t.Helper()
file, err := os.Open("testdata/" + name)
if err != nil {
t.Fatal(err)
}
defer file.Close()
catalog, err := read(file)
if err != nil {
t.Fatal(err)
}
return catalog
}
func TestCrossFormatPositionsAtJ2000(t *testing.T) {
stellarium := readFixtureCatalog(t, "stars_0_0v0_18.cat", func(f *os.File) (*Catalog, error) {
return ReadStellariumStars(f, ReadOptions{Strict: true})
})
hip := readFixtureCatalog(t, "hip2_sample.dat", func(f *os.File) (*Catalog, error) {
return ReadHipparcos2007(f, ReadOptions{Strict: true})
})
bsc := readFixtureCatalog(t, "bsc5_sample.dat", func(f *os.File) (*Catalog, error) {
return ReadBSC5(f, ReadOptions{})
})
if bsc.Skipped() != 1 {
t.Fatalf("BSC5 skipped = %d, want the record without coordinates", bsc.Skipped())
}
stars := map[string]string{"HIP 32349": "HR 2491", "HIP 91262": "HR 7001", "HIP 24436": "HR 1713", "HIP 21421": "HR 1457", "HIP 11767": "HR 424"}
for hipID, hrID := range stars {
h, ok := hip.Lookup(hipID)
if !ok {
t.Fatalf("hip2 missing %s", hipID)
}
b, ok := bsc.Lookup(hrID)
if !ok {
t.Fatalf("BSC5 missing %s", hrID)
}
hra, hdec := propagateToJ2000(h)
bra, bdec := propagateToJ2000(b)
// BSC5 只给到 0.1 s 与 1″,实测最大约 0.75″。
if d := separationMas(hra, hdec, bra, bdec); d > 1000 {
t.Fatalf("%s hip2 vs BSC5 = %.1f mas", hipID, d)
}
if s, ok := stellarium.Lookup(hipID); ok {
sra, sdec := propagateToJ2000(s)
// 两者同源于 Hipparcos 2007,实测 ≤ 0.5 mas。
if d := separationMas(hra, hdec, sra, sdec); d > 2 {
t.Fatalf("%s hip2 vs Stellarium = %.1f mas", hipID, d)
}
}
}
}
func TestCDSFieldConventions(t *testing.T) {
hip := readFixtureCatalog(t, "hip2_sample.dat", func(f *os.File) (*Catalog, error) {
return ReadHipparcos2007(f, ReadOptions{Strict: true})
})
sirius, _ := hip.Lookup("HIP 32349")
if math.Abs(sirius.Coordinate.ParallaxMas-379.21) > 1e-6 || math.Abs(sirius.Coordinate.ProperMotionRACosDecMasPerYear+546.01) > 1e-6 || sirius.Band != "Hp" {
t.Fatalf("hip2 Sirius = %#v", sirius)
}
bsc := readFixtureCatalog(t, "bsc5_sample.dat", func(f *os.File) (*Catalog, error) {
return ReadBSC5(f, ReadOptions{})
})
vega, ok := bsc.Lookup("alp lyr")
if !ok {
vega, ok = bsc.Lookup("HD 172167")
}
if !ok || math.Abs(vega.Coordinate.ParallaxMas-123) > 1e-6 || vega.Coordinate.RadialVelocityKmPerSecond != -14 || vega.Band != "V" {
t.Fatalf("BSC5 Vega = %#v", vega)
}
if math.Abs(vega.Coordinate.ProperMotionRACosDecMasPerYear-202) > 1e-6 {
t.Fatalf("BSC5 pmRA must already include cos(dec): %.3f", vega.Coordinate.ProperMotionRACosDecMasPerYear)
}
}
func TestCommonNames(t *testing.T) {
base := readFixtureCatalog(t, "stars_0_0v0_18.cat", func(f *os.File) (*Catalog, error) {
return ReadStellariumStars(f, ReadOptions{Strict: true})
})
names := "# comment\n 32349|_(\"Sirius\") 2\n91262|_(\"Vega\") 2\n32349|_(\"天狼\") 1\n24436|Rigel\n"
named, err := base.WithNames(strings.NewReader(names), ReadOptions{Strict: true})
if err != nil {
t.Fatal(err)
}
for _, name := range []string{"sirius", "天狼", "VEGA", "Rigel"} {
if _, ok := named.Lookup(name); !ok {
t.Fatalf("lookup %q failed", name)
}
}
if entry, _ := named.Lookup("Sirius"); entry.ID != "HIP 32349" || entry.Magnitude > -1 {
t.Fatalf("Sirius = %s V %.2f", entry.ID, entry.Magnitude)
}
if _, ok := base.Lookup("Sirius"); ok {
t.Fatal("WithNames must not modify the receiver")
}
if _, err := base.WithNames(strings.NewReader("32349|_(\"broken\n"), ReadOptions{Strict: true}); err == nil {
t.Fatal("unterminated name should fail in strict mode")
}
}