Astrometric Corrections#

Because we are combining the data from two different instruments, there are corrections that must be made to the coordinates of the X-ray sources such that it is propely in line with the coordinate system of HST. There will be some intrinsic amount of uncertainty in the XRBs’ coordinates due to the differences in the X-ray and optical resolution of their respective instruments. These are accounted for by defining 1- and 2-\(\sigma\) regions around each XRB, which trace out the regions within which we are 68% and 95% sure that the source falls. These regions are calculated using the information obtained through the astrometric correction process — namely, the standard deviation of the median offset between optical and X-ray coordinates. This Chapter outlines how to conduct an astrometric correction and calculate the 1- and 2-\(\sigma\) positional uncertainty regions around each XRB.

Calibrator Selection#

To calibrate between X-ray and optical source coordinates, we need to select a sample of optical sources that we are reasonably certain are producing the X-ray emissions we detect. These include background galaxies (AGN and quasars), foreground stars, and isolated globular clusters (for example, Fig. 13). To find these sources, we must plot the coordinates of each X-ray source onto the HST image. We then identify the X-ray sources and their optical counterparts that are best suited for the assessment. You will want to pull the coordinates for each selected X-ray source, and (if you choose to do a by-hand correction) their associated optical source.

../_images/astrometric_calibrators.png

Fig. 13 Examples of X-ray sources that can be used as calibrators for the astrometric correction. The first 3 images are of AGN and quasars, based on their shapes and/or extremely red colors. The 4th is a foreground star, identifiable by the diffraction spikes. The last is a (somewhat) isolated globular cluster, based on the shape, color, and size relative to typical stars in M101.#

Correction Calculation and Application#

Astrometric corrections are handled by the function XRBID.Align.CorrectAstrometry(). It takes in the coordinates of your selected X-ray sources (cat_coords) and finds the nearest optical source from the coordinates of some base catalog (base_coords, which in this case are the coordinates from the DAOStarFinder catalog from Using AutoPhots.RunPhots() for Everything). Alternatively, you can calculate the astrometric correction yourself by hand-selecting the best optical source position of each calibrator and finding the median x- and y-coordinate offset between the optical and X-ray positions, as well as the standard deviation of those offsets.

If the standard deviations on the offsets are larger than the median offsets, then that suggests the CXO sources are already well-aligned with the HST image and no coordinate shift is necessary. The standard deviations on those shifts, however, are still needed to calculate the positional uncertainties, as it represents the variations in where the optical counterparts fall with respect to the X-ray coordinates of the selected sources. Ideally these will be small, but they’re expected to be non-zero.

Below is the code I use to find the X-ray calibrators and call CorrectAstrometry to calculate the correction:

import pandas as pd 
from XRBID.WriteScript import WriteReg
from XRBID.Sources import LoadSources, GetCoords
from XRBID.DataFrameMod import RemoveElse, FindUnique
from XRBID.Align import CorrectAstrometry

# Reading in sources from the DataFrame containing all of the X-ray sources
# You can use pd.read_csv() instead of LoadSources, if you prefer
M101_CSC = LoadSources("../testdata/cscresults_M101_renamed.frame")

# Find X-ray sources with a unique CSC ID,
# in case multiple rows in the the DataFrame have the same CSC ID
M101_unique = FindUnique(M101_CSC, header="CSC ID")

# Saving M101 to a region file that can be opened in DS9
WriteReg(M101_unique, outfile="../testdata/M101_cscsources.reg", idname="CSC ID", 
         radius=10, radunit="arcsec", width=2, color="hotpink", showlabel=True)
Reading in sources from ../testdata/cscresults_M101_renamed.frame...
Saving ../testdata/M101_cscsources.reg

The code above reads in the DataFrame (CSV) file containing the X-ray sources (created in Converting from VOTable to Pandas DataFrame) and saves a region file of these sources called M101_cscsources.reg. By default, because this DataFrame only contains RA and Dec for coordinates, WriteReg() will save a region file using galaxy coordinate units (fk5 in DS9). I define the color as hotpink (the default is a variation on yellow) and label each source with the CSC ID, as defined by idname and showlabel. You can pass any header in your DataFrame under idname, and it will use those entries to label each of the sources/regions.

Open whatever image you want to align these sources to in DS9 (I use the F555W mosaic file created in Creating Mosaics with AstroDrizzle) with Region > Open. Then, inspect the image to select a handful of good astrometric calibrator sources (see Fig. 13). Save the CSC ID names of these sources in a list, and pull the information for these sources from the DataFrame; the RemoveElse() function from XRBID.DataFrameMod is useful for removing all entries except for those that match the IDs in your list.

# List of source names to use as calibrators in the Astrometric Correction
# These I identified manually by inspecting the HST image in DS9 with the 
# regions saved above plotted over the image
M101_calibrators = ['2CXO J140251.0+542420', 
                    '2CXO J140248.0+542403', 
                    '2CXO J140356.0+542057',
                    '2CXO J140355.8+542058',
                    '2CXO J140357.6+541856',
                    '2CXO J140339.3+541827',
                    '2CXO J140346.1+541615',
                    '2CXO J140253.3+541855',
                    '2CXO J140252.1+541946']

print(len(M101_calibrators), "calibrators to match...")

# Using DataFrameMod.RemoveElse() to remove all but the sources above from the DataFrame
M101_calibrators = RemoveElse(M101_unique, keep=M101_calibrators, header="CSC ID")
print(len(M101_calibrators), "calibrators found.")

# Saving these as a region file, in case we want to double-check
WriteReg(M101_calibrators, outfile="../testdata/M101_calibrators.reg", radius=25, width=2)
9 calibrators to match...
9 calibrators found.
Saving ../testdata/M101_calibrators.reg

For CorrectAstrometry, the catalog coordinates you’ll want to use are the coordiates of the calibrators, while the base coordinates you’re aligning them to come from the source extraction conducted in Using AutoPhots.RunPhots() for Everything. Make sure you’re using the same coordinate system for both lists (in my case, I’m using the galaxy coordiates, which are designated fk5 in DS9. Thus, I pull the base coordinates from M101_daofind_f555w_acs_fk5.reg).

# Setting up the base and the catalog coordinates for CorrectAstrometry
base_coords = GetCoords("testdata/M101_daofind_f555w_acs_fk5.reg")
cat_coords = [M101_calibrators["RA"].values.tolist(), 
              M101_calibrators["Dec"].values.tolist()]

# Running CorrectAstrometry
CorrectAstrometry(base_coords, cat_coords, returnshifts=True, \
                  savebasereg="testdata/M101_astrocorrect.reg")
../_images/astrocorrect-1.png
../_images/astrocorrect-2.png
../_images/astrocorrect-3.png

Here we see that the median x and y shifts are smaller than their standard deviation. This means the alignment is already pretty good and that we just need to use the standard deviations to calculate the positional uncertainty radii! Otherwise, you will want to take the median shifts and apply this astrometric correction to the coordinates of your X-ray catalog. CorrectAstrometry will not do this for you!

Calculating Positional Uncertainty#

Because of different instrumental resolutions and inherent uncertainties, it isn’t possible to know the exact location of an X-ray source on the HST optical image, but the positional uncertainty defines a region within which we are reasonably certain the X-ray source falls. This is defined by 1- and 2-\(\sigma\) radii, representing the region within which the source has a 68% and 95% chance of being found.

There are two components that go in to the positional uncertainty estimation: the X-ray positional uncertainty, and the standard deviation on the astrometric correction above. The X-ray positional uncertainty is due to the fact that the PSF of CXO increasingly degrades for sources that are an increasing distance from the telescope’s main pointing at the time of the observation. That is, the farther the X-ray source is from the center of the image, the harder it is to tell where those detected X-rays came from.

The X-ray positional uncertainty is obtained using Equations 12 and 14 from [KKW+07]:

../_images/kim07_pu.png

The source counts and the off-axis angle are src_cnts_aper_b and theta or theta_mean from Retrieving X-ray Data from the CSC, which I renamed Counts and Theta in my DataFrame.

The positional uncertainties from the standard deviations are added in quadrature to the X-ray uncertainties, meaning you sum the squares of the uncertainties and take the square root. So for 1- and 2-\(\sigma\), you’d combine the positional uncertainties from the equations above (let’s call them sig1_kim and sig2_kim) with those from the standard deviations on the astrometric correction (xstd and ystd) thus:

from numpy import sqrt

sig_astro = sqrt(xstd**2 + ystd**2)
sig1 = sqrt(sig1_kim**2 + sig_astro**2)
sig2 = sqrt(sig2_kim**2 + (2*sig_astro)**2) # 2sig is literally twice 1sig for the std-derived p.u.

All of these steps are handled by Align.CalcPU():

from XRBID.Align import  CalcPU

# Takes in DataFrame or Off-axis Angle/Counts and returns 1 and 2sig
# NOTE: theta needs to be in arcminutes, which is the default CSC unit
sig1, sig2 = CalcPU(M101_CSC, std=[0.3813,0.3037])

CalcPU calculates the total 1- and 2-\(\sigma\) positional uncertainty from both the X-ray data and the input standard deviations on the astrometric correction. This can be saved directly into to your DataFrame:

M101_CSC['1Sig'] = sig1  # Saves sig1 to a new header called '1Sig'
M101_CSC['2Sig'] = sig2  # Saves sig2 to a new header called '2Sig'

# The new DataFrame has 1 and 2sig added at the end of the columns list
display(M101_CSC)

M101_CSC.to_csv('../testdata/M101_csc_astrocorrected.frame')
Separation CSC ID RA Dec ExpTime Theta Err Ellipse Major Err Ellipse Minor Err Ellipse Angle Significance ... HM Ratio lolim HM Ratio hilim MS Ratio MS Ratio lolim MS Ratio hilim Counts Counts lolim Counts hilim 1Sig 2Sig
0 0.835778 2CXO J140312.5+542056 210.802227 54.348952 192658.710333 2.687974 0.296164 0.295254 89.606978 22.302136 ... -0.100562 0.156777 -0.553404 -0.61649 -0.485322 170.006026 155.698239 184.313814 0.499641 1.003207
1 0.835778 2CXO J140312.5+542056 210.802227 54.348952 142729.493705 2.727056 0.296164 0.295254 89.606978 22.302136 ... -0.100562 0.156777 -0.553404 -0.61649 -0.485322 254.489381 233.532285 274.136658 0.498084 0.999338
2 0.835778 2CXO J140312.5+542056 210.802227 54.348952 141298.623812 2.783596 0.296164 0.295254 89.606978 22.302136 ... -0.100562 0.156777 -0.553404 -0.61649 -0.485322 442.576953 419.112035 464.477543 0.496277 0.994895
3 0.835778 2CXO J140312.5+542056 210.802227 54.348952 132129.439441 2.690653 0.296164 0.295254 89.606978 22.302136 ... -0.100562 0.156777 -0.553404 -0.61649 -0.485322 131.798703 118.539780 144.321019 0.502819 1.014894
4 0.835778 2CXO J140312.5+542056 210.802227 54.348952 130186.649966 2.749161 0.296164 0.295254 89.606978 22.302136 ... -0.100562 0.156777 -0.553404 -0.61649 -0.485322 649.590529 616.129127 683.051932 0.494942 0.991738
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
3487 892.520732 2CXO J140447.2+542633 211.196627 54.442543 120993.038017 NaN 1.612100 0.795238 86.007749 2.650000 ... NaN NaN NaN NaN NaN 0.000000 0.000000 59.364933 0.487466 0.974933
3488 892.520732 2CXO J140447.2+542633 211.196627 54.442543 88598.047743 NaN 1.612100 0.795238 86.007749 2.650000 ... NaN NaN NaN NaN NaN 0.000000 0.000000 14.952548 0.487466 0.974933
3489 892.520732 2CXO J140447.2+542633 211.196627 54.442543 81021.851289 13.486809 1.612100 0.795238 86.007749 2.650000 ... NaN NaN NaN NaN NaN 53.481053 29.414579 77.547526 3.190131 7.658538
3490 892.520732 2CXO J140447.2+542633 211.196627 54.442543 52087.100775 NaN 1.612100 0.795238 86.007749 2.650000 ... NaN NaN NaN NaN NaN 0.000000 0.000000 34.780341 0.487466 0.974933
3491 892.520732 2CXO J140447.2+542633 211.196627 54.442543 14276.493764 3.416463 1.612100 0.795238 86.007749 2.650000 ... NaN NaN NaN NaN NaN 7.591890 4.313574 10.870206 0.738103 1.710737

3492 rows × 30 columns

Selecting the best 2-\(\sigma\) radius per source#

Depending on what information you chose to pull from CSCview, you may see that some sources have multiple observations listed in your DataFrame. These different observations may have different 2-\(\sigma\) radii associated with them. To get the best localization of each X-ray source, we should select the observation with the smallest 2-\(\sigma\). I use the code below to select what I think is likely the best observation (though you may make arguments for using a different method, depending on the project):

from XRBID.DataFrameMod import BuildFrame, Find

# Pulling the ID of each unique CSC source
ids = FindUnique(M101_CSC, header="CSC ID")["CSC ID"].values.tolist()

# Building an empty DataFrame, which I will fill below
M101_best = BuildFrame()

for i in ids: # for each unique ID pulled from CSC...
    
    # Search for all instances (i.e. observations) of each source
    Temp = Find(M101_CSC, "CSC ID = " + i)
    
    # Try to avoid sources where counts = NaN (invalid observations) 
    if len(Find(Temp, ["Counts != NaN", "Theta != NaN"])) > 0: 
        Temp = Find(Temp, ["Counts != NaN", "Theta != NaN"])
    
    # Specifically focus on those with a valid number of counts
    if len(Find(Temp, "Counts > 0")) > 0: 
        Temp = Find(Temp, ["Counts > 0"])
        
    # Otherwise, all instances with counts = 0 have the same measurements, 
    # so it doesn't matter which row is chosen for the best radii
    else: pass;
    
    # Take the source with the smallest 2sig. 
    # If there's more than one, take the first on the list.
    Tempbest = Find(Temp, "2Sig =< " + str(min(Temp["2Sig"]))).iloc[:1]
    
    # Add the chosen source observation to the new DataFrame
    M101_best = pd.concat([M101_best, Tempbest], ignore_index=True)

# Saving results to a DataFrame file
# This file contains only rows from M101_CSC that has the 
# best 2sigma radius, based on the search performed above
M101_best.to_csv("../testdata/M101_csc_bestrads.frame")

display(M101_best)
Separation CSC ID RA Dec ExpTime Theta Err Ellipse Major Err Ellipse Minor Err Ellipse Angle Significance ... HM Ratio lolim HM Ratio hilim MS Ratio MS Ratio lolim MS Ratio hilim Counts Counts lolim Counts hilim 1Sig 2Sig
0 0.835778 2CXO J140312.5+542056 210.802227 54.348952 98379.672624 1.280763 0.296164 0.295254 89.606978 22.302136 ... -0.100562 0.156777 -0.553404 -0.616490 -0.485322 234.492581 216.678502 252.306660 0.493011 0.988256
1 1.951564 2CXO J140312.7+542055 210.803345 54.348663 49085.676020 4.725025 0.548072 0.380093 86.863933 2.648649 ... -0.080575 1.000000 -0.999375 -1.000000 -0.715178 209.970149 192.843759 227.096539 0.516471 1.038077
2 2.480949 2CXO J140312.5+542053 210.802221 54.348072 98379.672624 1.321652 0.296164 0.295733 84.016491 19.554042 ... 0.026858 0.158026 -0.316052 -0.364147 -0.265459 252.846172 234.295673 271.396671 0.492957 0.988086
3 5.227586 2CXO J140313.1+542052 210.804553 54.347994 132129.439441 2.762119 0.880650 0.578584 29.467809 3.421053 ... 0.130543 0.906309 -0.868207 -0.973766 -0.718926 23.229213 16.216620 30.241806 0.561749 1.188360
4 9.124404 2CXO J140313.5+542053 210.806667 54.348188 98379.672624 1.410741 0.633314 0.466131 58.539300 1.739130 ... 0.194254 1.000000 -0.999375 -1.000000 -0.370394 7.040343 2.992146 11.088540 0.593981 1.300029
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
550 883.411465 2CXO J140138.8+541527 210.411860 54.257747 132129.439441 12.087047 8.647887 6.103176 111.468541 4.277778 ... -0.113054 0.935041 -0.792005 -0.932542 -0.617739 76.034629 52.273807 99.795450 1.921653 4.517698
551 884.529187 2CXO J140450.6+541721 211.211172 54.289330 139822.610581 9.951487 1.357801 1.057531 95.305118 8.976584 ... -0.099313 0.204247 -0.374766 -0.472829 -0.270456 155.562531 135.448493 174.718757 0.793067 1.558488
552 886.380054 2CXO J140216.3+543313 210.567813 54.553731 131222.705704 13.887674 2.480733 2.445301 115.654637 6.783292 ... -0.025609 0.271705 -0.222361 -0.322923 -0.118051 149.738677 121.454705 176.358886 1.676834 3.103807
553 890.405834 2CXO J140445.4+541452 211.189426 54.247897 139819.410649 10.703677 4.581313 2.651713 135.472019 4.411765 ... 0.005621 0.434104 0.999375 0.501562 1.000000 NaN NaN NaN 0.487466 0.974933
554 892.520732 2CXO J140447.2+542633 211.196627 54.442543 14276.493764 3.416463 1.612100 0.795238 86.007749 2.650000 ... NaN NaN NaN NaN NaN 7.591890 4.313574 10.870206 0.738103 1.710737

555 rows × 30 columns

Saving the positional uncertainties as region files#

You now have the RA and Dec of each source (ideally updated based on the results from your astrometric correction) and the best positional uncertainty radii of each one. Save these radii as region files that can be plotted over the HST image in DS9 using WriteScript.WriteReg():

# Saving the 1 and 2 sigma region files for DS9 use.
# The 2sig region files have the CSC ID printed above each source.
WriteReg(M101_best, radius=M101_best['2Sig'].values.tolist(), radunit='arcsec', \
         idname="CSC ID", showlabel=True, outfile='../testdata/M101_bestrads_2sig.reg')
WriteReg(M101_best, radius=M101_best['1Sig'].values.tolist(), radunit='arcsec', \
         outfile='../testdata/M101_bestrads_1sig.reg')
Saving ../testdata/M101_bestrads_2sig.reg
Saving ../testdata/M101_bestrads_1sig.reg

Now when you plot these region files on your HST image in DS9, you will see exactly where each XRB is expected to be found and which optical sources are associated with them. From here, we can get into the optical analysis, including: selecting the correct optical counterpart, extracting their photometric properties, and comparing those properties to theoretical models to estimate the donor star mass!

../_images/positional_uncertainties.png

Fig. 14 Examples of the 1- and 2-\(\sigma\) positional uncertainties of CXO sources on an HST image, after applying the astrometric correction. By eye, the central source is clearly associated with a background galaxy, while the others are likely genuine XRBs.#