NASA Exoplanet Archive

Copy Markdown View Source
Mix.install([
  {:space_tap, "~> 0.1.0"},
  {:kino, "~> 0.19.1"},
  {:kino_vega_lite, "~> 0.1.13"}
])

The service

The NASA Exoplanet Archive, run by IPAC at Caltech, keeps the catalogue of confirmed planets around other stars. Its table pscomppars has one row per planet, with the best value of each parameter.

The archive sends its CSV with Content-Type: text/plain instead of the text/csv that TAP 1.1 §3.2 requires. SpaceTAP reads it as CSV anyway, because CSV is what it asked for (docs/adr/0003-csv-sent-as-plain-text-is-read-as-csv.md).

alias VegaLite, as: Vl

nasa = "https://exoplanetarchive.ipac.caltech.edu/TAP"

to_maps = fn %SpaceTAP.Result{columns: columns, rows: rows} ->
  Enum.map(rows, fn row -> columns |> Enum.zip(row) |> Map.new() end)
end

number = fn
  nil ->
    nil

  string ->
    {number, ""} = Float.parse(string)
    number
end

Discoveries by method

ADQL has GROUP BY and COUNT, so the service does the counting:

{:ok, by_method} =
  SpaceTAP.query(nasa, """
  SELECT discoverymethod, COUNT(*) AS planets
  FROM pscomppars
  GROUP BY discoverymethod
  ORDER BY planets DESC
  """)

Kino.DataTable.new(to_maps.(by_method), keys: by_method.columns, name: "Planets by method")

Most planets were found by the transit method, as they crossed in front of their star and dimmed it; Kepler and TESS found thousands that way. Radial velocity measures the wobble of the star as the planet pulls on it.

Discoveries per year

{:ok, by_year} =
  SpaceTAP.query(nasa, """
  SELECT disc_year, discoverymethod, COUNT(*) AS planets
  FROM pscomppars
  GROUP BY disc_year, discoverymethod
  """)

discoveries =
  for row <- to_maps.(by_year) do
    %{
      "year" => String.to_integer(row["disc_year"]),
      "method" => row["discoverymethod"],
      "planets" => String.to_integer(row["planets"])
    }
  end

Vl.new(width: 700, height: 350, title: "Exoplanets discovered per year")
|> Vl.data_from_values(discoveries)
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "year", type: :ordinal, title: "Year")
|> Vl.encode_field(:y, "planets", type: :quantitative, aggregate: :sum, title: "Planets")
|> Vl.encode_field(:color, "method", type: :nominal, title: "Method")

The two tallest bars, 2016 and 2014, are the years when large batches of Kepler planets were confirmed at once.

Planets by size and orbit

Each dot is a planet: its orbital period, how long its year lasts, against its radius in Earth radii. Both axes are logarithmic, because periods go from hours to centuries:

{:ok, sizes} =
  SpaceTAP.query(nasa, """
  SELECT pl_name, pl_orbper, pl_rade, discoverymethod
  FROM pscomppars
  WHERE pl_orbper IS NOT NULL AND pl_rade IS NOT NULL
  """)

planets =
  for row <- to_maps.(sizes) do
    %{
      "name" => row["pl_name"],
      "period_days" => number.(row["pl_orbper"]),
      "radius_earth" => number.(row["pl_rade"]),
      "method" => row["discoverymethod"]
    }
  end

Vl.new(width: 700, height: 450, title: "Orbital period and radius")
|> Vl.data_from_values(planets)
|> Vl.mark(:circle, size: 12, opacity: 0.5)
|> Vl.encode_field(:x, "period_days",
  type: :quantitative,
  title: "Period (days)",
  scale: [type: :log]
)
|> Vl.encode_field(:y, "radius_earth",
  type: :quantitative,
  title: "Radius (Earth = 1)",
  scale: [type: :log]
)
|> Vl.encode_field(:color, "method", type: :nominal, title: "Method")
|> Vl.encode_field(:tooltip, "name", type: :nominal)

The clump at the top left are hot Jupiters, giant planets that orbit their star in a few days. Most planets found by transit have short periods, because a planet has to cross its star while someone is watching.

The nearest planets, and a quirk of the service

sy_dist is the distance to the planet's system in parsecs. In ADQL, ORDER BY sorts the rows before TOP keeps the first ones: ADQL 2.1 §4.10.1 applies ORDER BY, then OFFSET, then TOP. This service applies TOP first, so the query below returns ten planets that are not the nearest, sorted:

{:ok, wrong} =
  SpaceTAP.query(nasa, """
  SELECT TOP 10 pl_name, sy_dist
  FROM pscomppars
  WHERE sy_dist IS NOT NULL
  ORDER BY sy_dist
  """)

wrong.rows

MAXREC is applied to the final result, so leave out TOP and pass :maxrec instead:

{:ok, nearest} =
  SpaceTAP.query(
    nasa,
    """
    SELECT pl_name, hostname, sy_dist, disc_year, discoverymethod
    FROM pscomppars
    WHERE sy_dist IS NOT NULL
    ORDER BY sy_dist
    """,
    maxrec: 10
  )

Kino.DataTable.new(to_maps.(nearest), keys: nearest.columns, name: "Nearest planets")

Proxima Centauri, the nearest star to the Sun, has planets 1.3 parsecs away.

Errors

The archive sends its error documents as VOTable, so they are :service_errors:

SpaceTAP.query(nasa, "SELECT TOP 3 pl_name FROM no_such_table")