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)
|
|||
|
|
}
|
|||
|
|
}
|