From f85d06c573d3262fc17360b7b0a9e0396fc093e5 Mon Sep 17 00:00:00 2001 From: michealroberts Date: Mon, 25 Nov 2024 14:34:44 +0000 Subject: [PATCH] feat: add (s *SIMBADServiceClient) PerformRadialSearch to catalog module in @observerly/skysolve feat: add (s *SIMBADServiceClient) PerformRadialSearch to catalog module in @observerly/skysolve --- pkg/catalog/base.go | 18 ++--- pkg/catalog/simbad.go | 136 +++++++++++++++++++++++++++++++++++++ pkg/catalog/simbad_test.go | 51 ++++++++++++++ 3 files changed, 196 insertions(+), 9 deletions(-) create mode 100644 pkg/catalog/simbad_test.go diff --git a/pkg/catalog/base.go b/pkg/catalog/base.go index d2d9617..6364620 100644 --- a/pkg/catalog/base.go +++ b/pkg/catalog/base.go @@ -17,15 +17,15 @@ import ( /*****************************************************************************************************************/ type Source struct { - UID string `json:"uid" gaia:"source_id"` // Source ID (unique) - Designation string `json:"designation" gaia:"designation"` // Source Designation - RA float64 `json:"ra" gaia:"ra"` // Right Ascension (in degrees) - Dec float64 `json:"dec" gaia:"dec"` // Declination (in degrees) - ProperMotionRA float64 `json:"pmra" gaia:"pmra"` // Proper Motion in RA (in mas/yr) - ProperMotionDec float64 `json:"pmdec" gaia:"pmdec"` // Proper Motion in Dec (in mas/yr) - Parallax float64 `json:"parallax" gaia:"parallax"` // Parallax (in mas) - PhotometricGMeanFlux float64 `json:"flux" gaia:"phot_g_mean_mag"` // Mean Flux (in e-/s) - PhotometricGMeanMagnitude float64 `json:"magnitude" gaia:"phot_g_mean_flux"` // Mean Magnitude (in mag) + UID string `json:"uid" gaia:"source_id" simbad:"uid"` // Source ID (unique) + Designation string `json:"designation" gaia:"designation" simbad:"designation"` // Source Designation + RA float64 `json:"ra" gaia:"ra" simbad:"ra"` // Right Ascension (in degrees) + Dec float64 `json:"dec" gaia:"dec" simbad:"dec"` // Declination (in degrees) + ProperMotionRA float64 `json:"pmra" gaia:"pmra" simbad:"pmra"` // Proper Motion in RA (in mas/yr) + ProperMotionDec float64 `json:"pmdec" gaia:"pmdec" simbad:"pmdec"` // Proper Motion in Dec (in mas/yr) + Parallax float64 `json:"parallax" gaia:"parallax" simbad:"parallax"` // Parallax (in mas) + PhotometricGMeanFlux float64 `json:"flux" gaia:"phot_g_mean_flux" simbad:"flux"` // G-band Mean Flux (in e-/s) + PhotometricGMeanMagnitude float64 `json:"magnitude" gaia:"phot_g_mean_mag" simbad:"magnitude"` // G-band Mean Magnitude (in mag) } /*****************************************************************************************************************/ diff --git a/pkg/catalog/simbad.go b/pkg/catalog/simbad.go index ce308b9..a617203 100644 --- a/pkg/catalog/simbad.go +++ b/pkg/catalog/simbad.go @@ -11,10 +11,13 @@ package catalog /*****************************************************************************************************************/ import ( + "fmt" "net/url" + "strings" "time" "github.com/observerly/skysolve/pkg/adql" + "github.com/observerly/skysolve/pkg/astrometry" ) /*****************************************************************************************************************/ @@ -60,3 +63,136 @@ func NewSIMBADServiceClient() *SIMBADServiceClient { } /*****************************************************************************************************************/ + +const simbadRecord = "basic.oid AS uid, basic.main_id AS designation, basic.ra AS ra, basic.dec AS dec, basic.pmra AS pmra, basic.pmdec AS pmdec, basic.plx_value AS parallax, flux.flux AS flux, allfluxes.G AS magnitude" + +/*****************************************************************************************************************/ + +func (s *SIMBADServiceClient) PerformRadialSearch(eq astrometry.ICRSEquatorialCoordinate, radius float64, limit int, threshold float64) ([]Source, error) { + // Define the ADQL query template for the SIMBAD TAP service: + // @see https://simbad.u-strasbg.fr/Pages/guide/sim-q.htx + const simbadADQLTemplate = ` + SELECT TOP {{.Limit}} {{.Record}} + FROM basic + LEFT JOIN flux + ON basic.oid = flux.oidref + AND flux.filter = 'G' + LEFT JOIN allfluxes + ON basic.oid = allfluxes.oidref + WHERE CONTAINS( + POINT('ICRS', basic.ra, basic.dec), + CIRCLE('ICRS', {{.RA}}, {{.Dec}}, {{.Radius}}) + ) = 1 + ORDER BY magnitude ASC; + ` + + // Set the query parameters: + s.Query.RA = eq.RA + s.Query.Dec = eq.Dec + s.Query.Radius = radius + s.Query.Limit = limit + s.Query.Threshold = threshold + + // Construct the ADQL query from the template: + adqlQuery, err := s.BuildADQLQuery(simbadADQLTemplate, struct { + Record string + RA float64 + Dec float64 + Radius float64 + Limit int + Threshold float64 + }{ + Record: simbadRecord, + RA: s.Query.RA, + Dec: s.Query.Dec, + Radius: s.Query.Radius, + Limit: s.Query.Limit, + Threshold: s.Query.Threshold, + }) + if err != nil { + return nil, err + } + + // Execute the query and get the response: + tapResponse, err := s.ExecuteADQLQuery(adqlQuery) + + if err != nil { + return nil, err + } + + var stars []Source + + // Helper functions defined locally within the method to convert an unknown interface{} to float64: + toFloat64 := func(val interface{}) (float64, bool) { + v, ok := val.(float64) + return v, ok + } + + for _, record := range tapResponse.Data { + // Create a new Source struct from the record: + // Initialize a new Source struct + var star Source + + // Assign UID and Designation using fmt.Sprintf to handle various types: + star.UID = fmt.Sprintf("%v", record[0]) + star.Designation = strings.Join(strings.Fields(fmt.Sprintf("%v", record[1])), " ") + + // Safely assign RA and assig a default value if not a float64: + if ra, ok := toFloat64(record[2]); ok { + star.RA = ra + } else { + // Handle unexpected type or assign a default value: + star.RA = 0.0 + } + + // Safely assign Dec and assign a default value if not a float64: + if dec, ok := toFloat64(record[3]); ok { + star.Dec = dec + } else { + // Handle unexpected type or assign a default value: + star.Dec = 0.0 + } + + // Safely assign ProperMotionRA if not nil: + if record[4] != nil { + if pmra, ok := toFloat64(record[4]); ok { + star.ProperMotionRA = pmra + } + } + + // Safely assign ProperMotionDec if not nil: + if record[5] != nil { + if pmdec, ok := toFloat64(record[4]); ok { + star.ProperMotionDec = pmdec + } + } + + // Safely assign Parallax if not nil: + if record[6] != nil { + if parallax, ok := toFloat64(record[6]); ok { + star.Parallax = parallax + } + } + + // Safely assign PhotometricGMeanFlux if not nil: + if record[7] != nil { + if flux, ok := toFloat64(record[7]); ok { + star.PhotometricGMeanFlux = flux + } + } + + // Safely assign PhotometricGMeanMagnitude if not nil: + if record[8] != nil { + if magnitude, ok := toFloat64(record[8]); ok { + star.PhotometricGMeanMagnitude = magnitude + } + } + + // Append to the stars slice of Source structs: + stars = append(stars, star) + } + + return stars, nil +} + +/*****************************************************************************************************************/ diff --git a/pkg/catalog/simbad_test.go b/pkg/catalog/simbad_test.go new file mode 100644 index 0000000..cfd7aea --- /dev/null +++ b/pkg/catalog/simbad_test.go @@ -0,0 +1,51 @@ +/*****************************************************************************************************************/ + +// @author Michael Roberts +// @package @observerly/skysolve +// @license Copyright © 2021-2025 observerly + +/*****************************************************************************************************************/ + +package catalog + +import ( + "fmt" + "testing" + + "github.com/observerly/skysolve/pkg/astrometry" +) + +/*****************************************************************************************************************/ + +func TestSIMBADQueryExecutedSuccessfully(t *testing.T) { + var q = NewSIMBADServiceClient() + + stars, err := q.PerformRadialSearch(astrometry.ICRSEquatorialCoordinate{ + RA: 0, + Dec: 0, + }, 2.5, 100, 10) + + if err != nil { + t.Errorf("Failed to execute query: %v", err) + } + + if len(stars) == 0 { + t.Errorf("No stars returned") + } + + for _, star := range stars { + // Test that the star is within the search radius: + if !IsWithinICRSPolarRadius(star.RA, star.Dec, 2.5) { + t.Errorf("Star is not within the search radius") + } + + fmt.Println(star.Designation) + } + + // The SIMBAD catalog is expected to return a maximum of 100 stars for this query: + if len(stars) > 100 { + t.Errorf("Too many stars returned") + } +} + +/*****************************************************************************************************************/