Skip to content

Commit a3fcc75

Browse files
committed
add notebooks and data files
1 parent 0dc73fb commit a3fcc75

10 files changed

+9524
-0
lines changed

1_Coordinates-Intro.ipynb

Lines changed: 616 additions & 0 deletions
Large diffs are not rendered by default.

2MASS_results.ecsv

Lines changed: 5146 additions & 0 deletions
Large diffs are not rendered by default.

2_Coordinates-Transforms.ipynb

Lines changed: 771 additions & 0 deletions
Large diffs are not rendered by default.

3_Coordinates-Velocities.ipynb

Lines changed: 534 additions & 0 deletions
Large diffs are not rendered by default.

4_Coordinates-Crossmatch.ipynb

Lines changed: 387 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,387 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Astronomical Coordinates 4: Cross-matching Catalogs Using astropy.coordinates and astroquery\n",
8+
"\n",
9+
"## Authors\n",
10+
"Adrian Price-Whelan\n",
11+
"\n",
12+
"## Learning Goals\n",
13+
"* Demonstrate how to retrieve a catalog from Vizier using astroquery\n",
14+
"* Show how to perform positional cross-matches between catalogs of sky coordinates\n",
15+
"\n",
16+
"## Keywords\n",
17+
"coordinates, OOP, astroquery, gaia\n",
18+
"\n",
19+
"\n",
20+
"## Summary\n",
21+
"\n",
22+
"In the previous tutorials in this series, we introduced many of the key concepts underlying how to represent and transform astronomical coordinates using `astropy.coordinates`, including how to work with both position and velocity data within the coordinate objects.\n",
23+
"\n",
24+
"In this tutorial, we will explore how the `astropy.coordinates` package can be used to cross-match two catalogs that contain overlapping sources that may have been observed at different times. You may find it helpful to keep [the Astropy documentation for the coordinates package](http://docs.astropy.org/en/stable/coordinates/index.html) open alongside this tutorial for reference or additional reading. In the text below, you may also see some links that look like ([docs](http://docs.astropy.org/en/stable/coordinates/index.html)). These links will take you to parts of the documentation that are directly relevant to the cells from which they link. \n",
25+
"\n",
26+
"*Note: This is the 4th tutorial in a series of tutorials about astropy.coordinates. If you are new to astropy.coordinates, you may want to start from the beginning or an earlier tutorial.*"
27+
]
28+
},
29+
{
30+
"cell_type": "markdown",
31+
"metadata": {},
32+
"source": [
33+
"## Imports\n",
34+
"\n",
35+
"We start by importing some general packages that we will need below:"
36+
]
37+
},
38+
{
39+
"cell_type": "code",
40+
"execution_count": null,
41+
"metadata": {},
42+
"outputs": [],
43+
"source": [
44+
"import warnings\n",
45+
"\n",
46+
"import matplotlib.pyplot as plt\n",
47+
"\n",
48+
"%matplotlib inline\n",
49+
"import numpy as np\n",
50+
"\n",
51+
"from astropy import units as u\n",
52+
"from astropy.coordinates import SkyCoord, Distance\n",
53+
"from astropy.table import QTable\n",
54+
"from astropy.time import Time\n",
55+
"\n",
56+
"from astroquery.vizier import Vizier"
57+
]
58+
},
59+
{
60+
"cell_type": "markdown",
61+
"metadata": {},
62+
"source": [
63+
"## Cross-matching and comparing catalogs\n",
64+
"\n",
65+
"In this tutorial, we are going to return to a set of data that we downloaded from the *Gaia* archive back in [Tutorial 1](1-Coordinates-Intro) of this series.\n",
66+
"\n",
67+
"Let's recap what we did in that tutorial: We defined a `SkyCoord` object to represent the center of an open cluster (NGC 188), we queried the *Gaia* DR2 catalog to select stars that are close (on the sky) to the center of the cluster, and we used the parallax values from *Gaia* to select stars that are near NGC 188 in 3D position. Here, we will briefly reproduce those selections so that we can start here with a catalog of sources that are likely members of NGC 188 (see [Tutorial 1](1-Coordinates-Intro) for more information):"
68+
]
69+
},
70+
{
71+
"cell_type": "code",
72+
"execution_count": null,
73+
"metadata": {},
74+
"outputs": [],
75+
"source": [
76+
"ngc188_table = QTable.read(\"gaia_results.fits\")\n",
77+
"ngc188_table = ngc188_table[ngc188_table[\"parallax\"] > 0.25 * u.mas]\n",
78+
"\n",
79+
"ngc188_center_3d = SkyCoord(\n",
80+
" 12.11 * u.deg,\n",
81+
" 85.26 * u.deg,\n",
82+
" distance=1.96 * u.kpc,\n",
83+
" pm_ra_cosdec=-2.3087 * u.mas / u.yr,\n",
84+
" pm_dec=-0.9565 * u.mas / u.yr,\n",
85+
")\n",
86+
"\n",
87+
"# Deal with masked quantity data in a backwards-compatible way:\n",
88+
"parallax = ngc188_table[\"parallax\"]\n",
89+
"if hasattr(parallax, \"mask\"):\n",
90+
" parallax = parallax.filled(np.nan)\n",
91+
"\n",
92+
"velocity_data = {\n",
93+
" \"pm_ra_cosdec\": ngc188_table[\"pmra\"],\n",
94+
" \"pm_dec\": ngc188_table[\"pmdec\"],\n",
95+
" \"radial_velocity\": ngc188_table[\"radial_velocity\"],\n",
96+
"}\n",
97+
"for k, v in velocity_data.items():\n",
98+
" if hasattr(v, \"mask\"):\n",
99+
" velocity_data[k] = v.filled(0.0)\n",
100+
" velocity_data[k][np.isnan(velocity_data[k])] = 0.0\n",
101+
"\n",
102+
"ngc188_coords_3d = SkyCoord(\n",
103+
" ra=ngc188_table[\"ra\"],\n",
104+
" dec=ngc188_table[\"dec\"],\n",
105+
" distance=Distance(parallax=parallax),\n",
106+
" obstime=Time(\"J2015.5\"),\n",
107+
" **velocity_data,\n",
108+
")\n",
109+
"\n",
110+
"sep3d = ngc188_coords_3d.separation_3d(ngc188_center_3d)\n",
111+
"pm_diff = np.sqrt(\n",
112+
" (ngc188_table[\"pmra\"] - ngc188_center_3d.pm_ra_cosdec) ** 2\n",
113+
" + (ngc188_table[\"pmdec\"] - ngc188_center_3d.pm_dec) ** 2\n",
114+
")\n",
115+
"\n",
116+
"ngc188_members_mask = (sep3d < 50 * u.pc) & (pm_diff < 1.5 * u.mas / u.yr)\n",
117+
"ngc188_members = ngc188_table[ngc188_members_mask]\n",
118+
"ngc188_members_coords = ngc188_coords_3d[ngc188_members_mask]\n",
119+
"len(ngc188_members)"
120+
]
121+
},
122+
{
123+
"cell_type": "markdown",
124+
"metadata": {},
125+
"source": [
126+
"From the selections above, the table `ngc188_members` and the `SkyCoord` instance `ngc188_members_coords` contain 216 sources that, based on their 3D positions and proper motions, are consistent with being members of the open cluster NGC 188."
127+
]
128+
},
129+
{
130+
"cell_type": "markdown",
131+
"metadata": {},
132+
"source": [
133+
"Let's assume that we now want to cross-match our catalog of candidate members of NGC 188 — here, based on *Gaia* data — to some other catalog. In this tutorial, we will demonstrate how to manually cross-match these *Gaia* sources with the 2MASS photometric catalog to retrieve infrared magnitudes for these stars, and then we will plot a color–magnitude diagram. To do this, we first need to query the 2MASS catalog to retrieve all sources in a region around the center of NGC 188, as we did for *Gaia*. Here, we will also take into account the fact that the *Gaia* data release 2 reference epoch is J2015.5, whereas the 2MASS coordinates are likely reported at their time of observation (in the late 1990's). \n",
134+
"\n",
135+
"*Note that some data archives, like the Gaia science archive, support running cross-matches at the database level and even support epoch propagation. If you need to perform a large cross-match, it will be much more efficient to use these services!*\n",
136+
"\n",
137+
"We will again use `astroquery` to execute this query. This will again require an internet connection, but we have included the results of this query in a file along with this notebook in case you are not connected to the internet. To query 2MASS, we will use the `astroquery.vizier` module ([docs](https://astroquery.readthedocs.io/en/latest/vizier/vizier.html)) to run a cone search centered on the sky position of NGC 188 with a search radius of 0.5º:"
138+
]
139+
},
140+
{
141+
"cell_type": "code",
142+
"execution_count": null,
143+
"metadata": {},
144+
"outputs": [],
145+
"source": [
146+
"# NOTE: skip this cell if you do not have an internet connection\n",
147+
"\n",
148+
"# II/246 is the catalog name for the main 2MASS photometric catalog\n",
149+
"v = Vizier(catalog=\"II/246\", columns=[\"*\", \"Date\"])\n",
150+
"v.ROW_LIMIT = -1\n",
151+
"\n",
152+
"result = v.query_region(ngc188_center_3d, radius=0.5 * u.deg)\n",
153+
"tmass_table = result[0]"
154+
]
155+
},
156+
{
157+
"cell_type": "markdown",
158+
"metadata": {},
159+
"source": [
160+
"Alternatively, we can read the 2MASS table provided along with this tutorial:"
161+
]
162+
},
163+
{
164+
"cell_type": "code",
165+
"execution_count": null,
166+
"metadata": {},
167+
"outputs": [],
168+
"source": [
169+
"# the .read() below produces some warnings that we can safely ignore\n",
170+
"with warnings.catch_warnings():\n",
171+
" warnings.simplefilter(\"ignore\", UserWarning)\n",
172+
"\n",
173+
" tmass_table = QTable.read(\"2MASS_results.ecsv\")"
174+
]
175+
},
176+
{
177+
"cell_type": "markdown",
178+
"metadata": {},
179+
"source": [
180+
"As with the *Gaia* results table, we can now create a single `SkyCoord` object to represent all of the sources returned from our query to the 2MASS catalog. Let's look at the column names in this table by displaying the first few rows:"
181+
]
182+
},
183+
{
184+
"cell_type": "code",
185+
"execution_count": null,
186+
"metadata": {},
187+
"outputs": [],
188+
"source": [
189+
"tmass_table[:3]"
190+
]
191+
},
192+
{
193+
"cell_type": "markdown",
194+
"metadata": {},
195+
"source": [
196+
"From looking at the column names, the two relevant sky coordinate columns are `RAJ2000` for `ra` and `DEJ2000` for `dec`:"
197+
]
198+
},
199+
{
200+
"cell_type": "code",
201+
"execution_count": null,
202+
"metadata": {},
203+
"outputs": [],
204+
"source": [
205+
"tmass_coords = SkyCoord(tmass_table[\"RAJ2000\"], tmass_table[\"DEJ2000\"])\n",
206+
"len(tmass_coords)"
207+
]
208+
},
209+
{
210+
"cell_type": "markdown",
211+
"metadata": {},
212+
"source": [
213+
"Note also that the table contains a \"Date\" column that specifies the epoch of the coordinates. Are all of these epochs the same?"
214+
]
215+
},
216+
{
217+
"cell_type": "code",
218+
"execution_count": null,
219+
"metadata": {},
220+
"outputs": [],
221+
"source": [
222+
"np.unique(tmass_table[\"Date\"])"
223+
]
224+
},
225+
{
226+
"cell_type": "markdown",
227+
"metadata": {},
228+
"source": [
229+
"It looks like all of the sources in our 2MASS table have the same epoch, so let's create an `astropy.time.Time` object to represent this date:"
230+
]
231+
},
232+
{
233+
"cell_type": "code",
234+
"execution_count": null,
235+
"metadata": {},
236+
"outputs": [],
237+
"source": [
238+
"tmass_epoch = Time(np.unique(tmass_table[\"Date\"]))"
239+
]
240+
},
241+
{
242+
"cell_type": "markdown",
243+
"metadata": {},
244+
"source": [
245+
"We now want to cross-match our *Gaia*-selected candidate members of NGC 188, `ngc_members_coords`, with this table of photometry from 2MASS. However, as noted previously, the *Gaia* coordinates are given at a different epoch J2015.5, which is nearly ~16 years after the 2MASS epoch of the data we downloaded (1999-10-19 or roughly J1999.88). We will therefore first use the `SkyCoord.apply_space_motion()` method ([docs](http://docs.astropy.org/en/latest/api/astropy.coordinates.SkyCoord.html#astropy.coordinates.SkyCoord.apply_space_motion)) to transform the *Gaia* positions back to the 2MASS epoch before we do the cross-match:"
246+
]
247+
},
248+
{
249+
"cell_type": "code",
250+
"execution_count": null,
251+
"metadata": {},
252+
"outputs": [],
253+
"source": [
254+
"# you can ignore the warning raised here\n",
255+
"ngc188_members_coords_1999 = ngc188_members_coords.apply_space_motion(tmass_epoch)"
256+
]
257+
},
258+
{
259+
"cell_type": "markdown",
260+
"metadata": {},
261+
"source": [
262+
"The object `ngc188_members_coords_1999` now contains the coordinate information for our *Gaia*-selected members of NGC 188, as we think they would appear if observed on 1999-10-19.\n",
263+
"\n",
264+
"We can now use the ``SkyCoord.match_to_catalog_sky`` method to match these two catalogs ([docs](http://docs.astropy.org/en/latest/coordinates/matchsep.html#astropy-coordinates-matching)), using the `ngc188_members_coords_1999` as our NGC 188 members coordinates. \n",
265+
"\n",
266+
"Note that order matters with this method: Here we will match *Gaia* to 2MASS. `SkyCoord.match_to_catalog_sky` returns three objects: (1) the indices into `tmass_coords` that get the closest matches in `ngc188_members_coords_1999`, (2) the angular separation between each `ngc188_members_coords_1999` coordinate and the closest source in `tmass_coords`, and (3) the 3D distance between each `ngc188_members_coords_1999` coordinate and the closest source in `tmass_coords`. Here, the 3D distances will not be useful because the 2MASS coordinates do not have associated distance information, so we will ignore these quantities:"
267+
]
268+
},
269+
{
270+
"cell_type": "code",
271+
"execution_count": null,
272+
"metadata": {},
273+
"outputs": [],
274+
"source": [
275+
"idx_gaia, sep2d_gaia, _ = ngc188_members_coords_1999.match_to_catalog_sky(tmass_coords)"
276+
]
277+
},
278+
{
279+
"cell_type": "markdown",
280+
"metadata": {},
281+
"source": [
282+
"Let's now look at the distribution of separations (in arcseconds) for all of the cross-matched sources:"
283+
]
284+
},
285+
{
286+
"cell_type": "code",
287+
"execution_count": null,
288+
"metadata": {},
289+
"outputs": [],
290+
"source": [
291+
"plt.hist(sep2d_gaia.arcsec, histtype=\"step\", bins=np.logspace(-2, 2.0, 64))\n",
292+
"plt.xlabel(\"separation [arcsec]\")\n",
293+
"plt.xscale(\"log\")\n",
294+
"plt.yscale(\"log\")\n",
295+
"plt.tight_layout()"
296+
]
297+
},
298+
{
299+
"cell_type": "markdown",
300+
"metadata": {},
301+
"source": [
302+
"From this, it looks like all of sources in our *Gaia* NGC 188 member list cross-match to another sources within a few arcseconds, so these all seem like they are correctly matches to a 2MASS source!"
303+
]
304+
},
305+
{
306+
"cell_type": "code",
307+
"execution_count": null,
308+
"metadata": {},
309+
"outputs": [],
310+
"source": [
311+
"(sep2d_gaia < 2 * u.arcsec).sum(), len(ngc188_members)"
312+
]
313+
},
314+
{
315+
"cell_type": "markdown",
316+
"metadata": {},
317+
"source": [
318+
"With our cross-match done, we can now make `Gaia`+2MASS color–magnitude diagrams of our candidate NGC 188 members using the information returned by the cross-match:"
319+
]
320+
},
321+
{
322+
"cell_type": "code",
323+
"execution_count": null,
324+
"metadata": {},
325+
"outputs": [],
326+
"source": [
327+
"Jmag = tmass_table[\"Jmag\"][idx_gaia] # note that we use the index array returned above\n",
328+
"Gmag = ngc188_members[\"phot_g_mean_mag\"]\n",
329+
"Bmag = ngc188_members[\"phot_bp_mean_mag\"]"
330+
]
331+
},
332+
{
333+
"cell_type": "code",
334+
"execution_count": null,
335+
"metadata": {},
336+
"outputs": [],
337+
"source": [
338+
"fig, axes = plt.subplots(1, 2, figsize=(10, 5))\n",
339+
"\n",
340+
"ax = axes[0]\n",
341+
"ax.scatter(Gmag - Jmag, Gmag, marker=\"o\", color=\"k\", linewidth=0, alpha=0.5)\n",
342+
"ax.set_xlabel(\"$G - J$\")\n",
343+
"ax.set_ylabel(\"$G$\")\n",
344+
"ax.set_xlim(0, 3)\n",
345+
"ax.set_ylim(19, 10) # backwards because magnitudes!\n",
346+
"\n",
347+
"ax = axes[1]\n",
348+
"ax.scatter(Bmag - Gmag, Jmag, marker=\"o\", color=\"k\", linewidth=0, alpha=0.5)\n",
349+
"ax.set_xlabel(\"$G_{BP} - G$\")\n",
350+
"ax.set_ylabel(\"$J$\")\n",
351+
"ax.set_xlim(0.2, 1)\n",
352+
"ax.set_ylim(17, 8) # backwards because magnitudes!\n",
353+
"\n",
354+
"fig.tight_layout()"
355+
]
356+
},
357+
{
358+
"cell_type": "markdown",
359+
"metadata": {},
360+
"source": [
361+
"Those both look like color-magnitude diagrams of a main sequence + red giant branch of an intermediate-age stellar cluster, so it looks like our selection and cross-matching has worked!\n",
362+
"\n",
363+
"For more on what matching options are available, check out the [separation and matching section of the Astropy documentation](https://astropy.readthedocs.io/en/stable/coordinates/matchsep.html). Or for more on what you can do with `SkyCoord`, see [its API documentation](http://astropy.readthedocs.org/en/stable/api/astropy.coordinates.SkyCoord.html)."
364+
]
365+
},
366+
{
367+
"cell_type": "code",
368+
"execution_count": null,
369+
"metadata": {},
370+
"outputs": [],
371+
"source": []
372+
}
373+
],
374+
"metadata": {
375+
"language_info": {
376+
"codemirror_mode": {
377+
"name": "ipython"
378+
},
379+
"file_extension": ".py",
380+
"mimetype": "text/x-python",
381+
"name": "python",
382+
"nbconvert_exporter": "python"
383+
}
384+
},
385+
"nbformat": 4,
386+
"nbformat_minor": 4
387+
}

0 commit comments

Comments
 (0)