Mix.install([
{:space_tap, "~> 0.1.0"},
{:kino, "~> 0.19.1"},
{:kino_vega_lite, "~> 0.1.13"}
])The service
The GAVO Data Center in Heidelberg publishes hundreds of tables, from historical catalogues to the IVOA Registry itself. It runs DaCHS, the software behind many TAP services, so it is a good second implementation to try SpaceTAP against.
This notebook looks at what the service holds, draws the naked-eye sky from the Hipparcos catalogue, searches the Registry for other TAP services, and ends with the errors DaCHS sends.
alias VegaLite, as: Vl
gavo = "https://dc.g-vo.org/tap"
to_maps = fn %SpaceTAP.Result{columns: columns, rows: rows} ->
Enum.map(rows, fn row -> columns |> Enum.zip(row) |> Map.new() end)
end
table = fn result, name ->
Kino.DataTable.new(to_maps.(result), keys: result.columns, name: name)
endWhat the service holds
Every TAP service describes its own tables and columns in TAP_SCHEMA, which you query with ADQL like any other table (TAP 1.1 §4). The schemas with the most tables:
{:ok, schemas} =
SpaceTAP.query(gavo, """
SELECT TOP 15 schema_name, COUNT(*) AS n_tables
FROM tap_schema.tables
GROUP BY schema_name
ORDER BY n_tables DESC
""")
table.(schemas, "Schemas")The columns of a table, with their units, come from tap_schema.columns. Here, the Hipparcos catalogue:
{:ok, columns} =
SpaceTAP.query(gavo, """
SELECT column_name, unit, description
FROM tap_schema.columns
WHERE table_name = 'hipparcos.main'
""")
table.(columns, "hipparcos.main columns")Look at the units before using a column: here parallax is in degrees, not in the milliarcseconds most catalogues use.
The naked-eye sky
Hipparcos, an ESA satellite and Gaia's predecessor, measured about 118,000 stars. Those brighter than magnitude 4 are roughly the stars you can see from a city:
{:ok, bright} =
SpaceTAP.query(gavo, """
SELECT hipno, ra, dec, vmag, sptype AS spectral_type
FROM hipparcos.main
WHERE vmag < 4
ORDER BY vmag
""")
length(bright.rows)sptype AS spectral_type names the column in the result. Without the alias, DaCHS writes the header as SpType: ADQL names that aren't in double quotes ignore case (ADQL 2.1 §2.1.7), so the service may spell them its own way.
The first letter of the spectral type is the spectral class, from hot blue O stars to cool red M stars, so colour the dots by it:
classes = ~w(O B A F G K M)
stars =
for star <- to_maps.(bright) do
{vmag, ""} = Float.parse(star["vmag"])
{ra, ""} = Float.parse(star["ra"])
{dec, ""} = Float.parse(star["dec"])
class = String.first(star["spectral_type"] || "")
%{
"hip" => star["hipno"],
"ra" => ra,
"dec" => dec,
"vmag" => vmag,
"brightness" => 4.5 - vmag,
"class" => if(class in classes, do: class, else: "other")
}
end
Vl.new(width: 700, height: 350, title: "Stars brighter than magnitude 4")
|> Vl.data_from_values(stars)
|> Vl.mark(:circle, opacity: 0.8)
|> Vl.encode_field(:x, "ra", type: :quantitative, title: "RA (°)", scale: [domain: [360, 0]])
|> Vl.encode_field(:y, "dec", type: :quantitative, title: "Dec (°)", scale: [domain: [-90, 90]])
|> Vl.encode_field(:size, "brightness",
type: :quantitative,
legend: nil,
scale: [range: [5, 250]]
)
|> Vl.encode_field(:color, "class",
type: :nominal,
title: "Class",
scale: [
domain: classes ++ ["other"],
range: ~w(#5b7cfa #8fb0ff #d6e2ff #fff4d6 #ffe08a #ffb35c #ff7a45 #999999)
]
)
|> Vl.encode(:tooltip, [
[field: "hip", type: :nominal, title: "HIP"],
[field: "vmag", type: :quantitative, title: "V"],
[field: "class", type: :nominal]
])Hover over a dot to see its HIP number and magnitude. The biggest, at RA 101° and Dec −17°, is Sirius, the brightest star in the night sky.
The nearest stars
Sort by parallax to get the nearest stars. With the parallax in degrees, multiply by 3,600,000 to get milliarcseconds; the distance in parsecs is 1000 divided by that, and a parsec is 3.26 light years:
{:ok, nearest} =
SpaceTAP.query(gavo, """
SELECT TOP 10 hipno, vmag, parallax, sptype AS spectral_type
FROM hipparcos.main
ORDER BY parallax DESC
""")
nearest_stars =
for star <- to_maps.(nearest) do
{parallax_deg, ""} = Float.parse(star["parallax"])
parallax_mas = parallax_deg * 3_600_000
distance_pc = 1000 / parallax_mas
%{
"hip" => star["hipno"],
"vmag" => star["vmag"],
"spectral_type" => star["spectral_type"],
"parallax_mas" => Float.round(parallax_mas, 1),
"distance_ly" => Float.round(distance_pc * 3.2616, 2)
}
end
Kino.DataTable.new(nearest_stars,
keys: ~w(hip vmag spectral_type parallax_mas distance_ly),
name: "Nearest Hipparcos stars"
)HIP 70890 is Proxima Centauri, and the next two are Alpha Centauri A and B. Proxima is faint (V = 11) although it is the nearest: it is a small red dwarf.
Searching the Registry for TAP services
The IVOA Registry lists the services of the Virtual Observatory. GAVO serves it through TAP too, with the tables defined by RegTAP, so you can search it with ADQL. ivo_hasword is a RegTAP function for word searches in free text. TAP services whose description mentions a word:
word = "Gaia"
{:ok, services} =
SpaceTAP.query(gavo, """
SELECT res.res_title, intf.access_url
FROM rr.resource AS res
JOIN rr.capability AS cap ON cap.ivoid = res.ivoid
JOIN rr.interface AS intf ON intf.ivoid = cap.ivoid AND intf.cap_index = cap.cap_index
WHERE cap.standard_id = 'ivo://ivoa.net/std/tap'
AND intf.intf_role = 'std'
AND 1 = ivo_hasword(res.res_description, '#{word}')
ORDER BY res.res_title
""")
table.(services, "TAP services that mention #{word}")access_url is the service URL to pass to SpaceTAP.query/3. Many are registered with http://: if the service also answers on https://, use that, because a redirect from http:// to https:// loses the query (see the README).
Interpolating word into the ADQL is fine for a word you typed yourself. ADQL has no bound parameters, so never interpolate text from someone else without escaping it.
Errors from DaCHS
DaCHS answers invalid ADQL with HTTP 500 and a VOTable error document. The document starts with an xml-stylesheet processing instruction, so a browser can show it as a web page:
SpaceTAP.query(gavo, "SELEC TOP 3 hipno FROM hipparcos.main")An unknown column:
SpaceTAP.query(gavo, "SELECT TOP 3 b_v FROM hipparcos.main")Both are :service_errors with the message DaCHS wrote: the document says its query status is ERROR (DALI 1.1 §4.4), so the HTTP status doesn't decide the type.