044f2f4381
- 新增半解析、旧模型、自定义与多源堆叠星历实例,提供 Context、Instance、SPK 读取及状态与视位置计算 - 统一太阳、月球和行星实例接口,扩展黄经穿越、留、相位、最近角距、偕日可见、任意高度升落、轨道节点及凌日等天象查询 - 完善日月食、月掩与凌日的月缘剖面、有限盘面接触、站心修正、局地可见性、全球路径和地图输出 - 新增 SVG、GeoJSON、KML 天象输出及凌日、月掩和月缘地形相关缓存与几何求解 - 新增恒星星表导入与查询,支持 CSV、Hipparcos、BSC5、Stellarium 等格式及名称、交叉标识、锥面检索和星等过滤 - 补充 IAU 2006、地球定向、折射消光、大气观测、轨道传播及半解析行星/月球数据支持 - 将默认未来民用时标切换为 TimeScaleLeapHour,更新 2027–2400 农历速查表、中文日期解析和时区边界处理 - 增加事件基线、跨模型回归、几何边界、月缘地形和输出契约测试,完善中英文手册、示例资源、NOTICE 与忽略规则
257 lines
9.3 KiB
Go
257 lines
9.3 KiB
Go
package catalog
|
||
|
||
import (
|
||
"math"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
"testing/fstest"
|
||
|
||
"b612.me/astro/basic"
|
||
)
|
||
|
||
func TestReadCSVAndFilters(t *testing.T) {
|
||
input := "source_id,ra,dec,pmra,pmdec,parallax,radial_velocity,phot_g_mean_mag,bp_rp\n" +
|
||
"1,360,0,1,2,-3,,5,1\n" +
|
||
"2,1,91,1,2,3,0,6,1\n"
|
||
catalog, err := ReadCSV(strings.NewReader(input), GaiaDR3Columns(), ReadOptions{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if catalog.Len() != 1 || catalog.Skipped() != 1 {
|
||
t.Fatalf("len/skipped = %d/%d", catalog.Len(), catalog.Skipped())
|
||
}
|
||
entry, ok := catalog.Lookup("gaia dr3 1")
|
||
if !ok || entry.Coordinate.RA != 0 || entry.Coordinate.ParallaxMas != 0 || entry.Coordinate.RadialVelocityKmPerSecond != 0 {
|
||
t.Fatalf("unexpected entry: %#v", entry)
|
||
}
|
||
if !math.IsNaN(entry.BV) {
|
||
t.Fatalf("missing B-V = %v", entry.BV)
|
||
}
|
||
if value, ok := VMagnitude(entry); !ok || math.Abs(value-5.21414) > 1e-4 {
|
||
t.Fatalf("V magnitude = %v, %v", value, ok)
|
||
}
|
||
}
|
||
|
||
func TestReadCSVStreamsQuotedNewlinesAndComments(t *testing.T) {
|
||
input := "\ufeff# exported\n\nid,ra,dec,name\n" +
|
||
"a,10,20,\"two\nlines\"\n" +
|
||
"# note\n// note\n" +
|
||
"b,350,-20,plain\n"
|
||
columns := ColumnMap{ID: "id", RA: "ra", Dec: "dec", Epoch: julianYearTime(2000), Frame: basic.CoordinateFrameICRS}
|
||
catalog, err := ReadCSV(strings.NewReader(input), columns, ReadOptions{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if catalog.Len() != 2 || catalog.Skipped() != 0 {
|
||
t.Fatalf("len/skipped = %d/%d", catalog.Len(), catalog.Skipped())
|
||
}
|
||
_, err = ReadCSV(strings.NewReader("id,ra,dec\na,1,2\nb,x\"y,3\n"), columns, ReadOptions{Strict: true})
|
||
if err == nil || !strings.Contains(err.Error(), "line 3") {
|
||
t.Fatalf("strict parse error = %v", err)
|
||
}
|
||
}
|
||
|
||
func TestReadCSVReportsRecordStartLine(t *testing.T) {
|
||
columns := ColumnMap{ID: "id", RA: "ra", Dec: "dec", Epoch: julianYearTime(2000), Frame: basic.CoordinateFrameICRS}
|
||
cases := []struct {
|
||
input string
|
||
want string
|
||
}{
|
||
{"id,ra,dec,name\na,1,2,\"x\ny\nz\"\n,3,4,q\n", "line 5:"},
|
||
{"# c\n\nid,ra,dec,name\r\na,1,2,\"x\r\ny\"\r\n# note\r\n\r\nb,bad,4,q\r\n", "line 8:"},
|
||
{"id,ra,dec,name\n\"m\nn\",1,2,p\n\"k\nj\",bad,3,q\n", "line 4:"},
|
||
{"id,ra,dec,name\na,1,2," + strings.Repeat("w", 200000) + "\nb,1,2,\"s\nt\"\n,1,2,q\n", "line 5:"},
|
||
}
|
||
for _, c := range cases {
|
||
_, err := ReadCSV(strings.NewReader(c.input), columns, ReadOptions{Strict: true})
|
||
if err == nil || !strings.HasPrefix(err.Error(), c.want) {
|
||
t.Errorf("err = %v, want prefix %q", err, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCSVStrictAndCosineConvention(t *testing.T) {
|
||
columns := ColumnMap{ID: "id", RA: "ra", Dec: "dec", PMRA: "pmra", PMDec: "pmdec", Epoch: GaiaDR3Columns().Epoch, Frame: basic.CoordinateFrameICRS}
|
||
withoutCos, err := ReadCSV(strings.NewReader("id,ra,dec,pmra,pmdec\npolar,0,80,10,2\n"), columns, ReadOptions{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
columns.PMRAIncludesCosDec = true
|
||
withCos, err := ReadCSV(strings.NewReader("id,ra,dec,pmra,pmdec\npolar,0,80,1.7364818,2\n"), columns, ReadOptions{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if math.Abs(withoutCos.Entries()[0].Coordinate.ProperMotionRACosDecMasPerYear-withCos.Entries()[0].Coordinate.ProperMotionRACosDecMasPerYear) > 1e-5 {
|
||
t.Fatal("pmRA cosine convention differs")
|
||
}
|
||
_, err = ReadCSV(strings.NewReader("id,ra,dec\nx,0,91\n"), columns, ReadOptions{Strict: true})
|
||
if err == nil || !strings.Contains(err.Error(), "record 2") {
|
||
t.Fatalf("strict error = %v", err)
|
||
}
|
||
}
|
||
|
||
func TestCatalogSearchAndMerge(t *testing.T) {
|
||
base := newCatalog()
|
||
base.add(Entry{ID: "A", Aliases: []string{"Alpha"}, Coordinate: basic.StarCoordinate{ID: "A", RA: 359.9, Dec: 0, Epoch: GaiaDR3Columns().Epoch, Frame: basic.CoordinateFrameICRS}})
|
||
base.finalize()
|
||
other := newCatalog()
|
||
other.add(Entry{ID: "A", Aliases: []string{"Beta"}, Coordinate: base.entries[0].Coordinate})
|
||
other.finalize()
|
||
merged := base.Merge(other)
|
||
if _, ok := merged.Lookup("beta"); !ok {
|
||
t.Fatal("merged alias missing")
|
||
}
|
||
if got := merged.Near(0, 0, 1); len(got) != 1 {
|
||
t.Fatalf("near across zero = %d", len(got))
|
||
}
|
||
if got := merged.Band(-1, 1); len(got) != 1 {
|
||
t.Fatalf("band = %d", len(got))
|
||
}
|
||
}
|
||
|
||
func TestMergeKeepsComponents(t *testing.T) {
|
||
coordinate := basic.StarCoordinate{ID: "HIP 1", RA: 10, Dec: 0, Epoch: GaiaDR3Columns().Epoch, Frame: basic.CoordinateFrameICRS}
|
||
first := newCatalog()
|
||
first.add(Entry{ID: "HIP 1", Component: "A", Magnitude: 1, Coordinate: coordinate})
|
||
first.finalize()
|
||
second := newCatalog()
|
||
second.add(Entry{ID: "HIP 1", Component: "B", Magnitude: 8, Coordinate: coordinate})
|
||
second.finalize()
|
||
merged := first.Merge(second)
|
||
if merged.Len() != 2 {
|
||
t.Fatalf("merged len = %d", merged.Len())
|
||
}
|
||
if entry, ok := merged.Lookup("HIP 1"); !ok || entry.Component != "A" {
|
||
t.Fatalf("lookup component = %q", entry.Component)
|
||
}
|
||
}
|
||
|
||
func TestReadStellariumFixture(t *testing.T) {
|
||
file, err := os.Open("testdata/stars_0_0v0_18.cat")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer file.Close()
|
||
catalog, err := ReadStellariumStars(file, ReadOptions{Strict: true})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
checks := map[string][4]float64{
|
||
"HIP 32349": {101.284621, -16.721552, -546.06, -1223.15},
|
||
"HIP 91262": {279.235881, 38.784961, 0, 0},
|
||
"HIP 24436": {78.634473, -8.201636, 0, 0},
|
||
}
|
||
for id, want := range checks {
|
||
entry, ok := catalog.Lookup(id)
|
||
if !ok {
|
||
t.Fatalf("missing %s", id)
|
||
}
|
||
got := entry.Coordinate
|
||
if math.Abs(got.RA-want[0]) > 1e-6 || math.Abs(got.Dec-want[1]) > 1e-6 {
|
||
t.Fatalf("%s position = %.9f %.9f", id, got.RA, got.Dec)
|
||
}
|
||
if want[2] != 0 && math.Abs(got.ProperMotionRACosDecMasPerYear-want[2]) > 0.02 {
|
||
t.Fatalf("%s pmRA = %.6f", id, got.ProperMotionRACosDecMasPerYear)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestCrossIDLengthAndStellariumDir(t *testing.T) {
|
||
fixture := func(name string) []byte {
|
||
data, err := os.ReadFile("testdata/" + name)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return data
|
||
}
|
||
catalog, err := ReadStellariumDir(fstest.MapFS{
|
||
"stars_0_0v0_18.cat": &fstest.MapFile{Data: fixture("stars_0_0v0_18.cat")},
|
||
"name.fab": &fstest.MapFile{Data: fixture("name.fab")},
|
||
"extra_name.fab": &fstest.MapFile{Data: fixture("extra_name.fab")},
|
||
"cross-id.cat": &fstest.MapFile{Data: fixture("cross-id.cat")},
|
||
}, StellariumOptions{Names: true, CrossID: true}, ReadOptions{Strict: true})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
entry, ok := catalog.Lookup("HIP 32349")
|
||
if !ok || !contains(entry.Aliases, "SAO 151881") || !contains(entry.Aliases, "α CMa") {
|
||
t.Fatalf("aliases missing: %#v", entry.Aliases)
|
||
}
|
||
}
|
||
|
||
func contains(values []string, want string) bool {
|
||
for _, value := range values {
|
||
if value == want {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func mustFixture(t *testing.T) []byte {
|
||
t.Helper()
|
||
data, err := os.ReadFile("testdata/stars_0_0v0_18.cat")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return data
|
||
}
|
||
|
||
func TestReadCSVRejectsNonFiniteMotion(t *testing.T) {
|
||
header := "source_id,ra,dec,pmra,pmdec,parallax,radial_velocity,phot_g_mean_mag,bp_rp\n"
|
||
for _, row := range []string{"1,10,0,1,2,3,+Inf,5,1\n", "1,10,0,1,2,3,-Inf,5,1\n", "1,10,0,1,2,+Inf,0,5,1\n", "1,10,0,1,2,3,1e6,5,1\n"} {
|
||
if _, err := ReadCSV(strings.NewReader(header+row), GaiaDR3Columns(), ReadOptions{Strict: true}); err == nil {
|
||
t.Errorf("strict read accepted %q", row)
|
||
}
|
||
catalog, err := ReadCSV(strings.NewReader(header+row), GaiaDR3Columns(), ReadOptions{})
|
||
if err != nil || catalog.Len() != 0 || catalog.Skipped() != 1 {
|
||
t.Errorf("lenient read of %q: len %d skipped %d err %v, want the record skipped", row, catalog.Len(), catalog.Skipped(), err)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestReadCSVDropsDuplicateIDs(t *testing.T) {
|
||
input := "source_id,ra,dec,pmra,pmdec,parallax,radial_velocity,phot_g_mean_mag,bp_rp\n" +
|
||
"1,10,5,0,0,1,,5,1\n" +
|
||
"1,20,6,0,0,1,,6,1\n" +
|
||
"2,30,7,0,0,1,,7,1\n"
|
||
catalog, err := ReadCSV(strings.NewReader(input), GaiaDR3Columns(), ReadOptions{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if catalog.Len() != 2 || catalog.Skipped() != 1 {
|
||
t.Fatalf("len/skipped = %d/%d", catalog.Len(), catalog.Skipped())
|
||
}
|
||
entry, ok := catalog.Lookup("gaia dr3 1")
|
||
if !ok || entry.Coordinate.RA != 10 {
|
||
t.Fatalf("lookup kept %#v", entry)
|
||
}
|
||
if near := catalog.Near(20, 6, 0.5); len(near) != 0 {
|
||
t.Fatalf("dropped duplicate still reachable through Near: %#v", near)
|
||
}
|
||
}
|
||
|
||
func TestCSVNonFiniteParallax(t *testing.T) {
|
||
columns := ColumnMap{ID: "id", RA: "ra", Dec: "dec", Parallax: "plx",
|
||
Epoch: julianYearTime(2000), Frame: basic.CoordinateFrameICRS}
|
||
for _, parallax := range []string{"+Inf", "-Inf"} {
|
||
input := "id,ra,dec,plx\nx,10,20," + parallax + "\n"
|
||
if _, err := ReadCSV(strings.NewReader(input), columns, ReadOptions{Strict: true}); err == nil {
|
||
t.Fatalf("parallax %q: strict mode should reject a non-finite parallax", parallax)
|
||
}
|
||
loose, err := ReadCSV(strings.NewReader(input), columns, ReadOptions{})
|
||
if err != nil || loose.Len() != 0 || loose.Skipped() != 1 {
|
||
t.Fatalf("parallax %q: loose = %d entries, %d skipped, %v", parallax, loose.Len(), loose.Skipped(), err)
|
||
}
|
||
}
|
||
// 有限的负视差按无视差处理,仍然入库。
|
||
fine, err := ReadCSV(strings.NewReader("id,ra,dec,plx\nx,10,20,-1.5\n"), columns, ReadOptions{Strict: true})
|
||
if err != nil || fine.Len() != 1 {
|
||
t.Fatalf("finite negative parallax should load: len=%d err=%v", fine.Len(), err)
|
||
}
|
||
if entry, ok := fine.Lookup("x"); !ok || entry.Coordinate.ParallaxMas != 0 {
|
||
t.Fatalf("finite negative parallax should become 0: %v %v", entry.Coordinate.ParallaxMas, ok)
|
||
}
|
||
}
|